diff --git a/components/badge23/include/badge23/spio.h b/components/badge23/include/badge23/spio.h
index e12afbb207fac7654bb55fb81568e5acf9d311d9..6de6699987b0f306c7ae46bf02d5488eccb50298 100644
--- a/components/badge23/include/badge23/spio.h
+++ b/components/badge23/include/badge23/spio.h
@@ -16,16 +16,6 @@
 #define BADGE_LINK_PIN_MASK_LINE_OUT ((BADGE_LINK_PIN_MASK_LINE_OUT_TIP) | (BADGE_LINK_PIN_MASK_LINE_OUT_RING))
 #define BADGE_LINK_PIN_MASK_ALL ((BADGE_LINK_PIN_MASK_LINE_IN) | (BADGE_LINK_PIN_MASK_LINE_OUT))
 
-#define BADGE_LINK_PIN_INDEX_LINE_IN_TIP 4
-#define BADGE_LINK_PIN_INDEX_LINE_IN_RING 5
-#if defined(CONFIG_BADGE23_HW_GEN_P6)
-#define BADGE_LINK_PIN_INDEX_LINE_OUT_TIP 7
-#define BADGE_LINK_PIN_INDEX_LINE_OUT_RING 6
-#else
-#define BADGE_LINK_PIN_INDEX_LINE_OUT_TIP 6
-#define BADGE_LINK_PIN_INDEX_LINE_OUT_RING 7
-#endif
-
 /* Initializes GPIO modes, prefills structs, etc. Call before using library.
  */
 void init_buttons();
diff --git a/components/badge23/spio.c b/components/badge23/spio.c
index 1c65765713ee1daa9fdd9d226828490ca277145f..58855354bcc3b7506011b441b15df57c7f3c7661 100644
--- a/components/badge23/spio.c
+++ b/components/badge23/spio.c
@@ -1,424 +1,100 @@
-//special purpose input outputs
-#include "driver/gpio.h"
-#include "stdint.h"
 #include "badge23/spio.h"
 
-#include "st3m_audio.h"
-#include "flow3r_bsp_i2c.h"
-
-#include "driver/i2c.h"
-#define TIMEOUT_MS 1000
-
-#if defined(CONFIG_BADGE23_HW_GEN_P1)
-
-#define BADGE_LINK_LINE_OUT_TIP_ENABLE_PIN 5
-#define BADGE_LINK_LINE_OUT_RING_ENABLE_PIN 6
-#define BADGE_LINK_LINE_IN_TIP_ENABLE_PIN 3
-#define BADGE_LINK_LINE_IN_RING_ENABLE_PIN 4
-
-#elif defined(CONFIG_BADGE23_HW_GEN_P4) || defined(CONFIG_BADGE23_HW_GEN_P3)
-
-#define BADGE_LINK_LINE_OUT_TIP_ENABLE_PIN 6
-#define BADGE_LINK_LINE_OUT_RING_ENABLE_PIN 7
-#define BADGE_LINK_LINE_IN_TIP_ENABLE_PIN 5
-#define BADGE_LINK_LINE_IN_RING_ENABLE_PIN 4
-
-//on ESP32
-#define LEFT_BUTTON_LEFT 3
-#define LEFT_BUTTON_MID 0
-
-//on PORTEXPANDER
-#define LEFT_BUTTON_RIGHT (0+8)
-#define RIGHT_BUTTON_LEFT (6+8)
-#define RIGHT_BUTTON_MID (5+8)
-#define RIGHT_BUTTON_RIGHT (7+8)
-
-#elif defined(CONFIG_BADGE23_HW_GEN_P6)
-
-#define BADGE_LINK_LINE_OUT_RING_ENABLE_PIN 5
-#define BADGE_LINK_LINE_OUT_TIP_ENABLE_PIN 6
-#define BADGE_LINK_LINE_IN_TIP_ENABLE_PIN 4
-#define BADGE_LINK_LINE_IN_RING_ENABLE_PIN 3
-
-#define ENABLE_INVERTED 
-
-//on ESP32
-#define RIGHT_BUTTON_MID 3
-#define LEFT_BUTTON_MID 0
-
-//on PORTEXPANDER
-#define LEFT_BUTTON_RIGHT (0+8)
-#define RIGHT_BUTTON_LEFT (4+8)
-#define LEFT_BUTTON_LEFT (7+8)
-#define RIGHT_BUTTON_RIGHT (5+8)
-
-#define LINE_IN_JACKSENSE (6+8)
-#define CHARGER_STATE (2+8)
-
-#endif
-
-static int8_t leftbutton = 0;
-static int8_t rightbutton = 0;
-
-static bool line_in_jacksense = 1;
-static bool charger_state;
-
-static bool menu_button_left = 0;
-
-static uint8_t badge_link_enabled = 0;
-
-/* The MAX7321 doesn't have any input/output pin configuration methods. Instead,
- * it has a ~40k pullup resistor to VCC and a programmable shunt transistor to VEE.
- * Writing a byte to the IC closes each shunt with a LO at its index.
- * Reading a byte returns the state of each pin.
- * 
- * This means that changing a single output bit cannot be changed without some
- * information about the other pins. Also a output pin set to HI can read as LO
- * if there's an outside shunt.
- */
-typedef struct{
-    flow3r_i2c_address *addr;
-    uint8_t is_output_pin;  // mask for pins we wish to use as outputs
-    uint8_t read_state;     //
-    uint8_t output_state;   // goal output state
-} max7321_t;
-
-max7321_t port_expanders[2]; 
-
-void _max7321_update(max7321_t *max){
-    uint8_t rx = 0;
-    uint8_t tx = (~(max->is_output_pin)) | max->output_state;
-
-	flow3r_bsp_i2c_write_read_device(*max->addr, &tx, sizeof(tx), &rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    // TODO(q3k): handle error
-    max->read_state = rx;
-}
-
-void max7321s_update(){
-    _max7321_update(&port_expanders[0]);
-    _max7321_update(&port_expanders[1]);
-}
-
-bool max7321s_get_pin(uint8_t pin){
-    if(pin>15) return 0;
-    max7321_t * pe = &port_expanders[0];
-    if(pin>7){
-        pe = &port_expanders[1];
-        pin -= 8;
-    }
-    return ((pe->read_state) >> pin) & 1;
-}
-
-bool max7321s_set_pin(uint8_t pin, bool on){
-    if(pin>15) return 0;
-    max7321_t * pe = &port_expanders[0];
-    if(pin>7){
-        pe = &port_expanders[1];
-        pin -= 8;
-    }
-
-    if(((pe->is_output_pin) >> pin) & 1){
-        if(on){
-            pe->output_state |= 1<<pin;
-        } else {
-            pe->output_state &= ~(1<<pin);
-        }
-        return 1;
-    } else {
-        return 0;
-    }
-}
-
-void max7321s_set_pinmode_output(uint8_t pin, bool output){
-    if(pin>15) return 0;
-    max7321_t * pe = &port_expanders[0];
-    if(pin>7){
-        pe = &port_expanders[1];
-        pin -= 8;
-    }
-    if(output){
-        pe->is_output_pin |= (1<<pin);
-    } else {
-        pe->is_output_pin &= ~(1<<pin);
-    }
-}
-
-#if defined(CONFIG_BADGE23_HW_GEN_P1)
-
-#define RIGHT_BUTTON_LEFT 37
-#define RIGHT_BUTTON_MID 0
-#define RIGHT_BUTTON_RIGHT 35
-
-#define LEFT_BUTTON_LEFT 7
-#define LEFT_BUTTON_MID 6
-#define LEFT_BUTTON_RIGHT 5
-
-static void _init_buttons(){
-    //configure all buttons as pullup
-    uint64_t mask = 0;
-    mask |= (1ULL << RIGHT_BUTTON_LEFT);
-    mask |= (1ULL << RIGHT_BUTTON_RIGHT);
-    mask |= (1ULL << LEFT_BUTTON_LEFT);
-    mask |= (1ULL << LEFT_BUTTON_MID);
-    mask |= (1ULL << LEFT_BUTTON_RIGHT);
-    gpio_config_t cfg = {
-        .pin_bit_mask = mask,
-        .mode = GPIO_MODE_INPUT,
-        .pull_up_en = GPIO_PULLUP_ENABLE,
-        .pull_down_en = GPIO_PULLDOWN_DISABLE,
-        .intr_type = GPIO_INTR_DISABLE
-    };
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-    cfg.pin_bit_mask = 1;
-    cfg.pull_up_en = GPIO_PULLUP_DISABLE;
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-}
-
-void update_button_state(){
-    if(!gpio_get_level(RIGHT_BUTTON_LEFT)){
-        rightbutton = BUTTON_PRESSED_LEFT;
-    } else if(!gpio_get_level(RIGHT_BUTTON_MID)){
-        rightbutton = BUTTON_PRESSED_DOWN;
-    } else if(!gpio_get_level(RIGHT_BUTTON_RIGHT)){
-        rightbutton = BUTTON_PRESSED_RIGHT;
-    } else {
-        rightbutton = BUTTON_NOT_PRESSED;
-    }
-
-    if(!gpio_get_level(LEFT_BUTTON_LEFT)){
-        leftbutton = BUTTON_PRESSED_LEFT;
-    } else if(!gpio_get_level(LEFT_BUTTON_MID)){
-        leftbutton = BUTTON_PRESSED_DOWN;
-    } else if(!gpio_get_level(LEFT_BUTTON_RIGHT)){
-        leftbutton = BUTTON_PRESSED_RIGHT;
-    } else {
-        leftbutton = BUTTON_NOT_PRESSED;
-    }
-}
-
-#elif defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4)
-
-max7321_t port_expanders[] = {  {&flow3r_i2c_addresses.portexp[0], 0, 255, 255}, 
-                                {&flow3r_i2c_addresses.portexp[1], 0, 255, 255}  };
-
-static void _init_buttons(){
-    //configure all buttons as pullup
-    gpio_config_t cfg = {
-        .pin_bit_mask = 1 << LEFT_BUTTON_LEFT,
-        .mode = GPIO_MODE_INPUT,
-        .pull_up_en = GPIO_PULLUP_ENABLE,
-        .pull_down_en = GPIO_PULLDOWN_DISABLE,
-        .intr_type = GPIO_INTR_DISABLE
-    };
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-    cfg.pin_bit_mask = 1;
-    cfg.pull_up_en = GPIO_PULLUP_DISABLE;
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-    
-    max7321s_set_pinmode_output(RIGHT_BUTTON_RIGHT, 0);
-    max7321s_set_pinmode_output(RIGHT_BUTTON_MID, 0);
-    max7321s_set_pinmode_output(RIGHT_BUTTON_LEFT, 0);
-    max7321s_set_pinmode_output(LEFT_BUTTON_RIGHT, 0);
-}
-
-int8_t process_button_state(bool r, bool m, bool l){
-    if(!l){
-        return BUTTON_PRESSED_LEFT;
-    } else if(!m){
-        return BUTTON_PRESSED_DOWN;
-    } else if(!r){
-        return BUTTON_PRESSED_RIGHT;
-    } else {
-        return BUTTON_NOT_PRESSED;
-    }
-}
-
-void update_button_state(){
-    max7321s_update();
-    uint8_t rr = max7321s_get_pin(RIGHT_BUTTON_RIGHT);
-    uint8_t rm = max7321s_get_pin(RIGHT_BUTTON_MID);
-    uint8_t rl = max7321s_get_pin(RIGHT_BUTTON_LEFT);
-    uint8_t lr = max7321s_get_pin(LEFT_BUTTON_RIGHT);
-    uint8_t ll = gpio_get_level(LEFT_BUTTON_LEFT);
-    uint8_t lm = gpio_get_level(LEFT_BUTTON_MID);
-
-    int8_t new_rightbutton = process_button_state(rr, rm, rl);
-    int8_t new_leftbutton = process_button_state(lr, lm, ll);
-    if(new_rightbutton != rightbutton){
-        //TODO: CALLBACK button_state_has_changed_to(new_rightbutton)
-        //note: consider menubutton/application button config option
-    }
-    if(new_leftbutton != leftbutton){
-        //TODO: CALLBACK button_state_has_changed_to(new_leftbutton)
-    }
-    rightbutton = new_rightbutton;
-    leftbutton = new_leftbutton;
-}
-#elif defined(CONFIG_BADGE23_HW_GEN_P6)
-max7321_t port_expanders[] = {  {&flow3r_i2c_addresses.portexp[0], 0, 255, 255}, 
-                                {&flow3r_i2c_addresses.portexp[1], 0, 255, 255}  };
+static const char *TAG = "badge23-spio";
 
-static void _init_buttons(){
-    //configure all buttons as pullup
-    gpio_config_t cfg = {
-        .pin_bit_mask = 1 << RIGHT_BUTTON_MID,
-        .mode = GPIO_MODE_INPUT,
-        .pull_up_en = GPIO_PULLUP_ENABLE,
-        .pull_down_en = GPIO_PULLDOWN_DISABLE,
-        .intr_type = GPIO_INTR_DISABLE
-    };
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-    cfg.pin_bit_mask = 1;
-    cfg.pull_up_en = GPIO_PULLUP_DISABLE;
-    ESP_ERROR_CHECK(gpio_config(&cfg));
-    
-    max7321s_set_pinmode_output(RIGHT_BUTTON_RIGHT, 0);
-    max7321s_set_pinmode_output(LEFT_BUTTON_LEFT, 0);
-    max7321s_set_pinmode_output(RIGHT_BUTTON_LEFT, 0);
-    max7321s_set_pinmode_output(LEFT_BUTTON_RIGHT, 0);
-}
+#include "esp_log.h"
 
-int8_t process_button_state(bool r, bool m, bool l){
-    if(!l){
-        return BUTTON_PRESSED_LEFT;
-    } else if(!m){
-        return BUTTON_PRESSED_DOWN;
-    } else if(!r){
-        return BUTTON_PRESSED_RIGHT;
-    } else {
-        return BUTTON_NOT_PRESSED;
-    }
-}
+#include "st3m_audio.h"
+#include "flow3r_bsp_i2c.h"
+#include "flow3r_bsp.h"
 
 void update_button_state(){
-    max7321s_update();
-    uint8_t rr = max7321s_get_pin(RIGHT_BUTTON_RIGHT);
-    uint8_t ll = max7321s_get_pin(LEFT_BUTTON_LEFT);
-    uint8_t rl = max7321s_get_pin(RIGHT_BUTTON_LEFT);
-    uint8_t lr = max7321s_get_pin(LEFT_BUTTON_RIGHT);
-    uint8_t rm = gpio_get_level(RIGHT_BUTTON_MID);
-    uint8_t lm = gpio_get_level(LEFT_BUTTON_MID);
-
-    line_in_jacksense = max7321s_get_pin(LINE_IN_JACKSENSE);
-    charger_state = max7321s_get_pin(CHARGER_STATE);
-
-    int8_t new_rightbutton = process_button_state(rr, rm, rl);
-    int8_t new_leftbutton = process_button_state(lr, lm, ll);
-    if(new_rightbutton != rightbutton){
-        //TODO: CALLBACK button_state_has_changed_to(new_rightbutton)
-        //note: consider menubutton/application button config option
-    }
-    if(new_leftbutton != leftbutton){
-        //TODO: CALLBACK button_state_has_changed_to(new_leftbutton)
+    esp_err_t ret = flow3r_bsp_spio_update();
+    if (ret != ESP_OK) {
+        ESP_LOGE(TAG, "update failed: %s", esp_err_to_name(ret));
     }
-    rightbutton = new_rightbutton;
-    leftbutton = new_leftbutton;
 }
 
-#else
-#error "spio not implemented for this badge generation"
-#endif
-
 void init_buttons(){
-    _init_buttons();
-}
-
-//#define ALWAYS_UPDATE_BUTTON
-int8_t get_button_state(bool left){
-#ifdef ALWAYS_UPDATE_BUTTON
-    update_button_state();
-#endif
-    if(left) return leftbutton;
-    return rightbutton;
+    esp_err_t ret = flow3r_bsp_spio_init();
+    if (ret != ESP_OK) {
+        ESP_LOGE(TAG, "init failed: %s", esp_err_to_name(ret));
+        for (;;) {}
+    }
 }
 
-bool spio_charger_state_get(){
-#ifdef ALWAYS_UPDATE_BUTTON
-    update_button_state();
-#endif
-    return charger_state;
+bool spio_charger_state_get() {
+    return flow3r_bsp_spio_charger_state_get();
 }
 
 bool spio_line_in_jacksense_get(){
-#ifdef ALWAYS_UPDATE_BUTTON
-    update_button_state();
-#endif
-    return line_in_jacksense;
+    return flow3r_bsp_spio_jacksense_right_get();
 }
 
+static bool menu_button_left = false;
+
 void spio_menu_button_set_left(bool left){
-    menu_button_left = 1;
+    menu_button_left = left;
 }
 
-int8_t spio_menu_button_get(){
-    return get_button_state(menu_button_left);
+int8_t spio_menu_button_get() {
+    if (menu_button_left)
+        return flow3r_bsp_spio_left_button_get();
+    return flow3r_bsp_spio_right_button_get();
 }
 
 int8_t spio_application_button_get(){
-    return get_button_state(!menu_button_left);
+    if (menu_button_left)
+        return flow3r_bsp_spio_right_button_get();
+    return flow3r_bsp_spio_left_button_get();
 }
 
 int8_t spio_left_button_get(){
-    return get_button_state(1);
+    return flow3r_bsp_spio_left_button_get();
 }
 
 int8_t spio_right_button_get(){
-    return get_button_state(0);
+    return flow3r_bsp_spio_right_button_get();
 }
 
 int8_t spio_menu_button_get_left(){
     return menu_button_left;
 }
 
+static uint8_t badge_link_enabled = 0;
+
 uint8_t spio_badge_link_get_active(uint8_t pin_mask){
     return badge_link_enabled & pin_mask;
 }
 
-#if defined(CONFIG_BADGE23_HW_GEN_P1)
-
-static uint8_t spio_badge_link_set(uint8_t pin_mask, uint8_t state){
-    return 0; // no badge link here (yet)
-}
-
-#elif defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4) || defined(CONFIG_BADGE23_HW_GEN_P6)
-
-#define USER_WARNINGS_ENABLED
-
-static int8_t spio_badge_link_set(uint8_t pin_mask, bool state){
+static int8_t spio_badge_link_set(uint8_t mask, bool state) {
+    bool left_tip = (mask & BADGE_LINK_PIN_MASK_LINE_OUT_TIP) > 0;
+    bool left_ring = (mask & BADGE_LINK_PIN_MASK_LINE_OUT_RING) > 0;
+    bool right_tip = (mask & BADGE_LINK_PIN_MASK_LINE_IN_TIP) > 0;
+    bool right_ring = (mask & BADGE_LINK_PIN_MASK_LINE_IN_RING) > 0;
     if(state) {
-        if((pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_RING) || (pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_TIP)){
+        if (left_tip || left_ring) {
             if(!st3m_audio_headphones_are_connected()) {
-                pin_mask &= ~BADGE_LINK_PIN_MASK_LINE_OUT_RING;
-                pin_mask &= ~BADGE_LINK_PIN_MASK_LINE_OUT_TIP;
-#ifdef USER_WARNINGS_ENABLED
-                printf("cannot enable line out badge link without cable plugged in for safety reasons\n");
-            } else {
-                printf("badge link enabled on line out. please make sure not to have headphones or sound sources connected before transmitting data.\n");
-#endif
+                left_tip = false;
+                left_ring = false;
+                ESP_LOGE(TAG, "cannot enable line out badge link without cable plugged in for safety reasons");
             }
         }
+        if (left_tip) badge_link_enabled |= BADGE_LINK_PIN_MASK_LINE_OUT_TIP;
+        if (left_ring) badge_link_enabled |= BADGE_LINK_PIN_MASK_LINE_OUT_RING;
+        if (right_tip) badge_link_enabled |= BADGE_LINK_PIN_MASK_LINE_IN_TIP;
+        if (right_ring) badge_link_enabled |= BADGE_LINK_PIN_MASK_LINE_IN_RING;
+    } else {
+        if (!left_tip) badge_link_enabled &= ~BADGE_LINK_PIN_MASK_LINE_OUT_TIP;
+        if (!left_ring) badge_link_enabled &= ~BADGE_LINK_PIN_MASK_LINE_OUT_RING;
+        if (!right_tip) badge_link_enabled &= ~BADGE_LINK_PIN_MASK_LINE_IN_TIP;
+        if (!right_ring) badge_link_enabled &= ~BADGE_LINK_PIN_MASK_LINE_IN_RING;
     }
 
-#ifdef ENABLE_INVERTED
-    uint8_t hw_state = state;
-#else
-    uint8_t hw_state = !state;
-#endif
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_IN_RING) max7321s_set_pinmode_output(BADGE_LINK_LINE_IN_RING_ENABLE_PIN, 1);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_IN_TIP) max7321s_set_pinmode_output(BADGE_LINK_LINE_IN_TIP_ENABLE_PIN, 1);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_RING) max7321s_set_pinmode_output(BADGE_LINK_LINE_OUT_RING_ENABLE_PIN, 1);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_TIP) max7321s_set_pinmode_output(BADGE_LINK_LINE_OUT_TIP_ENABLE_PIN, 1);
-
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_IN_RING) max7321s_set_pin(BADGE_LINK_LINE_IN_RING_ENABLE_PIN, hw_state);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_IN_TIP) max7321s_set_pin(BADGE_LINK_LINE_IN_TIP_ENABLE_PIN, hw_state);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_RING) max7321s_set_pin(BADGE_LINK_LINE_OUT_RING_ENABLE_PIN, hw_state);
-    if(pin_mask & BADGE_LINK_PIN_MASK_LINE_OUT_TIP) max7321s_set_pin(BADGE_LINK_LINE_OUT_TIP_ENABLE_PIN, hw_state);
-
-    max7321s_update();
-    badge_link_enabled = (badge_link_enabled & (~pin_mask)) | (pin_mask & (hw_state ? 255 : 0));
-    return spio_badge_link_get_active(pin_mask);
+    flow3r_bsp_spio_badgelink_left_enable(left_tip, left_ring);
+    flow3r_bsp_spio_badgelink_right_enable(right_tip, right_ring);
+    return spio_badge_link_get_active(mask);
 }
-#endif
 
 uint8_t spio_badge_link_disable(uint8_t pin_mask){
     return spio_badge_link_set(pin_mask, 0);
diff --git a/components/flow3r_bsp/CMakeLists.txt b/components/flow3r_bsp/CMakeLists.txt
index 7d165308ddb41193fe16794ee21d2e6eaffe97a4..9414a39ee6c382900b5d0204e23c8be4feb341fb 100644
--- a/components/flow3r_bsp/CMakeLists.txt
+++ b/components/flow3r_bsp/CMakeLists.txt
@@ -5,10 +5,12 @@ idf_component_register(
 		flow3r_bsp_gc9a01.c
 		flow3r_bsp_hwconfig.c
 		flow3r_bsp_i2c.c
+		flow3r_bsp_max7321.c
 		flow3r_bsp_max98091.c
 		flow3r_bsp_leds.c
 		flow3r_bsp_rmtled.c
 		flow3r_bsp_spiled.c
+		flow3r_bsp_spio.c
     INCLUDE_DIRS
 		.
 )
diff --git a/components/flow3r_bsp/flow3r_bsp.h b/components/flow3r_bsp/flow3r_bsp.h
index 8fd857a7ecfd7901d2866b07abce050d6298eca0..def81e768bbe91854ad0b4c8ec5f97ec6d8696a8 100644
--- a/components/flow3r_bsp/flow3r_bsp.h
+++ b/components/flow3r_bsp/flow3r_bsp.h
@@ -137,4 +137,60 @@ void flow3r_bsp_leds_set_pixel(uint32_t index, uint32_t red, uint32_t green, uin
 
 // Transmit from internal buffer into LEDs. This will block in case there
 // already is a previous transmission happening.
-esp_err_t flow3r_bsp_leds_refresh(TickType_t timeout_ms);
\ No newline at end of file
+esp_err_t flow3r_bsp_leds_refresh(TickType_t timeout_ms);
+
+// A 'tripos' button is what we're calling the shoulder buttons. As the name
+// indicates, it has three positions: left, middle (a.k.a. down/press) and right.
+typedef enum {
+	// Not pressed.
+	flow3r_bsp_tripos_none = 0,
+	// Pressed towards the left.
+	flow3r_bsp_tripos_left = -1,
+	// Pressed down.
+	flow3r_bsp_tripos_mid = 2,
+	// Pressed towards the right.
+	flow3r_bsp_tripos_right = 1,
+} flow3r_bsp_tripos_state_t;
+
+// Initialize 'special purpose i/o', which is like gpio, but more :). This means
+// effectively all the buttons and on-board detect/enable signals.
+esp_err_t flow3r_bsp_spio_init(void);
+
+// Refresh the state of I/O: update outputs based on last called _set functions,
+// and make _get return the newest hardware state.
+esp_err_t flow3r_bsp_spio_update(void);
+
+// Return the latest updated position of the left shoulder/tripos button.
+//
+// flow3r_bsp_spio_update must be called for this data to be up to date.
+flow3r_bsp_tripos_state_t flow3r_bsp_spio_left_button_get(void);
+
+// Return the latest updated position of the right shoulder/tripos button.
+//
+// flow3r_bsp_spio_update must be called for this data to be up to date.
+flow3r_bsp_tripos_state_t flow3r_bsp_spio_right_button_get(void);
+
+// Returns true if the device is charging.
+//
+// flow3r_bsp_spio_update must be called for this data to be up to date.
+bool flow3r_bsp_spio_charger_state_get(void);
+
+// Returns true if the device has something plugged into the right 3.5mm jack.
+//
+// flow3r_bsp_spio_update must be called for this data to be up to date.
+bool flow3r_bsp_spio_jacksense_right_get(void);
+
+// Switch tip/ring muxes to 'badgelink' digital I/O data pins on the left hand
+// jack. As this is the same as the headphone jack, please observe caution when
+// enabling this! See the comment at the bottom of st3m_audio.h.
+//
+// flow3r_bsp_spio_update must be called for this setting to actually propagate
+// to hardware.
+void flow3r_bsp_spio_badgelink_left_enable(bool tip_on, bool ring_on);
+
+// Switch tip/ring muxes to 'badgelink' digital I/O data pins on the right hand
+// jack.
+//
+// flow3r_bsp_spio_update must be called for this setting to actually propagate
+// to hardware.
+void flow3r_bsp_spio_badgelink_right_enable(bool tip_on, bool ring_on);
\ No newline at end of file
diff --git a/components/flow3r_bsp/flow3r_bsp_max7321.c b/components/flow3r_bsp/flow3r_bsp_max7321.c
new file mode 100644
index 0000000000000000000000000000000000000000..47329c2d159ea67ee13743b20dcb1d2af8bc4cbe
--- /dev/null
+++ b/components/flow3r_bsp/flow3r_bsp_max7321.c
@@ -0,0 +1,59 @@
+#include "flow3r_bsp_max7321.h"
+#include "flow3r_bsp_i2c.h"
+
+/* The MAX7321 doesn't have any input/output pin configuration methods. Instead,
+ * it has a ~40k pullup resistor to VCC and a programmable shunt transistor to VEE.
+ * Writing a byte to the IC closes each shunt with a LO at its index.
+ * Reading a byte returns the state of each pin.
+ * 
+ * This means that changing a single output bit cannot be changed without some
+ * information about the other pins. Also a output pin set to HI can read as LO
+ * if there's an outside shunt.
+ */
+
+// TODO(q3k): unhardcode
+#define TIMEOUT_MS 1000
+
+esp_err_t flow3r_bsp_max7321_init(flow3r_bsp_max7321_t *max, flow3r_i2c_address addr) {
+	max->addr = addr;
+	max->is_output_pin = 0;
+	max->read_state = 0xff;
+	max->output_state = 0xff;
+
+	return flow3r_bsp_max7321_update(max);
+}
+
+void flow3r_bsp_max7321_set_pinmode_output(flow3r_bsp_max7321_t *max, uint32_t pin, bool output) {
+    if(output) {
+        max->is_output_pin |= (1<<pin);
+    } else {
+        max->is_output_pin &= ~(1<<pin);
+    }
+}
+
+bool flow3r_bsp_max7321_get_pin(flow3r_bsp_max7321_t *max, uint32_t pin) {
+    return ((max->read_state) >> pin) & 1;
+}
+
+void flow3r_bsp_max7321_set_pin(flow3r_bsp_max7321_t *max, uint32_t pin, bool on) {
+    if(((max->is_output_pin) >> pin) & 1){
+        if(on){
+            max->output_state |= 1<<pin;
+        } else {
+            max->output_state &= ~(1<<pin);
+        }
+	}
+}
+
+esp_err_t flow3r_bsp_max7321_update(flow3r_bsp_max7321_t *max) {
+    uint8_t rx = 0;
+    uint8_t tx = (~(max->is_output_pin)) | max->output_state;
+
+	esp_err_t ret = flow3r_bsp_i2c_write_read_device(max->addr, &tx, sizeof(tx), &rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
+	if (ret != ESP_OK) {
+		return ret;
+	}
+	max->read_state = rx;
+
+	return ESP_OK;
+}
\ No newline at end of file
diff --git a/components/flow3r_bsp/flow3r_bsp_max7321.h b/components/flow3r_bsp/flow3r_bsp_max7321.h
new file mode 100644
index 0000000000000000000000000000000000000000..79ae185e41d9566ae0e494c9241d56b887af01f9
--- /dev/null
+++ b/components/flow3r_bsp/flow3r_bsp_max7321.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "flow3r_bsp_i2c.h"
+
+#include "esp_err.h"
+
+typedef struct {
+	flow3r_i2c_address addr;
+	// mask for pins we wish to use as outputs
+    uint8_t is_output_pin; 
+    uint8_t read_state;
+	// goal output state
+    uint8_t output_state;
+} flow3r_bsp_max7321_t;
+
+esp_err_t flow3r_bsp_max7321_init(flow3r_bsp_max7321_t *max, flow3r_i2c_address addr);
+void flow3r_bsp_max7321_set_pinmode_output(flow3r_bsp_max7321_t *max, uint32_t pin, bool output);
+bool flow3r_bsp_max7321_get_pin(flow3r_bsp_max7321_t *max, uint32_t pin);
+void flow3r_bsp_max7321_set_pin(flow3r_bsp_max7321_t *max, uint32_t pin, bool on);
+esp_err_t flow3r_bsp_max7321_update(flow3r_bsp_max7321_t *max);
\ No newline at end of file
diff --git a/components/flow3r_bsp/flow3r_bsp_spio.c b/components/flow3r_bsp/flow3r_bsp_spio.c
new file mode 100644
index 0000000000000000000000000000000000000000..3acfb39525f4ff93dcc797fb2899874cf025ce8c
--- /dev/null
+++ b/components/flow3r_bsp/flow3r_bsp_spio.c
@@ -0,0 +1,319 @@
+#include "flow3r_bsp.h"
+
+#include <stdbool.h>
+
+#include "driver/gpio.h"
+#include "esp_log.h"
+
+static const char *TAG = "flow3r-spio";
+
+typedef enum {
+	flow3r_bsp_iochip_esp32 = 0,
+	flow3r_bsp_iochip_portexp = 1,
+	flow3r_bsp_iochip_dummy = 2,
+} flow3r_bsp_iochip_t;
+
+
+typedef struct {
+	flow3r_bsp_iochip_t chip;
+	uint32_t pin;
+	bool output;
+	bool pullup;
+	bool invert;
+} flow3r_bsp_iopin_t;
+
+#define IESP(pinno, pullup_, ...) { .chip = flow3r_bsp_iochip_esp32, .pin = pinno, .output = false, .pullup = pullup_ , __VA_ARGS__}
+#define IPEX(pinno, pexno, ...) { .chip = flow3r_bsp_iochip_portexp, .pin = pinno + pexno * 8, .output = false, __VA_ARGS__ }
+#define OPEX(pinno, pexno, ...) { .chip = flow3r_bsp_iochip_portexp, .pin = pinno + pexno * 8, .output = true, __VA_ARGS__ }
+#define IODUMMY { .chip = flow3r_bsp_iochip_dummy }
+
+typedef struct {
+	flow3r_bsp_iopin_t left;
+	flow3r_bsp_iopin_t mid;
+	flow3r_bsp_iopin_t right;
+} flow3r_bsp_iodef_tripos_t;
+
+typedef struct {
+	flow3r_bsp_iopin_t tip_badgelink_enable;
+	flow3r_bsp_iopin_t ring_badgelink_enable;
+} flow3r_bsp_iodef_trrs_t;
+
+typedef struct {
+	flow3r_bsp_iodef_tripos_t tripos_left;
+	flow3r_bsp_iodef_tripos_t tripos_right;
+	flow3r_bsp_iodef_trrs_t trrs_left;
+	flow3r_bsp_iodef_trrs_t trrs_right;
+
+	flow3r_bsp_iopin_t charger_state;
+	flow3r_bsp_iopin_t jacksense_right;
+} flow3r_bsp_iodef_t;
+
+#if defined(CONFIG_BADGE23_HW_GEN_P1)
+static const flow3r_bsp_iodef_t iodef = {
+	.tripos_left = {
+		.left = IPEX(37, 1),
+		.mid = IESP(0, false),
+		.right = IPEX(35, 1),
+	},
+	.tripos_right = {
+		.left = IESP(7, 1),
+		.mid = IESP(6, true),
+		.right = IESP(5, 1),
+	},
+	.trrs_left = {
+		.tip_badgelink_enable = IODUMMY,
+		.ring_badgelink_enable = IODUMMY,
+	},
+	.trrs_right = {
+		.tip_badgelink_enable = IODUMMY,
+		.ring_badgelink_enable = IODUMMY,
+	},
+	.charger_state = IODUMMY,
+	.jacksense_right = IODUMMY,
+};
+#define PORTEXP_NONE
+#elif defined(CONFIG_BADGE23_HW_GEN_P4) || defined(CONFIG_BADGE23_HW_GEN_P3)
+static const flow3r_bsp_iodef_t iodef = {
+	.tripos_left = {
+		.left = IESP(3, true),
+		.mid = IESP(0, false),
+		.right = IPEX(0, 1),
+	},
+	.tripos_right = {
+		.left = IPEX(6, 1),
+		.mid = IPEX(5, 1),
+		.right = IPEX(7, 1),
+	},
+	.trrs_left = {
+		.tip_badgelink_enable = OPEX(6, 0),
+		.ring_badgelink_enable = OPEX(7, 0),
+	},
+	.trrs_right = {
+		.tip_badgelink_enable = OPEX(5, 0),
+		.ring_badgelink_enable = OPEX(4, 0),
+	},
+	.charger_state = IODUMMY,
+	.jacksense_right = IODUMMY,
+};
+#define PORTEXP_MAX7321S
+#elif defined(CONFIG_BADGE23_HW_GEN_P6)
+static const flow3r_bsp_iodef_t iodef = {
+	.tripos_left = {
+		.left = IPEX(7, 1, .invert = true),
+		.mid = IESP(0, false, .invert = true),
+		.right = IPEX(0, 1, .invert = true),
+	},
+	.tripos_right = {
+		.left = IPEX(4, 1, .invert = true),
+		.mid = IESP(3, true, .invert = true),
+		.right = IPEX(5, 1, .invert = true),
+	},
+	.trrs_left = {
+		.tip_badgelink_enable = OPEX(6, 0, .invert = true),
+		.ring_badgelink_enable = OPEX(5, 0, .invert = true),
+	},
+	.trrs_right = {
+		.tip_badgelink_enable = OPEX(4, 0, .invert = true),
+		.ring_badgelink_enable = OPEX(3, 0, .invert = true),
+	},
+	.charger_state = IPEX(2, 1),
+	.jacksense_right = IPEX(6, 1),
+};
+#define PORTEXP_MAX7321S
+#else
+#error "spio unimplemented for this badge generation"
+#endif
+
+#ifdef PORTEXP_MAX7321S
+#include "flow3r_bsp_max7321.h"
+static flow3r_bsp_max7321_t portexps[2];
+
+static esp_err_t _portexp_init(void) {
+	esp_err_t ret = flow3r_bsp_max7321_init(&portexps[0], flow3r_i2c_addresses.portexp[0]);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to initialize portexp 0: %s", esp_err_to_name(ret));
+		return ret;
+	}
+	ret = flow3r_bsp_max7321_init(&portexps[1], flow3r_i2c_addresses.portexp[1]);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to initialize portexp 1: %s", esp_err_to_name(ret));
+		return ret;
+	}
+	return ESP_OK;
+}
+
+static uint32_t _portexp_pin(const flow3r_bsp_iopin_t *iopin) {
+	return iopin->pin % 8;
+}
+
+static flow3r_bsp_max7321_t *_portexp_max(const flow3r_bsp_iopin_t *iopin) {
+	uint32_t ix = (iopin->pin >> 3) % 2;
+	return &portexps[ix];
+}
+
+static void _iopin_portexp_init(const flow3r_bsp_iopin_t *iopin) {
+    flow3r_bsp_max7321_set_pinmode_output(_portexp_max(iopin), _portexp_pin(iopin), iopin->output);
+}
+
+static bool _iopin_portexp_get_pin(const flow3r_bsp_iopin_t *iopin) {
+    return flow3r_bsp_max7321_get_pin(_portexp_max(iopin), _portexp_pin(iopin));
+}
+
+static void _iopin_portexp_set_pin(const flow3r_bsp_iopin_t *iopin, bool on) {
+    flow3r_bsp_max7321_set_pin(_portexp_max(iopin), _portexp_pin(iopin), on);
+}
+
+static esp_err_t _portexp_update(void) {
+	esp_err_t ret = flow3r_bsp_max7321_update(&portexps[0]);
+	if (ret != ESP_OK) {
+		return ret;
+	}
+	ret = flow3r_bsp_max7321_update(&portexps[1]);
+	if (ret != ESP_OK) {
+		return ret;
+	}
+	return ESP_OK;
+}
+#endif
+
+#ifdef PORTEXP_NONE
+static esp_err_t _portexp_init(void) { return ESP_OK; }
+static void _iopin_portexp_init(const flow3r_bsp_iopin_t *iopin) {}
+static bool _iopin_portexp_get_pin(const flow3r_bsp_iopin_t *iopin) { return false; }
+static void _iopin_portexp_set_pin(const flow3r_bsp_iopin_t *iopin, bool on) {}
+static esp_err_t _portexp_update(void) { return ESP_OK; }
+#endif
+
+static esp_err_t _iopin_esp32_init(const flow3r_bsp_iopin_t *iopin) {
+    gpio_config_t cfg = {
+        .pin_bit_mask = 1 << iopin->pin,
+        .mode = GPIO_MODE_INPUT,
+        .pull_up_en = GPIO_PULLUP_DISABLE,
+        .pull_down_en = GPIO_PULLDOWN_DISABLE,
+        .intr_type = GPIO_INTR_DISABLE
+    };
+	if (iopin->output) {
+		// Untested, so unimplemented.
+		return ESP_ERR_NOT_SUPPORTED;
+	}
+	if (iopin->pullup) {
+        cfg.pull_up_en = GPIO_PULLUP_ENABLE;
+	}
+    return gpio_config(&cfg);
+}
+
+static esp_err_t _iopin_init(const flow3r_bsp_iopin_t *iopin) {
+	switch (iopin->chip) {
+	case flow3r_bsp_iochip_esp32:
+		return _iopin_esp32_init(iopin);
+	case flow3r_bsp_iochip_portexp:
+		_iopin_portexp_init(iopin);
+		return ESP_OK;
+	case flow3r_bsp_iochip_dummy:
+		return ESP_OK;
+	}
+	return ESP_ERR_NOT_SUPPORTED;
+}
+
+static bool _iopin_esp32_get_pin(const flow3r_bsp_iopin_t *iopin) {
+	return gpio_get_level(iopin->pin);
+}
+
+static bool _iopin_get_pin(const flow3r_bsp_iopin_t *iopin) {
+	bool res = false;
+	switch (iopin->chip) {
+	case flow3r_bsp_iochip_esp32:
+		res = _iopin_esp32_get_pin(iopin);
+		break;
+	case flow3r_bsp_iochip_portexp:
+		res = _iopin_portexp_get_pin(iopin);
+		break;
+	case flow3r_bsp_iochip_dummy:
+		res = false;
+	}
+	if (iopin->invert) {
+		res = !res;
+	}
+	return res;
+}
+
+static void _iopin_set_pin(const flow3r_bsp_iopin_t *iopin, bool on) {
+	if (iopin->invert) {
+		on = !on;
+	}
+
+	switch (iopin->chip) {
+	case flow3r_bsp_iochip_esp32:
+		// Not implemented.
+		break;
+	case flow3r_bsp_iochip_portexp:
+		_iopin_portexp_set_pin(iopin, on);
+		break;
+	case flow3r_bsp_iochip_dummy:
+		break;
+	}
+}
+
+#define INITIO(name) ret = _iopin_init(&iodef.name); if (ret != ESP_OK) { return ret; }
+esp_err_t flow3r_bsp_spio_init(void) {
+	esp_err_t ret = _portexp_init();
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Port Expander initialzation failed: %s", esp_err_to_name(ret));
+		return ret;
+	}
+	INITIO(tripos_left.left);
+	INITIO(tripos_left.mid);
+	INITIO(tripos_left.right);
+	INITIO(tripos_right.left);
+	INITIO(tripos_right.mid);
+	INITIO(tripos_right.right);
+	INITIO(trrs_left.tip_badgelink_enable);
+	INITIO(trrs_left.ring_badgelink_enable);
+	INITIO(trrs_right.tip_badgelink_enable);
+	INITIO(trrs_right.ring_badgelink_enable);
+	
+	return _portexp_update();
+}
+
+esp_err_t flow3r_bsp_spio_update(void) {
+	return _portexp_update();
+}
+
+static flow3r_bsp_tripos_state_t _tripos_get(const flow3r_bsp_iodef_tripos_t *io) {
+	if (_iopin_get_pin(&io->mid)) {
+		return flow3r_bsp_tripos_mid;
+	}
+	if (_iopin_get_pin(&io->left)) {
+		return flow3r_bsp_tripos_left;
+	}
+	if (_iopin_get_pin(&io->right)) {
+		return flow3r_bsp_tripos_right;
+	}
+		return flow3r_bsp_tripos_none;
+}
+
+flow3r_bsp_tripos_state_t flow3r_bsp_spio_left_button_get(void) {
+	return _tripos_get(&iodef.tripos_left);
+}
+
+flow3r_bsp_tripos_state_t flow3r_bsp_spio_right_button_get(void) {
+	return _tripos_get(&iodef.tripos_right);
+}
+
+bool flow3r_bsp_spio_charger_state_get(void) {
+	return _iopin_get_pin(&iodef.charger_state);
+}
+
+bool flow3r_bsp_spio_jacksense_right_get(void) {
+	return _iopin_get_pin(&iodef.jacksense_right);
+}
+
+void flow3r_bsp_spio_badgelink_left_enable(bool tip_on, bool ring_on) {
+	_iopin_set_pin(&iodef.trrs_left.ring_badgelink_enable, ring_on);
+	_iopin_set_pin(&iodef.trrs_left.tip_badgelink_enable, tip_on);
+}
+
+void flow3r_bsp_spio_badgelink_right_enable(bool tip_on, bool ring_on) {
+	_iopin_set_pin(&iodef.trrs_right.ring_badgelink_enable, ring_on);
+	_iopin_set_pin(&iodef.trrs_right.tip_badgelink_enable, tip_on);
+}
\ No newline at end of file
diff --git a/usermodule/mp_badge_link.c b/usermodule/mp_badge_link.c
index 237bd9bf9ba29ec5ab081b9566a8853f51b2764d..bf33edcfbca31fb16a25e29920dd2a00ab4abeba 100644
--- a/usermodule/mp_badge_link.c
+++ b/usermodule/mp_badge_link.c
@@ -43,10 +43,6 @@ STATIC const mp_rom_map_elem_t mp_module_badge_link_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_PIN_MASK_LINE_IN), MP_ROM_INT(BADGE_LINK_PIN_MASK_LINE_IN) },
     { MP_ROM_QSTR(MP_QSTR_PIN_MASK_LINE_OUT), MP_ROM_INT(BADGE_LINK_PIN_MASK_LINE_OUT) },
     { MP_ROM_QSTR(MP_QSTR_PIN_MASK_ALL), MP_ROM_INT(BADGE_LINK_PIN_MASK_ALL) },
-    { MP_ROM_QSTR(MP_QSTR_PIN_INDEX_LINE_IN_TIP), MP_ROM_INT(BADGE_LINK_PIN_INDEX_LINE_IN_TIP) },
-    { MP_ROM_QSTR(MP_QSTR_PIN_INDEX_LINE_IN_RING), MP_ROM_INT(BADGE_LINK_PIN_INDEX_LINE_IN_RING) },
-    { MP_ROM_QSTR(MP_QSTR_PIN_INDEX_LINE_OUT_TIP), MP_ROM_INT(BADGE_LINK_PIN_INDEX_LINE_OUT_TIP) },
-    { MP_ROM_QSTR(MP_QSTR_PIN_INDEX_LINE_OUT_RING), MP_ROM_INT(BADGE_LINK_PIN_INDEX_LINE_OUT_RING) },
 };
 
 STATIC MP_DEFINE_CONST_DICT(mp_module_badge_link_globals, mp_module_badge_link_globals_table);