From 13da591b01e03ffb4fbc83e382b7608b02d9ed4d Mon Sep 17 00:00:00 2001 From: gfhdhytghd <3391146750@qq.com> Date: Sun, 30 Aug 2026 22:01:45 -0400 Subject: [PATCH 1/3] feat(nspire): add CX II Ndless host --- .gitattributes | 1 + .gitignore | 2 + hosts/iphone2g/pocket_runtime.c | 61 ++++++- hosts/iphone2g/pocket_runtime.h | 2 + hosts/nspire/Makefile | 87 +++++++++ hosts/nspire/README.md | 85 +++++++++ hosts/nspire/compat.c | 36 ++++ hosts/nspire/demo.pocket.json | 28 +++ hosts/nspire/framebuffer.c | 14 ++ hosts/nspire/framebuffer.h | 14 ++ hosts/nspire/input.c | 42 +++++ hosts/nspire/input.h | 11 ++ hosts/nspire/main.c | 93 ++++++++++ hosts/nspire/targets/armv5te-nspire-eabi.json | 23 +++ hosts/nspire/tests/framebuffer_test.c | 22 +++ package.json | 1 + tests/nspire-profile.test.ts | 56 ++++++ tools/nspire-profile.ts | 58 ++++++ tools/nspire.ts | 168 ++++++++++++++++++ tools/nspire/quickjs-cx2.patch | 47 +++++ 20 files changed, 845 insertions(+), 6 deletions(-) create mode 100644 hosts/nspire/Makefile create mode 100644 hosts/nspire/README.md create mode 100644 hosts/nspire/compat.c create mode 100644 hosts/nspire/demo.pocket.json create mode 100644 hosts/nspire/framebuffer.c create mode 100644 hosts/nspire/framebuffer.h create mode 100644 hosts/nspire/input.c create mode 100644 hosts/nspire/input.h create mode 100644 hosts/nspire/main.c create mode 100644 hosts/nspire/targets/armv5te-nspire-eabi.json create mode 100644 hosts/nspire/tests/framebuffer_test.c create mode 100644 tests/nspire-profile.test.ts create mode 100644 tools/nspire-profile.ts create mode 100644 tools/nspire.ts create mode 100644 tools/nspire/quickjs-cx2.patch diff --git a/.gitattributes b/.gitattributes index 4d6f8a366..d54d7f7ef 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ tools/symbian/container/patches/*.patch whitespace=-trailing-space,-space-before-tab +tools/nspire/*.patch whitespace=-trailing-space,-space-before-tab diff --git a/.gitignore b/.gitignore index 77ab823a0..83ad88098 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ framework/src/styles.generated.ts apps/launcher/covers/ # built wasm backend (regenerated by `bun tools/wasm.ts`) hosts/web/pocketjs.wasm +# Ndless host objects, embedded guest source, link map, and executable. +hosts/nspire/build/ # generated per-demo XMB metadata (tools/psp.ts, from apps//psp/Psp.toml) hosts/psp/Psp.toml # Obsolete checkout-local SDK location; bootstrap uses the shared cache. diff --git a/hosts/iphone2g/pocket_runtime.c b/hosts/iphone2g/pocket_runtime.c index 44990522f..41ac40587 100644 --- a/hosts/iphone2g/pocket_runtime.c +++ b/hosts/iphone2g/pocket_runtime.c @@ -27,6 +27,10 @@ extern void pocket_host_boot_stage(int stage); #define REPORT_BOOT_STAGE(stage) ((void)(stage)) #endif +#ifndef POCKET_RUNTIME_JS_STACK_SIZE +#define POCKET_RUNTIME_JS_STACK_SIZE (256 * 1024) +#endif + typedef enum { HostCreateNode, HostDestroyNode, @@ -94,6 +98,34 @@ static void take_exception(JSContext *exception_context) { } else { set_error("QuickJS exception"); } +#if defined(POCKET_RUNTIME_INCLUDE_EXCEPTION_STACK) + JSValue stack = JS_GetPropertyStr(exception_context, exception, "stack"); + if (!JS_IsException(stack) && !JS_IsUndefined(stack) && !JS_IsNull(stack)) { + size_t stack_length = 0; + const char *stack_text = JS_ToCStringLen2( + exception_context, + &stack_length, + stack, + 0 + ); + if (stack_text != 0 && stack_length > 0) { + size_t used = strlen(last_error); + size_t available = sizeof(last_error) - used - 1; + if (available > 0) { + last_error[used++] = '\n'; + available -= 1; + size_t copy_length = stack_length < available ? stack_length : available; + memcpy(last_error + used, stack_text, copy_length); + last_error[used + copy_length] = '\0'; + } + JS_FreeCString(exception_context, stack_text); + } + } else if (JS_IsException(stack)) { + JSValue stack_error = JS_GetException(exception_context); + JS_FreeValue(exception_context, stack_error); + } + JS_FreeValue(exception_context, stack); +#endif JS_FreeValue(exception_context, exception); } @@ -561,7 +593,7 @@ int pocket_runtime_boot( return 0; } REPORT_BOOT_STAGE(4); - JS_SetMaxStackSize(runtime, 256 * 1024); + JS_SetMaxStackSize(runtime, POCKET_RUNTIME_JS_STACK_SIZE); context = JS_NewContext(runtime); if (context == 0) { set_error("QuickJS context allocation failed"); @@ -632,6 +664,7 @@ int pocket_runtime_boot( /* One guest turn (frame call + job drain), then `tick_count` core ticks. */ static int run_frame( uint32_t buttons, + uint32_t analog, const PocketRuntimeContact *contacts, unsigned int contact_count, unsigned int tick_count @@ -680,7 +713,7 @@ static int run_frame( } JSValue arguments[4] = { JS_NewUint32(context, buttons), - JS_NewInt32(context, POCKET_ANALOG_CENTER), + JS_NewUint32(context, analog), touch_array, hit_array, }; @@ -728,12 +761,22 @@ int pocket_runtime_tick(const PocketRuntimeInput *input) { input->touch_y, input->touch_hit ); - return run_frame(input->buttons, &contact, count, 1); + return run_frame(input->buttons, POCKET_ANALOG_CENTER, &contact, count, 1); +} + +int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog) { + return run_frame(buttons, analog, 0, 0, 1); } int pocket_runtime_tick_contacts(const PocketRuntimeContactsInput *input) { if (input == 0) return 0; - return run_frame(input->buttons, input->contacts, input->contact_count, 1); + return run_frame( + input->buttons, + POCKET_ANALOG_CENTER, + input->contacts, + input->contact_count, + 1 + ); } int pocket_runtime_frame_contacts( @@ -741,7 +784,13 @@ int pocket_runtime_frame_contacts( unsigned int tick_count ) { if (input == 0) return 0; - return run_frame(input->buttons, input->contacts, input->contact_count, tick_count); + return run_frame( + input->buttons, + POCKET_ANALOG_CENTER, + input->contacts, + input->contact_count, + tick_count + ); } int pocket_runtime_frame_ticks( @@ -753,7 +802,7 @@ int pocket_runtime_frame_ticks( ) { PocketRuntimeContact contact; unsigned int count = single_contact(&contact, touch_down, touch_x, touch_y, touch_hit); - return run_frame(0, &contact, count, tick_count); + return run_frame(0, POCKET_ANALOG_CENTER, &contact, count, tick_count); } int pocket_runtime_frame(int touch_down, int touch_x, int touch_y, int touch_hit) { diff --git a/hosts/iphone2g/pocket_runtime.h b/hosts/iphone2g/pocket_runtime.h index ca6bad89a..c082e3721 100644 --- a/hosts/iphone2g/pocket_runtime.h +++ b/hosts/iphone2g/pocket_runtime.h @@ -28,6 +28,8 @@ typedef struct { int touch_hit; } PocketRuntimeInput; int pocket_runtime_tick(const PocketRuntimeInput *input); +/* Button + analog-only entry for hosts with a stick/trackpad and no touch. */ +int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog); /* * Multi-contact frame entry. `id` is the host's contact slot (0-255, stable diff --git a/hosts/nspire/Makefile b/hosts/nspire/Makefile new file mode 100644 index 000000000..532c4d776 --- /dev/null +++ b/hosts/nspire/Makefile @@ -0,0 +1,87 @@ +REPO_ROOT := ../.. +BUILD_DIR := build +DIST_DIR ?= $(REPO_ROOT)/dist/nspire +TARGET_JSON := $(CURDIR)/targets/armv5te-nspire-eabi.json +CORE_MANIFEST := $(REPO_ROOT)/engine/symbian/Cargo.toml +CORE_ARCHIVE := $(REPO_ROOT)/engine/symbian/target/armv5te-nspire-eabi/release/libpocketjs_symbian_core.a +APP_DATA := $(BUILD_DIR)/app_data.c + +GCC := nspire-gcc +LD := nspire-ld +GENZEHN := genzehn +MAKE_PRG := make-prg + +ifeq ($(QUICKJS_DIR),) +ifneq ($(MAKECMDGOALS),clean) +$(error QUICKJS_DIR is required and must contain libquickjs-sys/embed/quickjs) +endif +endif +QJS_SRC := $(QUICKJS_DIR)/libquickjs-sys/embed/quickjs +QJS_NAMES := quickjs cutils libregexp libunicode dtoa +QJS_OBJS := $(addprefix $(BUILD_DIR)/qjs/,$(addsuffix .o,$(QJS_NAMES))) +HOST_OBJS := $(BUILD_DIR)/main.o $(BUILD_DIR)/input.o $(BUILD_DIR)/framebuffer.o \ + $(BUILD_DIR)/compat.o \ + $(BUILD_DIR)/pocket_runtime.o $(BUILD_DIR)/app_data.o + +COMMON_CFLAGS := -std=gnu11 -Os -marm -ffunction-sections -fdata-sections \ + -fno-strict-aliasing -funsigned-char -Wall -Wextra \ + -I. -I../iphone2g -I$(QJS_SRC) \ + -DPOCKETJS_TARGET_ID='"nspire-cx2-dev"' -DPOCKETJS_HOST_ABI=9 \ + -DPOCKET_RASTER_DENSITY=1 -DPOCKET_RUNTIME_REPORT_BOOT_STAGE=1 \ + -DPOCKET_RUNTIME_INCLUDE_EXCEPTION_STACK=1 \ + -DPOCKET_RUNTIME_JS_STACK_SIZE=65536 \ + -DPOCKETJS_NO_MALLOC_USABLE_SIZE=1 +QJS_CFLAGS := $(COMMON_CFLAGS) -D_GNU_SOURCE \ + -DPOCKETJS_NO_ATOMICS=1 -DPOCKETJS_FIXED_TIMEZONE=1 \ + -DCONFIG_VERSION='"pocket-nspire-cx2"' -D__TM_GMTOFF=tm_gmtoff \ + -include malloc.h -Wno-sign-compare -Wno-unused-parameter +LDFLAGS := -Wl,--gc-sections -Wl,-Map,$(BUILD_DIR)/pocketjs-cx2.map + +.PHONY: all clean core check-app-data +all: $(DIST_DIR)/pocketjs-cx2.tns + +check-app-data: + @test -f $(APP_DATA) || (echo "missing $(APP_DATA); run bun nspire bundle"; exit 1) + +core: + cd $(REPO_ROOT)/engine/symbian && cargo build --release --locked \ + --no-default-features --features software-only,boot-stage \ + --target $(TARGET_JSON) -Z json-target-spec \ + -Z build-std=core,alloc,compiler_builtins \ + -Z build-std-features=compiler-builtins-mem + +$(BUILD_DIR) $(BUILD_DIR)/qjs $(DIST_DIR): + mkdir -p $@ + +$(BUILD_DIR)/main.o: main.c | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/input.o: input.c | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/framebuffer.o: framebuffer.c | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/compat.o: compat.c | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/pocket_runtime.o: ../iphone2g/pocket_runtime.c | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/app_data.o: check-app-data $(APP_DATA) | $(BUILD_DIR) + $(GCC) $(COMMON_CFLAGS) -c $(APP_DATA) -o $@ + +$(BUILD_DIR)/qjs/%.o: $(QJS_SRC)/%.c | $(BUILD_DIR)/qjs + $(GCC) $(QJS_CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/pocketjs-cx2.elf: core $(HOST_OBJS) $(QJS_OBJS) | $(BUILD_DIR) + $(LD) $(HOST_OBJS) $(QJS_OBJS) $(CORE_ARCHIVE) -o $@ $(LDFLAGS) -lm + +$(DIST_DIR)/pocketjs-cx2.tns: $(BUILD_DIR)/pocketjs-cx2.elf | $(DIST_DIR) + $(GENZEHN) --input $< --output $(BUILD_DIR)/pocketjs-cx2.zehn \ + --name "PocketJS CX II" --uses-lcd-blit true + $(MAKE_PRG) $(BUILD_DIR)/pocketjs-cx2.zehn $@ + +clean: + rm -rf $(BUILD_DIR) + rm -f $(DIST_DIR)/pocketjs-cx2.tns diff --git a/hosts/nspire/README.md b/hosts/nspire/README.md new file mode 100644 index 000000000..ee14aadff --- /dev/null +++ b/hosts/nspire/README.md @@ -0,0 +1,85 @@ +# TI-Nspire CX II / Ndless host + +This development host runs one PocketJS guest on a **TI-Nspire CX II only**. +It embeds the compiled JavaScript and pak into one Ndless `.tns` program, +executes the guest with QuickJS, rasterizes the ordinary PocketJS DrawList in +software, converts its top-left BGRA output to RGB565, and presents it through +Ndless `lcd_blit`. + +The development target is `nspire-cx2-dev`, with a fixed 320x240 logical and +physical viewport at raster density 1. It advertises only: + +- `input.buttons` +- `input.analog.left` (the CX II touchpad sampled as two axes) +- `input.cursor` (the framework's synthesized cursor) +- `text.glyphs.baked` + +There is deliberately no audio, touchscreen, network, filesystem, or runtime +font capability. A manifest requiring one of those is rejected before its +guest bundle is produced. The touchpad is controller input, not +`input.touch`. + +## Toolchain + +Install the current Ndless SDK and put `nspire-gcc`, `nspire-ld`, `genzehn`, +and `make-prg` on `PATH`. The host targets the SDK's ARM926EJ-S ABI: +ARMv5TE, ARM mode, EABI, soft float. Rust uses the pinned nightly already used +by PocketJS's standalone `no_std` core and builds `core`, `alloc`, and +`compiler_builtins` for `targets/armv5te-nspire-eabi.json`. + +Check the local setup and fetch PocketJS's pinned QuickJS revision: + +```sh +bun install --frozen-lockfile +bun nspire doctor +bun nspire bootstrap +``` + +`bootstrap` writes its checkout under ignored `dist/nspire/` and applies the +small `tools/nspire/quickjs-cx2.patch` portability patch (Newlib's `int32_t` +typedef differs from the PSP SDK's). It does not use an arbitrary system QuickJS. To use an existing checkout instead, set +`POCKETJS_QUICKJS_DIR` to its `libquickjs-sys/embed/quickjs` directory. + +## Build + +The included demo reuses the Hero application at the CX II viewport: + +```sh +bun nspire bundle +bun nspire build +# dist/nspire/pocketjs-cx2.tns +``` + +Build another app with a CX II-compatible manifest: + +```sh +bun nspire build --manifest=path/to/pocket.json +``` + +Copy `pocketjs-cx2.tns` to the calculator and open it with Ndless. Hold +**Ctrl+Esc** to leave the runtime. Default controls are: + +| CX II key | Pocket button | +| --- | --- | +| arrows | D-pad | +| Ctrl / Enter / touchpad click | confirm (Circle) | +| Esc | back (Cross) | +| Shift | Square | +| Tab | Triangle | +| Menu | Start | +| Var | Select | +| touchpad position | analog axis / synthesized cursor | + +## Acceptance boundary + +The host remains outside PocketJS's production `POCKET_TARGETS` registry until +a CX II run proves all of the following: + +- Ndless loads the generated `.tns` and QuickJS evaluates the embedded guest. +- The first Hero frame has correct RGB565 channel order and orientation. +- D-pad focus, confirm/back edges, touchpad cursor, and Ctrl+Esc work. +- A sustained animation does not exhaust memory and input remains responsive. +- Exiting restores the OS framebuffer mode. + +Host-side tests cover the target contract and BGRA-to-RGB565 conversion, but +they are not substitutes for these calculator checks. diff --git a/hosts/nspire/compat.c b/hosts/nspire/compat.c new file mode 100644 index 000000000..862f6110f --- /dev/null +++ b/hosts/nspire/compat.c @@ -0,0 +1,36 @@ +/* Bare-metal compatibility for the single-threaded Ndless process. */ + +#include + +uint8_t __atomic_load_1(const volatile void *pointer, int order) { + (void)order; + return *(const volatile uint8_t *)pointer; +} + +uint16_t __atomic_load_2(const volatile void *pointer, int order) { + (void)order; + return *(const volatile uint16_t *)pointer; +} + +unsigned int __atomic_load_4(const volatile void *pointer, int order) { + (void)order; + return *(const volatile unsigned int *)pointer; +} + +void __atomic_store_1(volatile void *pointer, uint8_t value, int order) { + (void)order; + *(volatile uint8_t *)pointer = value; +} + +void __atomic_store_2(volatile void *pointer, uint16_t value, int order) { + (void)order; + *(volatile uint16_t *)pointer = value; +} + +void __atomic_store_4(volatile void *pointer, unsigned int value, int order) { + (void)order; + *(volatile unsigned int *)pointer = value; +} + +/* Arch's generic ARM Newlib references this hook; Ndless owns teardown. */ +void _fini(void) {} diff --git a/hosts/nspire/demo.pocket.json b/hosts/nspire/demo.pocket.json new file mode 100644 index 000000000..d49ff04b3 --- /dev/null +++ b/hosts/nspire/demo.pocket.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://pocketjs.dev/schema/pocket-2.json", + "pocket": 2, + "id": "dev.pocket-stack.nspire-demo", + "name": "pocketjs-nspire-demo", + "title": "PocketJS CX II Demo", + "version": "0.1.0", + "engine": { + "capabilities": { + "requires": [ + "text.glyphs.baked", + "input.buttons", + "input.analog.left" + ] + } + }, + "app": { + "entry": "apps/hero/main.tsx", + "output": "nspire-demo", + "framework": "solid", + "viewport": { + "fixed": { + "logical": [320, 240], + "presentation": "native" + } + } + } +} diff --git a/hosts/nspire/framebuffer.c b/hosts/nspire/framebuffer.c new file mode 100644 index 000000000..6f61f40de --- /dev/null +++ b/hosts/nspire/framebuffer.c @@ -0,0 +1,14 @@ +#include "framebuffer.h" + +void nspire_bgra_to_rgb565(uint16_t *out, const uint8_t *bgra, size_t pixels) { + size_t index; + if (out == 0 || bgra == 0) return; + for (index = 0; index < pixels; ++index) { + const uint8_t blue = bgra[index * 4u]; + const uint8_t green = bgra[index * 4u + 1u]; + const uint8_t red = bgra[index * 4u + 2u]; + out[index] = (uint16_t)(((uint16_t)(red & 0xf8u) << 8u) | + ((uint16_t)(green & 0xfcu) << 3u) | + ((uint16_t)blue >> 3u)); + } +} diff --git a/hosts/nspire/framebuffer.h b/hosts/nspire/framebuffer.h new file mode 100644 index 000000000..2d67fde09 --- /dev/null +++ b/hosts/nspire/framebuffer.h @@ -0,0 +1,14 @@ +#ifndef POCKETJS_NSPIRE_FRAMEBUFFER_H +#define POCKETJS_NSPIRE_FRAMEBUFFER_H + +#include +#include + +#define NSPIRE_SCREEN_WIDTH 320u +#define NSPIRE_SCREEN_HEIGHT 240u +#define NSPIRE_SCREEN_PIXELS (NSPIRE_SCREEN_WIDTH * NSPIRE_SCREEN_HEIGHT) + +/* PocketJS software frames are top-left BGRA bytes. Ndless consumes RGB565. */ +void nspire_bgra_to_rgb565(uint16_t *out, const uint8_t *bgra, size_t pixels); + +#endif diff --git a/hosts/nspire/input.c b/hosts/nspire/input.c new file mode 100644 index 000000000..b12d9690b --- /dev/null +++ b/hosts/nspire/input.c @@ -0,0 +1,42 @@ +#include "input.h" + +#include + +#include "pocket_spec.h" + +static uint8_t clamp_axis(uint16_t value, uint16_t extent) { + if (extent <= 1u) return 128u; + if (value >= extent) value = (uint16_t)(extent - 1u); + return (uint8_t)(((uint32_t)value * 255u) / (uint32_t)(extent - 1u)); +} + +uint32_t nspire_input_buttons(void) { + uint32_t buttons = 0; + if (isKeyPressed(KEY_NSPIRE_UP)) buttons |= POCKET_BTN_UP; + if (isKeyPressed(KEY_NSPIRE_RIGHT)) buttons |= POCKET_BTN_RIGHT; + if (isKeyPressed(KEY_NSPIRE_DOWN)) buttons |= POCKET_BTN_DOWN; + if (isKeyPressed(KEY_NSPIRE_LEFT)) buttons |= POCKET_BTN_LEFT; + if (isKeyPressed(KEY_NSPIRE_CTRL) || isKeyPressed(KEY_NSPIRE_ENTER) || + isKeyPressed(KEY_NSPIRE_CLICK)) buttons |= POCKET_BTN_CIRCLE; + if (isKeyPressed(KEY_NSPIRE_ESC)) buttons |= POCKET_BTN_CROSS; + if (isKeyPressed(KEY_NSPIRE_SHIFT)) buttons |= POCKET_BTN_SQUARE; + if (isKeyPressed(KEY_NSPIRE_TAB)) buttons |= POCKET_BTN_TRIANGLE; + if (isKeyPressed(KEY_NSPIRE_MENU)) buttons |= POCKET_BTN_START; + if (isKeyPressed(KEY_NSPIRE_VAR)) buttons |= POCKET_BTN_SELECT; + return buttons; +} + +uint32_t nspire_input_analog(void) { + touchpad_report_t report; + touchpad_info_t *info = touchpad_getinfo(); + if (info == 0 || touchpad_scan(&report) != 0 || !report.proximity) { + return POCKET_ANALOG_CENTER; + } + /* Ndless touchpad Y grows upward; Pocket's analog Y grows downward. */ + return ((uint32_t)clamp_axis(report.x, info->width) << 8u) | + (uint32_t)(255u - clamp_axis(report.y, info->height)); +} + +bool nspire_input_exit_requested(void) { + return isKeyPressed(KEY_NSPIRE_CTRL) && isKeyPressed(KEY_NSPIRE_ESC); +} diff --git a/hosts/nspire/input.h b/hosts/nspire/input.h new file mode 100644 index 000000000..f4f3693d2 --- /dev/null +++ b/hosts/nspire/input.h @@ -0,0 +1,11 @@ +#ifndef POCKETJS_NSPIRE_INPUT_H +#define POCKETJS_NSPIRE_INPUT_H + +#include +#include + +uint32_t nspire_input_buttons(void); +uint32_t nspire_input_analog(void); +bool nspire_input_exit_requested(void); + +#endif diff --git a/hosts/nspire/main.c b/hosts/nspire/main.c new file mode 100644 index 000000000..ca98546e1 --- /dev/null +++ b/hosts/nspire/main.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +#include "framebuffer.h" +#include "input.h" +#include "pocket_runtime.h" + +extern const unsigned char pocket_app_js[]; +extern const unsigned int pocket_app_js_len; +extern const unsigned char pocket_app_pak[]; +extern const unsigned int pocket_app_pak_len; + +void pocket_host_boot_stage(int stage) { + char path[256]; + FILE *file; + const char *documents = get_documents_dir(); + if (documents == 0 || + snprintf(path, sizeof(path), "%spocketjs-boot-stage.txt.tns", documents) >= + (int)sizeof(path)) return; + file = fopen(path, "wb"); + if (file == 0) return; + fprintf(file, "schema=1\nstage=%d\n", stage); + fclose(file); +} + +int main(void) { + uint16_t *screen; + unsigned frame = 0; + + pocket_host_boot_stage(0); + if (!is_cx2) { + show_msgbox("PocketJS", "This build only supports TI-Nspire CX II."); + return 1; + } + screen = (uint16_t *)malloc(NSPIRE_SCREEN_PIXELS * sizeof(uint16_t)); + if (screen == 0) { + show_msgbox("PocketJS", "RGB565 framebuffer allocation failed."); + return 1; + } + if (!pocket_runtime_boot( + (const char *)pocket_app_js, + (size_t)pocket_app_js_len, + pocket_app_pak, + (size_t)pocket_app_pak_len, + (int)NSPIRE_SCREEN_WIDTH, + (int)NSPIRE_SCREEN_HEIGHT)) { + show_msgbox("PocketJS boot error", pocket_runtime_error()); + free(screen); + return 1; + } + + pocket_host_boot_stage(20); + lcd_init(SCR_320x240_565); + pocket_host_boot_stage(21); + while (!nspire_input_exit_requested()) { + const uint32_t buttons = nspire_input_buttons(); + const uint32_t analog = nspire_input_analog(); + if (frame == 0) pocket_host_boot_stage(30); + if (!pocket_runtime_tick_analog(buttons, analog)) { + lcd_init(SCR_TYPE_INVALID); + show_msgbox("PocketJS runtime error", pocket_runtime_error()); + pocket_runtime_shutdown(); + free(screen); + return 1; + } + if (frame == 0) pocket_host_boot_stage(31); + /* Tick at 60 Hz, but avoid a full LCD transfer more than 30 times/s. */ + if ((frame++ & 1u) == 0u) { + const uint8_t *pixels = pocket_runtime_render(); + if (frame == 1) pocket_host_boot_stage(32); + if (pixels == 0 || pocket_runtime_width() != NSPIRE_SCREEN_WIDTH || + pocket_runtime_height() != NSPIRE_SCREEN_HEIGHT) { + lcd_init(SCR_TYPE_INVALID); + show_msgbox("PocketJS", "The software renderer returned a bad frame."); + pocket_runtime_shutdown(); + free(screen); + return 1; + } + nspire_bgra_to_rgb565(screen, pixels, NSPIRE_SCREEN_PIXELS); + if (frame == 1) pocket_host_boot_stage(33); + lcd_blit(screen, SCR_320x240_565); + if (frame == 1) pocket_host_boot_stage(34); + } + msleep(16); + } + + lcd_init(SCR_TYPE_INVALID); + pocket_runtime_shutdown(); + free(screen); + return 0; +} diff --git a/hosts/nspire/targets/armv5te-nspire-eabi.json b/hosts/nspire/targets/armv5te-nspire-eabi.json new file mode 100644 index 000000000..b328faabe --- /dev/null +++ b/hosts/nspire/targets/armv5te-nspire-eabi.json @@ -0,0 +1,23 @@ +{ + "abi": "eabi", + "arch": "arm", + "c-enum-min-bits": 8, + "crt-objects-fallback": "false", + "cpu": "arm926ej-s", + "data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64", + "emit-debug-gdb-scripts": false, + "features": "+v5te,+soft-float,+strict-align", + "frame-pointer": "always", + "has-thumb-interworking": true, + "linker": "rust-lld", + "linker-flavor": "gnu-lld", + "llvm-floatabi": "soft", + "llvm-target": "armv5te-none-eabi", + "max-atomic-width": 32, + "os": "none", + "panic-strategy": "abort", + "relocation-model": "static", + "target-endian": "little", + "target-pointer-width": 32, + "vendor": "unknown" +} diff --git a/hosts/nspire/tests/framebuffer_test.c b/hosts/nspire/tests/framebuffer_test.c new file mode 100644 index 000000000..2a1cad2c1 --- /dev/null +++ b/hosts/nspire/tests/framebuffer_test.c @@ -0,0 +1,22 @@ +#include +#include + +#include "../framebuffer.h" + +int main(void) { + const uint8_t pixels[] = { + 0, 0, 0, 255, + 255, 255, 255, 255, + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + }; + uint16_t out[5] = {0}; + nspire_bgra_to_rgb565(out, pixels, 5); + assert(out[0] == 0x0000); + assert(out[1] == 0xffff); + assert(out[2] == 0xf800); + assert(out[3] == 0x07e0); + assert(out[4] == 0x001f); + return 0; +} diff --git a/package.json b/package.json index b95cef37e..a2b266de9 100644 --- a/package.json +++ b/package.json @@ -218,6 +218,7 @@ "blackberry-android": "bun tools/blackberry-android.ts", "blackberry-qnx": "bun tools/blackberry-qnx.ts", "3ds": "bun tools/3ds.ts", + "nspire": "bun tools/nspire.ts", "vita:art": "bun tools/generate-vita-livearea.ts", "vita:art:check": "bun tools/generate-vita-livearea.ts --check", "e2e:vita": "bun tests/e2e/vita3k.ts", diff --git a/tests/nspire-profile.test.ts b/tests/nspire-profile.test.ts new file mode 100644 index 000000000..b4bd65d5e --- /dev/null +++ b/tests/nspire-profile.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test"; +import { POCKET_TARGETS } from "../contracts/spec/platforms.ts"; +import { + NSPIRE_CX2_DEV_HOST_ABI, + NSPIRE_CX2_DEV_CONTRACTS, + NSPIRE_CX2_DEV_TARGET_ID, + NSPIRE_CX2_VIEWPORT, + resolveNspireCx2BuildPlan, +} from "../tools/nspire-profile.ts"; + +function manifest(requires: string[] = ["input.buttons", "text.glyphs.baked"]) { + return { + $schema: "https://pocketjs.dev/schema/pocket-2.json", + pocket: 2, + id: "dev.pocket-stack.nspire-test", + name: "nspire-test", + title: "Nspire Test", + version: "0.1.0", + engine: { capabilities: { requires } }, + app: { + entry: "apps/hero/main.tsx", + output: "nspire-test", + framework: "solid", + viewport: { fixed: { logical: [320, 240], presentation: "native" } }, + }, + }; +} + +describe("TI-Nspire CX II development profile", () => { + test("is not promoted before hardware acceptance", () => { + expect(POCKET_TARGETS).not.toHaveProperty(NSPIRE_CX2_DEV_TARGET_ID); + }); + + test("resolves the fixed RGB565 takeover surface", () => { + const plan = resolveNspireCx2BuildPlan(manifest()); + expect(plan.target).toMatchObject({ + id: NSPIRE_CX2_DEV_TARGET_ID, + hostAbi: NSPIRE_CX2_DEV_HOST_ABI, + }); + expect(NSPIRE_CX2_DEV_CONTRACTS.targets[NSPIRE_CX2_DEV_TARGET_ID]).toMatchObject({ + platform: "nspire-cx2", + form: "takeover", + }); + expect(plan.viewport.logical).toEqual(NSPIRE_CX2_VIEWPORT); + expect(plan.viewport.rasterDensity).toBe(1); + }); + + test.each(["audio.pcm", "input.touch", "net.http"])( + "rejects unsupported capability %s", + (capability) => { + expect(() => resolveNspireCx2BuildPlan(manifest([capability]))).toThrow( + "manifest did not resolve", + ); + }, + ); +}); diff --git a/tools/nspire-profile.ts b/tools/nspire-profile.ts new file mode 100644 index 000000000..b7e363ff4 --- /dev/null +++ b/tools/nspire-profile.ts @@ -0,0 +1,58 @@ +import { + POCKET_CAPABILITIES, + definePlatformContractRegistry, + defineTargetRegistry, +} from "../contracts/spec/platforms.ts"; +import type { ResolvedBuildPlan } from "../framework/src/manifest/plan.ts"; +import { validateAndResolveBuildPlan } from "../framework/src/manifest/resolve.ts"; + +/** + * TI-Nspire CX II development profile. + * + * It stays outside POCKET_TARGETS until the Ndless binary has passed a real + * calculator boot/render/input run. The CX II owns one 320x240 RGB565 screen. + * Its touchpad is sampled as a two-axis controller; PocketJS can therefore + * synthesize its hardware-neutral cursor without exposing Ndless APIs to apps. + */ +export const NSPIRE_CX2_DEV_TARGET_ID = "nspire-cx2-dev"; +export const NSPIRE_CX2_DEV_HOST_ABI = 9; +export const NSPIRE_CX2_VIEWPORT = [320, 240] as const; + +export const NSPIRE_CX2_DEV_CONTRACTS = definePlatformContractRegistry( + POCKET_CAPABILITIES, + defineTargetRegistry({ + [NSPIRE_CX2_DEV_TARGET_ID]: { + hostAbi: NSPIRE_CX2_DEV_HOST_ABI, + platform: "nspire-cx2", + form: "takeover", + display: { + physicalViewport: NSPIRE_CX2_VIEWPORT, + logicalViewports: [NSPIRE_CX2_VIEWPORT], + presentations: ["native"], + rasterDensity: 1, + }, + capabilities: [ + "input.analog.left", + "input.buttons", + "input.cursor", + "text.glyphs.baked", + ], + }, + }), +); + +export function resolveNspireCx2BuildPlan(input: unknown): ResolvedBuildPlan { + const resolution = validateAndResolveBuildPlan( + input, + { target: NSPIRE_CX2_DEV_TARGET_ID }, + NSPIRE_CX2_DEV_CONTRACTS, + ); + if (!resolution.ok) { + throw new Error( + `pocket nspire: manifest did not resolve: ${resolution.diagnostics + .map((diagnostic) => `${diagnostic.path || "/"}: ${diagnostic.message}`) + .join("; ")}`, + ); + } + return resolution.plan; +} diff --git a/tools/nspire.ts b/tools/nspire.ts new file mode 100644 index 000000000..715944633 --- /dev/null +++ b/tools/nspire.ts @@ -0,0 +1,168 @@ +import { + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { ResolvedBuildPlan } from "../framework/src/manifest/plan.ts"; +import { extractHostBuildInputs } from "../framework/src/manifest/host-build-inputs.ts"; +import { + NSPIRE_CX2_DEV_TARGET_ID, + resolveNspireCx2BuildPlan, +} from "./nspire-profile.ts"; + +const repository = fileURLToPath(new URL("..", import.meta.url)); +const hostDirectory = join(repository, "hosts/nspire"); +const outputDirectory = join(repository, "dist/nspire"); +const planPath = join(repository, ".pocket/nspire-cx2-dev/plan.json"); +const embeddedPath = join(hostDirectory, "build/app_data.c"); +const defaultManifest = join(repository, "hosts/nspire/demo.pocket.json"); +const quickJsRevision = "ba5bdd0dc013518768e76cd9e05cd30ed53dd35b"; +const quickJsCheckout = join(outputDirectory, "quickjs-rs"); +const quickJsPatch = join(repository, "tools/nspire/quickjs-cx2.patch"); +const command = Bun.argv[2] ?? "doctor"; + +function run(executable: string, args: readonly string[], cwd = repository): void { + const result = Bun.spawnSync({ + cmd: [executable, ...args], + cwd, + stdout: "inherit", + stderr: "inherit", + }); + if (result.exitCode !== 0) { + throw new Error(`${executable} ${args.join(" ")} failed (${result.exitCode})`); + } +} + +function which(name: string): string | undefined { + return Bun.which(name) ?? undefined; +} + +function doctor(): void { + const checks = [ + ["Bun", which("bun")], + ["Rustup", which("rustup")], + ["Ndless compiler", which("nspire-gcc")], + ["Ndless linker", which("nspire-ld")], + ["Ndless packer", which("genzehn")], + ["Ndless program wrapper", which("make-prg")], + ] as const; + for (const [label, path] of checks) { + console.log(`${path ? "[ok]" : "[missing]"} ${label}: ${path ?? "not on PATH"}`); + } + const qjs = quickJsSource(); + console.log(`${qjs ? "[ok]" : "[missing]"} pinned QuickJS: ${qjs ?? "run bun nspire bootstrap"}`); + if (checks.some(([, path]) => !path) || !qjs) process.exitCode = 1; +} + +function quickJsSource(): string | undefined { + const configured = process.env.POCKETJS_QUICKJS_DIR; + if (configured && existsSync(join(configured, "quickjs.c"))) { + return dirname(dirname(dirname(configured))); + } + const nested = join(quickJsCheckout, "libquickjs-sys/embed/quickjs/quickjs.c"); + return existsSync(nested) ? quickJsCheckout : undefined; +} + +function bootstrap(): void { + if (!existsSync(join(quickJsCheckout, ".git"))) { + mkdirSync(outputDirectory, { recursive: true }); + run("git", [ + "clone", + "--filter=blob:none", + "--no-checkout", + "https://github.com/pocket-stack/quickjs-rs.git", + quickJsCheckout, + ]); + } + run("git", ["-C", quickJsCheckout, "checkout", "--detach", quickJsRevision]); + const revision = Bun.spawnSync({ + cmd: ["git", "-C", quickJsCheckout, "rev-parse", "HEAD"], + stdout: "pipe", + }).stdout.toString().trim(); + if (revision !== quickJsRevision) throw new Error(`unexpected QuickJS revision ${revision}`); + const reverse = Bun.spawnSync({ + cmd: ["git", "-C", quickJsCheckout, "apply", "--reverse", "--check", quickJsPatch], + stdout: "ignore", + stderr: "ignore", + }); + if (reverse.exitCode !== 0) { + run("git", ["-C", quickJsCheckout, "apply", "--check", quickJsPatch]); + run("git", ["-C", quickJsCheckout, "apply", quickJsPatch]); + } + console.log(`PocketJS Nspire: pinned QuickJS ready at ${quickJsCheckout}`); +} + +function currentPlan(manifestPath: string): ResolvedBuildPlan { + return resolveNspireCx2BuildPlan(JSON.parse(readFileSync(manifestPath, "utf8"))); +} + +function cArray(name: string, bytes: Uint8Array): string { + const rows: string[] = []; + for (let offset = 0; offset < bytes.length; offset += 16) { + rows.push( + ` ${[...bytes.subarray(offset, offset + 16)] + .map((value) => `0x${value.toString(16).padStart(2, "0")}`) + .join(", ")},`, + ); + } + return [ + `const unsigned char ${name}[] = {`, + ...rows, + "};", + `const unsigned int ${name}_len = ${bytes.length}u;`, + "", + ].join("\n"); +} + +function bundle(manifestPath: string): void { + const plan = currentPlan(manifestPath); + const inputs = extractHostBuildInputs(plan, { expectedTarget: NSPIRE_CX2_DEV_TARGET_ID }); + mkdirSync(dirname(planPath), { recursive: true }); + mkdirSync(outputDirectory, { recursive: true }); + mkdirSync(dirname(embeddedPath), { recursive: true }); + writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`); + run(process.execPath, [ + join(repository, "tools/build.ts"), + `--plan=${planPath}`, + `--project-root=${repository}`, + `--outdir=${outputDirectory}`, + ]); + const jsPath = join(outputDirectory, `${inputs.appOutput}.js`); + const pakPath = join(outputDirectory, `${inputs.appOutput}.pak`); + if (!existsSync(jsPath) || !existsSync(pakPath)) { + throw new Error("PocketJS Nspire compiler did not emit JavaScript and pak artifacts"); + } + writeFileSync( + embeddedPath, + cArray("pocket_app_js", readFileSync(jsPath)) + + cArray("pocket_app_pak", readFileSync(pakPath)), + ); + console.log(`PocketJS Nspire: embedded guest -> ${embeddedPath}`); +} + +function build(manifestPath: string): void { + bundle(manifestPath); + const quickjs = quickJsSource(); + if (!quickjs) throw new Error("pinned QuickJS is absent; run bun nspire bootstrap"); + run("make", [`QUICKJS_DIR=${quickjs}`], hostDirectory); + console.log(`PocketJS Nspire: ${join(outputDirectory, "pocketjs-cx2.tns")}`); +} + +const manifestArg = Bun.argv.find((value) => value.startsWith("--manifest=")); +const manifestPath = resolve(manifestArg?.slice("--manifest=".length) ?? defaultManifest); +if (command === "doctor") doctor(); +else if (command === "bootstrap") bootstrap(); +else if (command === "bundle") bundle(manifestPath); +else if (command === "build") build(manifestPath); +else if (command === "clean") { + rmSync(join(hostDirectory, "build"), { recursive: true, force: true }); + rmSync(planPath, { force: true }); + console.log("PocketJS Nspire: generated host and plan files removed"); +} else { + console.error("usage: bun nspire [--manifest=path]"); + process.exit(1); +} diff --git a/tools/nspire/quickjs-cx2.patch b/tools/nspire/quickjs-cx2.patch new file mode 100644 index 000000000..42d552207 --- /dev/null +++ b/tools/nspire/quickjs-cx2.patch @@ -0,0 +1,47 @@ +diff --git a/libquickjs-sys/embed/quickjs/quickjs.c b/libquickjs-sys/embed/quickjs/quickjs.c +index 39e334c..afc8bec 100644 +--- a/libquickjs-sys/embed/quickjs/quickjs.c ++++ b/libquickjs-sys/embed/quickjs/quickjs.c +@@ -99,3 +99,4 @@ _Static_assert(_Alignof(JSValue) == 8, "Vita JSValue must be 8-byte aligned"); + #endif +-#if defined(__PSP__) || defined(__vita__) ++#if defined(__PSP__) || defined(__vita__) || \ ++ defined(POCKETJS_NO_ATOMICS) + #undef CONFIG_ATOMICS +@@ -2167,3 +2168,6 @@ static size_t js_def_malloc_usable_size(const void *ptr) + { +-#if defined(__APPLE__) ++#if defined(POCKETJS_NO_MALLOC_USABLE_SIZE) ++ (void)ptr; ++ return 0; ++#elif defined(__APPLE__) + return malloc_size(ptr); +@@ -7471,3 +7475,4 @@ static int find_line_num(JSContext *ctx, JSFunctionBytecode *b, + const uint8_t *p_end, *p; +- int new_line_num, line_num, pc, v, ret, new_col_num, col_num; ++ int new_line_num, line_num, pc, ret, new_col_num, col_num; ++ int32_t v; + uint32_t val; +@@ -44891,3 +44896,4 @@ static JSValue js_parseInt(JSContext *ctx, JSValueConst this_val, + const char *str, *p; +- int radix, flags; ++ int32_t radix; ++ int flags; + JSValue ret; +@@ -47304,3 +47310,4 @@ static int getTimezoneOffset(int64_t time) + #else +-#if defined(__PSP__) || defined(__vita__) ++#if defined(__PSP__) || defined(__vita__) || \ ++ defined(POCKETJS_FIXED_TIMEZONE) + (void)ti; +@@ -53698,3 +53705,3 @@ static __exception int remainingElementsCount_add(JSContext *ctx, + JSValue val; +- int remainingElementsCount; ++ int32_t remainingElementsCount; + +@@ -53729,3 +53736,4 @@ static JSValue js_promise_all_resolve_element(JSContext *ctx, + JSValue ret, obj; +- int is_zero, index; ++ int is_zero; ++ int32_t index; + From 39ce06853a3a691e608d55260b6140e4e4787c01 Mon Sep 17 00:00:00 2001 From: gfhdhytghd <3391146750@qq.com> Date: Sun, 30 Aug 2026 22:10:41 -0400 Subject: [PATCH 2/3] test(nspire): add CX II input diagnostics --- hosts/nspire/README.md | 15 +++ hosts/nspire/input-test/app.tsx | 137 ++++++++++++++++++++++++++++ hosts/nspire/input-test/main.tsx | 5 + hosts/nspire/input-test/pocket.json | 28 ++++++ tests/nspire-profile.test.ts | 20 ++++ 5 files changed, 205 insertions(+) create mode 100644 hosts/nspire/input-test/app.tsx create mode 100644 hosts/nspire/input-test/main.tsx create mode 100644 hosts/nspire/input-test/pocket.json diff --git a/hosts/nspire/README.md b/hosts/nspire/README.md index ee14aadff..ad85e2fca 100644 --- a/hosts/nspire/README.md +++ b/hosts/nspire/README.md @@ -56,6 +56,21 @@ Build another app with a CX II-compatible manifest: bun nspire build --manifest=path/to/pocket.json ``` +### Input acceptance guest + +The input guest shows the **PocketJS button mask**, press and release edge +counts, the **raw 0-255 analog axes**, and a touchpad-position marker: + +```sh +bun nspire build --manifest=hosts/nspire/input-test/pocket.json +# dist/nspire/pocketjs-cx2.tns +``` + +An active button turns green. Every complete tap increments both its `P` and +`R` counters. Moving across the touchpad moves the amber marker across the +outlined range; lifting the finger reports `X 128 Y 128` because the host +centers an absent analog sample. + Copy `pocketjs-cx2.tns` to the calculator and open it with Ndless. Hold **Ctrl+Esc** to leave the runtime. Default controls are: diff --git a/hosts/nspire/input-test/app.tsx b/hosts/nspire/input-test/app.tsx new file mode 100644 index 000000000..971ba1e90 --- /dev/null +++ b/hosts/nspire/input-test/app.tsx @@ -0,0 +1,137 @@ +import { createSignal, For } from "solid-js"; +import { Text, View } from "@pocketjs/framework/components"; +import { + analogRaw, + onFrame, +} from "@pocketjs/framework/lifecycle"; +import { BTN } from "@pocketjs/framework/input"; + +interface ButtonProbe { + label: string; + mask: number; +} + +const BUTTONS: readonly ButtonProbe[] = [ + { label: "UP", mask: BTN.UP }, + { label: "RIGHT", mask: BTN.RIGHT }, + { label: "DOWN", mask: BTN.DOWN }, + { label: "LEFT", mask: BTN.LEFT }, + { label: "CIRCLE", mask: BTN.CIRCLE }, + { label: "CROSS", mask: BTN.CROSS }, + { label: "SQUARE", mask: BTN.SQUARE }, + { label: "TRIANGLE", mask: BTN.TRIANGLE }, + { label: "START", mask: BTN.START }, + { label: "SELECT", mask: BTN.SELECT }, +] as const; + +interface ProbeState { + buttons: number; + presses: readonly number[]; + releases: readonly number[]; + rawX: number; + rawY: number; + frame: number; +} + +function hex16(value: number): string { + return ("0000" + value.toString(16).toUpperCase()).slice(-4); +} + +export default function InputTest() { + const [state, setState] = createSignal({ + buttons: 0, + presses: BUTTONS.map(() => 0), + releases: BUTTONS.map(() => 0), + rawX: 128, + rawY: 128, + frame: 0, + }); + let previousButtons = 0; + let frame = 0; + const presses = BUTTONS.map(() => 0); + const releases = BUTTONS.map(() => 0); + + onFrame((buttons) => { + frame += 1; + const pressed = buttons & ~previousButtons; + const released = previousButtons & ~buttons; + let edge = false; + BUTTONS.forEach((button, index) => { + if (pressed & button.mask) { + presses[index] += 1; + edge = true; + } + if (released & button.mask) { + releases[index] += 1; + edge = true; + } + }); + previousButtons = buttons; + if (!edge && (frame & 1) !== 0) return; + const analog = analogRaw(); + setState({ + buttons, + presses: [...presses], + releases: [...releases], + rawX: (analog >> 8) & 0xff, + rawY: analog & 0xff, + frame, + }); + }); + + const dotX = () => Math.round((state().rawX / 255) * 116); + const dotY = () => Math.round((state().rawY / 255) * 90); + + return ( + + + CX II INPUT TEST + FRAME {state().frame} + + + + + + BUTTON MASK 0x{hex16(state().buttons)} + + + + {(button, index) => { + const active = () => (state().buttons & button.mask) !== 0; + return ( + + {button.label} + + P{state().presses[index()]} R{state().releases[index()]} + + + ); + }} + + + + + + ANALOG RAW + + X {state().rawX} Y {state().rawY} + + + + + + + LIFT = 128 / 128 + EXIT = CTRL + ESC + + + + ); +} diff --git a/hosts/nspire/input-test/main.tsx b/hosts/nspire/input-test/main.tsx new file mode 100644 index 000000000..62ff53889 --- /dev/null +++ b/hosts/nspire/input-test/main.tsx @@ -0,0 +1,5 @@ +// @title PocketJS: CX II Input Test +import InputTest from "./app.tsx"; +import { mount } from "@pocketjs/framework/solid"; + +mount(() => ); diff --git a/hosts/nspire/input-test/pocket.json b/hosts/nspire/input-test/pocket.json new file mode 100644 index 000000000..a11e3984f --- /dev/null +++ b/hosts/nspire/input-test/pocket.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://pocketjs.dev/schema/pocket-2.json", + "pocket": 2, + "id": "dev.pocket-stack.nspire-input-test", + "name": "pocketjs-nspire-input-test", + "title": "PocketJS: CX II Input Test", + "version": "0.1.0", + "engine": { + "capabilities": { + "requires": [ + "text.glyphs.baked", + "input.buttons", + "input.analog.left" + ] + } + }, + "app": { + "entry": "hosts/nspire/input-test/main.tsx", + "output": "nspire-input-test", + "framework": "solid", + "viewport": { + "fixed": { + "logical": [320, 240], + "presentation": "native" + } + } + } +} diff --git a/tests/nspire-profile.test.ts b/tests/nspire-profile.test.ts index b4bd65d5e..f103e8114 100644 --- a/tests/nspire-profile.test.ts +++ b/tests/nspire-profile.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; import { POCKET_TARGETS } from "../contracts/spec/platforms.ts"; import { NSPIRE_CX2_DEV_HOST_ABI, @@ -45,6 +46,25 @@ describe("TI-Nspire CX II development profile", () => { expect(plan.viewport.rasterDensity).toBe(1); }); + test("resolves the button and analog acceptance guest", () => { + const inputTest = JSON.parse( + readFileSync( + new URL("../hosts/nspire/input-test/pocket.json", import.meta.url), + "utf8", + ), + ); + const plan = resolveNspireCx2BuildPlan(inputTest); + expect(plan.app).toMatchObject({ + entry: "hosts/nspire/input-test/main.tsx", + output: "nspire-input-test", + }); + expect(plan.features).toMatchObject({ + "input.analog.left": true, + "input.buttons": true, + }); + expect(plan.features).not.toHaveProperty("input.touch"); + }); + test.each(["audio.pcm", "input.touch", "net.http"])( "rejects unsupported capability %s", (capability) => { From 18211818e4f876960d79346d849c9701d5691df7 Mon Sep 17 00:00:00 2001 From: gfhdhytghd <3391146750@qq.com> Date: Sun, 30 Aug 2026 22:30:10 -0400 Subject: [PATCH 3/3] test(nspire): show all live keypad inputs --- hosts/iphone2g/pocket_runtime.c | 12 +++++ hosts/iphone2g/pocket_runtime.h | 2 + hosts/nspire/README.md | 14 +++--- hosts/nspire/input-test/app.tsx | 79 ++++++--------------------------- hosts/nspire/input.c | 57 ++++++++++++++++++++++++ hosts/nspire/input.h | 2 + hosts/nspire/main.c | 5 ++- 7 files changed, 99 insertions(+), 72 deletions(-) diff --git a/hosts/iphone2g/pocket_runtime.c b/hosts/iphone2g/pocket_runtime.c index 41ac40587..39a0d7889 100644 --- a/hosts/iphone2g/pocket_runtime.c +++ b/hosts/iphone2g/pocket_runtime.c @@ -768,6 +768,18 @@ int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog) { return run_frame(buttons, analog, 0, 0, 1); } +int pocket_runtime_set_diagnostic_text(const char *text) { + if (runtime == 0 || context == 0 || runtime_failed || JS_IsUndefined(global)) return 0; + JSValue value = JS_NewString(context, text == 0 ? "" : text); + if (JS_IsException(value) || + JS_SetPropertyStr(context, global, "__pocketInputDiagnostic", value) < 0) { + take_exception(context); + runtime_failed = 1; + return 0; + } + return 1; +} + int pocket_runtime_tick_contacts(const PocketRuntimeContactsInput *input) { if (input == 0) return 0; return run_frame( diff --git a/hosts/iphone2g/pocket_runtime.h b/hosts/iphone2g/pocket_runtime.h index c082e3721..daf1c3f26 100644 --- a/hosts/iphone2g/pocket_runtime.h +++ b/hosts/iphone2g/pocket_runtime.h @@ -30,6 +30,8 @@ typedef struct { int pocket_runtime_tick(const PocketRuntimeInput *input); /* Button + analog-only entry for hosts with a stick/trackpad and no touch. */ int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog); +/* Optional host-acceptance text, exposed outside the application input ABI. */ +int pocket_runtime_set_diagnostic_text(const char *text); /* * Multi-contact frame entry. `id` is the host's contact slot (0-255, stable diff --git a/hosts/nspire/README.md b/hosts/nspire/README.md index ad85e2fca..63d074a63 100644 --- a/hosts/nspire/README.md +++ b/hosts/nspire/README.md @@ -58,18 +58,20 @@ bun nspire build --manifest=path/to/pocket.json ### Input acceptance guest -The input guest shows the **PocketJS button mask**, press and release edge -counts, the **raw 0-255 analog axes**, and a touchpad-position marker: +The input guest shows the **currently held CX II matrix keys**, the PocketJS +button mask, the **raw 0-255 analog axes**, and a touchpad-position marker: ```sh bun nspire build --manifest=hosts/nspire/input-test/pocket.json # dist/nspire/pocketjs-cx2.tns ``` -An active button turns green. Every complete tap increments both its `P` and -`R` counters. Moving across the touchpad moves the amber marker across the -outlined range; lifting the finger reports `X 128 Y 128` because the host -centers an absent analog sample. +The left panel updates from a host-only diagnostic string and does not add +calculator keys to the PocketJS button ABI. It recognizes the numeric, +alphabetic, operator, navigation, modifier, document, menu, and touchpad-click +matrix entries exposed by Ndless. Moving across the touchpad moves the amber +marker across the outlined range; lifting the finger reports `X 128 Y 128` +because the host centers an absent analog sample. Copy `pocketjs-cx2.tns` to the calculator and open it with Ndless. Hold **Ctrl+Esc** to leave the runtime. Default controls are: diff --git a/hosts/nspire/input-test/app.tsx b/hosts/nspire/input-test/app.tsx index 971ba1e90..56e2949f2 100644 --- a/hosts/nspire/input-test/app.tsx +++ b/hosts/nspire/input-test/app.tsx @@ -1,33 +1,13 @@ -import { createSignal, For } from "solid-js"; +import { createSignal } from "solid-js"; import { Text, View } from "@pocketjs/framework/components"; import { analogRaw, onFrame, } from "@pocketjs/framework/lifecycle"; -import { BTN } from "@pocketjs/framework/input"; - -interface ButtonProbe { - label: string; - mask: number; -} - -const BUTTONS: readonly ButtonProbe[] = [ - { label: "UP", mask: BTN.UP }, - { label: "RIGHT", mask: BTN.RIGHT }, - { label: "DOWN", mask: BTN.DOWN }, - { label: "LEFT", mask: BTN.LEFT }, - { label: "CIRCLE", mask: BTN.CIRCLE }, - { label: "CROSS", mask: BTN.CROSS }, - { label: "SQUARE", mask: BTN.SQUARE }, - { label: "TRIANGLE", mask: BTN.TRIANGLE }, - { label: "START", mask: BTN.START }, - { label: "SELECT", mask: BTN.SELECT }, -] as const; interface ProbeState { buttons: number; - presses: readonly number[]; - releases: readonly number[]; + keys: string; rawX: number; rawY: number; frame: number; @@ -40,39 +20,23 @@ function hex16(value: number): string { export default function InputTest() { const [state, setState] = createSignal({ buttons: 0, - presses: BUTTONS.map(() => 0), - releases: BUTTONS.map(() => 0), + keys: "NONE", rawX: 128, rawY: 128, frame: 0, }); - let previousButtons = 0; let frame = 0; - const presses = BUTTONS.map(() => 0); - const releases = BUTTONS.map(() => 0); onFrame((buttons) => { frame += 1; - const pressed = buttons & ~previousButtons; - const released = previousButtons & ~buttons; - let edge = false; - BUTTONS.forEach((button, index) => { - if (pressed & button.mask) { - presses[index] += 1; - edge = true; - } - if (released & button.mask) { - releases[index] += 1; - edge = true; - } - }); - previousButtons = buttons; - if (!edge && (frame & 1) !== 0) return; + if ((frame & 1) !== 0) return; const analog = analogRaw(); + const diagnostic = ( + globalThis as typeof globalThis & { __pocketInputDiagnostic?: string } + ).__pocketInputDiagnostic; setState({ buttons, - presses: [...presses], - releases: [...releases], + keys: diagnostic && diagnostic.length > 0 ? diagnostic : "NONE", rawX: (analog >> 8) & 0xff, rawY: analog & 0xff, frame, @@ -91,28 +55,13 @@ export default function InputTest() { - - BUTTON MASK 0x{hex16(state().buttons)} - - - - {(button, index) => { - const active = () => (state().buttons & button.mask) !== 0; - return ( - - {button.label} - - P{state().presses[index()]} R{state().releases[index()]} - - - ); - }} - + CURRENT KEYS + + {state().keys} + POCKET MASK + 0x{hex16(state().buttons)} + ALL CX II MATRIX KEYS SCANNED diff --git a/hosts/nspire/input.c b/hosts/nspire/input.c index b12d9690b..16533b33f 100644 --- a/hosts/nspire/input.c +++ b/hosts/nspire/input.c @@ -1,6 +1,7 @@ #include "input.h" #include +#include #include "pocket_spec.h" @@ -10,6 +11,62 @@ static uint8_t clamp_axis(uint16_t value, uint16_t extent) { return (uint8_t)(((uint32_t)value * 255u) / (uint32_t)(extent - 1u)); } +typedef struct { + const char *name; + const t_key *key; +} NspireKeyProbe; + +#define KEY_PROBE(name) {#name, &KEY_NSPIRE_##name} + +static const NspireKeyProbe diagnostic_keys[] = { + KEY_PROBE(RET), KEY_PROBE(ENTER), KEY_PROBE(SPACE), KEY_PROBE(NEGATIVE), + KEY_PROBE(PERIOD), KEY_PROBE(0), KEY_PROBE(COMMA), KEY_PROBE(PLUS), + KEY_PROBE(1), KEY_PROBE(2), KEY_PROBE(3), KEY_PROBE(eEXP), KEY_PROBE(PI), + KEY_PROBE(MINUS), KEY_PROBE(4), KEY_PROBE(5), KEY_PROBE(6), + KEY_PROBE(TENX), KEY_PROBE(EE), KEY_PROBE(MULTIPLY), KEY_PROBE(7), + KEY_PROBE(8), KEY_PROBE(9), KEY_PROBE(SQU), KEY_PROBE(DIVIDE), + KEY_PROBE(TAN), KEY_PROBE(EXP), KEY_PROBE(APOSTROPHE), KEY_PROBE(CAT), + KEY_PROBE(RP), KEY_PROBE(LP), KEY_PROBE(VAR), KEY_PROBE(DEL), + KEY_PROBE(FLAG), KEY_PROBE(CLICK), KEY_PROBE(HOME), KEY_PROBE(MENU), + KEY_PROBE(ESC), KEY_PROBE(BAR), KEY_PROBE(TAB), KEY_PROBE(EQU), + KEY_PROBE(UP), KEY_PROBE(RIGHT), KEY_PROBE(DOWN), KEY_PROBE(LEFT), + KEY_PROBE(SHIFT), KEY_PROBE(CTRL), KEY_PROBE(DOC), KEY_PROBE(TRIG), + KEY_PROBE(SCRATCHPAD), + KEY_PROBE(A), KEY_PROBE(B), KEY_PROBE(C), KEY_PROBE(D), KEY_PROBE(E), + KEY_PROBE(F), KEY_PROBE(G), KEY_PROBE(H), KEY_PROBE(I), KEY_PROBE(J), + KEY_PROBE(K), KEY_PROBE(L), KEY_PROBE(M), KEY_PROBE(N), KEY_PROBE(O), + KEY_PROBE(P), KEY_PROBE(Q), KEY_PROBE(R), KEY_PROBE(S), KEY_PROBE(T), + KEY_PROBE(U), KEY_PROBE(V), KEY_PROBE(W), KEY_PROBE(X), KEY_PROBE(Y), + KEY_PROBE(Z), +}; + +#undef KEY_PROBE + +void nspire_input_diagnostic(char *output, size_t capacity) { + size_t index; + size_t used = 0; + if (output == 0 || capacity == 0) return; + output[0] = '\0'; + for (index = 0; index < sizeof(diagnostic_keys) / sizeof(diagnostic_keys[0]); ++index) { + const NspireKeyProbe *probe = &diagnostic_keys[index]; + int written; + if (!isKeyPressed(*probe->key)) continue; + written = snprintf( + output + used, + capacity - used, + "%s%s", + used == 0 ? "" : " + ", + probe->name + ); + if (written < 0) return; + if ((size_t)written >= capacity - used) { + output[capacity - 1] = '\0'; + return; + } + used += (size_t)written; + } +} + uint32_t nspire_input_buttons(void) { uint32_t buttons = 0; if (isKeyPressed(KEY_NSPIRE_UP)) buttons |= POCKET_BTN_UP; diff --git a/hosts/nspire/input.h b/hosts/nspire/input.h index f4f3693d2..0780c8605 100644 --- a/hosts/nspire/input.h +++ b/hosts/nspire/input.h @@ -2,10 +2,12 @@ #define POCKETJS_NSPIRE_INPUT_H #include +#include #include uint32_t nspire_input_buttons(void); uint32_t nspire_input_analog(void); +void nspire_input_diagnostic(char *output, size_t capacity); bool nspire_input_exit_requested(void); #endif diff --git a/hosts/nspire/main.c b/hosts/nspire/main.c index ca98546e1..e4183eadb 100644 --- a/hosts/nspire/main.c +++ b/hosts/nspire/main.c @@ -27,6 +27,7 @@ void pocket_host_boot_stage(int stage) { int main(void) { uint16_t *screen; + char diagnostic_keys[512]; unsigned frame = 0; pocket_host_boot_stage(0); @@ -57,8 +58,10 @@ int main(void) { while (!nspire_input_exit_requested()) { const uint32_t buttons = nspire_input_buttons(); const uint32_t analog = nspire_input_analog(); + nspire_input_diagnostic(diagnostic_keys, sizeof(diagnostic_keys)); if (frame == 0) pocket_host_boot_stage(30); - if (!pocket_runtime_tick_analog(buttons, analog)) { + if (!pocket_runtime_set_diagnostic_text(diagnostic_keys) || + !pocket_runtime_tick_analog(buttons, analog)) { lcd_init(SCR_TYPE_INVALID); show_msgbox("PocketJS runtime error", pocket_runtime_error()); pocket_runtime_shutdown();