Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions AGENTS.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ each build, not an API diff.

## Unreleased

- **Choose how the Back button works.** `Settings > Controls` now offers a
device-wide choice between holding CENTER and double-clicking it to go back.
Hold stays the default and behaves exactly as before, including the instant
OK on a short click. Picking Double-click trades a short delay on OK (the
device has to wait and see whether a second click is coming) for a Back
gesture that doesn't require holding a button down.
- **Apps can take CENTER over as a game button.** An app may now claim the
hold and/or double-click gesture as its own input, in which case it becomes
responsible for its own way out — useful for games where CENTER is a
rapid-fire action button and a stray double-tap shouldn't drop you out to
the launcher. Apps that don't claim anything keep receiving a single "back"
event and never have to care which gesture the user picked.

- **App development split out into its own repository.** The `jppd-app-sdk`
Docker toolchain (`tools/app-sdk/`) and the two App SDK test apps
(`apps/testapp_native`, `apps/testapp_mp`) moved to
Expand Down
27 changes: 27 additions & 0 deletions components/jpp_core/include/jpp_keypad_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ extern "C" {
#define JPP_KEYPAD_DEFAULT_POLL_INTERVAL_MS 100
#define JPP_KEYPAD_DEFAULT_REPEAT_DELAY_MS 500
#define JPP_KEYPAD_DEFAULT_REPEAT_INTERVAL_MS 500
#define JPP_KEYPAD_DEFAULT_DOUBLE_CLICK_MS 300

/* Which physical CENTER gesture the user has chosen to mean "Back".
This is a *policy* type: the detector below never reads it. It is owned by
the settings layer and resolved into a Back action by keypad_task in
main/app_main.c, which is the only place that knows both the user
preference and what the foreground app has claimed. */
typedef enum {
JPP_KEYPAD_BACK_GESTURE_HOLD = 0,
JPP_KEYPAD_BACK_GESTURE_DOUBLE_CLICK,
} jpp_keypad_back_gesture_t;

typedef enum {
JPP_KEYPAD_KIND_NO_EVENT = 0,
Expand All @@ -22,6 +33,7 @@ typedef enum {
JPP_KEYPAD_KIND_REPEAT,
JPP_KEYPAD_KIND_CENTER_SHORT,
JPP_KEYPAD_KIND_CENTER_LONG,
JPP_KEYPAD_KIND_CENTER_DOUBLE,
} jpp_keypad_event_kind_t;

typedef struct {
Expand All @@ -41,6 +53,13 @@ typedef struct {
bool repeat_enabled;
int repeat_delay_ms;
int repeat_interval_ms;
/* When set, a short CENTER click is withheld for double_click_ms so a
second click can be reported as CENTER_DOUBLE instead. When clear,
CENTER_SHORT is emitted the moment the button is released and
CENTER_DOUBLE never fires. This is the only latency knob: pay for
double-click discrimination exactly when something needs it. */
bool detect_double_click;
int double_click_ms;
const jpp_keypad_band_t *bands;
size_t band_count;
} jpp_keypad_config_t;
Expand Down Expand Up @@ -70,6 +89,14 @@ typedef struct {
int last_repeat_ms;
bool center_long_emitted;
size_t sample_index;
/* A deferred short click waiting to see if a second click follows within
double_click_ms. Survives across the idle gap between two separate
press/release cycles, unlike press_started_ms above which
jpp_keypad_reset_hold_state() clears on every release. Flushed
immediately if detect_double_click is cleared while one is in flight,
so a mode change can never strand or replay a click. */
bool short_pending;
int pending_release_ms;
} jpp_keypad_state_t;

void jpp_keypad_state_init(jpp_keypad_state_t *state, const jpp_keypad_config_t *config);
Expand Down
46 changes: 46 additions & 0 deletions components/jpp_core/include/jpp_sdk_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,24 @@ typedef enum {
JPP_SDK_KEY_RIGHT,
JPP_SDK_KEY_CENTER,
JPP_SDK_KEY_CENTER_LONG,
/* Raw CENTER gestures, delivered only to apps that claimed them via
jpp_sdk_claim_center(). Appended deliberately: native apps are
separately-built ELFs, so these values must never be renumbered. */
JPP_SDK_KEY_CENTER_HOLD,
JPP_SDK_KEY_CENTER_DOUBLE,
/* Preferred spelling of the semantic "user wants to go back" event. Same
value as JPP_SDK_KEY_CENTER_LONG, which predates the Settings > Controls
preference and is kept so existing apps and binaries are unaffected. */
JPP_SDK_KEY_BACK = JPP_SDK_KEY_CENTER_LONG,
} jpp_sdk_key_event_t;

/* Bits for jpp_sdk_claim_center(). Claiming a gesture takes it over as your
own input; claiming anything at all means JPP_SDK_KEY_BACK is no longer
delivered and your app owns its exit. */
#define JPP_SDK_CENTER_CLAIM_NONE 0x00u
#define JPP_SDK_CENTER_CLAIM_HOLD 0x01u
#define JPP_SDK_CENTER_CLAIM_DOUBLE 0x02u

/* ---- High-level UI abstractions ----------------------------------------- */
/*
* Dialog, List, and Input are blocking, modal helpers built on the frame and
Expand Down Expand Up @@ -529,6 +545,11 @@ typedef struct {
char pending_cap_strs[JPP_SDK_PENDING_CAP_MAX][32];
int pending_cap_tiers[JPP_SDK_PENDING_CAP_MAX];
size_t pending_cap_count;
/* CENTER gestures this app has taken over; see jpp_sdk_claim_center().
New fields go here, at the tail: app binaries are built separately and
loaded from SD, so inserting above this point shifts every offset they
were compiled against. */
uint8_t center_claim;
} jpp_sdk_context_t;

void jpp_sdk_context_init(jpp_sdk_context_t *context);
Expand Down Expand Up @@ -973,6 +994,31 @@ jpp_sdk_status_t jpp_sdk_wait_key(jpp_sdk_context_t *context, uint32_t timeout_m
/* Called by the main loop to push a key event into the active app's queue. */
void jpp_sdk_push_key(jpp_sdk_context_t *context, jpp_sdk_key_event_t event);

/*
* Ungated — take over CENTER gestures as your own input.
*
* `mask` is a bitwise OR of JPP_SDK_CENTER_CLAIM_HOLD / _DOUBLE, or
* JPP_SDK_CENTER_CLAIM_NONE (the default on every bind) to leave CENTER
* alone. The rule is:
*
* claim nothing → you get JPP_SDK_KEY_CENTER and JPP_SDK_KEY_BACK.
* The firmware decides which physical gesture means Back
* from the user's Settings > Controls preference; your
* code never sees that choice.
* claim anything → the claimed gestures arrive as JPP_SDK_KEY_CENTER_HOLD
* / _DOUBLE, JPP_SDK_KEY_BACK is no longer delivered, and
* your app is responsible for its own way out.
*
* Claiming only HOLD also keeps JPP_SDK_KEY_CENTER instant: nothing then
* needs to tell a double-click apart, so the short click is never withheld
* to wait for a second one. That is the combination to use for an app where
* CENTER is a rapid action button ("fire") and hold opens a pause menu.
*
* Never affects UP/DOWN/LEFT/RIGHT, and never affects the launcher or
* Settings — the claim lives on your context and is dropped when you exit.
*/
jpp_sdk_status_t jpp_sdk_claim_center(jpp_sdk_context_t *context, uint8_t mask);

/* ---- High-level UI helpers (no capability required) ---------------------- */

/*
Expand Down
3 changes: 3 additions & 0 deletions components/jpp_core/include/jpp_ui_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ jpp_ui_status_t jpp_ui_display_render_lines(
jpp_ui_frame_t *rendered_frame,
bool *changed
);
/* Maps a keypad event to a UI action. CENTER hold/double-click gestures are
NOT handled here — they are policy (see the note in the implementation) and
are resolved by keypad_task() before this is called. */
jpp_ui_action_t jpp_ui_normalize_action(const jpp_keypad_event_t *event);
const char *jpp_ui_action_name(jpp_ui_action_t action);
const char *jpp_ui_status_name(jpp_ui_status_t status);
Expand Down
106 changes: 98 additions & 8 deletions components/jpp_core/src/jpp_keypad_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,60 @@ static int jpp_keypad_config_repeat_interval_ms(const jpp_keypad_config_t *confi
return config->repeat_interval_ms;
}

static bool jpp_keypad_config_detect_double_click(const jpp_keypad_config_t *config)
{
if (config == NULL) {
return false;
}
return config->detect_double_click;
}

static int jpp_keypad_config_double_click_ms(const jpp_keypad_config_t *config)
{
if (config == NULL || config->double_click_ms < 1) {
return JPP_KEYPAD_DEFAULT_DOUBLE_CLICK_MS;
}
return config->double_click_ms;
}

/* Releases a deferred short click once double_click_ms has passed with no
second click. Called once per poll, before any other event logic, so a
click that finally times out resolves before a later click in the same
poll can start a fresh pending cycle.

Also flushes immediately when double-click detection has been switched off
underneath a click that is still in flight — the mode can change between
polls (the user toggling Settings > Controls, or an app being foregrounded
that claims CENTER differently), and neither dropping the click nor
replaying it later would be correct. */
static int jpp_keypad_check_pending_short(
jpp_keypad_state_t *state,
const jpp_keypad_config_t *config,
int now_ms,
jpp_keypad_event_t *events,
size_t event_capacity,
size_t *event_count
)
{
if (state == NULL || !state->short_pending) {
return 0;
}
if (jpp_keypad_config_detect_double_click(config) &&
now_ms - state->pending_release_ms < jpp_keypad_config_double_click_ms(config)) {
return 0;
}
if (*event_count >= event_capacity) {
return -1;
}
state->short_pending = false;
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_SHORT;
events[*event_count].key = "CENTER";
events[*event_count].mapped = "OK";
events[*event_count].duration_ms = 0;
*event_count += 1u;
return 0;
}

static const jpp_keypad_band_t *jpp_keypad_band_by_key(const jpp_keypad_config_t *config, const char *key)
{
size_t band_count = 0u;
Expand Down Expand Up @@ -206,24 +260,50 @@ static int jpp_keypad_finalize_release(
duration_ms = 0;
}
if (strcmp(previous_key, "CENTER") == 0) {
if (duration_ms >= jpp_keypad_config_long_press_ms(config) && !state->center_long_emitted) {
if (duration_ms >= jpp_keypad_config_long_press_ms(config)) {
/* A hold. jpp_keypad_poll() normally emits CENTER_LONG live the
moment the threshold is crossed; finalize only has work to do
when the release landed in the same poll as the crossing. */
if (!state->center_long_emitted) {
if (*event_count >= event_capacity) {
return -1;
}
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_LONG;
events[*event_count].key = previous_key;
events[*event_count].mapped = "HOLD";
events[*event_count].duration_ms = duration_ms;
*event_count += 1u;
}
return 0;
}
/* A short click. Report it straight away unless someone needs to be
able to tell a double-click apart, in which case hold it back until
the window closes (jpp_keypad_check_pending_short). */
if (!jpp_keypad_config_detect_double_click(config)) {
if (*event_count >= event_capacity) {
return -1;
}
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_LONG;
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_SHORT;
events[*event_count].key = previous_key;
events[*event_count].mapped = "BACK";
events[*event_count].mapped = "OK";
events[*event_count].duration_ms = duration_ms;
*event_count += 1u;
} else if (duration_ms < jpp_keypad_config_long_press_ms(config)) {
return 0;
}
if (state->short_pending &&
now_ms - state->pending_release_ms <= jpp_keypad_config_double_click_ms(config)) {
if (*event_count >= event_capacity) {
return -1;
}
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_SHORT;
state->short_pending = false;
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_DOUBLE;
events[*event_count].key = previous_key;
events[*event_count].mapped = "OK";
events[*event_count].mapped = "DOUBLE";
events[*event_count].duration_ms = duration_ms;
*event_count += 1u;
} else {
state->short_pending = true;
state->pending_release_ms = now_ms;
}
return 0;
}
Expand Down Expand Up @@ -265,6 +345,8 @@ void jpp_keypad_state_init(jpp_keypad_state_t *state, const jpp_keypad_config_t
state->repeat_interval_ms = config == NULL ? 150 : config->repeat_interval_ms;
state->press_started_ms = -1;
state->last_repeat_ms = -1;
state->short_pending = false;
state->pending_release_ms = -1;
state->ready = state->enabled;
}

Expand All @@ -283,6 +365,8 @@ const char *jpp_keypad_event_kind_name(jpp_keypad_event_kind_t kind)
return "CENTER_SHORT";
case JPP_KEYPAD_KIND_CENTER_LONG:
return "CENTER_LONG";
case JPP_KEYPAD_KIND_CENTER_DOUBLE:
return "CENTER_DOUBLE";
}
return "UNKNOWN";
}
Expand Down Expand Up @@ -311,6 +395,9 @@ int jpp_keypad_poll(
return 0;
}
now_ms = (int)state->sample_index * jpp_keypad_config_poll_interval_ms(config);
if (jpp_keypad_check_pending_short(state, config, now_ms, events, event_capacity, event_count) != 0) {
return -1;
}
if (!sample_present) {
if (state->stable_key != NULL) {
previous_key = state->stable_key;
Expand All @@ -334,6 +421,9 @@ int jpp_keypad_poll(
state->press_started_ms = now_ms;
}
if (strcmp(candidate_key, "CENTER") == 0) {
/* A hold is always detected, whatever it ends up meaning —
the policy layer decides whether CENTER_LONG is "Back",
an app's own gesture, or nothing at all. */
int duration_ms = now_ms - state->press_started_ms;
if (duration_ms >= jpp_keypad_config_long_press_ms(config) && !state->center_long_emitted) {
if (*event_count >= event_capacity) {
Expand All @@ -342,7 +432,7 @@ int jpp_keypad_poll(
state->center_long_emitted = true;
events[*event_count].kind = JPP_KEYPAD_KIND_CENTER_LONG;
events[*event_count].key = candidate_key;
events[*event_count].mapped = "BACK";
events[*event_count].mapped = "HOLD";
events[*event_count].duration_ms = duration_ms;
*event_count += 1u;
} else if (state->center_long_emitted &&
Expand All @@ -356,7 +446,7 @@ int jpp_keypad_poll(
state->last_repeat_ms = now_ms;
events[*event_count].kind = JPP_KEYPAD_KIND_REPEAT;
events[*event_count].key = candidate_key;
events[*event_count].mapped = "BACK";
events[*event_count].mapped = "HOLD";
events[*event_count].duration_ms = 0;
*event_count += 1u;
}
Expand Down
22 changes: 22 additions & 0 deletions components/jpp_core/src/jpp_mp_sdk_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,21 @@ STATIC mp_obj_t mp_sdk_wait_key(mp_obj_t timeout_obj)
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_sdk_wait_key_obj, mp_sdk_wait_key);

/* claim_center(mask) → None */
STATIC mp_obj_t mp_sdk_claim_center(mp_obj_t mask_obj)
{
mp_int_t mask = mp_obj_get_int(mask_obj);
if (mask < 0 || mask > 0xFF) {
raise_sdk_error(JPP_SDK_STATUS_INVALID_ARGUMENT, NULL);
}
jpp_sdk_status_t st = jpp_sdk_claim_center(get_ctx(), (uint8_t)mask);
if (st != JPP_SDK_STATUS_OK) {
raise_sdk_error(st, NULL);
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_sdk_claim_center_obj, mp_sdk_claim_center);

/* -------------------------------------------------------------------------- */
/* BLE — scan (requires: ble.scan) */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -1096,6 +1111,12 @@ STATIC const mp_rom_map_elem_t jppsdk_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_KEY_RIGHT), MP_ROM_INT(JPP_SDK_KEY_RIGHT) },
{ MP_ROM_QSTR(MP_QSTR_KEY_CENTER), MP_ROM_INT(JPP_SDK_KEY_CENTER) },
{ MP_ROM_QSTR(MP_QSTR_KEY_CENTER_LONG), MP_ROM_INT(JPP_SDK_KEY_CENTER_LONG) },
{ MP_ROM_QSTR(MP_QSTR_KEY_BACK), MP_ROM_INT(JPP_SDK_KEY_BACK) },
{ MP_ROM_QSTR(MP_QSTR_KEY_CENTER_HOLD), MP_ROM_INT(JPP_SDK_KEY_CENTER_HOLD) },
{ MP_ROM_QSTR(MP_QSTR_KEY_CENTER_DOUBLE), MP_ROM_INT(JPP_SDK_KEY_CENTER_DOUBLE) },
{ MP_ROM_QSTR(MP_QSTR_CENTER_CLAIM_NONE), MP_ROM_INT(JPP_SDK_CENTER_CLAIM_NONE) },
{ MP_ROM_QSTR(MP_QSTR_CENTER_CLAIM_HOLD), MP_ROM_INT(JPP_SDK_CENTER_CLAIM_HOLD) },
{ MP_ROM_QSTR(MP_QSTR_CENTER_CLAIM_DOUBLE), MP_ROM_INT(JPP_SDK_CENTER_CLAIM_DOUBLE) },

/* Core */
{ MP_ROM_QSTR(MP_QSTR_set_frame), MP_ROM_PTR(&mp_sdk_set_frame_obj) },
Expand Down Expand Up @@ -1127,6 +1148,7 @@ STATIC const mp_rom_map_elem_t jppsdk_module_globals_table[] = {
/* Input */
{ MP_ROM_QSTR(MP_QSTR_poll_key), MP_ROM_PTR(&mp_sdk_poll_key_obj) },
{ MP_ROM_QSTR(MP_QSTR_wait_key), MP_ROM_PTR(&mp_sdk_wait_key_obj) },
{ MP_ROM_QSTR(MP_QSTR_claim_center), MP_ROM_PTR(&mp_sdk_claim_center_obj) },

/* High-level UI */
{ MP_ROM_QSTR(MP_QSTR_dialog), MP_ROM_PTR(&mp_sdk_dialog_obj) },
Expand Down
13 changes: 13 additions & 0 deletions components/jpp_core/src/jpp_sdk_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ void jpp_sdk_context_init(jpp_sdk_context_t *context)
return;
}
memset(context, 0, sizeof(*context));
/* memset leaves center_claim == JPP_SDK_CENTER_CLAIM_NONE, so every app
starts out with the system-managed Back gesture. */
context->key_queue = xQueueCreate(8u, sizeof(jpp_sdk_key_event_t));
}

Expand Down Expand Up @@ -2549,6 +2551,17 @@ void jpp_sdk_push_key(jpp_sdk_context_t *context, jpp_sdk_key_event_t event)
xQueueSendToBack(context->key_queue, &event, 0);
}

jpp_sdk_status_t jpp_sdk_claim_center(jpp_sdk_context_t *context, uint8_t mask)
{
jpp_sdk_status_t status = jpp_sdk_ensure_bound(context);
if (status != JPP_SDK_STATUS_OK) { return status; }
if ((mask & ~(JPP_SDK_CENTER_CLAIM_HOLD | JPP_SDK_CENTER_CLAIM_DOUBLE)) != 0u) {
return JPP_SDK_STATUS_INVALID_ARGUMENT;
}
context->center_claim = mask;
return JPP_SDK_STATUS_OK;
}

/* -------------------------------------------------------------------------- */
/* High-level UI helpers: Dialog, List, Input */
/* */
Expand Down
Loading
Loading