diff --git a/components/flow3r_bsp/CMakeLists.txt b/components/flow3r_bsp/CMakeLists.txt index 047b8c722014e6a69d5a87825195c6b15d09a490..11bc24a3266a684441199f7fd7545d4dca48c359 100644 --- a/components/flow3r_bsp/CMakeLists.txt +++ b/components/flow3r_bsp/CMakeLists.txt @@ -21,4 +21,5 @@ idf_component_register( vfs bmi270 bmp581 + esp_timer ) diff --git a/components/flow3r_bsp/flow3r_bsp_ad7147.c b/components/flow3r_bsp/flow3r_bsp_ad7147.c index ecce90c52e377ae2e5fd292852b3681f0a640550..387cb257299a7e7e2cb9b7d2631d66e29e83bf77 100644 --- a/components/flow3r_bsp/flow3r_bsp_ad7147.c +++ b/components/flow3r_bsp/flow3r_bsp_ad7147.c @@ -1,13 +1,3 @@ -// note: this code is kind of frankenstein'd out of the -// remains of a refactor that introduced a lot of complexity -// that wasn't needed anymore. -// -// it was an entire thing and we were exhausted and we did not -// clean this up properly. it works, and that's enough for now. -// -// also there is still a major state machine missing so we -// don't see good reason to improve structure at this point. - #include "flow3r_bsp_ad7147.h" #include "flow3r_bsp_ad7147_hw.h" #include "flow3r_bsp_captouch.h" @@ -29,33 +19,115 @@ #include "freertos/task.h" #include "driver/gpio.h" +#include "esp_timer.h" -// #define CAPTOUCH_PROFILING +// #define FLOW3R_BSP_CAPTOUCH_DEBUG_PROFILING +// #define FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING +// #define FLOW3R_BSP_CAPTOUCH_DEBUG_SEQUENCING +// #define FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION +// #define FLOW3R_BSP_CAPTOUCH_DEBUG_PETAL 2 -#ifdef CAPTOUCH_PROFILING -// not available for building recovery -#include "esp_timer.h" -#endif +/* +時折誰かが問う +いつまでどこまで向かう気かと +BABYどこまででも +*/ + +static ad7147_chip_t _top = { + .name = "top", + .is_bot = false, + .num_petals = 4, + .petals = { 0, 4, 6, 8 }, +}; + +static ad7147_chip_t _bot = { + .name = "bot", + .is_bot = true, + .num_petals = 6, + .petals = { 1, 2, 3, 5, 7, 9 }, +}; + +// callback function for the logging feature +static flow3r_bsp_data_callback_t _on_data = NULL; +void flow3r_bsp_ad7147_set_data_callback(flow3r_bsp_data_callback_t fun) { + _on_data = fun; +} + +// modes that the petals are supposed to be in (excl. calibration and +// transients) +static flow3r_bsp_captouch_petal_mode_t _petal_modes[10] = { + PETAL_MODE_2D, PETAL_MODE_1D, PETAL_MODE_2D, PETAL_MODE_1D, PETAL_MODE_2D, + PETAL_MODE_1D, PETAL_MODE_2D, PETAL_MODE_1D, PETAL_MODE_2D, PETAL_MODE_1D, +}; static const char *TAG = "flow3r-bsp-ad7147"; // output data that gets continuously written to and // memcpy'd when the user requests. static flow3r_bsp_captouch_data_t captouch_data; -// lock for captouch_data -static SemaphoreHandle_t captouch_output_lock = NULL; -// task that generates captouch_data -static TaskHandle_t _captouch_task_handle = NULL; -// helper task for the bottom chip -static TaskHandle_t _cursed_task_handle = NULL; -// container for unprocessed petal data and its lock. + +// user-facing calibration data +static ad7147_petal_calib_t calibration_data[10]; +// lock for calibration_data +static SemaphoreHandle_t calibration_lock = NULL; +// calibration data used internally by the chip tasks +static ad7147_petal_calib_t _chip_calibration_data[10]; + +// if you want both locks it's important to always take/free them in the +// same order to avoid deadlock. use these functions to avoid such bugs. +// xTicksToWait is implemented very halfassed and is the wait time for each +// lock, i.e. you might wait twice as long +static BaseType_t xSemaphoreTakeCaptouchOutput(TickType_t xTicksToWait) { + if (xSemaphoreTake(_top.output_lock, xTicksToWait) == pdFALSE) + return pdFALSE; + if (xSemaphoreTake(_bot.output_lock, xTicksToWait) == pdTRUE) return pdTRUE; + xSemaphoreGive(_top.output_lock); + return pdFALSE; +} +static BaseType_t xSemaphoreGiveCaptouchOutput() { + bool ret = true; + ret = (xSemaphoreGive(_bot.output_lock) == pdTRUE) && ret; + ret = (xSemaphoreGive(_top.output_lock) == pdTRUE) && ret; + return ret ? pdTRUE : pdFALSE; +} + +// we just want a function with that name really +static BaseType_t xSemaphoreBonk(SemaphoreHandle_t handle, + TickType_t xTicksToWait) { + if (xSemaphoreTake(handle, xTicksToWait) == pdFALSE) return pdFALSE; + return xSemaphoreGive(handle); +} + +// tasks that generate captouch_data +static TaskHandle_t _top_task_handle = NULL; +static TaskHandle_t _bot_task_handle = NULL; + +// notifications for _*_task_handle +// chip gpio interrupt +#define NOTIF_GPIO 1 +// send this if you have written new target modes to ad7147_chip_t.user_modes to +// apply them. +#define NOTIF_MODE_CHANGE 2 +// send this to start a calibration. +#define NOTIF_CALIB_START 4 +// send this to stop a calibration. note: this does not restore previous +// calibration data. use this only for restarting calibration or applying +// external calibration, else you might end up with poor performance. +#define NOTIF_CALIB_STOP 8 +// send this to if you have written new data to calibration_data to apply it. +#define NOTIF_CALIB_CHANGE 16 +// used internally to synchronize calibration stages between chips for +// consistent EMI, don't send externally. +#define NOTIF_INTERNAL_CALIB_NEXT_STAGE 32 +// used internally to iterate over calibrations. +#define NOTIF_INTERNAL_CALIB_CHANGE 64 + +#define NOTIF_CLEAR_MASK 127 + +// container for unprocessed petal data. // 10 petals, 4 potential pad positions according to // petal_kind_t. some fields are never used. static uint16_t raw_petals[10][4]; -// lock for parts of raw_petals: only petal indices -// that are served by the bottom chip (all uneven ones -// and 2). -static SemaphoreHandle_t raw_petal_bot_chip_lock = NULL; typedef struct { size_t petal_number; @@ -69,7 +141,22 @@ typedef struct { static press_latch_t latches[10]; -static inline void petal_process(uint8_t index); +static void petal_process(int index, uint32_t timestamp, int mode, + SemaphoreHandle_t lock); + +static void petals_process(ad7147_chip_t *chip, uint32_t timestamp) { + for (int i = 0; i < chip->num_petals; i++) { + int petal = chip->petals[i]; + petal_process(petal, timestamp, chip->modes[petal], chip->output_lock); + } +} + +static void petals_clear(ad7147_chip_t *chip, uint32_t timestamp) { + for (int i = 0; i < chip->num_petals; i++) { + int petal = chip->petals[i]; + petal_process(petal, timestamp, 0, chip->output_lock); + } +} // DATASHEET VIOLATION 1 // target value that we ideally wanna see from an idle petal. @@ -175,28 +262,163 @@ static bool _interrupt_shared = false; #error "captouch not implemented for this badge generation" #endif -static ad7147_chip_t _top = { - .name = "top", - .is_bot = false, - .nchannels = 12, - .sequence = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, -1 }, -}; +static uint8_t get_petal_pad_ch(uint8_t petal, petal_kind_t kind) { + // returns the channel but not the chip. defaults to channel 0. + // pretty bad function but does the trick for now. + static uint8_t cache[40] = { + 0, + }; + int index = kind + 4 * petal; + if (cache[index]) return cache[index] - 1; + int len; + len = sizeof(_map_bot) / sizeof(pad_mapping_t); + for (int i = 0; i < len; i++) { + if (_map_bot[i].petal_number == petal && _map_bot[i].pad_kind == kind) { + cache[index] = i + 1; + return i; + } + } + len = sizeof(_map_top) / sizeof(pad_mapping_t); + for (int i = 0; i < len; i++) { + if (_map_top[i].petal_number == petal && _map_top[i].pad_kind == kind) { + cache[index] = i + 1; + return i; + } + } + return 0; // should never be reached +} -static ad7147_chip_t _bot = { - .name = "bot", - .is_bot = true, - // don't change this field pls there's a bunch of hardcoded stuff around :3 - .nchannels = 13, - // first and last must be >= 7 bc i2c golf reasons - .sequence = { 8, 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 9 }, -}; +void _write_petal_mode_seq(uint16_t **write_head, uint8_t petal, + flow3r_bsp_captouch_petal_mode_t mode) { + if (petal & 1) { + switch (mode) { + case PETAL_MODE_OFF: // c is a clown language + break; + case PETAL_MODE_0D: + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_base); + **write_head |= 1 << get_petal_pad_ch(petal, petal_pad_tip); + *write_head += 1; + break; + case PETAL_MODE_1D: + case PETAL_MODE_2D: + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_base); + *write_head += 1; // the * is its little clown nose + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_tip); + *write_head += 1; + break; + } + } else { + switch (mode) { + case PETAL_MODE_OFF: + break; + case PETAL_MODE_0D: + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_base); + **write_head |= 1 << get_petal_pad_ch(petal, petal_pad_cw); + **write_head |= 1 << get_petal_pad_ch(petal, petal_pad_ccw); + *write_head += 1; + break; // it's good for when you want to + case PETAL_MODE_1D: + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_base); + *write_head += 1; + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_cw); + **write_head |= 1 << get_petal_pad_ch(petal, petal_pad_ccw); + *write_head += 1; + break; + case PETAL_MODE_2D: // be silly + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_base); + *write_head += 1; + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_cw); + *write_head += 1; + **write_head = 1 << get_petal_pad_ch(petal, petal_pad_ccw); + *write_head += 1; + break; + } + } +} + +// hardcoding this one. +// first and last must be >= 7 bc i2c golf reasons. +// keeping petal 2 away from the swap helps w noise and/or our ocd a bit. +static const uint16_t _cursed_seq[] = { 1 << 8, 1 << 0, 1 << 1, 1 << 2, + 1 << 3, 1 << 10, 1 << 11, 1 << 12, + 1 << 4, 1 << 5, 1 << 6, 1 << 7, + 1 << 9 }; + +static int _petal_modes_to_sequence(ad7147_chip_t *chip) { + uint16_t *write_head = chip->sequence; + for (int j = 0; j < chip->num_petals; j++) { // go thru bot chip petals + int petal = chip->petals[j]; + _write_petal_mode_seq(&write_head, petal, chip->modes[petal]); + } + int len = write_head - chip->sequence; + assert(len < 14 || len >= 0); + memset(write_head, 0, (13 - len) * sizeof(uint16_t)); + + chip->cursed_active = len == 13; + if (chip->cursed_active) { + memcpy(chip->sequence, _cursed_seq, 13 * sizeof(uint16_t)); + } +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_SEQUENCING + write_head = chip->sequence; + printf("%s: applying %d-sequence: ", chip->name, len); + while ((write_head - chip->sequence) < 13) + printf("%d ", (int)(*write_head)), write_head++; + if (chip->cursed_active) printf(" (cursed sequence)"); + printf("\n"); +#endif + return len; +} static ad7147_sequence_stage_t _cursed_swap_stage; +static inline int _mode_num_channels(flow3r_bsp_captouch_petal_mode_t mode) { + switch (mode) { + case PETAL_MODE_0D: + return 1; + case PETAL_MODE_1D: + return 2; + case PETAL_MODE_2D: + return 3; + default: + return 0; + } +} + +static int _get_calib_data_index(ad7147_chip_t *chip, int i, int *petal_ret, + int *index_ret) { + assert(chip == &_top || chip == &_bot); + bool top = chip == &_top; + const pad_mapping_t *map = top ? _map_top : _map_bot; + int step = chip->sequence[i]; + for (int j = 0; j < 13; j++) { + if (step & (1 << j)) { + int petal = map[j].petal_number; + int calib_index = _mode_num_channels(chip->modes[petal]) - 1; + if (calib_index < 0) continue; + if (calib_index) { + int offset = map[j].pad_kind; + if (petal & 1) { + if (offset == 3) offset = 1; + } else { // treat tip and base as same bc bw compat + if (offset == 3) offset = 0; + } + if (calib_index == 1 && offset > 1) offset = 1; + if (calib_index == 2) calib_index = 3; + calib_index += offset; + } + *petal_ret = petal; + *index_ret = calib_index; + return true; + } + } + return false; +} + void get_stage_hw_config(ad7147_chip_t *chip, ad7147_sequence_t *seq_out, - uint8_t i, uint8_t channel) { - int8_t offset = chip->channels[channel].afe_offset; - seq_out->stages[i].channel_mask = 1 << channel; + uint8_t i, uint16_t channel_mask) { + int offset = chip->stages[i].afe_offset; + offset = offset > 126 ? 126 : offset; + seq_out->stages[i].channel_mask = channel_mask; // DATASHEET VIOLATION 2 // the datasheet recommends to connect captouch pads to the internal bias // while they are not being measured. here's why we're not doing that: @@ -236,11 +458,37 @@ void get_stage_hw_config(ad7147_chip_t *chip, ad7147_sequence_t *seq_out, } } +static void _notify_from_isr(uint32_t mask, TaskHandle_t handle) { + if (handle == NULL) return; + BaseType_t xHigherPriorityTaskWoken; + xTaskNotifyFromISR(handle, mask, eSetBits, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +} + +static void _notify(uint32_t mask, TaskHandle_t handle) { + if (handle == NULL) return; + xTaskNotify(handle, mask, eSetBits); + portYIELD(); +} + +static void _notify_both(uint32_t mask) { + _notify(mask, _top.task); + _notify(mask, _bot.task); +} + // bad hack, see below -static esp_err_t _cursed_sequence_request(ad7147_chip_t *chip, bool init, - bool recalib) { - int8_t *seq = chip->sequence; +static esp_err_t _cursed_sequence_request(ad7147_chip_t *chip) { + uint16_t *seq = chip->sequence; ad7147_sequence_t seq_out; + int petal, calib_index; + if (_get_calib_data_index(chip, 12, &petal, &calib_index)) { + chip->stages[12].amb = _chip_calibration_data[petal].amb[calib_index]; + chip->stages[12].afe_offset = + _chip_calibration_data[petal].afe[calib_index]; + chip->stages[0].amb = _chip_calibration_data[petal].amb[calib_index]; + chip->stages[0].afe_offset = + _chip_calibration_data[petal].afe[calib_index]; + } get_stage_hw_config(chip, &seq_out, 0, seq[12]); memcpy(&_cursed_swap_stage, &(seq_out.stages[0]), sizeof(ad7147_sequence_stage_t)); @@ -253,35 +501,60 @@ static esp_err_t _cursed_sequence_request(ad7147_chip_t *chip, bool init, // In case of the bottom chip we request a 13-sequence, so that's // not possible, so we use a hardcoded hack to save the 13th // sequence step configuration data for later use. -static esp_err_t _sequence_request(ad7147_chip_t *chip, bool init, - bool recalib) { - if (chip->is_bot) { - _cursed_sequence_request(chip, init, recalib); +static void _sequence_request(ad7147_chip_t *chip, bool new_modes) { + if (new_modes) { + chip->len_sequence = _petal_modes_to_sequence(chip); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_PROFILING + } else { + uint16_t prev_seq[13]; + memcpy(prev_seq, chip->sequence, sizeof(chip->sequence)); + _petal_modes_to_sequence(chip); + if (memcmp(prev_seq, chip->sequence, sizeof(chip->sequence))) { + printf("sequence mismatch!\n"); + } +#endif } - int8_t *seq = chip->sequence; + int len = chip->len_sequence; + uint16_t *seq = chip->sequence; + + if (chip->cursed_active) { + _cursed_sequence_request(chip); + len = 12; + } + ad7147_sequence_t seq_out = { - .len = chip->nchannels, + .len = len, + .interrupt_stage = 0, }; - seq_out.len = seq_out.len > 12 ? 12 : seq_out.len; + for (int i = 0; i < len; i++) { + int petal, calib_index; + assert(_get_calib_data_index(chip, i, &petal, &calib_index)); + chip->stages[i].amb = _chip_calibration_data[petal].amb[calib_index]; + chip->stages[i].afe_offset = + _chip_calibration_data[petal].afe[calib_index]; + } for (size_t i = 0; i < seq_out.len; i++) { get_stage_hw_config(chip, &seq_out, i, seq[i]); } esp_err_t ret; if ((ret = ad7147_hw_configure_stages(&chip->dev, &seq_out)) != ESP_OK) { - return ret; + ESP_LOGE(TAG, "%s: requesting next sequence failed: %s", chip->name, + esp_err_to_name(ret)); + } + if (!len) { + petals_clear(chip, esp_timer_get_time()); } - return ESP_OK; } -static bool _channel_afe_tweak(ad7147_chip_t *chip, size_t cix) { - int32_t cur = chip->channels[cix].amb; +static bool _stage_afe_tweak(ad7147_chip_t *chip, size_t cix) { + int32_t cur = chip->stages[cix].amb; int32_t target = _calib_target; int32_t diff = (cur - target) / _calib_incr_cap; if (diff < 1 && diff > -1) { return false; } - int32_t offset = chip->channels[cix].afe_offset; + int32_t offset = chip->stages[cix].afe_offset; if (offset <= 0 && diff < 0) { return false; } @@ -295,7 +568,7 @@ static bool _channel_afe_tweak(ad7147_chip_t *chip, size_t cix) { if (offset > 126) { offset = 126; } - chip->channels[cix].afe_offset = offset; + chip->stages[cix].afe_offset = offset; return true; } @@ -306,192 +579,170 @@ static bool _channel_afe_tweak(ad7147_chip_t *chip, size_t cix) { // Should maybe get a proper name someday. Sorry. Only used by // raw_data_to_petal_pads atm, maybe it should just absorb it, it's // not that big. -static void _on_chip_data(ad7147_chip_t *chip, uint16_t *values, size_t len) { +static void _on_chip_data(ad7147_chip_t *chip, uint16_t *values, int len) { assert(chip == &_top || chip == &_bot); bool top = chip == &_top; const pad_mapping_t *map = top ? _map_top : _map_bot; - for (uint8_t i = 0; i < len; i++) { - raw_petals[map[i].petal_number][map[i].pad_kind] = values[i]; - } -} - -// Generates raw data and sends it to raw_petals[][]. -// Yes, raw data is processed. Hah. You have been lied to. -// A summary of lies: -// - While a calibration is running, data output isn't updated. This means -// we don't need to waste our time with updating raw_petals either. Not so -// bad right? -// - Calibration is a composition of "coarse" hardware biasing (that AFE thing -// that keeps popping up), plus a residual "fine" software offset. -// Since they both work in tandem, we decided to consider raw data whatever -// is left after both of them are applied, so raw_petals doesn't hold the -// raw readings, but already has the fine offset applied. This makes it -// considerably harder to forget to apply it. -static void raw_data_to_petal_pads(ad7147_chip_t *chip, uint16_t *data, - size_t len) { - if (chip->calibration_cycles > 0) { - // We're doing a calibration cycle on our channels. Instead of writing - // the data to channel->cdc, write it to channel->amb_meas. - int8_t j = chip->calibration_cycles - 1; - if (j < _AD7147_CALIB_CYCLES) { // throw away first few datapoints - for (int8_t i = 0; i < len; i++) { - uint8_t k = chip->sequence[i]; - chip->channels[k].amb_meas[j] = data[i]; + for (int i = 0; i < len; i++) { + int step = chip->sequence[i]; + int num_fields = 0; + int fields[3]; + for (int j = 0; j < (top ? 12 : 13); j++) { + if (step & (1 << j)) { + fields[num_fields] = j; + num_fields++; + if (num_fields == 3) break; } } - } else { - // Normal measurement, apply to channel->cdc. - for (size_t i = 0; i < len; i++) { - uint8_t k = chip->sequence[i]; - chip->channels[k].cdc = data[i]; + if (!num_fields) continue; + int value = values[i] / num_fields; + for (int k = 0; k < num_fields; k++) { + int petal = map[fields[k]].petal_number; + int pad = map[fields[k]].pad_kind; + if (petal >= 10 || petal < 0 || pad >= 4 || pad < 0) { + ESP_LOGE(TAG, "out of bounds at petal %d pad %d stage %d\n", + petal, pad, i); + } else { + raw_petals[petal][pad] = value; + } } } +} - bool recalib = chip->calibration_external; +// lazy copypasta code :3 +static void _on_chip_calib_data(ad7147_chip_t *chip, uint16_t *amb, + uint16_t *afe, size_t len) { + for (int i = 0; i < len; i++) { + int petal, calib_index; + assert(_get_calib_data_index(chip, i, &petal, &calib_index)); + _chip_calibration_data[petal].amb[calib_index] = amb[i]; + _chip_calibration_data[petal].afe[calib_index] = afe[i]; + } +} - // Deal with calibration pending flag, possibly starting calibration. - if (chip->calibration_pending) { - if (!chip->calibration_active) { - ESP_LOGI(TAG, "%s: calibration starting...", chip->name); - chip->calibration_cycles = _AD7147_CALIB_CYCLES + 2; - chip->calibration_active = true; +static void raw_data_to_calib(ad7147_chip_t *chip, uint16_t *data, size_t len) { + if (chip->calibration_cycles > 0) { + // We're doing a calibration cycle on our channels. Instead of + // writing the data to channel->cdc, write it to channel->amb_meas. + int cycle = chip->calibration_cycles - 1; + if (cycle < _AD7147_CALIB_CYCLES) { // throw away first few points + for (int i = 0; i < len; i++) { + chip->stages[i].amb_meas[cycle] = data[i]; + } } - chip->calibration_pending = false; } - if (chip->calibration_active) { - // Deal with active calibration. - chip->calibration_cycles--; - if (chip->calibration_cycles <= 0) { - // Calibration measurements done. Calculate average amb data for - // each channel. - for (size_t i = 0; i < chip->nchannels; i++) { - uint32_t avg = 0; - for (uint8_t j = 0; j < _AD7147_CALIB_CYCLES; j++) { - avg += chip->channels[i].amb_meas[j]; - } - chip->channels[i].amb = avg / _AD7147_CALIB_CYCLES; + // Deal with active calibration. + chip->calibration_cycles--; + if (chip->calibration_cycles <= 0) { + // Calibration measurements done. Calculate average amb data for + // each channel. + uint16_t amb[13]; + uint16_t afe[13]; + uint16_t rerun = 0; + for (size_t i = 0; i < len; i++) { + uint32_t avg = 0; + for (uint8_t j = 0; j < _AD7147_CALIB_CYCLES; j++) { + avg += chip->stages[i].amb_meas[j]; } - + chip->stages[i].amb = avg / _AD7147_CALIB_CYCLES; // Can we tweak the AFE to get a better measurement? - uint16_t rerun = 0; - for (size_t i = 0; i < chip->nchannels; i++) { - if (_channel_afe_tweak(chip, i)) { - rerun |= (1 << i); + if (_stage_afe_tweak(chip, i)) { + rerun |= (1 << i); + } + amb[i] = chip->stages[i].amb; + afe[i] = chip->stages[i].afe_offset; + } + _on_chip_calib_data(chip, amb, afe, len); + if (rerun != 0) { + chip->calibration_cycles = _AD7147_CALIB_CYCLES + 5; + _notify(NOTIF_INTERNAL_CALIB_CHANGE, chip->task); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s calibration stage %d: repeating ", chip->name, + chip->calibration_stage); + for (int i = 0; i < 13; i++) { + if (rerun & 1 << i) { + printf("%d, ", i); } } - if (rerun != 0) { - chip->calibration_cycles = _AD7147_CALIB_CYCLES + 2; - recalib = true; - } else { - chip->calibration_active = false; - ESP_LOGI(TAG, "%s: calibration done.", chip->name); + printf(" (%d, %d)\n", (int)rerun, (int)len); + for (int i = 0; i < 13; i++) { + if (rerun & 1 << i) { + printf(" %s stage %d: amb %d, afe %d\n", chip->name, i, + (int)chip->stages[i].amb, + (int)chip->stages[i].afe_offset); + } } - } - } else { - // Submit data to higher level for processing. - uint16_t val[13]; - for (size_t i = 0; i < chip->nchannels; i++) { - int32_t cdc = chip->channels[i].cdc; - int32_t amb = chip->channels[i].amb; - int32_t diff = cdc - amb; - val[i] = diff < 0 ? 0 : (diff > 65535 ? 65535 : diff); - } - _on_chip_data(chip, val, chip->nchannels); - } +#endif + } else { +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s calibration stage %d: done\n", chip->name, + chip->calibration_stage); + for (int i = 0; i < len; i++) { + printf(" %s stage %d: amb %d, afe %d\n", chip->name, i, + (int)chip->stages[i].amb, + (int)chip->stages[i].afe_offset); + } +#endif - if (recalib) { - esp_err_t ret; - if ((ret = _sequence_request(chip, false, recalib)) != ESP_OK) { - ESP_LOGE(TAG, "%s: requesting next sequence failed: %s", chip->name, - esp_err_to_name(ret)); - } - if (chip->calibration_external) { - chip->calibration_external = false; - ESP_LOGI(TAG, "%s: captouch calibration updated", chip->name); + chip->calibration_stage_active = false; + + ad7147_chip_t *other = chip == &_top ? &_bot : &_top; + _notify(NOTIF_INTERNAL_CALIB_NEXT_STAGE, other->task); + + ESP_LOGI(TAG, "%s: calibration stage done.", chip->name); } } } -// could probably delete half of this but meh who cares -esp_err_t flow3r_bsp_ad7147_chip_init(ad7147_chip_t *chip) { - esp_err_t ret; - for (size_t i = 0; i < chip->nchannels; i++) { - chip->channels[i].amb = 0; - } - if ((ret = ad7147_hw_init(&chip->dev)) != ESP_OK) { - return ret; +static void raw_data_to_petal_pads(ad7147_chip_t *chip, uint16_t *data, + size_t len) { + // Normal measurement, apply to channel->cdc. + for (size_t i = 0; i < len; i++) { + chip->stages[i].cdc = data[i]; } - chip->calibration_pending = true; - if ((ret = _sequence_request(chip, true, true)) != ESP_OK) { - return ret; + // Submit data to higher level for processing. + uint16_t val[13]; + for (size_t i = 0; i < len; i++) { + int32_t cdc = chip->stages[i].cdc; + int32_t amb = chip->stages[i].amb; + int32_t diff = cdc - amb; + val[i] = diff < 0 ? 0 : (diff > 65535 ? 65535 : diff); } - return ESP_OK; + _on_chip_data(chip, val, len); } -static bool _chip_process(ad7147_chip_t *chip) { - ad7147_hw_t *device = &chip->dev; - // Read complete status register. This acknowledges interrupts. - uint16_t st = 0; - if (!ad7147_hw_get_and_clear_completed(device, &st)) { - return false; - } - - // Nothing to do if no stages are expected to be read. - if (device->num_stages < 1) { - return false; - } - - // Bit indicating the conversion has been complete for the requested number - // of stages. - uint16_t complete_bit = (1 << (device->num_stages - 1)); - if (!(st & complete_bit)) return false; - - uint16_t data[12]; - size_t count = device->num_stages; - if (!ad7147_hw_get_cdc_data(device, data, count)) { - return false; - } - raw_data_to_petal_pads(chip, data, count); - return true; +// (sunshine emoji) tell the captouch task that top chip data is available :D +static void _top_isr(void *data) { + _notify_from_isr(NOTIF_GPIO, _top_task_handle); } -static void _notify_from_isr(uint32_t mask, TaskHandle_t handle) { - if (handle == NULL) return; - BaseType_t xHigherPriorityTaskWoken; - xTaskNotifyFromISR(handle, mask, eSetBits, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +// (dark cloud emoji) tell the cursed task that it is time for THE PROCEDURE +static void _bot_isr(void *data) { + _notify_from_isr(NOTIF_GPIO, _bot_task_handle); } -static void _notify(uint32_t mask, TaskHandle_t handle) { - if (handle == NULL) return; - xTaskNotify(handle, mask, eSetBits); - portYIELD(); +typedef struct { + uint16_t buffer[13]; + uint16_t buffer_len; + uint16_t reject; + // normal process only + uint16_t previously_filled; + uint16_t interrupt_stage; + // cursed process only + uint16_t step; +} _chip_process_data_t; + +static void _chip_process_reset(_chip_process_data_t *process_data) { + process_data->previously_filled = 0; + process_data->interrupt_stage = 0; + process_data->step = 0; + process_data->buffer_len = 0; + process_data->reject = 2; } -// (sunshine emoji) tell the captouch task that top chip data is available :D -static void _top_isr(void *data) { _notify_from_isr(2, _captouch_task_handle); } - -// (dark cloud emoji) tell the cursed task that it is time for THE PROCEDURE -static void _bot_isr(void *data) { _notify_from_isr(1, _cursed_task_handle); } - -// So here's a thing to generally keep in mind with this driver: -// It runs on interrupts. Which come from a hardware. Which -// needs to know that the interrupt has been read in order to -// be able to send another. So each time u receive an interrupt -// you need to clear it else the machinery will just halt. -// Unless maybe you put a timeout somewhere but that would be too -// reasonable. -// Not sure why we mention this here. -static void _kickstart(void) { - if (_captouch_task_handle == NULL) return; - ulTaskNotifyValueClear(_captouch_task_handle, 0xffffffff); - _notify(2, _captouch_task_handle); - _notify(1, _cursed_task_handle); -} - -#ifdef CAPTOUCH_PROFILING +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING static int64_t _cursed_step_time = 0; static uint32_t _cursed_step_to_step_time[4]; static uint32_t _cursed_step_execution_time[4]; @@ -504,11 +755,11 @@ static uint32_t _cursed_step_execution_time[4]; // poking the main captouch task when data is ready. // this is kinda sensitive and overall weird so we keep our lil crappy profiler // hooked up for now, we're sure we'll need it in the future again. -static bool _cursed_chip_process(ad7147_chip_t *chip, uint8_t *step, - uint16_t *data) { +static bool _cursed_chip_process(ad7147_chip_t *chip, uint32_t *timestamp, + _chip_process_data_t *process_data) { ad7147_hw_t *device = &chip->dev; uint16_t st = 0; -#ifdef CAPTOUCH_PROFILING +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING int64_t time = esp_timer_get_time(); #endif // this boi clears the interrupt flag of the captouch chip so it must @@ -517,31 +768,17 @@ static bool _cursed_chip_process(ad7147_chip_t *chip, uint8_t *step, if (!ad7147_hw_get_and_clear_completed(device, &st)) { return false; } - // since we can only reset the chip sequencer to stage 0, for the 13th - // stage the hardware interrupt should trigger when state 0 is completed. - // this is okay for all steps of the prodecure, so to keep i2c traffic low - // we have it hardcoded to that at all times. - if (!(st & 1)) { -#ifdef CAPTOUCH_PROFILING - // if u trigger this it's not bad, it just means that u generate more - // i2c traffic than absolutely necessary. remove bofh - // xTaskNotifyStateClear below for a demonstrations. with stock config - // this rarely triggers on step 1. don't really care to look into it. - ESP_LOGE(TAG, "cursed chip isr clear fail (step %u, mask %u\n)", - (uint16_t)(*step), st); -#endif - return false; - } // let's pretend for fun that channel0 is read by stage0 and so forth. // makes describing this a lot easier. // also i2c traffic functions are highly golfed, pls don't tear them apart // it saves like 1% cpu so be cool~ - switch (*step) { + switch (process_data->step) { case 0: // read all 12 "regular" channels into the array - if (!ad7147_hw_get_cdc_data(device, data, 12)) { + if (!ad7147_hw_get_cdc_data(device, process_data->buffer, 12)) { return false; } + process_data->buffer_len = 12; // reconfigures stage0 to channel 13 config, set up sequencer // to loop only stage0 (<-actually a lie but dw abt it~) if (!ad7147_hw_modulate_stage0_and_reset(device, @@ -559,9 +796,11 @@ static bool _cursed_chip_process(ad7147_chip_t *chip, uint8_t *step, // so we wait for another cycle. case 2: // grab data for channel 13 - if (!ad7147_hw_get_cdc_data(device, &(data[12]), 1)) { + if (!ad7147_hw_get_cdc_data(device, &(process_data->buffer[12]), + 1)) { return false; } + *timestamp = esp_timer_get_time(); // reconfigure stage0 to channel 0 config, set up sequencer // to loop all stages if (!ad7147_hw_modulate_stage0_and_reset(device, NULL)) { @@ -573,135 +812,436 @@ static bool _cursed_chip_process(ad7147_chip_t *chip, uint8_t *step, if (!ad7147_hw_get_and_clear_completed(device, &st)) { return false; } - // processing data here. - xSemaphoreTake(raw_petal_bot_chip_lock, portMAX_DELAY); - raw_data_to_petal_pads(chip, data, 13); - xSemaphoreGive(raw_petal_bot_chip_lock); - // notify the main captouch task that data is ready - _notify(1, _captouch_task_handle); + if (process_data->buffer_len != 12) { + process_data->step = 0; + return false; + } + process_data->buffer_len = 13; break; - // case 3: "whoa whoa whoa if you do the same here as with case 1 - // you'd wait for the whole 12-sequence without any good - // reason!" + // case 3: + // "whoa whoa whoa if you do the same here as with case 1 + // you'd wait for the whole 12-sequence without any good + // reason!" // observant, but there's a hidden trick! both case2->case3 and // case 3->case0 have the sequencer configure to 12 steps but // consider: case 2 resets to stage0 (only stage we can reset to, // remember?), and the next interrupt is triggered once that is // complete! that is the magic of ISR trigger reconf laziness! } -#ifdef CAPTOUCH_PROFILING - _cursed_step_to_step_time[*step] = time - _cursed_step_time; +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING + _cursed_step_to_step_time[process_data->step] = time - _cursed_step_time; _cursed_step_time = esp_timer_get_time(); - _cursed_step_execution_time[*step] = _cursed_step_time - time; + _cursed_step_execution_time[process_data->step] = _cursed_step_time - time; #endif - *step = ((*step) + 1) % 4; + process_data->step = (process_data->step + 1) % 4; + return !process_data->step; +} + +static bool _chip_process(ad7147_chip_t *chip, uint32_t *timestamp, + _chip_process_data_t *process_data) { + if (chip->cursed_active) + return _cursed_chip_process(chip, timestamp, process_data); + ad7147_hw_t *device = &chip->dev; + // Read complete status register. This acknowledges interrupts. + uint16_t st = 0; + if (!ad7147_hw_get_and_clear_completed(device, &st)) { + return false; + } + + // Nothing to do if no stages are expected to be read. + if (device->num_stages < 1) { + return false; + } + + int count = device->num_stages; + process_data->previously_filled |= st; + if (process_data->previously_filled < (1 << count) - 1) { + return false; + } + if (!ad7147_hw_get_cdc_data(device, process_data->buffer, count)) { + return false; + } + *timestamp = esp_timer_get_time(); + // so here's another curveball: we can't really lock on the output register + // of the captouch driver, it just keeps writing to it, so we get FUTURE + // DATA, whoa! this is actually bad because you may receive the same NO MORE + // FUTURE BUT NOW PRESENT DATA in the next round, because if you do any sort + // of velocity calculation this will bite u ^w^. + // we'd love to use the reset trick from the cursed sequence here, but turns + // out the jank behaves slightly different and procudes twitches on the + // 11th/last channel. so we're chasing st instead by moving around the + // interrupt stage. fun :3. this is actually faster than the reset sequence. + // and yeeeah for some applications future data is fine, and then we're + // sacrificing precious cycles here. but it's typically less than a ms. it's + // okay. it's the better default. also it's silly code :3 + if (!ad7147_hw_get_and_clear_completed(device, &st)) return false; + st = st & ((1 << count) - 1); // sanitize + if (st) { + int new_interrupt_stage = -1; + for (int i = 0; i < count; i++) { + int stage = (count + process_data->interrupt_stage - i) % count; + if (st & 1 << stage) { + new_interrupt_stage = stage; + break; + } + } + if (new_interrupt_stage < 0) { + ESP_LOGE(TAG, "bad stage vector"); + } + // there is a question to be asked if we gain anything by not setting + // the interrupt to "slice apart" petals. due to random timing between + // isr receive and us grabbing the data it's a bit unpredictable, but + // we'd definitely catch much less ~positional data temporal + // dyslocation~ phenomena :3. but it'd be slower. and probably not super + // duper reliable. we think we wanna be lazy here and argue that we care + // most about the minimum data quality, which does not change from + // adding this. we get some datarate++ in return. yay datarate! + if (new_interrupt_stage != process_data->interrupt_stage) { + if (!ad7147_hw_override_interrupt_stage( + device, process_data->interrupt_stage)) + return false; + process_data->interrupt_stage = new_interrupt_stage; + } + if (!ad7147_hw_get_and_clear_completed(device, &st)) return false; + if (st >= (1 << count) - 1) _notify(NOTIF_GPIO, chip->task); + } + process_data->buffer_len = count; + process_data->previously_filled = st; return true; } -// wrapper/data storage for the above. -static void _cursed_task(void *data) { - uint8_t step = 0; - uint16_t buffer[13]; - for (;;) { - uint32_t notif; // ignoring this - if (xTaskNotifyWait(0, 3, ¬if, portMAX_DELAY) == pdFALSE) { - ESP_LOGE(TAG, "Notification receive failed: cursed task"); - continue; +static bool _chip_process_kickstart(ad7147_chip_t *chip) { + ad7147_hw_t *device = &chip->dev; + uint16_t st = 0; + if (!ad7147_hw_get_and_clear_completed(device, &st)) { + ESP_LOGE(TAG, "kickstart failed"); + return false; + } + return true; +} + +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_PROFILING +typedef struct { + uint32_t timestamps[100]; + uint32_t index; + ad7147_chip_t *chip; +} _profiler_t; + +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING +static void _cursed_profiler_run() { + for (uint16_t i = 0; i < 4; i++) { + uint16_t k = (i + 3) % 4; + printf("last bot step %u to step %u time: %luus\n", k, i, + _cursed_step_to_step_time[i]); + } + for (uint16_t i = 0; i < 4; i++) { + printf("last bot step %u execution time: %luus\n", i, + _cursed_step_execution_time[i]); + } +} +#endif + +static void _profiler_run(_profiler_t *self, uint32_t timestamp) { + if (self->index < 100) { + self->timestamps[self->index] = timestamp; + self->index++; + } else { + uint32_t avg = self->timestamps[99] - self->timestamps[3]; + avg /= (99 - 3); + uint32_t min = UINT32_MAX; + uint32_t max = 0; + for (int i = 3; i < 99; i++) { + uint32_t time = self->timestamps[i + 1] - self->timestamps[i]; + if (time > max) max = time; + if (time < min) min = time; + } + if (chip->is_bot) { + printf( + "cycle time bot [us]: | %5lu %5lu " + "%5lu\n", + min, avg, max); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CURSED_PROFILING + if (chip->cursed_active) { + _cursed_profiler_run(); + } +#endif + } else { + printf("cycle time top [us]: %5lu %5lu %5lu |\n", min, avg, + max); } - _cursed_chip_process(&_bot, &step, buffer); + self->index = 0; } } +#endif -static void _task(void *data) { - (void)data; +static void _calibration_init_next_stage(ad7147_chip_t *chip, + _chip_process_data_t *process_data) { + chip->calibration_proceed = false; + int stage = chip->calibration_stage - 1; + if (stage > 0) { +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s: calibration stage %d\n", chip->name, stage); +#endif + chip->calibration_active = true; + flow3r_bsp_captouch_petal_mode_t modes[10]; + for (int i = 0; i < 10; i++) { + int limit = 3 - (i & 1); + modes[i] = stage > limit ? limit : stage; + } + chip->calibration_stage = stage; + chip->calibration_stage_active = true; + chip->calibration_cycles = _AD7147_CALIB_CYCLES + 5; + + memcpy(chip->modes, modes, sizeof(chip->modes)); + _sequence_request(chip, true); + _chip_process_reset(process_data); + _chip_process_kickstart(chip); + } else { + if (stage < 0) { + // restore previous calibration + _notify(NOTIF_CALIB_CHANGE, chip->task); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s: calibration stopped\n", chip->name); +#endif + } else { + // need to hold the lock for checking notifs + xSemaphoreTake(calibration_lock, portMAX_DELAY); + uint32_t notif; + // it's okay, we're not clearing any and make it pending later with + // NOTIF_MODE_CHANGE + if ((xTaskNotifyWait(0, 0, ¬if, 0) == pdTRUE) && + (notif & NOTIF_CALIB_CHANGE)) { +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf( + "%s: calibration data ignored because of pending user " + "data\n", + chip->name); +#endif + } else { + memcpy(&calibration_data, &_chip_calibration_data, + sizeof(ad7147_petal_calib_t) * 10); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s: calibration done\n", chip->name); +#endif + } + xSemaphoreGive(calibration_lock); + } + + chip->calibration_stage = 0; + chip->calibration_stage_active = false; + chip->calibration_cycles = 0; + chip->calibration_active = false; + _chip_process_reset(process_data); + _notify(NOTIF_MODE_CHANGE, chip->task); + } +} -#ifdef CAPTOUCH_PROFILING - int64_t top_timer[100]; - uint8_t top_timer_index = 0; - int64_t bot_timer[100]; - uint8_t bot_timer_index = 0; +static void _task(ad7147_chip_t *chip) { + assert(chip == &_bot || chip == &_top); + uint32_t timestamp = 0; + _chip_process_data_t process_data; + _chip_process_reset(&process_data); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_PROFILING + _profiler_t profiler = { index = 0, chip = chip }; #endif for (;;) { -#if defined(CONFIG_FLOW3R_HW_GEN_P4) - bool top = true, bot = true; - vTaskDelay(10 / portTICK_PERIOD_MS); -#else uint32_t notif; - if (xTaskNotifyWait(0, 3, ¬if, portMAX_DELAY) == pdFALSE) { - ESP_LOGE(TAG, "Notification receive failed"); + if (xTaskNotifyWait(0, NOTIF_CLEAR_MASK, ¬if, portMAX_DELAY) == + pdFALSE) { + ESP_LOGE(TAG, "%s: Notification receive failed", chip->name); continue; } - notif = notif & 3; - if (!notif) continue; - bool bot = notif & 1; - bool top = notif & 2; - - if (_interrupt_shared) { - // No way to know which captouch chip triggered the interrupt, so - // process both. - top = true; - bot = true; + if (notif & NOTIF_CALIB_CHANGE) { + xSemaphoreTake(calibration_lock, portMAX_DELAY); + memcpy(&_chip_calibration_data, &calibration_data, + sizeof(ad7147_petal_calib_t) * 10); + xSemaphoreGive(calibration_lock); } + if (notif & (NOTIF_CALIB_STOP | NOTIF_CALIB_CHANGE)) { + if (chip->calibration_active) { + chip->calibration_stage = -1; + _calibration_init_next_stage(chip, &process_data); + } + // drop NOTIF_CALIB_START if _STOP or _CHANGE have been received in + // the same notif + } else if (notif & NOTIF_CALIB_START) { + chip->calibration_active = true; + // we just wanna bonk the lock to make sure nobody is holding it + xSemaphoreBonk(calibration_lock, portMAX_DELAY); + petals_clear(chip, esp_timer_get_time()); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_CALIBRATION + printf("%s: starting calibration stage\n", chip->name); #endif - if (top) { - uint8_t top_chip_petals[] = { 0, 4, 6, 8 }; - // _chip_process grabs data from i2c and writes it to - // the respective raw petal pad - if (_chip_process(&_top)) { - xSemaphoreTake(captouch_output_lock, portMAX_DELAY); - // this one processes the raw data to user-facing - // parameters (mostly "pressed" and "position") - for (uint8_t i = 0; i < 4; i++) { - petal_process(top_chip_petals[i]); + chip->calibration_stage = 4; + _calibration_init_next_stage(chip, &process_data); + // drop _NEXT_STAGE if (see above) + } else if (notif & NOTIF_INTERNAL_CALIB_NEXT_STAGE) { + chip->calibration_proceed = true; + } + if (chip->calibration_proceed && (!chip->calibration_stage_active)) { + _calibration_init_next_stage(chip, &process_data); + } + if (notif & NOTIF_GPIO) { + bool new_data = _chip_process(chip, ×tamp, &process_data); + if (new_data) { + if (process_data.reject > 0) { + process_data.reject -= 1; + new_data = false; } - xSemaphoreGive(captouch_output_lock); - } -#ifdef CAPTOUCH_PROFILING - if (top_timer_index < 100) { - top_timer[top_timer_index] = esp_timer_get_time(); - top_timer_index++; - } else { - int32_t avg = top_timer[99] - top_timer[3]; - avg /= 1000 * (99 - 3); - printf("average top captouch cycle time: %ldms\n", avg); - top_timer_index = 0; } + if (new_data) { + if (chip->calibration_stage_active) { + raw_data_to_calib(chip, process_data.buffer, + process_data.buffer_len); + } + if (!chip->calibration_active) { + raw_data_to_petal_pads(chip, process_data.buffer, + process_data.buffer_len); + petals_process(chip, timestamp); +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_PROFILING + // not running this during calibration bc it's annoying + _profiler_run(&profiler, timestamp); #endif + } + process_data.buffer_len = 0; + } } - if (bot) { - uint8_t bot_chip_petals[] = { 1, 2, 3, 5, 7, 9 }; - // same as top, but _chip_process has been done already by - // the helper task - we do need to grab an extra lock tho - xSemaphoreTake(captouch_output_lock, portMAX_DELAY); - // grab this one l8r bc higher prio - xSemaphoreTake(raw_petal_bot_chip_lock, portMAX_DELAY); - for (uint8_t i = 0; i < 6; i++) { - petal_process(bot_chip_petals[i]); + if (notif & (NOTIF_MODE_CHANGE | NOTIF_CALIB_CHANGE | + NOTIF_INTERNAL_CALIB_CHANGE)) { + bool new_modes = notif & NOTIF_MODE_CHANGE; + if (new_modes) { + xSemaphoreTake(chip->user_lock, portMAX_DELAY); + new_modes = + memcmp(chip->modes, chip->user_modes, sizeof(chip->modes)); + if (new_modes) { + memcpy(chip->modes, chip->user_modes, sizeof(chip->modes)); + } + xSemaphoreGive(chip->user_lock); } - xSemaphoreGive(raw_petal_bot_chip_lock); - xSemaphoreGive(captouch_output_lock); -#ifdef CAPTOUCH_PROFILING - if (bot_timer_index < 100) { - bot_timer[bot_timer_index] = esp_timer_get_time(); - bot_timer_index++; + _sequence_request(chip, new_modes); + _chip_process_reset(&process_data); + _chip_process_kickstart(chip); + } + } +} + +static void _bot_task(void *data) { + (void)data; + _task(&_bot); +} + +static void _top_task(void *data) { + (void)data; + _task(&_top); +} + +void flow3r_bsp_ad7147_get_petal_modes(flow3r_bsp_captouch_petal_mode_t *data) { + memcpy(data, _petal_modes, sizeof(_petal_modes)); +} + +static void _set_petal_modes(flow3r_bsp_captouch_petal_mode_t *data) { + for (int i = 0; i < 2; i++) { + ad7147_chip_t *chip = i ? &_bot : &_top; + xSemaphoreTake(chip->user_lock, portMAX_DELAY); + memcpy(chip->user_modes, data, sizeof(chip->user_modes)); + xSemaphoreGive(chip->user_lock); + _notify(NOTIF_MODE_CHANGE, chip->task); + } +} + +void flow3r_bsp_ad7147_set_petal_modes(flow3r_bsp_captouch_petal_mode_t *data) { + memcpy(_petal_modes, data, sizeof(_petal_modes)); + _set_petal_modes(_petal_modes); +} + +void flow3r_bsp_ad7147_calibrate() { + // the notification would set them as soon as it is processed + // but we do want those flags earlier + // would be nice if we could just read the notification vector + _bot.calibration_active = true; + _top.calibration_active = true; + _notify_both(NOTIF_CALIB_START); +} + +int flow3r_bsp_ad7147_calibrating() { + if (_bot.calibration_active || _top.calibration_active) { + return _bot.calibration_stage > _top.calibration_stage + ? _bot.calibration_stage + : _top.calibration_stage; + } + return 0; +} + +bool flow3r_bsp_ad7147_get_calibration_data(int32_t *data) { + if (_top.calibration_active || _bot.calibration_active) return false; + xSemaphoreTake(calibration_lock, portMAX_DELAY); + for (int petal = 0; petal < 10; petal++) { + for (int calib_index = 0; calib_index < 6; calib_index++) { + int data_index = petal * 12 + calib_index * 2; + if ((petal & 1) && calib_index >= 3) { + data[data_index] = 0; + data[data_index + 1] = 0; } else { - int32_t avg = bot_timer[99] - bot_timer[3]; - avg /= 1000 * (99 - 3); - printf("average bot captouch cycle time: %ldms\n", avg); - bot_timer_index = 0; - for (uint16_t i = 0; i < 4; i++) { - uint16_t k = (i + 3) % 4; - printf("last bot step %u to step %u time: %luus\n", k, i, - _cursed_step_to_step_time[i]); - } - for (uint16_t i = 0; i < 4; i++) { - printf("last bot step %u execution time: %luus\n", i, - _cursed_step_execution_time[i]); - } + data[data_index] = calibration_data[petal].amb[calib_index]; + data[data_index + 1] = calibration_data[petal].afe[calib_index]; } -#endif } } + xSemaphoreGive(calibration_lock); + return true; +} + +static uint16_t amb_limit(int32_t data) { + return data > 65535 ? 65535 : (data < 0 ? 0 : data); +} + +static uint8_t afe_limit(int32_t data) { + return data > 126 ? 126 : (data < 0 ? 0 : data); +} + +void flow3r_bsp_ad7147_set_calibration_data(int32_t *data) { + xSemaphoreTake(calibration_lock, portMAX_DELAY); + for (int petal = 0; petal < 10; petal++) { + for (int calib_index = 0; calib_index < 6; calib_index++) { + int data_index = petal * 12 + calib_index * 2; + if (!((petal & 1) && calib_index >= 3)) { + calibration_data[petal].amb[calib_index] = + amb_limit(data[data_index]); + calibration_data[petal].afe[calib_index] = + afe_limit(data[data_index + 1]); + } + } + } + // we must notify while holding the lock so if the calibration has + // not stopped yet it will not write its results to calibration_data + _notify_both(NOTIF_CALIB_CHANGE); + xSemaphoreGive(calibration_lock); +} + +void flow3r_bsp_ad7147_get(flow3r_bsp_captouch_data_t *dest) { + xSemaphoreTakeCaptouchOutput(portMAX_DELAY); + memcpy(dest, &captouch_data, sizeof(captouch_data)); + xSemaphoreGiveCaptouchOutput(); +} + +void flow3r_bsp_ad7147_refresh_events() { + xSemaphoreTakeCaptouchOutput(portMAX_DELAY); + for (uint8_t i = 0; i < 10; i++) { + captouch_data.petals[i].press_event = latches[i].press_event_new; + latches[i].fresh = true; + } + xSemaphoreGiveCaptouchOutput(); +} + +esp_err_t flow3r_bsp_ad7147_chip_init(ad7147_chip_t *chip) { + esp_err_t ret; + for (size_t i = 0; i < 13; i++) { + chip->stages[i].amb = 0; + } + if ((ret = ad7147_hw_init(&chip->dev)) != ESP_OK) { + return ret; + } + return ESP_OK; } esp_err_t _gpio_interrupt_setup(gpio_num_t num, gpio_isr_t isr) { @@ -722,33 +1262,46 @@ esp_err_t _gpio_interrupt_setup(gpio_num_t num, gpio_isr_t isr) { return ESP_OK; } +void assertSemaphoreCreateMutex(SemaphoreHandle_t *target) { + assert(*target == NULL); + *target = xSemaphoreCreateMutex(); + assert(*target != NULL); +} + esp_err_t flow3r_bsp_ad7147_init() { - assert(captouch_output_lock == NULL); - captouch_output_lock = xSemaphoreCreateMutex(); - assert(captouch_output_lock != NULL); - assert(raw_petal_bot_chip_lock == NULL); - raw_petal_bot_chip_lock = xSemaphoreCreateMutex(); - assert(raw_petal_bot_chip_lock != NULL); + assertSemaphoreCreateMutex(&calibration_lock); + + for (int i = 0; i < 2; i++) { + ad7147_chip_t *chip = i ? &_top : &_bot; + assertSemaphoreCreateMutex(&chip->output_lock); + assertSemaphoreCreateMutex(&chip->user_lock); + chip->dev.dev_config.decimation = 0b10; + memcpy(chip->user_modes, _petal_modes, sizeof(chip->user_modes)); + } esp_err_t ret; - _top.dev.dev_config.decimation = 0b01; + for (int i = 0; i < 10; i++) { + captouch_data.petals[i].index = i; + } + _top.dev.addr = flow3r_i2c_addresses.touch_top; - _bot.dev.dev_config.decimation = 0b10; _bot.dev.addr = flow3r_i2c_addresses.touch_bottom; + xTaskCreate(&_top_task, "captouch-top", 4096, NULL, + configMAX_PRIORITIES - 2, &_top_task_handle); + xTaskCreate(&_bot_task, "captouch-bot", 4096, NULL, + configMAX_PRIORITIES - 2, &_bot_task_handle); + + _top.task = _top_task_handle; + _bot.task = _bot_task_handle; + if ((ret = flow3r_bsp_ad7147_chip_init(&_top)) != ESP_OK) { return ret; } if ((ret = flow3r_bsp_ad7147_chip_init(&_bot)) != ESP_OK) { return ret; } - ESP_LOGI(TAG, "Captouch initialized"); - - xTaskCreate(&_task, "captouch", 4096, NULL, configMAX_PRIORITIES - 2, - &_captouch_task_handle); - xTaskCreate(&_cursed_task, "ad7147", 4096, NULL, configMAX_PRIORITIES - 1, - &_cursed_task_handle); if ((ret = gpio_install_isr_service(ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_LOWMED)) != ESP_OK) { @@ -770,131 +1323,285 @@ esp_err_t flow3r_bsp_ad7147_init() { } } - _kickstart(); + flow3r_bsp_ad7147_calibrate(); + ESP_LOGI(TAG, "Captouch initialized"); return ESP_OK; } -void flow3r_bsp_ad7147_calibrate() { - _bot.calibration_pending = true; - _top.calibration_pending = true; -} +// roughly matches the behavior of the legacy api. someday we should have more +// meaningful output units. -bool flow3r_bsp_ad7147_calibrating() { - bool bot = _bot.calibration_pending || _bot.calibration_active; - bool top = _top.calibration_pending || _top.calibration_active; - return bot || top; +#define TOP_PETAL_THRESHOLD 8000 +#define BOTTOM_PETAL_THRESHOLD 12000 +#define PETAL_HYSTERESIS 1000 + +#define PETAL_TOP_GAIN 5300 +#define PETAL_TOP_2_GAIN 6000 +#define PETAL_BOT_GAIN 6500 + +static inline int16_t clip_to_int16(int32_t num) { + return (num > 32767 ? 32767 : (num < -32768 ? -32768 : num)); } -void flow3r_bsp_ad7147_get_calibration_data(int32_t *data) { - while (flow3r_bsp_captouch_calibrating()) { - }; - for (uint8_t i = 0; i < 13; i++) { - if (i < 12) { - data[2 * i] = _top.channels[i].afe_offset; - data[2 * i + 1] = _top.channels[i].amb; +static int32_t top_petal_filter[10]; + +static int32_t petal_pos_gain[10] = { + PETAL_TOP_GAIN, PETAL_BOT_GAIN, PETAL_TOP_2_GAIN, PETAL_BOT_GAIN, + PETAL_TOP_GAIN, PETAL_BOT_GAIN, PETAL_TOP_GAIN, PETAL_BOT_GAIN, + PETAL_TOP_GAIN, PETAL_BOT_GAIN, +}; + +static inline void pos_calc_top(int index, int mode, int *rad, int *phi, + int *raw_sum, uint16_t *raw_petal) { + int32_t cw = raw_petal[petal_pad_cw]; + int32_t ccw = raw_petal[petal_pad_ccw]; + int32_t base = raw_petal[petal_pad_base]; + int32_t tip = ((cw + ccw) * 3) >> 2; + *raw_sum = base + ccw + cw; + if (mode > 1) { + int rad_calc = tip - base; + rad_calc *= petal_pos_gain[index]; + rad_calc /= ((tip + base) >> 2) + 1; + if (mode > 2) { + int phi_calc = cw - ccw; + phi_calc *= petal_pos_gain[index]; + phi_calc /= ((cw + ccw) >> 2) + 1; + phi_calc = (phi_calc * 10) / 6; + + int phi_sq = (phi_calc * phi_calc) >> 15; + int phi_qb = (phi_sq * phi_calc) >> 15; + int blend = (rad_calc + (18 << 10)) >> 3; + phi_calc = (phi_calc * blend + phi_qb * ((1 << 12) - blend)) >> 12; + + phi_calc = (phi_calc * 6) / 10; + + *phi = phi_calc; + } else { + *phi = 0; } - data[2 * i + 24] = _bot.channels[i].afe_offset; - data[2 * i + 25] = _bot.channels[i].amb; + rad_calc -= 4096; + rad_calc += rad_calc >> 3; + *rad = rad_calc; + } else { + *rad = 0; } } -static uint16_t amb_limit(int32_t data) { - return data > 65535 ? 65535 : (data < 0 ? 0 : data); -} - -static uint8_t afe_limit(int32_t data) { - return data > 126 ? 126 : (data < 0 ? 0 : data); +static inline void pos_calc_bot(int index, int mode, int *rad, int *raw_sum, + uint16_t *raw_petal) { + int32_t tip = raw_petal[petal_pad_tip]; + int32_t base = raw_petal[petal_pad_base]; + *raw_sum = base + tip; + if (mode > 1) { + base += ((base * 3) >> 2); // tiny gain correction + int rad_calc = tip - base; + rad_calc *= petal_pos_gain[index]; + rad_calc /= ((tip + base) >> 2) + 1; + *rad = rad_calc; + } else { + *rad = 0; + } } -void flow3r_bsp_ad7147_set_calibration_data(int32_t *data) { - while (flow3r_bsp_captouch_calibrating()) { - }; - for (uint8_t i = 0; i < 13; i++) { - if (i < 12) { - _top.channels[i].afe_offset = afe_limit(data[2 * i]); - _top.channels[i].amb = amb_limit(data[2 * i + 1]); +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT +static int32_t top_petal_filter2[10]; + +static inline void pos_calc_top2(int index, int mode, int *rad, int *phi, + int *raw_sum, uint16_t *raw_petal) { + int32_t cw = raw_petal[petal_pad_cw]; + int32_t ccw = raw_petal[petal_pad_ccw]; + int32_t base = raw_petal[petal_pad_base]; + int32_t tip = (ccw + cw) >> 1; + *raw_sum = base + ccw + cw; + if (mode > 1) { + int rad_calc = tip - base; + rad_calc *= petal_pos_gain[index]; + rad_calc /= ((tip + base) >> 2) + 1; + *rad = rad_calc; + if (mode > 2) { + int phi_calc = cw - ccw; + phi_calc *= petal_pos_gain[index]; + phi_calc /= ((cw + ccw) >> 2) + 1; + *phi = phi_calc; + } else { + *phi = 0; } - _bot.channels[i].afe_offset = afe_limit(data[2 * i + 24]); - _bot.channels[i].amb = amb_limit(data[2 * i + 25]); + } else { + *rad = 0; } - _top.calibration_external = true; - _bot.calibration_external = true; } -void flow3r_bsp_ad7147_get(flow3r_bsp_captouch_data_t *dest) { - xSemaphoreTake(captouch_output_lock, portMAX_DELAY); - memcpy(dest, &captouch_data, sizeof(captouch_data)); - xSemaphoreGive(captouch_output_lock); +static void pos_calc_bot2(int index, int mode, int *rad, int *raw_sum, + uint16_t *raw_petal) { + pos_calc_bot(index, mode, rad, raw_sum, raw_petal); } +#endif -void flow3r_bsp_ad7147_refresh_events() { - xSemaphoreTake(captouch_output_lock, portMAX_DELAY); - for (uint8_t i = 0; i < 10; i++) { - captouch_data.petals[i].press_event = latches[i].press_event_new; - latches[i].fresh = true; +static void petal_process(int index, uint32_t timestamp, int mode, + SemaphoreHandle_t lock) { + // lock only locks multiple readers and one writer. + // for multiple writers you need to lock around this function. + flow3r_bsp_captouch_petal_data_t *petal = &(captouch_data.petals[index]); + if (!mode) { + if (!petal->mode) return; + if (lock) xSemaphoreTake(lock, portMAX_DELAY); + petal->mode = 0; + petal->pressed = false; + petal->coverage = 0; + petal->rad = 0; + petal->phi = 0; +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + petal->rad2 = 0; + petal->phi2 = 0; +#endif + if (lock) xSemaphoreGive(lock); + return; } - xSemaphoreGive(captouch_output_lock); -} + bool top = (index % 2) == 0; -// roughly matches the behavior of the legacy api. someday we should have more -// meaningful output units. + int rad = 0; + int phi = 0; + int raw_sum = 0; -#define TOP_PETAL_THRESHOLD 8000 -#define BOTTOM_PETAL_THRESHOLD 12000 -#define PETAL_HYSTERESIS 1000 + if (top) { + pos_calc_top(index, mode, &rad, &phi, &raw_sum, raw_petals[index]); +#if defined(CONFIG_FLOW3R_HW_GEN_P3) + rad = -rad; +#endif + phi = clip_to_int16(phi); + } else { + pos_calc_bot(index, mode, &rad, &raw_sum, raw_petals[index]); + } + rad = clip_to_int16(rad); -#define POS_AMPLITUDE 40000 -#define POS_AMPLITUDE_SHIFT 2 -#define POS_DIV_MIN 1000 + raw_sum = (raw_sum * 1024) / + (top ? (TOP_PETAL_THRESHOLD) : (BOTTOM_PETAL_THRESHOLD)); -static inline void petal_process(uint8_t index) { - flow3r_bsp_captouch_petal_data_t *petal = &(captouch_data.petals[index]); - int32_t tip = raw_petals[index][petal_pad_tip]; - int32_t cw = raw_petals[index][petal_pad_cw]; - int32_t ccw = raw_petals[index][petal_pad_ccw]; - int32_t base = raw_petals[index][petal_pad_base]; - bool top = (index % 2) == 0; - int32_t thres = top ? (TOP_PETAL_THRESHOLD) : (BOTTOM_PETAL_THRESHOLD); - thres = - petal->pressed ? thres - (PETAL_HYSTERESIS) : thres; // some hysteresis - int32_t distance; - int32_t angle; - int32_t raw_sum; - int8_t div; + bool pressed_prev = petal->pressed; + bool pressed = raw_sum > (pressed_prev ? 896 : 1024); + bool reset_filters = pressed && (!pressed_prev) && (mode > 1); + + if ((!latches[index].press_event_new) || latches[index].fresh) { + latches[index].press_event_new = pressed; + latches[index].fresh = false; + } + raw_sum = raw_sum > 65535 ? 65535 : raw_sum; + +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + int rad2 = rad; + int phi2 = phi; + int raw_sum2 = 0; if (top) { - raw_sum = base + ccw + cw; - div = 3; - tip = (ccw + cw) >> 1; - angle = cw - ccw; - angle *= (POS_AMPLITUDE) >> (POS_AMPLITUDE_SHIFT); - angle /= ((cw + ccw) >> (POS_AMPLITUDE_SHIFT)) + (POS_DIV_MIN); + pos_calc_top2(index, mode, &rad2, &phi2, &raw_sum2, raw_petals[index]); + phi2 = clip_to_int16(phi2); } else { - raw_sum = base + tip; - div = 2; - angle = 0; + pos_calc_bot2(index, mode, &rad2, &raw_sum2, raw_petals[index]); } - distance = tip - base; - distance *= (POS_AMPLITUDE) >> (POS_AMPLITUDE_SHIFT); - distance /= ((tip + base) >> (POS_AMPLITUDE_SHIFT)) + (POS_DIV_MIN); -#if defined(CONFIG_FLOW3R_HW_GEN_P3) - if (top) distance = -distance; + rad2 = clip_to_int16(rad2); #endif - petal->pressed = raw_sum > thres; - if (petal->pressed) { // backwards compat hack for the few ppl who use - petal->raw_coverage = raw_sum / div; // it as a "pressed" proxy - } else { // by comparing it to - petal->raw_coverage = 0; // 0 - } // TODO: undo - if ((!latches[index].press_event_new) || latches[index].fresh) { - latches[index].press_event_new = petal->pressed; - latches[index].fresh = false; +#ifdef FLOW3R_BSP_CAPTOUCH_DEBUG_PETAL + static int print_petal_counter = 0; + int base = raw_petals[index][petal_pad_base]; + int tip = raw_petals[index][petal_pad_tip]; + int cw = raw_petals[index][petal_pad_cw]; + int ccw = raw_petals[index][petal_pad_ccw]; + if (index == (FLOW3R_BSP_CAPTOUCH_DEBUG_PETAL)) { + if (pressed) { + if (!pressed_prev) + printf("data = [\n # cw, ccw/tip, base, rad, phi\n"); + if (!print_petal_counter) { + if (top) { + printf(" (%d, %d, ", cw, ccw); + } else { + printf(" (0, %d, ", tip); + } + printf("%d, %d, %d", base, rad, phi); +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + printf(", %d, %d", rad2, phi2); +#endif + printf("),\n"); + } + print_petal_counter++; + print_petal_counter %= 6; + } else if (pressed_prev) { + printf("]\n"); + print_petal_counter = 0; + } + } +#endif + + if (_on_data) _on_data(index, mode, pressed, timestamp, rad, phi, raw_sum); + + if (mode > 1) { + // filter settings + const int32_t f_div_pow = 4; + const int32_t f_mult_old = 9; + const int32_t f_mult_new = (1 << f_div_pow) - f_mult_old; + + if (reset_filters) { + if (top) { + top_petal_filter[index] = rad; + top_petal_filter[index + 1] = phi; + } + } else { + if (top) { + top_petal_filter[index] = + (f_mult_old * top_petal_filter[index] + f_mult_new * rad) >> + f_div_pow; + top_petal_filter[index + 1] = + (f_mult_old * top_petal_filter[index + 1] + + f_mult_new * phi) >> + f_div_pow; + rad = (f_mult_old * petal->rad + + f_mult_new * top_petal_filter[index]) >> + f_div_pow; + phi = (f_mult_old * petal->phi + + f_mult_new * top_petal_filter[index + 1]) >> + f_div_pow; + } else { + rad = (f_mult_old * petal->rad + f_mult_new * rad) >> f_div_pow; + } + } + +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + if (reset_filters) { + if (top) { + top_petal_filter2[index] = rad2; + top_petal_filter2[index + 1] = phi2; + } + } else { + if (top) { + top_petal_filter2[index] = + (f_mult_old * top_petal_filter2[index] + + f_mult_new * rad2) >> + f_div_pow; + top_petal_filter2[index + 1] = + (f_mult_old * top_petal_filter2[index + 1] + + f_mult_new * phi2) >> + f_div_pow; + rad2 = (f_mult_old * petal->rad2 + + f_mult_new * top_petal_filter2[index]) >> + f_div_pow; + phi2 = (f_mult_old * petal->phi2 + + f_mult_new * top_petal_filter2[index + 1]) >> + f_div_pow; + } else { + rad2 = + (f_mult_old * petal->rad2 + f_mult_new * rad2) >> f_div_pow; + } + } +#endif } - int8_t f_div_pow = 4; - int8_t f_mult_old = 9; - int8_t f_mult_new = (1 << f_div_pow) - f_mult_old; - petal->pos_distance = - (f_mult_old * petal->pos_distance + f_mult_new * distance) >> f_div_pow; - petal->pos_angle = - (f_mult_old * petal->pos_angle + f_mult_new * angle) >> f_div_pow; + if (lock) xSemaphoreTake(lock, portMAX_DELAY); + petal->mode = mode; + petal->pressed = pressed; + petal->coverage = raw_sum; + petal->rad = rad; + petal->phi = phi; +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + petal->rad2 = rad2; + petal->phi2 = phi2; +#endif + if (lock) xSemaphoreGive(lock); } diff --git a/components/flow3r_bsp/flow3r_bsp_ad7147.h b/components/flow3r_bsp/flow3r_bsp_ad7147.h index 61402e068f7c69c58f086cf3a3dd34b628f629e3..44faac3ed3ed3874f32f1ae609443daa2a8f4527 100644 --- a/components/flow3r_bsp/flow3r_bsp_ad7147.h +++ b/components/flow3r_bsp/flow3r_bsp_ad7147.h @@ -35,8 +35,16 @@ #include "flow3r_bsp_ad7147_hw.h" #include "flow3r_bsp_captouch.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + #define _AD7147_CALIB_CYCLES 16 +typedef struct { + uint16_t amb[6]; + uint16_t afe[6]; +} ad7147_petal_calib_t; + // State of an AD7147 channel. Each AD7147 has 13 channels, but can only access // 12 of them at once in a single sequence. typedef struct { @@ -50,7 +58,7 @@ typedef struct { volatile uint16_t amb; // Calibration samples gathered during the calibraiton process. uint16_t amb_meas[_AD7147_CALIB_CYCLES]; -} ad7147_channel_t; +} ad7147_stage_t; // State and configuration of an AD7147 chip. Wraps the low-level structure in // everything required to manage multiple sequences and perform calibration. @@ -62,23 +70,41 @@ typedef struct { // [0, n_channels) are the expected connected channels to the inputs of the // chip. - size_t nchannels; - ad7147_channel_t channels[13]; + ad7147_stage_t stages[13]; + + // modes that the petals are supposed to be in + flow3r_bsp_captouch_petal_mode_t user_modes[10]; + // take this lock to change user_modes, then send a NOTIF_MODE + SemaphoreHandle_t user_lock; + + // actual modes that the petals are in. + flow3r_bsp_captouch_petal_mode_t modes[10]; + // Sequence to be handled by this chip as a 0 right-padded + // list of channel bitmasks that the chip will read + uint16_t sequence[13]; + int len_sequence; + // Whether or not the cursed sequence is active. + bool cursed_active; + + // Task that processes the chip + TaskHandle_t task; + // underlying hardware device + ad7147_hw_t dev; + // number of petals this chip supplies + size_t num_petals; + // list of petal indices, num_petals long + uint8_t petals[6]; - // Sequence to be handled by this chip as a -1 right-padded - // list of channel numbers that the chip will read - int8_t sequence[13]; + // True if calibration is running + bool calibration_active; - ad7147_hw_t dev; - bool failed; - - // Request applying external calibration - volatile bool calibration_pending; - // True if calibration is running or pending - volatile bool calibration_active; - // Set true if external calibration is to be written to hw - volatile bool calibration_external; - int8_t calibration_cycles; + // internal flags used for calibration + bool calibration_stage_active; + bool calibration_proceed; + int calibration_stage; + int calibration_cycles; + + SemaphoreHandle_t output_lock; } ad7147_chip_t; // One of the four possible touch points (pads) on a petal. Top petals have @@ -138,13 +164,17 @@ flow3r_bsp_ad7147_petal_pad_state_t *flow3r_bsp_ad7147_pad_for_petal( void flow3r_bsp_ad7147_calibrate(); // Returns true if captouch is currently calibrating. -bool flow3r_bsp_ad7147_calibrating(); +int flow3r_bsp_ad7147_calibrating(); -// Set/get calibration data. data[] should be at least 50 entries long. -void flow3r_bsp_ad7147_get_calibration_data(int32_t *data); +// Set/get calibration data. data[] should be at least 120 entries long. +bool flow3r_bsp_ad7147_get_calibration_data(int32_t *data); void flow3r_bsp_ad7147_set_calibration_data(int32_t *data); #pragma once void flow3r_bsp_ad7147_get(flow3r_bsp_captouch_data_t *dest); void flow3r_bsp_ad7147_refresh_events(); esp_err_t flow3r_bsp_ad7147_init(); + +void flow3r_bsp_ad7147_set_data_callback(flow3r_bsp_data_callback_t fun); +void flow3r_bsp_ad7147_set_petal_modes(flow3r_bsp_captouch_petal_mode_t *data); +void flow3r_bsp_ad7147_get_petal_modes(flow3r_bsp_captouch_petal_mode_t *data); diff --git a/components/flow3r_bsp/flow3r_bsp_ad7147_hw.c b/components/flow3r_bsp/flow3r_bsp_ad7147_hw.c index f7ca12bb35e0fc53f4f2980f5bf6f380d3a79aca..738fc0c08cda9c3c29decbe1de455bce0a868fa9 100644 --- a/components/flow3r_bsp/flow3r_bsp_ad7147_hw.c +++ b/components/flow3r_bsp/flow3r_bsp_ad7147_hw.c @@ -209,8 +209,6 @@ static esp_err_t _reset_sequencer(const ad7147_hw_t *dev) { } esp_err_t ad7147_hw_init(ad7147_hw_t *device) { - // ;w; y tho - // memset(device, 0, sizeof(ad7147_hw_t)); for (size_t i = 0; i < 12; i++) { for (size_t j = 0; j < 13; j++) { device->stage_config[i].cinX_connection_setup[j] = CIN_BIAS; @@ -251,8 +249,14 @@ esp_err_t ad7147_hw_configure_stages(ad7147_hw_t *device, device->stage_config[i].neg_afe_offset_swap = true; device->stage_config[i].pos_afe_offset_swap = false; } - device->dev_config.sequence_stage_num = seq->len - 1; - device->dev_config.stageX_complete_int_enable[0] = true; + if (seq->len) { + device->dev_config.sequence_stage_num = seq->len - 1; + device->dev_config.stageX_complete_int_enable[seq->interrupt_stage] = + true; + device->dev_config.power_mode = 0; + } else { + device->dev_config.power_mode = 1; // shutdown + } // For our own record (more precisely, for the interrupt handler). device->num_stages = seq->len; @@ -315,6 +319,18 @@ bool ad7147_hw_reset_sequencer(ad7147_hw_t *device) { // namespace/duplicating/whatevs and we feel like putting it here is the better // of two bad options. anyways: +bool ad7147_hw_override_interrupt_stage(ad7147_hw_t *device, + uint8_t interrupt_stage) { + const uint16_t val = 1 << interrupt_stage; + esp_err_t res = + _i2c_write(device, AD7147_REG_STAGE_COMPLETE_INT_ENABLE, val); + if (res != ESP_OK) { + ESP_LOGE(TAG, "captouch i2c failed: %s", esp_err_to_name(res)); + return false; + } + return true; +} + // Configure stage. Defaults to stage_config if config is NULL. static esp_err_t _i2c_golf_override_stage(ad7147_hw_t *dev, uint8_t stage, ad7147_stage_config_t *config) { @@ -348,9 +364,9 @@ static esp_err_t _i2c_golf_override_stage(ad7147_hw_t *dev, uint8_t stage, // helper function for ad7147_hw_modulate_stage0_and_reset static bool _i2c_golf_override_lower_registers(ad7147_hw_t *dev, - uint8_t num_stages) { + int num_stages) { esp_err_t res; - dev->dev_config.sequence_stage_num = num_stages; + if (num_stages >= 0) dev->dev_config.sequence_stage_num = num_stages; #if 0 // writing to 0x00: AD7147_REG_PWR_CONTROL if ((res = _configure_pwr_control(dev)) != ESP_OK) { @@ -414,12 +430,7 @@ bool ad7147_hw_modulate_stage0_and_reset(ad7147_hw_t *dev, num_stages = 11; res = _i2c_golf_override_stage(dev, 0, NULL); } else { - // this should be 0 for max performance, but we're deliberately - // throttling it to match the old 18ms performance in order to not eat - // too much CPU. this can be modified once we upgrade to espidf5.2 and - // this thing uses non-blocking i2c ops, or if there is an api that lets - // users configure performance. - num_stages = 5; // 0; + num_stages = 0; ad7147_stage_config_t config = { 0 }; for (uint8_t j = 0; j < 13; j++) { if (s_conf->channel_mask & (1 << j)) { diff --git a/components/flow3r_bsp/flow3r_bsp_ad7147_hw.h b/components/flow3r_bsp/flow3r_bsp_ad7147_hw.h index 3def723a4da26e2f36c6e9a0d22ed3f2b243b6da..aabfad4fac439606539c4e5427b2354f669c8f69 100644 --- a/components/flow3r_bsp/flow3r_bsp_ad7147_hw.h +++ b/components/flow3r_bsp/flow3r_bsp_ad7147_hw.h @@ -7,6 +7,9 @@ #include "esp_err.h" #include "flow3r_bsp_i2c.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + #include <stdint.h> // 'Global' configuration for the AD7147 captouch controller. @@ -96,7 +99,9 @@ typedef struct { } ad7147_sequence_stage_t; typedef struct { - uint8_t len; // Number of sequencer stages, [1, 12]. + uint16_t interrupt_stage; // at which stage's completion the interrupt is + // to be sent, [0..11] + uint8_t len; // Number of sequencer stages, [1, 12]. ad7147_sequence_stage_t stages[13]; } ad7147_sequence_t; @@ -115,3 +120,5 @@ bool ad7147_hw_get_cdc_data(ad7147_hw_t *device, uint16_t *data, bool ad7147_hw_reset_sequencer(ad7147_hw_t *device); bool ad7147_hw_modulate_stage0_and_reset(ad7147_hw_t *dev, ad7147_sequence_stage_t *s_conf); +bool ad7147_hw_override_interrupt_stage(ad7147_hw_t *device, + uint8_t interrupt_stage); diff --git a/components/flow3r_bsp/flow3r_bsp_captouch.c b/components/flow3r_bsp/flow3r_bsp_captouch.c index ef52e10e8dd2793f6bbd6aee16198a3a905f3b45..c5576169e4897f34945cbb1e02f6f886676e02ec 100644 --- a/components/flow3r_bsp/flow3r_bsp_captouch.c +++ b/components/flow3r_bsp/flow3r_bsp_captouch.c @@ -1,4 +1,5 @@ #include "flow3r_bsp_captouch.h" +#include <math.h> #include "esp_err.h" #include "esp_log.h" @@ -12,10 +13,10 @@ static const char *TAG = "flow3r-bsp-captouch"; void flow3r_bsp_captouch_set_calibration_data(int32_t *data) { flow3r_bsp_ad7147_set_calibration_data(data); } -void flow3r_bsp_captouch_get_calibration_data(int32_t *data) { - flow3r_bsp_ad7147_get_calibration_data(data); +bool flow3r_bsp_captouch_get_calibration_data(int32_t *data) { + return flow3r_bsp_ad7147_get_calibration_data(data); } -bool flow3r_bsp_captouch_calibrating() { +int flow3r_bsp_captouch_calibrating() { return flow3r_bsp_ad7147_calibrating(); } void flow3r_bsp_captouch_calibration_request() { @@ -33,4 +34,15 @@ void flow3r_bsp_captouch_init() { ESP_LOGE(TAG, "Captouch init failed: %s", esp_err_to_name(ret)); } } +void flow3r_bsp_captouch_set_data_callback(flow3r_bsp_data_callback_t fun) { + flow3r_bsp_ad7147_set_data_callback(fun); +}; +void flow3r_bsp_captouch_set_petal_modes( + flow3r_bsp_captouch_petal_mode_t *data) { + flow3r_bsp_ad7147_set_petal_modes(data); +}; +void flow3r_bsp_captouch_get_petal_modes( + flow3r_bsp_captouch_petal_mode_t *data) { + flow3r_bsp_ad7147_get_petal_modes(data); +}; #endif diff --git a/components/flow3r_bsp/flow3r_bsp_captouch.h b/components/flow3r_bsp/flow3r_bsp_captouch.h index c006e4d4a9d30121145b6cd808d78a451e9b20ce..a04d6748eab43810117fc0a7dee453b28feb07f4 100644 --- a/components/flow3r_bsp/flow3r_bsp_captouch.h +++ b/components/flow3r_bsp/flow3r_bsp_captouch.h @@ -15,13 +15,26 @@ #include <stdbool.h> #include <stdint.h> +#define FLOW3R_BSP_CAPTOUCH_RAD_UNIT (16384.) +#define FLOW3R_BSP_CAPTOUCH_PHI_UNIT (16384.) +#define FLOW3R_BSP_CAPTOUCH_COVERAGE_UNIT (1024.) + +// allows to compare two different positional algorithms. +// unlocks the "._pos2" attribute on regular captouch output (but not on log +// frames) #define FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + typedef struct { - bool pressed; - bool press_event; - uint16_t raw_coverage; - // legacy - int32_t pos_distance; - int32_t pos_angle; + int16_t rad; + int16_t phi; +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + int16_t rad2; + int16_t phi2; +#endif + uint16_t coverage; + uint8_t index : 4; + uint8_t mode : 2; + uint8_t press_event : 1; + uint8_t pressed : 1; } flow3r_bsp_captouch_petal_data_t; typedef struct { @@ -38,8 +51,39 @@ void flow3r_bsp_captouch_init(); void flow3r_bsp_captouch_calibration_request(); // Returns true if captouch is currently calibrating. -bool flow3r_bsp_captouch_calibrating(); +int flow3r_bsp_captouch_calibrating(); -// Set/get calibration data. data[] should be at least 52 entries long. -void flow3r_bsp_captouch_get_calibration_data(int32_t *data); +// Set/get calibration data. data[] should be at least 120 entries long. +bool flow3r_bsp_captouch_get_calibration_data(int32_t *data); void flow3r_bsp_captouch_set_calibration_data(int32_t *data); + +// experiments +float flow3r_bsp_captouch_get_rad(flow3r_bsp_captouch_petal_data_t *petal, + uint8_t smooth, uint8_t drop_first, + uint8_t drop_last); +float flow3r_bsp_captouch_get_phi(flow3r_bsp_captouch_petal_data_t *petal, + uint8_t smooth, uint8_t drop_first, + uint8_t drop_last); + +void flow3r_bsp_captouch_get_rad_raw(flow3r_bsp_captouch_petal_data_t *petal, + float *ret); +void flow3r_bsp_captouch_get_phi_raw(flow3r_bsp_captouch_petal_data_t *petal, + float *ret); + +// uint8_t petal, bool pressed, uint32_t time_us, int32_t rad, int32_t phi, +// int32_t coverage +typedef void (*flow3r_bsp_data_callback_t)(uint8_t, uint8_t, bool, uint32_t, + int32_t, int32_t, int32_t); +void flow3r_bsp_captouch_set_data_callback(flow3r_bsp_data_callback_t fun); + +typedef enum { + PETAL_MODE_OFF = 0, + PETAL_MODE_0D = 1, + PETAL_MODE_1D = 2, + PETAL_MODE_2D = 3, +} flow3r_bsp_captouch_petal_mode_t; + +void flow3r_bsp_captouch_set_petal_modes( + flow3r_bsp_captouch_petal_mode_t *data); +void flow3r_bsp_captouch_get_petal_modes( + flow3r_bsp_captouch_petal_mode_t *data); diff --git a/components/micropython/usermodule/mp_captouch.c b/components/micropython/usermodule/mp_captouch.c index 86f0543cae7a8d84235968012280dd4a8c86d317..73e2f9821e070c22bdcfd8e44fe82173559f1c42 100644 --- a/components/micropython/usermodule/mp_captouch.c +++ b/components/micropython/usermodule/mp_captouch.c @@ -1,26 +1,220 @@ #include "py/builtin.h" +#include "py/objstr.h" +#include "py/parsenum.h" #include "py/runtime.h" +#include "py/smallint.h" +#include "esp_timer.h" #include "flow3r_bsp_captouch.h" +#include <math.h> #include <string.h> -STATIC mp_obj_t mp_captouch_calibration_active(void) { - return mp_obj_new_int(flow3r_bsp_captouch_calibrating()); +// :/ +typedef struct _mp_obj_complex_t { + mp_obj_base_t base; + mp_float_t real; + mp_float_t imag; +} mp_obj_complex_t; + +#define LIST_MIN_ALLOC 4 + +// this looks like the most useless optimization, this is not about caching tho: +// we wanna output data to micropython in floats but!! if we do the conversion +// in the cap touch driver task it can no longer jump between cores and that +// would make us very sad +// so we put the floating point mafh in the micropython task :D +// also we n-e-v-e-r get to use unions and they're so cool :3 +typedef union { + float post; + int32_t pre; +} processable; + +typedef struct { + processable rad; + processable phi; + processable coverage; + mp_uint_t timestamp; + uint8_t petal : 4; + uint8_t mode : 4; + bool pressed : 1; + bool processed : 1; +} log_frame_data_t; + +typedef struct _log_frame_data_ll_t { + log_frame_data_t frame; + struct _log_frame_data_ll_t *next; +} log_frame_data_ll_t; + +typedef struct { + mp_obj_base_t base; + log_frame_data_t data; +} mp_captouch_petal_log_frame_t; + +const mp_obj_type_t mp_type_captouch_petal_log_frame; + +static bool calib_trusted = false; + +// argument: the fastest we can go is like 0.75ms per frame if each chip only +// scans a single pad. we wanna avoid overflowing between thinks and garbage +// collection can be like 50ms (overflowing is somewhat graceish tho we're just +// dropping data) so a total worst case buffer length of 75ms seems reasonable +#define LOG_MAX_LEN 100 + +typedef struct { + log_frame_data_ll_t *start; // pointer to first (earliest) element in ll + log_frame_data_ll_t *end; // pointer to last (latest) element in ll + int len; // number of elements in log + SemaphoreHandle_t lock; // lock for log + + // when we kick the latch with refresh_events we buffer the log slice for + // the next captouch_read here: + log_frame_data_ll_t *latch_buffer_start; + int latch_buffer_len; + SemaphoreHandle_t latch_buffer_lock; +} petal_log_buffer_t; + +static uint32_t refresh_time; +static petal_log_buffer_t petal_log[10] = { + 0, +}; +static bool petal_log_initialized = false; +static bool petal_log_active[10]; + +static void _add_entry_to_log(uint8_t petal, uint8_t mode, bool pressed, + uint32_t timestamp, int32_t rad, int32_t phi, + int32_t coverage) { + if (petal >= 10) return; + if (!petal_log_active[petal]) return; + // TODO: can't m_new_obj here naively bc different task, fails in + // gc_alloc/mp_thread_get_state there's gotta be a lock, else _threads + // wouldn't work, will look into this sometime soon. + log_frame_data_ll_t *frame_ll = malloc(sizeof(log_frame_data_ll_t)); + if (frame_ll == NULL) { + printf("captouch frame init error"); + return; + } + frame_ll->next = NULL; + + log_frame_data_t *frame = &(frame_ll->frame); + frame->mode = mode; + frame->petal = petal; + frame->processed = false; + frame->pressed = pressed; + frame->timestamp = timestamp & (MICROPY_PY_UTIME_TICKS_PERIOD - 1); + frame->rad.pre = rad; + frame->phi.pre = phi; + frame->coverage.pre = coverage; + + // insert into linked list + petal_log_buffer_t *log = &(petal_log[petal]); + log_frame_data_ll_t *runaway = NULL; + xSemaphoreTake(log->lock, portMAX_DELAY); + + if (log->len >= LOG_MAX_LEN) { + runaway = log->start; + log->start = log->start->next; + log->len--; + } + + if (log->len == 0) { + assert(log->start == NULL); + assert(log->end == NULL); + log->start = frame_ll; + log->end = frame_ll; + } else { + log->end->next = frame_ll; + } + log->len++; + log->end = frame_ll; + + xSemaphoreGive(log->lock); + free(runaway); } -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) { - flow3r_bsp_captouch_calibration_request(); - return mp_const_none; +static void _kick_log_latch(uint8_t petal) { + petal_log_buffer_t *log = &(petal_log[petal]); + if (log->lock == NULL) return; + + xSemaphoreTake(log->lock, portMAX_DELAY); + int len = log->len; + log_frame_data_ll_t *start = log->start; + log->len = 0; + log->start = NULL; + log->end = NULL; + xSemaphoreGive(log->lock); + + xSemaphoreTake(log->latch_buffer_lock, portMAX_DELAY); + log_frame_data_ll_t *runaway = log->latch_buffer_start; + log->latch_buffer_len = len; + log->latch_buffer_start = start; + xSemaphoreGive(log->latch_buffer_lock); + while (runaway) { + log_frame_data_ll_t *runaway_next = runaway->next; + free(runaway); + runaway = runaway_next; + } +} + +static mp_obj_t _get_list_from_log(uint8_t petal) { + petal_log_buffer_t *log = &(petal_log[petal]); + + mp_obj_list_t *o = m_new_obj(mp_obj_list_t); + if (log->lock == NULL) { + mp_obj_list_init(o, 0); + return MP_OBJ_FROM_PTR(o); + } + + xSemaphoreTake(log->latch_buffer_lock, portMAX_DELAY); + + const int len = log->latch_buffer_len; // len 0 ok + log_frame_data_ll_t *frame_ll = log->latch_buffer_start; + + mp_obj_list_init(o, len); + log_frame_data_ll_t *del[len]; + mp_captouch_petal_log_frame_t *frames[len]; + + for (int i = 0; i < len; i++) { + assert(frame_ll); + frames[i] = m_new(mp_captouch_petal_log_frame_t, 1); + frames[i]->base.type = &mp_type_captouch_petal_log_frame; + memcpy(&(frames[i]->data), &(frame_ll->frame), + sizeof(log_frame_data_t)); + del[i] = frame_ll; + frame_ll = frame_ll->next; + } + assert(!frame_ll); + + log->latch_buffer_start = NULL; + log->latch_buffer_len = 0; + xSemaphoreGive(log->latch_buffer_lock); + + for (int i = 0; i < len; i++) { + o->items[i] = MP_OBJ_FROM_PTR(frames[i]); + free(del[i]); + } + return MP_OBJ_FROM_PTR(o); +} + +static void _initialize_log() { + if (petal_log_initialized) return; + for (size_t i = 0; i < 10; i++) { + petal_log_buffer_t *log = &(petal_log[i]); + assert(log->lock == NULL); + assert(log->latch_buffer_lock == NULL); + log->lock = xSemaphoreCreateMutex(); + assert(log->lock != NULL); + log->latch_buffer_lock = xSemaphoreCreateMutex(); + assert(log->latch_buffer_lock != NULL); + } + flow3r_bsp_captouch_set_data_callback(_add_entry_to_log); + petal_log_initialized = true; } -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 captouch; + mp_obj_t log; size_t ix; } mp_captouch_petal_state_t; @@ -29,11 +223,97 @@ const mp_obj_type_t captouch_petal_state_type; typedef struct { mp_obj_base_t base; mp_obj_t petals; + uint32_t timestamp; flow3r_bsp_captouch_data_t underlying; } mp_captouch_state_t; const mp_obj_type_t captouch_state_type; +static inline void log_frame_process_data(log_frame_data_t *data) { + if (!data->processed) { + data->rad.post = data->rad.pre / FLOW3R_BSP_CAPTOUCH_RAD_UNIT; + data->phi.post = data->phi.pre / FLOW3R_BSP_CAPTOUCH_PHI_UNIT; + data->coverage.post = + data->coverage.pre / FLOW3R_BSP_CAPTOUCH_COVERAGE_UNIT; + data->processed = true; + } +} + +STATIC void mp_captouch_petal_log_frame_attr(mp_obj_t self_in, qstr attr, + mp_obj_t *dest) { + mp_captouch_petal_log_frame_t *self = MP_OBJ_TO_PTR(self_in); + if (dest[0] != MP_OBJ_NULL) { + return; + } + log_frame_data_t *data = &self->data; + log_frame_process_data(data); + + switch (attr) { + case MP_QSTR_raw_pos: + dest[0] = mp_obj_new_complex(data->rad.post, data->phi.post); + break; + case MP_QSTR_pos: + if (data->pressed && data->mode > 1) { + dest[0] = mp_obj_new_complex(data->rad.post, data->phi.post); + } else { + dest[0] = mp_const_none; + } + break; + case MP_QSTR_raw_cap: + dest[0] = mp_obj_new_float(data->coverage.post); + break; + case MP_QSTR_pressed: + dest[0] = mp_obj_new_bool(data->pressed); + break; + case MP_QSTR_mode: + dest[0] = mp_obj_new_int(data->mode); + break; + case MP_QSTR_ticks_us: + dest[0] = MP_OBJ_NEW_SMALL_INT(data->timestamp); + break; + } +} + +STATIC mp_obj_t mp_captouch_petal_log_frame_make_new( + const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, + const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 2, 4, false); + // args: pressed, position, timestamp (optional, fallback: present time), + // coverage (optional, fallback: 5 if pressed else 0) + + mp_captouch_petal_log_frame_t *self = + m_new_obj(mp_captouch_petal_log_frame_t); + self->base.type = type_in; + log_frame_data_t *data = &self->data; + data->processed = true; + + data->pressed = mp_obj_is_true(args[0]); + + if (mp_obj_is_type(args[1], &mp_type_complex)) { + mp_obj_complex_t *position = MP_OBJ_TO_PTR(args[1]); + data->rad.post = position->real; + data->phi.post = position->imag; + } else { + data->rad.post = mp_obj_get_float(args[1]); + data->phi.post = 0; + } + + if (n_args > 2) { + data->timestamp = MP_OBJ_SMALL_INT_VALUE(args[2]); + } else { + data->timestamp = + esp_timer_get_time() & (MICROPY_PY_UTIME_TICKS_PERIOD - 1); + } + + if (n_args > 3) { + data->coverage.post = mp_obj_get_float(args[3]); + } else { + data->coverage.post = data->pressed ? 5. : 0.; + } + + return MP_OBJ_FROM_PTR(self); +} + 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); @@ -48,6 +328,10 @@ STATIC void mp_captouch_petal_state_attr(mp_obj_t self_in, qstr attr, bool top = (self->ix % 2) == 0; switch (attr) { + case MP_QSTR_log: { + mp_obj_list_t *log = MP_OBJ_TO_PTR(self->log); + dest[0] = mp_obj_new_list(log->len, log->items); + } break; case MP_QSTR_top: dest[0] = mp_obj_new_bool(top); break; @@ -57,17 +341,64 @@ STATIC void mp_captouch_petal_state_attr(mp_obj_t self_in, qstr attr, case MP_QSTR_pressed: dest[0] = mp_obj_new_bool(state->press_event); break; + case MP_QSTR_raw_cap: + dest[0] = mp_obj_new_float(state->coverage / + FLOW3R_BSP_CAPTOUCH_COVERAGE_UNIT); + break; + case MP_QSTR_raw_pos: + dest[0] = + mp_obj_new_complex(state->rad / FLOW3R_BSP_CAPTOUCH_RAD_UNIT, + state->phi / FLOW3R_BSP_CAPTOUCH_PHI_UNIT); + break; + case MP_QSTR_pos: + if (state->press_event && state->mode > 1) { + dest[0] = mp_obj_new_complex( + state->rad / FLOW3R_BSP_CAPTOUCH_RAD_UNIT, + state->phi / FLOW3R_BSP_CAPTOUCH_PHI_UNIT); + } else { + dest[0] = mp_const_none; + } + break; +#ifdef FLOW3R_BSP_CAPTOUCH_SECONDARY_OUTPUT + case MP_QSTR__pos2: + if (state->press_event && state->mode > 1) { + dest[0] = mp_obj_new_complex( + state->rad2 / FLOW3R_BSP_CAPTOUCH_RAD_UNIT, + state->phi2 / FLOW3R_BSP_CAPTOUCH_PHI_UNIT); + } else { + dest[0] = mp_const_none; + } + break; +#endif case MP_QSTR_pressure: - dest[0] = mp_obj_new_int(state->raw_coverage); + // legacy + if (state->press_event) { + dest[0] = mp_obj_new_int(state->coverage); + } else { + dest[0] = mp_obj_new_int(0); + } 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); + case MP_QSTR_position: + // legacy + { + int32_t ret[2]; + ret[0] = state->rad; + ret[1] = state->phi; + mp_obj_t items[2]; + // bitshift better than division here due to uneven spacing when + // crossing 0 fix manually when changing RAD_UNIT/PHI_UNIT + if (top) { + ret[0] = (int32_t)((35000 * ret[0]) >> 14); + ret[1] = (int32_t)((35000 * ret[1]) >> 14); + } else { + ret[0] = (int32_t)(((25000 * ret[0]) >> 14) + 5000); + ret[1] = 0; + } + items[0] = mp_obj_new_int(ret[0]); + items[1] = mp_obj_new_int(ret[1]); + dest[0] = mp_obj_new_tuple(2, items); + } break; - } } } @@ -82,9 +413,17 @@ STATIC void mp_captouch_state_attr(mp_obj_t self_in, qstr attr, case MP_QSTR_petals: dest[0] = self->petals; break; + case MP_QSTR_ticks_us: + dest[0] = MP_OBJ_NEW_SMALL_INT(self->timestamp); + break; } } +MP_DEFINE_CONST_OBJ_TYPE(mp_type_captouch_petal_log_frame, + MP_QSTR_PetalLogFrame, MP_TYPE_FLAG_NONE, attr, + mp_captouch_petal_log_frame_attr, make_new, + mp_captouch_petal_log_frame_make_new); + MP_DEFINE_CONST_OBJ_TYPE(captouch_petal_state_type, MP_QSTR_CaptouchPetalState, MP_TYPE_FLAG_NONE, attr, mp_captouch_petal_state_attr); @@ -94,6 +433,7 @@ MP_DEFINE_CONST_OBJ_TYPE(captouch_state_type, MP_QSTR_CaptouchState, STATIC mp_obj_t mp_captouch_state_new(const flow3r_bsp_captouch_data_t *underlying) { mp_captouch_state_t *captouch = m_new_obj(mp_captouch_state_t); + captouch->timestamp = refresh_time; captouch->base.type = &captouch_state_type; memcpy(&captouch->underlying, underlying, sizeof(flow3r_bsp_captouch_data_t)); @@ -104,7 +444,7 @@ mp_captouch_state_new(const flow3r_bsp_captouch_data_t *underlying) { petal->base.type = &captouch_petal_state_type; petal->captouch = MP_OBJ_FROM_PTR(captouch); petal->ix = i; - + petal->log = _get_list_from_log(i); mp_obj_list_append(captouch->petals, MP_OBJ_FROM_PTR(petal)); } @@ -120,6 +460,10 @@ STATIC mp_obj_t mp_captouch_read(void) { STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_read_obj, mp_captouch_read); STATIC mp_obj_t mp_captouch_refresh_events(void) { + for (int i = 0; i < 10; i++) { + _kick_log_latch(i); + } + refresh_time = esp_timer_get_time() & (MICROPY_PY_UTIME_TICKS_PERIOD - 1); flow3r_bsp_captouch_refresh_events(); return mp_const_none; } @@ -127,50 +471,788 @@ STATIC mp_obj_t mp_captouch_refresh_events(void) { STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_refresh_events_obj, mp_captouch_refresh_events); +STATIC mp_obj_t mp_captouch_calibration_get_trusted(void) { + return mp_obj_new_bool(calib_trusted); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_get_trusted_obj, + mp_captouch_calibration_get_trusted); + +STATIC mp_obj_t mp_captouch_calibration_set_trusted(mp_obj_t mp_data) { + calib_trusted = mp_obj_is_true(mp_data); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_captouch_calibration_set_trusted_obj, + mp_captouch_calibration_set_trusted); + +STATIC mp_obj_t mp_captouch_calibration_active(void) { + return mp_obj_new_int(flow3r_bsp_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) { + flow3r_bsp_captouch_calibration_request(); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_request_obj, + mp_captouch_calibration_request); + +static int get_calibration_index(int petal, int num_dimensions, + int dimension_index) { + int index = petal * 6; + index += ((num_dimensions + 1) * num_dimensions) / 2; + index += dimension_index; + index *= 2; + if (index >= 120) + mp_raise_ValueError( + MP_ERROR_TEXT("target index out of range (our fault)")); + return index; +} + STATIC mp_obj_t mp_captouch_calibration_get_data(void) { - int32_t data[50]; - flow3r_bsp_captouch_get_calibration_data(data); - mp_obj_t items[50]; - for (uint8_t i = 0; i < 50; i++) { - items[i] = mp_obj_new_int(data[i]); + int32_t data[120]; + if (!flow3r_bsp_captouch_get_calibration_data(data)) return mp_const_none; + + mp_obj_t petal_dicts[10]; + mp_obj_t items[3]; + mp_obj_t strings[4] = { mp_obj_new_str("0d", 2), mp_obj_new_str("1d", 2), + mp_obj_new_str("2d", 2), + mp_obj_new_str("petal", 5) }; + + for (int petal = 0; petal < 10; petal++) { + petal_dicts[petal] = + mp_obj_dict_make_new(&mp_type_ordereddict, 0, 0, 0); + mp_obj_dict_store(petal_dicts[petal], strings[3], + mp_obj_new_int(petal)); + for (int ndim = 0; ndim < (3 - (petal & 1)); ndim++) { + for (int dim = 0; dim <= ndim; dim++) { + int index = get_calibration_index(petal, ndim, dim); + char buf[25]; // max len of 32bit int decimal is 11 chars + int trunc = snprintf(buf, 25, "%d/%d", (int)data[index], + (int)data[index + 1]); + assert(trunc < 25); + items[dim] = mp_obj_new_str(buf, trunc); + } + mp_obj_dict_store(petal_dicts[petal], strings[ndim], + mp_obj_new_tuple(ndim + 1, items)); + } } - return mp_obj_new_tuple(50, items); + mp_obj_t ret = mp_obj_dict_make_new(&mp_type_ordereddict, 0, 0, 0); + mp_obj_dict_store(ret, mp_obj_new_str("ad7147", 6), + mp_obj_new_tuple(10, petal_dicts)); + return ret; } STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_captouch_calibration_get_data_obj, mp_captouch_calibration_get_data); +static bool parse_calibration_dict(mp_obj_t mp_data, int32_t *data) { + if (!mp_obj_is_dict_or_ordereddict(mp_data)) return false; + mp_map_elem_t *elem; + mp_obj_t len; + mp_obj_dict_t *calib_dict = MP_OBJ_TO_PTR(mp_data); + elem = mp_map_lookup(&calib_dict->map, mp_obj_new_str("ad7147", 6), + MP_MAP_LOOKUP); + if (!elem) return false; + mp_obj_t bias_tuple = elem->value; + len = mp_obj_len_maybe(bias_tuple); + if ((len == MP_OBJ_NULL) || (mp_obj_get_int(len) != 10)) return false; + mp_obj_t strings[4] = { mp_obj_new_str("0d", 2), mp_obj_new_str("1d", 2), + mp_obj_new_str("2d", 2), + mp_obj_new_str("petal", 5) }; + + uint16_t petals_scanned = 0; + for (int index = 0; index < 10; index++) { + mp_obj_t petal_obj = + mp_obj_subscr(bias_tuple, mp_obj_new_int(index), MP_OBJ_SENTINEL); + if (!mp_obj_is_dict_or_ordereddict(petal_obj)) return false; + mp_obj_dict_t *petal_dict = MP_OBJ_TO_PTR(petal_obj); + elem = mp_map_lookup(&petal_dict->map, strings[3], MP_MAP_LOOKUP); + int petal = elem ? mp_obj_get_int(elem->value) : index; + if (petal > 9 || petal < 0) + mp_raise_ValueError(MP_ERROR_TEXT("petal index out of range")); + if (petals_scanned & 1 << petal) + mp_raise_ValueError(MP_ERROR_TEXT("petal index duplicate")); + petals_scanned |= 1 << petal; + + for (int ndim = 0; ndim < (3 - (petal & 1)); ndim++) { + elem = + mp_map_lookup(&petal_dict->map, strings[ndim], MP_MAP_LOOKUP); + if (!elem) return false; + mp_obj_t calib_tuple = elem->value; + len = mp_obj_len_maybe(calib_tuple); + if ((len == MP_OBJ_NULL) || (mp_obj_get_int(len) != ndim + 1)) + return false; + for (int dim = 0; dim <= ndim; dim++) { + int index = get_calibration_index(petal, ndim, dim); + mp_obj_t calib_str[2] = { mp_obj_subscr(calib_tuple, + mp_obj_new_int(dim), + MP_OBJ_SENTINEL), + mp_obj_new_str("/", 1) }; + mp_obj_t calib_pt = mp_obj_str_split(2, calib_str); + for (int data_index = 0; data_index < 2; data_index++) { + size_t l; + const char *s = mp_obj_str_get_data( + mp_obj_subscr(calib_pt, mp_obj_new_int(data_index), + MP_OBJ_SENTINEL), + &l); + data[index + data_index] = + mp_obj_get_int(mp_parse_num_integer(s, l, 0, NULL)); + } + if (data[index] > UINT16_MAX || data[index] < 0 || + data[index + 1] > 126 || data[index + 1] < 0) { + mp_raise_ValueError( + MP_ERROR_TEXT("calibration data out of range")); + return false; // never reached bc __attribute__((noreturn)) + // but leaving it to make clear it doesn't + // return true + } + } + } + } + return true; +} + STATIC mp_obj_t mp_captouch_calibration_set_data(mp_obj_t mp_data) { - int32_t data[50]; - mp_obj_iter_buf_t iter_buf; - mp_obj_t iterable = mp_getiter(mp_data, &iter_buf); - mp_obj_t item; - uint8_t i = 0; - while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { - data[i] = mp_obj_get_int(item); - i++; - if (i == 50) break; - } - // if(i != 50) TODO: Throw error? Maybe? - flow3r_bsp_captouch_set_calibration_data(data); - return mp_const_none; + int32_t data[120]; + if (parse_calibration_dict(mp_data, data)) { + flow3r_bsp_captouch_set_calibration_data(data); + return mp_const_none; + } + mp_raise_TypeError(MP_ERROR_TEXT("calibration data malformed")); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_captouch_calibration_set_data_obj, mp_captouch_calibration_set_data); -STATIC const mp_rom_map_elem_t globals_table[] = { +STATIC const MP_DEFINE_STR_OBJ(mp_captouch_calibration_path_obj, + "/flash/captouch_calib.json"); + +STATIC const mp_rom_map_elem_t sys_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_captouch_read_obj) }, { MP_ROM_QSTR(MP_QSTR_refresh_events), MP_ROM_PTR(&mp_captouch_refresh_events_obj) }, - { MP_ROM_QSTR(MP_QSTR_calibration_active), - MP_ROM_PTR(&mp_captouch_calibration_active_obj) }, + { MP_ROM_QSTR(MP_QSTR_calibration_path), + MP_ROM_PTR(&mp_captouch_calibration_path_obj) }, { MP_ROM_QSTR(MP_QSTR_calibration_request), MP_ROM_PTR(&mp_captouch_calibration_request_obj) }, - { MP_ROM_QSTR(MP_QSTR_calibration_get_data), - MP_ROM_PTR(&mp_captouch_calibration_get_data_obj) }, + { MP_ROM_QSTR(MP_QSTR_calibration_active), + MP_ROM_PTR(&mp_captouch_calibration_active_obj) }, + { MP_ROM_QSTR(MP_QSTR_calibration_set_trusted), + MP_ROM_PTR(&mp_captouch_calibration_set_trusted_obj) }, + { MP_ROM_QSTR(MP_QSTR_calibration_get_trusted), + MP_ROM_PTR(&mp_captouch_calibration_get_trusted_obj) }, { MP_ROM_QSTR(MP_QSTR_calibration_set_data), MP_ROM_PTR(&mp_captouch_calibration_set_data_obj) }, + { MP_ROM_QSTR(MP_QSTR_calibration_get_data), + MP_ROM_PTR(&mp_captouch_calibration_get_data_obj) }, +}; + +STATIC MP_DEFINE_CONST_DICT(sys_globals, sys_globals_table); + +const mp_obj_module_t mp_module_sys_captouch_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&sys_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_sys_captouch, mp_module_sys_captouch_user_cmodule); + +typedef struct { + mp_obj_base_t base; + int8_t index; + int8_t mode; + int8_t logging; +} captouch_petal_config_obj_t; + +typedef struct { + mp_obj_base_t base; + mp_obj_t petals; +} captouch_config_obj_t; + +static int _limit_petal_mode(int mode, int petal) { + int limit = 3 - (petal & 1); + return mode > limit ? limit : (mode < 0 ? 0 : mode); +} + +STATIC void captouch_petal_config_attr(mp_obj_t self_in, qstr attr, + mp_obj_t *dest) { + captouch_petal_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (dest[0] != MP_OBJ_NULL) { + if (attr == MP_QSTR_mode) { + self->mode = + _limit_petal_mode(mp_obj_get_int(dest[1]), self->index); + dest[0] = MP_OBJ_NULL; + } else if (attr == MP_QSTR_logging) { + self->logging = mp_obj_is_true(dest[1]); + dest[0] = MP_OBJ_NULL; + } + } else { + if (attr == MP_QSTR_mode) { + dest[0] = mp_obj_new_int(self->mode); + } else if (attr == MP_QSTR_logging) { + dest[0] = self->logging ? mp_const_true : mp_const_false; + } else { + dest[1] = MP_OBJ_SENTINEL; + } + } +} + +STATIC mp_obj_t captouch_petal_config_set_default(mp_obj_t self_in) { + captouch_petal_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + self->mode = _limit_petal_mode(3, self->index); + self->logging = false; + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(captouch_petal_config_set_default_obj, + captouch_petal_config_set_default); + +STATIC mp_obj_t captouch_petal_config_clear(mp_obj_t self_in) { + captouch_petal_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + self->mode = 0; + self->logging = false; + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(captouch_petal_config_clear_obj, + captouch_petal_config_clear); + +STATIC mp_obj_t captouch_petal_config_set_min_mode(mp_obj_t self_in, + mp_obj_t mp_mode) { + captouch_petal_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + int mode = _limit_petal_mode(mp_obj_get_int(mp_mode), self->index); + self->mode = mode > self->mode ? mode : self->mode; + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(captouch_petal_config_set_min_mode_obj, + captouch_petal_config_set_min_mode); + +STATIC const mp_rom_map_elem_t captouch_petal_config_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_set_default), + MP_ROM_PTR(&captouch_petal_config_set_default_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), + MP_ROM_PTR(&captouch_petal_config_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_min_mode), + MP_ROM_PTR(&captouch_petal_config_set_min_mode_obj) }, +}; + +STATIC MP_DEFINE_CONST_DICT(captouch_petal_config_locals_dict, + captouch_petal_config_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE(mp_type_captouch_petal_config, MP_QSTR_PetalConfig, + MP_TYPE_FLAG_NONE, locals_dict, + &captouch_petal_config_locals_dict, attr, + captouch_petal_config_attr); + +static mp_obj_t captouch_config_new(int mode, mp_obj_type_t *type) { + captouch_config_obj_t *self = m_new_obj(captouch_config_obj_t); + self->base.type = type; + mp_obj_t petals[10]; + + flow3r_bsp_captouch_petal_mode_t mode_data[10]; + if (mode > 1) flow3r_bsp_captouch_get_petal_modes(mode_data); + for (int petal = 0; petal < 10; petal++) { + captouch_petal_config_obj_t *petalconf = + m_new_obj(captouch_petal_config_obj_t); + petalconf->base.type = &mp_type_captouch_petal_config; + petalconf->index = petal; + if (mode > 1) { + petalconf->logging = petal_log_active[petal]; + petalconf->mode = mode_data[petal]; + } else { + petalconf->logging = false; + petalconf->mode = mode ? 3 - (petal & 1) : 0; + } + petals[petal] = MP_OBJ_FROM_PTR(petalconf); + } + self->petals = mp_obj_new_list(10, petals); + return MP_OBJ_FROM_PTR(self); +} + +STATIC mp_obj_t captouch_config_empty(mp_obj_t type_in) { + return captouch_config_new(0, MP_OBJ_TO_PTR(type_in)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(captouch_config_empty_fun_obj, + captouch_config_empty); +STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ( + captouch_config_empty_obj, MP_ROM_PTR(&captouch_config_empty_fun_obj)); + +STATIC mp_obj_t captouch_config_default(mp_obj_t type_in) { + return captouch_config_new(1, MP_OBJ_TO_PTR(type_in)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(captouch_config_default_fun_obj, + captouch_config_default); +STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ( + captouch_config_default_obj, MP_ROM_PTR(&captouch_config_default_fun_obj)); + +STATIC mp_obj_t captouch_config_current(mp_obj_t type_in) { + return captouch_config_new(2, MP_OBJ_TO_PTR(type_in)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(captouch_config_current_fun_obj, + captouch_config_current); +STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ( + captouch_config_current_obj, MP_ROM_PTR(&captouch_config_current_fun_obj)); + +STATIC void captouch_config_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + if (dest[0] != MP_OBJ_NULL) { + return; + } + if (attr == MP_QSTR_petals) { + captouch_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + dest[0] = self->petals; + } else { + dest[1] = MP_OBJ_SENTINEL; + } +} + +static void captouch_config_verify(captouch_config_obj_t *self) { + for (int petal = 0; petal < 10; petal++) { + mp_obj_t petalconf_obj = + mp_obj_subscr(self->petals, mp_obj_new_int(petal), MP_OBJ_SENTINEL); + if (!mp_obj_is_type(petalconf_obj, &mp_type_captouch_petal_config)) + mp_raise_TypeError(MP_ERROR_TEXT("petal data malformed")); + } +} + +STATIC mp_obj_t captouch_config_apply(mp_obj_t self_in) { + _initialize_log(); + captouch_config_obj_t *self = MP_OBJ_TO_PTR(self_in); + captouch_config_verify(self); + flow3r_bsp_captouch_petal_mode_t mode_data[10]; + bool logging_data[10]; + for (int petal = 0; petal < 10; petal++) { + captouch_petal_config_obj_t *petalconf = MP_OBJ_TO_PTR(mp_obj_subscr( + self->petals, mp_obj_new_int(petal), MP_OBJ_SENTINEL)); + mode_data[petal] = _limit_petal_mode(petalconf->mode, petal); + logging_data[petal] = petalconf->logging; + } + flow3r_bsp_captouch_set_petal_modes(mode_data); + memcpy(petal_log_active, logging_data, sizeof(petal_log_active)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(captouch_config_apply_obj, captouch_config_apply); + +STATIC mp_obj_t captouch_config_apply_default(mp_obj_t self_in) { + flow3r_bsp_captouch_petal_mode_t mode_data[10]; + bool logging_data[10]; + for (int petal = 0; petal < 10; petal++) { + mode_data[petal] = _limit_petal_mode(3, petal); + logging_data[petal] = false; + } + flow3r_bsp_captouch_set_petal_modes(mode_data); + memcpy(petal_log_active, logging_data, sizeof(petal_log_active)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(captouch_config_apply_default_obj, + captouch_config_apply_default); + +// dirty hack: struct not exposed, internal to vendor/py/objcomplex.c but if we +// wanna write to ROM this is probably for the better +typedef struct { + mp_obj_base_t base; + mp_float_t real; + mp_float_t imag; +} mp_obj_complex_theft; + +STATIC const mp_obj_complex_theft mp_const_petal_angle[10] = { + { { &mp_type_complex }, (mp_float_t)0., (mp_float_t)-1. }, + { { &mp_type_complex }, + (mp_float_t)0.5877852522924731, + (mp_float_t)-0.8090169943749475 }, + { { &mp_type_complex }, + (mp_float_t)0.9510565162951535, + (mp_float_t)-0.3090169943749474 }, + { { &mp_type_complex }, + (mp_float_t)0.9510565162951535, + (mp_float_t)0.3090169943749474 }, + { { &mp_type_complex }, + (mp_float_t)0.5877852522924731, + (mp_float_t)0.8090169943749475 }, + { { &mp_type_complex }, (mp_float_t)0., (mp_float_t)1. }, + { { &mp_type_complex }, + (mp_float_t)-0.5877852522924731, + (mp_float_t)0.8090169943749475 }, + { { &mp_type_complex }, + (mp_float_t)-0.9510565162951535, + (mp_float_t)0.3090169943749474 }, + { { &mp_type_complex }, + (mp_float_t)-0.9510565162951535, + (mp_float_t)-0.3090169943749474 }, + { { &mp_type_complex }, + (mp_float_t)-0.5877852522924731, + (mp_float_t)-0.8090169943749475 }, +}; + +STATIC const mp_rom_obj_tuple_t mp_petal_angles_tuple_obj = { + { &mp_type_tuple }, + 10, + { + MP_ROM_PTR(&mp_const_petal_angle[0]), + MP_ROM_PTR(&mp_const_petal_angle[1]), + MP_ROM_PTR(&mp_const_petal_angle[2]), + MP_ROM_PTR(&mp_const_petal_angle[3]), + MP_ROM_PTR(&mp_const_petal_angle[4]), + MP_ROM_PTR(&mp_const_petal_angle[5]), + MP_ROM_PTR(&mp_const_petal_angle[6]), + MP_ROM_PTR(&mp_const_petal_angle[7]), + MP_ROM_PTR(&mp_const_petal_angle[8]), + MP_ROM_PTR(&mp_const_petal_angle[9]), + }, +}; + +STATIC const mp_rom_map_elem_t captouch_config_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_empty), MP_ROM_PTR(&captouch_config_empty_obj) }, + { MP_ROM_QSTR(MP_QSTR_default), MP_ROM_PTR(&captouch_config_default_obj) }, + { MP_ROM_QSTR(MP_QSTR_current), MP_ROM_PTR(&captouch_config_current_obj) }, + { MP_ROM_QSTR(MP_QSTR_apply), MP_ROM_PTR(&captouch_config_apply_obj) }, + { MP_ROM_QSTR(MP_QSTR_apply_default), + MP_ROM_PTR(&captouch_config_apply_default_obj) }, +}; + +STATIC MP_DEFINE_CONST_DICT(captouch_config_locals_dict, + captouch_config_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE(mp_type_captouch_config, MP_QSTR_Config, + MP_TYPE_FLAG_NONE, locals_dict, + &captouch_config_locals_dict, attr, + captouch_config_attr); + +typedef struct { + mp_obj_base_t base; + mp_obj_t frames; + int max_len; +} captouch_petal_log_obj_t; + +#define PREALLOC_LIMIT 128 + +static inline mp_int_t ticks_diff_inner(mp_uint_t end, mp_uint_t start) { + return (((end - start + MICROPY_PY_UTIME_TICKS_PERIOD / 2) & + (MICROPY_PY_UTIME_TICKS_PERIOD - 1)) - + MICROPY_PY_UTIME_TICKS_PERIOD / 2); +} + +static int list_multidel_nodealloc(mp_obj_list_t *list, int num_del, + bool from_newest) { + if (num_del <= 0) return 0; + num_del = num_del > list->len ? list->len : num_del; + list->len -= num_del; + if (!from_newest) + memmove(list->items, list->items + num_del, + (list->len) * sizeof(mp_obj_t)); + for (int i = 0; i < num_del; i++) { + list->items[list->len + i] = MP_OBJ_NULL; + } + mp_seq_clear(list->items, list->len + 1, list->alloc, sizeof(*list->items)); + return num_del; +} + +static mp_captouch_petal_log_frame_t *mp_obj_to_frame_ptr(mp_obj_t frame_obj) { + if (!mp_obj_is_type(frame_obj, &mp_type_captouch_petal_log_frame)) { + mp_raise_TypeError(MP_ERROR_TEXT("frame must be PetalLogFrame")); + } + return MP_OBJ_TO_PTR(frame_obj); +} + +static void log_crop(mp_obj_list_t *list, int min_frames, float min_time_ms, + bool from_newest) { + int len = list->len; + if (!len) return; + min_frames = min_frames < 0 ? 0 : min_frames; + + if (min_time_ms > 0.) { + mp_uint_t min_time_us = min_time_ms * 1000; + mp_captouch_petal_log_frame_t *frame = + mp_obj_to_frame_ptr(list->items[from_newest ? len - 1 : 0]); + mp_uint_t ref_timestamp = frame->data.timestamp; + + for (int i = min_frames + 1; i < len; i++) { + int index = from_newest ? len - 1 - i : i; + frame = mp_obj_to_frame_ptr(list->items[index]); + mp_uint_t start = + from_newest ? frame->data.timestamp : ref_timestamp; + mp_uint_t end = from_newest ? ref_timestamp : frame->data.timestamp; + if (ticks_diff_inner(end, start) >= min_time_us) { + min_frames = i - 1; + break; + } + } + } + list_multidel_nodealloc(list, min_frames, from_newest); +} + +static inline void get_slice_index(mp_obj_t mp_index, int len, int *ret) { + if (mp_index == mp_const_none) return; + int index = mp_obj_get_int(mp_index); + if (index < 0) index += len; + if (index < 0) { + index = 0; + } else if (index > len) { + index = len; + } + *ret = index; +} + +STATIC mp_obj_t captouch_petal_log_average(size_t n_args, + const mp_obj_t *args) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + int start = 0; + if (n_args > 1) get_slice_index(args[1], frames->len, &start); + int stop = frames->len; + if (n_args > 2) get_slice_index(args[2], frames->len, &stop); + + int len = stop - start; + if (len < 1) return mp_const_none; + + float sum_real = 0; + float sum_imag = 0; + for (int i = start; i < stop; i++) { + mp_captouch_petal_log_frame_t *frame = + mp_obj_to_frame_ptr(frames->items[i]); + log_frame_data_t *data = &frame->data; + log_frame_process_data(data); + sum_real += data->rad.post; + sum_imag += data->phi.post; + } + return mp_obj_new_complex(sum_real / len, sum_imag / len); +} +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(captouch_petal_log_average_obj, 1, 3, + captouch_petal_log_average); + +STATIC mp_obj_t captouch_petal_log_slope_per_ms(size_t n_args, + const mp_obj_t *args) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + int start = 0; + if (n_args > 1) get_slice_index(args[1], frames->len, &start); + int stop = frames->len; + if (n_args > 2) get_slice_index(args[2], frames->len, &stop); + + int len = stop - start; + if (len < 2) return mp_const_none; + + mp_uint_t timestamp_start; + float sum_real = 0; + float sum_imag = 0; + float sum_t_real = 0; + float sum_t_imag = 0; + float sum_t_sq = 0; + float sum_t = 0; + for (int i = 0; i < len; i++) { + mp_captouch_petal_log_frame_t *frame = + mp_obj_to_frame_ptr(frames->items[i + start]); + log_frame_data_t *data = &frame->data; + log_frame_process_data(data); + sum_real += data->rad.post; + sum_imag += data->phi.post; + if (!i) { + timestamp_start = data->timestamp; + continue; + } else { + float timestamp = + ticks_diff_inner(data->timestamp, timestamp_start); + sum_t_real += data->rad.post * timestamp; + sum_t_imag += data->phi.post * timestamp; + sum_t_sq += timestamp * timestamp; + sum_t += timestamp; + } + } + float div = len * sum_t_sq - sum_t * sum_t; + if (!div) return mp_const_none; + float k_real = (len * sum_t_real - sum_real * sum_t) * 1000 / div; + float k_imag = (len * sum_t_imag - sum_imag * sum_t) * 1000 / div; + return mp_obj_new_complex(k_real, k_imag); +} +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(captouch_petal_log_slope_per_ms_obj, 1, 3, + captouch_petal_log_slope_per_ms); + +STATIC mp_obj_t captouch_petal_log_length(mp_obj_t self_in) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + return mp_obj_new_int(frames->len); +} +MP_DEFINE_CONST_FUN_OBJ_1(captouch_petal_log_length_obj, + captouch_petal_log_length); + +STATIC mp_obj_t captouch_petal_log_length_ms(size_t n_args, + const mp_obj_t *args) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + int start = 0; + if (n_args > 1) get_slice_index(args[1], frames->len, &start); + int stop = frames->len; + if (n_args > 2) get_slice_index(args[2], frames->len, &stop); + if (stop - start < 2) return mp_obj_new_float(0.); + mp_captouch_petal_log_frame_t *oldest = + mp_obj_to_frame_ptr(frames->items[start]); + mp_captouch_petal_log_frame_t *newest = + mp_obj_to_frame_ptr(frames->items[stop - 1]); + float ret = + ticks_diff_inner(newest->data.timestamp, oldest->data.timestamp); + return mp_obj_new_float(ret / 1000); +} +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(captouch_petal_log_length_ms_obj, 1, 3, + captouch_petal_log_length_ms); + +STATIC mp_obj_t captouch_petal_log_index_offset_ms(mp_obj_t self_in, + mp_obj_t mp_index, + mp_obj_t mp_min_offset_ms) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + int ref_index = mp_obj_get_int(mp_index); + if (ref_index < 0) ref_index += frames->len; + if (ref_index >= frames->len || ref_index < 0) + mp_raise_msg(&mp_type_IndexError, MP_ERROR_TEXT("index out of range")); + float min_offset_ms = mp_obj_get_float(mp_min_offset_ms); + mp_uint_t ref_timestamp = + mp_obj_to_frame_ptr(frames->items[ref_index])->data.timestamp; + + if (!min_offset_ms) return mp_index; + if (min_offset_ms > 0) { + mp_int_t min_offset_us = min_offset_ms * 1000.; + // TODO: Make a smarter search that estimates based on length_ms/length + // and then expands linearily in whichever direction + for (int test_index = ref_index + 1; test_index < frames->len; + test_index++) { + mp_uint_t test_timestamp = + mp_obj_to_frame_ptr(frames->items[test_index])->data.timestamp; + if (ticks_diff_inner(test_timestamp, ref_timestamp) > min_offset_us) + return mp_obj_new_int(test_index); + } + } else { + mp_int_t min_offset_us = -min_offset_ms * 1000.; + for (int test_index = ref_index - 1; test_index >= 0; test_index--) { + mp_uint_t test_timestamp = + mp_obj_to_frame_ptr(frames->items[test_index])->data.timestamp; + if (ticks_diff_inner(ref_timestamp, test_timestamp) > min_offset_us) + return mp_obj_new_int(test_index); + } + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_3(captouch_petal_log_index_offset_ms_obj, + captouch_petal_log_index_offset_ms); + +STATIC mp_obj_t captouch_petal_log_crop(mp_obj_t self_in, mp_obj_t mp_index) { + if (mp_index == mp_const_none) return mp_obj_new_int(0); + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + int num_del = mp_obj_get_int(mp_index); + if (num_del < 0) num_del += frames->len; + num_del = list_multidel_nodealloc(frames, num_del, false); + return mp_obj_new_int(num_del); +} +MP_DEFINE_CONST_FUN_OBJ_2(captouch_petal_log_crop_obj, captouch_petal_log_crop); + +STATIC mp_obj_t captouch_petal_log_clear(mp_obj_t self_in) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + frames->len = 0; + frames->items = + m_renew(mp_obj_t, frames->items, frames->alloc, LIST_MIN_ALLOC); + frames->alloc = LIST_MIN_ALLOC; + mp_seq_clear(frames->items, 0, frames->alloc, sizeof(*frames->items)); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(captouch_petal_log_clear_obj, + captouch_petal_log_clear); + +STATIC mp_obj_t captouch_petal_log_append(mp_obj_t self_in, + mp_obj_t frame_obj) { + if (!mp_obj_is_type(frame_obj, &mp_type_captouch_petal_log_frame)) { + mp_raise_TypeError(MP_ERROR_TEXT("frame must be PetalLogFrame")); + } + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_list_t *frames = MP_OBJ_TO_PTR(self->frames); + if (frames->len >= frames->alloc) { + int new_alloc = frames->alloc * 2; + if (self->max_len > new_alloc) new_alloc = self->max_len; + frames->items = + m_renew(mp_obj_t, frames->items, frames->alloc, new_alloc); + frames->alloc = new_alloc; + mp_seq_clear(frames->items, frames->len + 1, frames->alloc, + sizeof(*frames->items)); + } + frames->items[frames->len++] = frame_obj; + if (frames->len > self->max_len) { + self->max_len = + frames->len > (PREALLOC_LIMIT) ? (PREALLOC_LIMIT) : frames->len; + } + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(captouch_petal_log_append_obj, + captouch_petal_log_append); + +STATIC void captouch_petal_log_attr(mp_obj_t self_in, qstr attr, + mp_obj_t *dest) { + captouch_petal_log_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (dest[0] != MP_OBJ_NULL) { + if (attr == MP_QSTR_frames) { + if (!mp_obj_is_type(dest[1], &mp_type_list)) + mp_raise_TypeError(MP_ERROR_TEXT("frames must be a list")); + self->frames = dest[1]; + dest[0] = MP_OBJ_NULL; + } + } else { + if (attr == MP_QSTR_frames) { + dest[0] = self->frames; + } else { + dest[1] = MP_OBJ_SENTINEL; + } + } +} + +STATIC mp_obj_t captouch_petal_log_make_new(const mp_obj_type_t *type_in, + size_t n_args, size_t n_kw, + const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 0, 1, false); + captouch_petal_log_obj_t *self = m_new_obj(captouch_petal_log_obj_t); + self->base.type = type_in; + if (n_args) { + if (!mp_obj_is_type(args[0], &mp_type_list)) + mp_raise_TypeError(MP_ERROR_TEXT("frames must be a list")); + self->frames = args[0]; + mp_obj_list_t *list = MP_OBJ_TO_PTR(self->frames); + self->max_len = list->len; + } else { + self->frames = mp_obj_new_list(0, NULL); + self->max_len = LIST_MIN_ALLOC; + } + return MP_OBJ_FROM_PTR(self); +} + +STATIC const mp_rom_map_elem_t captouch_petal_log_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_average), + MP_ROM_PTR(&captouch_petal_log_average_obj) }, + { MP_ROM_QSTR(MP_QSTR_slope_per_ms), + MP_ROM_PTR(&captouch_petal_log_slope_per_ms_obj) }, + { MP_ROM_QSTR(MP_QSTR_length), MP_ROM_PTR(&captouch_petal_log_length_obj) }, + { MP_ROM_QSTR(MP_QSTR_length_ms), + MP_ROM_PTR(&captouch_petal_log_length_ms_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&captouch_petal_log_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_crop), MP_ROM_PTR(&captouch_petal_log_crop_obj) }, + { MP_ROM_QSTR(MP_QSTR_append), MP_ROM_PTR(&captouch_petal_log_append_obj) }, + { MP_ROM_QSTR(MP_QSTR_index_offset_ms), + MP_ROM_PTR(&captouch_petal_log_index_offset_ms_obj) }, +}; + +STATIC MP_DEFINE_CONST_DICT(captouch_petal_log_locals_dict, + captouch_petal_log_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE(mp_type_captouch_petal_log, MP_QSTR_PetalLog, + MP_TYPE_FLAG_NONE, locals_dict, + &captouch_petal_log_locals_dict, attr, + captouch_petal_log_attr, make_new, + captouch_petal_log_make_new); + +STATIC const mp_rom_map_elem_t globals_table[] = { + { MP_ROM_QSTR(MP_QSTR_Config), MP_ROM_PTR(&mp_type_captouch_config) }, + { MP_ROM_QSTR(MP_QSTR_PetalLog), MP_ROM_PTR(&mp_type_captouch_petal_log) }, + { MP_ROM_QSTR(MP_QSTR_PetalLogFrame), + MP_ROM_PTR(&mp_type_captouch_petal_log_frame) }, + { MP_ROM_QSTR(MP_QSTR_PETAL_ANGLES), + MP_ROM_PTR(&mp_petal_angles_tuple_obj) }, +#ifndef FLOW3R_V2 + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_captouch_read_obj) }, +#endif }; STATIC MP_DEFINE_CONST_DICT(globals, globals_table); diff --git a/docs/api/assets/captouch_petals.png b/docs/api/assets/captouch_petals.png new file mode 100644 index 0000000000000000000000000000000000000000..615d765b75bf24bb86203bde0253aa447c00b332 Binary files /dev/null and b/docs/api/assets/captouch_petals.png differ diff --git a/docs/api/captouch.rst b/docs/api/captouch.rst index a93385143711944407f56269914dee3a6255e3e4..61cd39a738517322ff2660e4a427501f1549c1d5 100644 --- a/docs/api/captouch.rst +++ b/docs/api/captouch.rst @@ -1,7 +1,526 @@ ``captouch`` module =================== -.. automodule:: captouch - :members: - :undoc-members: +Basic usage +----------- +In a flow3r application you receive a ``CaptouchState`` object in each ``think()`` cycle. Here's a simple example: + +.. code-block:: python + + class App(Application): + def think(self, ins, delta_ms): + petal_0_is_pressed = ins.captouch.petals[0].pressed + +You cannot instantiate this object directly, but for REPL experiments there is a workaround listed below. + +.. py:class:: CaptouchState + + .. py:attribute:: petals + :type: List[CaptouchPetalState] + + State of individual petals. + + Contains 10 elements, with the zeroeth element being the petal closest to + the USB port. Then, every other petal in a clockwise direction. + + Even indices are top petals, odd indices are bottom petals. + + The top petal indices are printed in roman numerals around the flow3r display, + with "X" corresponding to 0. + + .. py:attribute:: ticks_us + :type: int + + Timestamp of when the captouch data has been requested from the backend, i.e. when + the ``think()`` cycle started. Mostly useful for comparing for to the same attribute + of ``PetalLogFrame``. Behaves identical to the return type of ``time.ticks_us()`` + and should only be used with ``time.ticks_diff()`` to avoid overflow issues. Overflow + occurs after ~10min. + + +.. py:class:: CaptouchPetalState + + .. py:attribute:: pressed + :type: bool + + True if the petal has been touched during the last ``think()`` cycle. + + May be affected by ``captouch.Config``. + + .. py:attribute:: pos + :type: Optional(float) + + Coordinates where this petal is touched or None if the petal isn't + touched or positional output is turned off via `captouch.Config``. + + The coordinate system is rotated with the petal's orientation: The + real part corresponds to the axis going from the center of the screen + to the center of this petal, the imaginary part is perpendicular to + that so that it increases with clockwise motion. + + Both real and imaginary part are centered around 0 and scaled to a + [-1..1] range. We try to guarantee that the output can span this full + range, but it may also go beyond. + + May be affected by ``captouch.Config``. + + See ``captouch.PETAL_ANGLES`` to align the output with the display + coordinate system. + + .. py:attribute:: raw_pos + :type: float + + Similar to ``.pos``, but never None. Will probably return garbage when + petal is not pressed. It is mostly useful for interpolating data between + petals. + + .. py:attribute:: raw_cap + :type: float + + Returns a the raw capacity reading from the petal in arbitrary units. + The value that kind-of-sort-of corresponds to how much the pad + is covered. Since the footprint of a finger expands when compressed + (what a sentence), this could in theory be roughly used for pressure, + but the data quality just doesn't cut it: + + It's mostly okay when not compared against fixed values, but rather + some sort of floating average, but it's not really monotonic and also + it doesn't react until the finger is a few mm away from the pad so it's + kinda bad for proximity sensing too. It's tempting to use it for gating + away light touches, but that results in poor performance in some + environmental conditions. Test carefully, and best make nothing important + depend on it. + + Normalized so that "1" corresponds to the upper hysteresis limit of the + ``pressed`` API. + + May be affected by ``captouch.Config``. + + +.. py:data:: PETAL_ANGLES + :type: list[complex] + + List of constants that can be used to rotate the output of the `.pos` attribute of both `CaptouchPetalState` and + `PetalLogFrame` to align with the display (x, y) coordinates. + + .. code-block:: python + + # (in think) + for x in range(10): + pos = ins.captouch.petals[x].pos + pos *= 60 * captouch.PETAL_ANGLES[x] + self.display_coords[x] = (pos.real, pos.imag) + + # (in draw) + for x in range(10): + ctx.move_to(* self.display_coords[x]) + ctx.text(str(x)) + + + +Speeding things up +------------------ + +The flow3r captouch driver is not the fastest, it takes at least 14ms to generate a full dataset with all channels running. +For applications where speed is key it is possible to merge data channels to reduce scanning time. Each petal can be turned +off entirely, act only as a button or be a 1D slider (also 2D for top petals). For example, if you turn off all all petals +except for 2 and 8 for a "dual joystick" mode, you increase your frame rate to up to 2.3ms! + +.. code-block:: python + + import captouch + + class App(Application): + def __init__(self): + self.captouch_config = captouch.Config.empty() + # top petals are used as buttons, bottom petals not at all + for petal in range(0,10,2): + self.captouch_config.petals[petal].mode = 1 + + def on_enter(self, vm): + self.captouch_config.apply() + +.. py:class:: Config + + .. py:method:: empty() -> Config: + :classmethod: + + Initializer method that returns a config with everything disabled. Ideal + for ORing the requirements of different components together. + + .. py:method:: default() -> Config: + :classmethod: + + Initializer method that returns the default config, same as when entering an application. + + .. py:method:: current() -> Config: + :classmethod: + + Initializer method that returns the currently active config. + + .. py:method:: apply() -> None: + + Apply this config to the driver. + + .. py:method:: apply_default() -> None: + + Convenience method to restore defaults. same as ``Config.default().apply()`` but mildly faster + if you already have one around. + + .. py:attribute:: petals + :type: List[PetalConfig] + + Config of individual petals, indexed as in the ``CaptouchState`` object. + +.. py:class:: Config.PetalConfig + + .. py:attribute:: mode + :type: int + + What kind of data should be collected for this petal. + + 0: No data at all + + 1: Button Mode: All pads combined, no positional output + + 2: 1D: Only radial position is provided + + 3: 2D: Full positional output. Only available for top petals. + + Default: 3 (top petals), 2 (bottom petals) + + .. py:attribute:: logging + :type: bool + + Whether or not you want to collect the raw data log. This + eats some CPU time proportional to the ``think()`` cycle + time, use only when you actually do something with the data. + + Default: False + + +Gestures +-------- + +For many common applications we provide widgets that do whatever data processing is needed so you don't have to implement +everything from scratch, see ``st3m.ui.widgets``. If whatever you want is already in there we recommend using these as +future performance improvements will then directly benefit your application. + +If you do want to do your own signal processing you will probably want to use the logging feature: The positional data is +fairly imperfect already, missing frames or needing to detect duplicates doesn't make it better, and the general-purpose +filtering on the "primitive" positional output may be an issue for fast motion detection. Using the unprocessed log +doesn't make postprocessing easy, but at least you get the best data quality the driver can offer. + +In order to use the logging feature effectively we also provide a ``PetalLog`` class that implements fast time-based +cropping and data processing. This could all be done in raw python too, but e.g. for linear regression the C implementation +runs around 40 times faster and creates less intermediate objects so that garbage collection triggers less. This is +particularily important if the captouch driver is configured to run a specific petal very fast. + +.. py:class:: PetalLogFrame + + .. py:attribute:: pressed + :type: bool + + Identical to ``pressed`` of ``CaptouchPetalState``. + + .. py:attribute:: pos + :type: Optional(float) + + Identical to ``pos`` of ``CaptouchPetalState``. + + .. py:attribute:: raw_pos + :type: float + + Identical to ``raw_pos`` of ``CaptouchPetalState``. + + .. py:attribute:: raw_cap + :type: float + + Identical to ``raw_cap`` of ``CaptouchPetalState``. + + .. py:attribute:: mode + :type: int + + Config mode setting that was used for recording the frame (see ``captouch.Config``). + + .. py:attribute:: ticks_us + :type: int + + Timestamp that reflects the approximate time at which the data was captured (to + be exact, when the I2C transmission has completed). Behaves identical to the return + type of ``time.ticks_us()`` and should only be used with ``time.ticks_diff()`` + to avoid overflow issues. Overflow occurs after ~10min. + + +.. py:class:: PetalLog + + .. py:attribute:: frames + + List of PetalLogFrames. May be manipulated or replaced by user. We use the binary structure of micropython + ``list`` as well as ``PetalLogFrame``, so any duck typing may result in ``TypeError`` when the other attributes + and methods of this class are used. + + .. py:method:: append(frame: PetalLogFrame): + + Appends frame to ``.frames``. There's a performance benefit when only modifying ``.frames`` with this method + alongside ``.crop()`` and ``.clear()``. + + .. py:method:: crop(index: Optional(int)) -> int + + Crops the oldest elements in ``.frames`` in-place and returns the number of cropped frames. The ``index`` + parameter behaves slice-like, equivalent to ``.frames = .frames[index:]``, i.e. positive values remove + that amount of oldest frames, negative values limit the list at most ``-index`` frames and None does nothing. + Typically used together with ``index_offset_ms()`` to keep the length of ``frames`` in check. + + .. py:method:: clear() + + Clears ``.frames``. + + .. py:method:: length() -> int + + Returns ``len(.frames)`` but slightly faster. + + .. py:method:: length_ms(start: Optional(int) = None, stop: Optional(int) = None, /) -> float + + Returns difference in timestamp between newest and oldest frame in milliseconds or 0 if ``.frames`` is empty. + The optional ``start`` and ``stop`` parameters delimit which slice of ``.frames`` is used for computation, + equivalent to ``.frames[start:stop]``. Negative values behave as expected. + + .. py:method:: index_offset_ms(index: int, min_offset_ms: float, /) -> Optional(int) + + Returns the index of the frame that is at least ``min_offset_ms`` newer (or older for negative ``min_offset_ms``) + than the frame at ``index``, or ``None`` if no such frame exists. Negative ``index`` values are allowed and work + as expected, e.g. ``index = -1`` indicates the newest frame. Will raise ``IndexError`` if the index is out of range. + + .. py:method:: average(start: Optional(int) = None, stop: Optional(int) = None, /) -> Optional(complex) + + Returns the average position of elements in ``.frames``. Will return ``None`` if no frames are available. + The optional ``start`` and ``stop`` parameters delimit which slice of ``.frames`` is used for computation, + equivalent to ``.frames[start:stop]``. Negative values behave as expected. + + .. py:method:: slope_per_ms(start: Optional(int) = None, stop: Optional(int) = None, /) -> Optional(complex) + + Returns the ordinary least squares linear regression slope of the position of elements in ``.frames``. Uses + timestamp and disregards order of ``.frames``. Will return ``None`` if less than 2 frames are available or all + timestamps are equal. + The optional ``start`` and ``stop`` parameters delimit which slice of ``.frames`` is used for computation, + equivalent to ``.frames[start:stop]``. Negative values behave as expected. + + +The nitty gritty +---------------- + +The flow3r captouch setup is not as good as a smartphone touchscreen. While a typical modern touchscreen receives data from +a fine grid of wire intersections, flow3r just has 2 per bottom pad and 3 per top pad. Here's an illustration: + +.. image:: assets/captouch_petals.png + +On a grid type touch device you can infer rough position even with rather high noise levels as long as a "high" and "low" +for each grid point is roughly represented. On a device like flow3r, unfortunately we do not have this luxury. This leads +to higher noise sensitivity and some other unexpected behaviors that limit how captouch can be used: + +**Liftoff artifacts** + +In general, the positional output is dependent on pressure, finger size and environmental factors. For example, if you have a USB +cable connected to the USB-C port and put it in your pants pocket without connecting it to anything, your finger will result in a +different excitation than another person's finger who touches a different petal. This is not a super pratical scenario, but people +have observed effects like this if flow3r has been on different surfaces (i.e. tables, couches). We tried our best to suppress +these side effects in the ``.pressed`` and ``.pos`` outputs, but for example ``.raw_cap`` is heavily affected by it and there's +little we can do about it. + +A more pratical side effect is that if you release a petal, the positional output will momentarily drift. This is bad for swipe +gesture recognition, as this can easily be misread as a swipe. You might think that the ``.raw_cap`` channel may help suppressing +this, but since ``.raw_cap`` also changes a lot during motion without liftoff, a trivial algorithm would suppress valid swipes. +The current implementation of the ``Scroller`` widget does not use ``.raw_cap`` at all since any math we could come up with reasonable +effort was situational at best, but typically detrimental to the feel. + +These liftoff artifacts (or lifton, for the counterpart at the beginning of a gesture) are a nuisance to many widgets in different +forms. In general, we found it to be the be the best approach to ignore 20ms of the positional data from the beginning and/or end of +each touch, depending on the use case. This pattern was found to be so common that the ``PetalLog`` class has been in great parts +designed around facilitating the implementation of such rejection algorithms. + +Some users may instinctively use slow liftoff in order to make sure they don't accidentially introduce a motion, erroneously +attributing these artifacts to their own performance rather than a shortcoming of the hardware. This is unfortunate, as these +slow liftoffs are much harder to detect (we did some testing with ``.raw_cap`` but found no universally applicable pattern, there +often is a visible kink in the data but it often occurs later than the artifacts, so if you investigate options like this make +sure to exclude "red herrings" - we wasted a good few hours that could've been prevented by plotting *all* the data). + +The hardware is out there in the world, the best we can do at this point is to accept its performance, explain it to the user and +then be **consistent** - if fast liftoffs are the most consistent way to work around these issues, we should go for them, even if +for some they may be counterintuitive. + +**Data rates** + +As a rule of thumb, all (even) top petals are hooked up to one chip, all (odd) bottom petals to another, except for petal 2, +which is connected to the "bottom" chip. This means for example that if you disable all bottom petals, petal 2 receives data +much faster than the other top petals. + +Generally, each data point that you collect (i.e., the integer value of ``.mode`` for each petal) takes about 0.75ms, however due +to a less-than-ideal peripheral protocol we typically run a bit slower than that, expect the full cycle to take 2-3ms on top. +Higher priority tasks (audio rendering, WiFi) may make this worse. Also, if the bottom chip is fully utilized (13 datapoints, 2 from +each bottom petal, 3 from petal 2) there is an additonal penalty resulting in a spin time of ~14ms typ. + +Since data rates are not constant, ``PetalLog`` doesn't simply assume a constant framerate and converts to indices based +on that for a good reason, each datapoint may be a random amount of time apart from its predecessor, with only a lower limit given +by the aforementioned 0.75ms \* (number of channels of the specific chip) rule. Depending on other hardware activity there may be +drastic differences. Any references to data rates are meant in a stochastic sense - they are best measured in Becquerel, not Hertz. + +Making widgets that feel the same-ish with different data rates is difficult. We're trying hard to make the provided widget library +perform satisfactory at all configurations, and fail at that thus far. But that is only of concern to us as we're trying to provide +generics: For a specific application, there is no shame in hardcoding a widget to only work as expected for a given data rate. Since +e.g. the top petals may spin at different rates however, you should take care in setting up the driver so that all petals which should +have equal behavior actually run at the same number of chip channels. For example, the Violin application, which extracts rubbing motion +from all top petals, activates bottom petal channels that it does not use in order to make sure that petal 2 runs at the same data rate. +Feel free to not use the widget auto-configuration at all and create your own for the purpose manually, or modify the autogenerated one +after it has been created. It is meant as a mere helper, you may find reasons to ignore or enhance it at times. + +You might feel tempted to dynamically switch configuration, for example to run petals as buttons very fast and only enable positional +output once they are touched. This would be great in theory and make many applications a bit snappier, however the chips exhibit strange +undocumented glitches when configurations are changed in certain ways. Our approach to configuration changes at this point is to try +to guarantee the validity of all datasets that you receive, but since these glitches often rare and difficult to track down we are +overshooting and throwing away more data than needed. Changing configuration at this point in time results in 3 datasets being thrown +away, resulting in a significant (~50ms typ.) gap in the data logs. We may be able to improve on this in some specific transition types, +(i.e., channel number remains constant), but it is unclear if we will ever find the effort to implement this justifiable. + + +**Miscellaneous quirks** + +- The top petals have quite a large deadzone near the outer tip. + +- The top petals like to "zig zag" around the center. For 1D value input the bottom petals are plain better. + +- The bottom petals are less noisy. To compensate, the top petals use stronger filtering in the non-logged positional output, + making them a bit slower. + +- Faster spin times do not only affect the log but also the built-in filters on the non-logged outputs, making especially the top + petals much more responsive. + +- ``.raw_cap`` is not monotonic with respect to how much of the petal you cover: In fact, if you cover an entire top petal with multiple + flat fingers, it fairly consistently outputs lower values compared to the flat of the thumb. The causes for this behavior are unknown. + + +Annex 1: Basics of complex numbers +---------------------------------- + +You may have noticed that the positional output is complex-valued. We find that it enables very concise 2D operations, +but not everyone is familiar with them. If you don't wanna deal with it at all and use linear algebra or the likes +instead, you can simply convert it to a tuple like so: + +.. code-block:: python + + # create complex number with real part 1 and imaginary part 3 + pos = complex(1, 3) + tuple_data = [pos.real, pos.imag] + +If you do want to use them directly however, here's some basic operations. Translation and scaling work just as you'd expect: + +.. code-block:: python + + # translate by 2 in the real direction and 4 in the imaginary direction + offset = complex(2, 4) + pos += offset + + # scale both real and imaginary part by 2 + pos += pos * 2 + +Complex numbers have an important trick up their sleeve: Multiplication of two complex numbers adds their respective angles together, +which can be used for rotation. That angle is the angle between the corresponding vector, as illustrated here: +`wikipedia <https://en.wikipedia.org/wiki/Argument_(complex_analysis)>`_ + +Of course, this can be used together with scaling in the same operation; the scaling factor is simply the length of the complex number. + +We can conveniently create numbers with a given length and angle or extract them from a given number like so: + +.. code-block:: python + + import cmath + + # create number with angle of 45 degrees (math.tau / 8 in radians) and length of 2: + rotator = 2 * cmath.exp(1j * math.tau / 8) + # alternative: programmer's choice + rotator = cmath.rect(2, math.tau / 8) + # apply the rotation + pos *= rotator + + # get angle, in this case math.tau / 8 + angle = cmath.phase(rotator) + # get length, in this case 2 + length = abs(rotator) + +For rotating around a point other than the origin, simply translate and de-translate before and after the rotation: + +.. code-block:: python + + pos += offset + pos *= rotator + pos -= offset + +As a practical example, you can set an RGB color like this (a ``Slider`` widget would do a better job and preventing artifacts +but let's keep things simple): + +.. code-block:: python + + # (in think) + petal = ins.captouch.petals[0] + if petal.pos is not None: + hue = cmath.phase(petal.pos) + sat = max(1-abs(petal.pos),0) + col = st3m.ui.colours.hsv_to_rgb(hue, sat, 1) + leds.set_all_rgb(*col) + leds.update() + +Annex 2: REPL workaround +------------------------ + +We've cornered ourselves there a little: some useful features of the captouch driver are synchronized to the ``think()`` cycle, but +many early applications don't use the ``CaptouchState`` provided but instead create their own via ``captouch.read()`` (legacy, don't do +this in new applications please). If this function were to trigger a reset in the ``.pressed`` attribute, half of the data would be +thrown away and it would be easy to miss button presses. ``.log`` would drop frames in similar manner. You could do some sort of lazy +evaluation of ``think()``'s object but that might just result in more subtle bugs if users aren't careful, we'd rather break loudly :D. +Instead, the OS uses a special trigger function at the beginning of each ``think()``. To construct a proper ``CaptouchState`` in the +repl we must call this function manually. Don't ever do it in applications tho, really. + +.. code-block:: python + + import sys_captouch # in REPL only, never in applications! + sys_captouch.refresh_events() # your app will break in subtle and annoying ways + captouch_state = sys_captouch.read() + + +Annex 3: Legacy API +------------------- + +.. py:class:: CaptouchPetalState + :no-index: + + .. py:attribute:: position + :type: Tuple(int, int) + + Similar to ``.raw_pos``, but not normalized. First element corresponds to real part, second element to imaginary. + For top petals about ``35000 * .raw_pos``, for bottom petals about ``25000 * .raw_pos + 5000`` (note that addition only + affects the real part, the imaginary part is always 0 for bottom petals). + + .. py:attribute:: pressure + :type: int + + Similar to ``.raw_cap``, but not normalized. Depending on firmware version roughly about ``8000 * .raw_cap``, but may + or may not be always 0 if the petal is not pressed. + + +.. py:function:: read() -> CaptouchState + + Reads current captouch state from hardware and returns a snapshot in time. + ``.pressed`` and ``.log`` attributes are broken in the REPL. + + Typically you'd want to use the captouch data provided by ``think()``, so this method for application purposes + is replaced with nothing. See workaround for reasoning. + + What if you do *need* the captouch state outside of ``think()`` though? Well, chances are you don't, it just appears convenient: + We've seen this pattern a few times where ``think()`` requires a previous state, and the first such previous state is generated by + ``__init__()``, but this is an anti-pattern. Instead, set the previous state to ``None`` in ``on_enter()`` and handle that case + in ``think()``. The common consequence of doing otherwise is that after exiting and reentering an application the previous state + is very stale, which can lead to unintended behavior. Dedicated "first think" functionality really is the way to go in these cases. + + Some example applications that ship with flow3r unfortunately use this pattern, and we should really clean that up, but we + didn't have time for this release yet. Apologies, IOU, will totally get around to it soon. diff --git a/docs/index.rst b/docs/index.rst index 3ee6a85bd16c203adbcc529ee03bc22b36cb16af..da20a0b8f7c74269a612ffe4a771961b9b3f6afd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,10 +49,10 @@ User manual api/application.rst api/input.rst + api/captouch.rst api/audio.rst api/badgelink.rst api/badgenet.rst - api/captouch.rst api/ctx.rst api/leds.rst api/colours.rst diff --git a/python_payload/apps/demo_cap_touch/__init__.py b/python_payload/apps/demo_cap_touch/__init__.py index b6416de76984f7aa877218631559f1ebb793d751..77d50b4ba68cb049799ce042731dd6e704c22f5d 100644 --- a/python_payload/apps/demo_cap_touch/__init__.py +++ b/python_payload/apps/demo_cap_touch/__init__.py @@ -1,131 +1,55 @@ -from st3m import logging -from st3m.application import Application, ApplicationContext -from st3m.goose import List, Optional +from st3m.application import Application +from st3m.goose import List from st3m.input import InputState from st3m.ui.view import ViewManager from ctx import Context -log = logging.Log(__name__, level=logging.INFO) -log.info("import") - +import captouch import cmath import math -import time -import captouch class Dot: - def __init__(self, size: float, imag: float, real: float) -> None: - self.size = size - self.imag = imag - self.real = real - - def draw(self, i: int, ctx: Context) -> None: - imag = self.imag - real = self.real - size = self.size - - col = (1.0, 0.0, 1.0) - if i % 2: - col = (0.0, 0.8, 0.8) - ctx.rgb(*col).rectangle( - -int(imag - (size / 2)), -int(real - (size / 2)), size, size - ).fill() + size = None + pos = 0j + filled = True + + def draw(self, ctx: Context): + if self.size is None or self.pos is None or self.filled is None: + return + ctx.save() + ctx.translate(self.pos.real, self.pos.imag) + ctx.rotate(cmath.phase(self.pos)) + ctx.move_to(-self.size / 2, -self.size / 2) + ctx.rel_line_to(self.size, self.size / 2) + ctx.rel_line_to(-self.size, self.size / 2) + ctx.close_path() + ctx.fill() if self.filled else ctx.stroke() + ctx.restore() class CapTouchDemo(Application): - def __init__(self, app_ctx: ApplicationContext) -> None: - super().__init__(app_ctx) - self.dots: List[Dot] = [] - self.last_calib = None - self.state = 0 - self.timer = 0 - self.button = None + def on_enter(self, vm: ViewManager): + super().on_enter(vm) + self.dots: List[Dot] = [Dot() for x in range(10)] def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) - if self.button is not None: - press_event = (self.button != ins.buttons.app) and ins.buttons.app - else: - press_event = False - self.button = int(ins.buttons.app) - if press_event: - print(self.button) - if self.state == 0: - if press_event: - self.state = 1 - self.dots = [] - for i in range(10): - petal = ins.captouch.petals[i] - (rad, phi) = petal.position - size = 4 - if petal.pressed: - size += 4 - x = 70 + (rad / 1000) + 0j - x += ((-phi) / 600) * 1j - rot = cmath.exp(-2j * math.pi * i / 10) - x = x * rot - self.dots.append(Dot(size, x.imag, x.real)) - elif self.state == 1: - if press_event: - if self.button == ins.buttons.PRESSED_DOWN: - self.state = 2 - self.timer = 2999 - else: - self.state = 0 - elif self.state == 2: - self.timer -= delta_ms - if self.timer < 0: - self.state = 3 - captouch.calibration_request() - elif self.state == 3: - if not captouch.calibration_active(): - self.state = 0 + for i in range(10): + petal = ins.captouch.petals[i] + dot = self.dots[i] + pos = 0j if petal.pos is None else petal.pos * 30 + dot.pos = (pos + 70) * captouch.PETAL_ANGLES[i] + dot.size = 5 + 8 * math.sqrt(petal.raw_cap) + dot.filled = not petal.pressed def draw(self, ctx: Context) -> None: ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill() - if self.state == 0: - for i, dot in enumerate(self.dots): - dot.draw(i, ctx) - else: - ctx.rgb(0, 0.8, 0.8) - ctx.text_align = ctx.CENTER - ctx.move_to(0, 0) - ctx.font = ctx.get_font_name(0) - ctx.font_size = 20 - pos = -55 - ctx.move_to(0, pos) - ctx.text("calibration mode") - pos += 30 - ctx.move_to(0, pos) - ctx.text("do not touch the petals") - pos += 20 - ctx.move_to(0, pos) - ctx.text("from the front during") - pos += 20 - ctx.move_to(0, pos) - ctx.text("calibration") - pos += 40 - ctx.move_to(0, pos) - if self.state == 1: - pos -= 10 - ctx.move_to(0, pos) - ctx.text("down: start 3s countdown") - pos += 20 - ctx.move_to(0, pos) - ctx.text("left/right: cancel") - elif self.state == 2: - ctx.text("calibrating in " + str(1 + int(self.timer / 1000))) - elif self.state == 3: - ctx.rgb(1.0, 0.5, 0.2) - ctx.text("calibrating...") - - def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) - self.button = None + for i in range(10): + ctx.rgb(*((0.0, 0.8, 0.8) if i % 2 else (1.0, 0.0, 1.0))) + self.dots[i].draw(ctx) -# For running with `mpremote run`: if __name__ == "__main__": import st3m.run diff --git a/python_payload/mypystubs/captouch.pyi b/python_payload/mypystubs/captouch.pyi index ea64b05c87458549bfda8db111aa63e100cda3cb..11607973a050b9c79058956018ebee5328d8841e 100644 --- a/python_payload/mypystubs/captouch.pyi +++ b/python_payload/mypystubs/captouch.pyi @@ -1,10 +1,53 @@ -from typing import Protocol, List, Tuple +from typing import Protocol, List, Tuple, Union + +class PetalLogFrame: + @property + def pressed(self) -> bool: + """ + Identical to ``pressed`` of ``CaptouchPetalState``. + """ + ... + @property + def pos(self) -> Union[complex, None]: + """ + Identical to ``pos`` of ``CaptouchPetalState``. + """ + ... + @property + def raw_pos(self) -> complex: + """ + Identical to ``raw_pos`` of ``CaptouchPetalState``. + """ + ... + @property + def raw_cap(self) -> float: + """ + Identical to ``raw_cap`` of ``CaptouchPetalState``. + """ + ... + @property + def mode(self) -> float: + """ + Config mode setting that was used for recording the frame (see ``captouch.Config``). + """ + ... + @property + def ticks_us(self) -> int: + """ + Timestamp that reflects the approximate time at which the data was captured (to + be exact, when the I2C transmission has completed). Behaves identical to the return + type of ``time.ticks_us()`` and should only be used with ``time.ticks_diff()`` + to avoid overflow issues. Overflow occurs after ~10min. + """ + ... class CaptouchPetalState(Protocol): @property def pressed(self) -> bool: """ - True if the petal is being touched. + True if the petal has been touched during the last ``think()`` cycle. + + May be affected by ``captouch.Config``. """ ... @property @@ -20,22 +63,83 @@ class CaptouchPetalState(Protocol): """ ... @property - def position(self) -> Tuple[int, int]: + def pos(self) -> Union[complex, None]: """ - Coordinates of touch on petal in the form of a (distance, angle) - tuple. The units are arbitrary, but centered around (0, 0). + Coordinates where this petal is touched or None if the petal isn't + touched or positional output is turned off via `captouch.Config``. + + The coordinate system is rotated with the petal's orientation: The + real part corresponds to the axis going from the center of the screen + to the center of this petal, the imaginary part is perpendicular to + that so that it increases with clockwise motion. - These are approximately cartesian, but the axes are rotated to align - with the radial and angular components of the petal position relative - to the center of the badge, meaning: + Both real and imaginary part are centered around 0 and scaled to a + [-1..1] range. We try to guarantee that the output can span this full + range, but it may also go beyond. + + May be affected by ``captouch.Config``. + + See ``captouch.PETAL_ANGLES`` to align the output with the display + coordinate system. + """ + ... + @property + def raw_pos(self) -> complex: + """ + Similar to ``.pos``, but never None. Will probably return garbage when + petal is not pressed. It is mostly useful for interpolating data between + petals. + """ + ... + @property + def raw_cap(self) -> float: + """ + Returns a the raw capacity reading from the petal in arbitrary units. + The value that kind-of-sort-of corresponds to how much the pad + is covered. Since the footprint of a finger expands when compressed + (what a sentence), this could in theory be roughly used for pressure, + but the data quality just doesn't cut it: - An increase in distance means the touch is further away from the centre - of the badge. + It's mostly okay when not compared against fixed values, but rather + some sort of floating average, but it's not really monotonic and also + it doesn't react until the finger is a few mm away from the pad so it's + kinda bad for proximity sensing too. It's tempting to use it for gating + away light touches, but that results in poor performance in some + environmental conditions. Test carefully, and best make nothing important + depend on it. - An increase in angle means the touch is more clockwise. + Normalized so that "1" corresponds to the upper hysteresis limit of the + ``pressed`` API. - The hardware only provides angular positional data for the top petals, - the bottom petals always return an angle of 0. + May be affected by ``captouch.Config``. + """ + ... + @property + def log(self) -> List[CaptouchPetalLogFrame]: + """ + Raw frame output of the captouch driver. Must be enabled by ``captouch.Config``. + + Since ``think()`` and the captouch driver are running asynchronously - oftentimes the + driver is even running asynchronously with _itself_! - the attributes provided in this + class are conflated from several frames for the purpose of not missing short presses, + hysteresis and filtering data. The filters are chosen to provide a good balance between + response time and noise, but it's easy to miss datapoints entirely, or accidentially + processing the same twice. For advanced captouch operation this is an issue. + + Every ``think()`` cycle this log is filled with the frames that occurred since the last + capture. The lowest indices are the oldest frames, so that you could compile a complete log + (or one cropped to arbitrary length) simply by appending new data. + + Is affected by REPL weirdness. # TODO + """ + ... + @property + def position(self) -> Tuple[int, int]: + """ + Legacy API + + Similar to raw_pos, but not normalized. + # TODO: Add conversion formula """ ... @@ -49,7 +153,7 @@ class CaptouchState(Protocol): """ State of individual petals. - Contains 10 elements, with the zeroth element being the petal closest to + Contains 10 elements, with the zeroeth element being the petal closest to the USB port. Then, every other petal in a clockwise direction. Petals 0, 2, 4, 6, 8 are Top petals. @@ -57,22 +161,91 @@ class CaptouchState(Protocol): Petals 1, 3, 5, 7, 9 are Bottom petals. """ ... + @property + def ticks_us(self) -> int: + """ + Timestamp of when the captouch data has been requested from the backend, i.e. when + the ``think()`` cycle started. Mostly useful for comparing for to the same attribute + of ``CaptouchPetalLogFrame``. Behaves identical to the return type of + ``time.ticks_us()`` and should only be used with ``time.ticks_diff()`` to avoid + overflow issues. Overflow occurs after ~10min. + """ + ... def read() -> CaptouchState: """ + LEGACY API + Reads current captouch state from hardware and returns a snapshot in time. - """ - ... + ``.pressed`` and ``.log`` attributes are broken in the REPL. -def calibration_active() -> bool: - """ - Returns true if the captouch system is current recalibrating. - """ - ... + Typically you'd want to use the captouch data provided by ``think()``, so this method + is replaced with nothing. See workaround for reasoning. -def calibration_request() -> None: - """ - Attempts to start calibration of captouch controllers. No-op if a - calibration is already active. """ ... + +class Config: + class PetalConfig: + @property + def mode(self) -> int: + """ + What kind of data should be collected for this petal. + + 0: No data at all + + 1: Button Mode: All pads combined, no positional output + + 2: 1D: Only radial position is provided + + 3: 2D: Full positional output. Only available for top petals. + + Default: 3 (top petals), 2 (bottom petals) + """ + ... + @property + def logging(self) -> bool: + """ + Whether or not you want to collect the raw data log. This + eats some CPU time proportional to the ``think()`` cycle + time, use only when you actually do something with the data. + + Default: False + """ + ... + @property + def petals(self) -> List[PetalConfig]: + """ + Config of individual petals, indexed as in the ``CaptouchState`` object. + """ + ... + @classmethod + def empty(cls) -> "Config": + """ + Initializer method that returns a config with everything disabled. Ideal + for ORing the requirements of different components together. + """ + ... + @classmethod + def default(cls) -> "Config": + """ + Initializer method that returns the default config, same as when entering an application. + """ + ... + @classmethod + def current(cls) -> "Config": + """ + Initializer method that returns the currently active config. + """ + ... + def apply(self) -> None: + """ + Apply this config to the driver. + """ + ... + def apply_default(self) -> None: + """ + Convenience method to restore defaults. same as ``Config.default().apply()`` but mildly faster + if you already have one around. + """ + ... diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py index 8d704e1eacf48fe1b6920a3e1c587145cdd20b5b..1eaa146a4529a1c2f0dc1a5822ef70e332321831 100644 --- a/python_payload/st3m/application.py +++ b/python_payload/st3m/application.py @@ -26,6 +26,7 @@ import sys import random import time import math +import captouch log = Log(__name__) @@ -62,6 +63,7 @@ def setup_for_app(app_ctx: Optional[ApplicationContext]) -> None: elif wifi_preference is False: st3m.wifi.disable() leds.set_slew_rate(settings.num_leds_speed.value) + captouch.Config.default().apply class Application(BaseView): diff --git a/python_payload/st3m/main_menu.py b/python_payload/st3m/main_menu.py index 010d80a238c8e73c197dc030aee0ed2befdaa977..201af8cff0dc226c927f87d0235f40f24ddae1f2 100644 --- a/python_payload/st3m/main_menu.py +++ b/python_payload/st3m/main_menu.py @@ -33,6 +33,7 @@ from st3m.ui.elements.menus import SimpleMenu, SunMenu from st3m.utils import rm_recursive, simplify_path from st3m import application_settings from st3m.ui import colours +import captouch import bl00mbox @@ -207,6 +208,7 @@ def restore_sys_defaults(): sys_display.set_backlight(settings.num_display_brightness.value) led_patterns.set_menu_colors() bl00mbox.Sys.foreground_clear() + captouch.Config.default().apply() # media.stop() diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py index 3f9f3342ac082bb6aff40096041b272b97dd047d..1c36b707e1d4e3de73c8bac42be18c8c64bd7a5d 100644 --- a/python_payload/st3m/reactor.py +++ b/python_payload/st3m/reactor.py @@ -7,7 +7,7 @@ from ctx import Context import time import sys_display import sys_kernel -import captouch +import sys_captouch import bl00mbox @@ -141,7 +141,7 @@ class Reactor: # temp band aid against input dropping, will be cleaned up in # upcoming input api refactor - captouch.refresh_events() + sys_captouch.refresh_events() hr = InputState() diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py index cc7534f2206c8e52713f125d0840e5d73ae316cc..ceb00e2b0867f0cbf15f6d4640c363b1666de29d 100644 --- a/python_payload/st3m/run.py +++ b/python_payload/st3m/run.py @@ -2,6 +2,7 @@ from st3m.reactor import Reactor, Responder from st3m.goose import Optional from st3m.ui.elements import overlays from st3m.ui.view import View, ViewManager, ViewTransitionBlend +from st3m.ui.captouch_calibrator import CaptouchCalibrator from st3m.main_menu import MainMenu from st3m.application import ( BundleManager, @@ -14,7 +15,7 @@ from st3m.input import InputState import st3m.wifi import st3m.utils -import captouch +import sys_captouch import audio import leds import gc @@ -23,6 +24,7 @@ import sys_display import sys_mode import bl00mbox import os +import json import machine import network @@ -125,6 +127,7 @@ def run_view(v: View, debug_vm=True) -> None: def run_app(klass, bundle_path=None): app_ctx = ApplicationContext(bundle_path) + CaptouchCalibrator.check_captouch() setup_for_app(app_ctx) run_view(klass(app_ctx), debug_vm=True) @@ -141,9 +144,11 @@ def run_main() -> None: log.info(f"starting main") log.info(f"free memory: {gc.mem_free()}") + captouch_calibration_ok = CaptouchCalibrator.check_captouch() + timer = None autostart_settings = application_settings.get_autostart() - autostart_slug = autostart_settings["slug"] + autostart_slug = autostart_settings["slug"] if captouch_calibration_ok else None if autostart_settings["slug"]: # crimes :> ctx = sys_display.ctx(sys_display.osd) @@ -171,8 +176,6 @@ def run_main() -> None: timer = time.ticks_ms() + 2000 sys_display.update(ctx) - captouch.calibration_request() - audio.headphones_set_volume_dB(settings.num_headphones_startup_volume_db.value) audio.speaker_set_volume_dB(settings.num_speaker_startup_volume_db.value) audio.headphones_set_minimum_volume_dB(settings.num_headphones_min_db.value) @@ -220,30 +223,34 @@ def run_main() -> None: application_settings.synchronize_apps(bundles.bundles.values()) menu_main = MainMenu(bundles) - views = [menu_main] - while timer: - if time.ticks_ms() > timer: - timer = None - ins = InputState() - if autostart_slug is None: - pass - elif ins.buttons.os + ins.buttons.app: - log.info(f"Bypassing autostart") - elif autostart_slug == "++back_button": - ViewManager._ignore_press = True - else: - requested = [ - b - for b in bundles.bundles.values() - if application_settings.is_autostart(b.path) - ] - if len(requested) > 1: - log.error(f"More than one bundle with slug {autostart_slug}") - elif len(requested) == 0: - log.error(f"Requested bundle {autostart_slug} not found") + if captouch_calibration_ok: + views = [menu_main] + while timer: + if time.ticks_ms() > timer: + timer = None + ins = InputState() + if autostart_slug is None: + pass + elif ins.buttons.os + ins.buttons.app: + log.info(f"Bypassing autostart") + elif autostart_slug == "++back_button": + ViewManager._ignore_press = True else: - log.info(f"Autostarting into {autostart_slug}") - views += [requested[0].load()] + requested = [ + b + for b in bundles.bundles.values() + if application_settings.is_autostart(b.path) + ] + if len(requested) > 1: + log.error(f"More than one bundle with slug {autostart_slug}") + elif len(requested) == 0: + log.error(f"Requested bundle {autostart_slug} not found") + else: + log.info(f"Autostarting into {autostart_slug}") + views += [requested[0].load()] + else: + views = [CaptouchCalibrator(menu=menu_main)] + run_views(views, debug_vm=False) diff --git a/python_payload/st3m/settings_menu.py b/python_payload/st3m/settings_menu.py index 6f802daaf1311c622ce7ecd85acbe38b09913cd9..f0906e6a583c3bbd43451f24949c3e47670b53fa 100644 --- a/python_payload/st3m/settings_menu.py +++ b/python_payload/st3m/settings_menu.py @@ -11,10 +11,16 @@ from st3m.goose import ( Union, TYPE_CHECKING, ) -from st3m.ui.menu import MenuItem, MenuItemBack, MenuItemAction +from st3m.ui.menu import ( + MenuItem, + MenuItemBack, + MenuItemAction, + MenuItemLaunchPersistentView, +) from st3m.application import BundleMetadata, MenuItemAppLaunch from st3m.ui.elements.menus import SimpleMenu from st3m.ui.view import ViewManager +from st3m.ui.captouch_calibrator import CaptouchCalibrator from st3m.settings import * from ctx import Context @@ -217,6 +223,7 @@ settings_menu_structure: "MenuStructure" = [ MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/audio_config")), MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/appearance")), MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/graphics_mode")), + MenuItemLaunchPersistentView("Captouch Calibrator", CaptouchCalibrator), onoff_wifi, onoff_wifi_preference, onoff_show_tray, diff --git a/python_payload/st3m/ui/captouch_calibrator.py b/python_payload/st3m/ui/captouch_calibrator.py new file mode 100644 index 0000000000000000000000000000000000000000..ec14763084da96d8479f45c67bb2ac7c27af0f2d --- /dev/null +++ b/python_payload/st3m/ui/captouch_calibrator.py @@ -0,0 +1,387 @@ +import cmath +import math +import time +import sys_captouch +import captouch +import json +from st3m.ui.view import BaseView + + +class CaptouchCalibrator(BaseView): + BASE_STATE = 0 + PRE_COUNTDOWN_STATE = 1 + COUNTDOWN_STATE = 2 + CALIBRATION_STATE = 3 + CALIBRATION_CHECK_STATE = 4 + SAVE_STATE = 5 + POST_SAVE_STATE = 6 + FIRST_BOOT_HINT_STATE = 7 + + def __init__(self, menu=None): + super().__init__() + self.menu = menu + self.vm = None + self.timer = 0 + self.button = None + self.calib_stage = -1 + self.calib_ok = None + self.calib = None + self.save_ok = None + self.full_redraw = True + self._selection_index = 0 + self.selections = [] + self.app_is_left = None + self.calib_prev = None + + @property + def state(self): + return self._state + + @state.setter + def state(self, val): + if val == self.PRE_COUNTDOWN_STATE: + self.selections = ["start 3s countdown"] + elif val == self.CALIBRATION_CHECK_STATE: + self.selections = ["redo", "save"] + elif val == self.POST_SAVE_STATE: + self.selections = ["ok"] + elif val == self.FIRST_BOOT_HINT_STATE: + self.selections = ["exit"] + elif val == self.BASE_STATE: + self.selections = ["clear", "calibrate"] + else: + self.selections = [] + self.selection_index = 0 + self._state = val + self.pos = [None] * 10 + self.pos_prev = [None] * 10 + self.full_redraw = True + + @property + def selection_index(self): + return self._selection_index + + @selection_index.setter + def selection_index(self, val): + if self.selections: + self._selection_index = val % len(self.selections) + else: + self._selection_index = None + + def restore_calib(self): + if self.calib_prev is not None: + sys_captouch.calibration_set_data(self.calib_prev) + self.calib_prev = None + + def think(self, ins, delta_ms): + super().think(ins, delta_ms) + self.app_is_left = ins.buttons.app_is_left + if self.button is not None: + press_event = (self.button != ins.buttons.app) and ins.buttons.app + else: + press_event = False + self.button = int(ins.buttons.app) + self.draw_time += delta_ms + + if self.state in [self.BASE_STATE, self.CALIBRATION_CHECK_STATE]: + for x in range(10): + self.pos[x] = ins.captouch.petals[x].pos + + if self.state == self.BASE_STATE: + if press_event: + if self.button == ins.buttons.PRESSED_DOWN: + if self.selection_index: + self.state = self.PRE_COUNTDOWN_STATE + else: + self.state = self.BASE_STATE + else: + self.selection_index += self.button + elif self.state == self.PRE_COUNTDOWN_STATE: + if press_event and self.button == ins.buttons.PRESSED_DOWN: + self.state = self.COUNTDOWN_STATE + self.timer = 2999 + elif self.state == self.COUNTDOWN_STATE: + self.timer -= delta_ms + if self.timer < 0: + self.state = self.CALIBRATION_STATE + self.calib_prev = sys_captouch.calibration_get_data() + sys_captouch.calibration_request() + self.calib_stage = 3 + elif self.state == self.CALIBRATION_STATE: + self.calib_stage = sys_captouch.calibration_active() + if not self.calib_stage: + self.calib = sys_captouch.calibration_get_data() + if self.calib: + self.state = self.CALIBRATION_CHECK_STATE + self.calib_stage = -1 + self.calib_ok = None + elif self.state == self.CALIBRATION_CHECK_STATE: + if self.calib_ok is None: + try: + self.calib_ok = [True] * 10 + + def check_entry(_arg): + _vals = [int(x) for x in _arg.split("/")] + return _vals[0] < 52000 and _vals[0] + + for petalbias in self.calib["ad7147"]: + petal = petalbias["petal"] + if petal % 2: + labels = ["0d", "1d"] + else: + labels = ["0d", "1d", "2d"] + for label in labels: + entries = petalbias[label] + for entry in entries: + self.calib_ok[petal] = self.calib_ok[ + petal + ] and check_entry(entry) + except Exception as e: + print(e) + self.calib_ok = False + self.full_redraw = True + if press_event: + if self.button == ins.buttons.PRESSED_DOWN: + if self.selection_index: + self.state = self.SAVE_STATE + self.calib_prev = None + else: + self.restore_calib() + self.state = self.PRE_COUNTDOWN_STATE + else: + self.selection_index += self.button + elif self.state == self.SAVE_STATE: + if not self.full_redraw: + self.save_ok = bool(self.calib) + if self.save_ok: + try: + with open(sys_captouch.calibration_path, "w") as f: + json.dump(self.calib, f, indent=4) + except: + self.save_ok = False + if self.save_ok: + sys_captouch.calibration_set_trusted(True) + self.state = self.POST_SAVE_STATE + elif self.state == self.POST_SAVE_STATE: + if press_event and self.button == ins.buttons.PRESSED_DOWN: + if self.menu: + self.state = self.FIRST_BOOT_HINT_STATE + else: + self.state = self.BASE_STATE + elif self.state == self.FIRST_BOOT_HINT_STATE: + if press_event and self.button == ins.buttons.PRESSED_DOWN: + if self.menu: + if self.vm and not self.vm._history: + self.vm.replace(self.menu) + self.menu = None + + def draw_selection(self, ctx, selections, selection_index): + ctx.save() + col_fg = (0, 1, 1) + col_bg = (0, 0, 0) + text_space = ctx.text_width(" ") + strings = ["["] + selections + ["]"] + text_widths = [ctx.text_width(x) + text_space for x in strings] + + orig_x, orig_y = ctx.x, ctx.y + ctx.rgb(*col_bg).rectangle(-self._width / 2, ctx.y - 18, self._width, 26).fill() + ctx.move_to(orig_x, orig_y) + + new_width = sum(text_widths) + if abs(new_width - self._width) < 0.1: + ctx.rel_move_to(-new_width / 2, 0) + for x, string in enumerate(strings): + orig_x, orig_y = ctx.x, ctx.y + ctx.rgb(*col_fg) + if selections and selection_index == x - 1: + ctx.rectangle( + ctx.x, ctx.y - ctx.font_size + 4, text_widths[x], ctx.font_size + ).fill() + ctx.move_to(orig_x, orig_y) + ctx.rgb(*col_bg) + ctx.rel_move_to(text_widths[x] / 2, 0) + ctx.text(string) + ctx.move_to(orig_x + text_widths[x], orig_y) + else: + if self._width < new_width: + self._width += self.draw_time * 0.6 + if self._width > new_width: + self._width = new_width + elif self._width > new_width: + self._width -= self.draw_time * 0.6 + if self._width < new_width: + self._width = new_width + orig_y = ctx.y + ctx.rgb(*col_fg) + ctx.move_to(-self._width / 2 + text_widths[0] / 2, orig_y) + ctx.text(strings[0]) + ctx.move_to(self._width / 2 - text_widths[-1] / 2, orig_y) + ctx.text(strings[-1]) + + ctx.restore() + + def draw(self, ctx) -> None: + if self.always_full_redraw: + self.full_redraw = True + ctx.text_align = ctx.CENTER + if self.full_redraw: + ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill() + ctx.rgb(1, 0, 1) + ctx.move_to(0, 0) + ctx.font = ctx.get_font_name(4) + ctx.font_size = 22 + ctx.move_to(0, -80) + ctx.text("Captouch") + ctx.move_to(0, -55) + ctx.text("Calibrator") + ctx.font = ctx.get_font_name(6) + ctx.font_size = 18 + ctx.rgb(0, 1, 1) + + pos = -20 + ctx.move_to(0, pos) + if self.state in [ + self.PRE_COUNTDOWN_STATE, + self.COUNTDOWN_STATE, + self.CALIBRATION_STATE, + ]: + if self.full_redraw: + ctx.move_to(0, pos) + ctx.text("do not touch the petals from") + ctx.move_to(0, pos + 20) + ctx.text("the front during calibration") + else: + ctx.rgb(0, 0, 0).rectangle(-120, pos + 35, 240, 20).fill().rgb(0, 1, 1) + ctx.move_to(0, pos + 50) + if self.state == self.COUNTDOWN_STATE: + ctx.text("calibrating in " + str(1 + int(self.timer / 1000))) + elif self.state == self.CALIBRATION_STATE: + ctx.rgb(1.0, 0.5, 0.2) + ctx.text("calibrating" + "." * (4 - self.calib_stage)) + elif self.full_redraw: + if self.state == self.BASE_STATE: + if sys_captouch.calibration_get_trusted(): + ctx.text("trusted calibration found") + ctx.move_to(0, pos + 20) + ctx.text("you can draw on here to try it!") + ctx.move_to(0, pos + 50) + ctx.text("(top petals: 2D, bottom: 1D)") + else: + ctx.rgb(1.0, 0, 0) + ctx.text("no trusted calibration found") + ctx.move_to(0, pos + 20) + ctx.text("please perform a calibration") + elif self.state == self.CALIBRATION_CHECK_STATE: + if self.calib_ok is None: + ctx.text("checking calibration...") + elif not self.calib_ok: + ctx.rgb(1.0, 0, 0) + ctx.text("bad calibration, driver error") + ctx.move_to(0, pos + 20) + ctx.text("you should never see this") + else: + if all(self.calib_ok): + ctx.text("calibration looks good!") + ctx.move_to(0, pos + 20) + ctx.text("you can draw on here to try it!") + ctx.move_to(0, pos + 50) + ctx.text("(top petals: 2D, bottom: 1D)") + else: + ctx.rgb(1.0, 0.5, 0.2) + ctx.text("issues found on petals:") + ctx.move_to(0, pos + 20) + ctx.text( + " ".join( + [str(x) for x in range(10) if not self.calib_ok[x]] + ) + ) + ctx.move_to(0, pos + 50) + ctx.text("check for dirt") + elif self.state == self.SAVE_STATE: + ctx.move_to(0, pos + 20) + ctx.text("saving calibration...") + elif self.state == self.POST_SAVE_STATE: + ctx.move_to(0, pos + 20) + if self.save_ok: + ctx.rgb(0, 1.0, 0) + ctx.text("saved calibration!") + else: + ctx.rgb(1.0, 0, 0) + ctx.text("saving failed :/") + elif self.state == self.FIRST_BOOT_HINT_STATE: + ctx.move_to(0, pos) + ctx.text("you can redo this anytime") + ctx.move_to(0, pos + 20) + ctx.text("in System -> Settings") + ctx.move_to(0, pos + 40) + ctx.text("-> Captouch Calibration") + + if self.state in [self.CALIBRATION_CHECK_STATE, self.BASE_STATE]: + for x in range(10): + if self.pos_prev[x] is not None: + pos = ((self.pos_prev[x] * 27) + 73) * captouch.PETAL_ANGLES[x] + # if abs(self.pos_prev[x].real) > 1 or abs(self.pos_prev[x].imag) > 1: + if abs(self.pos_prev[x]) > 1: + ctx.rgb(0.0, 0.0, 1.0) + else: + ctx.rgb(1.0, 1.0, 0.0) + ctx.rectangle(pos.real - 2, pos.imag - 2, 4, 4).fill() + self.pos_prev[x] = None + ctx.rgb(1.0, 0.0, 0.0) + for x in range(10): + if self.pos[x] is not None: + pos = ((self.pos[x] * 27) + 73) * captouch.PETAL_ANGLES[x] + ctx.rectangle(pos.real - 1.5, pos.imag - 1.5, 3, 3).fill() + self.pos_prev[x] = self.pos[x] + + pos = 60 + ctx.move_to(0, pos) + self.draw_selection(ctx, self.selections, self.selection_index) + if ( + self.full_redraw + and self.menu + and self.state in [self.PRE_COUNTDOWN_STATE, self.CALIBRATION_CHECK_STATE] + and self.app_is_left is not None + ): + ctx.move_to(0, pos + 25) + ctx.rgb(0.0, 0.7, 0.7) + ctx.font_size = 15 + shoulder_side = "left" if self.app_is_left else "right" + ctx.text(f"({shoulder_side} shoulder button") + ctx.move_to(0, pos + 40) + if self.state == self.PRE_COUNTDOWN_STATE: + ctx.text("down to confirm)") + if self.state == self.CALIBRATION_CHECK_STATE: + ctx.text("left/right to select)") + self.draw_time = 0 + self.full_redraw = self.app_is_left is None + + def on_enter(self, vm) -> None: + super().on_enter(vm) + self.button = None + self.state = self.PRE_COUNTDOWN_STATE if self.menu else self.BASE_STATE + self._width = 0 + self.draw_time = 0 + self.always_full_redraw = True + self.calibration_trusted = sys_captouch.calibration_get_trusted() + + def on_enter_done(self): + self.always_full_redraw = False + + def on_exit(self): + self.always_full_redraw = True + self.restore_calib() + + def on_exit_done(self): + self.always_full_redraw = False + + @classmethod + def check_captouch(cls): + if not sys_captouch.calibration_get_trusted(): + try: + with open(sys_captouch.calibration_path) as f: + sys_captouch.calibration_set_data(json.load(f)) + sys_captouch.calibration_set_trusted(True) + except: + pass + return sys_captouch.calibration_get_trusted() diff --git a/sim/fakes/captouch.py b/sim/fakes/captouch.py index 9194d98b79dd3f76666253662ef9073561198742..038fdf43aff363ad9b6451773e07d1fb6386284e 100644 --- a/sim/fakes/captouch.py +++ b/sim/fakes/captouch.py @@ -1,15 +1,57 @@ from typing import List +class PetalLogFrame: + def __init__(self, ix: int, pressed: bool): + self.mode = 2 if ix % 2 else 3 + self.pos = 0j if pressed else None + self.raw_cap = 4 if pressed else 0 + self.raw_pos = 0j + self.ticks_us = 0 + + +class PetalLog: + def __init__(self): + self.clear() + + def append(self, frame: PetalLogFrame): + self.frames.append(frame) + self.length += 1 + self.length_ms += 10 + + def clear(self): + self.frames = [] + self.length = 0 + self.length_ms = 0 + + def index_offset_ms(self, index: int, min_offset_ms: float): + if index < 0: + index += self.length + if index >= self.length: + index = self.length - 1 + index += int(min_offset_ms / 10) + if index < 0: + index = 0 + if index >= self.length: + index = self.length - 1 + return index + + def average(start: int = None, stop: int = None): + return 0 + + def slope_per_ms(start: int = None, stop: int = None): + return 0 + + class CaptouchPetalState: def __init__(self, ix: int, pressed: bool): - self._pressed = pressed + self.pressed = pressed self._ix = ix self.position = (0, 0) - - @property - def pressed(self) -> bool: - return self._pressed + self.pos = 0j if pressed else None + self.raw_cap = 4 if pressed else 0 + self.raw_pos = 0j + self.log = [CaptouchPetalLogFrame(ix, pressed)] @property def top(self) -> bool: @@ -20,6 +62,52 @@ class CaptouchPetalState: return not self.top +_modes = [3 - (x % 2) for x in range(10)] +_logging = [False for x in range(10)] + + +class PetalConfig: + mode = 0 + logging = False + + def set_min_mode(self, mode): + self.mode = max(self.mode, mode) + + +class Config: + def __init__(self, _token=None): + if _token is None: + raise ValueError("don't initialize directly") + self.petals = [PetalConfig() for x in range(10)] + for x in range(10): + if _token == 1: + self.petals[x].mode = 3 - (x % 2) + if _token == 2: + self.petals[x].mode = _modes[x] + + def apply(self): + for x in range(10): + _modes[x] = self.petals[x].mode + _logging[x] = self.petals[x].logging + + def apply_default(self): + for x in range(10): + _modes[x] = 3 - (x % 2) + _logging[x] = False + + @classmethod + def current(cls): + return cls(_token=2) + + @classmethod + def default(cls): + return cls(_token=1) + + @classmethod + def empty(cls): + return cls(_token=0) + + class CaptouchState: def __init__(self, petals: List[CaptouchPetalState]): self._petals = petals diff --git a/sim/fakes/sys_captouch.py b/sim/fakes/sys_captouch.py new file mode 100644 index 0000000000000000000000000000000000000000..d7aba9131100f8f4b5925571a6453e29a03beba4 --- /dev/null +++ b/sim/fakes/sys_captouch.py @@ -0,0 +1,46 @@ +def calibration_get_data(): + return [0] * 120 + + +def calibration_set_data(val): + pass + + +_trusted = False + + +def refresh_events(): + pass + + +def calibration_set_trusted(val): + global _trusted + _trusted = bool(val) + + +def calibration_get_trusted(): + return _trusted + + +_modes = [3, 2] * 5 + + +def config_set_petal_modes(val): + global _modes + _modes = val + + +def config_get_petal_modes(): + return _modes + + +_logging = [False] * 10 + + +def config_set_logging(val): + global _logging + _logging = val + + +def config_get_logging(): + return _logging