diff --git a/components/badge23/CMakeLists.txt b/components/badge23/CMakeLists.txt
index 8f910586a2827a8b6a586bdbef80778c6da61e23..02bcc745a35a2448901b68ab713a63cb29ba5ea9 100644
--- a/components/badge23/CMakeLists.txt
+++ b/components/badge23/CMakeLists.txt
@@ -1,8 +1,2 @@
 idf_component_register(
-    SRCS
-        captouch.c
-    INCLUDE_DIRS
-        include
-    REQUIRES
-        flow3r_bsp
 )
diff --git a/components/badge23/captouch.c b/components/badge23/captouch.c
deleted file mode 100644
index ffff6f214aedecb26e633f07c0f1140ea6eb7af0..0000000000000000000000000000000000000000
--- a/components/badge23/captouch.c
+++ /dev/null
@@ -1,293 +0,0 @@
-#include "badge23/captouch.h"
-
-#include "esp_err.h"
-#include "esp_log.h"
-
-#include "flow3r_bsp_captouch.h"
-#include "sdkconfig.h"
-
-#include "freertos/FreeRTOS.h"
-#include "freertos/semphr.h"
-#include "freertos/task.h"
-
-#include <string.h>
-
-static const char *TAG = "st3m-captouch";
-
-// A simple, non-concurrent ringbuffer. I feel like I've already implemented
-// this once in this codebase.
-//
-// TODO(q3k): unify/expose as common st3m API.
-typedef struct {
-    size_t write_ix;
-    bool wrapped;
-    uint16_t buf[4];
-} ringbuffer_t;
-
-// Size of ringbuffer, in elements.
-static inline size_t ringbuffer_size(const ringbuffer_t *rb) {
-    return sizeof(rb->buf) / sizeof(uint16_t);
-}
-
-// Write to ringbuffer.
-static void ringbuffer_write(ringbuffer_t *rb, uint16_t data) {
-    rb->buf[rb->write_ix] = data;
-    rb->write_ix++;
-    if (rb->write_ix >= ringbuffer_size(rb)) {
-        rb->write_ix = 0;
-        rb->wrapped = true;
-    }
-}
-
-// Get ringbuffer average (mean), or 0 if no values have yet been inserted.
-static uint16_t ringbuffer_avg(const ringbuffer_t *rb) {
-    int32_t res = 0;
-    if (rb->wrapped) {
-        for (size_t i = 0; i < ringbuffer_size(rb); i++) {
-            res += rb->buf[i];
-        }
-        res /= ringbuffer_size(rb);
-        return res;
-    }
-    if (rb->write_ix == 0) {
-        return 0;
-    }
-    for (size_t i = 0; i < rb->write_ix; i++) {
-        res += rb->buf[i];
-    }
-    res /= rb->write_ix;
-    return res;
-}
-
-// Get last inserted value, or 0 if no value have yet been inserted.
-static uint16_t ringbuffer_last(const ringbuffer_t *rb) {
-    if (rb->write_ix == 0) {
-        if (rb->wrapped) {
-            return rb->buf[ringbuffer_size(rb) - 1];
-        }
-        return 0;
-    }
-    return rb->buf[rb->write_ix - 1];
-}
-
-// TODO(q3k): expose these as user structs?
-
-typedef struct {
-    ringbuffer_t rb;
-    bool pressed;
-} st3m_captouch_petal_pad_t;
-
-typedef struct {
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-    st3m_captouch_petal_pad_t tip;
-#else
-    st3m_captouch_petal_pad_t base;
-#endif
-    st3m_captouch_petal_pad_t cw;
-    st3m_captouch_petal_pad_t ccw;
-    bool pressed;
-} st3m_captouch_petal_top_t;
-
-typedef struct {
-    st3m_captouch_petal_pad_t base;
-    st3m_captouch_petal_pad_t tip;
-    bool pressed;
-} st3m_captouch_petal_bottom_t;
-
-typedef struct {
-    flow3r_bsp_captouch_state_t raw;
-
-    st3m_captouch_petal_top_t top[5];
-    st3m_captouch_petal_bottom_t bottom[5];
-} st3m_captouch_state_t;
-
-static SemaphoreHandle_t _mu = NULL;
-static st3m_captouch_state_t _state = {};
-static bool _request_calibration = false;
-static bool _calibrating = false;
-
-static inline void _pad_feed(st3m_captouch_petal_pad_t *pad, uint16_t data,
-                             bool top) {
-    ringbuffer_write(&pad->rb, data);
-    int32_t thres = top ? 8000 : 12000;
-    pad->pressed = data > thres;
-}
-
-static void _on_data(const flow3r_bsp_captouch_state_t *st) {
-    xSemaphoreTake(_mu, portMAX_DELAY);
-    memcpy(&_state, st, sizeof(flow3r_bsp_captouch_state_t));
-    for (size_t i = 0; i < 5; i++) {
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-        _pad_feed(&_state.top[i].tip, _state.raw.petals[i * 2].tip.raw, true);
-#else
-        _pad_feed(&_state.top[i].base, _state.raw.petals[i * 2].base.raw, true);
-#endif
-        _pad_feed(&_state.top[i].cw, _state.raw.petals[i * 2].cw.raw, true);
-        _pad_feed(&_state.top[i].ccw, _state.raw.petals[i * 2].ccw.raw, true);
-        _state.top[i].pressed =
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-            _state.top[i].tip.pressed ||
-#else
-            _state.top[i].base.pressed ||
-#endif
-            _state.top[i].cw.pressed || _state.top[i].ccw.pressed;
-    }
-    for (size_t i = 0; i < 5; i++) {
-        _pad_feed(&_state.bottom[i].base, _state.raw.petals[i * 2 + 1].base.raw,
-                  false);
-        _pad_feed(&_state.bottom[i].tip, _state.raw.petals[i * 2 + 1].tip.raw,
-                  false);
-        _state.bottom[i].pressed =
-            _state.bottom[i].base.pressed || _state.bottom[i].tip.pressed;
-    }
-    if (_request_calibration) {
-        _request_calibration = false;
-        flow3r_bsp_captouch_calibrate();
-    }
-    _calibrating = flow3r_bsp_captouch_calibrating();
-    xSemaphoreGive(_mu);
-}
-
-void captouch_init(void) {
-    assert(_mu == NULL);
-    _mu = xSemaphoreCreateMutex();
-    assert(_mu != NULL);
-
-    esp_err_t ret = flow3r_bsp_captouch_init(_on_data);
-    if (ret != ESP_OK) {
-        ESP_LOGE(TAG, "Captouch init failed: %s", esp_err_to_name(ret));
-    }
-}
-
-void captouch_print_debug_info(void) {
-    // Deprecated no-op, will be removed.
-}
-
-void captouch_read_cycle(void) {
-    // Deprecated no-op, will be removed.
-    // (now handled by interrupt)
-}
-
-void captouch_set_calibration_afe_target(uint16_t target) {
-    // Deprecated no-op, will be removed.
-}
-
-void captouch_force_calibration() {
-    xSemaphoreTake(_mu, portMAX_DELAY);
-    _request_calibration = true;
-    xSemaphoreGive(_mu);
-}
-
-uint8_t captouch_calibration_active() {
-    xSemaphoreTake(_mu, portMAX_DELAY);
-    bool res = _calibrating || _request_calibration;
-    xSemaphoreGive(_mu);
-    return res;
-}
-
-void read_captouch_ex(captouch_state_t *state) {
-    memset(state, 0, sizeof(captouch_state_t));
-    xSemaphoreTake(_mu, portMAX_DELAY);
-    for (size_t i = 0; i < 5; i++) {
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-        bool base = _state.top[i].tip.pressed;
-#else
-        bool base = _state.top[i].base.pressed;
-#endif
-        bool cw = _state.top[i].cw.pressed;
-        bool ccw = _state.top[i].ccw.pressed;
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-        state->petals[i * 2].pads.tip_pressed = base;
-#else
-        state->petals[i * 2].pads.base_pressed = base;
-#endif
-        state->petals[i * 2].pads.cw_pressed = cw;
-        state->petals[i * 2].pads.ccw_pressed = ccw;
-        state->petals[i * 2].pressed = base || cw || ccw;
-    }
-    for (size_t i = 0; i < 5; i++) {
-        bool base = _state.bottom[i].base.pressed;
-        bool tip = _state.bottom[i].tip.pressed;
-        state->petals[i * 2 + 1].pads.base_pressed = base;
-        state->petals[i * 2 + 1].pads.tip_pressed = tip;
-        state->petals[i * 2 + 1].pressed = base || tip;
-    }
-    xSemaphoreGive(_mu);
-}
-
-uint16_t read_captouch(void) {
-    xSemaphoreTake(_mu, portMAX_DELAY);
-    uint16_t res = 0;
-    for (size_t i = 0; i < 5; i++) {
-        if (_state.top[i].pressed) res |= (1 << (i * 2));
-    }
-    for (size_t i = 0; i < 5; i++) {
-        if (_state.bottom[i].pressed) res |= (1 << (i * 2 + 1));
-    }
-    xSemaphoreGive(_mu);
-    return res;
-}
-
-void captouch_set_petal_pad_threshold(uint8_t petal, uint8_t pad,
-                                      uint16_t thres) {
-    // Deprecated no-op, will be removed.
-}
-
-uint16_t captouch_get_petal_pad_raw(uint8_t petal, uint8_t pad) {
-    // Deprecated no-op, will be removed.
-    return 0;
-}
-
-uint16_t captouch_get_petal_pad_calib_ref(uint8_t petal, uint8_t pad) {
-    // Deprecated no-op, will be removed.
-    return 0;
-}
-
-uint16_t captouch_get_petal_pad(uint8_t petal, uint8_t pad) {
-    // Deprecated no-op, will be removed.
-    return 0;
-}
-
-int32_t captouch_get_petal_phi(uint8_t petal) {
-    bool top = (petal % 2) == 0;
-    if (top) {
-        size_t ix = petal / 2;
-        xSemaphoreTake(_mu, portMAX_DELAY);
-        int32_t left = ringbuffer_avg(&_state.top[ix].ccw.rb);
-        int32_t right = ringbuffer_avg(&_state.top[ix].cw.rb);
-        xSemaphoreGive(_mu);
-        return left - right;
-    } else {
-        return 0;
-    }
-}
-
-int32_t captouch_get_petal_rad(uint8_t petal) {
-    bool top = (petal % 2) == 0;
-    if (top) {
-#if defined(CONFIG_FLOW3R_HW_GEN_P3)
-        size_t ix = petal / 2;
-        xSemaphoreTake(_mu, portMAX_DELAY);
-        int32_t left = ringbuffer_avg(&_state.top[ix].ccw.rb);
-        int32_t right = ringbuffer_avg(&_state.top[ix].cw.rb);
-        int32_t tip = ringbuffer_avg(&_state.top[ix].tip.rb);
-        xSemaphoreGive(_mu);
-        return tip - (left + right) / 2;
-#else
-        size_t ix = petal / 2;
-        xSemaphoreTake(_mu, portMAX_DELAY);
-        int32_t left = ringbuffer_avg(&_state.top[ix].ccw.rb);
-        int32_t right = ringbuffer_avg(&_state.top[ix].cw.rb);
-        int32_t base = ringbuffer_avg(&_state.top[ix].base.rb);
-        xSemaphoreGive(_mu);
-        return (left + right) / 2 - base;
-#endif
-    } else {
-        size_t ix = (petal - 1) / 2;
-        xSemaphoreTake(_mu, portMAX_DELAY);
-        int32_t tip = ringbuffer_avg(&_state.bottom[ix].tip.rb);
-        int32_t base = ringbuffer_avg(&_state.bottom[ix].base.rb);
-        xSemaphoreGive(_mu);
-        return tip - base;
-    }
-}
diff --git a/components/badge23/include/badge23/captouch.h b/components/badge23/include/badge23/captouch.h
deleted file mode 100644
index d25e5ae85d2dd20787a5d45b18ddd0af516cda9f..0000000000000000000000000000000000000000
--- a/components/badge23/include/badge23/captouch.h
+++ /dev/null
@@ -1,162 +0,0 @@
-#pragma once
-#include <stdbool.h>
-#include <stdint.h>
-
-/* GENERAL INFORMATION
- *
- * petal index:     0 is the top petal above the USB-C jack, increases ccw so
- * that bottom petals are uneven and top petals even.
- *                  TODO: LEDs are indexed differently, this should be
- * harmonized in the impending API refactor.
- *
- * captouch data:   full uint16_t range per stage, higher values indicate touch.
- * pad index:       defined in captouch.c
- *
- *
- * IOU: the internals are still subject to major restructuring and are not
- * documented as of yet. will do once the data structures actually make sense
- * and are not duct tape upon duct tape.
- */
-
-/* polls data from both captouch chips and processes it, either by updating
- * the captouch data exposed to the user or running a calibration cycle.
- *
- * the captouch chips has updated their registers every 9.2ms, so the fn
- * should be called every 10ms or so.
- *
- * this will be replaced someday by an interrupt event triggered system,
- * but this would ideally already implement configuration switching to
- * optimize latency by grouping pads and to expose the unused pad which
- * is a nontrivial task for another day.
- */
-void captouch_read_cycle(void);
-
-/* the captouch chip can generate an "afe" offset in the analog domain before
- * the ADC to optimize the readout range. according to the datasheet this should
- * be in the middle of the 16bit delta sigma ADC range (without much reasoning
- * supplied), however we found that having a higher range is beneficial.
- *
- * the calibration cycle is optimizing the afe coefficients so that the output
- * of the "untouched" pads is close to the target set with this with this
- * function/
- *
- * default target: 6000, manufacturer recommendation: 32676
- */
-void captouch_set_calibration_afe_target(uint16_t target);
-
-/* starts a a calibration cycle which is intended to run when the captouch
- * pads are not being touched. the cycle consists of two parts:
- *
- * 1) optimize the afe coefficients (see captouch_set_calibration_afe_target)
- *
- * 2) with the new afe coefficients applied, average the readings in the
- * untouched state into a software calibration list  which is normally
- * subtracted from the captouch outputs. this makes up for the limited
- * resolution of the of the afe coefficient calibration.
- */
-void captouch_force_calibration();
-
-/* indicates if a calibration cycle is currently active. the readings for
- * captouch_read_cycle and captouch_get_petal_* during a calibration cycle.
- *
- * 1: calibration cycle active
- * 0: calibration cycle inactive
- */
-uint8_t captouch_calibration_active();
-
-typedef struct {
-    // Not all pads are present on all petals.
-    // Top petals have a base, cw and ccw pad.
-    // Bottom petlas have a base and a tip.
-
-    // Is the tip pressed down?
-    bool tip_pressed;
-    // Is the base pressed down?
-    bool base_pressed;
-    // Is the clockwise pad pressed down?
-    bool cw_pressed;
-    // Is the counter-clockwise pad pressed down?
-    bool ccw_pressed;
-} captouch_pad_state_t;
-
-typedef struct {
-    captouch_pad_state_t pads;
-    // Are any of the pads pressed down?
-    bool pressed;
-} captouch_petal_state_t;
-
-typedef struct {
-    captouch_petal_state_t petals[10];
-} captouch_state_t;
-
-/* Extened/new API for reading captouch state. Allows for access to individual
- * pad data.
- *
- * Likely to evolce into the new st3m api for captouch.
- */
-void read_captouch_ex(captouch_state_t *state);
-
-/* returns uint16_t which encodes each petal "touched" state as the bit
- * corresponding to the petal index. "touched" is determined by checking if
- * any of the pads belonging to the petal read a value higher than their
- * calibration value plus their threshold (see captouch_set_petal_pad_threshold)
- */
-uint16_t read_captouch(void);
-
-/* sets threshold for a petal pad above which read_captouch considers a pad as
- * touched.
- *
- * petal: petal index
- * pad:   pad index
- * thres: threshold value
- */
-void captouch_set_petal_pad_threshold(uint8_t petal, uint8_t pad,
-                                      uint16_t thres);
-
-/* returns last read captouch value from a petal pad without subtracting its
- * calibration reference. typically only needed for debugging.
- *
- * petal: petal index
- * pad:   pad index
- */
-uint16_t captouch_get_petal_pad_raw(uint8_t petal, uint8_t pad);
-
-/* returns calibration reference for a petal pad.
- *
- * petal: petal index
- * pad:   pad index
- */
-uint16_t captouch_get_petal_pad_calib_ref(uint8_t petal, uint8_t pad);
-
-/* returns calibrated value from petal. clamps below 0.
- *
- * petal: petal index
- * pad:   pad index
- */
-uint16_t captouch_get_petal_pad(uint8_t petal, uint8_t pad);
-
-/* estimates the azimuthal finger position on a petal in arbitrary units.
- * returns 0 if hardware doesn't support this.
- *
- * petal: petal index
- * pad:   pad index
- */
-int32_t captouch_get_petal_phi(uint8_t petal);
-
-/* estimates the radial finger position on a petal in arbitrary units.
- * returns 0 if hardware doesn't support this.
- *
- * petal: petal index
- * pad:   pad index
- */
-int32_t captouch_get_petal_rad(uint8_t petal);
-
-/* configures the captouch chips, prefills internal structs etc.
- */
-void captouch_init(void);
-
-/* TODO: didn't look into what it does, never used it, just copied it from
- * the hardware verification firmware along with the rest. didn't break it
- * _intentionally_ but never tested it either.
- */
-void captouch_print_debug_info(void);
diff --git a/components/st3m/CMakeLists.txt b/components/st3m/CMakeLists.txt
index c894750319ebf70e23a392867b86ca6fcc3a51f7..4b8d4f8d3a1f7f034e9fc9a011a6e32bc135566c 100644
--- a/components/st3m/CMakeLists.txt
+++ b/components/st3m/CMakeLists.txt
@@ -15,6 +15,8 @@ idf_component_register(
         st3m_usb.c
         st3m_console.c
         st3m_mode.c
+        st3m_captouch.c
+        st3m_ringbuffer.c
     INCLUDE_DIRS
         .
     REQUIRES
diff --git a/components/st3m/st3m_board_startup.c b/components/st3m/st3m_board_startup.c
index 808e9d7ea0cea3abc33a8ccb364fd0428e21e819..659c7719552cda346532d60677bcd3dd5b644586 100644
--- a/components/st3m/st3m_board_startup.c
+++ b/components/st3m/st3m_board_startup.c
@@ -1,6 +1,7 @@
 #include "bl00mbox.h"
 #include "flow3r_bsp.h"
 #include "st3m_audio.h"
+#include "st3m_captouch.h"
 #include "st3m_console.h"
 #include "st3m_fs.h"
 #include "st3m_gfx.h"
@@ -67,6 +68,7 @@ void st3m_board_startup(void) {
     st3m_mode_update_display(NULL);
     st3m_leds_init();
     st3m_io_init();
+    st3m_captouch_init();
 
     st3m_mode_set(st3m_mode_kind_starting, "micropython");
     st3m_mode_update_display(NULL);
diff --git a/components/st3m/st3m_captouch.c b/components/st3m/st3m_captouch.c
new file mode 100644
index 0000000000000000000000000000000000000000..bf25555aa4af91f14205e95197c01127235c6332
--- /dev/null
+++ b/components/st3m/st3m_captouch.c
@@ -0,0 +1,118 @@
+#include "st3m_captouch.h"
+
+#include "esp_err.h"
+#include "esp_log.h"
+
+#include "flow3r_bsp_captouch.h"
+#include "sdkconfig.h"
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+#include "freertos/task.h"
+
+#include <string.h>
+
+static const char *TAG = "st3m-captouch";
+
+static SemaphoreHandle_t _mu = NULL;
+static st3m_captouch_state_t _state = {};
+static bool _request_calibration = false;
+static bool _calibrating = false;
+
+static inline void _pad_feed(st3m_petal_pad_state_t *pad, uint16_t data,
+                             bool top) {
+    ringbuffer_write(&pad->rb, data);
+    int32_t thres = top ? 8000 : 12000;
+    pad->pressed = data > thres;
+}
+
+static inline void _petal_process(st3m_petal_state_t *petal, bool top) {
+    if (top) {
+        petal->pressed =
+            petal->base.pressed || petal->ccw.pressed || petal->cw.pressed;
+        int32_t left = ringbuffer_avg(&petal->ccw.rb);
+        int32_t right = ringbuffer_avg(&petal->cw.rb);
+        int32_t base = ringbuffer_avg(&petal->base.rb);
+        petal->pos_distance = (left + right) / 2 - base;
+        petal->pos_angle = left - right;
+#if defined(CONFIG_FLOW3R_HW_GEN_P3)
+        petal->pos_distance = -petal->pos_distance;
+#endif
+    } else {
+        petal->pressed = petal->base.pressed || petal->tip.pressed;
+        int32_t base = ringbuffer_avg(&petal->base.rb);
+        int32_t tip = ringbuffer_avg(&petal->tip.rb);
+        petal->pos_distance = tip - base;
+        petal->pos_angle = 0;
+    }
+}
+
+static void _on_data(const flow3r_bsp_captouch_state_t *st) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+
+    for (size_t ix = 0; ix < 10; ix++) {
+        bool top = (ix % 2) == 0;
+
+        if (top) {
+#if defined(CONFIG_FLOW3R_HW_GEN_P3)
+            // Hack for P3 badges, pretend tip is base.
+            _pad_feed(&_state.petals[ix].base, st->petals[ix].tip.raw, true);
+#else
+            _pad_feed(&_state.petals[ix].base, st->petals[ix].base.raw, true);
+#endif
+            _pad_feed(&_state.petals[ix].cw, st->petals[ix].cw.raw, true);
+            _pad_feed(&_state.petals[ix].ccw, st->petals[ix].ccw.raw, true);
+            _petal_process(&_state.petals[ix], true);
+        } else {
+            _pad_feed(&_state.petals[ix].base, st->petals[ix].base.raw, false);
+            _pad_feed(&_state.petals[ix].tip, st->petals[ix].tip.raw, false);
+            _petal_process(&_state.petals[ix], false);
+        }
+    }
+
+    if (_request_calibration) {
+        _request_calibration = false;
+        flow3r_bsp_captouch_calibrate();
+    }
+    _calibrating = flow3r_bsp_captouch_calibrating();
+    xSemaphoreGive(_mu);
+}
+
+void st3m_captouch_init(void) {
+    assert(_mu == NULL);
+    _mu = xSemaphoreCreateMutex();
+    assert(_mu != NULL);
+
+    esp_err_t ret = flow3r_bsp_captouch_init(_on_data);
+    if (ret != ESP_OK) {
+        ESP_LOGE(TAG, "Captouch init failed: %s", esp_err_to_name(ret));
+    }
+}
+
+bool st3m_captouch_calibrating(void) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    bool res = _calibrating || _request_calibration;
+    xSemaphoreGive(_mu);
+    return res;
+}
+
+void st3m_captouch_request_calibration(void) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    _request_calibration = true;
+    xSemaphoreGive(_mu);
+}
+
+void st3m_captouch_get_all(st3m_captouch_state_t *dest) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    memcpy(dest, &_state, sizeof(_state));
+    xSemaphoreGive(_mu);
+}
+
+void st3m_captouch_get_petal(st3m_petal_state_t *dest, uint8_t petal_ix) {
+    if (petal_ix > 9) {
+        petal_ix = 9;
+    }
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    memcpy(dest, &_state.petals[petal_ix], sizeof(_state.petals[petal_ix]));
+    xSemaphoreGive(_mu);
+}
\ No newline at end of file
diff --git a/components/st3m/st3m_captouch.h b/components/st3m/st3m_captouch.h
new file mode 100644
index 0000000000000000000000000000000000000000..9c1184da3a9ddcaf24f8e0753ff56d4d64d84a1b
--- /dev/null
+++ b/components/st3m/st3m_captouch.h
@@ -0,0 +1,112 @@
+#pragma once
+
+// GENERAL INFORMATION
+//
+// Geometry:
+//
+// The badge has 10 petals, 5 top petals (on the top PCB) and 5 bottom petals
+// (on the bottom PCB). Each petal is made up of pads. Top petals have 3 pads,
+// bottom petals have 2 pads.
+//
+// Every pad on a petal has a kind. The kind infidicates the relative position
+// of the pad within the petal.
+//
+//   tip: pad closest to the outside of the badge
+//   base: pad closest to the inside of the badge
+//   cw: pad going clockwise around the badge
+//   ccw: pad going counter-clockwise around the badge
+//
+// Top petals have base, cw, ccw pads. Bottom petals have tip, base pads.
+//
+// NOTE: if you have a 'proto3' badge, it has a slightly different top petal
+// layout (tip, cw, ccw). This API pretends base == tip in this case.
+//
+// Petals are numbered. 0 is the top petal above the USB-C jack, increases
+// counter-clockwise so that bottom petals are uneven and top petals even.
+//
+// Processing:
+//
+// Every time new capacitive touch data is available, a 'raw' value is extracted
+// for each pad. This value is then used to calcualte the following information:
+//
+//  1. Per-pad touch: if the raw value exceeds some threshold, the pad is
+//     considered to be touched.
+//  2. Per-petal touch: if any of a pad's petals is considered to be touched,
+//     the petal is also considered to be touched.
+//  3. Per-petal position: petals allow for estimting a polar coordinate of
+//     touch. Top petals have two degrees of freedom, bottom petals have a
+//     single degree of freedom (distance from center).
+
+#include "st3m_ringbuffer.h"
+
+// NOTE: keep the enum definitions below in-sync with flow3r_bsp_captouch.h, as
+// they are converted by numerical value internally.
+
+// One of the four possible touch points (pads) on a petal. Top petals have
+// base/cw/ccw. Bottom petals have base/tip.
+typedef enum {
+    // Pad away from centre of badge.
+    st3m_petal_pad_tip = 0,
+    // Pad going counter-clockwise around the badge.
+    st3m_petal_pad_ccw = 1,
+    // Pad going clockwise around the badge.
+    st3m_petal_pad_cw = 2,
+    // Pad going towards the centre of the badge.
+    st3m_petal_pad_base = 3,
+} st3m_petal_pad_kind_t;
+
+// Each petal can be either top or bottom (that is, on the bottom PCB or top
+// PCB).
+typedef enum {
+    // Petal on the top layer. Has base, cw, ccw pads.
+    st3m_petal_top = 0,
+    // petal on the bottom layer. Has base and tip fields.
+    st3m_petal_bottom = 1,
+} st3m_petal_kind_t;
+
+// State of capacitive touch for a petal's pad.
+typedef struct {
+    // Raw data ringbuffer.
+    st3m_ringbuffer_t rb;
+    // Whether the pad is currently being touched. Calculated from ringbuffer
+    // data.
+    bool pressed;
+} st3m_petal_pad_state_t;
+
+// State of capacitive touch for a petal.
+typedef struct {
+    // Is this a top or bottom petal?
+    st3m_petal_kind_t kind;
+
+    // Valid if top or bottom.
+    st3m_petal_pad_state_t base;
+    // Valid if bottom.
+    st3m_petal_pad_state_t tip;
+    // Valid if top.
+    st3m_petal_pad_state_t cw;
+    // Valid if top.
+    st3m_petal_pad_state_t ccw;
+
+    // Whether the petal is currently being touched. Calculated from individual
+    // pad data.
+    bool pressed;
+
+    // Touch position on petal, calculated from pad data.
+    //
+    // Arbitrary units around (0, 0).
+    // TODO(q3k): normalize and document.
+    float pos_distance;
+    float pos_angle;
+} st3m_petal_state_t;
+
+typedef struct {
+    // Petal 0 is a top petal next to the USB socket. Then, all other petals
+    // follow counter-clockwise.
+    st3m_petal_state_t petals[10];
+} st3m_captouch_state_t;
+
+void st3m_captouch_init(void);
+bool st3m_captouch_calibrating(void);
+void st3m_captouch_request_calibration(void);
+void st3m_captouch_get_all(st3m_captouch_state_t *dest);
+void st3m_captouch_get_petal(st3m_petal_state_t *dest, uint8_t petal_ix);
\ No newline at end of file
diff --git a/components/st3m/st3m_io.c b/components/st3m/st3m_io.c
index 7c78b0d5131faf441f296e66e4fc245825b083dd..10e14887c1455c6e310b8b41333cdb2b0425298a 100644
--- a/components/st3m/st3m_io.c
+++ b/components/st3m/st3m_io.c
@@ -113,16 +113,10 @@ uint8_t st3m_io_badge_link_enable(uint8_t pin_mask) {
     return st3m_io_badge_link_set(pin_mask, 1);
 }
 
-// Imports from badge23, will be removed once captouch gets moved to bsp/st3m.
-void captouch_read_cycle(void);
-void captouch_init(void);
-void captouch_force_calibration(void);
-
 static void _task(void *data) {
     TickType_t last_wake = xTaskGetTickCount();
     while (1) {
         vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(10));  // 100 Hz
-        captouch_read_cycle();
         _update_button_state();
     }
 }
@@ -135,8 +129,6 @@ void st3m_io_init(void) {
         }
     }
 
-    captouch_init();
-    captouch_force_calibration();
     st3m_io_badge_link_disable(BADGE_LINK_PIN_MASK_ALL);
 
     xTaskCreate(&_task, "io", 4096, NULL, configMAX_PRIORITIES - 1, NULL);
diff --git a/components/st3m/st3m_ringbuffer.c b/components/st3m/st3m_ringbuffer.c
new file mode 100644
index 0000000000000000000000000000000000000000..b32fd3125d09022cc9e3aa3edb84cfc96c2afe18
--- /dev/null
+++ b/components/st3m/st3m_ringbuffer.c
@@ -0,0 +1,39 @@
+#include "st3m_ringbuffer.h"
+
+void ringbuffer_write(st3m_ringbuffer_t *rb, uint16_t data) {
+    rb->buf[rb->write_ix] = data;
+    rb->write_ix++;
+    if (rb->write_ix >= ringbuffer_size(rb)) {
+        rb->write_ix = 0;
+        rb->wrapped = true;
+    }
+}
+
+uint16_t ringbuffer_avg(const st3m_ringbuffer_t *rb) {
+    int32_t res = 0;
+    if (rb->wrapped) {
+        for (size_t i = 0; i < ringbuffer_size(rb); i++) {
+            res += rb->buf[i];
+        }
+        res /= ringbuffer_size(rb);
+        return res;
+    }
+    if (rb->write_ix == 0) {
+        return 0;
+    }
+    for (size_t i = 0; i < rb->write_ix; i++) {
+        res += rb->buf[i];
+    }
+    res /= rb->write_ix;
+    return res;
+}
+
+uint16_t ringbuffer_last(const st3m_ringbuffer_t *rb) {
+    if (rb->write_ix == 0) {
+        if (rb->wrapped) {
+            return rb->buf[ringbuffer_size(rb) - 1];
+        }
+        return 0;
+    }
+    return rb->buf[rb->write_ix - 1];
+}
diff --git a/components/st3m/st3m_ringbuffer.h b/components/st3m/st3m_ringbuffer.h
new file mode 100644
index 0000000000000000000000000000000000000000..4fc97cf2d67fe8f2b5284eb6ca09c09403d0b0df
--- /dev/null
+++ b/components/st3m/st3m_ringbuffer.h
@@ -0,0 +1,29 @@
+#pragma once
+
+// A simple, non-concurrent ringbuffer.
+//
+// TODO(q3k): make generic and use from scope code
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct {
+    size_t write_ix;
+    bool wrapped;
+    uint16_t buf[4];
+} st3m_ringbuffer_t;
+
+// Size of ringbuffer, in elements.
+inline size_t ringbuffer_size(const st3m_ringbuffer_t *rb) {
+    return sizeof(rb->buf) / sizeof(uint16_t);
+}
+
+// Write to ringbuffer.
+void ringbuffer_write(st3m_ringbuffer_t *rb, uint16_t data);
+
+// Get ringbuffer average (mean), or 0 if no values have yet been inserted.
+uint16_t ringbuffer_avg(const st3m_ringbuffer_t *rb);
+
+// Get last inserted value, or 0 if no value have yet been inserted.
+uint16_t ringbuffer_last(const st3m_ringbuffer_t *rb);
\ No newline at end of file
diff --git a/python_payload/apps/cap_touch_demo.py b/python_payload/apps/cap_touch_demo.py
index 2f9e426bb987c4e1e65adee542065b6d50234a4d..b7006f60a39ae882dcdc58a4131b197d9a66ff6e 100644
--- a/python_payload/apps/cap_touch_demo.py
+++ b/python_payload/apps/cap_touch_demo.py
@@ -7,7 +7,8 @@ import cmath
 import math
 import time
 
-import hardware
+import captouch
+
 from st3m import utils, application, ui, event
 
 
@@ -50,19 +51,15 @@ class CapTouchDemo(application.Application):
 
     def main_foreground(self):
         self.dots = []
+        cps = captouch.read()
         for i in range(10):
-            size = (hardware.get_captouch(i) * 4) + 4
-            size += int(
-                max(
-                    0,
-                    sum(
-                        [hardware.captouch_get_petal_pad(i, x) for x in range(0, 3 + 1)]
-                    )
-                    / 8000,
-                )
-            )
-            x = 70 + (hardware.captouch_get_petal_rad(i) / 1000)
-            x += (hardware.captouch_get_petal_phi(i) / 600) * 1j
+            petal = cps.petals[i]
+            (rad, phi) = petal.position
+            size = 4
+            if petal.pressed:
+                size += 4
+            x = 70 + (rad / 1000)
+            x += (phi / 600) * 1j
             rot = cmath.exp(2j * math.pi * i / 10)
             x = x * rot
 
@@ -78,7 +75,7 @@ class CapTouchDemo(application.Application):
 
     def do_autocalib(self, data):
         log.info("Performing captouch autocalibration")
-        hardware.captouch_autocalib()
+        captouch.calibration_request()
         self.last_calib = 50
 
 
diff --git a/python_payload/apps/harmonic_demo.py b/python_payload/apps/harmonic_demo.py
index 9301bea890c28e7aa919d0011f826947c3a11676..f62e963e4f0a21c8a6a7fac7ae2b16d47bf38007 100644
--- a/python_payload/apps/harmonic_demo.py
+++ b/python_payload/apps/harmonic_demo.py
@@ -1,5 +1,6 @@
 from bl00mbox import tinysynth
 from hardware import *
+import captouch
 import leds
 
 
@@ -65,8 +66,9 @@ class HarmonicApp(Application):
     def main_foreground(self):
         if self.color_intensity > 0:
             self.color_intensity -= self.color_intensity / 20
+        cts = captouch.read()
         for i in range(10):
-            if get_captouch(i):
+            if cts.petals[i].pressed:
                 if i % 2:
                     k = int((i - 1) / 2)
                     self._set_chord(k)
diff --git a/python_payload/apps/melodic_demo.py b/python_payload/apps/melodic_demo.py
index 7b37378bc0aea44e59810bb7b97dcbb5c2ab9a9e..ce7dce09cd907bcf28fea27e14a81f59aaeed56d 100644
--- a/python_payload/apps/melodic_demo.py
+++ b/python_payload/apps/melodic_demo.py
@@ -1,5 +1,6 @@
 from bl00mbox import tinysynth
 from hardware import *
+import captouch
 import leds
 
 octave = 0
@@ -40,8 +41,9 @@ def run():
     global scale
     global octave
     global synths
+    cts = captouch.read()
     for i in range(10):
-        if get_captouch(i):
+        if cts.petals[i].pressed:
             if i == 4:
                 octave = -1
                 adjust_playing_field_to_octave()
diff --git a/python_payload/main.py b/python_payload/main.py
index f2b0b0ac6a51c6af419d28f362bcda60a58cd66d..ff80588fea09617c2f9025c0c20ea39a74ed1ba3 100644
--- a/python_payload/main.py
+++ b/python_payload/main.py
@@ -22,10 +22,10 @@ ts_end = time.time()
 log.info(f"boot took {ts_end-ts_start} seconds")
 
 # TODO persistent settings
-from st3m.system import hardware, audio
+from st3m.system import audio, captouch
 
 log.info("calibrating captouch, reset volume")
-hardware.captouch_autocalib()
+captouch.calibration_request()
 audio.set_volume_dB(0)
 
 # Start default app
diff --git a/python_payload/mypystubs/captouch.pyi b/python_payload/mypystubs/captouch.pyi
index 1b8822d50dfc0ac389b898d2a048bc5d09fa3f9c..99edb701794e2b1ca6f2d90fb10df11606cbc62e 100644
--- a/python_payload/mypystubs/captouch.pyi
+++ b/python_payload/mypystubs/captouch.pyi
@@ -58,6 +58,20 @@ class CaptouchPetalState(Protocol):
         State of individual pads of the petal.
         """
         ...
+    @property
+    def position(seld) -> Tuple[int, int]:
+        """
+        Polar coordinates of touch on petal in the form of a (distance, angle)
+        tuple.
+
+        The units are arbitrary, but centered around (0, 0).
+
+        An increase in distance means the touch is further away from the centre
+        of the badge.
+
+        An increase in angle means the touch is more counter-clockwise.
+        """
+        ...
 
 class CaptouchState(Protocol):
     """
@@ -89,3 +103,10 @@ def calibration_active() -> bool:
     Returns true if the captouch system is current recalibrating.
     """
     ...
+
+def calibration_request() -> None:
+    """
+    Attempts to start calibration of captouch controllers. No-op if a
+    calibration is already active.
+    """
+    ...
diff --git a/python_payload/mypystubs/hardware.pyi b/python_payload/mypystubs/hardware.pyi
index 7134d33247efa91e4c9406854d5e2f99357a1172..e6aab870e8d8385648cb89201c571abf521ad9bd 100644
--- a/python_payload/mypystubs/hardware.pyi
+++ b/python_payload/mypystubs/hardware.pyi
@@ -12,15 +12,6 @@ def usb_connected() -> bool: ...
 def usb_console_active() -> bool: ...
 def version() -> str: ...
 def i2c_scan() -> list[int]: ...
-def captouch_calibration_active() -> int: ...
-def get_captouch(ix: int) -> bool: ...
-def captouch_get_petal_pad_raw(petal: int, pad: int) -> int: ...
-def captouch_get_petal_pad(petal: int, pad: int) -> int: ...
-def captouch_get_petal_rad(petal: int) -> int: ...
-def captouch_get_petal_phi(petal: int) -> int: ...
-def captouch_set_petal_pad_threshold(petal: int, pad: int, thres: int) -> None: ...
-def captouch_autocalib() -> None: ...
-def captouch_set_calibration_afe_target(target: int) -> None: ...
 def menu_button_get() -> int: ...
 def application_button_get() -> int: ...
 def left_button_get() -> int: ...
diff --git a/python_payload/st3m/event.py b/python_payload/st3m/event.py
index 9d5429eb162803a2da701c614c385c3e40a52cb3..e48505e3a69408b4534c47f2c05db4dd04041e56 100644
--- a/python_payload/st3m/event.py
+++ b/python_payload/st3m/event.py
@@ -3,7 +3,7 @@ from st3m import logging
 log = logging.Log(__name__, level=logging.INFO)
 log.info("import")
 
-from st3m.system import hardware
+from st3m.system import hardware, captouch
 
 import kernel
 import time
@@ -124,14 +124,17 @@ class Engine:
         )
 
         # captouch
+        cps = captouch.read()
         for i in range(0, 10):
+            petal = cps.petals[i]
+            (radius, angle) = petal.position
             input_state.append(
                 {
                     "type": "captouch",
                     "index": i,
-                    "value": hardware.get_captouch(i),
-                    "radius": hardware.captouch_get_petal_rad(i),
-                    "angle": hardware.captouch_get_petal_phi(i),
+                    "value": petal.pressed,
+                    "radius": radius,
+                    "angle": angle,
                 }
             )
 
diff --git a/python_payload/st3m/system/__init__.py b/python_payload/st3m/system/__init__.py
index 3b3aaf1b374dae9c7951e51d3a7f641a6c1a6c76..f2512322c2b05e13b73849779ca80d1a941cfcfd 100644
--- a/python_payload/st3m/system/__init__.py
+++ b/python_payload/st3m/system/__init__.py
@@ -1,4 +1,5 @@
 import hardware as _hardware
+import captouch as _captouch
 
 
 class NamedObject:
@@ -36,4 +37,5 @@ except ModuleNotFoundError:
 
 
 hardware = _hardware
+captouch = _captouch
 audio = _audio
diff --git a/sim/fakes/bl00mbox.py b/sim/fakes/bl00mbox.py
index 7be5719b27f48a71d36f1f2c1393a3e0004d7b59..c9c644ecfd11f3c62ddbc7c7fdebadb6e707e3b0 100644
--- a/sim/fakes/bl00mbox.py
+++ b/sim/fakes/bl00mbox.py
@@ -1,5 +1,5 @@
 class tinysynth:
-    def __init__(self, a, b):
+    def __init__(self, a):
         pass
 
     def decay(self, a):
@@ -13,3 +13,6 @@ class tinysynth:
 
     def start(self):
         pass
+
+    def sustain(self, a):
+        pass
diff --git a/sim/fakes/captouch.py b/sim/fakes/captouch.py
index a7c4f3d46366968ad22ebc45edaccf87905e172a..2deb829a6aab23fdfc97c28cb0d590f569e774b4 100644
--- a/sim/fakes/captouch.py
+++ b/sim/fakes/captouch.py
@@ -29,6 +29,7 @@ class CaptouchPetalState:
     def __init__(self, ix: int, pads: CaptouchPetalPadsState):
         self._pads = pads
         self._ix = ix
+        self.position = (0, 0)
 
     @property
     def pressed(self) -> bool:
@@ -85,3 +86,7 @@ def read() -> CaptouchState:
 
 def calibration_active() -> bool:
     return False
+
+
+def calibration_request() -> None:
+    return
diff --git a/sim/fakes/hardware.py b/sim/fakes/hardware.py
index 4afe93921058c0facc39d19027ae112637f58780..4b8d78381e6f605e34cab4941b0458a308123442 100644
--- a/sim/fakes/hardware.py
+++ b/sim/fakes/hardware.py
@@ -422,14 +422,6 @@ def init_done():
     return True
 
 
-def captouch_autocalib():
-    pass
-
-
-def captouch_calibration_active():
-    return False
-
-
 import ctx
 
 
@@ -534,25 +526,6 @@ def menu_button_get_left():
     return menu_button_left
 
 
-def get_captouch(a):
-    _sim.process_events()
-    _sim.render_gui_lazy()
-    return _sim.petals.state_for_petal(a)
-
-
-# TODO(iggy/q3k do proper positional captouch)
-def captouch_get_petal_rad(a):
-    return 0
-
-
-def captouch_get_petal_phi(a):
-    return 0
-
-
-def captouch_get_petal_pad(i, x):
-    return 0
-
-
 def freertos_sleep(ms):
     import _time
 
diff --git a/usermodule/mp_captouch.c b/usermodule/mp_captouch.c
index 4203140a2e88fc89da0e95f1c42e2bdb43715c6b..21d26b048b3611778524d54b506da0c9177be7c6 100644
--- a/usermodule/mp_captouch.c
+++ b/usermodule/mp_captouch.c
@@ -1,16 +1,23 @@
 #include "py/builtin.h"
 #include "py/runtime.h"
 
-#include "badge23/captouch.h"
+#include "st3m_captouch.h"
 
 #include <string.h>
 
 STATIC mp_obj_t mp_captouch_calibration_active(void) {
-    return mp_obj_new_int(captouch_calibration_active());
+    return mp_obj_new_int(st3m_captouch_calibrating());
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_active_obj,
                                  mp_captouch_calibration_active);
 
+STATIC mp_obj_t mp_captouch_calibration_request(void) {
+    st3m_captouch_request_calibration();
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_request_obj,
+                                 mp_captouch_calibration_request);
+
 typedef struct {
     mp_obj_base_t base;
     mp_obj_t petal;
@@ -30,7 +37,7 @@ const mp_obj_type_t captouch_petal_state_type;
 typedef struct {
     mp_obj_base_t base;
     mp_obj_t petals;
-    captouch_state_t underlying;
+    st3m_captouch_state_t underlying;
 } mp_captouch_state_t;
 
 const mp_obj_type_t captouch_state_type;
@@ -44,28 +51,28 @@ STATIC void mp_captouch_petal_pads_state_attr(mp_obj_t self_in, qstr attr,
 
     mp_captouch_petal_state_t *petal = MP_OBJ_TO_PTR(self->petal);
     mp_captouch_state_t *captouch = MP_OBJ_TO_PTR(petal->captouch);
-    captouch_petal_state_t *state = &captouch->underlying.petals[petal->ix];
+    st3m_petal_state_t *state = &captouch->underlying.petals[petal->ix];
     bool top = (petal->ix % 2) == 0;
 
     if (top) {
         switch (attr) {
             case MP_QSTR_base:
-                dest[0] = mp_obj_new_bool(state->pads.base_pressed);
+                dest[0] = mp_obj_new_bool(state->base.pressed);
                 break;
             case MP_QSTR_cw:
-                dest[0] = mp_obj_new_bool(state->pads.cw_pressed);
+                dest[0] = mp_obj_new_bool(state->cw.pressed);
                 break;
             case MP_QSTR_ccw:
-                dest[0] = mp_obj_new_bool(state->pads.ccw_pressed);
+                dest[0] = mp_obj_new_bool(state->ccw.pressed);
                 break;
         }
     } else {
         switch (attr) {
             case MP_QSTR_tip:
-                dest[0] = mp_obj_new_bool(state->pads.tip_pressed);
+                dest[0] = mp_obj_new_bool(state->tip.pressed);
                 break;
             case MP_QSTR_base:
-                dest[0] = mp_obj_new_bool(state->pads.base_pressed);
+                dest[0] = mp_obj_new_bool(state->base.pressed);
                 break;
         }
     }
@@ -79,7 +86,7 @@ STATIC void mp_captouch_petal_state_attr(mp_obj_t self_in, qstr attr,
     }
 
     mp_captouch_state_t *captouch = MP_OBJ_TO_PTR(self->captouch);
-    captouch_petal_state_t *state = &captouch->underlying.petals[self->ix];
+    st3m_petal_state_t *state = &captouch->underlying.petals[self->ix];
 
     bool top = (self->ix % 2) == 0;
 
@@ -96,6 +103,14 @@ STATIC void mp_captouch_petal_state_attr(mp_obj_t self_in, qstr attr,
         case MP_QSTR_pads:
             dest[0] = self->pads;
             break;
+        case MP_QSTR_position: {
+            mp_obj_t items[2] = {
+                mp_obj_new_int(state->pos_distance),
+                mp_obj_new_int(state->pos_angle),
+            };
+            dest[0] = mp_obj_new_tuple(2, items);
+            break;
+        }
     }
 }
 
@@ -123,10 +138,10 @@ MP_DEFINE_CONST_OBJ_TYPE(captouch_petal_state_type, MP_QSTR_CaptouchPetalState,
 MP_DEFINE_CONST_OBJ_TYPE(captouch_state_type, MP_QSTR_CaptouchState,
                          MP_TYPE_FLAG_NONE, attr, mp_captouch_state_attr);
 
-STATIC mp_obj_t mp_captouch_state_new(const captouch_state_t *underlying) {
+STATIC mp_obj_t mp_captouch_state_new(const st3m_captouch_state_t *underlying) {
     mp_captouch_state_t *captouch = m_new_obj(mp_captouch_state_t);
     captouch->base.type = &captouch_state_type;
-    memcpy(&captouch->underlying, underlying, sizeof(captouch_state_t));
+    memcpy(&captouch->underlying, underlying, sizeof(st3m_captouch_state_t));
 
     captouch->petals = mp_obj_new_list(0, NULL);
     for (int i = 0; i < 10; i++) {
@@ -148,8 +163,8 @@ STATIC mp_obj_t mp_captouch_state_new(const captouch_state_t *underlying) {
 }
 
 STATIC mp_obj_t mp_captouch_read(void) {
-    captouch_state_t st;
-    read_captouch_ex(&st);
+    st3m_captouch_state_t st;
+    st3m_captouch_get_all(&st);
     return mp_captouch_state_new(&st);
 }
 
@@ -158,7 +173,9 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_read_obj, mp_captouch_read);
 STATIC const mp_rom_map_elem_t globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_captouch_read_obj) },
     { MP_ROM_QSTR(MP_QSTR_calibration_active),
-      MP_ROM_PTR(&mp_captouch_calibration_active_obj) }
+      MP_ROM_PTR(&mp_captouch_calibration_active_obj) },
+    { MP_ROM_QSTR(MP_QSTR_calibration_request),
+      MP_ROM_PTR(&mp_captouch_calibration_request_obj) },
 };
 
 STATIC MP_DEFINE_CONST_DICT(globals, globals_table);
diff --git a/usermodule/mp_hardware.c b/usermodule/mp_hardware.c
index 0c7cce149c3a485e5d95506ae902f8cbfd81dcdd..7f30eafeab0f1a8941efe710d9988ef599c41968 100644
--- a/usermodule/mp_hardware.c
+++ b/usermodule/mp_hardware.c
@@ -10,8 +10,6 @@
 #include "py/mphal.h"
 #include "py/runtime.h"
 
-#include "badge23/captouch.h"
-
 #include "flow3r_bsp.h"
 #include "st3m_console.h"
 #include "st3m_gfx.h"
@@ -26,12 +24,6 @@
 
 mp_obj_t mp_ctx_from_ctx(Ctx *ctx);
 
-STATIC mp_obj_t mp_captouch_calibration_active(void) {
-    return mp_obj_new_int(captouch_calibration_active());
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_active_obj,
-                                 mp_captouch_calibration_active);
-
 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);
@@ -40,80 +32,6 @@ STATIC mp_obj_t mp_display_set_backlight(mp_obj_t percent_in) {
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_display_set_backlight_obj,
                                  mp_display_set_backlight);
 
-STATIC mp_obj_t mp_get_captouch(size_t n_args, const mp_obj_t *args) {
-    uint16_t captouch = read_captouch();
-    uint16_t pad = mp_obj_get_int(args[0]);
-    uint8_t output = (captouch >> pad) & 1;
-
-    return mp_obj_new_int(output);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_get_captouch_obj, 1, 2,
-                                           mp_get_captouch);
-
-STATIC mp_obj_t mp_captouch_get_petal_pad_raw(mp_obj_t petal_in,
-                                              mp_obj_t pad_in) {
-    uint8_t petal = mp_obj_get_int(petal_in);
-    uint8_t pad = mp_obj_get_int(pad_in);
-    uint16_t output = captouch_get_petal_pad_raw(petal, pad);
-
-    return mp_obj_new_int(output);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_captouch_get_petal_pad_raw_obj,
-                                 mp_captouch_get_petal_pad_raw);
-
-STATIC mp_obj_t mp_captouch_get_petal_pad(mp_obj_t petal_in, mp_obj_t pad_in) {
-    uint8_t petal = mp_obj_get_int(petal_in);
-    uint8_t pad = mp_obj_get_int(pad_in);
-    return mp_obj_new_int(captouch_get_petal_pad(petal, pad));
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_captouch_get_petal_pad_obj,
-                                 mp_captouch_get_petal_pad);
-
-STATIC mp_obj_t mp_captouch_get_petal_rad(mp_obj_t petal_in) {
-    uint8_t petal = mp_obj_get_int(petal_in);
-    int32_t ret = captouch_get_petal_rad(petal);
-
-    return mp_obj_new_int(ret);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_captouch_get_petal_rad_obj,
-                                 mp_captouch_get_petal_rad);
-
-STATIC mp_obj_t mp_captouch_get_petal_phi(mp_obj_t petal_in) {
-    uint8_t petal = mp_obj_get_int(petal_in);
-    int32_t ret = captouch_get_petal_phi(petal);
-
-    return mp_obj_new_int(ret);
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_captouch_get_petal_phi_obj,
-                                 mp_captouch_get_petal_phi);
-
-STATIC mp_obj_t mp_captouch_set_petal_pad_threshold(mp_obj_t petal_in,
-                                                    mp_obj_t pad_in,
-                                                    mp_obj_t thres_in) {
-    uint8_t petal = mp_obj_get_int(petal_in);
-    uint8_t pad = mp_obj_get_int(pad_in);
-    uint16_t thres = mp_obj_get_int(thres_in);
-    captouch_set_petal_pad_threshold(petal, pad, thres);
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_3(mp_captouch_set_petal_pad_threshold_obj,
-                                 mp_captouch_set_petal_pad_threshold);
-
-STATIC mp_obj_t mp_captouch_autocalib(void) {
-    captouch_force_calibration();
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_autocalib_obj,
-                                 mp_captouch_autocalib);
-
-STATIC mp_obj_t mp_captouch_set_calibration_afe_target(mp_obj_t target_in) {
-    uint16_t target = mp_obj_get_int(target_in);
-    captouch_set_calibration_afe_target(target);
-    return mp_const_none;
-}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_captouch_set_calibration_afe_target_obj,
-                                 mp_captouch_set_calibration_afe_target);
-
 STATIC mp_obj_t mp_menu_button_set_left(mp_obj_t left) {
     st3m_io_menu_button_set_left(mp_obj_get_int(left));
     return mp_const_none;
@@ -272,24 +190,6 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_usb_console_active_obj,
 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_captouch_calibration_active),
-      MP_ROM_PTR(&mp_captouch_calibration_active_obj) },
-    { MP_ROM_QSTR(MP_QSTR_get_captouch), MP_ROM_PTR(&mp_get_captouch_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_get_petal_pad_raw),
-      MP_ROM_PTR(&mp_captouch_get_petal_pad_raw_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_get_petal_pad),
-      MP_ROM_PTR(&mp_captouch_get_petal_pad_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_get_petal_rad),
-      MP_ROM_PTR(&mp_captouch_get_petal_rad_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_get_petal_phi),
-      MP_ROM_PTR(&mp_captouch_get_petal_phi_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_set_petal_pad_threshold),
-      MP_ROM_PTR(&mp_captouch_set_petal_pad_threshold_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_autocalib),
-      MP_ROM_PTR(&mp_captouch_autocalib_obj) },
-    { MP_ROM_QSTR(MP_QSTR_captouch_set_calibration_afe_target),
-      MP_ROM_PTR(&mp_captouch_set_calibration_afe_target_obj) },
-
     { MP_ROM_QSTR(MP_QSTR_menu_button_get),
       MP_ROM_PTR(&mp_menu_button_get_obj) },
     { MP_ROM_QSTR(MP_QSTR_application_button_get),