diff --git a/docs/APP_BUILDER_JSON.md b/docs/APP_BUILDER_JSON.md new file mode 100644 index 0000000..532fe7e --- /dev/null +++ b/docs/APP_BUILDER_JSON.md @@ -0,0 +1,87 @@ +# `app-builder.json` schema + +Every directory in an application repository that should be discovered, built +and packaged contains one `app-builder.json` at its root. The file plays two +roles: + +1. **Packaging** — feeds the CI `.deb` pipeline (existing behaviour). +2. **Desktop dev loop** — tells `czdev` how to build and run the app inside the + emulator (new fields, optional). + +Fields added for the desktop loop are all optional; a file that only contains +the packaging fields keeps working exactly as before. + +## Full schema + +```jsonc +{ + // ── Packaging (existing) ───────────────────────────────────────── + "package_name": "hello_cz", // Debian package name (lowercase, dash) + "version": "0.1", // SemVer-ish; goes into control file + "app_name": "Hello CZ", // Display name in APPLaunch + "bin_name": "hello_cz", // Shared-object basename (no lib prefix) + "description": "Desktop dev hello app", + + // ── Desktop dev (new, all optional) ────────────────────────────── + "runtime": "lvgl-dlopen", // "lvgl-dlopen" (default) | "legacy-deb-only" + "entry": "app_main", // C symbol name, default "app_main" + "event_entry": "app_event", // optional, default "app_event" + "lvgl_version": "9.5", // host must match on major.minor + "caps": [ // capabilities the app declares it uses + "keyboard", + "audio", + "network" + ], + "assets": [ // paths relative to app dir; copied next + "assets/fonts/", // to the built library under its app dir + "assets/sprites/" + ] +} +``` + +## Field reference + +| Field | Required | Type | Default | Used by | +|---|---|---|---|---| +| `package_name` | yes | string | — | deb, czdev | +| `version` | no | string | `"0.1"` | deb | +| `app_name` | no | string | same as `package_name` | deb, czdev | +| `bin_name` | yes | string | — | deb, czdev | +| `description` | no | string | `""` | deb | +| `runtime` | no | `"lvgl-dlopen"` \| `"legacy-deb-only"` | `"lvgl-dlopen"` | czdev | +| `entry` | no | string | `"app_main"` | czdev, emulator | +| `event_entry` | no | string | `"app_event"` | czdev, emulator | +| `lvgl_version` | no | string | `"9.5"` | czdev (refuses mismatch) | +| `caps` | no | string[] | `[]` | czdev (future: sandboxing) | +| `assets` | no | string[] | `[]` | czdev (stage next to lib) | + +## Runtime modes + +- **`lvgl-dlopen`** — the app is a shared library that exports the `entry` and + optional `event_entry` symbols described in `cz_app.h`. `czdev run` loads it + into the emulator via `dlopen` / `LoadLibrary`. This is the default for new + apps. +- **`legacy-deb-only`** — the project is a standalone executable (Framebuffer, + SDL, Qt, Python, …). `czdev` skips it during `run`; the CI `.deb` pipeline + still produces a package. Use this for examples in + `CardputerZero-Examples/` that are not LVGL. + +Apps that omit `runtime` are treated as `lvgl-dlopen` iff they export +`app_main`; otherwise `czdev` prints a clear error and suggests setting +`"runtime": "legacy-deb-only"`. + +## Capabilities (`caps`) + +Reserved vocabulary (enforcement comes later): + +| Cap | Means | +|---|---| +| `keyboard` | Uses the 44-key physical keyboard / emulator skin | +| `audio` | Plays audio via ALSA on device, SDL_mixer in emulator | +| `network` | Reads hostname / IP / Wi-Fi state | +| `filesystem` | Writes into `/usr/share/APPLaunch` or the emu sandbox | +| `pty` | Spawns a PTY (terminal-style apps) | +| `process` | fork/exec of sub-processes | + +Today `caps` is metadata only; a future milestone uses it to decide which +HAL shims the emulator pre-loads. diff --git a/docs/DESKTOP_DEV.md b/docs/DESKTOP_DEV.md new file mode 100644 index 0000000..df74a41 --- /dev/null +++ b/docs/DESKTOP_DEV.md @@ -0,0 +1,178 @@ +# Desktop Development for CardputerZero Apps + +This document describes how the **desktop emulator** lets you develop 320×170 +LVGL apps for M5 CardputerZero without having the physical device. It is the +contract between: + +- App authors writing `.dylib` / `.so` / `.dll` modules +- The emulator (`cardputer-zero-emu`) that loads them +- The real device (APPLaunch on aarch64 Linux) that loads the same sources + compiled natively + +The goal: **one source tree → same `.dylib/.so/.dll` on desktop, same `.so` +inside the `.deb` on the device — zero per-platform `#ifdef` in app code.** + +--- + +## 1. How LVGL is shared between emulator and app (all platforms) + +The emulator hosts a single LVGL instance. Apps are **loaded into the same +process** via `dlopen` / `LoadLibrary` and call LVGL directly — they do **not** +link their own LVGL. + +On macOS and Linux this works because: + +- `emulator.exe` links LVGL with `-Wl,-force_load` (mac) / `--whole-archive` + (Linux), so every LVGL symbol lives in the emulator's global symbol table. +- The app `.dylib/.so` is linked with `-undefined dynamic_lookup` (mac) / + `-Wl,--unresolved-symbols=ignore-all` (Linux) — LVGL references are left + unresolved at link time. +- `dlopen(RTLD_GLOBAL)` at runtime binds the app's LVGL references to the + emulator's copy. + +Windows needs a different shape (see §4) but the **result is the same**: one +LVGL instance, one display, one input group, one timer handler. + +On the real device, APPLaunch does the same thing with `dlopen` against its own +LVGL — the app `.so` behaves identically. + +### Rules for app authors + +1. **Do not call `lv_init()` or `lv_display_create()`.** The host already did. +2. **Your `lv_conf.h` must byte-match the emulator's.** v9.5, color depth 16, + same font set, same feature flags. Mismatch = segfault. Use the provided + `sdk/include/lv_conf.h` verbatim. +3. **Entry point is `app_main(lv_obj_t* parent)`.** Hang your UI off `parent`. +4. **Clean up in `app_event(CZ_EV_EXIT_REQUEST)`.** Delete widgets you own. +5. **Do not export C++ symbols across the boundary.** Only `extern "C"` + functions. C++ statics inside the library are fine. + +--- + +## 2. App ABI contract (`cz_app.h`) + +Every app exports exactly two C symbols: + +```c +// sdk/include/cz_app.h +#pragma once +#include + +#if defined(_WIN32) + #define CZ_APP_EXPORT __declspec(dllexport) +#else + #define CZ_APP_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +CZ_APP_EXPORT void app_main(lv_obj_t *parent); +CZ_APP_EXPORT void app_event(int type, void *data); + +#ifdef __cplusplus +} +#endif + +enum { + CZ_EV_PAUSE = 1, // app is being backgrounded; persist state + CZ_EV_RESUME = 2, // app is foregrounded again + CZ_EV_EXIT_REQUEST = 3, // about to unload; free everything + CZ_EV_SIDE_KEY = 4, // data: (int*) side-button id (ESC/HOME/...) + // Future values are additive. Unknown types must be ignored by the app. +}; +``` + +**Why this shape:** + +- `app_main(parent)` — one UI entry point. `parent` is a full-screen container + already sized 320×170. The app never touches display / screen APIs. +- `app_event(int, void*)` — system notifications. Integer type + opaque data + keeps the ABI cheap to extend. Old apps that don't know a new type just + ignore it. +- No string-keyed events, no C++ types, no callbacks with ownership semantics. +- `lvgl_version` is pinned in `app-builder.json` — CI refuses to build if the + app's declared version and the host's version disagree on major/minor. + +Apps that only need UI provide `app_main` and leave `app_event` as a one-line +no-op. It is mandatory so the host can always call it. + +--- + +## 3. Why we use LVGL's API directly (no wrapper SDK) + +APPLaunch and UserDemo on the real device are written against LVGL v9 directly. +If the desktop emulator introduced its own wrapper, the two surfaces would +drift and every app would need double maintenance. LVGL v9's API is stable +within the 9.x line (we pin 9.5); we version-lock so that 9.6 adoption is an +explicit coordinated bump, not a silent breakage. + +We keep the host→app surface (§2) minimal precisely because LVGL already is +the SDK. + +--- + +## 4. Windows LVGL + emulator — known issues and plan + +Windows currently builds the emulator with `EMU_STATIC_APP=1` (the app is +static-linked into the exe). To reach the same "host + dlopen'd app" model as +mac/Linux we need to resolve these: + +| # | Issue | Root cause | Plan | +|---|---|---|---| +| 1 | PE requires all symbols resolved at link time | No equivalent of ELF `--unresolved-symbols=ignore-all` or Mach-O `-undefined dynamic_lookup` | Produce `lvgl.dll` + `liblvgl.dll.a` import lib; app links the import lib | +| 2 | LVGL global data not marked dllexport | v9 headers use `LV_ATTRIBUTE_EXTERN_DATA` inconsistently for fonts/styles/builtin tables | Apply a one-shot Windows export header that blankets `__declspec(dllexport/dllimport)` over the public API. Revisit on each LVGL upgrade. | +| 3 | MinGW `__attribute__((weak))` doesn't link | Known MinGW limitation | Already handled via macro in `emu_compat_win.h:9` | +| 4 | C++ runtime duplicated across DLLs | MinGW defaults to `-static-libstdc++`; EXE and app.dll each bring a copy | App ABI is `extern "C"` only (§2). LVGL is C. No C++ objects cross the boundary. | +| 5 | App can't reach emulator symbols via `RTLD_DEFAULT` | Windows hides symbols unless exported | Emulator uses `__declspec(dllexport)` whitelist for the few host-provided helpers (or `--export-all-symbols` during bring-up) | +| 6 | Risk of two `lvgl.dll` copies loaded | If app.dll and emulator.exe resolve different copies, LVGL state splits | CI packs both into one directory; `czdev run` uses `SetDllDirectory` to pin lookup | +| 7 | SDL2 / freetype DLL bundling | MinGW runtime deps | Existing `ldd | awk` logic in `emulator-build.yml:116` stays | +| 8 | Freetype disabled on Windows | Present workaround; CJK fallback differs from mac/Linux | Accept for now; re-enable in a dedicated follow-up task | + +The app source does **not** change per platform — all Windows-specific work +lives in the emulator + LVGL build. + +--- + +## 5. Explicit non-goals (this development cycle) + +These are good ideas but deliberately **not** in scope right now, to keep the +LVGL desktop-dev loop small and shippable: + +- **Python / Qt / Tkinter / raw-framebuffer examples.** They require syscall + interception (fb0 mmap, evdev, ALSA). Use the existing + `CardputerZero-Examples/scripts/dev-on-mac/` Docker path for those. +- **Rust `framebuffer` + `evdev` examples.** Same reason. +- **Subprocess + shared-memory runtime channel.** The dlopen model covers + 100% of LVGL apps. Subprocess/SHM is reserved for a later pass when we + want to cover fb/Qt/Python. +- **Hot `dlclose` / live reload.** `dlclose` is effectively a no-op on macOS + and dangerous across C++ statics. `czdev watch` will rebuild and restart + the emulator process instead. +- **Emscripten / WASM target for `czdev`.** The web playground continues to + build via the emulator's own CI; `czdev` stays desktop-only. +- **GUI IDE.** CLI first. Tauri shell is parked until the CLI loop is solid. + +--- + +## 6. Directory layout (target state) + +``` +CardputerZero-AppBuilder/ +├── emulator/ # git submodule → eggfly/M5CardputerZero-Emulator +├── crates/ +│ └── czdev/ # Rust CLI: list / build / run / watch / deploy / doctor +├── src-tauri/ # existing Tauri shell (unchanged) +├── sdk/ +│ ├── include/ +│ │ ├── cz_app.h # app ABI (§2) +│ │ └── lv_conf.h # pinned LVGL config (must match emulator's) +│ └── cmake/ +│ └── CZApp.cmake # cz_add_lvgl_app() helper +└── docs/ + └── DESKTOP_DEV.md # this file +``` + +App repos consume the SDK by pointing CMake at `sdk/cmake/CZApp.cmake`; they +do not vendor LVGL. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 0000000..22ba56c --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,142 @@ +# Quickstart — desktop dev for CardputerZero apps + +Get a 320×170 LVGL app running on your Mac or Linux machine in ~3 minutes — +no CardputerZero device required. + +## 1. Prerequisites + +Install the native toolchain. + +**macOS:** +```bash +brew install cmake pkg-config sdl2 sdl2_image sdl2_mixer freetype +``` + +**Linux (Debian/Ubuntu):** +```bash +sudo apt install -y build-essential cmake pkg-config \ + libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libfreetype-dev +``` + +**Windows:** MSYS2 MINGW64 shell. See +[DESKTOP_DEV.md §4](DESKTOP_DEV.md#4-windows-lvgl--emulator--known-issues-and-plan) +for the Windows-specific work still in progress — the mac/Linux loop below is +what's supported end-to-end today. + +You also need a recent Rust toolchain (for `czdev`). If you don't have one: +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +## 2. Clone with submodules + +```bash +git clone --recursive git@github.com:m5stack/CardputerZero-AppBuilder.git +cd CardputerZero-AppBuilder +``` + +If you already cloned without `--recursive`: +```bash +git submodule update --init --recursive +``` + +## 3. Verify the environment + +```bash +cargo run -p czdev --release -- doctor +``` + +You should see all required rows green. If anything is MISSING, the output +shows the exact install command for your OS. + +## 4. Run the hello app + +```bash +cargo run -p czdev --release -- run examples/hello_cz +``` + +On first run this will: + +1. Build the emulator (once, cached in `emulator/build/`). +2. Build `examples/hello_cz` into `examples/hello_cz/.czdev/build/`. +3. Stage the resulting `libhello_cz.dylib` (or `.so`) into the emulator's + `apps/` directory. +4. Launch the emulator with the app loaded via `dlopen`. + +You should see a 320×170 LCD inside a keyboard skin, showing +`Hello, CardputerZero!`. Close the emulator window to exit. + +## 5. Edit-run loop + +```bash +cargo run -p czdev --release -- watch examples/hello_cz +``` + +The watcher polls `src/`, `include/`, `assets/`, `CMakeLists.txt` and +`app-builder.json`. Any change triggers a rebuild and relaunches the emulator. + +## 6. Writing your own app + +Copy `examples/hello_cz/` and edit `src/hello_cz.c`. The ABI is documented in +`sdk/include/cz_app.h`: + +```c +#include + +void app_main(lv_obj_t *parent) { + lv_obj_t *label = lv_label_create(parent); + lv_label_set_text(label, "your UI here"); + lv_obj_center(label); +} + +void app_event(int type, void *data) { + (void)type; (void)data; +} +``` + +The `CMakeLists.txt` is three lines: + +```cmake +cmake_minimum_required(VERSION 3.16) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../sdk/cmake") +include(CZApp) +cz_add_lvgl_app(my_app SOURCES src/my_app.c) +``` + +And the manifest (see `docs/APP_BUILDER_JSON.md`): + +```json +{ + "package_name": "my_app", + "bin_name": "my_app", + "app_name": "My App", + "runtime": "lvgl-dlopen", + "lvgl_version": "9.5" +} +``` + +## 7. Shipping to a real device + +Building the aarch64 `.deb` stays on CI — trigger the existing +`build-deb.yml` workflow (see the repo README). Then push to the device: + +```bash +cargo run -p czdev --release -- deploy \ + --host pi@192.168.50.150 \ + --deb path/to/my_app_arm64.deb +``` + +## Troubleshooting + +- **`emulator submodule not checked out`** — you forgot `--recursive`. Fix: + `git submodule update --init --recursive`. +- **LVGL link errors about unresolved symbols** — expected in the app + library; they're resolved at `dlopen` time by the emulator. If the linker + *fails* instead of warns, see `DESKTOP_DEV.md` for the per-platform link + flags (`CZApp.cmake` handles these automatically). +- **`indev_read_cb is not registered` warnings in the log** — benign; the + emulator falls back to a default keypad indev when the app doesn't install + its own `lv_sdl_keyboard_create`. +- **macOS: `Library not loaded: @rpath/SDL2.framework/...`** — `brew install + sdl2` puts the library in a non-framework path; re-run `czdev doctor` and + install what it reports.