From 2c2a5a5613ed92dec3c0c4bf8de4332907dda9c2 Mon Sep 17 00:00:00 2001 From: whowechina Date: Sun, 12 Apr 2026 22:09:00 +0800 Subject: [PATCH] PS4 support, but no auth --- Production/Firmware/diva_pico.uf2 | Bin 122368 -> 135680 bytes firmware/src/CMakeLists.txt | 2 +- firmware/src/commands.c | 6 +- firmware/src/gesture.c | 477 ++++++++++++++++++++++++ firmware/src/gesture.h | 18 + firmware/src/main.c | 12 +- firmware/src/slide.c | 598 ------------------------------ firmware/src/slide.h | 33 -- 8 files changed, 503 insertions(+), 643 deletions(-) create mode 100644 firmware/src/gesture.c create mode 100644 firmware/src/gesture.h delete mode 100644 firmware/src/slide.c delete mode 100644 firmware/src/slide.h diff --git a/Production/Firmware/diva_pico.uf2 b/Production/Firmware/diva_pico.uf2 index 53278e0ae29a28e58b400fce4f04eed2241e9a93..966a362d7694e2e85a793ec7dfeceb14c12dd56e 100644 GIT binary patch delta 41077 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 delta 34519 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 diff --git a/firmware/src/CMakeLists.txt b/firmware/src/CMakeLists.txt index 5114b79..4078b1a 100644 --- a/firmware/src/CMakeLists.txt +++ b/firmware/src/CMakeLists.txt @@ -5,7 +5,7 @@ function(make_firmware board board_def) pico_sdk_init() add_executable(${board} main.c slider.c rgb.c button.c savedata.c config.c commands.c - cli.c mpr121.c hebtn.c lzfx.c usb_descriptors.c ps4_feat.c slide.c) + cli.c mpr121.c hebtn.c lzfx.c usb_descriptors.c ps4_feat.c gesture.c) target_compile_definitions(${board} PUBLIC ${board_def}) pico_enable_stdio_usb(${board} 1) pico_enable_stdio_uart(${board} 0) diff --git a/firmware/src/commands.c b/firmware/src/commands.c index 599601a..72b3868 100644 --- a/firmware/src/commands.c +++ b/firmware/src/commands.c @@ -13,7 +13,7 @@ #include "savedata.h" #include "cli.h" -#include "slide.h" +#include "gesture.h" #include "usb_descriptors.h" @@ -401,7 +401,7 @@ static void handle_debug(int argc, char *argv[]) break; case 1: diva_runtime.debug.slide_cluster ^= true; - slide_set_debug_cluster(diva_runtime.debug.slide_cluster); + gesture_set_debug_cluster(diva_runtime.debug.slide_cluster); break; default: printf(usage); @@ -414,7 +414,7 @@ void cli_ctrl_c_cb(void) diva_runtime.debug.sensor = false; diva_runtime.debug.slide_cluster = false; hebtn_debug(false); - slide_set_debug_cluster(false); + gesture_set_debug_cluster(false); } static void handle_save() diff --git a/firmware/src/gesture.c b/firmware/src/gesture.c new file mode 100644 index 0000000..8aac65c --- /dev/null +++ b/firmware/src/gesture.c @@ -0,0 +1,477 @@ +/* + * Extract gesture information from raw electrode touch bitmask. + * + * I'm using a region clustering + tracking approach. + * Regions are contiguous (or gap-bridged) sets of active electrodes in one frame. + * Clusters are living, persistent objects that are driven by regions. + * Gestures are recognized from cluster movement and reported as joystick axes. + * + * WHowe + */ + +#include "gesture.h" + +#include +#include +#include +#include + +#include "pico/stdlib.h" + +#define ELEC_COUNT 32 + +#define MAX_REGIONS 12 // Per-frame extracted regions. +#define MAX_CLUSTERS 12 // Living clusters tracked across frames. + +#define REGION_GAP_SIZE 1 // Maximum gap size (in electrodes) allowed inside one region. +#define MATCH_MAX_DIST_Q4 56 // Max cluster-to-region match distance in Q4 units. + +#define DEBOUNCE_SET_FRAMES 4 +#define DEBOUNCE_RESET_FRAMES 50 + +#define SEQ_MIN_STEPS 3 // Required consecutive +/-1 index moves to confirm direction. +#define SEQ_TIMEOUT_FRAMES 200 // Max frames allowed between sequential steps. +#define DEATH_HOLD_FRAMES 100 // Cluster death delay. + +#define AXIS_CENTER 0x80 +#define AXIS_OFFSET 100 // Signed offset from center when steering left/right. + +// A region is a contiguous (or gap-bridged) set of active electrodes in one frame. +// Q4 units are used for more precise center position and distance calculation. +typedef struct { + int start; + int end; + int width; + int center_q4; +} region_t; + +// A cluster is a living, persistent object that follows a region across frames. +typedef struct { + bool alive; + int id; + int miss; + int width; + int slot; + int center_q4; + int prev_center_q4; + int last_idx; + int seq_age; + int seq_len; + int seq_dir; + int dir; +} cluster_t; + +static cluster_t clusters[MAX_CLUSTERS]; +static int next_id; + +static uint32_t db_state; +static int db_cnt[ELEC_COUNT]; + +static bool debug_cluster; +static int latch_frames = 100; + +static int hold_left_dir; +static int hold_right_dir; +static int hold_left_frames; +static int hold_right_frames; + +static uint32_t debounce_mask(uint32_t raw) +{ + for (int i = 0; i < ELEC_COUNT; i++) { + uint32_t bit = (raw >> i) & 1u; + bool state = ((db_state >> i) & 1u) != 0u; + int threshold = state ? DEBOUNCE_RESET_FRAMES : DEBOUNCE_SET_FRAMES; + if (bit != state) { + if (db_cnt[i] < threshold) { + db_cnt[i]++; + } + if (db_cnt[i] >= threshold) { + db_state ^= (1u << i); + db_cnt[i] = 0; + } + } else { + db_cnt[i] = 0; + } + } + return db_state; +} + +static int extract_regions(uint32_t mask, region_t *regions, int max_regions) +{ + int count = 0; + int i = 0; + + while (i < ELEC_COUNT) { + if (((mask >> i) & 1u) == 0u) { + i++; + continue; + } + + int start = i; + int end = i; + i++; + + while (i < ELEC_COUNT) { + if (((mask >> i) & 1u) != 0u) { + end = i; + i++; + } else { + int j = i; + while ((j < ELEC_COUNT) && (((mask >> j) & 1u) == 0u)) { + j++; + } + int gap = j - i; + if ((gap <= REGION_GAP_SIZE) && (j < ELEC_COUNT)) { + i = j; + } else { + break; + } + } + } + + if (count < max_regions) { + regions[count].start = start; + regions[count].end = end; + regions[count].width = end - start + 1; + regions[count].center_q4 = (start + end) * 8; + count++; + } + } + + return count; +} + +static int alloc_cluster(void) +{ + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive) { + return i; + } + } + return -1; +} + +static int q4_to_idx(int q4) +{ + if (q4 <= 0) { + return 0; + } + if (q4 >= 31 * 16) { + return 31; + } + return (q4 + 8) / 16; +} + +static int sgn16(int x) +{ + return (x > 0) - (x < 0); +} + +static void update_seq(cluster_t *c) +{ + int idx = q4_to_idx(c->center_q4); + if (idx == c->last_idx) { + if (c->seq_age < 0x7FFFFFFF) { + c->seq_age++; + } + if (c->seq_age > SEQ_TIMEOUT_FRAMES) { + c->dir = 0; + } + return; + } + + int diff = idx - c->last_idx; + int ad = abs(diff); + int sd = sgn16(diff); + + if (ad == 1 && c->seq_age <= SEQ_TIMEOUT_FRAMES) { + if (sd == c->seq_dir) { + if (c->seq_len < 0x7FFFFFFF) { + c->seq_len++; + } + } else { + c->seq_dir = sd; + c->seq_len = 1; + } + } else { + c->seq_dir = 0; + c->seq_len = 0; + } + + c->last_idx = idx; + c->seq_age = 0; + + if (c->seq_len >= SEQ_MIN_STEPS) { + c->dir = c->seq_dir; + } +} + +static void assign_slot_if_needed(cluster_t *c) +{ + if (c->slot != -1 || c->dir == 0) { + return; + } + + bool left_used = false; + bool right_used = false; + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive) { + continue; + } + if (clusters[i].slot == 0) { + left_used = true; + } else if (clusters[i].slot == 1) { + right_used = true; + } + } + + if (c->center_q4 < (16 * 16)) { + if (!left_used) { + c->slot = 0; + } else if (!right_used) { + c->slot = 1; + } + } else { + if (!right_used) { + c->slot = 1; + } else if (!left_used) { + c->slot = 0; + } + } +} + +static void match_and_update(const region_t *regions, int region_count) +{ + bool region_used[MAX_REGIONS] = {0}; + bool cluster_used[MAX_CLUSTERS] = {0}; + + while (true) { + int best = 0x7FFFFFFF; + int best_cluster = -1; + int best_region = -1; + + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive || cluster_used[i]) { + continue; + } + for (int j = 0; j < region_count; j++) { + if (region_used[j]) { + continue; + } + int d = abs(clusters[i].center_q4 - regions[j].center_q4); + if (d < best) { + best = d; + best_cluster = i; + best_region = j; + } + } + } + + if (best_cluster < 0 || best_region < 0 || best > MATCH_MAX_DIST_Q4) { + break; + } + + cluster_t *c = &clusters[best_cluster]; + const region_t *reg = ®ions[best_region]; + + c->miss = 0; + c->prev_center_q4 = c->center_q4; + c->center_q4 = reg->center_q4; + c->width = reg->width; + update_seq(c); + assign_slot_if_needed(c); + + cluster_used[best_cluster] = true; + region_used[best_region] = true; + } + + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive || cluster_used[i]) { + continue; + } + clusters[i].miss++; + if (clusters[i].miss > DEATH_HOLD_FRAMES) { + memset(&clusters[i], 0, sizeof(clusters[i])); + } + } + + for (int j = 0; j < region_count; j++) { + if (region_used[j]) { + continue; + } + int ci = alloc_cluster(); + if (ci < 0) { + break; + } + cluster_t *c = &clusters[ci]; + memset(c, 0, sizeof(*c)); + c->alive = true; + c->id = next_id++; + c->slot = -1; + c->center_q4 = regions[j].center_q4; + c->prev_center_q4 = regions[j].center_q4; + c->width = regions[j].width; + c->last_idx = q4_to_idx(regions[j].center_q4); + } +} + +static void choose_dirs(int *left_dir, int *right_dir) +{ + int l = 0; + int r = 0; + int lscore = 0; + int rscore = 0; + + int moving_count = 0; + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (clusters[i].alive && clusters[i].dir != 0) { + moving_count++; + } + } + + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive || clusters[i].dir == 0) { + continue; + } + if (clusters[i].slot == 0 && clusters[i].seq_len >= lscore) { + lscore = clusters[i].seq_len; + l = clusters[i].dir; + } else if (clusters[i].slot == 1 && clusters[i].seq_len >= rscore) { + rscore = clusters[i].seq_len; + r = clusters[i].dir; + } + } + + if (moving_count >= 3) { + bool has_neg = false; + bool has_pos = false; + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive || clusters[i].dir == 0) { + continue; + } + if (clusters[i].dir < 0) { + has_neg = true; + } else if (clusters[i].dir > 0) { + has_pos = true; + } + } + if (has_neg && has_pos) { + if (l == 0) { + l = -1; + } + if (r == 0) { + r = 1; + } + } + } + + *left_dir = l; + *right_dir = r; +} + +static void hold_axis(int in_l, int in_r, int *out_l, int *out_r) +{ + if (in_l != 0) { + hold_left_dir = in_l; + hold_left_frames = latch_frames; + } else if (hold_left_frames > 0) { + hold_left_frames--; + } else { + hold_left_dir = 0; + } + + if (in_r != 0) { + hold_right_dir = in_r; + hold_right_frames = latch_frames; + } else if (hold_right_frames > 0) { + hold_right_frames--; + } else { + hold_right_dir = 0; + } + + *out_l = hold_left_dir; + *out_r = hold_right_dir; +} + +static void debug_print(void) +{ + if (!debug_cluster) { + return; + } + + bool printed = false; + for (int i = 0; i < MAX_CLUSTERS; i++) { + if (!clusters[i].alive) { + continue; + } + const char *tag = "*"; + if (clusters[i].dir > 0) { + tag = ">>"; + } else if (clusters[i].dir < 0) { + tag = "<<"; + } + if (printed) { + printf(" "); + } + printed = true; + printf("{%d %d %d %s %u s%u}", + clusters[i].id, + q4_to_idx(clusters[i].center_q4), + clusters[i].width, + tag, + (unsigned)(DEATH_HOLD_FRAMES - ((clusters[i].miss > DEATH_HOLD_FRAMES) ? DEATH_HOLD_FRAMES : clusters[i].miss)), + (unsigned)((clusters[i].slot < 0) ? 9 : clusters[i].slot)); + } + if (printed) { + printf("\n"); + } +} + +void gesture_reset(void) +{ + memset(clusters, 0, sizeof(clusters)); + next_id = 1; + + db_state = 0; + memset(db_cnt, 0, sizeof(db_cnt)); + + hold_left_dir = 0; + hold_right_dir = 0; + hold_left_frames = 0; + hold_right_frames = 0; +} + +void gesture_set_latch(int frames) +{ + latch_frames = (frames == 0) ? 1 : frames; +} + +void gesture_set_debug_cluster(bool enable) +{ + debug_cluster = enable; +} + +static int axis_from_dir(int dir) +{ + return AXIS_CENTER + dir * AXIS_OFFSET; +} + +void gesture_process(uint32_t mask, uint8_t *axis_a, uint8_t *axis_b) +{ + uint32_t filtered_mask = debounce_mask(mask); + + region_t regions[MAX_REGIONS]; + int region_num = extract_regions(filtered_mask, regions, count_of(regions)); + + match_and_update(regions, region_num); + + int l = 0; + int r = 0; + choose_dirs(&l, &r); + hold_axis(l, r, &l, &r); + + *axis_a = axis_from_dir(l); + *axis_b = axis_from_dir(r); + + debug_print(); +} diff --git a/firmware/src/gesture.h b/firmware/src/gesture.h new file mode 100644 index 0000000..0f6ed6a --- /dev/null +++ b/firmware/src/gesture.h @@ -0,0 +1,18 @@ +/* + * Extract gesture information from raw electrode touch bitmask. + * + * WHowe + */ + +#ifndef GESTURE_H +#define GESTURE_H + +#include +#include + +void gesture_reset(void); +void gesture_set_latch(int frames); +void gesture_set_debug_cluster(bool enable); +void gesture_process(uint32_t mask, uint8_t *axis_a, uint8_t *axis_b); + +#endif diff --git a/firmware/src/main.c b/firmware/src/main.c index 7a78bf1..c98dadc 100644 --- a/firmware/src/main.c +++ b/firmware/src/main.c @@ -35,7 +35,7 @@ #include "hebtn.h" #include "lzfx.h" #include "ps4_feat.h" -#include "slide.h" +#include "gesture.h" struct __attribute__((packed)) { uint16_t buttons; // 16 buttons; see JoystickButtons_t for bit mapping @@ -151,8 +151,6 @@ static void gen_ns_report() static void gen_ps4_report() { uint16_t ps4_buttons = mapped_buttons; - slide_result_t slide_result; - hid_ps4.left_y = 0x80; hid_ps4.right_y = 0x80; hid_ps4.hat_buttons = (ps4_buttons << 4); @@ -161,9 +159,7 @@ static void gen_ps4_report() hid_ps4.trigger_l = 0; hid_ps4.trigger_r = 0; - slide_process(touch_mask_raw(), &slide_result); - hid_ps4.left_x = slide_result.left_x; - hid_ps4.right_x = slide_result.right_x; + gesture_process(touch_mask_raw(), &hid_ps4.left_x, &hid_ps4.right_x); } static void gen_hid_report() @@ -326,8 +322,8 @@ void init() diva_runtime.hid_ps4 = (diva_cfg->hid.joy_map == 3); hid_use_ps4(diva_runtime.hid_ps4); - slide_reset(); - slide_set_latch(100); + gesture_reset(); + gesture_set_latch(100); board_init(); tusb_init(); diff --git a/firmware/src/slide.c b/firmware/src/slide.c deleted file mode 100644 index 4c9e628..0000000 --- a/firmware/src/slide.c +++ /dev/null @@ -1,598 +0,0 @@ -#include "slide.h" - -#include -#include -#include - -#define SLIDE_MIN_CLUSTER_WIDTH 1 -#define SLIDE_MAX_CLUSTERS 8 -#define SLIDE_CLUSTER_GAP 4 -#define SLIDE_TRACK_COUNT 2 -#define SLIDE_TRACK_HOLD_FRAMES 40 -#define SLIDE_TRIGGER_RANGE_Q4 32 -#define SLIDE_STOP_REBASE_FRAMES 192 -#define SLIDE_TOUCH_HOLD_FRAMES 64 - -#define SLIDE_DEBOUNCE_SET 10 -#define SLIDE_DEBOUNCE_RESET 50 - -#define SLIDE_NEUTRAL_AXIS 0x80 -#define SLIDE_AXIS_DELTA 0x50 - -typedef struct { - uint8_t start; - uint8_t end; - uint8_t width; - int16_t center_q4; -} cluster_t; - -typedef struct { - bool active; - uint8_t miss; - uint8_t age; - uint8_t width; - uint8_t still_frames; - int8_t step_dir; - int16_t anchor_q4; - int16_t pos_q4; - int16_t last_pos_q4; - int16_t min_q4; - int16_t max_q4; -} track_t; - -typedef struct { - uint8_t count; - uint8_t pos[SLIDE_MAX_CLUSTERS]; - uint8_t len[SLIDE_MAX_CLUSTERS]; - int8_t dir[SLIDE_MAX_CLUSTERS]; -} debug_snapshot_t; - -static track_t tracks[SLIDE_TRACK_COUNT]; - -static uint32_t debounce_state; -static uint8_t debounce_cnt[32]; - -static slide_gesture_t stable_gesture; -static uint16_t hold_frames; -static uint16_t latch_frames = 100; - -static bool debug_cluster; -static debug_snapshot_t debug_last; - -static uint8_t axis_from_dir(int8_t dir) -{ - if (dir < 0) { - return (uint8_t)(SLIDE_NEUTRAL_AXIS - SLIDE_AXIS_DELTA); - } - if (dir > 0) { - return (uint8_t)(SLIDE_NEUTRAL_AXIS + SLIDE_AXIS_DELTA); - } - return SLIDE_NEUTRAL_AXIS; -} - -static uint32_t debounce_mask(uint32_t raw) -{ - for (uint8_t i = 0; i < 32; i++) { - uint32_t bit = (raw >> i) & 1u; - bool state = (debounce_state >> i) & 1u; - if (!state) { - if (bit) { - if (debounce_cnt[i] < SLIDE_DEBOUNCE_SET) { - debounce_cnt[i]++; - } - if (debounce_cnt[i] >= SLIDE_DEBOUNCE_SET) { - debounce_state |= (1u << i); - } - } else { - debounce_cnt[i] = 0; - } - } else { - if (!bit) { - if (debounce_cnt[i] < SLIDE_DEBOUNCE_RESET) { - debounce_cnt[i]++; - } - if (debounce_cnt[i] >= SLIDE_DEBOUNCE_RESET) { - debounce_state &= ~(1u << i); - } - } else { - debounce_cnt[i] = 0; - } - } - } - return debounce_state; -} - -static int16_t abs16(int16_t x) -{ - return (x < 0) ? (int16_t)(-x) : x; -} - -static int8_t dir_from_displacement(int16_t delta_q4) -{ - if (delta_q4 >= SLIDE_TRIGGER_RANGE_Q4) { - return 1; - } - if (delta_q4 <= -SLIDE_TRIGGER_RANGE_Q4) { - return -1; - } - return 0; -} - -static int8_t track_dir(const track_t *t) -{ - if (!t->active) { - return 0; - } - return dir_from_displacement((int16_t)(t->pos_q4 - t->anchor_q4)); -} - -static uint16_t track_ttl(const track_t *t) -{ - if (!t->active) { - return 0; - } - return (t->miss >= SLIDE_TRACK_HOLD_FRAMES) ? 0u : (uint16_t)(SLIDE_TRACK_HOLD_FRAMES - t->miss); -} - -static uint8_t q4_to_idx(int16_t q4) -{ - if (q4 <= 0) { - return 0; - } - if (q4 >= (31 * 16)) { - return 31; - } - return (uint8_t)((q4 + 8) / 16); -} - -static bool track_triggered(const track_t *t) -{ - if (!t->active) { - return false; - } - return (t->max_q4 - t->min_q4) >= SLIDE_TRIGGER_RANGE_Q4; -} - -static uint8_t extract_clusters(uint32_t mask, cluster_t *clusters, uint8_t max_clusters) -{ - uint8_t count = 0; - uint8_t i = 0; - - while (i < 32) { - if (((mask >> i) & 1u) == 0u) { - i++; - continue; - } - - uint8_t start = i; - uint8_t end = i; - i++; - - /* extend cluster, bridging gaps <= SLIDE_CLUSTER_GAP */ - while (i < 32) { - if (((mask >> i) & 1u) != 0u) { - end = i; - i++; - } else { - /* measure gap length */ - uint8_t j = i; - while ((j < 32) && (((mask >> j) & 1u) == 0u)) { - j++; - } - uint8_t gap = (uint8_t)(j - i); - if ((gap <= SLIDE_CLUSTER_GAP) && (j < 32)) { - /* bridge the gap */ - i = j; - } else { - break; - } - } - } - - uint8_t width = (uint8_t)(end - start + 1); - - if (width < SLIDE_MIN_CLUSTER_WIDTH) { - continue; - } - - if (count < max_clusters) { - clusters[count].start = start; - clusters[count].end = end; - clusters[count].width = width; - clusters[count].center_q4 = (int16_t)((start + end) * 8); - count++; - } - } - - return count; -} - -static void sort_clusters(cluster_t *clusters, uint8_t count) -{ - for (uint8_t i = 0; i < count; i++) { - for (uint8_t j = (uint8_t)(i + 1); j < count; j++) { - if (clusters[j].center_q4 < clusters[i].center_q4) { - cluster_t t = clusters[i]; - clusters[i] = clusters[j]; - clusters[j] = t; - } - } - } -} - -static void update_track(track_t *t, bool matched, const cluster_t *c) -{ - if (matched) { - int16_t pos_q4 = c->center_q4; - - if (!t->active) { - t->active = true; - t->miss = 0; - t->age = 0; - t->width = c->width; - t->still_frames = 0; - t->step_dir = 0; - t->anchor_q4 = pos_q4; - t->pos_q4 = pos_q4; - t->last_pos_q4 = pos_q4; - t->min_q4 = pos_q4; - t->max_q4 = pos_q4; - return; - } - - int16_t delta_q4 = (int16_t)(pos_q4 - t->pos_q4); - int8_t step_dir = (delta_q4 > 0) ? 1 : ((delta_q4 < 0) ? -1 : 0); - - t->miss = 0; - t->age++; - t->last_pos_q4 = t->pos_q4; - t->pos_q4 = pos_q4; - t->width = c->width; - - if (step_dir != 0) { - if ((t->step_dir != 0) && (step_dir != t->step_dir)) { - t->anchor_q4 = t->last_pos_q4; - } - t->step_dir = step_dir; - t->still_frames = 0; - } else { - if (t->still_frames < 0xFF) { - t->still_frames++; - } - if (t->still_frames >= SLIDE_STOP_REBASE_FRAMES) { - t->anchor_q4 = t->pos_q4; - t->step_dir = 0; - } - } - - if (pos_q4 < t->min_q4) { - t->min_q4 = pos_q4; - } - if (pos_q4 > t->max_q4) { - t->max_q4 = pos_q4; - } - return; - } - - if (!t->active) { - return; - } - - if (t->miss < 0xFF) { - t->miss++; - } - - if (t->miss > SLIDE_TRACK_HOLD_FRAMES) { - memset(t, 0, sizeof(*t)); - } else { - t->last_pos_q4 = t->pos_q4; - if (t->still_frames < 0xFF) { - t->still_frames++; - } - } -} - -static slide_gesture_t classify_gesture(void) -{ - track_t *a = &tracks[0]; - track_t *b = &tracks[1]; - - if (a->active && b->active && (a->pos_q4 > b->pos_q4)) { - track_t tmp = *a; - *a = *b; - *b = tmp; - } - - bool a_ready = a->active && (a->age > 0) && track_triggered(a); - bool b_ready = b->active && (b->age > 0) && track_triggered(b); - - if (a_ready && b_ready) { - int8_t da = track_dir(a); - int8_t db = track_dir(b); - - if (da == db && da < 0) { - return SLIDE_GESTURE_DUAL_LEFT; - } - if (da == db && da > 0) { - return SLIDE_GESTURE_DUAL_RIGHT; - } - if (da > 0 && db < 0) { - return SLIDE_GESTURE_DUAL_CONVERGE; - } - if (da < 0 && db > 0) { - return SLIDE_GESTURE_DUAL_DIVERGE; - } - - return SLIDE_GESTURE_NONE; - } - - if (a_ready || b_ready) { - track_t *t = a_ready ? a : b; - int8_t d = track_dir(t); - if (d < 0) { - return SLIDE_GESTURE_SINGLE_LEFT; - } - if (d > 0) { - return SLIDE_GESTURE_SINGLE_RIGHT; - } - } - - return SLIDE_GESTURE_NONE; -} - -static void apply_latch(slide_gesture_t instant, bool touching) -{ - if (instant != SLIDE_GESTURE_NONE) { - stable_gesture = instant; - hold_frames = latch_frames; - return; - } - - if (touching && hold_frames > SLIDE_TOUCH_HOLD_FRAMES) { - hold_frames = SLIDE_TOUCH_HOLD_FRAMES; - } - - if (hold_frames > 0) { - hold_frames--; - } else { - stable_gesture = SLIDE_GESTURE_NONE; - } -} - -static bool snapshot_equals(const debug_snapshot_t *a, const debug_snapshot_t *b) -{ - if (a->count != b->count) { - return false; - } - for (uint8_t i = 0; i < a->count; i++) { - if (a->pos[i] != b->pos[i] || a->len[i] != b->len[i] || a->dir[i] != b->dir[i]) { - return false; - } - } - return true; -} - -static bool snapshot_contains(const debug_snapshot_t *s, uint8_t pos, uint8_t len) -{ - for (uint8_t i = 0; i < s->count; i++) { - if (s->pos[i] == pos && s->len[i] == len) { - return true; - } - } - return false; -} - -static int8_t nearest_track_info(int16_t center_q4, uint16_t *ttl, bool *matched) -{ - int16_t best = 0x7FFF; - int8_t best_dir = 0; - uint16_t best_ttl = 0; - bool best_match = false; - - for (uint8_t i = 0; i < SLIDE_TRACK_COUNT; i++) { - if (!tracks[i].active) { - continue; - } - int16_t d = abs16((int16_t)(tracks[i].pos_q4 - center_q4)); - if (d < best) { - best = d; - best_dir = track_dir(&tracks[i]); - best_ttl = track_ttl(&tracks[i]); - best_match = (d <= 24); - } - } - - if (ttl) { - *ttl = best_ttl; - } - if (matched) { - *matched = best_match; - } - return best_dir; -} - -static void debug_print_clusters(const cluster_t *clusters, uint8_t count) -{ - if (!debug_cluster) { - memset(&debug_last, 0, sizeof(debug_last)); - return; - } - - debug_snapshot_t now = {0}; - uint16_t ttl[SLIDE_MAX_CLUSTERS] = {0}; - - for (uint8_t i = 0; (i < count) && (i < SLIDE_MAX_CLUSTERS); i++) { - bool matched = false; - int8_t dir = nearest_track_info(clusters[i].center_q4, &ttl[i], &matched); - now.pos[now.count] = q4_to_idx(clusters[i].center_q4); - now.len[now.count] = clusters[i].width; - now.dir[now.count] = matched ? dir : 0; - now.count++; - } - - bool changed = !snapshot_equals(&now, &debug_last); - bool has_dead = false; - for (uint8_t i = 0; i < debug_last.count; i++) { - if (!snapshot_contains(&now, debug_last.pos[i], debug_last.len[i])) { - has_dead = true; - break; - } - } - - if (!changed && !has_dead) { - return; - } - - bool printed = false; - for (uint8_t i = 0; i < now.count; i++) { - const char *tag = "*"; - if (now.dir[i] > 0) { - tag = ">>"; - } else if (now.dir[i] < 0) { - tag = "<<"; - } - if (printed) { - printf(" "); - } - printed = true; - printf("(%u %u %s %u)", now.pos[i], now.len[i], tag, (unsigned)ttl[i]); - } - - for (uint8_t i = 0; i < debug_last.count; i++) { - if (snapshot_contains(&now, debug_last.pos[i], debug_last.len[i])) { - continue; - } - if (printed) { - printf(" "); - } - printed = true; - printf("(%u %u X %u)", debug_last.pos[i], debug_last.len[i], 0u); - } - - printf("\n"); - debug_last = now; -} - -void slide_reset(void) -{ - memset(tracks, 0, sizeof(tracks)); - debounce_state = 0; - memset(debounce_cnt, 0, sizeof(debounce_cnt)); - memset(&debug_last, 0, sizeof(debug_last)); - stable_gesture = SLIDE_GESTURE_NONE; - hold_frames = 0; -} - -void slide_set_latch(uint16_t frames) -{ - latch_frames = (frames == 0) ? 1 : frames; -} - -void slide_set_debug_cluster(bool enable) -{ - debug_cluster = enable; - if (!enable) { - memset(&debug_last, 0, sizeof(debug_last)); - } -} - -void slide_process(uint32_t mask, slide_result_t *out) -{ - cluster_t clusters[SLIDE_MAX_CLUSTERS]; - bool used[SLIDE_MAX_CLUSTERS] = {0}; - int8_t idx0 = -1; - int8_t idx1 = -1; - - uint8_t count = extract_clusters(debounce_mask(mask), clusters, SLIDE_MAX_CLUSTERS); - sort_clusters(clusters, count); - - if (tracks[0].active && count > 0) { - int16_t best = 0x7FFF; - int8_t best_idx = -1; - for (uint8_t i = 0; i < count; i++) { - int16_t d = abs16((int16_t)(clusters[i].center_q4 - tracks[0].pos_q4)); - if (d < best) { - best = d; - best_idx = (int8_t)i; - } - } - if (best_idx >= 0) { - idx0 = best_idx; - used[(uint8_t)best_idx] = true; - } - } - - if (tracks[1].active && count > 0) { - int16_t best = 0x7FFF; - int8_t best_idx = -1; - for (uint8_t i = 0; i < count; i++) { - if (used[i]) { - continue; - } - int16_t d = abs16((int16_t)(clusters[i].center_q4 - tracks[1].pos_q4)); - if (d < best) { - best = d; - best_idx = (int8_t)i; - } - } - if (best_idx >= 0) { - idx1 = best_idx; - used[(uint8_t)best_idx] = true; - } - } - - for (uint8_t i = 0; i < count; i++) { - if (used[i]) { - continue; - } - if (idx0 < 0) { - idx0 = (int8_t)i; - used[i] = true; - continue; - } - if (idx1 < 0) { - idx1 = (int8_t)i; - used[i] = true; - continue; - } - break; - } - - update_track(&tracks[0], idx0 >= 0, (idx0 >= 0) ? &clusters[(uint8_t)idx0] : NULL); - update_track(&tracks[1], idx1 >= 0, (idx1 >= 0) ? &clusters[(uint8_t)idx1] : NULL); - - apply_latch(classify_gesture(), count > 0); - debug_print_clusters(clusters, count); - - out->gesture = stable_gesture; - out->left_x = SLIDE_NEUTRAL_AXIS; - out->right_x = SLIDE_NEUTRAL_AXIS; - out->cluster_count = count; - - switch (stable_gesture) { - case SLIDE_GESTURE_SINGLE_LEFT: - out->left_x = axis_from_dir(-1); - break; - case SLIDE_GESTURE_SINGLE_RIGHT: - out->left_x = axis_from_dir(1); - break; - case SLIDE_GESTURE_DUAL_LEFT: - out->left_x = axis_from_dir(-1); - out->right_x = axis_from_dir(-1); - break; - case SLIDE_GESTURE_DUAL_RIGHT: - out->left_x = axis_from_dir(1); - out->right_x = axis_from_dir(1); - break; - case SLIDE_GESTURE_DUAL_CONVERGE: - out->left_x = axis_from_dir(1); - out->right_x = axis_from_dir(-1); - break; - case SLIDE_GESTURE_DUAL_DIVERGE: - out->left_x = axis_from_dir(-1); - out->right_x = axis_from_dir(1); - break; - default: - break; - } -} diff --git a/firmware/src/slide.h b/firmware/src/slide.h deleted file mode 100644 index 19ae90e..0000000 --- a/firmware/src/slide.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Slider gesture extraction for PS4 HID mapping - */ - -#ifndef SLIDE_H -#define SLIDE_H - -#include -#include - -typedef enum { - SLIDE_GESTURE_NONE = 0, - SLIDE_GESTURE_SINGLE_LEFT, - SLIDE_GESTURE_SINGLE_RIGHT, - SLIDE_GESTURE_DUAL_LEFT, - SLIDE_GESTURE_DUAL_RIGHT, - SLIDE_GESTURE_DUAL_CONVERGE, - SLIDE_GESTURE_DUAL_DIVERGE, -} slide_gesture_t; - -typedef struct { - slide_gesture_t gesture; - uint8_t left_x; - uint8_t right_x; - uint8_t cluster_count; -} slide_result_t; - -void slide_reset(void); -void slide_set_latch(uint16_t frames); -void slide_set_debug_cluster(bool enable); -void slide_process(uint32_t mask, slide_result_t *out); - -#endif