docs(nanokvm-go): add custom app guide
Add bilingual Custom App installation and development documentation, including App Store and ZIP workflows,
AppContext lifecycle, deployment instructions, screenshots, and sidebar entries.
Co-authored-by: taorye <taorye@outlook.com>
BIN
docs/hardware/assets/NanoKVM/go/custom_app/apps-demo.mp4
Normal file
BIN
docs/hardware/assets/NanoKVM/go/custom_app/exit-app-cancel.mp4
Normal file
BIN
docs/hardware/assets/NanoKVM/go/custom_app/exit-app.mp4
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
555
docs/hardware/en/kvm/NanoKVM_Go/custom_app.md
Normal file
@@ -0,0 +1,555 @@
|
||||
---
|
||||
title: "Extension: Custom Apps"
|
||||
keywords: NanoKVM Go, Python App, appbase, framebuffer, touchscreen
|
||||
update:
|
||||
- date: 2026-08-04
|
||||
version: v1.1
|
||||
author: Liang Ziyue
|
||||
content:
|
||||
- Explained the origin of AppContext, the App lifecycle, and the main loop
|
||||
- Standardized the alignment of procedural screenshots
|
||||
- Fixed in-page navigation anchors
|
||||
- Corrected the manifest, lifecycle-script, and screen-recording examples
|
||||
- Synchronized ZIP installation screenshots and screenshot placeholders
|
||||
- date: 2026-07-30
|
||||
version: v1.0
|
||||
author: taonyx
|
||||
content:
|
||||
- Custom App development and deployment guide
|
||||
---
|
||||
|
||||
# Custom Apps
|
||||
|
||||
## Introduction
|
||||
|
||||
A custom App is a full-screen Python application that runs on the NanoKVM Go touchscreen. It uses the `appbase` SDK provided by the device to draw on the RGB565 framebuffer and receive touch events. You can use custom Apps to turn the NanoKVM Go display into a status panel, timer, market dashboard, or another interactive tool.
|
||||
|
||||
NanoKVM Go provides a complete workflow for managing and running Apps:
|
||||
|
||||
```text
|
||||
App Server or local ZIP -> install and manage in the web interface -> launch from the device Apps page
|
||||
```
|
||||
|
||||
Regular users can install and run existing Apps, while developers can write their own Python Apps, package them as ZIP files, and upload them to the device. This guide first explains how to install and use Apps, then uses a `Hello World` example to demonstrate development and deployment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, make sure that:
|
||||
|
||||
- the NanoKVM Go system and application are updated to the latest versions;
|
||||
- the NanoKVM Go web interface is accessible from the local network;
|
||||
- the touchscreen works correctly and the main interface includes an `Apps` page;
|
||||
- NanoKVM Go can access the relevant App repository when installing from an App Server;
|
||||
- a text editor, basic Python environment, and ZIP utility are available on the development computer when creating a custom App.
|
||||
|
||||
> If the web settings do not include an `Apps` option, or the device does not have an `Apps` page, check for and install the latest NanoKVM Go system and application updates.
|
||||
|
||||
## Install and Use Apps
|
||||
|
||||
### Run a Built-in App
|
||||
|
||||
Open the `Apps` page on the device touchscreen and select a built-in App. These examples demonstrate full-screen rendering, animation, and touch interaction:
|
||||
|
||||
<video src="./../../../assets/NanoKVM/go/custom_app/apps-demo.mp4" aria-label="NanoKVM Go built-in App demonstration" style="width: 100%; max-width: 568px;" playsinline controls autoplay loop muted preload="metadata"></video>
|
||||
|
||||
| App directory | Display name | Main feature |
|
||||
| --- | --- | --- |
|
||||
| `conways-game-of-life` | Conway | Frame-limited animation |
|
||||
| `crypto-candlestick` | Crypto | Market data and swipe navigation |
|
||||
| `nyan-cat` | Nyan Cat | Animated pixel sprite |
|
||||
| `pomodoro-timer` | Pomodoro | Buttons, taps, and a countdown timer |
|
||||
|
||||
Crypto uses a public market-data API and falls back to simulated data when the network is unavailable. It is intended only as an example of a network-enabled App.
|
||||
|
||||
### Install an App from the App Store
|
||||
|
||||
An App Server is a repository from which NanoKVM Go can browse and download Apps. The `Settings > Apps` page has two sections:
|
||||
|
||||
- `Installed`: manage installed Apps, including editing their configuration, downloading them, or removing them;
|
||||
- `Store`: browse the official repository or a repository added by the user, and install Apps directly.
|
||||
|
||||
Whether you use the official repository or another App Server, first open the NanoKVM Go web interface and click the settings icon in the top toolbar.
|
||||
|
||||

|
||||
|
||||
#### Select an App Server
|
||||
|
||||
The official Sipeed repository, [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps), is preconfigured on the device. No additional App Server configuration is required when using this repository.
|
||||
|
||||
To use another App repository, open `Settings > Apps`, select `Store`, and click `Servers`.
|
||||
|
||||

|
||||
|
||||
Click `Add server`, enter the server name and URL, and save the configuration. You can then switch to that repository in the App Store.
|
||||
|
||||

|
||||
|
||||
> Only add repositories that you trust. App installation scripts run with root privileges. If the page indicates that an App includes installation scripts, inspect its source code, configuration, and actual behavior before installing it.
|
||||
|
||||
#### Install an App
|
||||
|
||||
After selecting an App Server, install an App as follows:
|
||||
|
||||
1. On the settings page, open `Apps > Store`.
|
||||
|
||||

|
||||
|
||||
2. Select the App Server and the App to install, then click `Install`.
|
||||
|
||||

|
||||
|
||||
3. If the App requires environment variables, complete the form and confirm the installation.
|
||||
|
||||
4. Wait for the `Installation log` window to report that installation has finished, then close the window.
|
||||
|
||||

|
||||
|
||||
The log shows repository access, validation, extraction, installation scripts, and the final result. If installation fails, copy the log or capture the error at the end before closing the window.
|
||||
|
||||
5. Return to the `Setting` page on the device touchscreen and tap the Apps icon.
|
||||
|
||||

|
||||
|
||||
6. Select the newly installed App to launch it.
|
||||
|
||||

|
||||
|
||||
<a id="upload-zip-install-app"></a>
|
||||
|
||||
### Install an App from a ZIP File
|
||||
|
||||
You can upload a ZIP file to install an App obtained elsewhere or one that you developed yourself. The ZIP archive must contain exactly one top-level App directory:
|
||||
|
||||
```text
|
||||
example-app.zip
|
||||
└── example-app/
|
||||
├── app.json
|
||||
├── main.py
|
||||
└── assets/ # Optional resources
|
||||
```
|
||||
|
||||
To install the ZIP file:
|
||||
|
||||
1. Open the NanoKVM Go web interface and go to `Settings > Apps > Installed`;
|
||||
|
||||
2. click `Upload ZIP` and select the App ZIP file;
|
||||
|
||||

|
||||
|
||||
3. after the web interface validates the ZIP file and `app.json`, complete any requested App configuration;
|
||||
|
||||

|
||||
|
||||
4. click `Install` and wait for the `Installation log` window to report success;
|
||||
|
||||

|
||||
|
||||
5. open the `Apps` page on the device touchscreen and select the App.
|
||||
|
||||
<a id="exit-app"></a>
|
||||
|
||||
### Exit an App
|
||||
|
||||
While an App is running full screen, use the reserved left-edge gesture to return to the `Apps` page:
|
||||
|
||||
1. Touch and hold the left edge of the screen, then swipe right toward the center;
|
||||
2. keep holding while the upper and lower progress bars on the left edge move together and fill the edge;
|
||||
3. release your finger after the progress bars are full to exit the App.
|
||||
|
||||

|
||||
|
||||
To cancel, move your finger back to the left before releasing it. Release your finger after the two progress bars separate again, and the App will continue running:
|
||||
|
||||

|
||||
|
||||
### Manage Installed Apps
|
||||
|
||||
On `Settings > Apps > Installed`, you can edit an App's environment variables, download its ZIP file, or remove it. After installing, deleting, or renaming an App, or modifying its `app.json`, the device list normally refreshes within about 10 seconds. You do not need to restart `kvmcomm`.
|
||||
|
||||

|
||||
|
||||
## Develop a Custom App
|
||||
|
||||
### Get the SDK and Examples
|
||||
|
||||
Public resources are available in [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps). The repository uses two independent branches:
|
||||
|
||||
- [`main`](https://github.com/sipeed/NanoKVM-Go-Apps/tree/main): the App Store catalog, App source code, and `_utils`;
|
||||
- [`base`](https://github.com/sipeed/NanoKVM-Go-Apps/tree/base): the shared `appbase.py`, `appbase.pyi`, English and Chinese development guides, and `_utils`.
|
||||
|
||||
Use `base` for the SDK and API documentation when developing an App. Use `main` to browse, install, or learn from existing Apps. Both branches include `_utils`, so general tools such as the screen-recording script can be obtained from either branch.
|
||||
|
||||
<details>
|
||||
<summary>Record the NanoKVM Go device display</summary>
|
||||
|
||||
The `_utils/record-nanokvm-fb0.sh` script in the public repository reads the device's `fb0` and encodes it as a video on the local computer. It is useful for App demonstrations and reproducing issues.
|
||||
|
||||
The script reads the framebuffer from the NanoKVM Go through `ssh`; it does not use `scp`. The local computer needs Bash, `ssh`, and `ffmpeg`. Before using the script, also make sure that the computer can sign in to the device without a password:
|
||||
|
||||
```sh
|
||||
ssh root@<DEVICE_IP> 'echo ok'
|
||||
```
|
||||
|
||||
If the local computer does not have an SSH key yet, create one in a **local terminal**:
|
||||
|
||||
```sh
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
Then copy the public key to the NanoKVM Go and verify the login again:
|
||||
|
||||
```sh
|
||||
ssh-copy-id root@<DEVICE_IP>
|
||||
ssh root@<DEVICE_IP> 'echo ok'
|
||||
```
|
||||
|
||||
When the last command prints `ok` without asking for a password, passwordless SSH is ready.
|
||||
|
||||
1. Clone the repository and enter the `_utils` directory in a **local terminal**:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/sipeed/NanoKVM-Go-Apps.git
|
||||
cd NanoKVM-Go-Apps/_utils
|
||||
```
|
||||
|
||||
2. Confirm that the required commands are available on the local computer:
|
||||
|
||||
```sh
|
||||
command -v ssh
|
||||
command -v ffmpeg
|
||||
```
|
||||
|
||||
If either command does not print a path, install the corresponding tool before continuing.
|
||||
3. Set the actual NanoKVM Go address through `NANOKVM_HOST`, then run the script:
|
||||
|
||||
```sh
|
||||
chmod +x record-nanokvm-fb0.sh
|
||||
NANOKVM_HOST=root@<DEVICE_IP> ./record-nanokvm-fb0.sh xxx.mp4
|
||||
```
|
||||
|
||||
Operate the App on the device touchscreen while recording. Press `Ctrl-C` in the local terminal when finished. The script stops capturing, saves the video, and prints its location.
|
||||
|
||||
`NANOKVM_HOST` applies only to this command, so you do not need to modify the script. On Windows 10/11, run the Bash script through Git Bash or WSL and install both `ssh` and `ffmpeg` in the same environment.
|
||||
|
||||
</details>
|
||||
|
||||
### Create Your First Hello World App
|
||||
|
||||
#### App Directory Structure
|
||||
|
||||
Each App uses a separate directory. Use lowercase English letters and hyphens for the directory name, for example, `hello-world`:
|
||||
|
||||
```text
|
||||
hello-world/
|
||||
├── app.json
|
||||
├── main.py
|
||||
├── assets/ # Optional resources, such as icon.png
|
||||
├── pre-install.sh # Optional; required only when app.json declares pre_script
|
||||
└── post-install.sh # Optional; required only when app.json declares post_script
|
||||
```
|
||||
|
||||
The launcher scans direct child directories of `launcher.apps_dir`, which defaults to `/kvmcomm/apps`. A directory appears on the Apps page only when all of the following conditions are met:
|
||||
|
||||
1. its name does not start with `_`;
|
||||
2. it contains a regular file named `main.py`;
|
||||
3. it contains a valid `app.json`;
|
||||
4. `app.json.app_id` is a valid reverse-domain package name and matches the directory name;
|
||||
5. `app.json.name` is a non-empty string.
|
||||
|
||||
The Apps list refreshes automatically. After adding, deleting, or renaming an App, or modifying its `app.json`, wait about 10 seconds. You do not need to restart `kvmcomm`.
|
||||
|
||||
#### Write the Manifest and Entry Point
|
||||
|
||||
Add the minimal App manifest to `hello-world/app.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_id": "com.example.hello_world",
|
||||
"name": "Hello World",
|
||||
"creator": "Your Name",
|
||||
"create_time": "2026-07-30",
|
||||
"version": "1.0.0",
|
||||
"desc": "A minimal NanoKVM App.",
|
||||
"category": "demo"
|
||||
}
|
||||
```
|
||||
|
||||
`app_id`, `name`, `creator`, `create_time`, and `version` are required. `desc`, `category`, and `icon` are optional. `app_id` must have at least three components. Its final component must equal the directory name with each `-` replaced by `_`; for example, `hello-world` maps to `com.example.hello_world`. This minimal example does not configure an icon. When adding one later, set `icon` to a resource path relative to the App directory.
|
||||
|
||||
If the App needs environment variables, define them in the `app.json.env` object. The web interface automatically creates a configuration form, and the App reads the values through `ctx.env`. Do not create a separate `.env` file.
|
||||
|
||||
If an App needs to install dependencies or generate configuration during its first installation, declare optional lifecycle scripts in the same `app.json`. The following continues with `hello-world` and shows a complete manifest that includes environment variables and lifecycle scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_id": "com.example.hello_world",
|
||||
"name": "Hello World",
|
||||
"creator": "Your Name",
|
||||
"create_time": "2026-07-30",
|
||||
"version": "1.0.0",
|
||||
"desc": "A configurable NanoKVM App.",
|
||||
"category": "demo",
|
||||
"env": {
|
||||
"GREETING": {
|
||||
"label": "Greeting",
|
||||
"default": "Hello",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Text displayed on the screen"
|
||||
}
|
||||
},
|
||||
"pre_script": "pre-install.sh",
|
||||
"post_script": "post-install.sh"
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `env`, `pre_script`, and `post_script` are not separate JSON files, and they must not be written after the outer `app.json` braces. Put them in the same `{ ... }` object as `app_id`, `name`, and the other manifest fields, separated by commas. If your App does not need these features, do not add these fields to the manifest.
|
||||
|
||||
When the manifest declares `pre_script` or `post_script`, the corresponding script must exist in the App directory and must be included in the ZIP archive; otherwise, installation fails. Remove a script field from the manifest if the App does not need that lifecycle script.
|
||||
|
||||
- `pre_script` runs before deployment. If it fails, the App is not installed;
|
||||
- `post_script` runs after the App files are deployed. If it fails, the installation is rolled back;
|
||||
- each path must be a safe relative path inside the App directory, which is also the script's working directory;
|
||||
- scripts can read the configuration from `app.json.env`, as well as `NANOKVM_APP_DIR`, `NANOKVM_APP_ID`, and `NANOKVM_APP_PHASE`;
|
||||
- scripts run through `/bin/bash` with root privileges, so only install Apps from trusted sources.
|
||||
|
||||
Put one-time work such as dependency installation and directory initialization in lifecycle scripts. Do not require regular users to sign in over SSH and manually run `apt install` or `pip install`. Scripts should be safe to run repeatedly and should use non-interactive options so that the installation page does not wait indefinitely for input.
|
||||
|
||||
Add the minimal program to `hello-world/main.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from appbase import AppContext, WHITE, app
|
||||
|
||||
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
greeting = ctx.env.get("GREETING", "Hello")
|
||||
|
||||
def tick(dt: float) -> None:
|
||||
ctx.fb.clear(0)
|
||||
ctx.fb.text_center(
|
||||
ctx.width // 2,
|
||||
ctx.height // 2,
|
||||
greeting,
|
||||
WHITE,
|
||||
2,
|
||||
)
|
||||
|
||||
ctx.run(tick, fps=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
`@app()` reads `app.json` from the same directory, opens the framebuffer and touch device, creates the `AppContext`, and cleans up resources when the App exits. Keep the `if __name__ == "__main__"` guard so that importing the module from another tool does not open hardware devices unexpectedly.
|
||||
|
||||
#### Package, Upload, and Launch
|
||||
|
||||
Get an example from [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps), or create the `hello-world` directory locally. The web uploader requires the ZIP file to contain exactly one top-level App directory:
|
||||
|
||||
```text
|
||||
hello-world.zip
|
||||
└── hello-world/
|
||||
├── app.json
|
||||
├── main.py
|
||||
├── assets/ # Optional
|
||||
├── pre-install.sh # Required only when declared in app.json
|
||||
└── post-install.sh # Required only when declared in app.json
|
||||
```
|
||||
|
||||
Create the archive in a **local terminal**:
|
||||
|
||||
```bash
|
||||
zip -r hello-world.zip hello-world
|
||||
```
|
||||
|
||||
Install `hello-world.zip` by following [Install an App from a ZIP File](#upload-zip-install-app). During installation:
|
||||
|
||||
- if `app.json.env` is defined, the web interface displays an environment-variable form before installation;
|
||||
- the `Installation log` displays upload, validation, extraction, and lifecycle-script output in real time. Keep it open until installation succeeds;
|
||||
- if installation fails, save the complete log and use the final error messages to correct the App or its configuration;
|
||||
- after installation succeeds, open the `Apps` page on the device touchscreen and select `Hello World`.
|
||||
|
||||
> 📷 **Screenshot needed:** The final `Hello World` display on the NanoKVM Go touchscreen, confirming that the example was installed and launched successfully.
|
||||
|
||||
The installation log records only the current operation. When troubleshooting, copy or capture at least the final lines before closing it.
|
||||
|
||||
<details>
|
||||
<summary>Advanced: Deploy from the command line with SSH/SCP</summary>
|
||||
|
||||
For debugging or automation, copy the directory directly over SSH/SCP from a local terminal. The destination is still `launcher.apps_dir`, which defaults to `/kvmcomm/apps`:
|
||||
|
||||
```bash
|
||||
scp -r hello-world root@<DEVICE_IP>:/kvmcomm/apps/
|
||||
ssh root@<DEVICE_IP> 'python3 -m py_compile /kvmcomm/apps/hello-world/main.py'
|
||||
```
|
||||
|
||||
After copying the directory, wait for the Apps list to refresh. Do not overwrite the shared `appbase.py` or `appbase.pyi` already on the device unless you have confirmed that the SDK and App versions match.
|
||||
|
||||
</details>
|
||||
|
||||
## Common APIs
|
||||
|
||||
### AppContext, Lifecycle, and Main Loop
|
||||
|
||||
In the previous Hello World example, `main()` receives an argument named `ctx`:
|
||||
|
||||
```python
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
`ctx` is the conventional abbreviation for “context,” and its type is `AppContext`. Here, a context is the collection of resources an App needs while it is running, including the framebuffer drawing object, touch input, display dimensions, and environment variables configured in the web interface.
|
||||
|
||||
Developers do not create `ctx` manually, and it is not a global variable. The `@app()` decorator wraps the original `main(ctx)` function in an entry point that takes no arguments. The complete loading and execution sequence is:
|
||||
|
||||
```text
|
||||
Load main.py and define main(ctx)
|
||||
-> @app() reads and validates app.json and its environment configuration,
|
||||
then creates an entry point that takes no arguments
|
||||
-> the guard at the end of the file calls the decorated main()
|
||||
-> open the framebuffer and touch device
|
||||
-> create an AppContext object
|
||||
-> pass that object to the original main(ctx) as ctx
|
||||
-> close the touch device and framebuffer after the App exits
|
||||
```
|
||||
|
||||
The argument does not have to be named `ctx`; `context` would work as well. This guide and the official examples consistently use the conventional shorter name.
|
||||
|
||||
Common `AppContext` members include:
|
||||
|
||||
| API | Purpose |
|
||||
| --- | --- |
|
||||
| `ctx.width`, `ctx.height` | Full logical framebuffer dimensions after rotation |
|
||||
| `ctx.fb` | Drawing object |
|
||||
| `ctx.touch` | Low-level touch reader, normally accessed through the helper methods below |
|
||||
| `ctx.env` | Read-only environment mapping built from the `env` field in `app.json` and the Launcher configuration |
|
||||
| `ctx.poll()` | Return a list of pending `(kind, x, y)` touch events |
|
||||
| `ctx.taps()` | Return tap coordinates only and ignore swipe events |
|
||||
| `ctx.button(rect, label, bg, fg, scale)` | Draw a button and return the same `Rect` for touch hit-testing |
|
||||
| `ctx.flush()` | Submit the back buffer to the display |
|
||||
| `ctx.run(tick, fps, on_tap, on_swipe)` | Limit frame rate, dispatch touch input, and refresh automatically |
|
||||
|
||||
Most Apps can use `ctx.run()` directly as their main loop. On every frame, it performs these steps:
|
||||
|
||||
1. Read touch events and dispatch taps and swipes to `on_tap` and `on_swipe`;
|
||||
2. Call `tick(dt)` once to update state and draw the current frame;
|
||||
3. Call `ctx.flush()` automatically to present the back buffer on the display;
|
||||
4. Wait for the next frame according to `fps`, preventing the loop from consuming an entire CPU core.
|
||||
|
||||
The `dt` argument passed to `tick(dt)` is the number of seconds elapsed since the previous frame, which is useful for animations and timers. Returning `False` from `tick()` ends the main loop. `main(ctx)` then returns, and `@app()` cleans up the hardware resources.
|
||||
|
||||
Therefore, when using `ctx.run()`, do not call `flush()` again inside `tick()`, and do not independently open or close the same framebuffer or touch device. An App that does not need continuous updates can instead organize its own flow with `ctx.poll()` and `ctx.flush()`.
|
||||
|
||||
### Drawing, Colors, and Buttons
|
||||
|
||||
`ctx.fb` provides `clear()`, `put_pixel()`, `fill_rect()`, `draw_line()`, `draw_text()`, `text_center()`, `draw_sprite()`, and `flush()`. Create colors with `rgb565(r, g, b)` or use the SDK constants:
|
||||
|
||||
```text
|
||||
BLACK WHITE RED GREEN BLUE YELLOW GRAY DKGRAY
|
||||
ORANGE CYAN MAGENTA NAVY
|
||||
```
|
||||
|
||||
Use the same `Rect` to draw a button and test whether it was tapped:
|
||||
|
||||
```python
|
||||
from appbase import AppContext, GREEN, RED, Rect, app
|
||||
|
||||
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
state = {"count": 0}
|
||||
add_button = Rect(70, 80, 100, 48)
|
||||
|
||||
def on_tap(x: int, y: int) -> None:
|
||||
if add_button.contains(x, y):
|
||||
state["count"] += 1
|
||||
|
||||
def tick(dt: float) -> None:
|
||||
ctx.fb.clear(0)
|
||||
ctx.button(add_button, "ADD", GREEN)
|
||||
ctx.fb.text_center(ctx.width // 2, 145, str(state["count"]), RED, 2)
|
||||
|
||||
ctx.run(tick, fps=20, on_tap=on_tap)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Resources and Relative Paths
|
||||
|
||||
The launcher changes the current working directory to the App directory, so relative paths such as `assets/icon.png` work directly. To support importing or testing from another directory, calculate paths from `__file__`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parent
|
||||
icon_path = APP_DIR / "assets" / "icon.png"
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Display Dimensions and Hidden Area
|
||||
|
||||
The physical framebuffer is `284×240`, and its leftmost 14 columns are not visible. The host normally launches Apps with `rotate=90`, producing a `240×284` logical canvas whose bottom 14 rows are hidden. When the display is inverted, the host uses `rotate=270`, moving the hidden area to the top.
|
||||
|
||||
| `rotate` | Logical dimensions | Hidden area | Visible logical area |
|
||||
| ---: | --- | --- | --- |
|
||||
| `0` | `284×240` | Left 14 columns | `x=14..283, y=0..239` |
|
||||
| `90` | `240×284` | Bottom 14 rows | `x=0..239, y=0..269` |
|
||||
| `180` | `284×240` | Right 14 columns | `x=0..269, y=0..239` |
|
||||
| `270` | `240×284` | Top 14 rows | `x=0..239, y=14..283` |
|
||||
|
||||
Always base the layout on `ctx.width`, `ctx.height`, and `ctx.fb.rotate`; do not hard-code a fixed canvas. Use the table above when calculating the visible area.
|
||||
|
||||
### Touch Interaction
|
||||
|
||||
`ctx.poll()` returns `tap`, `up`, `down`, `left`, and `right` events. Their coordinates are already transformed into the rotated logical coordinate system.
|
||||
|
||||
The host reserves the [left-edge exit gesture](#exit-app). Avoid placing controls that require horizontal swipes on the left edge, because they could be triggered while a user exits the App. The host refuses to launch an App if the touch device is unavailable.
|
||||
|
||||
### Runtime and Security
|
||||
|
||||
The runtime requires an available `/dev/fb0`, the `/dev/input/event0` touch device, Python 3, and the launcher enabled in the device configuration. Configure non-default device paths with environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `APPBASE_FB_DEVICE` | `/dev/fb0` | Framebuffer device |
|
||||
| `APPBASE_FB_ROTATE` | `0` | Rotation angle; the host normally sets `90` or `270` |
|
||||
| `APPBASE_TOUCH_DEVICE` | `/dev/input/event0` | Touch input device |
|
||||
|
||||
Apps may run with elevated privileges and can access device configuration, credentials, and the network. Deploy only trusted code, do not store secrets in source files, and do not install dependencies from unknown sources. The `appbase.py` and `appbase.pyi` versions on the device must match.
|
||||
|
||||
### Simulator and AI-assisted Development
|
||||
|
||||
The x86 SDL simulator only reads directories and displays the Apps list. It does not execute Python, map the framebuffer, read touch input, or simulate the exit gesture. Test the display, colors, orientation, and frame rate on a physical NanoKVM Go.
|
||||
|
||||
Before asking an AI assistant to write an App, provide it with the device README, existing App source code, and your requirements:
|
||||
|
||||
<details>
|
||||
<summary>Advanced: Read the runtime guide from the device</summary>
|
||||
|
||||
```bash
|
||||
ssh root@<DEVICE_IP> 'cat /kvmcomm/apps/README.md'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Explicitly require the directory-based architecture: generate both `app.json` and `main.py`, do not place a standalone Python file loosely in the Apps directory, and do not overwrite the shared SDK.
|
||||
|
||||
## Release Checklist
|
||||
|
||||
An App is ready to share after it passes the following checks:
|
||||
|
||||
- all required `app.json` fields are present, and directory names and resource paths are correct;
|
||||
- `main.py` passes `py_compile`;
|
||||
- the App appears on the Apps page and launches successfully;
|
||||
- no content is placed in the 14-pixel hidden area;
|
||||
- taps, swipes, and the left-edge exit gesture work correctly;
|
||||
- the main interface and touch controls recover after the App exits;
|
||||
- no secrets, real IP addresses, or private device data are embedded in code or resources.
|
||||
@@ -570,6 +570,8 @@ items:
|
||||
file: kvm/NanoKVM_Go/system/introduction.md
|
||||
- label: Flash Image
|
||||
file: kvm/NanoKVM_Go/system/flashing.md
|
||||
- label: Custom Apps
|
||||
file: kvm/NanoKVM_Go/custom_app.md
|
||||
- label: FAQ
|
||||
file: kvm/NanoKVM_Go/faq.md
|
||||
- label: NanoKVM PCIe
|
||||
|
||||
555
docs/hardware/zh/kvm/NanoKVM_Go/custom_app.md
Normal file
@@ -0,0 +1,555 @@
|
||||
---
|
||||
title: 扩展:自定义 APP
|
||||
keywords: NanoKVM Go, Python APP, appbase, framebuffer, 触摸屏
|
||||
update:
|
||||
- date: 2026-08-04
|
||||
version: v1.1
|
||||
author: Liang Ziyue
|
||||
content:
|
||||
- 补充 AppContext 来源、生命周期和主循环说明
|
||||
- 统一操作截图的对齐方式
|
||||
- 修复章节内跳转锚点
|
||||
- 修正清单、生命周期脚本和录屏命令示例
|
||||
- 同步中英文 ZIP 安装截图和待补截图
|
||||
- date: 2026-07-30
|
||||
version: v1.0
|
||||
author: taonyx
|
||||
content:
|
||||
- 自定义 APP 开发与部署说明
|
||||
---
|
||||
|
||||
# 自定义 APP
|
||||
|
||||
## 自定义 APP 简介
|
||||
|
||||
自定义 APP 是运行在 NanoKVM Go 触摸屏上的全屏 Python 应用。它通过设备提供的 `appbase` SDK 在 RGB565 framebuffer 上绘制界面、读取触摸事件,可以把 NanoKVM Go 的屏幕扩展成状态面板、计时器、行情看板或其他交互工具。
|
||||
|
||||
NanoKVM Go 提供了完整的 APP 管理和运行流程:
|
||||
|
||||
```text
|
||||
App Server 或本地 ZIP → 网页安装和管理 → 设备 Apps 页面启动
|
||||
```
|
||||
|
||||
普通用户可以直接安装和使用现成 APP;开发者也可以编写自己的 Python APP,打包为 ZIP 后上传到设备。本文先介绍如何安装和使用 APP,再以 `Hello World` 为例说明开发和部署流程。
|
||||
|
||||
## 使用前准备
|
||||
|
||||
开始操作前,请确认:
|
||||
|
||||
- NanoKVM Go 的系统和应用已更新至最新版本;
|
||||
- 可以在局域网内正常访问 NanoKVM Go 网页;
|
||||
- 设备触摸屏可以正常操作,并且主界面中有 `Apps` 页面;
|
||||
- 从 App Server 安装 APP 时,NanoKVM Go 可以访问对应的应用仓库;
|
||||
- 开发自定义 APP 时,电脑上已准备文本编辑器、Python 基础环境和 ZIP 打包工具。
|
||||
|
||||
> 如果网页设置中没有 `Apps` 选项,或设备端没有 `Apps` 页面,请先检查并更新 NanoKVM Go 的系统和应用版本。
|
||||
|
||||
## 安装和使用 APP
|
||||
|
||||
### 运行内置 APP
|
||||
|
||||
在设备触摸屏上进入 `Apps` 页面,选择一个内置 APP 即可启动。可以先通过这些示例了解 APP 的全屏显示、动画和触摸交互效果:
|
||||
|
||||
<video src="./../../../assets/NanoKVM/go/custom_app/apps-demo.mp4" aria-label="NanoKVM Go 内置 APP 运行演示" style="width: 100%; max-width: 568px;" playsinline controls autoplay loop muted preload="metadata"></video>
|
||||
|
||||
| APP 目录 | 页面名称 | 主要功能 |
|
||||
| --- | --- | --- |
|
||||
| `conways-game-of-life` | Conway | 运行限帧动画 |
|
||||
| `crypto-candlestick` | Crypto | 显示行情数据并支持滑动翻页 |
|
||||
| `nyan-cat` | Nyan Cat | 显示像素精灵动画 |
|
||||
| `pomodoro-timer` | Pomodoro | 提供按钮、点击和倒计时功能 |
|
||||
|
||||
Crypto 使用公开行情接口;网络不可用时会回退到模拟数据,仅用于展示网络 APP 的实现方式。
|
||||
|
||||
### 从 App Store 安装 APP
|
||||
|
||||
App Server 是供 NanoKVM Go 浏览和下载 APP 的应用仓库。网页 `设置 > 应用` 中包含两个区域:
|
||||
|
||||
- `已安装`(`Installed`):管理已安装的 APP,可以编辑配置、下载或移除 APP;
|
||||
- `应用商店`(`Store`):浏览官方仓库或用户添加的仓库,并直接安装 APP。
|
||||
|
||||
无论使用官方仓库还是其他 App Server,都需要先打开 NanoKVM Go 网页,点击顶部工具栏中的设置图标进入 `设置`。
|
||||
|
||||

|
||||
|
||||
#### 选择 App Server
|
||||
|
||||
Sipeed 官方仓库 [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps) 已预置在设备中,使用官方仓库时不需要额外配置 App Server。
|
||||
|
||||
如果需要使用其他 APP 仓库,请先进入 `设置 > 应用`,点击 `应用商店 > 应用源`。
|
||||
|
||||

|
||||
|
||||
点击添加服务器,填写服务器名称和 URL 后保存。添加完成后,可以在应用商店中切换至该仓库。
|
||||
|
||||

|
||||
|
||||
> 只添加可信的应用仓库。APP 的安装脚本会以 root 权限运行;如果页面提示 APP 包含安装脚本,请先检查其源码、配置项和实际功能。
|
||||
|
||||
#### 安装 APP
|
||||
|
||||
选择好 App Server 后,按照以下步骤安装 APP:
|
||||
|
||||
1. 在设置页面中进入 `应用 > 应用商店`。
|
||||
|
||||

|
||||
|
||||
2. 选择 App Server 和需要安装的 APP,然后点击 `安装`。
|
||||
|
||||

|
||||
|
||||
3. 如果 APP 需要配置环境变量,按照页面提示填写,然后确认安装。
|
||||
|
||||
4. 在 `安装日志`(`Installation log`)窗口中等待安装完成。显示安装成功后再关闭窗口。
|
||||
|
||||

|
||||
|
||||
安装日志会显示仓库读取、校验、解压、安装脚本和最终结果。如果安装失败,请先复制或截图保存日志末尾的错误信息。
|
||||
|
||||
5. 回到设备触摸屏的 `Setting` 页面,点击应用图标进入 `Apps` 页面。
|
||||
|
||||

|
||||
|
||||
6. 然后选择刚安装的 APP 启动。
|
||||
|
||||

|
||||
|
||||
|
||||
<a id="upload-zip-install-app"></a>
|
||||
|
||||
### 上传 ZIP 安装 APP
|
||||
|
||||
从其他渠道取得 APP,或需要安装自己开发的 APP 时,可以上传 ZIP 文件。ZIP 内必须只有一个顶层 APP 目录,例如:
|
||||
|
||||
```text
|
||||
example-app.zip
|
||||
└── example-app/
|
||||
├── app.json
|
||||
├── main.py
|
||||
└── assets/ # 可选资源
|
||||
```
|
||||
|
||||
安装步骤如下:
|
||||
|
||||
1. 打开 NanoKVM Go 网页,进入 `设置 > 应用 > 已安装`;
|
||||
|
||||
2. 点击 `上传 ZIP`,选择 APP 的 ZIP 文件;
|
||||
|
||||

|
||||
|
||||
3. 网页完成 ZIP 和 `app.json` 校验后,按提示填写 APP 配置;
|
||||
|
||||

|
||||
|
||||
4. 点击 `安装`,在 `安装日志` 窗口中等待安装成功;
|
||||
|
||||

|
||||
|
||||
5. 回到设备触摸屏的 `Apps` 页面,选择对应 APP 启动。
|
||||
|
||||
<a id="exit-app"></a>
|
||||
|
||||
### 退出 APP
|
||||
|
||||
APP 全屏运行时,可以使用设备保留的左边缘手势返回 `Apps` 页面:
|
||||
|
||||
1. 在屏幕左边缘按住手指,向右滑到屏幕中部;
|
||||
2. 保持手指不动,等待左边缘的上下两段进度条逐渐靠近并填满;
|
||||
3. 进度条填满后松开手指,退出 APP。
|
||||
|
||||

|
||||
|
||||
如果想取消退出,请在松手前将手指向左移回去,等进度条重新分开后再松手,APP 会继续运行:
|
||||
|
||||

|
||||
|
||||
### 管理已安装的 APP
|
||||
|
||||
在网页 `设置 > 应用 > 已安装` 中,可以编辑 APP 的环境变量、下载 ZIP 或移除 APP。安装、删除、重命名 APP,或修改 `app.json` 后,设备端列表通常会在约 10 秒内自动刷新,不需要重启 `kvmcomm`。
|
||||
|
||||

|
||||
|
||||
## 开发自定义 APP
|
||||
|
||||
### 获取 SDK 和示例
|
||||
|
||||
公开内容位于 [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps),仓库按用途分为两个独立分支:
|
||||
|
||||
- [`main`](https://github.com/sipeed/NanoKVM-Go-Apps/tree/main):App Store 使用的应用目录、各 APP 源码和 `_utils`;
|
||||
- [`base`](https://github.com/sipeed/NanoKVM-Go-Apps/tree/base):共享 `appbase.py`、`appbase.pyi`、中英文开发文档和 `_utils`。
|
||||
|
||||
开发 APP 时从 `base` 获取 SDK 和接口文档;浏览、安装或参考现有 APP 时使用 `main`。两个分支都保留 `_utils`,录屏等通用工具可从任一分支获取。
|
||||
|
||||
<details>
|
||||
<summary>录制 NanoKVM Go 的设备屏幕画面</summary>
|
||||
|
||||
如果需要把 APP 的实际操作过程录成视频,可以使用开源仓库 `_utils/record-nanokvm-fb0.sh`。它从设备的 `fb0` 读取屏幕画面,在本机编码为视频,适合录制 APP 演示或问题复现过程。
|
||||
|
||||
脚本只通过 `ssh` 从 NanoKVM Go 读取 framebuffer,不使用 `scp`。本机需要准备 Bash、`ssh` 和 `ffmpeg`;使用前还要确认本机可以免密登录设备:
|
||||
|
||||
```sh
|
||||
ssh root@<设备IP> 'echo ok'
|
||||
```
|
||||
|
||||
如果还没有本机 SSH 密钥,先在**本机终端**生成一个:
|
||||
|
||||
```sh
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
然后把公钥复制到 NanoKVM Go,并再次验证登录:
|
||||
|
||||
```sh
|
||||
ssh-copy-id root@<设备IP>
|
||||
ssh root@<设备IP> 'echo ok'
|
||||
```
|
||||
|
||||
最后一条命令输出 `ok`,并且不再要求输入密码时,说明免密登录已经可用。
|
||||
|
||||
1. 在**本机终端**获取脚本并进入 `_utils` 目录:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/sipeed/NanoKVM-Go-Apps.git
|
||||
cd NanoKVM-Go-Apps/_utils
|
||||
```
|
||||
|
||||
2. 确认本机可以找到脚本所需的命令:
|
||||
|
||||
```sh
|
||||
command -v ssh
|
||||
command -v ffmpeg
|
||||
```
|
||||
|
||||
如果任一命令没有输出路径,请先在本机安装对应工具。
|
||||
3. 通过 `NANOKVM_HOST` 指定 NanoKVM Go 的实际地址,然后执行脚本:
|
||||
|
||||
```sh
|
||||
chmod +x record-nanokvm-fb0.sh
|
||||
NANOKVM_HOST=root@<设备IP> ./record-nanokvm-fb0.sh xxx.mp4
|
||||
```
|
||||
|
||||
脚本开始后,在设备触摸屏上操作 APP。录制结束时在本机终端按 `Ctrl-C`,脚本会结束采集并保存视频文件;终端会显示保存位置。
|
||||
|
||||
`NANOKVM_HOST` 只对本次命令生效,不需要修改脚本源码。Windows 10/11 用户请通过 Git Bash 或 WSL 执行 Bash 脚本,并在同一环境中安装 `ssh` 和 `ffmpeg`。
|
||||
|
||||
</details>
|
||||
|
||||
### 创建第一个 Hello World
|
||||
|
||||
#### APP 目录结构
|
||||
|
||||
每个 APP 使用独立目录。目录名建议使用小写英文和连字符,例如 `hello-world`:
|
||||
|
||||
```text
|
||||
hello-world/
|
||||
├── app.json
|
||||
├── main.py
|
||||
├── assets/ # 可选资源,例如 icon.png
|
||||
├── pre-install.sh # 可选;仅在 app.json 声明 pre_script 时需要
|
||||
└── post-install.sh # 可选;仅在 app.json 声明 post_script 时需要
|
||||
```
|
||||
|
||||
启动器扫描 `launcher.apps_dir` 的直接子目录(默认 `/kvmcomm/apps`)。一个目录要出现在 Apps 页面,必须同时满足:
|
||||
|
||||
1. 目录名不以 `_` 开头;
|
||||
2. 有普通文件 `main.py`;
|
||||
3. 有合法的 `app.json`;
|
||||
4. `app.json.app_id` 是合法的倒置域名包名,并与目录名对应;
|
||||
5. `app.json.name` 是非空字符串。
|
||||
|
||||
Apps 列表会自动刷新,新增、删除、重命名 APP 或修改 `app.json` 通常等待约 10 秒即可,不需要重启 `kvmcomm`。
|
||||
|
||||
#### 编写清单和入口文件
|
||||
|
||||
在 `hello-world/app.json` 中写入最小 APP 清单:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_id": "com.example.hello_world",
|
||||
"name": "Hello World",
|
||||
"creator": "Your Name",
|
||||
"create_time": "2026-07-30",
|
||||
"version": "1.0.0",
|
||||
"desc": "A minimal NanoKVM App.",
|
||||
"category": "demo"
|
||||
}
|
||||
```
|
||||
|
||||
`app_id`、`name`、`creator`、`create_time` 和 `version` 是必填字段;`desc`、`category` 和 `icon` 可选。`app_id` 至少包含三段,最后一段必须等于目录名将 `-` 替换为 `_` 后的结果,例如 `hello-world` 对应 `com.example.hello_world`。为了先跑通最小示例,这里暂不配置图标;以后添加图标时,`icon` 应填写相对于 APP 目录的资源路径。
|
||||
|
||||
如果 APP 需要配置环境变量,把它们写在 `app.json.env` 对象中。网页会根据它自动生成配置表单,APP 通过 `ctx.env` 读取。不要再单独创建 `.env` 文件。
|
||||
|
||||
如果 APP 在首次安装时需要安装依赖或生成配置,可以在同一个 `app.json` 中声明可选的安装生命周期脚本。下面继续以 `hello-world` 为例,展示包含环境变量和生命周期脚本的完整清单:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_id": "com.example.hello_world",
|
||||
"name": "Hello World",
|
||||
"creator": "Your Name",
|
||||
"create_time": "2026-07-30",
|
||||
"version": "1.0.0",
|
||||
"desc": "A configurable NanoKVM App.",
|
||||
"category": "demo",
|
||||
"env": {
|
||||
"GREETING": {
|
||||
"label": "问候语",
|
||||
"default": "Hello",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "显示在屏幕上的文字"
|
||||
}
|
||||
},
|
||||
"pre_script": "pre-install.sh",
|
||||
"post_script": "post-install.sh"
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`env`、`pre_script` 和 `post_script` 不是单独的 JSON 文件,也不能写在 `app.json` 最外层大括号后面。它们必须和 `app_id`、`name` 等字段一起放在同一个 `{ ... }` 中,并用逗号分隔。如果你的 APP 不需要这些功能,就不要把这些字段写进清单。
|
||||
|
||||
清单声明 `pre_script` 或 `post_script` 后,APP 目录中必须存在对应脚本,打包 ZIP 时也必须包含这些文件;否则安装会失败。如果不需要安装脚本,请从清单中删除对应字段。
|
||||
|
||||
- `pre_script` 在 APP 正式部署前运行,失败时不会安装 APP;
|
||||
- `post_script` 在 APP 文件部署后运行,失败时会回滚本次安装;
|
||||
- 路径必须是 APP 目录内的安全相对路径,脚本工作目录也是 APP 目录;
|
||||
- 脚本可以读取 `app.json.env` 中的配置,以及 `NANOKVM_APP_DIR`、`NANOKVM_APP_ID` 和 `NANOKVM_APP_PHASE`;
|
||||
- 脚本通过 `/bin/bash` 以 root 权限执行,因此只能安装来源可信的 APP。
|
||||
|
||||
依赖安装、目录初始化等一次性工作应放在生命周期脚本中,不要再要求普通用户登录 SSH 后手动执行 `apt install` 或 `pip install`。脚本应支持重复执行,并使用非交互模式,避免安装页面一直等待输入。
|
||||
|
||||
在 `hello-world/main.py` 中写入最小程序:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from appbase import AppContext, WHITE, app
|
||||
|
||||
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
greeting = ctx.env.get("GREETING", "Hello")
|
||||
|
||||
def tick(dt: float) -> None:
|
||||
ctx.fb.clear(0)
|
||||
ctx.fb.text_center(
|
||||
ctx.width // 2,
|
||||
ctx.height // 2,
|
||||
greeting,
|
||||
WHITE,
|
||||
2,
|
||||
)
|
||||
|
||||
ctx.run(tick, fps=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
`@app()` 会读取同目录的 `app.json`,打开 framebuffer 和触摸设备,创建 `AppContext`,并在 APP 退出时清理资源。保留 `if __name__ == "__main__"`,这样其他工具导入模块时不会意外打开硬件设备。
|
||||
|
||||
#### 打包、上传和启动
|
||||
|
||||
先从 [NanoKVM-Go-Apps](https://github.com/sipeed/NanoKVM-Go-Apps) 获取示例,或在本机创建 `hello-world` 目录。网页上传要求 ZIP 内只有一个顶层 APP 目录:
|
||||
|
||||
```text
|
||||
hello-world.zip
|
||||
└── hello-world/
|
||||
├── app.json
|
||||
├── main.py
|
||||
├── assets/ # 可选
|
||||
├── pre-install.sh # 仅在 app.json 声明时需要
|
||||
└── post-install.sh # 仅在 app.json 声明时需要
|
||||
```
|
||||
|
||||
在**本机终端**打包:
|
||||
|
||||
```bash
|
||||
zip -r hello-world.zip hello-world
|
||||
```
|
||||
|
||||
然后按照[上传 ZIP 安装 APP](#upload-zip-install-app)中的流程安装 `hello-world.zip`。安装时请注意:
|
||||
|
||||
- 如果声明了 `app.json.env`,网页会在安装前显示环境变量表单;
|
||||
- `Installation log` 会实时显示上传、校验、解压和生命周期脚本的输出,请等日志显示安装成功后再关闭窗口;
|
||||
- 安装失败时,请保存完整日志,并根据末尾的错误信息修正 APP 或配置;
|
||||
- 安装成功后,回到设备触摸屏的 `Apps` 页面并选择 `Hello World`。
|
||||
|
||||
> 📷 **待补截图:** NanoKVM Go 触摸屏上运行 `Hello World` 的最终效果,用于确认示例已成功安装并启动。
|
||||
|
||||
安装日志只记录本次操作,遇到问题时建议复制或截图保存末尾几行。
|
||||
|
||||
<details>
|
||||
<summary>进阶:通过 SSH/SCP 命令行部署</summary>
|
||||
|
||||
如需调试或自动化,也可以在本机终端通过 SSH/SCP 直接复制目录;设备端目标仍是 `launcher.apps_dir`(默认 `/kvmcomm/apps`):
|
||||
|
||||
```bash
|
||||
scp -r hello-world root@<设备IP>:/kvmcomm/apps/
|
||||
ssh root@<设备IP> 'python3 -m py_compile /kvmcomm/apps/hello-world/main.py'
|
||||
```
|
||||
|
||||
直接复制后同样等待 Apps 列表自动刷新。不要覆盖设备已有的共享 `appbase.py` 和 `appbase.pyi`,除非你确认 SDK 与 APP 版本匹配。
|
||||
|
||||
</details>
|
||||
|
||||
## 认识常用 API
|
||||
|
||||
### AppContext、生命周期和主循环
|
||||
|
||||
在前面的 Hello World 示例中,`main()` 函数接收了一个名为 `ctx` 的参数:
|
||||
|
||||
```python
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
`ctx` 是英文 `context`(上下文)的常用缩写,它的类型是 `AppContext`。这里的“上下文”可以理解为 APP 运行期间所需资源的集合,其中包含 framebuffer 绘图对象、触摸输入、屏幕尺寸和网页配置的环境变量。
|
||||
|
||||
`ctx` 不需要开发者手动创建,也不是全局变量。`@app()` 装饰器会把原始的 `main(ctx)` 包装成一个无参数入口。程序加载和运行时的完整顺序如下:
|
||||
|
||||
```text
|
||||
加载 main.py 并定义 main(ctx)
|
||||
→ @app() 读取、校验 app.json 和环境变量配置,并生成无参数入口
|
||||
→ 文件末尾调用装饰后的 main()
|
||||
→ 打开 framebuffer 和触摸设备
|
||||
→ 创建 AppContext 对象
|
||||
→ 将该对象作为 ctx 传给原始 main(ctx)
|
||||
→ APP 结束后关闭触摸设备和 framebuffer
|
||||
```
|
||||
|
||||
参数名并非必须写成 `ctx`,改成 `context` 也可以;本文和官方示例统一使用更常见的 `ctx`。
|
||||
|
||||
`AppContext` 的常用成员如下:
|
||||
|
||||
| API | 作用 |
|
||||
| --- | --- |
|
||||
| `ctx.width`、`ctx.height` | 旋转后的完整逻辑 framebuffer 尺寸 |
|
||||
| `ctx.fb` | 绘图对象 |
|
||||
| `ctx.touch` | 底层触摸读取对象,通常通过下方辅助方法使用 |
|
||||
| `ctx.env` | 从 `app.json` 的 `env` 字段和 Launcher 配置生成的只读环境变量映射 |
|
||||
| `ctx.poll()` | 返回尚未处理的 `(kind, x, y)` 触摸事件列表 |
|
||||
| `ctx.taps()` | 只返回点击坐标,忽略滑动事件 |
|
||||
| `ctx.button(rect, label, bg, fg, scale)` | 绘制按钮并返回用于触摸命中检测的同一个 `Rect` |
|
||||
| `ctx.flush()` | 把后备缓冲提交到屏幕 |
|
||||
| `ctx.run(tick, fps, on_tap, on_swipe)` | 限帧、分发触摸并自动刷新 |
|
||||
|
||||
大多数 APP 可以直接调用 `ctx.run()` 运行主循环。它会在每一帧依次完成:
|
||||
|
||||
1. 读取触摸事件,并将点击和滑动分别交给 `on_tap`、`on_swipe`;
|
||||
2. 调用一次 `tick(dt)` 更新状态并绘制当前画面;
|
||||
3. 自动调用 `ctx.flush()`,把后备缓冲显示到屏幕;
|
||||
4. 根据 `fps` 等待下一帧,避免循环占满 CPU。
|
||||
|
||||
`tick(dt)` 中的 `dt` 表示距上一帧经过的秒数,适合用来计算动画或倒计时。让 `tick()` 返回 `False` 可以结束主循环;`main(ctx)` 随后返回,`@app()` 会清理硬件资源。
|
||||
|
||||
因此,使用 `ctx.run()` 时不需要在 `tick()` 中再次调用 `flush()`,也不要独立打开或关闭同一个 framebuffer 和触摸设备。如果 APP 不需要持续刷新,也可以直接使用 `ctx.poll()` 和 `ctx.flush()` 自行组织流程。
|
||||
|
||||
### 绘图、颜色和按钮
|
||||
|
||||
`ctx.fb` 提供 `clear()`、`put_pixel()`、`fill_rect()`、`draw_line()`、`draw_text()`、`text_center()`、`draw_sprite()` 和 `flush()`。颜色可以使用 `rgb565(r, g, b)` 或 SDK 内置常量:
|
||||
|
||||
```text
|
||||
BLACK WHITE RED GREEN BLUE YELLOW GRAY DKGRAY
|
||||
ORANGE CYAN MAGENTA NAVY
|
||||
```
|
||||
|
||||
按钮可以用同一个 `Rect` 绘制和命中检测:
|
||||
|
||||
```python
|
||||
from appbase import AppContext, GREEN, RED, Rect, app
|
||||
|
||||
|
||||
@app()
|
||||
def main(ctx: AppContext) -> None:
|
||||
state = {"count": 0}
|
||||
add_button = Rect(70, 80, 100, 48)
|
||||
|
||||
def on_tap(x: int, y: int) -> None:
|
||||
if add_button.contains(x, y):
|
||||
state["count"] += 1
|
||||
|
||||
def tick(dt: float) -> None:
|
||||
ctx.fb.clear(0)
|
||||
ctx.button(add_button, "ADD", GREEN)
|
||||
ctx.fb.text_center(ctx.width // 2, 145, str(state["count"]), RED, 2)
|
||||
|
||||
ctx.run(tick, fps=20, on_tap=on_tap)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### 资源和相对路径
|
||||
|
||||
启动器会把当前工作目录切换到 APP 目录,因此 `assets/icon.png` 这样的相对路径可以直接使用。需要兼容从其他目录导入或测试时,建议根据 `__file__` 计算路径:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parent
|
||||
icon_path = APP_DIR / "assets" / "icon.png"
|
||||
```
|
||||
|
||||
## 开发时注意这些
|
||||
|
||||
### 屏幕尺寸和不可见区域
|
||||
|
||||
物理 framebuffer 为 `284×240`,左侧 14 列不会显示。主机通常以 `rotate=90` 启动 APP,逻辑画布是 `240×284`,底部 14 行不可见;屏幕反转时使用 `rotate=270`,隐藏区域移动到顶部。
|
||||
|
||||
| `rotate` | 逻辑尺寸 | 不可见区域 | 可见逻辑区域 |
|
||||
| ---: | --- | --- | --- |
|
||||
| `0` | `284×240` | 左 14 列 | `x=14..283, y=0..239` |
|
||||
| `90` | `240×284` | 底部 14 行 | `x=0..239, y=0..269` |
|
||||
| `180` | `284×240` | 右 14 列 | `x=0..269, y=0..239` |
|
||||
| `270` | `240×284` | 顶部 14 行 | `x=0..239, y=14..283` |
|
||||
|
||||
布局始终依据 `ctx.width`、`ctx.height` 和 `ctx.fb.rotate`,不要硬编码固定画布。需要计算可见区域时,可按上述表格处理。
|
||||
|
||||
### 触摸交互
|
||||
|
||||
`ctx.poll()` 返回的事件包括 `tap`、`up`、`down`、`left`、`right`,坐标已经转换到旋转后的逻辑坐标系。
|
||||
|
||||
主机保留了[左边缘退出手势](#exit-app)。APP 自己定义横向滑动操作时,建议不要把控件放在左边缘区域,以免用户退出 APP 时同时触发其他操作。触摸设备不可用时,主机会拒绝启动 APP。
|
||||
|
||||
### 运行环境和安全
|
||||
|
||||
运行前提包括可用的 `/dev/fb0`、触摸设备 `/dev/input/event0`、Python 3,以及设备配置中已启用的 launcher。非默认设备路径可通过环境变量配置:
|
||||
|
||||
| 变量 | 默认值 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `APPBASE_FB_DEVICE` | `/dev/fb0` | framebuffer 设备 |
|
||||
| `APPBASE_FB_ROTATE` | `0` | 旋转角度;主机通常设置为 `90` 或 `270` |
|
||||
| `APPBASE_TOUCH_DEVICE` | `/dev/input/event0` | 触摸设备 |
|
||||
|
||||
APP 可能以高权限运行,可以访问设备配置、凭据和网络。只部署可信代码,不要在源码中保存密钥,也不要随意安装来源不明的依赖。设备上的 `appbase.py` 和 `appbase.pyi` 必须保持版本匹配。
|
||||
|
||||
### 模拟器和 AI 开发
|
||||
|
||||
x86 SDL 模拟器只读取目录并展示 Apps 列表,不会真正执行 Python、映射 framebuffer、读取触摸或模拟退出手势。画面、颜色、方向和帧率必须在 NanoKVM Go 实机验证。
|
||||
|
||||
让 AI 编写 APP 前,先把设备上的 README、现有 APP 源码和需求一起提供给它:
|
||||
|
||||
<details>
|
||||
<summary>进阶:从设备读取运行说明</summary>
|
||||
|
||||
```bash
|
||||
ssh root@<设备IP> 'cat /kvmcomm/apps/README.md'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
明确要求 AI 遵循目录式架构:生成 `app.json` 和 `main.py`,不要把 APP 写成散落在目录中的单个 Python 文件,也不要覆盖共享 SDK。
|
||||
|
||||
## 完成发布
|
||||
|
||||
一个 APP 通过下面的检查,就可以交给其他人使用:
|
||||
|
||||
- `app.json` 字段完整,目录名和资源路径正确;
|
||||
- `main.py` 能通过 `py_compile`;
|
||||
- Apps 页面可以显示并启动;
|
||||
- 画面没有落入 14 像素不可见区;
|
||||
- 点击、滑动和左边缘退出手势正常;
|
||||
- APP 退出后主界面和触摸控制恢复;
|
||||
- 没有把密钥、真实 IP 或设备隐私数据写进代码和资源。
|
||||
@@ -571,6 +571,8 @@ items:
|
||||
file: kvm/NanoKVM_Go/system/introduction.md
|
||||
- label: 烧录镜像
|
||||
file: kvm/NanoKVM_Go/system/flashing.md
|
||||
- label: 扩展:自定义 APP
|
||||
file: kvm/NanoKVM_Go/custom_app.md
|
||||
- label: FAQ
|
||||
file: kvm/NanoKVM_Go/faq.md
|
||||
- label: NanoKVM PCIe
|
||||
|
||||