mirror of
https://github.com/m5stack/CardputerZero-AppBuilder.git
synced 2026-05-20 11:51:57 -07:00
docs: desktop dev contract, app-builder.json schema, quickstart
DESKTOP_DEV.md freezes the app ABI (app_main + app_event + CZ_EV_*), the LVGL-shared-via-dlopen mechanism, and the 8-issue Windows plan so future work can't silently drift. APP_BUILDER_JSON.md documents the backward-compatible schema extension (runtime / entry / lvgl_version / caps / assets). QUICKSTART.md is the 3-minute onboarding for mac/Linux. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
eggfly
co-authored by
Claude Opus 4.7
parent
463a523d47
commit
372de634e3
@@ -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.
|
||||
@@ -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 <lvgl.h>
|
||||
|
||||
#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.
|
||||
@@ -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 <cz_app.h>
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user