diff --git a/components/micropython/usermodule/micropython.cmake b/components/micropython/usermodule/micropython.cmake
index a8d4be37ce958bb4313571b5c29a989a470a1806..86115b6e3a21e192fead240f151482a21289690e 100644
--- a/components/micropython/usermodule/micropython.cmake
+++ b/components/micropython/usermodule/micropython.cmake
@@ -5,7 +5,7 @@
 add_library(usermod_badge23 INTERFACE)
 
 target_sources(usermod_badge23 INTERFACE
-    ${CMAKE_CURRENT_LIST_DIR}/mp_hardware.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_sys_buttons.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_leds.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_audio.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_sys_bl00mbox.c
@@ -14,6 +14,7 @@ target_sources(usermod_badge23 INTERFACE
     ${CMAKE_CURRENT_LIST_DIR}/mp_kernel.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_uctx.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_captouch.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_sys_display.c
 )
 
 target_include_directories(usermod_badge23 INTERFACE
diff --git a/components/micropython/usermodule/mp_hardware.c b/components/micropython/usermodule/mp_hardware.c
deleted file mode 100644
index ef956f8bd2e4558e68c7aa6ebbaed5739e9e4a43..0000000000000000000000000000000000000000
--- a/components/micropython/usermodule/mp_hardware.c
+++ /dev/null
@@ -1,206 +0,0 @@
-// probably doesn't need all of these idk
-#include <stdio.h>
-#include <string.h>
-
-#include "extmod/virtpin.h"
-#include "machine_rtc.h"
-#include "modmachine.h"
-#include "mphalport.h"
-#include "py/builtin.h"
-#include "py/mphal.h"
-#include "py/runtime.h"
-
-#include "flow3r_bsp.h"
-#include "st3m_console.h"
-#include "st3m_gfx.h"
-#include "st3m_io.h"
-#include "st3m_scope.h"
-#include "st3m_usb.h"
-
-// clang-format off
-#include "ctx_config.h"
-#include "ctx.h"
-// clang-format on
-
-mp_obj_t mp_ctx_from_ctx(Ctx *ctx);
-
-STATIC mp_obj_t mp_display_set_backlight(mp_obj_t percent_in) {
-    uint8_t percent = mp_obj_get_int(percent_in);
-    flow3r_bsp_display_set_backlight(percent);
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_display_set_backlight_obj,
-                                 mp_display_set_backlight);
-
-STATIC mp_obj_t mp_left_button_get() {
-    return mp_obj_new_int(st3m_io_left_button_get());
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_left_button_get_obj, mp_left_button_get);
-
-STATIC mp_obj_t mp_right_button_get() {
-    return mp_obj_new_int(st3m_io_right_button_get());
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_right_button_get_obj, mp_right_button_get);
-
-STATIC mp_obj_t mp_version(void) {
-    mp_obj_t str =
-        mp_obj_new_str(flow3r_bsp_hw_name, strlen(flow3r_bsp_hw_name));
-    return str;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_version_obj, mp_version);
-
-static st3m_ctx_desc_t *gfx_last_desc = NULL;
-
-STATIC mp_obj_t mp_get_ctx(void) {
-    if (gfx_last_desc == NULL) {
-        gfx_last_desc = st3m_gfx_drawctx_free_get(0);
-        if (gfx_last_desc == NULL) {
-            return mp_const_none;
-        }
-    }
-    mp_obj_t mp_ctx = mp_ctx_from_ctx(gfx_last_desc->ctx);
-    return mp_ctx;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_ctx_obj, mp_get_ctx);
-
-STATIC mp_obj_t mp_freertos_sleep(mp_obj_t ms_in) {
-    uint32_t ms = mp_obj_get_int(ms_in);
-    MP_THREAD_GIL_EXIT();
-    vTaskDelay(ms / portTICK_PERIOD_MS);
-    MP_THREAD_GIL_ENTER();
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_freertos_sleep_obj, mp_freertos_sleep);
-
-STATIC mp_obj_t mp_display_update(mp_obj_t in_ctx) {
-    // TODO(q3k): check in_ctx? Or just drop from API?
-
-    if (gfx_last_desc != NULL) {
-        st3m_gfx_drawctx_pipe_put(gfx_last_desc);
-        gfx_last_desc = NULL;
-    }
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_display_update_obj, mp_display_update);
-
-STATIC mp_obj_t mp_display_pipe_full(void) {
-    if (st3m_gfx_drawctx_pipe_full()) {
-        return mp_const_true;
-    }
-    return mp_const_false;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_display_pipe_full_obj,
-                                 mp_display_pipe_full);
-
-STATIC mp_obj_t mp_display_pipe_flush(void) {
-    st3m_gfx_flush();
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_display_pipe_flush_obj,
-                                 mp_display_pipe_flush);
-
-STATIC mp_obj_t mp_scope_draw(mp_obj_t ctx_in) {
-    // TODO(q3k): check in_ctx? Or just drop from API?
-
-    if (gfx_last_desc != NULL) {
-        st3m_scope_draw(gfx_last_desc->ctx);
-    }
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_scope_draw_obj, mp_scope_draw);
-
-STATIC mp_obj_t mp_i2c_scan(void) {
-    flow3r_bsp_i2c_scan_result_t scan;
-    flow3r_bsp_i2c_scan(&scan);
-
-    mp_obj_t res = mp_obj_new_list(0, NULL);
-    for (int i = 0; i < 127; i++) {
-        size_t ix = i / 32;
-        size_t offs = i % 32;
-        if (scan.res[ix] & (1 << offs)) {
-            mp_obj_list_append(res, mp_obj_new_int_from_uint(i));
-        }
-    }
-    return res;
-}
-
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_i2c_scan_obj, mp_i2c_scan);
-
-STATIC mp_obj_t mp_usb_connected(void) {
-    static int64_t last_check = 0;
-    static bool value = false;
-    int64_t now = esp_timer_get_time();
-
-    if (last_check == 0) {
-        last_check = now;
-        value = st3m_usb_connected();
-    }
-
-    if ((now - last_check) > 10000) {
-        value = st3m_usb_connected();
-        last_check = now;
-    }
-    return mp_obj_new_bool(value);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_usb_connected_obj, mp_usb_connected);
-
-STATIC mp_obj_t mp_usb_console_active(void) {
-    static int64_t last_check = 0;
-    static bool value = false;
-    int64_t now = esp_timer_get_time();
-
-    if (last_check == 0) {
-        last_check = now;
-        value = st3m_console_active();
-    }
-
-    if ((now - last_check) > 10000) {
-        value = st3m_console_active();
-        last_check = now;
-    }
-    return mp_obj_new_bool(value);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_usb_console_active_obj,
-                                 mp_usb_console_active);
-
-STATIC const mp_rom_map_elem_t mp_module_hardware_globals_table[] = {
-    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hardware) },
-
-    { MP_ROM_QSTR(MP_QSTR_left_button_get),
-      MP_ROM_PTR(&mp_left_button_get_obj) },
-    { MP_ROM_QSTR(MP_QSTR_right_button_get),
-      MP_ROM_PTR(&mp_right_button_get_obj) },
-
-    { MP_ROM_QSTR(MP_QSTR_display_update), MP_ROM_PTR(&mp_display_update_obj) },
-    { MP_ROM_QSTR(MP_QSTR_freertos_sleep), MP_ROM_PTR(&mp_freertos_sleep_obj) },
-    { MP_ROM_QSTR(MP_QSTR_display_pipe_full),
-      MP_ROM_PTR(&mp_display_pipe_full_obj) },
-    { MP_ROM_QSTR(MP_QSTR_display_pipe_flush),
-      MP_ROM_PTR(&mp_display_pipe_flush_obj) },
-    { MP_ROM_QSTR(MP_QSTR_display_set_backlight),
-      MP_ROM_PTR(&mp_display_set_backlight_obj) },
-    { MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&mp_version_obj) },
-    { MP_ROM_QSTR(MP_QSTR_get_ctx), MP_ROM_PTR(&mp_get_ctx_obj) },
-    { MP_ROM_QSTR(MP_QSTR_usb_connected), MP_ROM_PTR(&mp_usb_connected_obj) },
-    { MP_ROM_QSTR(MP_QSTR_usb_console_active),
-      MP_ROM_PTR(&mp_usb_console_active_obj) },
-    { MP_ROM_QSTR(MP_QSTR_i2c_scan), MP_ROM_PTR(&mp_i2c_scan_obj) },
-
-    { MP_ROM_QSTR(MP_QSTR_BUTTON_PRESSED_LEFT), MP_ROM_INT(st3m_tripos_left) },
-    { MP_ROM_QSTR(MP_QSTR_BUTTON_PRESSED_RIGHT),
-      MP_ROM_INT(st3m_tripos_right) },
-    { MP_ROM_QSTR(MP_QSTR_BUTTON_PRESSED_DOWN), MP_ROM_INT(st3m_tripos_mid) },
-    { MP_ROM_QSTR(MP_QSTR_BUTTON_NOT_PRESSED), MP_ROM_INT(st3m_tripos_none) },
-
-    { MP_ROM_QSTR(MP_QSTR_scope_draw), MP_ROM_PTR(&mp_scope_draw_obj) },
-};
-
-STATIC MP_DEFINE_CONST_DICT(mp_module_hardware_globals,
-                            mp_module_hardware_globals_table);
-
-const mp_obj_module_t mp_module_hardware = {
-    .base = { &mp_type_module },
-    .globals = (mp_obj_dict_t *)&mp_module_hardware_globals,
-};
-
-MP_REGISTER_MODULE(MP_QSTR_hardware, mp_module_hardware);
diff --git a/components/micropython/usermodule/mp_kernel.c b/components/micropython/usermodule/mp_kernel.c
index 72d4bce0cb5a30debfe153eb8a6b52c5acd7dde1..cbaff5919c77dd97b57fa19e3553b42f7febd792 100644
--- a/components/micropython/usermodule/mp_kernel.c
+++ b/components/micropython/usermodule/mp_kernel.c
@@ -4,8 +4,11 @@
 
 #include <stdio.h>
 #include <string.h>
+#include "flow3r_bsp.h"
 #include "py/obj.h"
 #include "py/runtime.h"
+#include "st3m_console.h"
+#include "st3m_usb.h"
 #include "st3m_version.h"
 
 #if (configUSE_TRACE_FACILITY != 1)
@@ -299,12 +302,89 @@ STATIC mp_obj_t mp_firmware_version(void) {
 
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_firmware_version_obj, mp_firmware_version);
 
+STATIC mp_obj_t mp_hardware_version(void) {
+    mp_obj_t str =
+        mp_obj_new_str(flow3r_bsp_hw_name, strlen(flow3r_bsp_hw_name));
+    return str;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_hardware_version_obj, mp_hardware_version);
+
+STATIC mp_obj_t mp_freertos_sleep(mp_obj_t ms_in) {
+    uint32_t ms = mp_obj_get_int(ms_in);
+    MP_THREAD_GIL_EXIT();
+    vTaskDelay(ms / portTICK_PERIOD_MS);
+    MP_THREAD_GIL_ENTER();
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_freertos_sleep_obj, mp_freertos_sleep);
+
+STATIC mp_obj_t mp_usb_connected(void) {
+    static int64_t last_check = 0;
+    static bool value = false;
+    int64_t now = esp_timer_get_time();
+
+    if (last_check == 0) {
+        last_check = now;
+        value = st3m_usb_connected();
+    }
+
+    if ((now - last_check) > 10000) {
+        value = st3m_usb_connected();
+        last_check = now;
+    }
+    return mp_obj_new_bool(value);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_usb_connected_obj, mp_usb_connected);
+
+STATIC mp_obj_t mp_usb_console_active(void) {
+    static int64_t last_check = 0;
+    static bool value = false;
+    int64_t now = esp_timer_get_time();
+
+    if (last_check == 0) {
+        last_check = now;
+        value = st3m_console_active();
+    }
+
+    if ((now - last_check) > 10000) {
+        value = st3m_console_active();
+        last_check = now;
+    }
+    return mp_obj_new_bool(value);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_usb_console_active_obj,
+                                 mp_usb_console_active);
+
+STATIC mp_obj_t mp_i2c_scan(void) {
+    flow3r_bsp_i2c_scan_result_t scan;
+    flow3r_bsp_i2c_scan(&scan);
+
+    mp_obj_t res = mp_obj_new_list(0, NULL);
+    for (int i = 0; i < 127; i++) {
+        size_t ix = i / 32;
+        size_t offs = i % 32;
+        if (scan.res[ix] & (1 << offs)) {
+            mp_obj_list_append(res, mp_obj_new_int_from_uint(i));
+        }
+    }
+    return res;
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_i2c_scan_obj, mp_i2c_scan);
+
 STATIC const mp_rom_map_elem_t globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_scheduler_snapshot),
       MP_ROM_PTR(&mp_scheduler_snapshot_obj) },
     { MP_ROM_QSTR(MP_QSTR_heap_stats), MP_ROM_PTR(&mp_heap_stats_obj) },
     { MP_ROM_QSTR(MP_QSTR_firmware_version),
       MP_ROM_PTR(&mp_firmware_version_obj) },
+    { MP_ROM_QSTR(MP_QSTR_hardware_version),
+      MP_ROM_PTR(&mp_hardware_version_obj) },
+    { MP_ROM_QSTR(MP_QSTR_freertos_sleep), MP_ROM_PTR(&mp_freertos_sleep_obj) },
+    { MP_ROM_QSTR(MP_QSTR_usb_connected), MP_ROM_PTR(&mp_usb_connected_obj) },
+    { MP_ROM_QSTR(MP_QSTR_usb_console_active),
+      MP_ROM_PTR(&mp_usb_console_active_obj) },
+    { MP_ROM_QSTR(MP_QSTR_i2c_scan), MP_ROM_PTR(&mp_i2c_scan_obj) },
 
     { MP_ROM_QSTR(MP_QSTR_RUNNING), MP_ROM_INT(eRunning) },
     { MP_ROM_QSTR(MP_QSTR_READY), MP_ROM_INT(eReady) },
diff --git a/components/micropython/usermodule/mp_sys_buttons.c b/components/micropython/usermodule/mp_sys_buttons.c
new file mode 100644
index 0000000000000000000000000000000000000000..76a87dc03b8ff083dea48e2750c19ec0ef54eb85
--- /dev/null
+++ b/components/micropython/usermodule/mp_sys_buttons.c
@@ -0,0 +1,51 @@
+// probably doesn't need all of these idk
+#include <stdio.h>
+#include <string.h>
+
+#include "extmod/virtpin.h"
+#include "machine_rtc.h"
+#include "modmachine.h"
+#include "mphalport.h"
+#include "py/builtin.h"
+#include "py/mphal.h"
+#include "py/runtime.h"
+
+#include "flow3r_bsp.h"
+#include "st3m_console.h"
+#include "st3m_gfx.h"
+#include "st3m_io.h"
+#include "st3m_scope.h"
+
+#include "mp_uctx.h"
+
+STATIC mp_obj_t mp_get_left() {
+    return mp_obj_new_int(st3m_io_left_button_get());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_left_obj, mp_get_left);
+
+STATIC mp_obj_t mp_get_right() {
+    return mp_obj_new_int(st3m_io_right_button_get());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_right_obj, mp_get_right);
+
+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_PRESSED_LEFT), MP_ROM_INT(st3m_tripos_left) },
+    { MP_ROM_QSTR(MP_QSTR_PRESSED_RIGHT), MP_ROM_INT(st3m_tripos_right) },
+    { MP_ROM_QSTR(MP_QSTR_PRESSED_DOWN), MP_ROM_INT(st3m_tripos_mid) },
+    { MP_ROM_QSTR(MP_QSTR_NOT_PRESSED), MP_ROM_INT(st3m_tripos_none) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(mp_module_sys_buttons_globals,
+                            mp_module_sys_buttons_globals_table);
+
+const mp_obj_module_t mp_module_sys_buttons = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_sys_buttons_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_sys_buttons, mp_module_sys_buttons);
diff --git a/components/micropython/usermodule/mp_sys_display.c b/components/micropython/usermodule/mp_sys_display.c
new file mode 100644
index 0000000000000000000000000000000000000000..959499a78edf41c18b35e11ff3012a89b595678c
--- /dev/null
+++ b/components/micropython/usermodule/mp_sys_display.c
@@ -0,0 +1,93 @@
+// probably doesn't need all of these idk
+#include <stdio.h>
+#include <string.h>
+
+#include "extmod/virtpin.h"
+#include "machine_rtc.h"
+#include "modmachine.h"
+#include "mphalport.h"
+#include "py/builtin.h"
+#include "py/mphal.h"
+#include "py/runtime.h"
+
+#include "flow3r_bsp.h"
+#include "st3m_console.h"
+#include "st3m_gfx.h"
+#include "st3m_io.h"
+#include "st3m_scope.h"
+
+#include "mp_uctx.h"
+
+static st3m_ctx_desc_t *gfx_last_desc = NULL;
+
+STATIC mp_obj_t mp_set_backlight(mp_obj_t percent_in) {
+    uint8_t percent = mp_obj_get_int(percent_in);
+    flow3r_bsp_display_set_backlight(percent);
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_backlight_obj, mp_set_backlight);
+
+STATIC mp_obj_t mp_get_ctx(void) {
+    if (gfx_last_desc == NULL) {
+        gfx_last_desc = st3m_gfx_drawctx_free_get(0);
+        if (gfx_last_desc == NULL) {
+            return mp_const_none;
+        }
+    }
+    mp_obj_t mp_ctx = mp_ctx_from_ctx(gfx_last_desc->ctx);
+    return mp_ctx;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_ctx_obj, mp_get_ctx);
+
+STATIC mp_obj_t mp_update(mp_obj_t ctx_in) {
+    mp_ctx_obj_t *self = MP_OBJ_TO_PTR(ctx_in);
+    if (self->base.type != &mp_ctx_type) {
+        mp_raise_ValueError("not a ctx");
+        return mp_const_none;
+    }
+
+    if (gfx_last_desc != NULL) {
+        if (gfx_last_desc->ctx != self->ctx) {
+            mp_raise_ValueError(
+                "not the correct ctx (do not hold on to ctx objects!)");
+            return mp_const_none;
+        }
+        st3m_gfx_drawctx_pipe_put(gfx_last_desc);
+        gfx_last_desc = NULL;
+    }
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_update_obj, mp_update);
+
+STATIC mp_obj_t mp_pipe_full(void) {
+    if (st3m_gfx_drawctx_pipe_full()) {
+        return mp_const_true;
+    }
+    return mp_const_false;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_pipe_full_obj, mp_pipe_full);
+
+STATIC mp_obj_t mp_pipe_flush(void) {
+    st3m_gfx_flush();
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_pipe_flush_obj, mp_pipe_flush);
+
+STATIC const mp_rom_map_elem_t mp_module_sys_display_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_display) },
+    { MP_ROM_QSTR(MP_QSTR_pipe_full), MP_ROM_PTR(&mp_pipe_full_obj) },
+    { MP_ROM_QSTR(MP_QSTR_pipe_flush), MP_ROM_PTR(&mp_pipe_flush_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_backlight), MP_ROM_PTR(&mp_set_backlight_obj) },
+    { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&mp_update_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_ctx), MP_ROM_PTR(&mp_get_ctx_obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(mp_module_sys_display_globals,
+                            mp_module_sys_display_globals_table);
+
+const mp_obj_module_t mp_module_sys_display = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_sys_display_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_sys_display, mp_module_sys_display);
diff --git a/components/micropython/usermodule/mp_uctx.c b/components/micropython/usermodule/mp_uctx.c
index bc0373ee074684cac39d98d016f140d04bbb6c68..c668bf2a5273f0a7d1eaf0346390711dafd2b2c7 100644
--- a/components/micropython/usermodule/mp_uctx.c
+++ b/components/micropython/usermodule/mp_uctx.c
@@ -4,16 +4,8 @@
 #include "py/objarray.h"
 #include "py/runtime.h"
 
-// clang-format off
-#include "ctx_config.h"
-#include "ctx.h"
-// clang-format on
-
-typedef struct _mp_ctx_obj_t {
-    mp_obj_base_t base;
-    Ctx *ctx;
-    mp_obj_t user_data;
-} mp_ctx_obj_t;
+#include "mp_uctx.h"
+#include "st3m_scope.h"
 
 void gc_collect(void);
 #ifdef EMSCRIPTEN
@@ -245,6 +237,13 @@ MP_CTX_COMMON_FUN_6F(radial_gradient);
 
 MP_CTX_COMMON_FUN_3F(logo);
 
+static mp_obj_t mp_ctx_scope(mp_obj_t self_in) {
+    mp_ctx_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    st3m_scope_draw(self->ctx);
+    return self_in;
+}
+MP_DEFINE_CONST_FUN_OBJ_1(mp_ctx_scope_obj, mp_ctx_scope);
+
 static mp_obj_t mp_ctx_key_down(size_t n_args, const mp_obj_t *args) {
     mp_ctx_obj_t *self = MP_OBJ_TO_PTR(args[0]);
     ctx_key_down(self->ctx,
@@ -514,8 +513,6 @@ STATIC void generic_method_lookup(mp_obj_t obj, qstr attr, mp_obj_t *dest) {
     }
 }
 
-extern const mp_obj_type_t mp_ctx_type;
-
 #if CTX_TINYVG
 static mp_obj_t mp_ctx_tinyvg_get_size(mp_obj_t self_in, mp_obj_t buffer_in) {
     mp_buffer_info_t buffer_info;
@@ -857,6 +854,7 @@ static const mp_rom_map_elem_t mp_ctx_locals_dict_table[] = {
 #endif
 #endif
     MP_CTX_METHOD(logo),
+    MP_CTX_METHOD(scope),
 
     // Instance attributes
     MP_CTX_ATTR(x),
diff --git a/components/micropython/usermodule/mp_uctx.h b/components/micropython/usermodule/mp_uctx.h
new file mode 100644
index 0000000000000000000000000000000000000000..96b4f2633c66bea212a5a1ac17f9291c650d44d7
--- /dev/null
+++ b/components/micropython/usermodule/mp_uctx.h
@@ -0,0 +1,16 @@
+#pragma once
+
+// clang-format off
+#include "ctx_config.h"
+#include "ctx.h"
+// clang-format on
+
+typedef struct _mp_ctx_obj_t {
+    mp_obj_base_t base;
+    Ctx *ctx;
+    mp_obj_t user_data;
+} mp_ctx_obj_t;
+
+extern const mp_obj_type_t mp_ctx_type;
+
+mp_obj_t mp_ctx_from_ctx(Ctx *ctx);
\ No newline at end of file
diff --git a/docs/api/hardware.rst b/docs/api/hardware.rst
deleted file mode 100644
index 63fa6e05dec3d52e371c27d00549a60d6d42ce79..0000000000000000000000000000000000000000
--- a/docs/api/hardware.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-``hardware`` module
-===================
-
-.. automodule:: hardware
-   :members:
-   :undoc-members:
-
diff --git a/docs/api/sys_buttons.rst b/docs/api/sys_buttons.rst
new file mode 100644
index 0000000000000000000000000000000000000000..fbe227077f98cb143b95839211fdafddca795e92
--- /dev/null
+++ b/docs/api/sys_buttons.rst
@@ -0,0 +1,7 @@
+``sys_buttons`` module
+======================
+
+.. automodule:: sys_buttons
+   :members:
+   :undoc-members:
+
diff --git a/docs/api/sys_display.rst b/docs/api/sys_display.rst
new file mode 100644
index 0000000000000000000000000000000000000000..288b8eca26601c735c9558e57fc5af68054ebe4a
--- /dev/null
+++ b/docs/api/sys_display.rst
@@ -0,0 +1,7 @@
+``sys_display`` module
+======================
+
+.. automodule:: sys_display
+   :members:
+   :undoc-members:
+
diff --git a/docs/badge/application-programming.rst b/docs/badge/application-programming.rst
index 4d1d5e0bbf05ab82d4f393d4859191ec3ba973f3..2d7124023be61c81e1eb15670eae515ede9fab3f 100644
--- a/docs/badge/application-programming.rst
+++ b/docs/badge/application-programming.rst
@@ -80,13 +80,14 @@ Example 1b: React to input
 
 If we want to react to the user, we can use the :py:class:`InputState` which got
 handed to us. In this example we look at the state of the app (by default left)
-shoulder button.  The values contained in the input state are the same as used
-by the :py:mod:`hardware` module.
+shoulder button. The values for buttons contained in the input state are one of
+``InputButtonState.PRESSED_LEFT``, ``PRESSED_RIGHT``, ``PRESSED_DOWN``,
+``NOT_PRESSED`` - same values as in the low-level
+:py:mod:`sys_buttons`.
 
 .. code-block:: python
 
     from st3m.reactor import Responder
-    from hardware import BUTTON_PRESSED_LEFT, BUTTON_PRESSED_RIGHT, BUTTON_PRESSED_DOWN
     import st3m.run
 
     class Example(Responder):
@@ -103,9 +104,9 @@ by the :py:mod:`hardware` module.
         def think(self, ins: InputState, delta_ms: int) -> None:
             direction = ins.buttons.app
 
-            if direction == BUTTON_PRESSED_LEFT:
+            if direction == ins.buttons.PRESSED_LEFT:
                 self._x -= 1
-            elif direction == BUTTON_PRESSED_RIGHT:
+            elif direction == ins.buttons.PRESSED_RIGHT:
                 self._x += 1
 
 
@@ -128,7 +129,6 @@ represents the time which has passed since the last call to `think()`.
 .. code-block:: python
 
     from st3m.reactor import Responder
-    from hardware import BUTTON_PRESSED_LEFT, BUTTON_PRESSED_RIGHT, BUTTON_PRESSED_DOWN
     import st3m.run
 
     class Example(Responder):
@@ -145,9 +145,9 @@ represents the time which has passed since the last call to `think()`.
         def think(self, ins: InputState, delta_ms: int) -> None:
             direction = ins.buttons.app # -1 (left), 1 (right), or 2 (pressed)
 
-            if direction == BUTTON_PRESSED_LEFT:
+            if direction == ins.buttons.PRESSED_LEFT:
                 self._x -= 20 * delta_ms / 1000
-            elif direction == BUTTON_PRESSED_RIGHT:
+            elif direction == ins.buttons.PRESSED_RIGHT:
                 self._x += 40 * delta_ms / 1000
 
 
@@ -213,7 +213,6 @@ a square.
     from st3m.input import InputController
     from st3m.utils import tau
 
-    from hardware import BUTTON_PRESSED_LEFT, BUTTON_PRESSED_RIGHT, BUTTON_PRESSED_DOWN
     import st3m.run
 
     class Example(Responder):
diff --git a/docs/index.rst b/docs/index.rst
index fbebc5de7061d054d6e525e8b7502db12a2e71af..6129613d2742045c2d38945170397d77010cc317 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -26,10 +26,11 @@ Welcome to flow3r's documentation!
    api/audio.rst
    api/badgelink.rst
    api/captouch.rst
-   api/hardware.rst
    api/kernel.rst
    api/leds.rst
    api/ctx.rst
+   api/sys_buttons.rst
+   api/sys_display.rst
 
 Indices and tables
 ==================
diff --git a/python_payload/apps/demo_cap_touch/main.py b/python_payload/apps/demo_cap_touch/main.py
index 2819745974e53ece984f554ae476e8879ad8a5be..c5057ceb33a98b1708c085aa66747adb7011442c 100644
--- a/python_payload/apps/demo_cap_touch/main.py
+++ b/python_payload/apps/demo_cap_touch/main.py
@@ -11,8 +11,6 @@ import cmath
 import math
 import time
 
-import hardware
-
 
 class Dot:
     def __init__(self, size: float, imag: float, real: float) -> None:
diff --git a/python_payload/apps/demo_harmonic/__init__.py b/python_payload/apps/demo_harmonic/__init__.py
index 98402812b3506a1af08b3db5fd6a1ffd021ca643..f2683276cd9d71f7f173bc4b23fcf1a7b389391c 100644
--- a/python_payload/apps/demo_harmonic/__init__.py
+++ b/python_payload/apps/demo_harmonic/__init__.py
@@ -3,7 +3,6 @@ import bl00mbox
 
 blm = bl00mbox.Channel("Harmonic Demo")
 import leds
-import hardware
 
 from st3m.goose import List
 from st3m.input import InputState
@@ -57,7 +56,7 @@ class HarmonicApp(Application):
         ctx.rgb(i, i, i).rectangle(-120, -120, 240, 240).fill()
 
         ctx.rgb(0, 0, 0)
-        hardware.scope_draw(ctx)
+        ctx.scope()
         ctx.fill()
 
     def think(self, ins: InputState, delta_ms: int) -> None:
diff --git a/python_payload/apps/demo_melodic/__init__.py b/python_payload/apps/demo_melodic/__init__.py
index 6866af46cd3012c466e0f50389f024938878108c..97e4cc19510fba58ae686fdd51c3d4e416b2c007 100644
--- a/python_payload/apps/demo_melodic/__init__.py
+++ b/python_payload/apps/demo_melodic/__init__.py
@@ -2,7 +2,6 @@ import bl00mbox
 
 blm = bl00mbox.Channel("Melodic Demo")
 
-from hardware import *
 import captouch
 import leds
 
@@ -97,7 +96,7 @@ class MelodicApp(Application):
     def draw(self, ctx: Context) -> None:
         ctx.rgb(1, 1, 1).rectangle(-120, -120, 240, 240).fill()
         ctx.rgb(0, 0, 0)
-        scope_draw(ctx)
+        ctx.scope()
         ctx.fill()
 
     def on_enter(self, vm: Optional[ViewManager]) -> None:
diff --git a/python_payload/apps/shoegaze/__init__.py b/python_payload/apps/shoegaze/__init__.py
index f625b651ced7efb7bd33485e753a7276c5742b6a..e45d1b1f980275cd9d3aaf64553526d864eeb280 100644
--- a/python_payload/apps/shoegaze/__init__.py
+++ b/python_payload/apps/shoegaze/__init__.py
@@ -10,7 +10,6 @@ import captouch
 import bl00mbox
 
 import leds
-import hardware
 import random
 
 chords = [
diff --git a/python_payload/apps/simple_drums/__init__.py b/python_payload/apps/simple_drums/__init__.py
index b9ade9a3a060cfa9041b3a73fa25c93cb67fc0b2..6ee84938b2286aefb44a52dc744698980fcfb64d 100644
--- a/python_payload/apps/simple_drums/__init__.py
+++ b/python_payload/apps/simple_drums/__init__.py
@@ -1,5 +1,4 @@
 import bl00mbox
-import hardware
 import captouch
 import leds
 
diff --git a/python_payload/mypystubs/ctx.pyi b/python_payload/mypystubs/ctx.pyi
index 7e50072c324fef8601b21c57e049662a9fda080e..bbd830b3767a2d469112a65c174b4e9521c99cd5 100644
--- a/python_payload/mypystubs/ctx.pyi
+++ b/python_payload/mypystubs/ctx.pyi
@@ -313,3 +313,11 @@ class Context(Protocol):
         TOD(q3k): document
         """
         pass
+    def scope(self) -> "Context":
+        """
+        Draw an audio 'oscilloscope'-style visualizer at -120,-120,120,120
+        bounding box.
+
+        Needs to be stroked/filled afterwards.
+        """
+        pass
diff --git a/python_payload/mypystubs/hardware.pyi b/python_payload/mypystubs/hardware.pyi
deleted file mode 100644
index 57de5ae57b1b028533bf9e3ba301d96a7557d6ee..0000000000000000000000000000000000000000
--- a/python_payload/mypystubs/hardware.pyi
+++ /dev/null
@@ -1,26 +0,0 @@
-import time
-
-from ctx import Context
-
-def freertos_sleep(ms: int) -> None: ...
-def get_ctx() -> Context: ...
-def display_pipe_full() -> bool: ...
-def display_pipe_flush() -> None: ...
-def display_update(c: Context) -> None: ...
-def display_set_backlight(percent: int) -> None: ...
-def usb_connected() -> bool: ...
-def usb_console_active() -> bool: ...
-def version() -> str: ...
-def i2c_scan() -> list[int]: ...
-def menu_button_get() -> int: ...
-def application_button_get() -> int: ...
-def left_button_get() -> int: ...
-def right_button_get() -> int: ...
-def menu_button_set_left(left: int) -> None: ...
-def menu_button_get_left() -> int: ...
-def scope_draw(ctx: Context) -> None: ...
-
-BUTTON_PRESSED_LEFT: int
-BUTTON_PRESSED_RIGHT: int
-BUTTON_PRESSED_DOWN: int
-BUTTON_NOT_PRESSED: int
diff --git a/python_payload/mypystubs/kernel.pyi b/python_payload/mypystubs/kernel.pyi
index 95e1ab33de9d0b7c76f50b1cb4423265f45fa4f6..a737a3059f634399587364bc9922f1f93213a3d5 100644
--- a/python_payload/mypystubs/kernel.pyi
+++ b/python_payload/mypystubs/kernel.pyi
@@ -64,3 +64,10 @@ BLOCKED: int
 SUSPENDED: int
 DELETED: int
 INVALID: int
+
+def freertos_sleep(ms: int) -> None: ...
+def usb_connected() -> bool: ...
+def usb_console_active() -> bool: ...
+def hardware_version() -> str: ...
+def firmware_version() -> str: ...
+def i2c_scan() -> list[int]: ...
diff --git a/python_payload/mypystubs/sys_buttons.pyi b/python_payload/mypystubs/sys_buttons.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..4b3346a2106c0ceac5d9fa6744dbe5826e7024e1
--- /dev/null
+++ b/python_payload/mypystubs/sys_buttons.pyi
@@ -0,0 +1,7 @@
+def get_left() -> int: ...
+def get_right() -> int: ...
+
+PRESSED_LEFT: int
+PRESSED_RIGHT: int
+PRESSED_DOWN: int
+NOT_PRESSED: int
diff --git a/python_payload/mypystubs/sys_display.pyi b/python_payload/mypystubs/sys_display.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..38e4e6325c14990a7d108f61b09d7cf4a68a04c1
--- /dev/null
+++ b/python_payload/mypystubs/sys_display.pyi
@@ -0,0 +1,7 @@
+from ctx import Context
+
+def get_ctx() -> Context: ...
+def pipe_full() -> bool: ...
+def pipe_flush() -> None: ...
+def update(c: Context) -> None: ...
+def set_backlight(percent: int) -> None: ...
diff --git a/python_payload/st3m/input.py b/python_payload/st3m/input.py
index a1fdbbc5e5d62547bce3a1c826d061cfe1d267f3..99dafd6b7f8a83f3ce129b71dcbe885544636871 100644
--- a/python_payload/st3m/input.py
+++ b/python_payload/st3m/input.py
@@ -1,6 +1,6 @@
 from st3m.goose import List, Optional, Enum, Tuple
 
-import hardware
+import sys_buttons
 import captouch
 import imu
 from st3m.power import Power
@@ -53,6 +53,11 @@ class InputButtonState:
 
     __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, left: int, right: int, swapped: bool):
         app = left
         os = right
@@ -96,8 +101,8 @@ class InputState:
         Reactor.
         """
         cts = captouch.read()
-        left = hardware.left_button_get()
-        right = hardware.right_button_get()
+        left = sys_buttons.get_left()
+        right = sys_buttons.get_right()
         buttons = InputButtonState(left, right, swapped_buttons)
 
         acc = imu.acc_read()
diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py
index b24384d8dd7778ed5564ec9ecfeae739dc4f5e4d..3094719097f96919f6cdd71c0e7ee3bf774c8861 100644
--- a/python_payload/st3m/reactor.py
+++ b/python_payload/st3m/reactor.py
@@ -2,7 +2,9 @@ from st3m.goose import ABCBase, abstractmethod, List, Optional
 from st3m.input import InputState
 from ctx import Context
 
-import time, hardware
+import time
+import sys_display
+import kernel
 
 
 class Responder(ABCBase):
@@ -126,7 +128,7 @@ class Reactor:
         self.stats.record_run_time(elapsed)
         wait = deadline - end
         if wait > 0:
-            hardware.freertos_sleep(wait)
+            kernel.freertos_sleep(wait)
 
     def _run_top(self, start: int) -> None:
         # Skip if we have no top Responder.
@@ -148,7 +150,7 @@ class Reactor:
 
         # Draw!
         if self._ctx is None:
-            self._ctx = hardware.get_ctx()
+            self._ctx = sys_display.get_ctx()
             if self._ctx is not None:
                 if self._last_ctx_get is not None:
                     diff = start - self._last_ctx_get
@@ -158,6 +160,6 @@ class Reactor:
                 self._ctx.save()
                 self._top.draw(self._ctx)
                 self._ctx.restore()
-        if self._ctx is not None and not hardware.display_pipe_full():
-            hardware.display_update(self._ctx)
+        if self._ctx is not None and not sys_display.pipe_full():
+            sys_display.update(self._ctx)
             self._ctx = None
diff --git a/python_payload/st3m/ui/elements/overlays.py b/python_payload/st3m/ui/elements/overlays.py
index aa6ca04944f81c068eb4c0966808a01daacd4594..47a1fcabb8f4520bf4d6d8243390ac26f9c0ac94 100644
--- a/python_payload/st3m/ui/elements/overlays.py
+++ b/python_payload/st3m/ui/elements/overlays.py
@@ -13,7 +13,7 @@ from ctx import Context
 
 import math
 import audio
-import hardware
+import kernel
 
 
 class OverlayKind(Enum):
@@ -343,7 +343,7 @@ class USBIcon(Icon):
     """
 
     def visible(self) -> bool:
-        return hardware.usb_connected()
+        return kernel.usb_connected()
 
     def draw(self, ctx: Context) -> None:
         ctx.gray(1.0)
diff --git a/sim/fakes/hardware.py b/sim/fakes/_sim.py
similarity index 93%
rename from sim/fakes/hardware.py
rename to sim/fakes/_sim.py
index 12637406fd6a64a80ff71220ad01f5c3c1c0866a..68101a8cb4c433c8d3e4e644d82ae3a87b39edbe 100644
--- a/sim/fakes/hardware.py
+++ b/sim/fakes/_sim.py
@@ -440,10 +440,6 @@ class Simulation:
 _sim = Simulation()
 
 
-def init_done():
-    return True
-
-
 import ctx
 
 
@@ -501,14 +497,6 @@ def display_update(subctx):
     fbm.put(fbp, c)
 
 
-def display_pipe_full():
-    return False
-
-
-def set_global_volume_dB(a):
-    pass
-
-
 def get_button_state(left):
     _sim.process_events()
     _sim.render_gui_lazy()
@@ -528,64 +516,3 @@ def get_button_state(left):
     elif sub[2]:
         return +1
     return 0
-
-
-menu_button_left = 0
-
-
-def menu_button_get():
-    return get_button_state(menu_button_left)
-
-
-def application_button_get():
-    return get_button_state(1 - menu_button_left)
-
-
-def left_button_get():
-    return get_button_state(1)
-
-
-def right_button_get():
-    return get_button_state(0)
-
-
-def menu_button_set_left(_broken):
-    global menu_button_left
-    menu_button_left = 1
-
-
-def menu_button_get_left():
-    return menu_button_left
-
-
-def freertos_sleep(ms):
-    import _time
-
-    _time.sleep(ms / 1000.0)
-
-
-def scope_draw(ctx):
-    import math
-
-    x = -120
-    ctx.move_to(x, 0)
-    for i in range(240):
-        x2 = x + i
-        y2 = math.sin(i / 10) * 80
-        ctx.line_to(x2, y2)
-    ctx.line_to(130, 0)
-    ctx.line_to(130, 130)
-    ctx.line_to(-130, 130)
-    ctx.line_to(-130, 0)
-
-
-def usb_connected():
-    return True
-
-
-def usb_console_active():
-    return True
-
-
-def i2c_scan():
-    return [16, 44, 45, 85, 109, 110]
diff --git a/sim/fakes/captouch.py b/sim/fakes/captouch.py
index 19dea2197a66bcba9df4bd5503b00c887eacf2e2..028384fbd9e9915c318091511c69fc6e0e009cf7 100644
--- a/sim/fakes/captouch.py
+++ b/sim/fakes/captouch.py
@@ -67,11 +67,11 @@ class CaptouchState:
 
 
 def read() -> CaptouchState:
-    import hardware
+    import _sim
 
-    hardware._sim.process_events()
-    hardware._sim.render_gui_lazy()
-    petals = hardware._sim.petals
+    _sim._sim.process_events()
+    _sim._sim.render_gui_lazy()
+    petals = _sim._sim.petals
 
     res = []
     for petal in range(10):
diff --git a/sim/fakes/ctx.py b/sim/fakes/ctx.py
index 845d0fe4c127f458f9715d913d1d3c1cb70d9083..a6b65efca30cee150b1d93234ed004d0484ce999 100644
--- a/sim/fakes/ctx.py
+++ b/sim/fakes/ctx.py
@@ -6,6 +6,7 @@ serialized ctx protocol as described in [1].
 [1] - https://ctx.graphics/protocol/
 """
 import os
+import math
 
 import wasmer
 import wasmer_compiler_cranelift
@@ -285,3 +286,16 @@ class Context:
             "Camp Font 3",
             "Material Icons",
         ][i]
+
+    def scope(self):
+        x = -120
+        self.move_to(x, 0)
+        for i in range(240):
+            x2 = x + i
+            y2 = math.sin(i / 10) * 80
+            self.line_to(x2, y2)
+        self.line_to(130, 0)
+        self.line_to(130, 130)
+        self.line_to(-130, 130)
+        self.line_to(-130, 0)
+        return self
diff --git a/sim/fakes/kernel.py b/sim/fakes/kernel.py
index 25fe328f2ac239c9d0bfc6a54ebc17dad6f6c5ce..4f33a799c9d4675a8e58bab32b80a0e4b4941013 100644
--- a/sim/fakes/kernel.py
+++ b/sim/fakes/kernel.py
@@ -13,3 +13,21 @@ class FakeHeapStats:
 
 def heap_stats():
     return FakeHeapStats()
+
+
+def usb_connected():
+    return True
+
+
+def usb_console_active():
+    return True
+
+
+def freertos_sleep(ms):
+    import _time
+
+    _time.sleep(ms / 1000.0)
+
+
+def i2c_scan():
+    return [16, 44, 45, 85, 109, 110]
diff --git a/sim/fakes/leds.py b/sim/fakes/leds.py
index ff13695998890c85ff3a35db762993e4359c058e..2235346c62956db4f053684515941c2eb53c68a4 100644
--- a/sim/fakes/leds.py
+++ b/sim/fakes/leds.py
@@ -1,4 +1,4 @@
-from hardware import _sim
+from _sim import _sim
 
 import pygame
 
diff --git a/sim/fakes/sys_buttons.py b/sim/fakes/sys_buttons.py
new file mode 100644
index 0000000000000000000000000000000000000000..46e4aef28dc6d13f5490367c4459ef99d57d3701
--- /dev/null
+++ b/sim/fakes/sys_buttons.py
@@ -0,0 +1,15 @@
+import _sim
+
+
+def get_left():
+    return _sim.get_button_state(1)
+
+
+def get_right():
+    return _sim.get_button_state(0)
+
+
+PRESSED_LEFT = -1
+PRESSED_RIGHT = 1
+PRESSED_DOWN = 2
+NOT_PRESSED = 0
diff --git a/sim/fakes/sys_display.py b/sim/fakes/sys_display.py
new file mode 100644
index 0000000000000000000000000000000000000000..3180dc2030586d54a34a7a969254a369cec45424
--- /dev/null
+++ b/sim/fakes/sys_display.py
@@ -0,0 +1,9 @@
+import _sim
+
+
+def pipe_full():
+    return False
+
+
+update = _sim.display_update
+get_ctx = _sim.get_ctx
diff --git a/sim/run.py b/sim/run.py
index 8da91ef535ded29c6deb630037505508d8f8ec97..b07e31245b21ef2ad44ba3d5357c4c42a0a62c0c 100644
--- a/sim/run.py
+++ b/sim/run.py
@@ -108,9 +108,9 @@ def _stat(path):
 os.stat = _stat
 
 if len(sys.argv) >= 2 and sys.argv[1] == "screenshot":
-    import hardware
+    import _sim
 
-    hardware.SCREENSHOT = True
+    _sim.SCREENSHOT = True
 elif len(sys.argv) == 2:
     import st3m.run