From 91d608b0e2ebb2eb0ff1d05b5dcac0130540917d Mon Sep 17 00:00:00 2001 From: whowechina Date: Sun, 12 Apr 2026 16:09:02 +0800 Subject: [PATCH] Slider to joystick kinda works --- firmware/src/CMakeLists.txt | 2 +- firmware/src/main.c | 31 +- firmware/src/slide.c | 598 ++++++++++++++++++++++++++++++++++++ firmware/src/slide.h | 33 ++ 4 files changed, 658 insertions(+), 6 deletions(-) create mode 100644 firmware/src/slide.c create mode 100644 firmware/src/slide.h diff --git a/firmware/src/CMakeLists.txt b/firmware/src/CMakeLists.txt index 658997c..5114b79 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) + cli.c mpr121.c hebtn.c lzfx.c usb_descriptors.c ps4_feat.c slide.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/main.c b/firmware/src/main.c index 861aed9..7a78bf1 100644 --- a/firmware/src/main.c +++ b/firmware/src/main.c @@ -35,6 +35,7 @@ #include "hebtn.h" #include "lzfx.h" #include "ps4_feat.h" +#include "slide.h" struct __attribute__((packed)) { uint16_t buttons; // 16 buttons; see JoystickButtons_t for bit mapping @@ -97,7 +98,7 @@ const static uint8_t maps[4][7] = { { 3, 0, 1, 2, 12, 8, 9 }, { 0, 3, 2, 1, 12, 8, 9 }, // Steam { 3, 0, 1, 2, 9, 12, 8 }, // Arcade - { 0, 1, 2, 3, 12, 8, 9 }, // PS4 + { 3, 0, 1, 2, 12, 9, 5 }, // PS4 }; static uint16_t mapped_buttons; @@ -124,28 +125,45 @@ static uint32_t touch_axis_bits() axis |= bits; } } - return axis ^ 0x80808080; + return axis; +} + +static uint32_t touch_mask_raw(void) +{ + uint32_t mask = 0; + int zone_num = slider_zone_num(); + + for (int i = 0; i < zone_num; i++) { + if (slider_touched(i)) { + mask |= (1u << i); + } + } + + return mask; } static void gen_ns_report() { - hid_ns.axis = touch_axis_bits(); + hid_ns.axis = touch_axis_bits() ^ 0x80808080; hid_ns.buttons = mapped_buttons; } static void gen_ps4_report() { uint16_t ps4_buttons = mapped_buttons; + slide_result_t slide_result; - hid_ps4.left_x = 0x80; hid_ps4.left_y = 0x80; - hid_ps4.right_x = 0x80; hid_ps4.right_y = 0x80; hid_ps4.hat_buttons = (ps4_buttons << 4); hid_ps4.buttons_11_4 = ps4_buttons >> 4; hid_ps4.buttons_counter = ps4_buttons >> 12; 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; } static void gen_hid_report() @@ -308,6 +326,9 @@ 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); + board_init(); tusb_init(); stdio_init_all(); diff --git a/firmware/src/slide.c b/firmware/src/slide.c new file mode 100644 index 0000000..4c9e628 --- /dev/null +++ b/firmware/src/slide.c @@ -0,0 +1,598 @@ +#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 new file mode 100644 index 0000000..19ae90e --- /dev/null +++ b/firmware/src/slide.h @@ -0,0 +1,33 @@ +/* + * 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