From 5fa3bb583731d4df164188a5ca3c16d2d0a257d4 Mon Sep 17 00:00:00 2001
From: Serge Bazanski <q3k@q3k.org>
Date: Sat, 15 Jul 2023 19:15:09 +0200
Subject: [PATCH] mpy: new captouch module

---
 components/badge23/include/badge23/captouch.h |  15 +-
 python_payload/mypystubs/captouch.pyi         | 100 +++++++++++
 sim/fakes/captouch.py                         |  86 ++++++++++
 sim/fakes/hardware.py                         |  95 +++++++++--
 usermodule/micropython.cmake                  |   1 +
 usermodule/mp_captouch.c                      | 158 ++++++++++++++++++
 6 files changed, 443 insertions(+), 12 deletions(-)
 create mode 100644 python_payload/mypystubs/captouch.pyi
 create mode 100644 sim/fakes/captouch.py
 create mode 100644 usermodule/mp_captouch.c

diff --git a/components/badge23/include/badge23/captouch.h b/components/badge23/include/badge23/captouch.h
index c35a2eab7c..1afe8cd681 100644
--- a/components/badge23/include/badge23/captouch.h
+++ b/components/badge23/include/badge23/captouch.h
@@ -64,14 +64,23 @@ void captouch_force_calibration();
 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;
 
@@ -79,9 +88,13 @@ 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
diff --git a/python_payload/mypystubs/captouch.pyi b/python_payload/mypystubs/captouch.pyi
new file mode 100644
index 0000000000..6a4d4c07b9
--- /dev/null
+++ b/python_payload/mypystubs/captouch.pyi
@@ -0,0 +1,100 @@
+from typing import Protocol, List
+
+
+class CaptouchPetalPadsState(Protocol):
+    """
+    Current state of pads on a captouch petal.
+
+    Not all petals have all pads. Top petals have a a base, cw and ccw pad.
+    Bottom petals have a base and tip pad.
+    """
+
+    @property
+    def tip(self) -> bool:
+        """
+        True if the petals's tip is currently touched.
+        """
+        ...
+    
+    @property
+    def base(self) -> bool:
+        """
+        True if the petal's base is currently touched.
+        """
+        ...
+
+    @property
+    def cw(self) -> bool:
+        """
+        True if the petal's clockwise pad is currently touched.
+        """
+        ...
+
+    @property
+    def ccw(self) -> bool:
+        """
+        True if the petal's counter clockwise pad is currently touched.
+        """
+        ...
+
+
+class CaptouchPetalState(Protocol):
+    @property
+    def pressed(self) -> bool:
+        """
+        True if any of the petal's pads is currently touched.
+        """
+        ...
+
+    @property
+    def top(self) -> bool:
+        """
+        True if this is a top petal.
+        """
+        ...
+
+    @property
+    def bottom(self) -> bool:
+        """
+        True if this is a bottom petal.
+        """
+        ...
+
+    @property
+    def pads(self) -> CaptouchPetalPadsState:
+        """
+        State of individual pads of the petal.
+        """
+        ...
+
+
+class CaptouchState(Protocol):
+    """
+    State of captouch sensors, captured at some time.
+    """
+    @property
+    def petals(self) -> List[CaptouchPetalState]:
+        """
+        State of individual petals.
+
+        Contains 10 elements, with the zeroth element being the pad closest to
+        the USB port. Then, every other pad in a counter-clockwise direction.
+
+        Pads 0, 2, 4, 6, 8 are Top pads.
+
+        Pads 1, 3, 5, 7, 9 are Bottom pads.
+        """
+        ...
+
+
+def read() -> CaptouchState:
+    """
+    Reads current captouch state from hardware and returns a snapshot in time.
+    """
+    ...
+
+def calibration_active() -> bool:
+    """
+    Returns true if the captouch system is current recalibrating.
+    """
+    ...
\ No newline at end of file
diff --git a/sim/fakes/captouch.py b/sim/fakes/captouch.py
new file mode 100644
index 0000000000..4706637827
--- /dev/null
+++ b/sim/fakes/captouch.py
@@ -0,0 +1,86 @@
+from typing import List
+
+
+class CaptouchPetalPadsState:
+    def __init__(self, tip, base, cw, ccw) -> None:
+        self._tip = tip
+        self._base = base
+        self._cw = cw
+        self._ccw = ccw
+
+    @property
+    def tip(self) -> bool:
+        return self._tip
+    
+    @property
+    def base(self) -> bool:
+        return  self._base
+
+    @property
+    def cw(self) -> bool:
+        return self._cw
+
+    @property
+    def ccw(self) -> bool:
+        return self._ccw
+
+
+class CaptouchPetalState:
+    def __init__(self, ix: int, pads: CaptouchPetalPadsState):
+        self._pads = pads
+        self._ix = ix
+
+    @property
+    def pressed(self) -> bool:
+        if self.top:
+            return self._pads.base or self._pads.ccw or self._pads.cw
+        else:
+            return self._pads.tip or self._pads.base
+
+    @property
+    def top(self) -> bool:
+        return self._ix % 2 == 0
+
+    @property
+    def bottom(self) -> bool:
+        return not self.bottom
+
+    @property
+    def pads(self) -> CaptouchPetalPadsState:
+        return self._pads
+
+
+class CaptouchState:
+    def __init__(self, petals: List[CaptouchPetalState]):
+        self._petals = petals
+
+    @property
+    def petals(self) -> List[CaptouchPetalState]:
+        return self._petals
+
+
+def read() -> CaptouchState:
+    import hardware
+    hardware._sim.process_events()
+    hardware._sim.render_gui_lazy()
+    petals = hardware._sim.petals
+
+    res = []
+    for petal in range(10):
+        top = petal % 2 == 0
+        if top:
+            ccw = petals.state_for_petal_pad(petal, 1)
+            cw = petals.state_for_petal_pad(petal, 2)
+            base = petals.state_for_petal_pad(petal, 3)
+            pads = CaptouchPetalPadsState(False, base, cw, ccw)
+            res.append(CaptouchPetalState(petal, pads))
+        else:
+            tip = petals.state_for_petal_pad(petal, 0)
+            base = petals.state_for_petal_pad(petal, 3)
+            pads = CaptouchPetalPadsState(tip, base, False, False)
+            res.append(CaptouchPetalState(petal, pads))
+    return CaptouchState(res)
+
+
+def calibration_active() -> bool:
+    return False
\ No newline at end of file
diff --git a/sim/fakes/hardware.py b/sim/fakes/hardware.py
index 05003e6e3a..3f6ef563b4 100644
--- a/sim/fakes/hardware.py
+++ b/sim/fakes/hardware.py
@@ -1,6 +1,7 @@
 import math
 import os
 import time
+import itertools
 
 import pygame
 
@@ -42,8 +43,6 @@ class Input:
 
     def _mouse_coords_to_id(self, mouse_x, mouse_y):
         for i, (x, y) in enumerate(self.POSITIONS):
-            x += self.MARKER_SIZE // 2
-            y += self.MARKER_SIZE // 2
             dx = mouse_x - x
             dy = mouse_y - y
             if math.sqrt(dx**2 + dy**2) < self.MARKER_SIZE // 2:
@@ -71,8 +70,6 @@ class Input:
     def render(self, surface):
         s = self.state()
         for i, (x, y) in enumerate(self.POSITIONS):
-            x += self.MARKER_SIZE // 2
-            y += self.MARKER_SIZE // 2
             if s[i]:
                 pygame.draw.circle(surface, self.COLOR_HELD, (x, y), self.MARKER_SIZE//2)
             elif i == self._mouse_hover:
@@ -82,16 +79,83 @@ class Input:
 
 
 class PetalsInput(Input):
-    # First petal is above USB-C jack, then CCW.
-    POSITIONS = [
-        (356, 122), (163, 112), (114, 302), ( 49, 477), (204, 587), (352, 696), (504, 587), (660, 477), (602, 298), (547, 117),
+    _petal_positions_top = [
+        (406, 172), (164, 352), (254, 637), (554, 637), (652, 348),
+    ]
+    _petal_positions_bottom = [
+        (213, 162), (99, 527), (402, 746), (710, 527), (597, 167)
     ]
+    POSITIONS = list(itertools.chain(*[
+        [
+            (x + math.cos(i * -1.256 + 1.57) * 40, y + math.sin(i * -1.256 + 1.57) * 40), # base
+            (x + math.cos(i * -1.256 + 5.75) * 40, y + math.sin(i * -1.256 + 5.75) * 40), # cw
+            (x + math.cos(i * -1.256 + 3.66) * 40, y + math.sin(i * -1.256 + 3.66) * 40), # ccw
+        ]
+        for i, (x, y) in enumerate(_petal_positions_top)
+    ] + [
+        [
+            (x + math.cos(i * -1.256 - 2.20) * 40, y + math.sin(i * -1.256 - 2.20) * 40), # tip
+            (x + math.cos(i * -1.256 - 5.34) * 40, y + math.sin(i * -1.256 - 5.34) * 40), # base
+        ]
+        for i, (x, y) in enumerate(_petal_positions_bottom)
+    ]))
+    MARKER_SIZE = 40
+
+    def _index_for_petal_pad(self, petal, pad):
+        if petal >= 10:
+            raise ValueError("petal cannot be > 10")
+        
+        # convert from st3m/bsp index into input state index
+        top = False
+        if petal % 2 == 0:
+            top = True
+        res = petal // 2
+        if top:
+            res *= 3
+        else:
+            res *= 2
+            res += 3 * 5
+
+        if top:
+            if pad == 1: # ccw
+                res += 2
+            elif pad == 2: # cw
+                res += 1
+            elif pad == 3: # base
+                res += 0
+            else:
+                raise ValueError("invalid pad number")
+        else:
+            if pad == 0: # tip
+                res += 0
+            elif pad == 3: # base
+                res += 1
+            else:
+                raise ValueError("invalid pad number")
+        return res
+
+    def state_for_petal_pad(self, petal, pad):
+        ix = self._index_for_petal_pad(petal, pad)
+        return self.state()[ix]
+
+    def state_for_petal(self, petal):
+        res = False
+        if petal % 2 == 0:
+            # top
+            res = res or self.state_for_petal_pad(petal, 1)
+            res = res or self.state_for_petal_pad(petal, 2)
+            res = res or self.state_for_petal_pad(petal, 3)
+        else:
+            # bottom
+            res = res or self.state_for_petal_pad(petal, 0)
+            res = res or self.state_for_petal_pad(petal, 3)
+        return res
 
 
 class ButtonsInput(Input):
     POSITIONS = [
-        ( 14, 230), ( 46, 230), ( 78, 230),
-        (714, 230), (746, 230), (778, 230),
+        ( 24, 240), ( 56, 240), ( 88, 240),
+        (724, 240), (756, 240), (788, 240),
     ]
     MARKER_SIZE = 20
     COLOR_HELD = (0x80, 0x80, 0x80, 0xff)
@@ -394,7 +458,7 @@ def menu_button_get_left():
 def get_captouch(a):
     _sim.process_events()
     _sim.render_gui_lazy()
-    return _sim.petals.state()[a]
+    return _sim.petals.state_for_petal(a)
 
 #TODO(iggy/q3k do proper positional captouch)
 def captouch_get_petal_rad(a):
@@ -423,4 +487,13 @@ def scope_draw(ctx):
     ctx.line_to(130, 0)
     ctx.line_to(130, 130)
     ctx.line_to(-130, 130)
-    ctx.line_to(-130, 0)
\ No newline at end of file
+    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/usermodule/micropython.cmake b/usermodule/micropython.cmake
index ab06366471..6cc83509d6 100644
--- a/usermodule/micropython.cmake
+++ b/usermodule/micropython.cmake
@@ -12,6 +12,7 @@ target_sources(usermod_badge23 INTERFACE
     ${CMAKE_CURRENT_LIST_DIR}/mp_badge_link.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_kernel.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_uctx.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_captouch.c
 )
 
 target_include_directories(usermod_badge23 INTERFACE
diff --git a/usermodule/mp_captouch.c b/usermodule/mp_captouch.c
new file mode 100644
index 0000000000..e92fdc115a
--- /dev/null
+++ b/usermodule/mp_captouch.c
@@ -0,0 +1,158 @@
+#include "py/runtime.h"
+#include "py/builtin.h"
+
+#include "badge23/captouch.h"
+
+#include <string.h>
+
+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);
+
+typedef struct {
+	mp_obj_base_t base;
+	mp_obj_t petal;
+} mp_captouch_petal_pads_state_t;
+
+const mp_obj_type_t captouch_petal_pads_state_type;
+
+typedef struct {
+	mp_obj_base_t base;
+	mp_obj_t captouch;
+	mp_obj_t pads;
+	size_t ix;
+} mp_captouch_petal_state_t;
+
+const mp_obj_type_t captouch_petal_state_type;
+
+typedef struct {
+	mp_obj_base_t base;
+	mp_obj_t petals;
+	captouch_state_t underlying;
+} mp_captouch_state_t;
+
+const mp_obj_type_t captouch_state_type;
+
+STATIC void mp_captouch_petal_pads_state_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+	mp_captouch_petal_pads_state_t *self = MP_OBJ_TO_PTR(self_in);
+	if (dest[0] != MP_OBJ_NULL) {
+		return;
+	}
+
+	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];
+	bool top = (petal->ix % 2) == 0;
+
+	if (top) {
+		switch (attr) {
+		case MP_QSTR_base: dest[0] = mp_obj_new_bool(state->pads.base_pressed); break;
+		case MP_QSTR_cw: dest[0] = mp_obj_new_bool(state->pads.cw_pressed); break;
+		case MP_QSTR_ccw: dest[0] = mp_obj_new_bool(state->pads.ccw_pressed); break;
+		}
+	} else {
+		switch (attr) {
+		case MP_QSTR_tip: dest[0] = mp_obj_new_bool(state->pads.tip_pressed); break;
+		case MP_QSTR_base: dest[0] = mp_obj_new_bool(state->pads.base_pressed); break;
+		}
+	}
+}
+
+STATIC void mp_captouch_petal_state_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+	mp_captouch_petal_state_t *self = MP_OBJ_TO_PTR(self_in);
+	if (dest[0] != MP_OBJ_NULL) {
+		return;
+	}
+
+	mp_captouch_state_t *captouch = MP_OBJ_TO_PTR(self->captouch);
+	captouch_petal_state_t *state = &captouch->underlying.petals[self->ix];
+
+	bool top = (self->ix % 2) == 0;
+
+	switch (attr) {
+	case MP_QSTR_top: dest[0] = mp_obj_new_bool(top); break;
+	case MP_QSTR_bottom: dest[0] = mp_obj_new_bool(!top); break;
+	case MP_QSTR_pressed: dest[0] = mp_obj_new_bool(state->pressed); break;
+	case MP_QSTR_pads: dest[0] = self->pads; break;
+	}
+}
+
+STATIC void mp_captouch_state_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+	mp_captouch_state_t *self = MP_OBJ_TO_PTR(self_in);
+	if (dest[0] != MP_OBJ_NULL) {
+		return;
+	}
+
+	switch (attr) {
+	case MP_QSTR_petals: dest[0] = self->petals; break;
+	}
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+	captouch_petal_pads_state_type,
+	MP_QSTR_CaptouchPetalPadsState,
+	MP_TYPE_FLAG_NONE,
+	attr, mp_captouch_petal_pads_state_attr
+);
+
+MP_DEFINE_CONST_OBJ_TYPE(
+	captouch_petal_state_type,
+	MP_QSTR_CaptouchPetalState,
+	MP_TYPE_FLAG_NONE,
+	attr, mp_captouch_petal_state_attr
+);
+
+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) {
+	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));
+
+	captouch->petals = mp_obj_new_list(0, NULL);
+	for (int i = 0; i < 10; i++) {
+		mp_captouch_petal_state_t *petal = m_new_obj(mp_captouch_petal_state_t);
+		petal->base.type = &captouch_petal_state_type;
+		petal->captouch = MP_OBJ_FROM_PTR(captouch);
+		petal->ix = i;
+
+		mp_captouch_petal_pads_state_t *pads = m_new_obj(mp_captouch_petal_pads_state_t);
+		pads->base.type = &captouch_petal_pads_state_type;
+		pads->petal = MP_OBJ_FROM_PTR(petal);
+		petal->pads = MP_OBJ_FROM_PTR(pads);
+
+		mp_obj_list_append(captouch->petals, MP_OBJ_FROM_PTR(petal));
+	}
+
+	return MP_OBJ_FROM_PTR(captouch);
+}
+
+STATIC mp_obj_t mp_captouch_read(void) {
+	mp_captouch_state_t st;
+	read_captouch_ex(&st);
+	return mp_captouch_state_new(&st);
+}
+
+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) }
+};
+
+STATIC MP_DEFINE_CONST_DICT(globals, globals_table);
+
+
+const mp_obj_module_t mp_module_captouch_user_cmodule = {
+	.base = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_captouch, mp_module_captouch_user_cmodule);
\ No newline at end of file
-- 
GitLab