Slider to joystick kinda works

This commit is contained in:
whowechina
2026-04-12 16:09:02 +08:00
parent 675ed488cb
commit 91d608b0e2
4 changed files with 658 additions and 6 deletions
+1 -1
View File
@@ -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)
+26 -5
View File
@@ -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();
+598
View File
@@ -0,0 +1,598 @@
#include "slide.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#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;
}
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Slider gesture extraction for PS4 HID mapping
*/
#ifndef SLIDE_H
#define SLIDE_H
#include <stdint.h>
#include <stdbool.h>
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