{"id":32892789,"url":"https://github.com/sipeed/nanokvm-userapps","last_synced_at":"2026-06-23T03:32:16.740Z","repository":{"id":322609785,"uuid":"1090136923","full_name":"sipeed/NanoKVM-UserApps","owner":"sipeed","description":null,"archived":false,"fork":false,"pushed_at":"2025-11-14T01:32:50.000Z","size":67,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-14T03:24:54.121Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sipeed.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-05T09:16:04.000Z","updated_at":"2025-11-14T01:32:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sipeed/NanoKVM-UserApps","commit_stats":null,"previous_names":["sipeed/nanokvm-userapps"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sipeed/NanoKVM-UserApps","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sipeed%2FNanoKVM-UserApps","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sipeed%2FNanoKVM-UserApps/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sipeed%2FNanoKVM-UserApps/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sipeed%2FNanoKVM-UserApps/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sipeed","download_url":"https://codeload.github.com/sipeed/NanoKVM-UserApps/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sipeed%2FNanoKVM-UserApps/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34674702,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-11-10T05:06:43.735Z","updated_at":"2026-06-23T03:32:16.735Z","avatar_url":"https://github.com/sipeed.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NanoKVM-UserApps\n\nNanoKVM-Desk is an IP-KVM product developed by Sipeed, featuring an AX630 as its core (dual-core \u003cA53@1.2GHz\u003e, built-in 3TOPS NPU), configured with 1GB LPDDR4 memory and 32GB eMMC, while supporting TF card expansion, and optional WiFi and POE configurations. In addition to powerful remote control functions, it has a 1.47-inch touch display and rotary knob, offering infinite DIY possibilities as a desktop accessory.\n\nThis repository is an open-source UserApp repository. Users can download all applications from here using the \"APP Hub\" feature. If you have any ideas, you can refer to the documentation below to build your own applications. This repository welcomes your contributions as well. After our basic functionality review, your developed applications can be downloaded and used by all NanoKVM-Desk users.\n\n## How to Build Your Own Application\n\n\u003e You can send this document to an AI to assist in generating your own application!\n\n### Project Folder Introduction\n\nNanoKVM-Desk UserAPP scans all folders in the `/userapp` directory, with each folder representing an app. The folder name serves as the app name. Each folder must contain at least `main.py` and `app.toml`.\n\n`main.py` is the executable code, and `app.toml` is the configuration file with the following content:\n\n```toml\n[application]\nname = \"XXX\"                        # Use folder name, displayed on startup (required and must match directory name)\nversion = \"1.0.0\"                   # Used for version upgrade, displayed on startup (required for checking updates, must be SemVer subset MAJOR.MINOR.PATCH format)\ndescriptions = \"Example\"            # Short app description, displayed during download/update (required for users to quickly understand app functionality)\n\n[author]\nname = \"Sipeed-xxx\"                 # Fill in author name, displayed on startup (required)\nemail = \"xxx@sipeed.com\"            # Facilitates user contact with author (optional)\n\n[interaction]\nrequires_user_input = false         # Whether to require access to touch screen and rotary events; if true, program must have explicit exit mechanism (optional)\n```\n\n### Screen Information and Usage\n\nThe NanoKVM-Desk screen has a resolution of 320x172 and is accessible via `/dev/fb0`. The device features a 172x320 pixel RGB565 color display, accessible via the framebuffer device `/dev/fb0`. Applications can draw directly to this display using the framebuffer interface.\n\n#### Display Characteristics\n\n- **Resolution**: 172x320 pixels (but logical screen is 320x172 - see rotation below)\n- **Color Depth**: 16-bit RGB565 format (5 bits red, 6 bits green, 5 bits blue)\n- **Framebuffer Device**: `/dev/fb0`\n- **Display Orientation**: The physical display is in portrait mode, but applications typically create landscape images (320x172) and rotate them 90 degrees counterclockwise for display.\n\n#### Basic Display Usage\n\nTo use the display in your Python application:\n\n1. **Set up constants** for the physical display dimensions:\n\n   ```python\n   PHYSICAL_WIDTH = 172\n   PHYSICAL_HEIGHT = 320\n   BPP = 16  # Bits per pixel\n   ```\n\n2. **Create a display class** that interfaces with the framebuffer:\n\n   ```python\n   import mmap\n   import os\n   import numpy as np\n   from PIL import Image, ImageDraw\n\n   class RGB565Display:\n       def __init__(self, fb_device=\"/dev/fb0\"):\n           self.physical_width = PHYSICAL_WIDTH\n           self.physical_height = PHYSICAL_HEIGHT\n           self.bpp = BPP\n           self.fb_size = self.physical_width * self.physical_height * (self.bpp // 8)\n\n           # Open framebuffer device\n           self.fb_fd = os.open(fb_device, os.O_RDWR)\n           self.fb_mmap = mmap.mmap(\n               self.fb_fd, self.fb_size, mmap.MAP_SHARED, mmap.PROT_WRITE\n           )\n           self.fb_array = np.frombuffer(self.fb_mmap, dtype=np.uint16).reshape(\n               (self.physical_height, self.physical_width)\n           )\n\n       def rgb_to_rgb565(self, r, g, b):\n           \"\"\"Convert 8-bit RGB to RGB565 format\"\"\"\n           return ((r \u0026 0xF8) \u003c\u003c 8) | ((g \u0026 0xFC) \u003c\u003c 3) | (b \u003e\u003e 3)\n\n       def clear_screen(self, color=0x0000):\n           \"\"\"Clear screen with specified color\"\"\"\n           self.fb_array.fill(color)\n\n       def _display_image(self, logical_img):\n           \"\"\"Rotate logical image and display on physical screen\"\"\"\n           # Rotate logical image 90 degrees counterclockwise to get physical image\n           physical_img = logical_img.rotate(90, expand=True)\n\n           # Convert to RGB565 and copy to framebuffer\n           rgb_array = np.array(physical_img)\n           r = (rgb_array[:, :, 0] \u003e\u003e 3).astype(np.uint16)\n           g = (rgb_array[:, :, 1] \u003e\u003e 2).astype(np.uint16)\n           b = (rgb_array[:, :, 2] \u003e\u003e 3).astype(np.uint16)\n           rgb565 = (r \u003c\u003c 11) | (g \u003c\u003c 5) | b\n\n           # Directly copy entire array to framebuffer\n           self.fb_array[:, :] = rgb565\n\n       def close(self):\n           \"\"\"Close resources\"\"\"\n           self.fb_mmap.close()\n           os.close(self.fb_fd)\n   ```\n\n3. **Draw content** to the display:\n\n   ```python\n   def main():\n       display = RGB565Display()\n       \n       try:\n           # Create a logical landscape image (320x172)\n           logical_img = Image.new(\"RGB\", (320, 172), (0, 0, 0))\n           draw = ImageDraw.Draw(logical_img)\n\n           # Draw your content (e.g., rectangles, text)\n           draw.rectangle([10, 10, 100, 100], fill=(255, 0, 0))  # Red rectangle\n           \n           # Display the image\n           display._display_image(logical_img)\n           \n           # Wait for some time\n           import time\n           time.sleep(5)\n           \n       finally:\n           display.close()\n\n   if __name__ == \"__main__\":\n       main()\n   ```\n\n#### Best Practices for Display Usage\n\n- Always rotate logical landscape images (320x172) counterclockwise to match the physical portrait display (172x320)\n- Use efficient drawing methods when possible to minimize rendering time\n- Close resources properly in a `finally` block or context manager to prevent resource leaks\n- Consider performance when drawing frequently updated content (e.g., animations)\n\n#### Input Events Information and Usage\n\nNanoKVM-Desk has three types of input events: rotary rotation, rotary press, and touch.\n\n\u003e When using input events, you need to declare `requires_user_input = true` in `app.toml`, and your program must have an explicit exit mechanism, otherwise you cannot exit to NanoKVM-UI;\n\u003e If your program doesn't need touch or rotary input events, configure the field as `requires_user_input = false` or omit it, and NanoKVM-UI will exit the program when the screen is touched or the button is pressed.\n\n##### Input Devices Description\n\nThis system uses three input devices: rotary rotation events, rotary press/hold/release events, and a touch screen. The `/dev/input/eventN` numbering is dynamic and may change between boots due to device enumeration order; therefore you must not rely on fixed event numbers. Use the kernel-registered device name under sysfs (`/sys/class/input/eventN/device/name`) to reliably identify devices.\n\n##### Typical device mapping (example)\n\n- Rotary rotation (relative): driver name `rotary@0`, reports EV_REL / REL_X, used for incremental adjustments, paging or focus movement.\n- Rotary button (press/hold/release): driver name `gpio_keys`, reports EV_KEY (KEY_ENTER), supports press/release and key repeat for long press.\n- Touchscreen: driver name `hyn_ts`, reports multitouch events (EV_ABS, ABS_MT_*), including coordinates, pressure and tracking id.\n\n##### Behavior notes\n\n- Do not hard-code `/dev/input/event0` style paths; resolve the mapping at runtime by inspecting sysfs.\n- If a device name is not found, returning the original name helps diagnostics (indicates device not ready or different name).\n- Accessing `/dev/input` devices usually requires root privileges or proper udev rules to grant access.\n\n##### Complete Python example\n\n```python\nimport os\nimport re\nfrom typing import Dict\n\nclass InputDeviceFinder:\n    \"\"\"Scan /sys/class/input, build eventN -\u003e device name map, and resolve devices by name to /dev/input/eventN.\"\"\"\n\n    def __init__(self, input_root: str = \"/sys/class/input\") -\u003e None:\n        self.input_root = input_root\n        self.event_regex = re.compile(r\"event(\\d+)$\")\n        self.devices = self._get_event_device_names()\n\n    def _get_event_device_names(self) -\u003e Dict[int, str]:\n        \"\"\"Return a mapping { event_num: device_name } by scanning input_root.\"\"\"\n        event_map: Dict[int, str] = {}\n\n        try:\n            for entry in os.scandir(self.input_root):\n                if not entry.is_dir():\n                    continue\n\n                m = self.event_regex.match(entry.name)\n                if not m:\n                    continue\n\n                try:\n                    event_num = int(m.group(1))\n                except ValueError:\n                    continue\n\n                name_path = os.path.join(entry.path, \"device\", \"name\")\n                if not os.path.exists(name_path):\n                    continue\n\n                try:\n                    with open(name_path, \"r\", encoding=\"utf-8\") as f:\n                        name = f.readline().strip()\n                        if name:\n                            event_map[event_num] = name\n                except Exception:\n                    continue\n        except FileNotFoundError:\n            pass\n\n        return event_map\n\n    def find_devices(self, targets: Dict[str, str]) -\u003e Dict[str, str]:\n        \"\"\"Resolve device names to /dev/input/eventN paths.\n\n        @param targets: e.g. {\"rotary\": \"rotary@0\", \"key\": \"gpio_keys\"}\n        @return: e.g. {\"rotary\": \"/dev/input/event2\", ...}\n                 If not found, the value will be the original name for troubleshooting.\n        \"\"\"\n        result: Dict[str, str] = {}\n\n        for role, name in targets.items():\n            found = False\n            for n, dev_name in self.devices.items():\n                if dev_name == name:\n                    result[role] = f\"/dev/input/event{n}\"\n                    found = True\n                    break\n            if not found:\n                result[role] = name\n\n        return result\n\n\nif __name__ == \"__main__\":\n    finder = InputDeviceFinder()\n    devices = finder.find_devices({\n        \"rotary\": \"rotary@0\",\n        \"key\": \"gpio_keys\",\n        \"touch\": \"hyn_ts\",\n    })\n\n    print(\"Detected devices:\", devices)\n    # Example output: Detected devices: {'rotary': '/dev/input/event0', 'key': '/dev/input/event1', 'touch': '/dev/input/event2'}\n```\n\n##### Additional recommendations\n\n- For increased robustness, when name lookup fails, try parsing `/proc/bus/input/devices` or use `udevadm`/`libinput` to obtain richer device metadata.\n- In user-space programs, open `/dev/input/eventX` via `evdev`/`libinput` libraries to read events. For services, consider caching the mapping for a short time and re-scan on device changes.\n\n#### Automatic Loading of Third-Party Python Libraries\n\nThis approach is suitable when:\n\n- You want your application to load dependencies only when they are needed.\n- You prefer not to pre-package many libraries in your system image or firmware.\n- You want the application to run out-of-the-box without requiring users to install dependencies manually.\n\nThe following class can be used directly:\n\n```python\nimport importlib\nimport subprocess\nimport sys\n\nclass AutoImport:\n    @staticmethod\n    def import_package(pip_name: str, import_name: str | None = None):\n        import_name = import_name or pip_name\n\n        try:\n            package = importlib.import_module(import_name)\n            print(f\"Package '{import_name}' imported successfully.\")\n            return package\n        except ImportError:\n            print(f\"Package '{import_name}' not found. Trying to install '{pip_name}'...\")\n            AutoImport.install_package(pip_name)\n\n            package = importlib.import_module(import_name)\n            print(f\"Package '{import_name}' imported successfully after installation.\")\n            return package\n\n    @staticmethod\n    def install_package(pip_name: str):\n        try:\n            subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pip_name])\n            print(f\"Package '{pip_name}' installed successfully.\")\n        except subprocess.CalledProcessError:\n            print(f\"Failed to install '{pip_name}'. Check network or permissions.\")\n```\n\nExample: Load `evdev` on demand for handling input events:\n\n```python\nevdev = AutoImport.import_package(\"evdev\")\nInputDevice = evdev.InputDevice\necodes = evdev.ecodes\n```\n\nIf `evdev` is already installed, it will be imported directly. Otherwise, it will be installed automatically before continuing.\n\n## Contributing to the Software Repository\n\nWe encourage the community to create and upload their own applications to this repository! This serves as the software source for NanoKVM-Desk, and your contributions make our ecosystem richer.\n\n### How to Upload Your Application\n\n1. Create a pull request with your application in the `apps` folder\n2. Your application will go through a simple review process (as an open source community, we only review basic functionality; security is the responsibility of the developer)\n3. Once approved, your application will be available in the NanoKVM-Desk APP Hub\n\n### How to Report UserAPP Issues\n\nPlease report issues in the issues section of this repository, and @ the author specified in the app.toml file of the corresponding app.\n\n### Examples\n\nSeveral examples in the `apps` directory may help you better build your own applications:\n\n- `hello`: Basic display functionality\n- `drawo`: Drawing application with touch screen support\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsipeed%2Fnanokvm-userapps","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsipeed%2Fnanokvm-userapps","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsipeed%2Fnanokvm-userapps/lists"}