mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-10 09:59:15 -06:00
Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc072ab158 | ||
|
|
b0a01a9837 | ||
|
|
2d575b916c | ||
|
|
1f908ef181 | ||
|
|
fb2adf13ee | ||
|
|
ad9eb44ad8 | ||
|
|
b4962e7a58 | ||
|
|
14614e5e35 | ||
|
|
9a93ab24f5 | ||
|
|
aaad552633 | ||
|
|
79cc1cc2bf | ||
|
|
01bcda6886 | ||
|
|
ec49bffe06 | ||
|
|
2db5d86a3c | ||
|
|
da6d4840f8 | ||
|
|
9afa21a0b0 | ||
|
|
6168069301 | ||
|
|
ffe71ae6b1 | ||
|
|
a6a97df4e7 | ||
|
|
f05467f45c | ||
|
|
969e3a5e6a | ||
|
|
f0e7dc069c | ||
|
|
f5f70bd015 | ||
|
|
aa80e1d7cc | ||
|
|
d6b03a345b | ||
|
|
c0cfcfa31a | ||
|
|
afdbb179ad | ||
|
|
ed9b7c5126 | ||
|
|
551329a41d | ||
|
|
b3c6b6c53d | ||
|
|
5a9cd18f94 | ||
|
|
63c8fc8eb5 | ||
|
|
3305c69e2d | ||
|
|
56a0468a53 | ||
|
|
52416dfdd7 | ||
|
|
bcd0b717ee | ||
|
|
d9c9eae4f9 | ||
|
|
c1a21c2d69 | ||
|
|
8d822948cf | ||
|
|
7404a205a5 | ||
|
|
d774bf8c58 | ||
|
|
0e1fae8751 | ||
|
|
3ff64483df | ||
|
|
fed92254d2 | ||
|
|
dec1621850 | ||
|
|
116bb50364 | ||
|
|
957d86d64c | ||
|
|
1fa6f48d4e | ||
|
|
0610f6fd1a | ||
|
|
ade9ac863f | ||
|
|
61ff3c5b23 | ||
|
|
19076b2508 | ||
|
|
e175147674 | ||
|
|
2cc64d2208 | ||
|
|
346315b4bb | ||
|
|
648c9f866e | ||
|
|
b5866088fc | ||
|
|
cd954b6f24 | ||
|
|
f27102d344 | ||
|
|
ba8e8b7a5c | ||
|
|
cb3834bb71 | ||
|
|
8ec134c182 | ||
|
|
631d944d70 | ||
|
|
f539576a49 | ||
|
|
fa11347f1b | ||
|
|
34c66dbd8b | ||
|
|
3d5c14405c | ||
|
|
9e4408409f | ||
|
|
9b629c8dc0 | ||
|
|
8da7a70bfc | ||
|
|
ea6ac3c903 | ||
|
|
6b783677e5 | ||
|
|
1cc31c30a3 | ||
|
|
92d5f63679 | ||
|
|
9fd3728aee | ||
|
|
916803d0f0 | ||
|
|
8de298d245 | ||
|
|
75fc8d09ce | ||
|
|
b157b9fe28 | ||
|
|
53a800d252 | ||
|
|
124868e093 | ||
|
|
e684936d15 | ||
|
|
5958fd5c6f | ||
|
|
4d7ee39ccf | ||
|
|
1bb91518b6 | ||
|
|
2d0123cb18 | ||
|
|
455bc19fab | ||
|
|
0003be478e | ||
|
|
d448f11861 | ||
|
|
53924dc8ef | ||
|
|
9311b898f2 | ||
|
|
0e30a26db4 | ||
|
|
24a460e64a | ||
|
|
f62320f3e1 | ||
|
|
0d73e8b52a | ||
|
|
11d66da064 | ||
|
|
f24fecf01c | ||
|
|
1c16444614 | ||
|
|
8b7b1236ba | ||
|
|
c44bf06cc9 | ||
|
|
8230d59354 | ||
|
|
0f270c8823 | ||
|
|
95970f87fa | ||
|
|
469c1693bd | ||
|
|
2a45a8a8fa | ||
|
|
2180fa70f7 | ||
|
|
b093e11aeb | ||
|
|
140812606f | ||
|
|
14239b8a6f | ||
|
|
0d1dca3e0a | ||
|
|
dfee0934f0 | ||
|
|
42c332a406 | ||
|
|
4c1ebc84e9 | ||
|
|
81a6e5d152 | ||
|
|
145a43d688 | ||
|
|
386c5fa86c | ||
|
|
1499edd83c | ||
|
|
db719c32ad | ||
|
|
340477a31f | ||
|
|
fd57047e86 | ||
|
|
496b9f981e | ||
|
|
76dc2312b4 | ||
|
|
ade6d80f10 | ||
|
|
574b41a0a1 | ||
|
|
f907a340b6 | ||
|
|
0498b39f0e | ||
|
|
5d196ff8d8 | ||
|
|
84d65fb167 | ||
|
|
0f22d53e7c | ||
|
|
f44bc72201 | ||
|
|
310d550ff6 | ||
|
|
044292ccb4 | ||
|
|
0dc0f9ea73 | ||
|
|
5fcdf888b3 | ||
|
|
dda98e2a61 | ||
|
|
f209cb1e0e | ||
|
|
7d3df10915 | ||
|
|
fb9f127289 | ||
|
|
bc64084516 | ||
|
|
e86d385521 | ||
|
|
6ca0fa7626 | ||
|
|
f8d8b04de3 | ||
|
|
305a75d1a7 | ||
|
|
c3a9e9d402 | ||
|
|
48c941fb86 | ||
|
|
1016853d25 | ||
|
|
90068a3c2d | ||
|
|
8fc71a2e36 | ||
|
|
8310e43b31 | ||
|
|
0e30b62894 | ||
|
|
3de21f295b | ||
|
|
d8c4de9a01 | ||
|
|
acf366d61e | ||
|
|
b9c85fad7b | ||
|
|
196b9555ab | ||
|
|
e9cccd58ea | ||
|
|
00d30efa45 | ||
|
|
309b9665cb | ||
|
|
759abc92c9 | ||
|
|
ae8f0e2236 | ||
|
|
815770fabf | ||
|
|
e82069dea2 | ||
|
|
396aee7d0a | ||
|
|
3299c202b0 | ||
|
|
755cbe113d | ||
|
|
bab729ed18 | ||
|
|
350dbdfe4b | ||
|
|
1b5ca8eef6 | ||
|
|
e8016c1d51 | ||
|
|
c1e2bed94e | ||
|
|
ba242be427 | ||
|
|
fce974c146 | ||
|
|
a82aa792d8 | ||
|
|
105f77d9ec | ||
|
|
b6d87b9799 | ||
|
|
6279086b5a | ||
|
|
61ea4bd385 | ||
|
|
7f3e039d1b | ||
|
|
f2c91c09c7 | ||
|
|
32c81f220d | ||
|
|
c77981cc0c | ||
|
|
cd767d16df | ||
|
|
8591aecb9c | ||
|
|
34746afea3 | ||
|
|
393328eaf6 | ||
|
|
7771d1f326 | ||
|
|
2a34eb55e6 | ||
|
|
9d6cf49c3f | ||
|
|
dcb87d0d89 | ||
|
|
9a0c77bb8b | ||
|
|
6e45e6b135 | ||
|
|
a1df6376ec | ||
|
|
7f7e31bae9 | ||
|
|
4499f9ed44 | ||
|
|
9a5ad1dff2 | ||
|
|
2f8f77cb89 | ||
|
|
a4978d93d6 | ||
|
|
01e28f10ae | ||
|
|
ea9068fd0d | ||
|
|
d85823abcf | ||
|
|
4c0027ffd3 | ||
|
|
afb868af30 | ||
|
|
66b1d5dc30 | ||
|
|
7d8cdc74c6 | ||
|
|
809581df49 | ||
|
|
5d47a8d48d | ||
|
|
6242f1931e | ||
|
|
b768219c18 | ||
|
|
6935923bd1 | ||
|
|
559e6e2556 | ||
|
|
f354fdfbf4 | ||
|
|
f242b5a378 | ||
|
|
e18ec2219d | ||
|
|
d075ae58d9 | ||
|
|
e8ccac8e61 | ||
|
|
7c8f9f3cd8 | ||
|
|
66e434db24 | ||
|
|
0be553c4c8 | ||
|
|
1dda0f29be | ||
|
|
fbdedc7d8a | ||
|
|
e325352309 | ||
|
|
bf51b50346 | ||
|
|
96b52f3ac0 | ||
|
|
ad44db254b | ||
|
|
736b89d7d1 | ||
|
|
3122531cae | ||
|
|
cbb47ab735 | ||
|
|
fa8d4fd5ca | ||
|
|
423d05a087 | ||
|
|
7036e94dab | ||
|
|
3a19180a51 | ||
|
|
f8f8920fc5 | ||
|
|
507b53c10e | ||
|
|
642a00051e | ||
|
|
7f9bd1c457 | ||
|
|
c4e8d19649 | ||
|
|
82be18c469 |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
Dockerfile
|
||||
!server/go.mod
|
||||
server/go.sum
|
||||
server/NanoKVM-Server
|
||||
web
|
||||
|
||||
support/sg2002/kvm_system/build
|
||||
support/sg2002/kvm_system/dist
|
||||
support/sg2002/kvm_system/CMakeLists.txt
|
||||
|
||||
support/sg2002/kvm_vision_test/build
|
||||
support/sg2002/kvm_vision_test/dist
|
||||
support/sg2002/kvm_vision_test/CMakeLists.txt
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
@@ -1,3 +1,107 @@
|
||||
## 2.3.1 (2025-12-26)
|
||||
|
||||
### Features
|
||||
|
||||
* Added support for offline application updates (thanks to [@Alexander-Ger-Reich](https://github.com/Alexander-Ger-Reich))
|
||||
* Implemented support for uploading ISO images (thanks to [@Alexander-Ger-Reich](https://github.com/Alexander-Ger-Reich))
|
||||
* Extended clipboard compatibility to support Russian characters (thanks to [@pekishev](https://github.com/pekishev))
|
||||
* Added configuration options for video scaling
|
||||
* Added support for configuring mouse scroll wheel direction
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Resolved an issue where custom port may not be accessible
|
||||
* Fixed a bug where Web Terminal and Serial Terminal may not disconnect when the page is closed
|
||||
|
||||
### Security
|
||||
|
||||
* Enhanced validation logic on the Wi-Fi configuration page. Requests are now rejected when the device is not in AP mode to prevent unauthorized changes
|
||||
|
||||
### Chore
|
||||
|
||||
* Added a Docker image environment to support building `libkvm.so` and `NanoKVM-Server` (thanks to [@lowtech-guy](https://github.com/lowtech-guy))
|
||||
* Optimized the Settings UI
|
||||
|
||||
### Localization
|
||||
|
||||
* Updated Korean translation (thanks to [@kmw0410](https://github.com/kmw0410))
|
||||
* Updated Swedish translation (thanks to [@acidflash](https://github.com/acidflash))
|
||||
* Updated Spanish translation (thanks to [@Deses](https://github.com/Deses))
|
||||
|
||||
## 2.3.0 [0e30a26](https://github.com/sipeed/NanoKVM/commit/0e30a26db42cbc08416662b49a88a5d16ad93424) (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* Added an EDID editor in the terminal with a built-in 1080P EDID template
|
||||
* Added German virtual keyboard and German clipboard support (thanks to [@Alexander-Ger-Reich](https://github.com/Alexander-Ger-Reich))
|
||||
* Resetting the HID also resets the USB hardware
|
||||
* Added support for deleting ISO images
|
||||
|
||||
### Refactoring & Improvements
|
||||
|
||||
* Video (H.264 WebRTC): Refactored the module to significantly reduce video latency
|
||||
* Video (H.264 Direct): Refactored to optimize data transmission and support data parsing even when the page is in the background
|
||||
* Video (MJPEG): Refactored and ensure correct data length transmission
|
||||
* Added locking before resetting USB to prevent HID deadlock (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Optimized HID writing logic
|
||||
* Made HDMI toggle state persist across reboots (thanks to [@tpretz](https://github.com/tpretz))
|
||||
* Enhanced the paste feature with browser clipboard API integration (thanks to [@patrickpilon](https://github.com/patrickpilon))
|
||||
* Set the inquiry string for the virtual storage device (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Hostname now updates `/etc/hosts` simultaneously and takes effect immediately (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Improved compatibility for static IP configuration on Windows
|
||||
* Optimized the web page title update logic
|
||||
* Added a confirmation dialog when uninstalling Tailscale
|
||||
* Improved UI for Clipboard, Image Mounting, and settings pages
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Tailscale: Fixed an issue where Tailscale would turn the device into a router
|
||||
* Tailscale: Fixed an issue where Tailscale would disable IPv6
|
||||
* Tailscale: Added a swap memory option to prevent Tailscale Out-Of-Memory errors
|
||||
* Tailscale: Fixed Tailscale accept-dns configuration (thanks to [@lazydba247](https://github.com/lazydba247))
|
||||
* Fixed an issue where certain modifier keys were not recognized
|
||||
* Fixed vertical mouse drift when the page is zoomed in or out
|
||||
|
||||
### Localization
|
||||
|
||||
* Update Korean translation (thanks to [@xenix4845](https://github.com/xenix4845))
|
||||
* Update Traditional Chinese translation (thanks to [@protonchang](https://github.com/protonchang))
|
||||
* Add Brazilian Portuguese translation (thanks to [@chiconws](https://github.com/chiconws) and [@Luccas-LF](https://github.com/Luccas-LF))
|
||||
* Add Swedish translation (thanks to [@acidflash](https://github.com/acidflash))
|
||||
* Add Catalan translation (thanks to [@Zagur](https://github.com/Zagur))
|
||||
* Add Turkish translation (thanks to [@Keylem](https://github.com/Keylem))
|
||||
|
||||
### Security
|
||||
|
||||
* Implemented a delay after failed login attempts to mitigate brute-force attacks
|
||||
* Upgraded dependencies to fix known security vulnerabilities
|
||||
|
||||
## 2.2.9 [c77981c](https://github.com/sipeed/NanoKVM/commit/c77981cc0ceebd8f6705b6c5d8c3cf4edf4f6717) (2025-06-13)
|
||||
|
||||
* fix(security): resolve parameter injection in serial port terminal
|
||||
|
||||
## 2.2.8 [01e28f1](https://github.com/sipeed/NanoKVM/commit/01e28f10ae8b581d484bb6077ddfe7bbe4e57919) (2025-05-22)
|
||||
|
||||
* feat: add AZERTY virtual keyboard Layout (thanks to [@felix068](https://github.com/felix068))
|
||||
* feat: add support for enabling/disabling HDMI output (PCIe version only) (thanks to [@tpretz](https://github.com/tpretz))
|
||||
* feat: add support for custom mouse wheel speed
|
||||
* fix: prevent direct H.264 stream buffer overflow and replay issues
|
||||
* perf: improve keyboard paste performance (thanks to [@ethanperrine](https://github.com/ethanperrine))
|
||||
* Localization
|
||||
* update Korean translation (thanks to [@kmw0410](https://github.com/kmw0410))
|
||||
* update Ukrainian translation (thanks to [@arbdevml](https://github.com/arbdevml))
|
||||
* update Russian translation (thanks to [@arbdevml](https://github.com/arbdevml))
|
||||
|
||||
## 2.2.7 [e18ec22](https://github.com/sipeed/NanoKVM/commit/e18ec2219d22886529575d1fdaad5c320e05f5b2) (2025-05-08)
|
||||
|
||||
* feat: add HTTPS support
|
||||
* feat: support direct H.264 streaming over HTTP
|
||||
* Localization
|
||||
* update Russian translation (thanks to [polyzium](https://github.com/polyzium))
|
||||
* update Dutch translation (thanks to [LeonStraathof](https://github.com/LeonStraathof))
|
||||
* update Ukrainian translation (thanks to [click0](https://github.com/click0))
|
||||
* update German translation (thanks to [3limin4tor](https://github.com/3limin4tor))
|
||||
|
||||
## 2.2.6 [c83dc55](https://github.com/sipeed/NanoKVM/commit/c83dc5565c9dbed22336661a8832edbd93a06d11) (2025-04-17)
|
||||
|
||||
* feat: add mouse jiggler to prevent system sleep
|
||||
@@ -7,6 +111,7 @@
|
||||
* feat: add support for assigning custom names to Wake-on-LAN MAC addresses
|
||||
* feat: add confirmation prompts for power operations
|
||||
* feat: add logo to the login page (thanks to [S33G](https://github.com/S33G))
|
||||
* fix: fix possible privacy issues with MIC drivers. [Repair Record](https://github.com/sipeed/NanoKVM/commit/f9244b36df090a05cd59ba11ea4fd01e9b638995)
|
||||
* fix: fix iptables rule that could interfere with SSH connections (thanks to [scpcom](https://github.com/scpcom))
|
||||
* fix: fix the static IP gateway configuration might not apply correctly (thanks to [xitation](https://github.com/xitation))
|
||||
* perf: optimized OLED display handling and sleep logic
|
||||
|
||||
97
Makefile
Normal file
97
Makefile
Normal file
@@ -0,0 +1,97 @@
|
||||
# Makefile for NanoKVM Project
|
||||
|
||||
# Configuration
|
||||
IMAGE_NAME := nanokvm-builder
|
||||
UID := $(shell id -u)
|
||||
GID := $(shell id -g)
|
||||
PWD := $(shell pwd)
|
||||
|
||||
# Docker run common parameters
|
||||
DOCKER_RUN_BASE := docker run -e UID=$(UID) -e GID=$(GID) -v $(PWD):/home/build/NanoKVM --rm
|
||||
|
||||
# Build commands
|
||||
GO_BUILD_CMD := cd /home/build/NanoKVM/server && go mod tidy && CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build
|
||||
SUPPORT_BUILD_CMD := . ./home/build/MaixCDK/bin/activate && cd /home/build/NanoKVM/support/sg2002 && ./build kvm_system && ./build kvm_system add_to_kvmapp
|
||||
|
||||
.PHONY: help check-root builder-image rebuild-image check-image shell app support all clean
|
||||
|
||||
# Default target
|
||||
all: app support
|
||||
|
||||
# Help target
|
||||
help:
|
||||
@echo "NanoKVM Build System"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " help - Show this help message"
|
||||
@echo " check-image - Check builder Docker image and show versions"
|
||||
@echo " builder-image - Build Docker image if not exists"
|
||||
@echo " rebuild-image - Force rebuild Docker image"
|
||||
@echo " shell - Enter interactive builder environment"
|
||||
@echo " app - Build Go application server"
|
||||
@echo " support - Build hardware support libraries"
|
||||
@echo " all - Build both app and support (default)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@echo "Prerequisites:"
|
||||
@echo " - Docker must be installed and running"
|
||||
@echo " - Must not run as root user"
|
||||
|
||||
# Security check - prevent running as root
|
||||
check-root:
|
||||
@if [ "$$(id -u)" -eq 0 ]; then \
|
||||
echo "Can't run as root"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Check if builder image exists and show versions
|
||||
check-image: check-root
|
||||
@echo "Checking builder image..."
|
||||
@echo "Golang version: " && \
|
||||
docker run --rm -i $(IMAGE_NAME) go version && \
|
||||
echo "" && \
|
||||
echo "Host-tools version:" && \
|
||||
docker run --rm -i $(IMAGE_NAME) riscv64-unknown-linux-musl-gcc -v && \
|
||||
echo ""
|
||||
|
||||
# Build Docker image if it doesn't exist
|
||||
builder-image: check-root
|
||||
@if ! docker image inspect $(IMAGE_NAME) >/dev/null 2>&1; then \
|
||||
echo "Building Docker image..."; \
|
||||
docker build -t $(IMAGE_NAME) -f docker/Dockerfile ./; \
|
||||
else \
|
||||
echo "Docker image $(IMAGE_NAME) already exists."; \
|
||||
fi
|
||||
|
||||
# Force rebuild Docker image
|
||||
rebuild-image: check-root
|
||||
@echo "Force rebuilding Docker image..."
|
||||
@docker build --no-cache -t $(IMAGE_NAME) -f docker/Dockerfile ./
|
||||
|
||||
# Enter interactive shell (equivalent to build.sh with no arguments)
|
||||
shell: check-root builder-image
|
||||
@echo "Switching into builder..."
|
||||
@$(DOCKER_RUN_BASE) -it $(IMAGE_NAME) /bin/bash -c ". ./home/build/MaixCDK/bin/activate && cd /home/build/NanoKVM ; exec bash"
|
||||
|
||||
# Build Go application
|
||||
app: check-root builder-image
|
||||
@echo "Building app..."
|
||||
@$(DOCKER_RUN_BASE) -it $(IMAGE_NAME) /bin/bash -c '$(GO_BUILD_CMD)'
|
||||
|
||||
# Build hardware support libraries
|
||||
support: check-root builder-image
|
||||
@echo "Building support..."
|
||||
@$(DOCKER_RUN_BASE) -it $(IMAGE_NAME) /bin/bash -c '$(SUPPORT_BUILD_CMD)'
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@if [ -f server/NanoKVM-Server ]; then \
|
||||
rm -f server/NanoKVM-Server; \
|
||||
echo "Removed server/NanoKVM-Server"; \
|
||||
fi
|
||||
@if [ -d support/sg2002/build ]; then \
|
||||
rm -rf support/sg2002/build; \
|
||||
echo "Removed support/sg2002/build"; \
|
||||
fi
|
||||
@echo "Clean completed."
|
||||
194
README.md
194
README.md
@@ -1,92 +1,90 @@
|
||||
NanoKVM
|
||||
======
|
||||
# NanoKVM
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> Quick Start </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> Hardware </a>
|
||||
</h3>
|
||||
|
||||
English | [中文](./README_ZH.md) | [日本語](./README_JA.md)
|
||||
|
||||
<br>
|
||||
<img src="https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM_3.png" alt="NanoKVM" style="margin: 20px 0;">
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html">🚀 Quick Start</a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM">🛠️ Hardware Details</a>
|
||||
|
|
||||
<a href="https://github.com/sipeed/NanoKVM/releases/latest">💾 Firmware Releases</a>
|
||||
</h3>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
> Your NanoKVM is powered by [RISC-V](https://en.wikipedia.org/wiki/RISC-V)!
|
||||
> If you have any issues or suggestions, creating issue here, or tell us with [MaixHub Discussion](https://maixhub.com/discussion/nanokvm).
|
||||
> For the recent discussion regarding the **built-in microphone**, please check [Issue #693](https://github.com/sipeed/NanoKVM/issues/693).
|
||||
|
||||
## Introduction
|
||||
## 🌟 What is NanoKVM?
|
||||
|
||||
Lichee NanoKVM is an IP-KVM product based on LicheeRV Nano, inheriting the extreme size and powerful features of LicheeRV Nano.
|
||||
NanoKVM is a series of compact, open-source IP-KVM devices based on the LicheeRV Nano (RISC-V). NanoKVM allows you to remotely access and control computers as if you were sitting right in front of them – perfect for managing servers, embedded systems, or any headless machine.
|
||||
|
||||
The Lichee NanoKVM is now available in three versions:
|
||||
## 📦 Product Family
|
||||
|
||||
* NanoKVM-Cube Lite is a basic configuration that is suitable for individual users with certain DIY capabilities and enterprise users with bulk requirements.
|
||||
We offer several NanoKVM versions to suit your needs:
|
||||
|
||||
* NanoKVM-Cube Full is a full version with a sophisticated case and complete accessories, as well as a built-in system mirror card that is ready to use at boot, and is recommended for individual users.
|
||||
|
||||
* NanoKVM-PCle is a new form with a built-in PCle baffle that can be fixed inside the chassis. NanoKVM-PCle adds optional WiFi and PoE functions based on NanoKVM-Cube; it has its own PCle slot and can draw power from the motherboard PCle slot; in addition, the wired connection (ETH) is also more stable, meeting more professional needs.
|
||||
* **NanoKVM-Cube Lite:** A barebones kit for DIY enthusiasts and enterprise users requiring bulk deployment.
|
||||
* **NanoKVM-Cube Full:** A complete package featuring a sleek case, accessories, and a pre-flashed system SD card. Ready to use out-of-the-box; ideal for individual users.
|
||||
* **NanoKVM-PCIe:** A unique form factor with a PCIe bracket for internal chassis mounting. It draws power directly from the PCIe slot and supports optional WiFi and PoE functions.
|
||||
* **[NanoKVM-Pro:](https://github.com/sipeed/NanoKVM-Pro)** Based on popular demand, the **Pro version** delivers significant performance upgrades:
|
||||
* **Resolution:** Upgraded from 1080P to stunning **4K@30fps / 2K@60fps**.
|
||||
* **Network:** Blazing fast **1Gbps Ethernet + PoE + WiFi 6** ((upgraded from 100Mbps).
|
||||
* **Latency:** Encoder accelerated latency reduced from 100-150ms to **50-100ms**.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<img src="https://cdn.sipeed.com/public/nanokvm-products.jpg" alt="NanoKVM Product Family" width="80%" style="margin: 20px 0;">
|
||||
</div>
|
||||
|
||||
> For users concerned about security, you can read this issue: [#301](https://github.com/sipeed/NanoKVM/issues/301). You will find explanations addressing all security concerns.
|
||||
> GitHub, as an open and transparent platform, provides a space for discussing so-called "backdoor" concerns. Open-source makes products more secure!
|
||||
> If you are looking for a USB-based KVM solution, check out [NanoKVM-USB](https://github.com/sipeed/NanoKVM-USB).
|
||||
|
||||
## Project Structure
|
||||
## 🛠️ Technical Specifications
|
||||
|
||||
| Feature | NanoKVM-Pro | NanoKVM (Cube/PCIe) | GxxKVM | JxxKVM |
|
||||
|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |------------------------------------ |
|
||||
| Core | AX630C 2xA53 1.2G | SG2002 1xC906 1.0G | RV1126 4xA7 1.5G | RV1106 1xA7 1.2G |
|
||||
| Memory & Storage | 1G LPDDR4X + 32G eMMC | 256M DDR3 + 32G microSD | 1G DDR3 + 8G eMMc | 256M DDR3 + 16G eMMC |
|
||||
| System | NanoKVM / PiKVM | NanoKVM | GxxKVM | JxxKVM |
|
||||
| Resolution | 4K@30fps / 2K@60fps | 1080P@60fps | 4K@30fps / 2K@60fps | 1080P@60fps |
|
||||
| HDMI Loopout | 4K loopout | x | x | x |
|
||||
| Video Encoding | MJPEG / H.264 / H.265 | MJPEG / H264 | MJPEG / H264 | MJPEG / H264 |
|
||||
| Audio Transmit | ✓ | x | ✓ | x |
|
||||
| UEFI / BIOS | ✓ | ✓ | ✓ | ✓ |
|
||||
| Emulated USB Keyboard & Mouse | ✓ | ✓ | ✓ | ✓ |
|
||||
| Emulated USB ISO | ✓ | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ | x |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ | ✓ |
|
||||
| Web Terminal | ✓ | ✓ | ✓ | ✓ |
|
||||
| Serial Terminal | 2 channels | 2 channels | x | 1 channel |
|
||||
| Custom Scripts | ✓ | ✓ | x | x |
|
||||
| Storage | 32G eMMC 300MB/s | 32G MicroSD 12MB/s | 8G eMMC 120MB/s | 8G eMMC 60MB/s |
|
||||
| Ethernet | 1000M | 100M | 1000M | 100M |
|
||||
| PoE | Optional | Optional | x | x |
|
||||
| WiFi | Optional WiFi6 | Optional WiFi6 | x | x |
|
||||
| ATX Power Control | ✓ | ✓ | Extra $15 | Extra $10 |
|
||||
| Display | 1.47" 320x172 LCD / 0.96" 128x64 OLED | 0.96" 128x64 OLED | - | 1.68" 280x240 |
|
||||
| More Features | Sync LED Strip / Smart Assistant | - | - | - |
|
||||
| Power Consumption | 0.6A@5V | 0.2A@5V | 0.4A@5V | 0.2A@5V |
|
||||
| Power Input | USB-C or PoE | USB-C | USB-C | USB-C |
|
||||
| Dimensions | 65x65x26mm | 40x36x36mm | 80x60x17.5mm | 60x43x(24~31)mm |
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP update package
|
||||
│ ├── jpg_stream # Compatible for direct updates from very old versions
|
||||
│ ├── kvm_new_app # Triggers necessary components for kvm_system update
|
||||
│ ├── kvm_system # kvm_system application
|
||||
│ ├── server # NanoKVM front-end and back-end applications
|
||||
│ └── system # Necessary system components
|
||||
├── web # NanoKVM front-end
|
||||
├── server # NanoKVM back-end
|
||||
├── support # Auxiliary functions (image subsystem, system status, system updates, screen, keys, etc.)
|
||||
│ ├── sg2002 # NanoKVM-Lite/Full/PCIe
|
||||
│ └── h618 # NanoKVM-Pro
|
||||
│ ├── jpg_stream # Legacy support for direct updates from older versions
|
||||
│ ├── kvm_new_app # Triggers components for kvm_system updates
|
||||
│ ├── kvm_system # Core KVM application
|
||||
│ ├── server # Front-end and back-end integration
|
||||
│ └── system # Essential system components
|
||||
├── web # NanoKVM Front-end (UI)
|
||||
├── server # NanoKVM Back-end (Service)
|
||||
├── support # Auxiliary modules (Image subsystem, status, updates, OLED, HID, etc.)
|
||||
├── ...
|
||||
```
|
||||
|
||||
## Tech Specs
|
||||
## 🔩 Hardware Platform (NanoKVM Cube/PCIe)
|
||||
|
||||
| Products | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |
|
||||
| Compute Units | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| Resolution | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| Video encoding | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
|
||||
| Video delay | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| Virtual HID | ✓ | ✓ | ✓ |
|
||||
| Virtual CD-ROM | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX power control | None,Users can connect by their own | USB interface IO control board | RJ-45 interface IO control board |
|
||||
| OLED | None,Users can connect by their own | 128x64 0.96" white | 128x32 0.91" white |
|
||||
| UART | 2 | 2 | 1 |
|
||||
| TF Card | None | ✓ | ✓ |
|
||||
| Expansion | None | PoE | WiFi/LTE |
|
||||
| Power consumption | 0.2A@5V | 0.2A@5V | Peak 2.6A@5V |
|
||||
| Power input | PC USB can be powered | PC USB or auxiliary power supply | DC 5V 3A power supply is required |
|
||||
| Cooling | Silent fanless | Silent fanless | Fan cooling |
|
||||
| Size | 23x37x15mm ~1/30 PiKVM V4 size | 40x36x36mm ~1/7 PiKVM V4 size | 120x68x44mm |
|
||||
|
||||

|
||||
|
||||
## Hardware platform NanoKVM
|
||||
|
||||
NanoKVM is based on Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html),you can find specifications, schematics, and dimensional drawings [here](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano).
|
||||
NanoKVM is based on Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html). You can find specifications, schematics, and dimensional drawings in the [download station](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano).
|
||||
|
||||
The NanoKVM Lite is constructed by the LicheeRV Nano and HDMItoCSI board, and the NanoKVM FULL adds the NanoKVM-A/B board and shell to the NanoKVM Lite. The HDMItoCSI board is used to convert the HDMI signal; NanoKVM-A, including OLED, ATX control output (USB Type-C interface), auxiliary power supply and ATX power on/off and reset buttons; The NanoKVM-B is connected to the plate at one end and the computer at the other end is connected to the computer ATX-Pin, which is used to remotely control the power of the computer.
|
||||
|
||||
@@ -96,60 +94,40 @@ Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the mu
|
||||
|
||||
* [NanoKVM-A Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
* [NanoKVM-B Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_B_30131.pdf)
|
||||
* [NanoKVM-PCIe Schematic](https://cn.dl.sipeed.com/fileList/KVM/KVM_PCIE/HDK/01_Schematic/SCH_nanoKVM_PCIE_3105D_2025-12-19.pdf)
|
||||
* [NanoKVM img](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## Roadmap
|
||||
<div align="center">
|
||||
<img src="https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_2.jpg" alt="NanoKVM PCB Pinout" width="80%" style="margin: 20px 0;">
|
||||
</div>
|
||||
|
||||
**Enhancements:**
|
||||
## 🤝 Contributing
|
||||
|
||||
* [x] Refactor the HDMI module and remove the libmaixcam_lib.so dependency
|
||||
* [x] Disable SSH by default and add an enable/disable toggle in the web UI
|
||||
* [x] Disable Tailscale by default and add an enable/disable toggle in the web UI
|
||||
* [x] Add JWT configuration options to the configuration file
|
||||
* [x] Secure password storage with bcrypt
|
||||
We welcome contributions! Here's how you can help:
|
||||
|
||||
**Distribution:**
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch.
|
||||
3. Commit your changes.
|
||||
4. Push to the branch.
|
||||
5. Open a Pull Request.
|
||||
|
||||
* [x] Distribute applications via GitHub
|
||||
* [x] Implement integrity checks for online updates
|
||||
* [ ] Support offline updates
|
||||
Please keep your pull requests small and focused to facilitate easier review and merging.
|
||||
|
||||
**Bug Fixes:**
|
||||
> 🎁 **Contributors who submit high-quality Pull Requests may receive a NanoKVM Cube, PCIe, or Pro as a token of our appreciation!**
|
||||
|
||||
* [x] Fix the DNS issue
|
||||
* [x] Fix the CSRF vulnerability
|
||||
|
||||
**Features**
|
||||
|
||||
* [ ] [79](https://github.com/sipeed/NanoKVM/issues/79) Support Zerotier
|
||||
* [ ] [99](https://github.com/sipeed/NanoKVM/issues/99) Support WireGuard
|
||||
* [ ] [249](https://github.com/sipeed/NanoKVM/issues/249) Add Mouse Jiggler
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
Thanks for your interest in contributing to NanoKVM!
|
||||
|
||||
Please keep your pull requests small and focused. This will make it easier to review and merge.
|
||||
|
||||
> Developers who have contributed high-quality Pull Requests will receive a NanoKVM Cube, PCIe, or Pro as a token of our appreciation.
|
||||
|
||||
## Where to buy
|
||||
## 🛒 Where to Buy
|
||||
|
||||
* [Aliexpress(global except USA&Russia)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [Preorder (anyother country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm)
|
||||
* [Preorder (any other country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm)
|
||||
|
||||
## Community
|
||||
## 💬 Community & Support
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* [Discord](https://discord.gg/V4sAZ9XWpN)
|
||||
* QQ group: 703230713
|
||||
* email: [support@sipeed.com](mailto:support@sipeed.com)
|
||||
* [FAQ](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/faq.html)
|
||||
|
||||
## License
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
|
||||
|
||||
129
README_JA.md
129
README_JA.md
@@ -1,129 +0,0 @@
|
||||
NanoKVM
|
||||
======
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> クイックスタート </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> ハードウェア </a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | [中文](./README_ZH.md) | 日本語
|
||||
|
||||
</div>
|
||||
|
||||
> NanoKVM は [RISC-V](https://en.wikipedia.org/wiki/RISC-V) を搭載しています!
|
||||
> 使用中に問題や提案がある場合は、ここで issue を提出するか、[MaixHub Discussion](https://maixhub.com/discussion/nanokvm) でお知らせください。
|
||||
|
||||
## オープンソース & コントリビューション
|
||||
|
||||
### 2025.02.19 更新
|
||||
|
||||
NanoKVM のすべてのコンポーネントはオープンソース化されており、[front-end](https://github.com/sipeed/NanoKVM/tree/main/web)、[back-end](https://github.com/sipeed/NanoKVM/tree/main/server)、[kvm_vision](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm)、[kvm_mmf](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm_mmf)、[kvm_system](https://github.com/sipeed/NanoKVM/tree/main/support)、[kvmapp update package](https://github.com/sipeed/NanoKVM-System/tree/main/kvmapp)、[system sdk](https://github.com/sipeed/LicheeRV-Nano-Build/tree/NanoKVM)、[packaging methods](https://github.com/sipeed/LicheeRV-Nano-Build/blob/NanoKVM/kvm/NanoKVM_img.sh)が含まれます。
|
||||
|
||||
### 2025.02.14 更新
|
||||
|
||||
最新のアプリケーションバージョン v2.1.6 には、多くのセキュリティ強化とバグ修正が追加されているため、一般のユーザーはこのバージョン以降へのアップグレードをお勧めします。
|
||||
|
||||
### 2025.02.05 更新
|
||||
|
||||
セキュリティを懸念しているユーザーは、この issue をお読みください: <https://github.com/sipeed/NanoKVM/issues/301>、セキュリティに関するすべての懸念事項について説明されています。
|
||||
GitHub は、オープンで透明性のあるプラットフォームとして、いわゆる「バックドア」の懸念事項を議論する場を提供しています。オープンソースであることにより、製品のセキュリティがさらに強化されます!
|
||||
|
||||
### 2024.10.18 更新
|
||||
|
||||
10/08にバックエンドコードをオープンソース化しました。すぐにPRを提出してくれたCivilに感謝します。最新のNanoKVM-PCIeを報酬として提供します!
|
||||
10月中旬のバッチはテストとパッケージングを開始しており、10/01以前のほとんどの注文は次週と次々週に出荷されます。
|
||||
10/01以降、アリエクスプレスの出荷日は12月に変更されました。これは保守的な納期です。10月の注文は11/15〜12/15の間に発送される予定です。
|
||||
|
||||
## 紹介
|
||||
|
||||
Lichee NanoKVMは、LicheeRV NanoをベースにしたIP-KVM製品で、LicheeRV Nanoの極小サイズと強力な機能を継承しています。
|
||||
Lichee NanoKVMは2つのバージョンがあります:
|
||||
NanoKVM Liteは基本構成で、一定のDIY能力を持つ個人ユーザーや大量のニーズを持つ企業ユーザーに適しています。
|
||||
NanoKVM Fullは完全版で、洗練されたケースと完全なアクセサリーを備え、起動時にすぐに使用できるシステムミラーカードが内蔵されており、個人ユーザーに推奨されます。
|
||||
|
||||

|
||||
|
||||
## 技術仕様
|
||||
|
||||
| 製品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
|----------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |
|
||||
| 計算ユニット | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 解像度 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| ビデオエンコーディング | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
|
||||
| ビデオ遅延 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 仮想HID | ✓ | ✓ | ✓ |
|
||||
| 仮想CD-ROM | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX電源制御 | なし、ユーザーが自分で接続 | USBインターフェースIO制御ボード | RJ-45インターフェースIO制御ボード |
|
||||
| OLED | なし、ユーザーが自分で接続 | 128x64 0.96" 白 | 128x32 0.91" 白 |
|
||||
| UART | 2 | 2 | 1 |
|
||||
| TFカード | なし | ✓ | ✓ |
|
||||
| 拡張 | なし | PoE | WiFi/LTE |
|
||||
| 消費電力 | 0.2A@5V | 0.2A@5V | ピーク時 2.6A@5V |
|
||||
| 電源入力 | PC USBで給電可能 | PC USBまたは補助電源 | DC 5V 3A電源が必要 |
|
||||
| 冷却 | 静音ファンレス | 静音ファンレス | ファン冷却 |
|
||||
| サイズ | 23x37x15mm ~1/30 PiKVM V4サイズ | 40x36x36mm ~1/7 PiKVM V4サイズ | 120x68x44mm |
|
||||
|
||||

|
||||
|
||||
## NanoKVM ハードウェアプラットフォーム
|
||||
|
||||
NanoKVMはSipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html)に基づいており、仕様、回路図、寸法図は[こちら](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)で確認できます。
|
||||
|
||||
NanoKVM LiteはLicheeRV NanoとHDMItoCSIボードで構成され、NanoKVM FULLはNanoKVM LiteにNanoKVM-A/Bボードとシェルを追加しています。HDMItoCSIボードはHDMI信号を変換するために使用されます。NanoKVM-AにはOLED、ATX制御出力(USB Type-Cインターフェース)、補助電源供給およびATX電源オン/オフおよびリセットボタンが含まれています。NanoKVM-Bは一端がAボードに接続され、他端がコンピュータのATXピンに接続され、コンピュータの電源をリモートで制御します。
|
||||
|
||||
NanoKVMイメージはLicheeRV Nano SDKおよびMaixCDKに基づいて構築されており、LicheeRV Nanoを使用する資料と互換性があります。逆に、KVMソフトウェアはLicheeRV Nanoや他のSG2002製品では使用できません。LicheeRV NanoやMaixCamでHDMI入力アプリケーションを構築したい場合は、技術サポートを受けるためにお問い合わせください。
|
||||
|
||||
注:SG2002の256MBメモリのうち、現在158MBがマルチメディアサブシステムに割り当てられており、NanoKVMはビデオ画像の取得と処理にこのメモリを使用します。
|
||||
|
||||
+ [NanoKVM-A 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
+ [NanoKVM-B 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf)
|
||||
+ [NanoKVM イメージダウンロード](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## ロードマップ
|
||||
|
||||
### Q1 2025
|
||||
|
||||
**改善:**
|
||||
|
||||
- [x] HDMI モジュールをリファクタリングし、libmaixcam_lib.so の依存関係を削除
|
||||
- [x] SSH をデフォルトで無効にし、Web UI に有効/無効の切り替えを追加
|
||||
- [x] Tailscale をデフォルトで無効にし、Web UI に有効/無効の切り替えを追加
|
||||
- [x] 設定ファイルに JWT 設定オプションを追加
|
||||
- [x] bcrypt を利用したパスワードストレージのセキュリティ保護
|
||||
|
||||
**配布:**
|
||||
|
||||
- [x] GitHub 経由でのアプリケーション配布
|
||||
- [x] オンラインでの更新の際に整合性チェックを実施
|
||||
- [ ] オフラインでの更新をサポート
|
||||
|
||||
**バグ修正:**
|
||||
|
||||
- [x] DNS の問題を修正
|
||||
- [x] CSRF の脆弱性を修正
|
||||
|
||||
**機能**
|
||||
|
||||
- [ ] [79](https://github.com/sipeed/NanoKVM/issues/79) Zerotier をサポート
|
||||
- [ ] [99](https://github.com/sipeed/NanoKVM/issues/99) WireGuard をサポート
|
||||
- [ ] [249](https://github.com/sipeed/NanoKVM/issues/249) Mouse Jiggler を追加
|
||||
|
||||
## 購入方法
|
||||
|
||||
* [Aliexpress(グローバル、米国とロシアを除く)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [予約注文 (淘宝とアリエクスプレスがサポートされていない国)](https://sipeed.com/nanokvm)
|
||||
|
||||
## コミュニティ
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQグループ: 703230713
|
||||
111
README_ZH.md
111
README_ZH.md
@@ -1,111 +0,0 @@
|
||||
NanoKVM
|
||||
======
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> Quick Start </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> Hardware </a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | 中文 | [日本語](./README_JA.md)
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> 如果您在使用过程中有任何问题或建议,请在这里提交 issue, 或在 [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)告诉我们.
|
||||
|
||||
## 简介
|
||||
|
||||
Lichee NanoKVM 是基于 LicheeRV Nano 的 IP-KVM 产品,继承了 LicheeRV Nano的极致体积 和 强大功能。
|
||||
|
||||
Lichee NanoKVM 目前有三个版本:
|
||||
|
||||
* NanoKVM-Cube Lite 为基础版配置,适合 具有一定DIY能力的个人用户 和 有批量需求的企业用户。
|
||||
* NanoKVM-Cube Full 为完整版配置,带精致外壳和完整配件,内置开机即用的系统镜像卡,推荐个人用户购买。
|
||||
* NanoKVM-PCle 为全新形态,内置 PCle 挡板,可固定在机箱内部。NanoKVM-PCle 在 NanoKVM-Cube 基础上增加了可选的 WiFi 和 PoE 功能;拥有自家的 PCle 插槽,可从主板 PCle 插槽取电;另外有线连接(ETH)也更加稳定,满足更多专业需求。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
> 对于关注安全的用户,可以阅读此问题 [#301](https://github.com/sipeed/NanoKVM/issues/301),您会找到有关所有安全问题的解释。GitHub 作为一个开放和透明的平台,为讨论所谓的“后门”担忧提供了空间。开源使产品更加安全!
|
||||
|
||||
## 项目结构
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP更新包
|
||||
│ ├── jpg_stream # 兼容从非常老的版本中直接更新
|
||||
│ ├── kvm_new_app # 触发 kvm_system 更新必要组件
|
||||
│ ├── kvm_system # kvm_system 应用
|
||||
│ ├── server # NanoKVM 前后端应用
|
||||
│ └── system # 必要系统组件
|
||||
├── server # NanoKVM 后端
|
||||
├── support # 辅助功能(图像子系统、系统状态、系统更新、屏幕、按键……)
|
||||
│ ├── sg2002 # NanoKVM-Lite/Full/PCIe
|
||||
│ └── h618 # NanoKVM-Pro
|
||||
├── web # NanoKVM 前端
|
||||
├── LICENSE
|
||||
├── README_JA.md
|
||||
├── README.md
|
||||
├── README_ZH.md
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 产品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
| --- | --- | --- | --- |
|
||||
| 计算单元 | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 分辨率 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| 视频编码 | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
|
||||
| 视频延迟 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 模拟USB键鼠 | ✓ | ✓ | ✓ |
|
||||
| 模拟USB存储 | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX电源控制 | 无,用户可自行连接 | USB接口IO控制板 | RJ45接口IO控制板 |
|
||||
| OLED显示 | 无,用户可自行扩展 | 128x64 0.96" white | 128x32 0.91" white |
|
||||
| 外接串口 | 2路 | 2路 | 1路 |
|
||||
| TF卡 | 无,用户自备 | 有,开机即用 | 有 |
|
||||
| 扩展配件 | 无 | PoE | WiFi/LTE |
|
||||
| 功耗 | 0.2A@5V | 0.2A@5V | Peak 2.6A@5V |
|
||||
| 电源输入 | PC USB即可供电 | PC USB即可供电 <br> 也支持额外辅助供电 | 需要DC 5V 3A供电 |
|
||||
| 散热 | 静音无风扇 | 静音无风扇 | 需要风扇主动散热 |
|
||||
| 尺寸 | 23x37x15mm <br> ~1/30 PiKVM V4 体积 | 40x36x36mm <br/> ~1/7 PiKVM V4 体积 | 120x68x44mm |
|
||||
|
||||

|
||||
|
||||
## NanoKVM 硬件平台
|
||||
|
||||
NanoKVM 基于 Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html) 核心板搭建,这部分硬件的规格书、原理图、尺寸图等均可在这里找到:[点击这里](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)
|
||||
|
||||
NanoKVM Lite 由 LicheeRV Nano E 和 HDMItoCSI 小板构成,NanoKVM FULL 在 NanoKVM Lite 基础上增加 NanoKVM-A/B 板和外壳。HDMItoCSI板用于转换HDMI信号;NanoKVM-A 包含 OLED、ATX控制输出(TypeC接口形式)、辅助供电(TypeC接口)以及ATX开关机、复位按键;NanoKVM-B 一端连接A板,一端连接电脑ATX针脚,用于电脑的远程开关机。
|
||||
|
||||
NanoKVM 镜像在LicheeRV Nano SDK 和 MaixCDK 基础上构建,可以兼容使用 LicheeRV Nano 的资料,反之LicheeRV Nano 或其他 SG2002 产品无法使用KVM软件。如果您想在 LicheeRV Nano 或 MaixCam 上构建 HDMI输入相关应用,请与我们联系,以获得技术支持。
|
||||
|
||||
注: SG2002的256MB内存中, 目前划分105MB用于多媒体子系统, NanoKVM会在视频图像采集和处理中使用这部分内存.
|
||||
|
||||
* [NanoKVM-A 原理图](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
* [NanoKVM-B 原理图](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf)
|
||||
* [NanoKVM 镜像下载](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## 购买渠道
|
||||
|
||||
* [Aliexpress(全球,除了美国和俄罗斯)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [Preorder (其它不支持淘宝和速卖通的国家)](https://sipeed.com/nanokvm)
|
||||
|
||||
## 社区
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
79
docker/Dockerfile
Normal file
79
docker/Dockerfile
Normal file
@@ -0,0 +1,79 @@
|
||||
FROM ubuntu:24.04 AS base_apt
|
||||
|
||||
# RUN echo 'Acquire::http::Proxy "http://cache.lan:3142";' > /etc/apt/apt.conf.d/00cacher
|
||||
RUN userdel -r ubuntu
|
||||
RUN apt update \
|
||||
&& apt install wget git cmake build-essential python3 python3-venv python3-pip autoconf automake libtool gosu -y
|
||||
|
||||
ARG DOCKER_USER=build
|
||||
RUN useradd -m "$DOCKER_USER"
|
||||
USER $DOCKER_USER
|
||||
|
||||
FROM base_apt AS host_tools
|
||||
|
||||
USER root
|
||||
RUN wget -O /tmp/host-tools.tar.gz https://sophon-file.sophon.cn/sophon-prod-s3/drive/23/03/07/16/host-tools.tar.gz \
|
||||
&& tar -C /usr/local -zxvf /tmp/host-tools.tar.gz \
|
||||
&& rm -f /tmp/host-tools.tar.gz \
|
||||
&& export PATH=$PATH:/usr/local/host-tools/gcc/riscv64-linux-musl-x86_64/bin \
|
||||
&& echo "Installed host-tools gcc" && riscv64-unknown-linux-musl-gcc -v
|
||||
|
||||
FROM base_apt AS golang
|
||||
|
||||
USER root
|
||||
RUN bash -c 'wget -O /tmp/go.tar.gz https://go.dev/dl/go1.25.0.linux-$([ `uname -m` == "x86_64" ] && echo "amd64" || uname -m).tar.gz' \
|
||||
&& tar -C /usr/local -zxvf /tmp/go.tar.gz \
|
||||
&& export PATH=$PATH:/usr/local/go/bin \
|
||||
&& echo "Installed golang" && go version
|
||||
|
||||
COPY server /tmp/go_cache
|
||||
|
||||
ENV PATH="$PATH:/usr/local/go/bin"
|
||||
|
||||
RUN cd /tmp/go_cache \
|
||||
&& echo "Caching go dep" \
|
||||
&& go mod download \
|
||||
&& go mod tidy \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/go_cache
|
||||
|
||||
FROM base_apt AS sdk
|
||||
|
||||
RUN cd ~ \
|
||||
&& git clone https://github.com/Sipeed/MaixCDK \
|
||||
&& cd MaixCDK \
|
||||
&& python3 -m venv . \
|
||||
&& . ./bin/activate \
|
||||
&& pip install -U -r requirements.txt
|
||||
|
||||
COPY --chown=$DOCKER_USER . /home/$DOCKER_USER/NanoKVM
|
||||
|
||||
RUN cd ~/MaixCDK \
|
||||
&& . ./bin/activate \
|
||||
&& cd ~/NanoKVM/support/sg2002 \
|
||||
&& ./build kvm_system
|
||||
|
||||
FROM base_apt
|
||||
|
||||
# Install golang
|
||||
COPY --from=golang /usr/local/go /usr/local/go
|
||||
COPY --chown=$DOCKER_USER --from=golang /root/go /home/$DOCKER_USER/go
|
||||
ENV PATH="$PATH:/usr/local/go/bin"
|
||||
|
||||
# Install host tools
|
||||
COPY --from=host_tools /usr/local/host-tools /usr/local/host-tools
|
||||
ENV PATH="$PATH:/usr/local/host-tools/gcc/riscv64-linux-musl-x86_64/bin"
|
||||
|
||||
# Install SDK
|
||||
COPY --from=sdk /home/$DOCKER_USER/MaixCDK /home/$DOCKER_USER/MaixCDK
|
||||
COPY --chmod=+x docker/entrypoint /entrypoint
|
||||
|
||||
RUN echo "Verify build tools" \
|
||||
&& echo "Installed host-tools gcc" && riscv64-unknown-linux-musl-gcc -v \
|
||||
&& echo "Installed golang" && go version
|
||||
|
||||
USER root
|
||||
RUN chmod 0755 /entrypoint \
|
||||
&& sed "s/\$DOCKER_USER/$DOCKER_USER/g" -i /entrypoint
|
||||
|
||||
ENTRYPOINT ["/entrypoint"]
|
||||
22
docker/entrypoint
Normal file
22
docker/entrypoint
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
: "${UID:=0}"
|
||||
: "${GID:=${UID}}"
|
||||
|
||||
if [ "$#" = 0 ]
|
||||
then set -- "$(command -v bash 2>/dev/null || command -v sh)" -l
|
||||
fi
|
||||
|
||||
if [ "$UID" != 0 ]
|
||||
then
|
||||
usermod -u "$UID" "$DOCKER_USER" 2>/dev/null && {
|
||||
groupmod -g "$GID" "$DOCKER_USER" 2>/dev/null ||
|
||||
usermod -a -G "$GID" "$DOCKER_USER"
|
||||
}
|
||||
set -- gosu "${UID}:${GID}" "${@}"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -9,8 +9,20 @@ start_usb_dev(){
|
||||
mkdir g0
|
||||
cd g0
|
||||
|
||||
echo 0x3346 > idVendor
|
||||
echo 0x1009 > idProduct
|
||||
# echo 0x3346 > idVendor
|
||||
# echo 0x1009 > idProduct
|
||||
if [ -e /boot/usb.vid ]
|
||||
then
|
||||
cat /boot/usb.vid > idVendor
|
||||
else
|
||||
echo 0x3346 > idVendor
|
||||
fi
|
||||
if [ -e /boot/usb.pid ]
|
||||
then
|
||||
cat /boot/usb.pid > idProduct
|
||||
else
|
||||
echo 0x1009 > idProduct
|
||||
fi
|
||||
mkdir strings/0x409
|
||||
echo '0123456789ABCDEF' > strings/0x409/serialnumber
|
||||
echo 'sipeed' > strings/0x409/manufacturer
|
||||
@@ -63,7 +75,7 @@ start_usb_dev(){
|
||||
fi
|
||||
echo 2 > functions/hid.GS1/protocol
|
||||
echo 4 > functions/hid.GS1/report_length
|
||||
echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc
|
||||
echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc
|
||||
ln -s functions/hid.GS1 configs/c.1
|
||||
|
||||
# touchpad
|
||||
@@ -92,6 +104,7 @@ start_usb_dev(){
|
||||
echo 1 > functions/mass_storage.disk0/lun.0/ro
|
||||
echo 0 > functions/mass_storage.disk0/lun.0/cdrom
|
||||
fi
|
||||
echo "NanoKVM USB Mass Storage0520" > functions/mass_storage.disk0/lun.0/inquiry_string
|
||||
disk=$(cat /boot/usb.disk0)
|
||||
if [ -z "${disk}" ]
|
||||
then
|
||||
@@ -136,4 +149,11 @@ case "$1" in
|
||||
start_usb_host
|
||||
start_usb_dev
|
||||
;;
|
||||
restart_phy)
|
||||
echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/unbind
|
||||
sleep 1
|
||||
echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/bind
|
||||
start_usb_host
|
||||
start_usb_dev
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -11,8 +11,20 @@ start_usb_dev(){
|
||||
|
||||
echo 0x0101 > bcdUSB
|
||||
echo 0x0623 > bcdDevice
|
||||
echo 0x3346 > idVendor
|
||||
echo 0x1009 > idProduct
|
||||
# echo 0x3346 > idVendor
|
||||
# echo 0x1009 > idProduct
|
||||
if [ -e /boot/usb.vid ]
|
||||
then
|
||||
cat /boot/usb.vid > idVendor
|
||||
else
|
||||
echo 0x3346 > idVendor
|
||||
fi
|
||||
if [ -e /boot/usb.pid ]
|
||||
then
|
||||
cat /boot/usb.pid > idProduct
|
||||
else
|
||||
echo 0x1009 > idProduct
|
||||
fi
|
||||
mkdir strings/0x409
|
||||
# echo '0' > strings/0x409/serialnumber
|
||||
echo 'sipeed' > strings/0x409/manufacturer
|
||||
@@ -45,7 +57,7 @@ start_usb_dev(){
|
||||
fi
|
||||
echo 2 > functions/hid.GS1/protocol
|
||||
echo 4 > functions/hid.GS1/report_length
|
||||
echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc
|
||||
echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc
|
||||
ln -s functions/hid.GS1 configs/c.1
|
||||
|
||||
# touchpad
|
||||
@@ -90,4 +102,11 @@ case "$1" in
|
||||
start_usb_host
|
||||
start_usb_dev
|
||||
;;
|
||||
restart_phy)
|
||||
echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/unbind
|
||||
sleep 1
|
||||
echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/bind
|
||||
start_usb_host
|
||||
start_usb_dev
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,119 +1,67 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /etc/profile
|
||||
RESERVE_INET="192.168.90.1/22"
|
||||
|
||||
# /boot/eth.nodhcp for example
|
||||
# ipaddr/net gw[optional]
|
||||
# 192.168.0.101/24 192.168.0.1
|
||||
# 192.168.3.116/22
|
||||
clean_line() {
|
||||
echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/\r//g'
|
||||
}
|
||||
|
||||
RESERVE_INET="192.168.0.1/24"
|
||||
calc_gw() {
|
||||
addr="$1"
|
||||
netid="$2"
|
||||
IFS=. read a b c d <<EOF
|
||||
$addr
|
||||
EOF
|
||||
ipnum=$(( (a<<24) + (b<<16) + (c<<8) + d ))
|
||||
mask=$(( 0xFFFFFFFF << (32-netid) & 0xFFFFFFFF ))
|
||||
gwnum=$(( (ipnum & mask) | 1 ))
|
||||
echo "$(( (gwnum>>24)&255 )).$(( (gwnum>>16)&255 )).$(( (gwnum>>8)&255 )).$(( gwnum&255 ))"
|
||||
}
|
||||
|
||||
start() {
|
||||
printf "start ethernet: "
|
||||
if [ -e /boot/eth.nodhcp ]
|
||||
then
|
||||
[ -e /boot/eth.nodhcp ] &&
|
||||
cat /boot/eth.nodhcp | while read inet gw
|
||||
do
|
||||
addr=${inet%/*}
|
||||
netid=${inet#*/}
|
||||
[ -z $gw ] &&
|
||||
gw=$( echo $addr| ( IFS='.' read a b c d; echo $((
|
||||
(((((($a<<8)+$b)<<8)+$c)<<8)+$d)
|
||||
& (((1<<$netid)-1)<<(32-$netid))
|
||||
))
|
||||
)) &&
|
||||
gw=$(($gw>>24&0xff)).$(($gw>>16&0xff)).$(($gw>>8&0xff)).$((1+( $gw>>0&0xff )))
|
||||
if [ -e /boot/eth.nodhcp ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line=$(clean_line "$line")
|
||||
[ -z "$line" ] && continue
|
||||
set -- $line
|
||||
inet="$1"
|
||||
gw="$2"
|
||||
[ -z "$inet" ] && continue
|
||||
addr=${inet%/*}
|
||||
netid=${inet#*/}
|
||||
[ -z "$gw" ] && gw=$(calc_gw "$addr" "$netid")
|
||||
arping -Dqc2 -Ieth0 "$addr" || continue
|
||||
ip a add "$inet" brd + dev eth0
|
||||
ip r add default via "$gw" dev eth0
|
||||
echo -e "nameserver $gw" >> /etc/resolv.conf
|
||||
break
|
||||
done < /boot/eth.nodhcp
|
||||
|
||||
arping -Dqc2 -Ieth0 $addr || continue
|
||||
ip a add $inet brd + dev eth0
|
||||
ip r add default via $gw dev eth0
|
||||
cp -f /etc/resolv.conf /etc/resolv.conf.old
|
||||
cat > /etc/resolv.conf << EOF
|
||||
nameserver $gw
|
||||
nameserver 8.8.4.4
|
||||
nameserver 8.8.8.8
|
||||
nameserver 114.114.114.114
|
||||
nameserver 119.29.29.29
|
||||
nameserver 223.5.5.5
|
||||
EOF
|
||||
break
|
||||
done &&
|
||||
ip a show dev eth0|grep inet || (
|
||||
udhcpc -i eth0 -t 3 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &>/dev/null
|
||||
ip a show dev eth0|grep inet
|
||||
) || (
|
||||
# failed to apply dynamic addr, need a available static addr to visit the LAN
|
||||
inet=$RESERVE_INET
|
||||
addr=${inet%/*}
|
||||
ip a add $inet brd + dev eth0
|
||||
) || exit 1
|
||||
ip a show dev eth0 | grep inet > /dev/null || {
|
||||
udhcpc -i eth0 -t 3 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &>/dev/null
|
||||
ip a show dev eth0 | grep inet > /dev/null
|
||||
} || {
|
||||
inet=$RESERVE_INET
|
||||
addr=${inet%/*}
|
||||
ip a add "$inet" brd + dev eth0
|
||||
} || exit 1
|
||||
else
|
||||
(udhcpc -i eth0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid) &
|
||||
udhcpc -i eth0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &
|
||||
fi
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
stop() {
|
||||
[[ ! -e "/run/udhcpc.eth0.pid" ]] && echo "udhcpc is not running..." && exit 1
|
||||
kill `cat /run/udhcpc.eth0.pid`
|
||||
[ ! -e "/run/udhcpc.eth0.pid" ] && exit 1
|
||||
kill "$(cat /run/udhcpc.eth0.pid)"
|
||||
rm /run/udhcpc.eth0.pid
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
doc)
|
||||
cat <<EOF
|
||||
If you want to set your own static ip address
|
||||
rather then fetch it by dhcp.
|
||||
Then you need to create a file \`/boot/eth.nodhcp\`,
|
||||
and edit it following the format below:
|
||||
\`addr/netid gw[optional]\` in one line
|
||||
for example (support muti definations, checks with arp):
|
||||
1. 192.168.0.101/24 192.168.0.1
|
||||
2. 192.168.3.116/22
|
||||
Delete this file if you change to use dhcp default.
|
||||
ATTENTION: If all the STATIC address you set in the file is
|
||||
already in use on this LAN checked with arp, then none
|
||||
of them will be yours. And it will trigger dhcp, try to
|
||||
fetch DYNAMIC one. If it's also failed, your address will
|
||||
be the REVERSE one, 192.168.0.1/24. Just to be sure that
|
||||
you can visit your NanoKVM device through eth to fix some
|
||||
problems by ssh, unless you must disassemble it and modify
|
||||
or rewrite the TFCard once again.
|
||||
EOF
|
||||
echo ""
|
||||
cat <<EOF
|
||||
如果您想自行设置静态 ip 地址而不是使用 dhcp 来获取它。
|
||||
|
||||
那么您需要新建一个文件 \`/boot/eth.nodhcp\`,然后按照以下格式编辑它:
|
||||
\`addr/netid gw[optional]\` 在一行内
|
||||
示例(支持多个预设,会通过 arp 检查是否占用):
|
||||
1. 192.168.0.101/24 192.168.0.1
|
||||
2. 192.168.3.116/22
|
||||
|
||||
如果您想改回默认 dhcp 获取 ip 地址的方式,删除该文件后即可恢复。
|
||||
|
||||
注意:如果您预设的所有静态 ip 地址都被 arp 检测到已占用,则不会成功设置。
|
||||
此时会触发 dhcp 尝试获取 ip 地址,如果仍失败,那么最终会强制设置成
|
||||
192.168.90.1/24。这是为了保证您的 NanoKVM 设备总是能有一个可用的
|
||||
ip 地址,方便您通过局域网 ssh 连接设备进行一些修复,否则您可能需要
|
||||
拆开外壳以手动修改 TF 卡或重新烧卡。
|
||||
|
||||
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
esac
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
@@ -2,24 +2,26 @@
|
||||
|
||||
. /etc/profile
|
||||
|
||||
APFLAG_FILE=/tmp/wifiap
|
||||
|
||||
gen_hostapd_conf() {
|
||||
ssid="${1}"
|
||||
pass="${2}"
|
||||
echo "ctrl_interface=/var/run/hostapd"
|
||||
echo "ctrl_interface_group=0"
|
||||
echo "ssid=${ssid}"
|
||||
echo "hw_mode=g"
|
||||
echo "channel=1"
|
||||
echo "beacon_int=100"
|
||||
echo "dtim_period=2"
|
||||
echo "max_num_sta=255"
|
||||
echo "rts_threshold=-1"
|
||||
echo "fragm_threshold=-1"
|
||||
echo "macaddr_acl=0"
|
||||
echo "auth_algs=3"
|
||||
echo "wpa=2"
|
||||
echo "wpa_passphrase=${pass}"
|
||||
echo "ieee80211n=1"
|
||||
ssid="${1}"
|
||||
pass="${2}"
|
||||
echo "ctrl_interface=/var/run/hostapd"
|
||||
echo "ctrl_interface_group=0"
|
||||
echo "ssid=${ssid}"
|
||||
echo "hw_mode=g"
|
||||
echo "channel=1"
|
||||
echo "beacon_int=100"
|
||||
echo "dtim_period=2"
|
||||
echo "max_num_sta=255"
|
||||
echo "rts_threshold=-1"
|
||||
echo "fragm_threshold=-1"
|
||||
echo "macaddr_acl=0"
|
||||
echo "auth_algs=3"
|
||||
echo "wpa=2"
|
||||
echo "wpa_passphrase=${pass}"
|
||||
echo "ieee80211n=1"
|
||||
}
|
||||
|
||||
gen_udhcpd_conf() {
|
||||
@@ -61,19 +63,16 @@ start() {
|
||||
chown root:root /etc/kvm/wifi.ssid /etc/kvm/wifi.pass
|
||||
chmod 644 /etc/kvm/wifi.ssid /etc/kvm/wifi.pass
|
||||
fi
|
||||
if [ -e /etc/kvm/wifi.ssid ]
|
||||
then
|
||||
ssid=`cat /etc/kvm/wifi.ssid`
|
||||
if [ -e /etc/kvm/wifi.ssid ]; then
|
||||
ssid=$(cat /etc/kvm/wifi.ssid)
|
||||
fi
|
||||
if [ -e /etc/kvm/wifi.pass ]
|
||||
then
|
||||
pass=`cat /etc/kvm/wifi.pass`
|
||||
if [ -e /etc/kvm/wifi.pass ]; then
|
||||
pass=$(cat /etc/kvm/wifi.pass)
|
||||
fi
|
||||
echo "ctrl_interface=/var/run/wpa_supplicant" > /etc/wpa_supplicant.conf
|
||||
wpa_passphrase "$ssid" "$pass" >> /etc/wpa_supplicant.conf
|
||||
echo "ctrl_interface=/var/run/wpa_supplicant" >/etc/wpa_supplicant.conf
|
||||
wpa_passphrase "$ssid" "$pass" >>/etc/wpa_supplicant.conf
|
||||
wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf
|
||||
if [ ! -e /boot/wifi.nodhcp ]
|
||||
then
|
||||
if [ ! -e /boot/wifi.nodhcp ]; then
|
||||
(udhcpc -i wlan0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.wlan0.pid) &
|
||||
fi
|
||||
}
|
||||
@@ -82,33 +81,27 @@ ap_start() {
|
||||
echo "wifi mode: ap"
|
||||
id2=$(printf "%d" 0x$(sha512sum /sys/class/cvi-base/base_uid | head -c 2))
|
||||
id3=$(printf "%d" 0x$(sha512sum /sys/class/cvi-base/base_uid | head -c 4 | tail -c 2))
|
||||
if [ "$id2" = "$id3" ]
|
||||
then
|
||||
if [ "$id2" = "$id3" ]; then
|
||||
id2=$((id2 + 1))
|
||||
fi
|
||||
if [ "$id2" -ge 255 ]
|
||||
then
|
||||
if [ "$id2" -ge 255 ]; then
|
||||
id2=253
|
||||
fi
|
||||
if [ "$id3" -ge 255 ]
|
||||
then
|
||||
if [ "$id3" -ge 255 ]; then
|
||||
id3=254
|
||||
fi
|
||||
ssid=""
|
||||
pass=""
|
||||
if [ -e /kvmapp/kvm/ap.ssid ]
|
||||
then
|
||||
ssid=`cat /kvmapp/kvm/ap.ssid`
|
||||
if [ -e /kvmapp/kvm/ap.ssid ]; then
|
||||
ssid=$(cat /kvmapp/kvm/ap.ssid)
|
||||
fi
|
||||
if [ -e /kvmapp/kvm/ap.pass ]
|
||||
then
|
||||
pass=`cat /kvmapp/kvm/ap.pass`
|
||||
if [ -e /kvmapp/kvm/ap.pass ]; then
|
||||
pass=$(cat /kvmapp/kvm/ap.pass)
|
||||
fi
|
||||
gen_hostapd_conf "$ssid" "$pass" > /etc/hostapd.conf
|
||||
gen_hostapd_conf "$ssid" "$pass" >/etc/hostapd.conf
|
||||
ipv4_prefix=10.$id3.$id2
|
||||
if [ ! -e /etc/udhcpd.wlan0.conf ]
|
||||
then
|
||||
gen_udhcpd_conf wlan0 "${ipv4_prefix}" > /etc/udhcpd.wlan0.conf
|
||||
if [ ! -e /etc/udhcpd.wlan0.conf ]; then
|
||||
gen_udhcpd_conf wlan0 "${ipv4_prefix}" >/etc/udhcpd.wlan0.conf
|
||||
fi
|
||||
# gen_dnsmasq_conf "${ipv4_prefix}" > /etc/dnsmasq.conf
|
||||
|
||||
@@ -126,50 +119,52 @@ ap_start() {
|
||||
# dnsmasq --pid-file=/tmp/dnsmasq.run -C /etc/dnsmasq.conf
|
||||
hostapd -B -i wlan0 /etc/hostapd.conf
|
||||
udhcpd -S /etc/udhcpd.wlan0.conf
|
||||
|
||||
if [ ! -f "$APFLAG_FILE" ]; then
|
||||
touch "$APFLAG_FILE"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
stop() {
|
||||
ps -ef|grep hostapd|grep -v grep|awk '{print $1}'|xargs kill -2 || true
|
||||
ps -ef|grep "udhcpd -S /etc/udhcpd.wlan0.conf" |grep -v grep|awk '{print $1}'|xargs kill -2 || true
|
||||
killall wpa_supplicant || true
|
||||
if [ -e /run/udhcpc.wlan0.pid ]
|
||||
then
|
||||
kill `cat /run/udhcpc.wlan0.pid` || true
|
||||
rm -f /run/udhcpc.wlan0.pid
|
||||
fi
|
||||
if [ -e /var/run/udhcpd.wlan0.pid ]
|
||||
then
|
||||
kill `cat /var/run/udhcpd.wlan0.pid` || true
|
||||
rm -f /var/run/udhcpd.wlan0.pid
|
||||
fi
|
||||
if [ -e /tmp/dnsmasq.run ]
|
||||
then
|
||||
kill `cat /tmp/dnsmasq.run` || true
|
||||
rm -f /tmp/dnsmasq.run
|
||||
fi
|
||||
# if [ -e /etc/wpa_supplicant.conf]
|
||||
# then
|
||||
# echo > /etc/wpa_supplicant.conf
|
||||
# fi
|
||||
airmon-ng stop wlan0mon || true
|
||||
ps -ef | grep hostapd | grep -v grep | awk '{print $1}' | xargs kill -2 || true
|
||||
ps -ef | grep "udhcpd -S /etc/udhcpd.wlan0.conf" | grep -v grep | awk '{print $1}' | xargs kill -2 || true
|
||||
killall wpa_supplicant || true
|
||||
if [ -e /run/udhcpc.wlan0.pid ]; then
|
||||
kill $(cat /run/udhcpc.wlan0.pid) || true
|
||||
rm -f /run/udhcpc.wlan0.pid
|
||||
fi
|
||||
if [ -e /var/run/udhcpd.wlan0.pid ]; then
|
||||
kill $(cat /var/run/udhcpd.wlan0.pid) || true
|
||||
rm -f /var/run/udhcpd.wlan0.pid
|
||||
fi
|
||||
if [ -e /tmp/dnsmasq.run ]; then
|
||||
kill $(cat /tmp/dnsmasq.run) || true
|
||||
rm -f /tmp/dnsmasq.run
|
||||
fi
|
||||
# if [ -e /etc/wpa_supplicant.conf]
|
||||
# then
|
||||
# echo > /etc/wpa_supplicant.conf
|
||||
# fi
|
||||
airmon-ng stop wlan0mon || true
|
||||
|
||||
if [ -f "$APFLAG_FILE" ]; then
|
||||
rm "$APFLAG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
if [ "${1}" = "start" ]
|
||||
then
|
||||
start
|
||||
elif [ "${1}" = "stop" ]
|
||||
then
|
||||
stop
|
||||
elif [ "${1}" = "ap" ]
|
||||
then
|
||||
if [ "${1}" = "start" ]; then
|
||||
start
|
||||
elif [ "${1}" = "stop" ]; then
|
||||
stop
|
||||
ap_start
|
||||
elif [ "${1}" = "restart" ]
|
||||
then
|
||||
restart
|
||||
elif [ "${1}" = "ap" ]; then
|
||||
stop
|
||||
ap_start
|
||||
elif [ "${1}" = "restart" ]; then
|
||||
restart
|
||||
fi
|
||||
|
||||
@@ -21,94 +21,69 @@ PKG_URL_LATEST="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
exit 1
|
||||
VERSION=$(/usr/sbin/$DAEMON --version|sed -n '1p'|xargs echo -n)
|
||||
|
||||
[ -x /usr/bin/tailscale ] || echo "/usr/bin/tailscale not found, your installation of tailscale may be broken"
|
||||
[ -x /usr/bin/tailscale ] ||
|
||||
(echo "/usr/bin/tailscale not found, your installation of tailscale may be broken" && exit 1)
|
||||
|
||||
# just for those need forwarding
|
||||
[ ! -f /etc/sysctl.d/99-tailscale.conf ] &&
|
||||
mkdir -p /etc/sysctl.d/ &&
|
||||
echo "missing /etc/sysctl.d/99-tailscale.conf, try make it below:" &&
|
||||
(tee /etc/sysctl.d/99-tailscale.conf <<EOF
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
EOF
|
||||
) &&
|
||||
echo "if this message repeats showing, please look at $0 if you need forwarding"
|
||||
# Clean up old forwarding configuration if it exists
|
||||
if [ -f /etc/sysctl.d/99-tailscale.conf ]; then
|
||||
echo "Found old forwarding configuration, cleaning up..."
|
||||
rm -f /etc/sysctl.d/99-tailscale.conf
|
||||
sysctl -w net.ipv4.ip_forward=0 >/dev/null 2>&1
|
||||
sysctl -w net.ipv6.conf.all.forwarding=0 >/dev/null 2>&1
|
||||
echo "Cleanup completed. IPv4/IPv6 forwarding disabled."
|
||||
|
||||
# Trigger IPv6 address renewal on all interfaces
|
||||
for iface in $(ls /sys/class/net/ | grep -v lo); do
|
||||
echo 0 > /proc/sys/net/ipv6/conf/$iface/forwarding 2>/dev/null
|
||||
sysctl -w net.ipv6.conf.$iface.accept_ra=2 >/dev/null 2>&1
|
||||
done
|
||||
|
||||
echo "If you need exit node features, please configure manually."
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
tailscale set --accept-dns=false
|
||||
if [ -f /etc/kvm/GOMEMLIMIT ]; then
|
||||
value=$(cat /etc/kvm/GOMEMLIMIT)
|
||||
export GOMEMLIMIT="${value}MiB"
|
||||
else
|
||||
export GOMEMLIMIT=1024MiB
|
||||
export GOMEMLIMIT=512MiB
|
||||
fi
|
||||
|
||||
|
||||
echo "GOMEMLIMIT set to ${GOMEMLIMIT}"
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p /var/lib/tailscale /var/run/tailscale
|
||||
|
||||
printf "Starting $DAEMON[$VERSION]: "
|
||||
start-stop-daemon -S -bmq -p "$PIDFILE" -x "/usr/sbin/$DAEMON" -- \
|
||||
--state=/var/lib/tailscale/tailscaled.state \
|
||||
--socket=/var/run/tailscale/tailscaled.sock \
|
||||
--port=${PORT} \
|
||||
$FLAGS
|
||||
[ $? = 0 ] && echo "OK" || echo "FAIL"
|
||||
if [ $? = 0 ]; then
|
||||
sleep 5
|
||||
echo "OK"
|
||||
tailscale set --accept-dns=false
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
printf "Stopping $DAEMON: "
|
||||
start-stop-daemon -K -p "$PIDFILE"
|
||||
[ $? = 0 ] && (echo "OK"; rm -f "$PIDFILE") || echo "FAIL"
|
||||
printf "cleaning tailscaled: "
|
||||
/usr/sbin/$DAEMON --cleanup &>/dev/null
|
||||
/usr/sbin/$DAEMON --cleanup >/dev/null 2>&1
|
||||
[ $? = 0 ] && echo "OK" || echo "FAIL"
|
||||
;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
doc)
|
||||
cat <<EOF
|
||||
To use tailscale, you need to meet these conditions below:
|
||||
1. Both tailscale and tailscaled were installed.
|
||||
2. At least one tailscale's account were registered.
|
||||
(https://login.tailscale.com/start)
|
||||
3. Your NanoKVM device must keep online.
|
||||
|
||||
Then, follow the steps below:
|
||||
1. Login in https://login.tailscale.com on your PC
|
||||
to enter into yout admin console.
|
||||
2. Run 'tailscale login' on your device, and copy the link showed
|
||||
to open on your PC, and confirm the auth.
|
||||
3. Congratulations, your NanoKVM device is now on the tailnet, try to
|
||||
visit it from all your device which on the same tailnet.
|
||||
4. run 'tailscale set --webclient' and manage your device
|
||||
on the web interface from localnet and tailenet.
|
||||
|
||||
Added: no more action to be repeat after your device reboot.
|
||||
EOF
|
||||
echo ""
|
||||
cat <<EOF
|
||||
为了正常使用 tailscale, 你需要满足以下几个条件:
|
||||
1. tailscale 和 tailscaled 这两个静态链接的二进制程序
|
||||
都已经正确安装在系统上。
|
||||
2. 至少注册一个 tailscale 的帐号才能使用其服务。
|
||||
(https://login.tailscale.com/start)
|
||||
3. 你的 NanoKVM 设备必须保持在线。
|
||||
|
||||
然后,跟着以下步骤操作:
|
||||
1. 在你的电脑上登录 https://login.tailscale.com 以进入控制台。
|
||||
2. 在你的 NanoKVM ssh 终端上执行 'tailscale login', 然后复制
|
||||
出现的链接到浏览器上,登录并通过验证。
|
||||
3. 祝贺,你的 NanoKVM 现在已经加入了 tailnet 然后你可以从任意
|
||||
同样已加入同一 tailnet 的设备去访问你的 NanoKVM。
|
||||
4. 执行 'tailscale set --webclient' 然后你就可以从 '局域网' 和
|
||||
'tailnet' 来访问特定网页,用来管理你的设备。
|
||||
|
||||
补充: 不需要重复上面的操作,哪怕你的设备重启过。
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
64
server/build.sh
Executable file
64
server/build.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration Variables
|
||||
BINARY_NAME="NanoKVM-Server"
|
||||
CC_COMPILER="riscv64-unknown-linux-musl-gcc"
|
||||
CGO_CFLAGS_OPTS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d"
|
||||
|
||||
# Define colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper function to check if a command exists
|
||||
check_dependency() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
echo -e "${RED}[ERROR] Required command '$1' not found.${NC}"
|
||||
echo "Please install it or ensure it is in your PATH."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 1: Check Prerequisites
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Checking build environment...${NC}"
|
||||
|
||||
check_dependency "go"
|
||||
check_dependency "patchelf"
|
||||
check_dependency "$CC_COMPILER"
|
||||
|
||||
echo -e "${GREEN}[OK] All dependencies found.${NC}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 2: Build the Binary
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Starting cross-compilation for RISC-V 64-bit (BoringCrypto enabled)...${NC}"
|
||||
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=linux
|
||||
export GOARCH=riscv64
|
||||
export GOEXPERIMENT=boringcrypto
|
||||
export CC="$CC_COMPILER"
|
||||
export CGO_CFLAGS="$CGO_CFLAGS_OPTS"
|
||||
|
||||
go build -o "$BINARY_NAME" -v
|
||||
|
||||
if [ -f "$BINARY_NAME" ]; then
|
||||
echo -e "${GREEN}[SUCCESS] Binary '$BINARY_NAME' created successfully.${NC}"
|
||||
else
|
||||
echo -e "${RED}[ERROR] Build failed. Binary not found.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 3: Patch RPATH
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Patching RPATH with patchelf...${NC}"
|
||||
|
||||
patchelf --add-rpath '$ORIGIN/dl_lib' "$BINARY_NAME"
|
||||
|
||||
echo -e "${GREEN}[DONE] Build script completed successfully!${NC}"
|
||||
@@ -7,8 +7,6 @@ package common
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
@@ -26,15 +24,8 @@ func GetKvmVision() *KvmVision {
|
||||
kvmVisionOnce.Do(func() {
|
||||
kvmVision = &KvmVision{}
|
||||
|
||||
conf := config.GetInstance()
|
||||
logLevel := strings.ToLower(conf.Logger.Level)
|
||||
|
||||
logEnable := C.uint8_t(0)
|
||||
if logLevel == "debug" {
|
||||
logEnable = C.uint8_t(1)
|
||||
}
|
||||
|
||||
C.kvmv_init(logEnable)
|
||||
logLevel := C.uint8_t(0)
|
||||
C.kvmv_init(logLevel)
|
||||
log.Debugf("kvm vision initialized")
|
||||
})
|
||||
|
||||
@@ -62,8 +53,6 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data
|
||||
defer C.free_kvmv_data(&kvmData)
|
||||
|
||||
data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
log.Debugf("read kvm image: %v", result)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,8 +77,6 @@ func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data
|
||||
defer C.free_kvmv_data(&kvmData)
|
||||
|
||||
data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
log.Debugf("read kvm image: %v", result)
|
||||
return
|
||||
}
|
||||
|
||||
45
server/config/file.go
Normal file
45
server/config/file.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const ConfigurationFile = "/etc/kvm/server.yaml"
|
||||
|
||||
func Read() (*Config, error) {
|
||||
data, err := os.ReadFile(ConfigurationFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read config: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conf Config
|
||||
|
||||
if err := yaml.Unmarshal(data, &conf); err != nil {
|
||||
log.Fatalf("failed to unmarshal config: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("read %s successfully", ConfigurationFile)
|
||||
return &conf, nil
|
||||
}
|
||||
|
||||
func Write(conf *Config) error {
|
||||
data, err := yaml.Marshal(&conf)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(ConfigurationFile, data, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("write to %s successfully", ConfigurationFile)
|
||||
return nil
|
||||
}
|
||||
@@ -54,24 +54,27 @@ func (h HWVersion) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func getHwVersion() HWVersion {
|
||||
func GetHwVersion() HWVersion {
|
||||
content, err := os.ReadFile(HWVersionFile)
|
||||
if err != nil {
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
version := strings.ReplaceAll(string(content), "\n", "")
|
||||
if version == "beta" {
|
||||
switch version {
|
||||
case "alpha":
|
||||
return HWVersionAlpha
|
||||
case "beta":
|
||||
return HWVersionBeta
|
||||
} else if version == "pcie" {
|
||||
case "pcie":
|
||||
return HWVersionPcie
|
||||
default:
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
func getHardware() (h Hardware) {
|
||||
version := getHwVersion()
|
||||
version := GetHwVersion()
|
||||
|
||||
switch version {
|
||||
case HWVersionAlpha:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module NanoKVM-Server
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.2
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.24
|
||||
@@ -12,12 +10,14 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mervick/aes-everywhere/go/aes256 v0.0.0-20240803013625-6759956693c0
|
||||
github.com/pion/dtls/v3 v3.0.3
|
||||
github.com/pion/rtp v1.8.18
|
||||
github.com/pion/webrtc/v4 v4.0.1
|
||||
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/unrolled/secure v1.15.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -44,14 +44,12 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||
github.com/pion/interceptor v0.1.37 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/interceptor v0.1.39 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.9 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
@@ -73,9 +71,9 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
@@ -34,8 +34,6 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
@@ -80,18 +78,18 @@ github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/interceptor v0.1.39 h1:Y6k0bN9Y3Lg/Wb21JBWp480tohtns8ybJ037AGr9UuA=
|
||||
github.com/pion/interceptor v0.1.39/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
@@ -141,8 +139,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -160,35 +159,19 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -3,8 +3,10 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
"NanoKVM-Server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
@@ -36,7 +39,9 @@ func initialize() {
|
||||
vision := common.GetKvmVision()
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
vision.SetHDMI(true)
|
||||
if !utils.IsHdmiDisabled() {
|
||||
vision.SetHDMI(true)
|
||||
}
|
||||
|
||||
// run mouse jiggler
|
||||
jiggler.GetJiggler().Run()
|
||||
@@ -58,7 +63,6 @@ func run() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
if conf.Authentication == "disable" {
|
||||
r.Use(cors.AllowAll())
|
||||
}
|
||||
@@ -67,20 +71,17 @@ func run() {
|
||||
|
||||
httpAddr := fmt.Sprintf(":%d", conf.Port.Http)
|
||||
httpsAddr := fmt.Sprintf(":%d", conf.Port.Https)
|
||||
log.Printf("proto: %s, port: %d %d\n", conf.Proto, conf.Port.Http, conf.Port.Https)
|
||||
|
||||
if conf.Proto == "https" {
|
||||
r.Use(middleware.Tls())
|
||||
|
||||
go func() {
|
||||
if err := r.Run(httpAddr); err != nil {
|
||||
panic("start http server failed")
|
||||
r.Use(middleware.Tls())
|
||||
err := r.RunTLS(httpsAddr, conf.Cert.Crt, conf.Cert.Key)
|
||||
if err != nil {
|
||||
panic("start https server failed")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := r.RunTLS(httpsAddr, conf.Cert.Crt, conf.Cert.Key); err != nil {
|
||||
panic("start https server failed")
|
||||
}
|
||||
runRedirect(httpAddr, httpsAddr)
|
||||
} else {
|
||||
if err := r.Run(httpAddr); err != nil {
|
||||
panic("start http server failed")
|
||||
@@ -88,6 +89,26 @@ func run() {
|
||||
}
|
||||
}
|
||||
|
||||
func runRedirect(httpPort string, httpsPort string) {
|
||||
err := http.ListenAndServe(httpPort, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
host := req.Host
|
||||
if strings.Contains(host, httpPort) {
|
||||
host = strings.Split(host, httpPort)[0]
|
||||
}
|
||||
|
||||
targetURL := "https://" + host + req.URL.String()
|
||||
if httpsPort != ":443" {
|
||||
targetURL = "https://" + host + httpsPort + req.URL.String()
|
||||
}
|
||||
|
||||
http.Redirect(w, req, targetURL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
panic("start http server failed")
|
||||
}
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
common.GetKvmVision().Close()
|
||||
}
|
||||
|
||||
@@ -21,8 +21,3 @@ type ChangePasswordReq struct {
|
||||
type IsPasswordUpdatedRsp struct {
|
||||
IsUpdated bool `json:"isUpdated"`
|
||||
}
|
||||
|
||||
type ConnectWifiReq struct {
|
||||
Ssid string `validate:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
@@ -17,28 +17,14 @@ type SetMacNameReq struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type TailscaleState string
|
||||
|
||||
const (
|
||||
TailscaleNotInstall TailscaleState = "notInstall"
|
||||
TailscaleNotRunning TailscaleState = "notRunning"
|
||||
TailscaleNotLogin TailscaleState = "notLogin"
|
||||
TailscaleStopped TailscaleState = "stopped"
|
||||
TailscaleRunning TailscaleState = "running"
|
||||
)
|
||||
|
||||
type GetTailscaleStatusRsp struct {
|
||||
State TailscaleState `json:"state"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
type LoginTailscaleRsp struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type GetWifiRsp struct {
|
||||
Supported bool `json:"supported"`
|
||||
Connected bool `json:"connected"`
|
||||
Supported bool `json:"supported"`
|
||||
ApMode bool `json:"apMode"`
|
||||
Connected bool `json:"connected"`
|
||||
Ssid string `json:"ssid"`
|
||||
}
|
||||
|
||||
type ConnectWifiReq struct {
|
||||
Ssid string `validate:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
@@ -16,3 +16,7 @@ type GetMountedImageRsp struct {
|
||||
type GetCdRomRsp struct {
|
||||
Cdrom int64 `json:"cdrom"`
|
||||
}
|
||||
|
||||
type DeleteImageReq struct {
|
||||
File string `json:"file" validate:"required"`
|
||||
}
|
||||
|
||||
22
server/proto/tailscale.go
Normal file
22
server/proto/tailscale.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package proto
|
||||
|
||||
type TailscaleState string
|
||||
|
||||
const (
|
||||
TailscaleNotInstall TailscaleState = "notInstall"
|
||||
TailscaleNotRunning TailscaleState = "notRunning"
|
||||
TailscaleNotLogin TailscaleState = "notLogin"
|
||||
TailscaleStopped TailscaleState = "stopped"
|
||||
TailscaleRunning TailscaleState = "running"
|
||||
)
|
||||
|
||||
type GetTailscaleStatusRsp struct {
|
||||
State TailscaleState `json:"state"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
type LoginTailscaleRsp struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -55,8 +55,18 @@ type DeleteScriptReq struct {
|
||||
Name string `validate:"required"`
|
||||
}
|
||||
|
||||
// autostart
|
||||
type GetAutostartRsp struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
type UploadAutostartReq struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GetVirtualDeviceRsp struct {
|
||||
Network bool `json:"network"`
|
||||
Media bool `json:"media"`
|
||||
Disk bool `json:"disk"`
|
||||
}
|
||||
|
||||
@@ -87,6 +97,10 @@ type GetOLEDRsp struct {
|
||||
Sleep int `json:"sleep"`
|
||||
}
|
||||
|
||||
type GetGetHdmiStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetSSHStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
@@ -128,3 +142,7 @@ type SetWebTitleReq struct {
|
||||
type GetWebTitleRsp struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type SetTlsReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ func applicationRouter(r *gin.Engine) {
|
||||
service := application.NewService()
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/application/version", service.GetVersion) // get application version
|
||||
api.POST("/application/update", service.Update) // update application
|
||||
api.GET("/application/version", service.GetVersion) // get application version
|
||||
api.POST("/application/update", service.Update) // update application
|
||||
api.POST("/application/update/offline", service.OfflineUpdate) // update application offline
|
||||
|
||||
api.GET("/application/preview", service.GetPreview) // get preview updates state
|
||||
api.POST("/application/preview", service.SetPreview) // set preview updates state
|
||||
|
||||
@@ -14,4 +14,5 @@ func downloadRouter(r *gin.Engine) {
|
||||
api.POST("/download/image", service.DownloadImage) // download image
|
||||
api.GET("/download/image/status", service.StatusImage) // download image
|
||||
api.GET("/download/image/enabled", service.ImageEnabled) // download image
|
||||
api.POST("/download/file", service.DownloadImageFile) // download image
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ func hidRouter(r *gin.Engine) {
|
||||
service := hid.NewService()
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.POST("/hid/reset", service.Reset) // reset hid
|
||||
api.POST("/hid/paste", service.Paste) // paste
|
||||
|
||||
api.GET("/hid/mode", service.GetHidMode) // get hid mode
|
||||
api.POST("/hid/mode", service.SetHidMode) // set hid mode
|
||||
api.POST("/hid/reset", service.ResetHid) // reset hid
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func networkRouter(r *gin.Engine) {
|
||||
service := network.NewService()
|
||||
|
||||
r.POST("/api/network/wifi", service.ConnectWifi) // connect Wi-Fi
|
||||
r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode)
|
||||
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
@@ -18,5 +18,8 @@ func networkRouter(r *gin.Engine) {
|
||||
api.GET("/network/wol/mac", service.GetMac) // get mac list
|
||||
api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac
|
||||
api.POST("/network/wol/mac/name", service.SetMacName) // set mac name
|
||||
api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information
|
||||
|
||||
api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information
|
||||
api.POST("/network/wifi/connect", service.ConnectWifi) // connect Wi-Fi
|
||||
api.POST("/network/wifi/disconnect", service.DisconnectWifi) // disconnect Wi-Fi
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ func storageRouter(r *gin.Engine) {
|
||||
api.GET("/storage/image/mounted", service.GetMountedImage) // get mounted image
|
||||
api.POST("/storage/image/mount", service.MountImage) // mount image
|
||||
api.GET("/storage/cdrom", service.GetCdRom) // get CD-ROM flag
|
||||
api.POST("/storage/image/delete", service.DeleteImage) // delete image
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package router
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/service/stream/h264"
|
||||
"NanoKVM-Server/service/stream/direct"
|
||||
"NanoKVM-Server/service/stream/mjpeg"
|
||||
"NanoKVM-Server/service/stream/webrtc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -15,5 +16,6 @@ func streamRouter(r *gin.Engine) {
|
||||
api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect
|
||||
api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect
|
||||
|
||||
api.GET("/stream/h264", h264.Connect) // h264 stream
|
||||
api.GET("/stream/h264", webrtc.Connect) // h264 stream (webrtc)
|
||||
api.GET("/stream/h264/direct", direct.Connect) // h264 stream (http)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ func vmRouter(r *gin.Engine) {
|
||||
api.GET("/vm/oled", service.GetOLED) // get OLED configuration
|
||||
api.POST("/vm/oled", service.SetOLED) // set OLED configuration
|
||||
|
||||
api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi (pcie only)
|
||||
// Only supported by PCIe version
|
||||
api.GET("/vm/hdmi", service.GetHdmiState) // get HDMI state
|
||||
api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi
|
||||
api.POST("/vm/hdmi/enable", service.EnableHdmi) // enable hdmi
|
||||
api.POST("/vm/hdmi/disable", service.DisableHdmi) // disable hdmi
|
||||
|
||||
api.GET("/vm/ssh", service.GetSSHState) // get SSH state
|
||||
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
|
||||
@@ -57,5 +61,12 @@ func vmRouter(r *gin.Engine) {
|
||||
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
|
||||
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS
|
||||
|
||||
api.POST("/vm/tls", service.SetTls) // enable/disable TLS
|
||||
|
||||
api.GET("/vm/autostart", service.GetAutostart) // get autostart list
|
||||
api.GET("/vm/autostart/:name", service.GetAutostartContent) // get autostart content
|
||||
api.DELETE("/vm/autostart/:name", service.DeleteAutostart) // delete autostart script
|
||||
api.POST("/vm/autostart/:name", service.UploadAutostart) // upload autostart script
|
||||
|
||||
api.POST("/vm/system/reboot", service.Reboot) // reboot system
|
||||
}
|
||||
|
||||
76
server/service/application/install.go
Normal file
76
server/service/application/install.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"NanoKVM-Server/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
isUpdating bool
|
||||
)
|
||||
|
||||
func acquireUpdateLock() bool {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if isUpdating {
|
||||
return false
|
||||
}
|
||||
isUpdating = true
|
||||
return true
|
||||
}
|
||||
|
||||
func releaseUpdateLock() {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
isUpdating = false
|
||||
}
|
||||
|
||||
func installPackage(source string) error {
|
||||
dir, err := utils.UnTarGz(source, CacheDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decompress app: %w", err)
|
||||
}
|
||||
|
||||
if err := backupCurrentApp(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyUpdate(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := utils.ChmodRecursively(AppDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to chmod: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupCurrentApp() error {
|
||||
if err := os.RemoveAll(BackupDir); err != nil {
|
||||
return fmt.Errorf("failed to remove backup: %w", err)
|
||||
}
|
||||
|
||||
if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil {
|
||||
return fmt.Errorf("failed to backup app: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyUpdate(sourceDir string) error {
|
||||
if err := utils.MoveFilesRecursively(sourceDir, AppDir); err != nil {
|
||||
// Try to restore backup on failure
|
||||
if restoreErr := utils.MoveFilesRecursively(BackupDir, AppDir); restoreErr != nil {
|
||||
log.Errorf("Failed to restore backup after update failure: %v", restoreErr)
|
||||
}
|
||||
return fmt.Errorf("failed to move update in place: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
server/service/application/progress_writer.go
Normal file
84
server/service/application/progress_writer.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
sentinelPath = "/tmp/.download_in_progress"
|
||||
tickerInterval = 2500 * time.Millisecond
|
||||
sentinelPermission = 0644
|
||||
)
|
||||
|
||||
// progressWriter wraps an io.Writer to track and report upload progress.
|
||||
type progressWriter struct {
|
||||
writer io.Writer
|
||||
written int64
|
||||
totalSize int64
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newProgressWriter(w io.Writer, totalSize int64) *progressWriter {
|
||||
pw := &progressWriter{
|
||||
writer: w,
|
||||
totalSize: totalSize,
|
||||
ticker: time.NewTicker(tickerInterval),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go pw.reportProgress()
|
||||
return pw
|
||||
}
|
||||
|
||||
func (pw *progressWriter) reportProgress() {
|
||||
for {
|
||||
select {
|
||||
case <-pw.done:
|
||||
return
|
||||
case <-pw.ticker.C:
|
||||
pw.updateSentinel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pw *progressWriter) updateSentinel() {
|
||||
if pw.totalSize == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(sentinelPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to read sentinel file")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(content), ";", 2)
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
percentage := float64(pw.written) / float64(pw.totalSize) * 100
|
||||
newContent := fmt.Sprintf("%s;%.2f%%", parts[0], percentage)
|
||||
|
||||
if err := os.WriteFile(sentinelPath, []byte(newContent), sentinelPermission); err != nil {
|
||||
log.Error("Failed to update sentinel file")
|
||||
}
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.writer.Write(p)
|
||||
pw.written += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Stop() {
|
||||
pw.ticker.Stop()
|
||||
close(pw.done)
|
||||
}
|
||||
@@ -24,6 +24,12 @@ const (
|
||||
func (s *Service) Update(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if !acquireUpdateLock() {
|
||||
rsp.ErrRsp(c, -1, "update already in progress")
|
||||
return
|
||||
}
|
||||
defer releaseUpdateLock()
|
||||
|
||||
if err := update(); err != nil {
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err))
|
||||
return
|
||||
@@ -53,7 +59,6 @@ func update() error {
|
||||
|
||||
// download
|
||||
target := fmt.Sprintf("%s/%s", CacheDir, latest.Name)
|
||||
|
||||
if err := download(latest.Url, target); err != nil {
|
||||
log.Errorf("download app failed: %s", err)
|
||||
return err
|
||||
@@ -65,34 +70,9 @@ func update() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// decompress
|
||||
dir, err := utils.UnTarGz(target, CacheDir)
|
||||
log.Debugf("untar: %s", dir)
|
||||
if err != nil {
|
||||
log.Errorf("decompress app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// backup old version
|
||||
if err := os.RemoveAll(BackupDir); err != nil {
|
||||
log.Errorf("remove backup failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil {
|
||||
log.Errorf("backup app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
if err := utils.MoveFilesRecursively(dir, AppDir); err != nil {
|
||||
log.Errorf("failed to move update back in place: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// modify permissions
|
||||
if err := utils.ChmodRecursively(AppDir, 0o755); err != nil {
|
||||
log.Errorf("chmod failed: %s", err)
|
||||
// install
|
||||
if err := installPackage(target); err != nil {
|
||||
log.Errorf("failed to install package: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,7 +83,7 @@ func download(url string, target string) (err error) {
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
if i > 0 {
|
||||
time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
|
||||
176
server/service/application/update_offline.go
Normal file
176
server/service/application/update_offline.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var validFilenameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
|
||||
func (s *Service) OfflineUpdate(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if !acquireUpdateLock() {
|
||||
rsp.ErrRsp(c, -1, "update already in progress")
|
||||
return
|
||||
}
|
||||
defer releaseUpdateLock()
|
||||
|
||||
if err := offlineUpdate(c); err != nil {
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("offline update application success")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
|
||||
}
|
||||
|
||||
func offlineUpdate(c *gin.Context) error {
|
||||
_ = os.RemoveAll(CacheDir)
|
||||
_ = os.MkdirAll(CacheDir, 0o755)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(CacheDir)
|
||||
}()
|
||||
|
||||
if err := checkDownloadInProgress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createSentinelFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeSentinelFile()
|
||||
|
||||
reader, err := c.Request.MultipartReader()
|
||||
if err != nil {
|
||||
log.Errorf("Invalid multipart data: %v", err)
|
||||
return fmt.Errorf("invalid multipart data: %w", err)
|
||||
}
|
||||
|
||||
target, err := processUpload(reader, c.Request.ContentLength)
|
||||
if err != nil {
|
||||
log.Errorf("failed to upload install package: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := installPackage(target); err != nil {
|
||||
log.Errorf("failed to install package: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDownloadInProgress() error {
|
||||
if _, err := os.Stat(sentinelPath); err == nil {
|
||||
log.Debug("Download in progress")
|
||||
return fmt.Errorf("download already in progress")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSentinelFile() error {
|
||||
if err := os.WriteFile(sentinelPath, []byte("downloading"), sentinelPermission); err != nil {
|
||||
log.Errorf("Failed to create sentinel file: %v", err)
|
||||
return fmt.Errorf("failed to create sentinel file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processUpload(reader *multipart.Reader, contentLength int64) (string, error) {
|
||||
var outPath string
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read multipart: %w", err)
|
||||
}
|
||||
|
||||
if part.FormName() != "file" {
|
||||
continue
|
||||
}
|
||||
|
||||
outPath, err = saveUploadedFile(part, contentLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if outPath == "" {
|
||||
return "", fmt.Errorf("no file uploaded")
|
||||
}
|
||||
|
||||
return outPath, nil
|
||||
}
|
||||
|
||||
func saveUploadedFile(part *multipart.Part, contentLength int64) (string, error) {
|
||||
filename := part.FileName()
|
||||
if filename == "" {
|
||||
return "", fmt.Errorf("no filename provided")
|
||||
}
|
||||
|
||||
if err := validateFilename(filename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outPath := filepath.Join(CacheDir, filename)
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
pw := newProgressWriter(out, contentLength)
|
||||
defer pw.Stop()
|
||||
|
||||
if _, err := io.Copy(pw, part); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return outPath, nil
|
||||
}
|
||||
|
||||
func validateFilename(filename string) error {
|
||||
baseName := filepath.Base(filename)
|
||||
|
||||
// Check if the path contains directory components
|
||||
if baseName != filename {
|
||||
log.Warnf("Path detected in filename: %s", filename)
|
||||
return fmt.Errorf("path detected in filename")
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if strings.Contains(filename, "..") {
|
||||
log.Warnf("Path traversal attempt: %s", filename)
|
||||
return fmt.Errorf("invalid filename: path traversal detected")
|
||||
}
|
||||
|
||||
// Validate filename characters
|
||||
if !validFilenameRegex.MatchString(filename) {
|
||||
log.Warnf("Invalid filename characters: %s", filename)
|
||||
return fmt.Errorf("invalid filename: contains invalid characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeSentinelFile() {
|
||||
_ = os.Remove(sentinelPath)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -10,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -36,15 +37,15 @@ func (s *Service) GetVersion(c *gin.Context) {
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
latestVersion := ""
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get latest version failed")
|
||||
return
|
||||
if err == nil && latest != nil {
|
||||
latestVersion = latest.Version
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latest.Version,
|
||||
Latest: latestVersion,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -23,17 +24,20 @@ func (s *Service) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
time.Sleep(3 * time.Second)
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if ok := CompareAccount(req.Username, req.Password); !ok {
|
||||
time.Sleep(2 * time.Second)
|
||||
rsp.ErrRsp(c, -2, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateJWT(req.Username)
|
||||
if err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
rsp.ErrRsp(c, -3, "generate token failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
@@ -51,6 +52,28 @@ func (s *Service) ImageEnabled(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func isISO9660(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// ISO-9660 Magic "CD001" bei Offset 32769
|
||||
_, err = f.Seek(0x8001, io.SeekStart)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
buf := make([]byte, 5)
|
||||
_, err = io.ReadFull(f, buf)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return string(buf) == "CD001", nil
|
||||
}
|
||||
|
||||
func (s *Service) StatusImage(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
@@ -93,6 +116,170 @@ func (s *Service) StatusImage(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImageFile(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
log.Debug("DownloadImage")
|
||||
|
||||
// Set a sentinel file to mark that there is a download in progress
|
||||
// This is to prevent multiple downloads at the same time
|
||||
if _, err := os.Stat(sentinelPath); err == nil {
|
||||
log.Debug("Download in progress")
|
||||
rsp.ErrRsp(c, -1, "download in progress")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the sentinel file
|
||||
err := os.WriteFile(sentinelPath, []byte("start"), 0644)
|
||||
if err != nil {
|
||||
log.Error("Failed to create sentinel file")
|
||||
rsp.ErrRsp(c, -1, "failed to create sentinel file")
|
||||
return
|
||||
}
|
||||
|
||||
// Multipart Reader direkt nutzen (keine FormFile!)
|
||||
reader, err := c.Request.MultipartReader()
|
||||
if err != nil {
|
||||
log.Error("invalid multipart data")
|
||||
rsp.ErrRsp(c, -1, "invalid multipart data")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var lw *loggingWriter
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("failed to read part")
|
||||
rsp.ErrRsp(c, -1, "failed to read part")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
if part.FormName() != "file" {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := part.FileName()
|
||||
if filename == "" {
|
||||
log.Error("no filename")
|
||||
rsp.ErrRsp(c, -1, "no filename")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
filename = filepath.Base(filename)
|
||||
|
||||
if filename != part.FileName() {
|
||||
log.Warn("path detected in filename")
|
||||
rsp.ErrRsp(c, -1, "invalid filename")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(filename, "..") {
|
||||
log.Warn("path traversal attempt")
|
||||
rsp.ErrRsp(c, -1, "invalid filename")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(strings.ToLower(filename), ".iso") {
|
||||
rsp.ErrRsp(c, -1, "only .iso files allowed")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
valid := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
if !valid.MatchString(filename) {
|
||||
rsp.ErrRsp(c, -1, "invalid filename")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(sentinelPath)
|
||||
if err != nil {
|
||||
log.Error("Read failed")
|
||||
rsp.ErrRsp(c, -1, "Read failed")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
outPath := "/data/" + filename
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
log.Error("cannot create file")
|
||||
rsp.ErrRsp(c, -1, "cannot create file")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if strings.Contains(string(data), "start") {
|
||||
err = os.WriteFile(sentinelPath, []byte(filename), 0644)
|
||||
if err != nil {
|
||||
log.Error("Failed to create sentinel file")
|
||||
rsp.ErrRsp(c, -1, "failed to create sentinel file")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(outPath)
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
lw = &loggingWriter{writer: out, totalSize: c.Request.ContentLength}
|
||||
lw.startTicker()
|
||||
} else {
|
||||
if !strings.Contains(string(data), filename) {
|
||||
log.Error("failed")
|
||||
rsp.ErrRsp(c, -1, "failed")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(outPath)
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Direkt streamen → kein RAM-Bedarf außer kleinem Buffer
|
||||
_, err = io.Copy(lw, part)
|
||||
if err != nil {
|
||||
log.Error("write failed")
|
||||
rsp.ErrRsp(c, -1, "write failed")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(outPath)
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := isISO9660(outPath)
|
||||
if err != nil || !ok {
|
||||
rsp.ErrRsp(c, -1, "file is not a valid ISO image")
|
||||
lw.stopTicker()
|
||||
defer os.Remove(outPath)
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
lw.stopTicker()
|
||||
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "idle",
|
||||
File: "",
|
||||
Percentage: "",
|
||||
})
|
||||
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImage(c *gin.Context) {
|
||||
var req proto.MountImageReq
|
||||
var rsp proto.Response
|
||||
|
||||
@@ -69,8 +69,6 @@ func (c *Cli) Stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
return os.Remove(ScriptPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ type Service struct{}
|
||||
const (
|
||||
TailscalePath = "/usr/bin/tailscale"
|
||||
TailscaledPath = "/usr/sbin/tailscaled"
|
||||
ConfigPath = "etc/sysctl.d/99-tailscale.conf"
|
||||
|
||||
GoMemLimit int64 = 75
|
||||
)
|
||||
@@ -40,6 +39,8 @@ func (s *Service) Install(c *gin.Context) {
|
||||
rsp.ErrRsp(c, -1, "install failed")
|
||||
return
|
||||
}
|
||||
|
||||
_ = NewCli().Start()
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
@@ -54,7 +55,6 @@ func (s *Service) Uninstall(c *gin.Context) {
|
||||
|
||||
_ = os.Remove(TailscalePath)
|
||||
_ = os.Remove(TailscaledPath)
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("uninstall tailscale successfully")
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
"time"
|
||||
|
||||
var (
|
||||
hid *Hid
|
||||
hidOnce sync.Once
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Hid struct {
|
||||
@@ -18,6 +17,24 @@ type Hid struct {
|
||||
mouseMutex sync.Mutex
|
||||
}
|
||||
|
||||
const (
|
||||
HID0 = "/dev/hidg0"
|
||||
HID1 = "/dev/hidg1"
|
||||
HID2 = "/dev/hidg2"
|
||||
)
|
||||
|
||||
var (
|
||||
hid *Hid
|
||||
hidOnce sync.Once
|
||||
)
|
||||
|
||||
func GetHid() *Hid {
|
||||
hidOnce.Do(func() {
|
||||
hid = &Hid{}
|
||||
})
|
||||
return hid
|
||||
}
|
||||
|
||||
func (h *Hid) Lock() {
|
||||
h.kbMutex.Lock()
|
||||
h.mouseMutex.Lock()
|
||||
@@ -28,9 +45,117 @@ func (h *Hid) Unlock() {
|
||||
h.mouseMutex.Unlock()
|
||||
}
|
||||
|
||||
func GetHid() *Hid {
|
||||
hidOnce.Do(func() {
|
||||
hid = &Hid{}
|
||||
})
|
||||
return hid
|
||||
func (h *Hid) OpenNoLock() {
|
||||
var err error
|
||||
h.CloseNoLock()
|
||||
|
||||
h.g0, err = os.OpenFile(HID0, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID0, err)
|
||||
}
|
||||
|
||||
h.g1, err = os.OpenFile(HID1, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID1, err)
|
||||
}
|
||||
|
||||
h.g2, err = os.OpenFile(HID2, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID2, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) CloseNoLock() {
|
||||
for _, file := range []*os.File{h.g0, h.g1, h.g2} {
|
||||
if file != nil {
|
||||
_ = file.Sync()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Open() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
|
||||
h.OpenNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) Close() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid0(data []byte) {
|
||||
h.kbMutex.Lock()
|
||||
_, err := h.g0.Write(data)
|
||||
h.kbMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
} else {
|
||||
log.Debugf("write to %s failed: %s", HID0, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID0, data)
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid1(data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
|
||||
h.mouseMutex.Lock()
|
||||
_ = h.g1.SetWriteDeadline(deadline)
|
||||
_, err := h.g1.Write(data)
|
||||
h.mouseMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to %s timeout", HID1)
|
||||
default:
|
||||
log.Errorf("write to %s failed: %s", HID1, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID1, data)
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid2(data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
|
||||
h.mouseMutex.Lock()
|
||||
_ = h.g2.SetWriteDeadline(deadline)
|
||||
_, err := h.g2.Write(data)
|
||||
h.mouseMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to %s timeout", HID2)
|
||||
default:
|
||||
log.Errorf("write to %s failed: %s", HID2, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID2, data)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,14 @@ package hid
|
||||
|
||||
func (h *Hid) Keyboard(queue <-chan []int) {
|
||||
for event := range queue {
|
||||
h.kbMutex.Lock()
|
||||
h.writeKeyboard(event)
|
||||
h.kbMutex.Unlock()
|
||||
code := byte(event[0])
|
||||
|
||||
var modifier byte = 0x00
|
||||
if code > 0 {
|
||||
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
h.WriteHid0(data)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) writeKeyboard(event []int) {
|
||||
code := byte(event[0])
|
||||
|
||||
var modifier byte = 0x00
|
||||
if code > 0 {
|
||||
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
h.Write(h.g0, data)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package hid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -25,7 +22,6 @@ var mouseButtonMap = map[byte]bool{
|
||||
|
||||
func (h *Hid) Mouse(queue <-chan []int) {
|
||||
for event := range queue {
|
||||
h.mouseMutex.Lock()
|
||||
|
||||
switch event[0] {
|
||||
case MouseDown:
|
||||
@@ -39,10 +35,8 @@ func (h *Hid) Mouse(queue <-chan []int) {
|
||||
case MouseScroll:
|
||||
h.mouseScroll(event)
|
||||
default:
|
||||
log.Debugf("invalid mouse event: %+v", event)
|
||||
log.Debugf("invalid mouse event: %v", event)
|
||||
}
|
||||
|
||||
h.mouseMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,29 +44,28 @@ func (h *Hid) mouseDown(event []int) {
|
||||
button := byte(event[1])
|
||||
|
||||
if _, ok := mouseButtonMap[button]; !ok {
|
||||
log.Debugf("invalid mouse button: %+v", event)
|
||||
log.Errorf("invalid mouse button: %v", event)
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte{button, 0, 0, 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseUp() {
|
||||
data := []byte{0, 0, 0, 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseScroll(event []int) {
|
||||
direction := 0x01
|
||||
if event[3] > 0 {
|
||||
if event[3] < 0 {
|
||||
direction = -0x1
|
||||
}
|
||||
|
||||
data := []byte{0, 0, 0, byte(direction)}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseMoveAbsolute(event []int) {
|
||||
x := make([]byte, 2)
|
||||
y := make([]byte, 2)
|
||||
@@ -80,32 +73,10 @@ func (h *Hid) mouseMoveAbsolute(event []int) {
|
||||
binary.LittleEndian.PutUint16(y, uint16(event[3]))
|
||||
|
||||
data := []byte{0, x[0], x[1], y[0], y[1], 0}
|
||||
h.writeWithTimeout(h.g2, data)
|
||||
h.WriteHid2(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseMoveRelative(event []int) {
|
||||
data := []byte{byte(event[1]), byte(event[2]), byte(event[3]), 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
}
|
||||
|
||||
func (h *Hid) writeWithTimeout(file *os.File, data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
_ = file.SetWriteDeadline(deadline)
|
||||
|
||||
_, err := file.Write(data)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Debugf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to hid timeout")
|
||||
default:
|
||||
log.Errorf("write to hid failed: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to hid: %+v", data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *Hid) OpenNoLock() {
|
||||
var err error
|
||||
h.CloseNoLock()
|
||||
|
||||
h.g0, err = os.OpenFile("/dev/hidg0", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg0 failed: %s", err)
|
||||
}
|
||||
|
||||
h.g1, err = os.OpenFile("/dev/hidg1", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg1 failed: %s", err)
|
||||
}
|
||||
|
||||
h.g2, err = os.OpenFile("/dev/hidg2", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg2 failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Open() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
|
||||
h.OpenNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) CloseNoLock() {
|
||||
for _, file := range []*os.File{h.g0, h.g1, h.g2} {
|
||||
if file != nil {
|
||||
_ = file.Sync()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Close() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) Write(file *os.File, data []byte) {
|
||||
_, err := file.Write(data)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
log.Debugf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
} else {
|
||||
log.Errorf("write to hid failed: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to hid: %+v", data)
|
||||
}
|
||||
@@ -16,6 +16,75 @@ type Char struct {
|
||||
|
||||
type PasteReq struct {
|
||||
Content string `form:"content" validate:"required"`
|
||||
Langue string `form:"langue"`
|
||||
}
|
||||
|
||||
func LangueSwitch(base map[rune]Char, lang string) map[rune]Char {
|
||||
// if no language is specified → return base map
|
||||
if lang == "" {
|
||||
return base
|
||||
}
|
||||
|
||||
// always create a copy of the base map
|
||||
m := copyMap(base)
|
||||
|
||||
switch lang {
|
||||
case "de":
|
||||
// swap Y
|
||||
m['y'] = Char{0, 29}
|
||||
m['Y'] = Char{2, 29}
|
||||
|
||||
// swap Z
|
||||
m['z'] = Char{0, 28}
|
||||
m['Z'] = Char{2, 28}
|
||||
|
||||
// add German special characters or remap them
|
||||
m['\u00E4'] = Char{0, 52} // ä
|
||||
m['\u00C4'] = Char{2, 52} // Ä
|
||||
m['\u00F6'] = Char{0, 51} // ö
|
||||
m['\u00D6'] = Char{2, 51} // Ö
|
||||
m['\u00FC'] = Char{0, 47} // ü
|
||||
m['\u00DC'] = Char{2, 47} // Ü
|
||||
m['\u00DF'] = Char{0, 45} // ß
|
||||
|
||||
// swap special characters
|
||||
m['^'] = Char{0, 53} // must be double
|
||||
m['/'] = Char{2, 36} // Shift + 7
|
||||
m['('] = Char{2, 37} // Shift + 8
|
||||
m['&'] = Char{2, 35} // Shift + 6
|
||||
m[')'] = Char{2, 38} // Shift + 9
|
||||
m['`'] = Char{2, 46} // Grave Accent / Backtick
|
||||
m['"'] = Char{2, 31} // Shift + 2
|
||||
m['?'] = Char{2, 45} // Shift + ß
|
||||
m['{'] = Char{0x40, 36} // ALt Gr + 7
|
||||
m['['] = Char{0x40, 37} // ALt Gr + 8
|
||||
m[']'] = Char{0x40, 38} // ALt Gr + 6
|
||||
m['}'] = Char{0x40, 39} // ALt Gr + 0
|
||||
m['\\'] = Char{0x40, 45} // ALt Gr + ß
|
||||
m['@'] = Char{0x40, 20} // ALt Gr + q
|
||||
m['+'] = Char{0, 48} // Shift + +
|
||||
m['*'] = Char{2, 48} // Shift + +
|
||||
m['~'] = Char{0x40, 48} // Shift + +
|
||||
m['#'] = Char{0, 49} // Shift + #
|
||||
m['\''] = Char{2, 49} // Shift + #
|
||||
m['<'] = Char{0, 100} // Shift + <
|
||||
m['>'] = Char{2, 100} // Shift + <
|
||||
m['|'] = Char{0x40, 100} // ALt Gr + <
|
||||
m[';'] = Char{2, 54} // Shift + ,
|
||||
m[':'] = Char{2, 55} // Shift + .
|
||||
m['-'] = Char{0, 56} // Shift + -
|
||||
m['_'] = Char{2, 56} // Shift + -
|
||||
|
||||
// new special characters
|
||||
m['\u00B4'] = Char{0, 46} // ´
|
||||
m['\u00B0'] = Char{2, 53} // °
|
||||
m['\u00A7'] = Char{2, 32} // §
|
||||
m['\u20AC'] = Char{0x40, 8} // €
|
||||
m['\u00B2'] = Char{0x40, 31} // ²
|
||||
m['\u00B3'] = Char{0x40, 32} // ³
|
||||
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *Service) Paste(c *gin.Context) {
|
||||
@@ -28,145 +97,101 @@ func (s *Service) Paste(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(req.Content) > 1024 {
|
||||
rsp.ErrRsp(c, -2, "")
|
||||
rsp.ErrRsp(c, -2, "content too long")
|
||||
return
|
||||
}
|
||||
|
||||
s.hid.kbMutex.Lock()
|
||||
charMapLocal := LangueSwitch(charMap, req.Langue)
|
||||
|
||||
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
for _, char := range req.Content {
|
||||
key, ok := charMap[char]
|
||||
key, ok := charMapLocal[char]
|
||||
if !ok {
|
||||
log.Debugf("unknown key %c", char)
|
||||
log.Debugf("unknown key '%c' (rune: %d)", char, char)
|
||||
continue
|
||||
}
|
||||
|
||||
keyDown := []byte{byte(key.Modifiers), 0x00, byte(key.Code), 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
// only handle shift. Need to handle all modifiers?
|
||||
if key.Modifiers > 0 {
|
||||
keyShift := []byte{0x00, 0x00, byte(225), 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
s.hid.Write(s.hid.g0, keyShift)
|
||||
}
|
||||
|
||||
s.hid.Write(s.hid.g0, keyDown)
|
||||
s.hid.Write(s.hid.g0, keyUp)
|
||||
|
||||
if key.Modifiers > 0 {
|
||||
s.hid.Write(s.hid.g0, keyUp)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
hid.WriteHid0(keyDown)
|
||||
hid.WriteHid0(keyUp)
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
}
|
||||
s.hid.kbMutex.Unlock()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("hid paste success, total %d", len(req.Content))
|
||||
log.Debugf("hid paste success, total %d characters processed", len(req.Content))
|
||||
}
|
||||
|
||||
func copyMap(src map[rune]Char) map[rune]Char {
|
||||
dst := make(map[rune]Char, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
var charMap = map[rune]Char{
|
||||
97: {0, 4}, // a
|
||||
98: {0, 5}, // b
|
||||
99: {0, 6}, // c
|
||||
100: {0, 7}, // d
|
||||
101: {0, 8}, // e
|
||||
102: {0, 9}, // f
|
||||
103: {0, 10}, // g
|
||||
104: {0, 11}, // h
|
||||
105: {0, 12}, // i
|
||||
106: {0, 13}, // j
|
||||
107: {0, 14}, // k
|
||||
108: {0, 15}, // l
|
||||
109: {0, 16}, // m
|
||||
110: {0, 17}, // n
|
||||
111: {0, 18}, // o
|
||||
112: {0, 19}, // p
|
||||
113: {0, 20}, // q
|
||||
114: {0, 21}, // r
|
||||
115: {0, 22}, // s
|
||||
116: {0, 23}, // t
|
||||
117: {0, 24}, // u
|
||||
118: {0, 25}, // v
|
||||
119: {0, 26}, // w
|
||||
120: {0, 27}, // x
|
||||
121: {0, 28}, // y
|
||||
122: {0, 29}, // z
|
||||
// Lowercase letters
|
||||
'a': {0, 4}, 'b': {0, 5}, 'c': {0, 6}, 'd': {0, 7}, 'e': {0, 8},
|
||||
'f': {0, 9}, 'g': {0, 10}, 'h': {0, 11}, 'i': {0, 12}, 'j': {0, 13},
|
||||
'k': {0, 14}, 'l': {0, 15}, 'm': {0, 16}, 'n': {0, 17}, 'o': {0, 18},
|
||||
'p': {0, 19}, 'q': {0, 20}, 'r': {0, 21}, 's': {0, 22}, 't': {0, 23},
|
||||
'u': {0, 24}, 'v': {0, 25}, 'w': {0, 26}, 'x': {0, 27}, 'y': {0, 28},
|
||||
'z': {0, 29},
|
||||
|
||||
49: {0, 30}, // 1
|
||||
50: {0, 31}, // 2
|
||||
51: {0, 32}, // 3
|
||||
52: {0, 33}, // 4
|
||||
53: {0, 34}, // 5
|
||||
54: {0, 35}, // 6
|
||||
55: {0, 36}, // 7
|
||||
56: {0, 37}, // 8
|
||||
57: {0, 38}, // 9
|
||||
48: {0, 39}, // 0
|
||||
// Uppercase letters (Modifier 2 typically means Left Shift)
|
||||
'A': {2, 4}, 'B': {2, 5}, 'C': {2, 6}, 'D': {2, 7}, 'E': {2, 8},
|
||||
'F': {2, 9}, 'G': {2, 10}, 'H': {2, 11}, 'I': {2, 12}, 'J': {2, 13},
|
||||
'K': {2, 14}, 'L': {2, 15}, 'M': {2, 16}, 'N': {2, 17}, 'O': {2, 18},
|
||||
'P': {2, 19}, 'Q': {2, 20}, 'R': {2, 21}, 'S': {2, 22}, 'T': {2, 23},
|
||||
'U': {2, 24}, 'V': {2, 25}, 'W': {2, 26}, 'X': {2, 27}, 'Y': {2, 28},
|
||||
'Z': {2, 29},
|
||||
|
||||
10: {0, 40}, // Enter
|
||||
9: {0, 43}, // Tab
|
||||
32: {0, 44}, // Space
|
||||
45: {0, 45}, // -
|
||||
61: {0, 46}, // =
|
||||
91: {0, 47}, // [
|
||||
93: {0, 48}, // ]
|
||||
92: {0, 49}, // \
|
||||
// Numbers
|
||||
'1': {0, 30}, '2': {0, 31}, '3': {0, 32}, '4': {0, 33}, '5': {0, 34},
|
||||
'6': {0, 35}, '7': {0, 36}, '8': {0, 37}, '9': {0, 38}, '0': {0, 39},
|
||||
|
||||
59: {0, 51}, // ;
|
||||
39: {0, 52}, // '
|
||||
96: {0, 53}, // `
|
||||
44: {0, 54}, // ,
|
||||
46: {0, 55}, // .
|
||||
47: {0, 56}, // /
|
||||
// Shifted numbers / Symbols
|
||||
'!': {2, 30}, // Shift + 1
|
||||
'@': {2, 31}, // Shift + 2
|
||||
'#': {2, 32}, // Shift + 3
|
||||
'$': {2, 33}, // Shift + 4
|
||||
'%': {2, 34}, // Shift + 5
|
||||
'^': {2, 35}, // Shift + 6
|
||||
'&': {2, 36}, // Shift + 7
|
||||
'*': {2, 37}, // Shift + 8
|
||||
'(': {2, 38}, // Shift + 9
|
||||
')': {2, 39}, // Shift + 0
|
||||
|
||||
65: {2, 4}, // A
|
||||
66: {2, 5}, // B
|
||||
67: {2, 6}, // C
|
||||
68: {2, 7}, // D
|
||||
69: {2, 8}, // E
|
||||
70: {2, 9}, // F
|
||||
71: {2, 10}, // G
|
||||
72: {2, 11}, // H
|
||||
73: {2, 12}, // I
|
||||
74: {2, 13}, // J
|
||||
75: {2, 14}, // K
|
||||
76: {2, 15}, // L
|
||||
77: {2, 16}, // M
|
||||
78: {2, 17}, // N
|
||||
79: {2, 18}, // O
|
||||
80: {2, 19}, // P
|
||||
81: {2, 20}, // Q
|
||||
82: {2, 21}, // R
|
||||
83: {2, 22}, // S
|
||||
84: {2, 23}, // T
|
||||
85: {2, 24}, // U
|
||||
86: {2, 25}, // V
|
||||
87: {2, 26}, // W
|
||||
88: {2, 27}, // X
|
||||
89: {2, 28}, // Y
|
||||
90: {2, 29}, // Z
|
||||
// Other common characters
|
||||
'\n': {0, 40}, // Enter (Return)
|
||||
'\t': {0, 43}, // Tab
|
||||
' ': {0, 44}, // Space
|
||||
'-': {0, 45}, // Hyphen / Minus
|
||||
'=': {0, 46}, // Equals
|
||||
'[': {0, 47}, // Left Square Bracket
|
||||
']': {0, 48}, // Right Square Bracket
|
||||
'\\': {0, 49}, // Backslash
|
||||
|
||||
33: {2, 30}, // !
|
||||
64: {2, 31}, // @
|
||||
35: {2, 32}, // #
|
||||
36: {2, 33}, // $
|
||||
37: {2, 34}, // %
|
||||
94: {2, 35}, // ^
|
||||
38: {2, 36}, // &
|
||||
42: {2, 37}, // *
|
||||
40: {2, 38}, // (
|
||||
41: {2, 39}, // )
|
||||
';': {0, 51}, // Semicolon
|
||||
'\'': {0, 52}, // Apostrophe / Single Quote
|
||||
'`': {0, 53}, // Grave Accent / Backtick
|
||||
',': {0, 54}, // Comma
|
||||
'.': {0, 55}, // Period / Dot
|
||||
'/': {0, 56}, // Slash
|
||||
|
||||
95: {2, 45}, // _
|
||||
43: {2, 46}, // +
|
||||
123: {2, 47}, // {
|
||||
125: {2, 48}, // }
|
||||
124: {2, 49}, // |
|
||||
// Shifted symbols
|
||||
'_': {2, 45}, // Underscore (Shift + Hyphen)
|
||||
'+': {2, 46}, // Plus (Shift + Equals)
|
||||
'{': {2, 47}, // Left Curly Brace (Shift + Left Square Bracket)
|
||||
'}': {2, 48}, // Right Curly Brace (Shift + Right Square Bracket)
|
||||
'|': {2, 49}, // Pipe (Shift + Backslash)
|
||||
|
||||
58: {2, 51}, // :
|
||||
34: {2, 52}, // "
|
||||
126: {2, 53}, // ~
|
||||
60: {2, 54}, // <
|
||||
62: {2, 55}, // >
|
||||
63: {2, 56}, // ?
|
||||
':': {2, 51}, // Colon (Shift + Semicolon)
|
||||
'"': {2, 52}, // Double Quote (Shift + Apostrophe)
|
||||
'~': {2, 53}, // Tilde (Shift + Grave Accent)
|
||||
'<': {2, 54}, // Less Than (Shift + Comma)
|
||||
'>': {2, 55}, // Greater Than (Shift + Period)
|
||||
'?': {2, 56}, // Question Mark (Shift + Slash)
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) Reset(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// reset USB
|
||||
f, err := os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "open usb gadget file failed")
|
||||
return
|
||||
}
|
||||
err = f.Truncate(0)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("truncate /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "truncate usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("seek to 0 failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "seek to 0 in usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_, err = f.WriteString("\n")
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "write to usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
devices, err := os.ReadDir("/sys/class/udc/")
|
||||
if err != nil {
|
||||
log.Errorf("read udc directory failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "read udc directory failed")
|
||||
return
|
||||
}
|
||||
|
||||
f, err = os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "open usb gadget file failed")
|
||||
return
|
||||
}
|
||||
for _, device := range devices {
|
||||
_, err = f.WriteString(device.Name() + "\n")
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "write to usb gadget file failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = f.Close()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("reset hid success")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package hid
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -16,11 +17,12 @@ import (
|
||||
const (
|
||||
ModeNormal = "normal"
|
||||
ModeHidOnly = "hid-only"
|
||||
ModeFlag = "/sys/kernel/config/usb_gadget/g0/bcdDevice"
|
||||
|
||||
ModeFlag = "/sys/kernel/config/usb_gadget/g0/bcdDevice"
|
||||
NormalModeScript = "/kvmapp/system/init.d/S03usbdev"
|
||||
HidOnlyModeScript = "/kvmapp/system/init.d/S03usbhid"
|
||||
TargetModeScript = "/etc/init.d/S03usbdev"
|
||||
ModeNormalScript = "/kvmapp/system/init.d/S03usbdev"
|
||||
ModeHidOnlyScript = "/kvmapp/system/init.d/S03usbhid"
|
||||
|
||||
USBDevScript = "/etc/init.d/S03usbdev"
|
||||
)
|
||||
|
||||
var modeMap = map[string]string{
|
||||
@@ -61,9 +63,17 @@ func (s *Service) SetHidMode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
srcScript := NormalModeScript
|
||||
h := GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
srcScript := ModeNormalScript
|
||||
if req.Mode == ModeHidOnly {
|
||||
srcScript = HidOnlyModeScript
|
||||
srcScript = ModeHidOnlyScript
|
||||
}
|
||||
|
||||
if err := copyModeFile(srcScript); err != nil {
|
||||
@@ -78,6 +88,29 @@ func (s *Service) SetHidMode(c *gin.Context) {
|
||||
_ = exec.Command("reboot").Run()
|
||||
}
|
||||
|
||||
func (s *Service) ResetHid(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
h := GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
command := fmt.Sprintf("%s restart_phy", USBDevScript)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset hid: %v", err)
|
||||
rsp.ErrRsp(c, -1, "failed to reset hid")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("reset hid success")
|
||||
}
|
||||
|
||||
func copyModeFile(srcScript string) error {
|
||||
// open the source file
|
||||
srcFile, err := os.Open(srcScript)
|
||||
@@ -98,7 +131,7 @@ func copyModeFile(srcScript string) error {
|
||||
// create and copy to temporary file
|
||||
tmpFile, err := os.CreateTemp("/etc/init.d/", ".S03usbdev-")
|
||||
if err != nil {
|
||||
log.Errorf("failed to create temp %s: %s", TargetModeScript, err)
|
||||
log.Errorf("failed to create temp %s: %s", USBDevScript, err)
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
@@ -131,12 +164,12 @@ func copyModeFile(srcScript string) error {
|
||||
}
|
||||
|
||||
// replace the target file with the temporary file
|
||||
if err := os.Rename(tmpPath, TargetModeScript); err != nil {
|
||||
if err := os.Rename(tmpPath, USBDevScript); err != nil {
|
||||
log.Errorf("failed to rename %s: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("copy %s to %s successful", srcScript, TargetModeScript)
|
||||
log.Debugf("copy %s to %s successful", srcScript, USBDevScript)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
WiFiExistFile = "/etc/kvm/wifi_exist"
|
||||
WiFiSSID = "/etc/kvm/wifi.ssid"
|
||||
WiFiPasswd = "/etc/kvm/wifi.pass"
|
||||
WiFiConnect = "/kvmapp/kvm/wifi_try_connect"
|
||||
WiFiStateFile = "/kvmapp/kvm/wifi_state"
|
||||
WiFiExistFile = "/etc/kvm/wifi_exist"
|
||||
WiFiApModeFile = "/tmp/wifiap"
|
||||
WiFiSSID = "/etc/kvm/wifi.ssid"
|
||||
WiFiPasswd = "/etc/kvm/wifi.pass"
|
||||
WiFiConnect = "/kvmapp/kvm/wifi_try_connect"
|
||||
WiFiStateFile = "/kvmapp/kvm/wifi_state"
|
||||
WiFiScript = "/etc/init.d/S30wifi"
|
||||
)
|
||||
|
||||
func (s *Service) GetWifi(c *gin.Context) {
|
||||
@@ -22,25 +28,55 @@ func (s *Service) GetWifi(c *gin.Context) {
|
||||
|
||||
data := &proto.GetWifiRsp{}
|
||||
|
||||
_, err := os.Stat(WiFiExistFile)
|
||||
if err != nil {
|
||||
data.Supported = isSupported()
|
||||
if !data.Supported {
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
data.Supported = true
|
||||
data.ApMode = isAPMode()
|
||||
|
||||
content, err := os.ReadFile(WiFiStateFile)
|
||||
if err != nil {
|
||||
data.Connected = isConnected()
|
||||
if !data.Connected {
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
state := strings.ReplaceAll(string(content), "\n", "")
|
||||
data.Connected = state == "1"
|
||||
data.Ssid = getWiFiSsid()
|
||||
if data.Ssid == "" {
|
||||
data.Ssid = "Wi-Fi"
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
log.Debugf("get wifi state: %s", state)
|
||||
log.Debugf("get wifi state: %+v", data)
|
||||
}
|
||||
|
||||
func (s *Service) ConnectWifiNoAuth(c *gin.Context) {
|
||||
var req proto.ConnectWifiReq
|
||||
var rsp proto.Response
|
||||
|
||||
// Check Wi-Fi configuration mode
|
||||
if !isSupported() || !isAPMode() {
|
||||
time.Sleep(2 * time.Second)
|
||||
rsp.ErrRsp(c, -1, "invalid mode")
|
||||
return
|
||||
}
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
rsp.ErrRsp(c, -2, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if err := connect(req.Ssid, req.Password); err != nil {
|
||||
rsp.ErrRsp(c, -3, "failed to connect wifi")
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set wifi ap mode successfully")
|
||||
}
|
||||
|
||||
func (s *Service) ConnectWifi(c *gin.Context) {
|
||||
@@ -52,24 +88,97 @@ func (s *Service) ConnectWifi(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiSSID, []byte(req.Ssid), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi ssid: %s", err)
|
||||
rsp.ErrRsp(c, -2, "failed to save wifi")
|
||||
if err := connect(req.Ssid, req.Password); err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to connect wifi")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiPasswd, []byte(req.Password), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi password: %s", err)
|
||||
rsp.ErrRsp(c, -3, "failed to save wifi")
|
||||
// Waiting for Wi-Fi connection
|
||||
maxTries := 7
|
||||
if isConnected() {
|
||||
maxTries = 3
|
||||
time.Sleep(12 * time.Second)
|
||||
}
|
||||
|
||||
for range maxTries {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if isConnected() && getWiFiSsid() != "" {
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set wifi successfully: %s", req.Ssid)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.ErrRsp(c, -3, "failed to get wifi status")
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) DisconnectWifi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
command := fmt.Sprintf("%s stop", WiFiScript)
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Errorf("failed to disconnect wifi: %s", output)
|
||||
rsp.ErrRsp(c, -1, "failed to stop wifi")
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
_ = os.Remove(WiFiSSID)
|
||||
_ = os.Remove(WiFiPasswd)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("stop wifi successfully")
|
||||
}
|
||||
|
||||
func connect(ssid string, password string) error {
|
||||
if err := os.WriteFile(WiFiSSID, []byte(ssid), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi ssid: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiPasswd, []byte(password), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiConnect, nil, 0o644); err != nil {
|
||||
log.Errorf("failed to connect wifi: %s", err)
|
||||
rsp.ErrRsp(c, -4, "failed to connect wifi")
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set wifi successfully: %s", req.Ssid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSupported() bool {
|
||||
_, err := os.Stat(WiFiExistFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func isAPMode() bool {
|
||||
_, err := os.Stat(WiFiApModeFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func isConnected() bool {
|
||||
content, err := os.ReadFile(WiFiStateFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
state := strings.ReplaceAll(string(content), "\n", "")
|
||||
return state == "1"
|
||||
}
|
||||
|
||||
func getWiFiSsid() string {
|
||||
ssidByte, err := os.ReadFile(WiFiSSID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(string(ssidByte), "\n", "")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/service/hid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,6 +21,7 @@ const (
|
||||
imageNone = "/dev/mmcblk0p3"
|
||||
cdromFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/cdrom"
|
||||
mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file"
|
||||
inquiryString = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/inquiry_string"
|
||||
roFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/ro"
|
||||
)
|
||||
|
||||
@@ -91,6 +94,20 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
inquiryVen := "NanoKVM"
|
||||
inquiryPrd := "USB Mass Storage"
|
||||
inquiryVer := 0x0520
|
||||
if req.Cdrom {
|
||||
inquiryPrd = "USB CD/DVD-ROM"
|
||||
}
|
||||
inquiryData := fmt.Sprintf("%-8s%-16s%04x", inquiryVen, inquiryPrd, inquiryVer)
|
||||
|
||||
if err := os.WriteFile(inquiryString, []byte(inquiryData), 0o666); err != nil {
|
||||
log.Errorf("set inquiry %s failed: %s", inquiryData, err)
|
||||
rsp.ErrRsp(c, -2, "set inquiry failed")
|
||||
return
|
||||
}
|
||||
|
||||
// mount
|
||||
image := req.File
|
||||
if image == "" {
|
||||
@@ -103,6 +120,14 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h := hid.GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
// reset usb
|
||||
commands := []string{
|
||||
"echo > /sys/kernel/config/usb_gadget/g0/UDC",
|
||||
@@ -165,3 +190,31 @@ func (s *Service) GetCdRom(c *gin.Context) {
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteImage(c *gin.Context) {
|
||||
var req proto.DeleteImageReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.ToLower(req.File)
|
||||
validPrefix := strings.HasPrefix(filename, imageDirectory)
|
||||
validSuffix := strings.HasSuffix(filename, ".iso") || strings.HasSuffix(filename, ".img")
|
||||
|
||||
if !validPrefix || !validSuffix {
|
||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(req.File); err != nil {
|
||||
rsp.ErrRsp(c, -3, "remove file failed")
|
||||
log.Errorf("failed to remove file %s: %s", req.File, err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("delete image %s success", req.File)
|
||||
}
|
||||
|
||||
44
server/service/stream/direct/h264.go
Normal file
44
server/service/stream/direct/h264.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
streamer = newStreamer()
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to upgrade to websocket: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = ws.Close()
|
||||
log.Debugf("h264 websocket disconnected: %s", ws.RemoteAddr())
|
||||
}()
|
||||
log.Debugf("h264 websocket connected: %s", ws.RemoteAddr())
|
||||
|
||||
_ = ws.SetReadDeadline(time.Time{})
|
||||
|
||||
streamer.addClient(ws)
|
||||
defer streamer.removeClient(ws)
|
||||
|
||||
for {
|
||||
if _, _, err := ws.ReadMessage(); err != nil {
|
||||
log.Debugf("failed to read message (client disconnected): %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
12
server/service/stream/direct/pool.go
Normal file
12
server/service/stream/direct/pool.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var BufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
123
server/service/stream/direct/streamer.go
Normal file
123
server/service/stream/direct/streamer.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Streamer struct {
|
||||
mutex sync.RWMutex
|
||||
clients map[*websocket.Conn]bool
|
||||
running int32
|
||||
}
|
||||
|
||||
func newStreamer() *Streamer {
|
||||
return &Streamer{
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) addClient(ws *websocket.Conn) {
|
||||
s.mutex.Lock()
|
||||
s.clients[ws] = true
|
||||
s.mutex.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||
go s.run()
|
||||
log.Debug("h264 stream started")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) removeClient(ws *websocket.Conn) {
|
||||
s.mutex.Lock()
|
||||
delete(s.clients, ws)
|
||||
s.mutex.Unlock()
|
||||
|
||||
log.Debugf("h264 websocket disconnected, remaining clients: %d", len(s.clients))
|
||||
}
|
||||
|
||||
func (s *Streamer) getClientCount() int {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return len(s.clients)
|
||||
}
|
||||
|
||||
func (s *Streamer) run() {
|
||||
defer atomic.StoreInt32(&s.running, 0)
|
||||
|
||||
duration := time.Second / time.Duration(120)
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
screen := common.GetScreen()
|
||||
vision := common.GetKvmVision()
|
||||
startTime := time.Now()
|
||||
|
||||
for range ticker.C {
|
||||
if s.getClientCount() == 0 {
|
||||
log.Debug("h264 stream stopped due to no clients")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
isKeyFrame := byte(0)
|
||||
if result == 3 {
|
||||
isKeyFrame = byte(1)
|
||||
}
|
||||
|
||||
timestamp := time.Since(startTime).Microseconds()
|
||||
|
||||
if err := s.send(isKeyFrame, timestamp, data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) send(isKeyFrame byte, timestamp int64, data []byte) error {
|
||||
buf := BufferPool.Get().(*bytes.Buffer)
|
||||
defer BufferPool.Put(buf)
|
||||
|
||||
buf.Reset()
|
||||
|
||||
if err := buf.WriteByte(isKeyFrame); err != nil {
|
||||
log.Errorf("failed to write keyframe flag: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
tsBytes := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(tsBytes, uint64(timestamp))
|
||||
if _, err := buf.Write(tsBytes); err != nil {
|
||||
log.Errorf("failed to write timestamp: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Write(data); err != nil {
|
||||
log.Errorf("failed to write h264 data: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for client := range s.clients {
|
||||
if err := client.WriteMessage(websocket.BinaryMessage, buf.Bytes()); err != nil {
|
||||
log.Errorf("failed to write message to client %s: %s.", client.RemoteAddr(), err)
|
||||
|
||||
s.removeClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func send() {
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 {
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@ func send() {
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("send h264 data: %d", len(data))
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
|
||||
@@ -1,99 +1,22 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
chanMap = make(map[*gin.Context]bool)
|
||||
mutex = sync.Mutex{}
|
||||
exitSig = make(chan bool, 1)
|
||||
)
|
||||
var streamer = NewStreamer()
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
c.Header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("X-Server-Date", time.Now().Format(time.RFC1123))
|
||||
|
||||
mutex.Lock()
|
||||
chanMap[c] = true
|
||||
if len(chanMap) == 1 {
|
||||
go send()
|
||||
}
|
||||
mutex.Unlock()
|
||||
streamer.AddClient(c)
|
||||
defer streamer.RemoveClient(c)
|
||||
|
||||
<-c.Request.Context().Done()
|
||||
|
||||
mutex.Lock()
|
||||
delete(chanMap, c)
|
||||
if len(chanMap) == 0 {
|
||||
exitSig <- true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
func send() {
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
|
||||
fps := screen.FPS
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(fps))
|
||||
defer ticker.Stop()
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
data, result := vision.ReadMjpeg(screen.Width, screen.Height, screen.Quality)
|
||||
if result < 0 || result == 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
for c := range chanMap {
|
||||
if err := write(c, data); err != nil {
|
||||
log.Debugf("failed to write mjpeg data: %s", err)
|
||||
}
|
||||
}
|
||||
log.Debugf("send mjpeg data: %d", len(data))
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
}
|
||||
|
||||
case <-exitSig:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func write(c *gin.Context, data []byte) (err error) {
|
||||
if _, err = c.Writer.Write([]byte("--frame\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("Content-Type: image/jpeg\r\n\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
131
server/service/stream/mjpeg/streamer.go
Normal file
131
server/service/stream/mjpeg/streamer.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Streamer struct {
|
||||
mutex sync.RWMutex
|
||||
clients map[*gin.Context]bool
|
||||
running int32
|
||||
}
|
||||
|
||||
func NewStreamer() *Streamer {
|
||||
return &Streamer{
|
||||
clients: make(map[*gin.Context]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) AddClient(c *gin.Context) {
|
||||
s.mutex.Lock()
|
||||
s.clients[c] = true
|
||||
s.mutex.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||
go s.run()
|
||||
log.Debug("mjpeg stream started")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) RemoveClient(c *gin.Context) {
|
||||
s.mutex.Lock()
|
||||
delete(s.clients, c)
|
||||
s.mutex.Unlock()
|
||||
|
||||
log.Debugf("mjpeg connection removed, remaining clients: %d", len(s.clients))
|
||||
}
|
||||
|
||||
func (s *Streamer) getClients() []*gin.Context {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
clients := make([]*gin.Context, 0, len(s.clients))
|
||||
for c := range s.clients {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (s *Streamer) getClientCount() int {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return len(s.clients)
|
||||
}
|
||||
|
||||
func (s *Streamer) run() {
|
||||
defer atomic.StoreInt32(&s.running, 0)
|
||||
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
fps := screen.FPS
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(fps))
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if s.getClientCount() == 0 {
|
||||
log.Debug("mjpeg stream stopped due to no clients")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadMjpeg(screen.Width, screen.Height, screen.Quality)
|
||||
if result < 0 || result == 5 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
clients := s.getClients()
|
||||
for _, client := range clients {
|
||||
if err := writeFrame(client, data); err != nil {
|
||||
log.Errorf("failed to write mjpeg frame for client %s: %s", client.Request.RemoteAddr, err)
|
||||
s.RemoveClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
if screen.FPS != fps && screen.FPS != 0 {
|
||||
fps = screen.FPS
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
|
||||
func writeFrame(c *gin.Context, data []byte) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = c.Request.Context().Err()
|
||||
if err == nil {
|
||||
err = fmt.Errorf("panic recovered in writeFrame: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
header := "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + strconv.Itoa(len(data)) + "\r\n\r\n"
|
||||
if _, err = c.Writer.WriteString(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("\r\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writer.Flush()
|
||||
return nil
|
||||
}
|
||||
112
server/service/stream/webrtc/client.go
Normal file
112
server/service/stream/webrtc/client.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewClient(ws *websocket.Conn, videoConn *webrtc.PeerConnection) *Client {
|
||||
return &Client{
|
||||
ws: ws,
|
||||
video: videoConn,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WriteMessage(event string, data string) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
message := &Message{
|
||||
Event: event,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if err := c.ws.WriteJSON(message); err != nil {
|
||||
log.Errorf("failed to send message %s: %v", event, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("sent message %s", event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadMessage() (*Message, error) {
|
||||
_, raw, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read message: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
log.Errorf("failed to unmarshal message: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack() error {
|
||||
// video track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
|
||||
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264},
|
||||
"video",
|
||||
"pion-video",
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create video track: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
videoPacketizer := rtp.NewPacketizer(
|
||||
1200,
|
||||
100,
|
||||
0x1234ABCD,
|
||||
&codecs.H264Payloader{},
|
||||
rtp.NewRandomSequencer(),
|
||||
90000,
|
||||
)
|
||||
if videoPacketizer == nil {
|
||||
err := errors.New("failed to create rtp packetizer")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
videoSender, err := c.video.AddTrack(videoTrack)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add video track: %s", err)
|
||||
return err
|
||||
}
|
||||
go startRTCPReader(videoSender)
|
||||
|
||||
track := &Track{
|
||||
videoPacketizer: videoPacketizer,
|
||||
video: videoTrack,
|
||||
}
|
||||
track.updateExtension()
|
||||
|
||||
c.mutex.Lock()
|
||||
c.track = track
|
||||
c.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startRTCPReader(sender *webrtc.RTPSender) {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
if _, _, err := sender.Read(rtcpBuf); err != nil {
|
||||
log.Debugf("RTCP reader error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
158
server/service/stream/webrtc/h264.go
Normal file
158
server/service/stream/webrtc/h264.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
globalManager *WebRTCManager
|
||||
managerOnce sync.Once
|
||||
)
|
||||
|
||||
func getManager() *WebRTCManager {
|
||||
managerOnce.Do(func() {
|
||||
globalManager = NewWebRTCManager()
|
||||
})
|
||||
return globalManager
|
||||
}
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
// create WebSocket connection
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 websocket: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = wsConn.Close()
|
||||
log.Debugf("h264 websocket disconnected: %s", c.ClientIP())
|
||||
}()
|
||||
log.Debugf("h264 websocket connected: %s", c.ClientIP())
|
||||
|
||||
var zeroTime time.Time
|
||||
_ = wsConn.SetReadDeadline(zeroTime)
|
||||
|
||||
// create video connection
|
||||
iceServers := createICEServers()
|
||||
|
||||
mediaEngine, err := createMediaEngine()
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 media engine: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
videoConn, err := createPeerConnection(iceServers, mediaEngine)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 video peer connection: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = videoConn.Close()
|
||||
log.Debugf("h264 video peer disconnected: %s", c.ClientIP())
|
||||
}()
|
||||
|
||||
// create client
|
||||
client := NewClient(wsConn, videoConn)
|
||||
if err := client.AddTrack(); err != nil {
|
||||
log.Errorf("failed to add track: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
manager := getManager()
|
||||
manager.AddClient(wsConn, client)
|
||||
defer manager.RemoveClient(wsConn)
|
||||
|
||||
// handle signaling
|
||||
signalingHandler := NewSignalingHandler(client)
|
||||
signalingHandler.RegisterCallbacks()
|
||||
|
||||
// read and wait
|
||||
for {
|
||||
message, err := client.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if message != nil {
|
||||
if err := signalingHandler.HandleMessage(message); err != nil {
|
||||
log.Errorf("failed to handle signaling message: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createICEServers() []webrtc.ICEServer {
|
||||
var iceServers []webrtc.ICEServer
|
||||
|
||||
conf := config.GetInstance()
|
||||
|
||||
if conf.Stun != "" && conf.Stun != "disable" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"stun:" + conf.Stun},
|
||||
})
|
||||
}
|
||||
|
||||
if conf.Turn.TurnAddr != "" && conf.Turn.TurnUser != "" && conf.Turn.TurnCred != "" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"turn:" + conf.Turn.TurnAddr},
|
||||
Username: conf.Turn.TurnUser,
|
||||
Credential: conf.Turn.TurnCred,
|
||||
})
|
||||
}
|
||||
|
||||
return iceServers
|
||||
}
|
||||
|
||||
func createMediaEngine() (*webrtc.MediaEngine, error) {
|
||||
mediaEngine := &webrtc.MediaEngine{}
|
||||
|
||||
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
||||
log.Errorf("failed to register default codecs: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mediaEngine.RegisterHeaderExtension(
|
||||
webrtc.RTPHeaderExtensionCapability{URI: "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"},
|
||||
webrtc.RTPCodecTypeVideo,
|
||||
); err != nil {
|
||||
log.Errorf("failed to register header extension: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mediaEngine, nil
|
||||
}
|
||||
|
||||
func createPeerConnection(iceServers []webrtc.ICEServer, mediaEngine *webrtc.MediaEngine) (*webrtc.PeerConnection, error) {
|
||||
settingEngine := webrtc.SettingEngine{}
|
||||
settingEngine.SetSRTPProtectionProfiles(
|
||||
dtls.SRTP_AEAD_AES_128_GCM,
|
||||
dtls.SRTP_AES128_CM_HMAC_SHA1_80,
|
||||
)
|
||||
|
||||
apiOptions := []func(api *webrtc.API){
|
||||
webrtc.WithSettingEngine(settingEngine),
|
||||
}
|
||||
if mediaEngine != nil {
|
||||
apiOptions = append(apiOptions, webrtc.WithMediaEngine(mediaEngine))
|
||||
}
|
||||
|
||||
api := webrtc.NewAPI(apiOptions...)
|
||||
|
||||
return api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
||||
})
|
||||
}
|
||||
96
server/service/stream/webrtc/manager.go
Normal file
96
server/service/stream/webrtc/manager.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewWebRTCManager() *WebRTCManager {
|
||||
return &WebRTCManager{
|
||||
clients: make(map[*websocket.Conn]*Client),
|
||||
videoSending: 0,
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) AddClient(ws *websocket.Conn, client *Client) {
|
||||
client.track.updateExtension()
|
||||
|
||||
m.mutex.Lock()
|
||||
m.clients[ws] = client
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("added client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) RemoveClient(ws *websocket.Conn) {
|
||||
m.mutex.Lock()
|
||||
delete(m.clients, ws)
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("removed client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) GetClientCount() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return len(m.clients)
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) StartVideoStream() {
|
||||
if atomic.CompareAndSwapInt32(&m.videoSending, 0, 1) {
|
||||
go m.sendVideoStream()
|
||||
log.Debugf("start sending h264 stream")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) sendVideoStream() {
|
||||
defer atomic.StoreInt32(&m.videoSending, 0)
|
||||
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if m.GetClientCount() == 0 {
|
||||
log.Debugf("stop sending h264 stream")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sample := media.Sample{
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
for _, client := range m.clients {
|
||||
client.track.writeVideo(sample)
|
||||
}
|
||||
|
||||
if screen.FPS != fps && screen.FPS != 0 {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
149
server/service/stream/webrtc/signaling.go
Normal file
149
server/service/stream/webrtc/signaling.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewSignalingHandler(client *Client) *SignalingHandler {
|
||||
return &SignalingHandler{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCallbacks Register callback functions
|
||||
func (s *SignalingHandler) RegisterCallbacks() {
|
||||
// video ICE candidate
|
||||
s.client.video.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidateByte, err := json.Marshal(candidate.ToJSON())
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal video candidate: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.client.WriteMessage("video-candidate", string(candidateByte)); err != nil {
|
||||
log.Errorf("failed to send video candidate: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
manager := getManager()
|
||||
|
||||
// video connection state change
|
||||
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state == webrtc.ICEConnectionStateConnected {
|
||||
manager.StartVideoStream()
|
||||
}
|
||||
|
||||
log.Debugf("video connection state changed to %s", state.String())
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMessage handle the received message
|
||||
func (s *SignalingHandler) HandleMessage(message *Message) error {
|
||||
switch message.Event {
|
||||
case "video-offer":
|
||||
return s.handleVideoOffer(message.Data)
|
||||
case "video-candidate":
|
||||
return s.handleVideoCandidate(message.Data)
|
||||
case "heartbeat":
|
||||
return s.handleHeartbeat()
|
||||
default:
|
||||
log.Debugf("Unhandled message event: %s", message.Event)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalingHandler) handleVideoOffer(data string) error {
|
||||
if s.client.video.SignalingState() != webrtc.SignalingStateStable {
|
||||
err := errors.New("video signaling is not stable")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
offer := webrtc.SessionDescription{}
|
||||
if err := json.Unmarshal([]byte(data), &offer); err != nil {
|
||||
log.Errorf("failed to unmarshal video offer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetRemoteDescription(offer); err != nil {
|
||||
log.Errorf("failed to set remote description: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := s.client.video.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetLocalDescription(answer); err != nil {
|
||||
log.Errorf("failed to set local description: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.updateHeaderExtensionID(); err != nil {
|
||||
log.Errorf("could not update header extension ID: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
answerByte, err := json.Marshal(answer)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.WriteMessage("video-answer", string(answerByte))
|
||||
}
|
||||
|
||||
// set extension ID
|
||||
func (s *SignalingHandler) updateHeaderExtensionID() error {
|
||||
receivers := s.client.video.GetReceivers()
|
||||
if len(receivers) == 0 {
|
||||
return errors.New("no RTP receiver found for video")
|
||||
}
|
||||
|
||||
params := receivers[0].GetParameters()
|
||||
if len(params.HeaderExtensions) == 0 {
|
||||
return errors.New("no header extensions found in negotiated parameters")
|
||||
}
|
||||
|
||||
for _, ext := range params.HeaderExtensions {
|
||||
if ext.URI == "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" {
|
||||
s.client.track.playoutDelayExtensionID = uint8(ext.ID)
|
||||
log.Debugf("found and set playout delay extension ID to: %d", ext.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("no track extension found in negotiated parameters, use default value 5")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle video candidate
|
||||
func (s *SignalingHandler) handleVideoCandidate(data string) error {
|
||||
candidate := webrtc.ICECandidateInit{}
|
||||
if err := json.Unmarshal([]byte(data), &candidate); err != nil {
|
||||
log.Errorf("failed to unmarshal candidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.AddICECandidate(candidate); err != nil {
|
||||
log.Errorf("failed to add ICECandidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle heartbeat
|
||||
func (s *SignalingHandler) handleHeartbeat() error {
|
||||
return s.client.WriteMessage("heartbeat", "")
|
||||
}
|
||||
53
server/service/stream/webrtc/track.go
Normal file
53
server/service/stream/webrtc/track.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (t *Track) updateExtension() {
|
||||
if t.playoutDelayExtensionID == 0 {
|
||||
t.playoutDelayExtensionID = 5
|
||||
}
|
||||
|
||||
if t.playoutDelayExtensionData == nil || len(t.playoutDelayExtensionData) == 0 {
|
||||
playoutDelay := &rtp.PlayoutDelayExtension{
|
||||
MinDelay: 0,
|
||||
MaxDelay: 0,
|
||||
}
|
||||
playoutDelayExtensionData, err := playoutDelay.Marshal()
|
||||
if err == nil {
|
||||
t.playoutDelayExtensionData = playoutDelayExtensionData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Track) writeVideoSample(sample media.Sample) error {
|
||||
samples := uint32(sample.Duration.Seconds() * 90000)
|
||||
packets := t.videoPacketizer.Packetize(sample.Data, samples)
|
||||
|
||||
for _, p := range packets {
|
||||
p.Header.Extension = true
|
||||
p.Header.ExtensionProfile = 0xBEDE
|
||||
|
||||
if err := p.Header.SetExtension(t.playoutDelayExtensionID, t.playoutDelayExtensionData); err != nil {
|
||||
log.Errorf("Failed to set extension: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.video.WriteRTP(p); err != nil {
|
||||
log.Errorf("failed to write RTP: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) writeVideo(sample media.Sample) {
|
||||
err := t.writeVideoSample(sample)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write h264 video: %s", err)
|
||||
}
|
||||
}
|
||||
38
server/service/stream/webrtc/types.go
Normal file
38
server/service/stream/webrtc/types.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type WebRTCManager struct {
|
||||
clients map[*websocket.Conn]*Client
|
||||
videoSending int32
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
video *webrtc.PeerConnection
|
||||
track *Track
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type SignalingHandler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
playoutDelayExtensionID uint8
|
||||
playoutDelayExtensionData []byte
|
||||
videoPacketizer rtp.Packetizer
|
||||
video *webrtc.TrackLocalStaticRTP
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
104
server/service/vm/autostart.go
Normal file
104
server/service/vm/autostart.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const autostartDirectory = "/etc/kvm/autostart"
|
||||
|
||||
func (s *Service) GetAutostart(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
var files []string
|
||||
err := filepath.Walk(autostartDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
files = append(files, info.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get autostart directory fail")
|
||||
return
|
||||
}
|
||||
rsp.OkRspWithData(c, &proto.GetAutostartRsp{
|
||||
Files: files,
|
||||
})
|
||||
|
||||
log.Debugf("get autostart total %d", len(files))
|
||||
}
|
||||
|
||||
func (s *Service) UploadAutostart(c *gin.Context) {
|
||||
var req proto.UploadAutostartReq
|
||||
var rsp proto.Response
|
||||
|
||||
fileName := c.Param("name")
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "parse form request fail")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(autostartDirectory); err != nil {
|
||||
_ = os.MkdirAll(autostartDirectory, 0o755)
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s/%s", autostartDirectory, fileName)
|
||||
|
||||
f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "create file fail")
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(req.Content)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "write content fail")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, fileName)
|
||||
|
||||
log.Debugf("upload autostart %s success", fileName)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAutostart(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
fileName := c.Param("name")
|
||||
|
||||
file := fmt.Sprintf("%s/%s", autostartDirectory, fileName)
|
||||
if err := os.Remove(file); err != nil {
|
||||
log.Errorf("delete autostart file %s fail", fileName)
|
||||
rsp.ErrRsp(c, -3, "remove file fail")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("delete autostart %s success", fileName)
|
||||
}
|
||||
|
||||
func (s *Service) GetAutostartContent(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
fileName := c.Param("name")
|
||||
file := fmt.Sprintf("%s/%s", autostartDirectory, fileName)
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read file fail")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, string(content))
|
||||
log.Debugf("get autostart content %s success", fileName)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
"NanoKVM-Server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -17,7 +19,42 @@ func (s *Service) ResetHdmi(c *gin.Context) {
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(1 * time.Second)
|
||||
vision.SetHDMI(true)
|
||||
utils.PersistHDMIEnabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("reset hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) EnableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(true)
|
||||
utils.PersistHDMIEnabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("enable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) DisableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(false)
|
||||
utils.PersistHDMIDisabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("disable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) GetHdmiState(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetGetHdmiStateRsp{
|
||||
Enabled: !utils.IsHdmiDisabled(),
|
||||
})
|
||||
|
||||
log.Debug("get hdmi state")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package vm
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
const (
|
||||
BootHostnameFile = "/boot/hostname"
|
||||
EtcHostname = "/etc/hostname"
|
||||
EtcHosts = "/etc/hosts"
|
||||
)
|
||||
|
||||
func (s *Service) SetHostname(c *gin.Context) {
|
||||
@@ -25,6 +27,29 @@ func (s *Service) SetHostname(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dataRead, err := os.ReadFile(EtcHostname)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hostname failed")
|
||||
return
|
||||
}
|
||||
|
||||
oldHostname := strings.Replace(string(dataRead), "\n", "", -1)
|
||||
|
||||
if (oldHostname != req.Hostname) {
|
||||
dataRead, err = os.ReadFile(EtcHosts)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hosts failed")
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte(strings.Replace(string(dataRead), oldHostname, req.Hostname, -1))
|
||||
|
||||
if err := os.WriteFile(EtcHosts, data, 0o644); err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to write data")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := []byte(fmt.Sprintf("%s", req.Hostname))
|
||||
|
||||
if err := os.WriteFile(BootHostnameFile, data, 0o644); err != nil {
|
||||
@@ -39,6 +64,8 @@ func (s *Service) SetHostname(c *gin.Context) {
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set Hostname: %s", req.Hostname)
|
||||
|
||||
_ = exec.Command("hostname", "-F", EtcHostname).Run()
|
||||
}
|
||||
|
||||
func (s *Service) GetHostname(c *gin.Context) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jiggler
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/service/hid"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -117,3 +118,17 @@ func (j *Jiggler) IsEnabled() bool {
|
||||
func (j *Jiggler) GetMode() string {
|
||||
return j.mode
|
||||
}
|
||||
|
||||
func move(mode string) {
|
||||
h := hid.GetHid()
|
||||
|
||||
if mode == "absolute" {
|
||||
h.WriteHid2([]byte{0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00})
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
h.WriteHid2([]byte{0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00})
|
||||
} else {
|
||||
h.WriteHid1([]byte{0x00, 0xa, 0xa, 0x00})
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
h.WriteHid1([]byte{0x00, 0xf6, 0xf6, 0x00})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package jiggler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func move(mode string) {
|
||||
var (
|
||||
hid string
|
||||
data [][]byte
|
||||
)
|
||||
|
||||
if mode == "absolute" {
|
||||
hid = "/dev/hidg2"
|
||||
data = [][]byte{
|
||||
{0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00},
|
||||
{0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00},
|
||||
}
|
||||
} else {
|
||||
hid = "/dev/hidg1"
|
||||
data = [][]byte{
|
||||
{0x00, 0x0a, 0x0a, 0x00},
|
||||
{0x00, 0xf6, 0xf6, 0x00},
|
||||
}
|
||||
}
|
||||
|
||||
write(hid, data)
|
||||
}
|
||||
|
||||
func write(hid string, data [][]byte) {
|
||||
file, err := os.OpenFile(hid, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open %s: %s", hid, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
for _, b := range data {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
if err := file.SetWriteDeadline(deadline); err != nil {
|
||||
log.Errorf("failed to set deadline: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := file.Write(b); err != nil {
|
||||
log.Errorf("failed to write: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func (s *Service) Terminal(c *gin.Context) {
|
||||
defer func() {
|
||||
_ = ptmx.Close()
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
go wsWrite(ws, ptmx)
|
||||
|
||||
76
server/service/vm/tls.go
Normal file
76
server/service/vm/tls.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
func (s *Service) SetTls(c *gin.Context) {
|
||||
var req proto.SetTlsReq
|
||||
var rsp proto.Response
|
||||
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("invalid arguments: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enabled {
|
||||
err = enableTls()
|
||||
} else {
|
||||
err = disableTls()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to set TLS: %s", err)
|
||||
rsp.ErrRsp(c, -2, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
|
||||
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
|
||||
}
|
||||
|
||||
func enableTls() error {
|
||||
if err := utils.GenerateCert(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf, err := config.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf.Proto = "https"
|
||||
conf.Cert.Crt = "/etc/kvm/server.crt"
|
||||
conf.Cert.Key = "/etc/kvm/server.key"
|
||||
|
||||
if err := config.Write(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableTls() error {
|
||||
conf, err := config.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf.Proto = "http"
|
||||
|
||||
if err := config.Write(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
server/utils/cert.go
Normal file
100
server/utils/cert.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GenerateCert() error {
|
||||
var (
|
||||
host = "localhost"
|
||||
ipAddress = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
|
||||
validFor = time.Hour * 24 * 365 * 10
|
||||
certFile = "/etc/kvm/server.crt"
|
||||
keyFile = "/etc/kvm/server.key"
|
||||
)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate RSA private key: %v", err)
|
||||
return err
|
||||
}
|
||||
publicKey := &privateKey.PublicKey
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate serial number: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: host,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(validFor),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
DNSNames: []string{host},
|
||||
IPAddresses: ipAddress,
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create certificate: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// generate certificate
|
||||
certOut, err := os.Create(certFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create %s: %v", certFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
log.Errorf("failed to encode %s: %v", certFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
_ = certOut.Sync()
|
||||
_ = certOut.Close()
|
||||
log.Debugf("%s generated", certFile)
|
||||
|
||||
// generate private key
|
||||
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) // 权限 0600
|
||||
if err != nil {
|
||||
log.Errorf("failed to create %s: %v", keyFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
privateBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal private key: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateBytes}); err != nil {
|
||||
log.Errorf("failed to encode %s: %v", keyFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
_ = keyOut.Sync()
|
||||
_ = keyOut.Close()
|
||||
log.Debugf("%s generated", keyFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
38
server/utils/hdmi.go
Normal file
38
server/utils/hdmi.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
HDMIDisableFile = "/etc/kvm/hdmi_disable"
|
||||
)
|
||||
|
||||
func PersistHDMIDisabled() {
|
||||
f, err := os.OpenFile(HDMIDisableFile, os.O_CREATE|os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
log.Error("failed to create hdmi disable file:", err)
|
||||
return
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func PersistHDMIEnabled() {
|
||||
if err := os.Remove(HDMIDisableFile); err != nil {
|
||||
log.Error("failed to remove hdmi disable file:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func IsHdmiDisabled() bool {
|
||||
if _, err := os.Stat(HDMIDisableFile); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false // HDMI is enabled
|
||||
}
|
||||
log.Error("failed to check hdmi disable file:", err)
|
||||
return false // Assume HDMI is enabled on error
|
||||
}
|
||||
return true // HDMI is disabled
|
||||
}
|
||||
@@ -95,10 +95,11 @@ typedef struct {
|
||||
int8_t type = -1; // cat /kvmapp/kvm/type
|
||||
int8_t now_fps = -1; // cat /kvmapp/kvm/now_fps
|
||||
int16_t qlty = -1; // cat /kvmapp/kvm/qlty
|
||||
uint8_t oled_sleep_param = 0;
|
||||
uint16_t oled_sleep_param = 0;
|
||||
uint8_t oled_sleep_state = 0; // 0:wakeup; 1:sleep;
|
||||
uint64_t oled_sleep_start = 0;
|
||||
uint64_t ue_patch_state = 0;
|
||||
} kvm_oled_state_t;
|
||||
|
||||
#endif // CONFIG_H_
|
||||
|
||||
|
||||
BIN
tools/nanokvm_update_edid/E21_NanoKVM.bin
Normal file
BIN
tools/nanokvm_update_edid/E21_NanoKVM.bin
Normal file
Binary file not shown.
22
tools/nanokvm_update_edid/Makefile
Normal file
22
tools/nanokvm_update_edid/Makefile
Normal file
@@ -0,0 +1,22 @@
|
||||
# CROSS_COMPILE = aarch64-none-linux-gnu-
|
||||
# CROSS_COMPILE = riscv64-unknown-linux-gnu-
|
||||
CROSS_COMPILE = riscv64-unknown-linux-musl-
|
||||
# CROSS_COMPILE = riscv64-unknown-elf-
|
||||
|
||||
CFLAGS += -I/usr/local/RISC-V-toolchain/riscv64-linux-musl-x86_64/sysroot/usr/include
|
||||
|
||||
CC = $(CROSS_COMPILE)gcc
|
||||
|
||||
SRC = nanokvm_update_edid.c
|
||||
|
||||
TARGET = nanokvm_update_edid
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) -march=rv64gc -o $@ $^
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: all clean
|
||||
BIN
tools/nanokvm_update_edid/nanokvm_update_edid
Executable file
BIN
tools/nanokvm_update_edid/nanokvm_update_edid
Executable file
Binary file not shown.
749
tools/nanokvm_update_edid/nanokvm_update_edid.c
Normal file
749
tools/nanokvm_update_edid/nanokvm_update_edid.c
Normal file
@@ -0,0 +1,749 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <linux/i2c-dev.h>
|
||||
#include <linux/i2c.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "nanokvm_update_edid.h"
|
||||
|
||||
|
||||
int client;
|
||||
// old offset set to 0xff first
|
||||
static uint8_t old_offset = 0xff;
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
int get_edid_from_file(const char *filename, uint8_t *edid_data, uint16_t *edid_size) {
|
||||
FILE *file = fopen(filename, "rb");
|
||||
if (!file) {
|
||||
perror("Failed to open EDID file");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*edid_size = fread(edid_data, 1, EDID_BUFFER_SIZE, file);
|
||||
fclose(file);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int check_edid(uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
uint16_t i;
|
||||
// Check if the EDID data length is valid
|
||||
if (edid_size != EDID_BUFFER_SIZE) {
|
||||
fprintf(stderr, "EDID data length is not %d bytes\n", EDID_BUFFER_SIZE);
|
||||
return -1; // EDID data length is not enough
|
||||
}
|
||||
|
||||
// Check EDID header
|
||||
if (edid_data[0] != 0x00 || edid_data[1] != 0xFF ||
|
||||
edid_data[2] != 0xFF || edid_data[3] != 0xFF ||
|
||||
edid_data[4] != 0xFF || edid_data[5] != 0xFF ||
|
||||
edid_data[6] != 0xFF || edid_data[7] != 0x00) {
|
||||
fprintf(stderr, "EDID header is invalid\n");
|
||||
return -1; // EDID header is invalid
|
||||
}
|
||||
|
||||
// First 128 Bytes checksum
|
||||
uint8_t checksum1 = 0;
|
||||
for (i = 0; i < 127; i++) {
|
||||
checksum1 += edid_data[i];
|
||||
}
|
||||
checksum1 = 0x100 - checksum1; // Reverse
|
||||
if (checksum1 != edid_data[127]) {
|
||||
// Checksum for first 128 bytes is incorrect
|
||||
fprintf(stderr, "Checksum for first 128 bytes is incorrect\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Second 128 Bytes checksum
|
||||
uint8_t checksum2 = 0;
|
||||
for (i = 128; i < 255; i++) {
|
||||
checksum2 += edid_data[i];
|
||||
}
|
||||
checksum2 = 0x100 - checksum2; // Reverse
|
||||
if (checksum2 != edid_data[255]) {
|
||||
// Checksum for second 128 bytes is incorrect
|
||||
fprintf(stderr, "Checksum for second 128 bytes is incorrect\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0; // EDID is valid
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
void NanoKVM_PCIe_HDMI_Reset(void)
|
||||
{
|
||||
system("echo 0 > /sys/class/gpio/gpio451/value");
|
||||
usleep(100000);
|
||||
system("echo 1 > /sys/class/gpio/gpio451/value");
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
// I2C write function
|
||||
int i2c_write_byte(uint8_t offset, uint8_t reg, uint8_t data)
|
||||
{
|
||||
// reg buffer
|
||||
uint8_t reg_buf[2] = {0};
|
||||
|
||||
// if offset is changed, write it first
|
||||
if (offset != old_offset) {
|
||||
old_offset = offset;
|
||||
reg_buf[0] = LT6911_REG_OFFSET; // Set the offset register
|
||||
reg_buf[1] = offset; // Set the register to read
|
||||
if (write(client, reg_buf, 2) != 2) {
|
||||
perror("Failed to write offset to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// write the data to the i2c bus
|
||||
reg_buf[0] = reg; // Set the register to write
|
||||
reg_buf[1] = data; // Set the data to write
|
||||
if (write(client, reg_buf, 2) != 2) {
|
||||
perror("Failed to write to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int i2c_write_bytes(uint8_t offset, uint8_t reg, const uint8_t *data, size_t len)
|
||||
{
|
||||
// check len
|
||||
if (len == 0) {
|
||||
fprintf(stderr, "Data length must be greater than 0.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// reg buffer
|
||||
uint8_t reg_buf[1 + len];
|
||||
|
||||
// if offset is changed, write it first
|
||||
if (offset != old_offset) {
|
||||
old_offset = offset;
|
||||
reg_buf[0] = LT6911_REG_OFFSET; // Set the offset register
|
||||
reg_buf[1] = offset; // Set the register to read
|
||||
if (write(client, reg_buf, 2) != 2) {
|
||||
perror("Failed to write offset to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// write the data to the i2c bus
|
||||
reg_buf[0] = reg;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
reg_buf[i + 1] = data[i];
|
||||
}
|
||||
|
||||
// write to the I2C bus
|
||||
if (write(client, reg_buf, 1 + len) != 1 + len) {
|
||||
perror("Failed to write to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// I2C read function
|
||||
int i2c_read_byte(uint8_t offset, uint8_t reg, uint8_t *data)
|
||||
{
|
||||
// reg buffer
|
||||
uint8_t reg_buf[2] = {0};
|
||||
|
||||
// if offset is changed, write it first
|
||||
if (offset != old_offset) {
|
||||
old_offset = offset;
|
||||
reg_buf[0] = LT6911_REG_OFFSET; // Set the offset register
|
||||
reg_buf[1] = offset; // Set the register to read
|
||||
if (write(client, reg_buf, 2) != 2) {
|
||||
perror("Failed to write offset to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// write the register address to read
|
||||
reg_buf[0] = reg; // Set the register to read
|
||||
if (write(client, reg_buf, 1) != 1) {
|
||||
perror("Failed to write register address to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// read the data from the i2c bus
|
||||
if (read(client, data, 1) != 1 ) {
|
||||
perror("Failed to read from the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int i2c_read_bytes(uint8_t offset, uint8_t reg, uint8_t *data, size_t len)
|
||||
{
|
||||
// 检查数据长度
|
||||
if (len == 0) {
|
||||
fprintf(stderr, "Data length must be greater than 0.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t reg_buf[2]; // 创建寄存器缓冲区
|
||||
|
||||
// 如果偏移量改变,先写入偏移量
|
||||
if (offset != old_offset) {
|
||||
old_offset = offset;
|
||||
reg_buf[0] = LT6911_REG_OFFSET; // 设置偏移寄存器
|
||||
reg_buf[1] = offset; // 设置偏移值
|
||||
if (write(client, reg_buf, 2) != 2) {
|
||||
perror("Failed to write offset to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 写入要读取的寄存器地址
|
||||
reg_buf[0] = reg; // 设置要读取的寄存器
|
||||
if (write(client, reg_buf, 1) != 1) {
|
||||
perror("Failed to write register address to the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 从 I2C 总线读取数据
|
||||
if (read(client, data, len) != len) {
|
||||
perror("Failed to read from the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
int lt6911_enable(void) {
|
||||
// Enable the LT6911UXC by writing to the appropriate register
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xEE, 0x01) != 0) {
|
||||
fprintf(stderr, "Failed to enable LT6911UXC\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lt6911_disable(void) {
|
||||
// Disable the LT6911UXC by writing to the appropriate register
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xEE, 0x00) != 0) {
|
||||
fprintf(stderr, "Failed to disable LT6911UXC\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
int lt6911uxc_edid_write(uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
uint8_t i;
|
||||
int ret;
|
||||
uint8_t chip_data[16] = {0};
|
||||
uint8_t wr_count = edid_size / LT6911UXC_WR_SIZE + 1;
|
||||
uint8_t version_str[32] = {0};
|
||||
|
||||
fprintf(stdout, "Writing EDID....\n");
|
||||
|
||||
// Start
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xDF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x59, 0x51) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x10) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x21) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x84) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x81) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
// Waiting for erasure
|
||||
usleep(500000);
|
||||
|
||||
if (i2c_read_byte(LT6911_SYS3_OFFSET, 0x08, chip_data) != 0) return -1;
|
||||
if (chip_data[0] != 0xEE) {
|
||||
fprintf(stderr, "Unsupported chip version\n");
|
||||
return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS3_OFFSET, 0x08, 0xAE) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS3_OFFSET, 0x08, 0xEE) != 0) return -1;
|
||||
// Write
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x84) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x84) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
for (i = 0; i < wr_count; i++) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xDF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x20) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x21) != 0) return -1;
|
||||
if (i != wr_count-1) {
|
||||
if (i2c_write_bytes(LT6911_SYS_OFFSET, 0x59, edid_data+(LT6911UXC_WR_SIZE*i), LT6911UXC_WR_SIZE) != 0) return -1;
|
||||
} else {
|
||||
if (i2c_write_bytes(LT6911_SYS_OFFSET, 0x59, version_str, LT6911UXC_WR_SIZE) != 0) return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i != wr_count-1) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00+(LT6911UXC_WR_SIZE*i)) != 0) return -1;
|
||||
} else {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x81) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00) != 0) return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xC0) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x90) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i != wr_count-1) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x84) != 0) return -1;
|
||||
} else {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x88) != 0) return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
}
|
||||
if (i2c_read_byte(LT6911_SYS3_OFFSET, 0x08, chip_data) != 0) return -1;
|
||||
if (chip_data[0] != 0xEE) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS3_OFFSET, 0x08, 0xAE) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS3_OFFSET, 0x08, 0xEE) != 0) return -1;
|
||||
|
||||
fprintf(stdout, "EDID write completed\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lt6911uxc_edid_read(uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
uint8_t i;
|
||||
int ret;
|
||||
uint8_t chip_data[16] = {0};
|
||||
uint8_t wr_count = edid_size / LT6911UXC_WR_SIZE;
|
||||
|
||||
// Read EDID data from LT6911UXC
|
||||
fprintf(stdout, "Reading EDID...\n");
|
||||
// Read
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x84) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
for (i = 0; i < wr_count; i++) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0x5F) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0xA0) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00+(LT6911UXC_WR_SIZE*i)) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x90) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x21) != 0) return -1;
|
||||
if (i2c_read_bytes(LT6911_SYS_OFFSET, 0x5F, edid_data+(LT6911UXC_WR_SIZE*i), LT6911UXC_WR_SIZE) != 0) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lt6911c_edid_write(uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
uint8_t i;
|
||||
int ret;
|
||||
uint8_t chip_data[16] = {0};
|
||||
uint8_t wr_count = edid_size / LT6911C_WR_SIZE;
|
||||
uint8_t version_str[32] = {0};
|
||||
|
||||
fprintf(stdout, "Writing EDID....\n");
|
||||
|
||||
// Start
|
||||
lt6911_enable();
|
||||
if (i2c_read_bytes(LT6911_SYS4_OFFSET, 0x00, chip_data, 2) != 0) return -1;
|
||||
if (chip_data[0] != 0x16 || chip_data[1] != 0x05) {
|
||||
fprintf(stderr, "Unsupported chip version\n");
|
||||
return -1;
|
||||
}
|
||||
lt6911_disable();
|
||||
|
||||
usleep(100000);
|
||||
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_read_bytes(LT6911_SYS4_OFFSET, 0x00, chip_data, 2) != 0) return -1;
|
||||
if (chip_data[0] != 0x16 || chip_data[1] != 0x05) {
|
||||
fprintf(stderr, "Unsupported chip version\n");
|
||||
return -1;
|
||||
}
|
||||
lt6911_disable();
|
||||
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xC0) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x59, 0x51) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x92) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x86) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x83) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
|
||||
// waiting to clean
|
||||
usleep(500000);
|
||||
if (i2c_read_byte(LT6911_SYS2_OFFSET, 0x02, chip_data) != 0) return -1;
|
||||
if (chip_data[0] != 0xFF) {
|
||||
fprintf(stderr, "Clean Error\n");
|
||||
return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS2_OFFSET, 0x02, 0xDF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS2_OFFSET, 0x02, 0xFF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x86) != 0) return -1;
|
||||
|
||||
for (i = 0; i < wr_count; i++) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x86) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xEF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0xA2) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x01) != 0) return -1;
|
||||
if (i2c_write_bytes(LT6911_SYS_OFFSET, 0x59, edid_data+(LT6911C_WR_SIZE*i), LT6911C_WR_SIZE) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00+(LT6911C_WR_SIZE*i)) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0xE0) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x92) != 0) return -1;
|
||||
}
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x8A) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_read_byte(LT6911_SYS2_OFFSET, 0x02, chip_data) != 0) return -1;
|
||||
if (chip_data[0] != 0xFF) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS2_OFFSET, 0x02, 0xDF) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS2_OFFSET, 0x02, 0xFF) != 0) return -1;
|
||||
|
||||
fprintf(stdout, "EDID write completed\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lt6911c_edid_read(uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
uint8_t i;
|
||||
int ret;
|
||||
uint8_t chip_data[16] = {0};
|
||||
uint8_t wr_count = edid_size / LT6911C_WR_SIZE;
|
||||
|
||||
// Read EDID data from LT6911UXC
|
||||
fprintf(stdout, "Reading EDID...\n");
|
||||
// Read
|
||||
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0xFF, 0x80) != 0) return -1;
|
||||
lt6911_enable();
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x86) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
|
||||
for (i = 0; i < wr_count; i++) {
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5E, 0x6F) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0xA2) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5B, 0x01) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5C, 0x80) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5D, 0x00+(LT6911C_WR_SIZE*i)) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x92) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x5A, 0x82) != 0) return -1;
|
||||
if (i2c_write_byte(LT6911_SYS_OFFSET, 0x58, 0x01) != 0) return -1;
|
||||
if (i2c_read_bytes(LT6911_SYS_OFFSET, 0x5F, edid_data+(LT6911C_WR_SIZE*i), LT6911C_WR_SIZE) != 0) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
int lt6911_edid_config(chip_version_t chip_version, uint8_t *edid_data, uint16_t edid_size)
|
||||
{
|
||||
if (chip_version == CHIP_UNKNOWN)
|
||||
{
|
||||
fprintf(stderr, "Unknown chip version\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// open i2c device
|
||||
if ((client = open(I2C_DEVICE, O_RDWR)) < 0) {
|
||||
perror("Failed to open the i2c bus");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// // set the lt6911uxc address
|
||||
if (ioctl(client, I2C_SLAVE, I2C_ADDRESS) < 0) {
|
||||
perror("Failed to acquire bus access and/or talk to slave");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// edid write
|
||||
if (chip_version == CHIP_LT6911UXC) {
|
||||
if (lt6911uxc_edid_write(edid_data, edid_size) != 0) {
|
||||
fprintf(stderr, "Failed to write EDID data to LT6911UXC\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
} else if (chip_version == CHIP_LT6911C) {
|
||||
if (lt6911c_edid_write(edid_data, edid_size) != 0) {
|
||||
fprintf(stderr, "Failed to write EDID data to LT6911C\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Unknown chip version\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
// sleep 1s
|
||||
sleep(1);
|
||||
|
||||
// read edid data back
|
||||
uint8_t edid_read_data[EDID_BUFFER_SIZE] = {0};
|
||||
if (chip_version == CHIP_LT6911UXC) {
|
||||
if (lt6911uxc_edid_read(edid_read_data, EDID_BUFFER_SIZE) != 0) {
|
||||
fprintf(stderr, "Failed to read EDID data from LT6911UXC\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
} else if (chip_version == CHIP_LT6911C) {
|
||||
if (lt6911c_edid_read(edid_read_data, EDID_BUFFER_SIZE) != 0) {
|
||||
fprintf(stderr, "Failed to read EDID data from LT6911C\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Unknown chip version\n");
|
||||
close(client);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// check if the read data matches the written data
|
||||
if (memcmp(edid_data, edid_read_data, EDID_BUFFER_SIZE) != 0) {
|
||||
fprintf(stderr, "EDID data mismatch after write/read cycle\n");
|
||||
// print edid with 16*16 hex format
|
||||
fprintf(stderr, "Written EDID data:\n");
|
||||
for (int i = 0; i < EDID_BUFFER_SIZE; i++) {
|
||||
fprintf(stderr, "%02X ", edid_data[i]);
|
||||
if ((i + 1) % 16 == 0) {
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "\nRead EDID data:\n");
|
||||
for (int i = 0; i < EDID_BUFFER_SIZE; i++) {
|
||||
fprintf(stderr, "%02X ", edid_read_data[i]);
|
||||
if ((i + 1) % 16 == 0) {
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
}
|
||||
close(client);
|
||||
return -1;
|
||||
} else {
|
||||
fprintf(stdout, "EDID data verified successfully\n");
|
||||
}
|
||||
|
||||
// close client
|
||||
close(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
void print_warning(product_version_t product_version) {
|
||||
printf("\n=========================================================\n");
|
||||
printf("Incorrect EDID may cause issues such as \n");
|
||||
printf("inability to display images, please modify with caution\n");
|
||||
printf("=========================================================\n\n");
|
||||
|
||||
if (product_version == PRODUCT_CUBE_A || product_version == PRODUCT_CUBE_B) {
|
||||
printf("\n==========================================================\n");
|
||||
printf("⚠️ WARNING: Hardware version detected as Cube/Lite!\n");
|
||||
printf("==========================================================\n");
|
||||
printf("After flashing, you MUST manually power cycle the device!\n");
|
||||
printf("Please ensure you can physically disconnect its power,\n");
|
||||
printf("NOT just remotely reboot it!!\n");
|
||||
printf("==========================================================\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
void print_success(product_version_t product_version) {
|
||||
printf("\n=========================================================\n");
|
||||
printf("✅ EDID update successful!\n");
|
||||
if (product_version == PRODUCT_CUBE_A || product_version == PRODUCT_CUBE_B) {
|
||||
printf("Please manually power cycle the device to apply changes.\n");
|
||||
}
|
||||
printf("=========================================================\n\n");
|
||||
}
|
||||
|
||||
int get_user_confirmation() {
|
||||
char input[256];
|
||||
printf("Do you want to continue? (Y/N): \n");
|
||||
|
||||
while (1) {
|
||||
|
||||
if (fgets(input, sizeof(input), stdin) == NULL) {
|
||||
printf("\nInput error. Exiting.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 去除换行符
|
||||
input[strcspn(input, "\n")] = '\0';
|
||||
|
||||
if (strlen(input) == 0) {
|
||||
continue; // 空输入,重新提示
|
||||
}
|
||||
|
||||
// 转换为小写方便比较
|
||||
char choice = tolower(input[0]);
|
||||
|
||||
if (choice == 'y') {
|
||||
return 1;
|
||||
} else if (choice == 'n') {
|
||||
return 0;
|
||||
} else {
|
||||
printf("Invalid input. Please enter Y or N.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================================================
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
uint8_t counter = 0;
|
||||
|
||||
// Check version
|
||||
chip_version_t chip_version = CHIP_UNKNOWN;
|
||||
product_version_t product_version = PRODUCT_UNKNOWN;
|
||||
char chip_version_str[32] = {0};
|
||||
char product_version_str[32] = {0};
|
||||
|
||||
FILE *file = fopen(VERSION_PATH, "r");
|
||||
if (file == NULL) {
|
||||
fprintf(stderr, "Please upgrade to the latest system\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fgets(chip_version_str, sizeof(chip_version_str), file) == NULL) {
|
||||
fprintf(stderr, "Failed to read chip version\n");
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
file = fopen(PRODUCT_PATH, "r");
|
||||
if (file == NULL) {
|
||||
fprintf(stderr, "Please upgrade to the latest system\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fgets(product_version_str, sizeof(product_version_str), file) == NULL) {
|
||||
fprintf(stderr, "Failed to read product version\n");
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
chip_version_str[strcspn(chip_version_str, "\n")] = '\0';
|
||||
product_version_str[strcspn(product_version_str, "\n")] = '\0';
|
||||
|
||||
if (strcmp(chip_version_str, "c") == 0) {
|
||||
fprintf(stdout, "Chip Version: LT6911C\n");
|
||||
chip_version = CHIP_LT6911C;
|
||||
} else if (strcmp(chip_version_str, "ux") == 0) {
|
||||
fprintf(stdout, "Chip Version: LT6911UXC\n");
|
||||
chip_version = CHIP_LT6911UXC;
|
||||
} else if (strcmp(chip_version_str, "ue") == 0) {
|
||||
fprintf(stderr, "Chip Version Error: UE version's edid can't be updated\n");
|
||||
return 1;
|
||||
} else {
|
||||
fprintf(stderr, "Chip Version Error: Unknown version\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(product_version_str, "alpha") == 0) {
|
||||
fprintf(stdout, "Product Version: CUBE_A\n");
|
||||
product_version = PRODUCT_CUBE_A;
|
||||
} else if (strcmp(product_version_str, "beta") == 0) {
|
||||
fprintf(stdout, "Product Version: CUBE_B\n");
|
||||
product_version = PRODUCT_CUBE_B;
|
||||
} else if (strcmp(product_version_str, "pcie") == 0) {
|
||||
fprintf(stdout, "Product Version : PCIE_A\n");
|
||||
product_version = PRODUCT_PCIE_A;
|
||||
} else {
|
||||
fprintf(stderr, "Product Version Error: Unknown version\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
print_warning(product_version);
|
||||
if (product_version == PRODUCT_CUBE_A || product_version == PRODUCT_CUBE_B) {
|
||||
if (get_user_confirmation() == 0) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// check command line arguments
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Please enter the location of the EDID file using \"%s /path/to/edid.bin\"\n", argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
uint16_t edid_size = 0;
|
||||
uint8_t edid_data[EDID_BUFFER_SIZE] = {0};
|
||||
if (get_edid_from_file(argv[1], edid_data, &edid_size) != 0) {
|
||||
fprintf(stderr, "Failed to read EDID data from file %s\n", argv[1]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (check_edid(edid_data, edid_size) != 0) {
|
||||
fprintf(stderr, "EDID data is invalid\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
fprintf(stdout, "EDID data loaded successfully from %s\n", argv[1]);
|
||||
|
||||
if (product_version == PRODUCT_PCIE_A) NanoKVM_PCIe_HDMI_Reset();
|
||||
|
||||
// configure lt6911 edid
|
||||
if (lt6911_edid_config(chip_version, edid_data, edid_size) != 0) {
|
||||
fprintf(stderr, "Failed to configure LT6911 EDID\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (product_version == PRODUCT_PCIE_A) NanoKVM_PCIe_HDMI_Reset();
|
||||
|
||||
print_success(product_version);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
41
tools/nanokvm_update_edid/nanokvm_update_edid.h
Normal file
41
tools/nanokvm_update_edid/nanokvm_update_edid.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef NANOKVM_UPDATE_EDID_H
|
||||
#define NANOKVM_UPDATE_EDID_H
|
||||
|
||||
#define I2C_DEVICE "/dev/i2c-4" // I2C 设备文件
|
||||
#define EDID_BUFFER_SIZE 256 // 最大支持的字节数
|
||||
|
||||
#define I2C_ADDRESS 0x2b // I2C 设备地址(根据实际设备调整)
|
||||
#define LT6911_REG_OFFSET 0xFF // LT6911UXC 寄存器偏移地址
|
||||
#define LT6911_SYS_OFFSET 0x80 // LT6911UXC 寄存器偏移地址
|
||||
#define LT6911_SYS2_OFFSET 0x90 // LT6911UXC 寄存器偏移地址
|
||||
#define LT6911_SYS3_OFFSET 0x81 // LT6911UXC 寄存器偏移地址
|
||||
#define LT6911_SYS4_OFFSET 0xA0 // LT6911UXC 寄存器偏移地址
|
||||
#define LT6911_CSI_INFO_OFFSET 0x85 // LT6911UXC CSI接口信息寄存器偏移地址
|
||||
#define LT6911_HDMI_INFO_OFFSET 0x86 // LT6911UXC HDMI信息寄存器偏移地址
|
||||
#define LT6911_CSI_TOTAL_OFFSET 0xD4 // LT6911UXC CSI总线统计信息
|
||||
#define LT6911_AUDIO_INFO_OFFSET 0xB0 // LT6911UXC 音频信息寄存器偏移地址
|
||||
#define LT6911C_HDMI_INFO_OFFSET 0xD2 // LT6911C HDMI信息寄存器偏移地址
|
||||
#define LT6911C_AUDIO_INFO_OFFSET 0xD1 // LT6911C 音频信息寄存器偏移地址
|
||||
#define LT6911C_CSI_INFO_OFFSET 0xC2 // LT6911C CSI信息寄存器偏移地址
|
||||
|
||||
#define EDID_BUFFER_SIZE 256 // 最大支持的字节数
|
||||
#define LT6911UXC_WR_SIZE 32 // LT6911UXC单次读写最大字节数
|
||||
#define LT6911C_WR_SIZE 16 // LT6911C单次读写最大字节数
|
||||
|
||||
#define VERSION_PATH "/etc/kvm/hdmi_version"
|
||||
#define PRODUCT_PATH "/etc/kvm/hw"
|
||||
|
||||
typedef enum {
|
||||
CHIP_LT6911UXC = 0,
|
||||
CHIP_LT6911C,
|
||||
CHIP_UNKNOWN
|
||||
} chip_version_t;
|
||||
|
||||
typedef enum {
|
||||
PRODUCT_CUBE_A = 0,
|
||||
PRODUCT_CUBE_B,
|
||||
PRODUCT_PCIE_A,
|
||||
PRODUCT_UNKNOWN,
|
||||
} product_version_t;
|
||||
|
||||
#endif // NANOKVM_UPDATE_EDID_H
|
||||
@@ -1,3 +1,3 @@
|
||||
VITE_SERVER_IP=192.168.0.65
|
||||
VITE_SERVER_IP=192.168.0.1
|
||||
VITE_SERVER_PORT=80
|
||||
VITE_WITH_CREDENTIALS=false
|
||||
|
||||
@@ -11,18 +11,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.1",
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"antd": "^5.21.6",
|
||||
"axios": "^1.8.4",
|
||||
"antd": "^5.29.1",
|
||||
"axios": "1.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"i18next": "^23.16.4",
|
||||
"jotai": "^2.10.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.469.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
@@ -34,21 +35,22 @@
|
||||
"react-simple-keyboard": "^3.8.19",
|
||||
"semver": "^7.6.3",
|
||||
"vaul": "^0.9.9",
|
||||
"websocket": "^1.0.35"
|
||||
"websocket": "^1.0.35",
|
||||
"yocto-queue": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||
"@typescript-eslint/parser": "^8.19.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
@@ -57,9 +59,9 @@
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^6.2.1",
|
||||
"vite": "7.1.11",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"msw": {
|
||||
|
||||
2045
web/pnpm-lock.yaml
generated
2045
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { http } from '@/lib/http.ts';
|
||||
import { getBaseUrl } from '@/lib/service.ts';
|
||||
|
||||
// get application version
|
||||
export function getVersion() {
|
||||
@@ -14,6 +15,16 @@ export function update() {
|
||||
});
|
||||
}
|
||||
|
||||
// offline update application
|
||||
export function offlineUpdate(data: FormData) {
|
||||
const baseUrl = getBaseUrl('http');
|
||||
const url = `${baseUrl}/api/application/update/offline`;
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
// enable/disable preview updates
|
||||
export function setPreviewUpdates(enable: boolean) {
|
||||
const data = {
|
||||
|
||||
20
web/src/api/autostart.ts
Normal file
20
web/src/api/autostart.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { http } from '@/lib/http.ts';
|
||||
|
||||
export function getAutostart() {
|
||||
return http.get('/api/vm/autostart');
|
||||
}
|
||||
|
||||
export function uploadAutostart(name: string, content: string) {
|
||||
return http.post('/api/vm/autostart/' + name, { content });
|
||||
}
|
||||
|
||||
export function deleteAutostart(name: string) {
|
||||
return http.request({
|
||||
url: '/api/vm/autostart/' + name,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
export function getAutostartContent(name: string) {
|
||||
return http.get('/api/vm/autostart/' + name);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { http } from '@/lib/http.ts';
|
||||
|
||||
// paste
|
||||
export function paste(content: string) {
|
||||
return http.post('/api/hid/paste', { content });
|
||||
export function paste(content: string, langue: string) {
|
||||
return http.post('/api/hid/paste', { content, langue });
|
||||
}
|
||||
|
||||
// reset hid
|
||||
|
||||
@@ -31,11 +31,25 @@ export function getWiFi() {
|
||||
return http.get('/api/network/wifi');
|
||||
}
|
||||
|
||||
// connect wifi
|
||||
export function connectWifi(ssid: string, password: string) {
|
||||
// connect wifi without auth (only available in wifi configuration mode)
|
||||
export function connectWifiNoAuth(ssid: string, password: string) {
|
||||
const data = {
|
||||
ssid,
|
||||
password
|
||||
};
|
||||
return http.post('/api/network/wifi', data);
|
||||
}
|
||||
|
||||
// connect wifi
|
||||
export function connectWifi(ssid: string, password: string) {
|
||||
const data = {
|
||||
ssid,
|
||||
password
|
||||
};
|
||||
return http.post('/api/network/wifi/connect', data);
|
||||
}
|
||||
|
||||
// disconnect wifi
|
||||
export function disconnectWifi() {
|
||||
return http.post('/api/network/wifi/disconnect');
|
||||
}
|
||||
|
||||
@@ -23,3 +23,10 @@ export function mountImage(file?: string, cdrom?: boolean) {
|
||||
export function getCdRom() {
|
||||
return http.get('/api/storage/cdrom');
|
||||
}
|
||||
|
||||
export function deleteImage(file: string) {
|
||||
const data = {
|
||||
file
|
||||
};
|
||||
return http.post('/api/storage/image/delete', data);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,29 @@ export function resetHdmi() {
|
||||
return http.post('/api/vm/hdmi/reset');
|
||||
}
|
||||
|
||||
// get HDMI state
|
||||
export function getHdmiState() {
|
||||
return http.get('/api/vm/hdmi');
|
||||
}
|
||||
|
||||
// enable HDMI
|
||||
export function enableHdmi() {
|
||||
return http.post('/api/vm/hdmi/enable');
|
||||
}
|
||||
|
||||
// disable HDMI
|
||||
export function disableHdmi() {
|
||||
return http.post('/api/vm/hdmi/disable');
|
||||
}
|
||||
|
||||
// set HDMI state
|
||||
export function setHdmiState(enabled: boolean) {
|
||||
if (enabled) {
|
||||
return enableHdmi();
|
||||
}
|
||||
return disableHdmi();
|
||||
}
|
||||
|
||||
// get SSH state
|
||||
export function getSSHState() {
|
||||
return http.get('/api/vm/ssh');
|
||||
@@ -132,6 +155,11 @@ export function disableMdns() {
|
||||
return http.post('/api/vm/mdns/disable');
|
||||
}
|
||||
|
||||
// enable / disable TLS
|
||||
export function setTLS(enabled: boolean) {
|
||||
return http.post('/api/vm/tls', { enabled });
|
||||
}
|
||||
|
||||
// reboot
|
||||
export function reboot() {
|
||||
return http.post('/api/vm/system/reboot');
|
||||
|
||||
@@ -103,3 +103,7 @@
|
||||
.keyboard-header {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.hg-candidate-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Helmet, HelmetData } from 'react-helmet-async';
|
||||
|
||||
import { getWebTitle } from '@/api/vm.ts';
|
||||
import { existToken } from '@/lib/cookie.ts';
|
||||
import { webTitleAtom } from '@/jotai/settings.ts';
|
||||
|
||||
type HeadProps = {
|
||||
@@ -11,7 +14,17 @@ type HeadProps = {
|
||||
const helmetData = new HelmetData({});
|
||||
|
||||
export const Head = ({ title = '', description = '' }: HeadProps = {}) => {
|
||||
const webTitle = useAtomValue(webTitleAtom);
|
||||
const [webTitle, setWebTitle] = useAtom(webTitleAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!existToken()) return;
|
||||
|
||||
getWebTitle().then((rsp) => {
|
||||
if (rsp.data?.title) {
|
||||
setWebTitle(rsp.data.title);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
|
||||
53
web/src/components/ui/scroll-area.tsx
Normal file
53
web/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={clsx('relative', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px]"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={clsx(
|
||||
'flex touch-none select-none p-px transition-colors',
|
||||
orientation === 'vertical' && 'h-full w-2 border-l border-l-transparent',
|
||||
orientation === 'horizontal' && 'h-2 flex-col border-t border-t-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full bg-neutral-500/30"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,4 +1,5 @@
|
||||
const languages = [
|
||||
{ key: 'ca', name: 'Català' },
|
||||
{ key: 'nl', name: 'Nederlands' },
|
||||
{ key: 'da', name: 'Danish' },
|
||||
{ key: 'de', name: 'Deutsch' },
|
||||
@@ -8,7 +9,9 @@ const languages = [
|
||||
{ key: 'id', name: 'Indonesia' },
|
||||
{ key: 'it', name: 'Italian' },
|
||||
{ key: 'pl', name: 'Polski' },
|
||||
{ key: 'pt_br', name: 'Português (Brasil)' },
|
||||
{ key: 'ru', name: 'Русский' },
|
||||
{ key: 'tr', name: 'Türkçe'},
|
||||
{ key: 'ko', name: '한국어' },
|
||||
{ key: 'zh', name: '简体中文' },
|
||||
{ key: 'zh_tw', name: '繁體中文' },
|
||||
@@ -18,7 +21,8 @@ const languages = [
|
||||
{ key: 'cz', name: 'Česky' },
|
||||
{ key: 'uk', name: 'Українська' },
|
||||
{ key: 'nb', name: 'Norsk, bokmål' },
|
||||
{ key: 'th', name: 'ภาษาไทย' }
|
||||
{ key: 'th', name: 'ภาษาไทย' },
|
||||
{ key: 'se', name: "Svenska"}
|
||||
];
|
||||
|
||||
languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
|
||||
|
||||
347
web/src/i18n/locales/ca.ts
Normal file
347
web/src/i18n/locales/ca.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
const ca = {
|
||||
translation: {
|
||||
head: {
|
||||
desktop: "Escriptori remot",
|
||||
login: "Inici de sessió",
|
||||
changePassword: "Canviar contrasenya",
|
||||
terminal: "Terminal",
|
||||
wifi: "Xarxa Sense fils"
|
||||
},
|
||||
auth: {
|
||||
login: "Inici de sessió",
|
||||
placeholderUsername: "Nom d'usuari",
|
||||
placeholderPassword: "Contrasenya",
|
||||
placeholderPassword2: "Torna a introduir la contrasenya",
|
||||
noEmptyUsername: "Cal introduir el nom d'usuari",
|
||||
noEmptyPassword: "Cal introduir la contrasenya",
|
||||
noAccount: "No s'ha pogut obtenir la informació de l'usuari, actualitza la pàgina web o restableix la contrasenya",
|
||||
invalidUser: "Nom d'usuari o contrasenya invàlids",
|
||||
error: "Error inesperat",
|
||||
changePassword: "Canviar la contrasenya",
|
||||
changePasswordDesc: "Per a la seguretat del dispositiu, canvia la contrasenya!",
|
||||
differentPassword: "Les contrasenyes no coincideixen",
|
||||
illegalUsername: "El nom d'usuari conté caràcters no permesos",
|
||||
illegalPassword: "La contrasenya conté caràcters no permesos",
|
||||
forgetPassword: "Has oblidat la contrasenya",
|
||||
ok: "D'acord",
|
||||
cancel: "Cancel·la",
|
||||
loginButtonText: "Inicia sessió",
|
||||
tips: {
|
||||
reset1: "Per restablir les contrasenyes, mantingues premut el botó BOOT del NanoKVM durant 10 segons.",
|
||||
reset2: "Per veure els passos detallats, consulta aquest document:",
|
||||
reset3: "Compte web per defecte:",
|
||||
reset4: "Compte SSH per defecte:",
|
||||
change1: "Aquesta acció canviarà les següents contrasenyes:",
|
||||
change2: "Contrasenya d'inici de sessió web",
|
||||
change3: "Contrasenya root del sistema (inici de sessió SSH)",
|
||||
change4: "Per restablir les contrasenyes, mantingues premut el botó BOOT del NanoKVM."
|
||||
}
|
||||
},
|
||||
wifi: {
|
||||
title: "Xarxa Sense fils",
|
||||
description: "Configura la xarxa sense fils per al NanoKVM",
|
||||
success: "Comprova l'estat de la xarxa del NanoKVM i visita la nova adreça IP.",
|
||||
failed: "L'operació ha fallat, torna-ho a intentar.",
|
||||
confirmBtn: "D'acord",
|
||||
finishBtn: "Fet"
|
||||
},
|
||||
screen: {
|
||||
title: "Pantalla",
|
||||
video: "Mode de vídeo",
|
||||
videoDirectTips: "Activa HTTPS a 'Configuració > Dispositiu' per utilitzar aquest mode",
|
||||
resolution: "Resolució",
|
||||
auto: "Automàtic",
|
||||
autoTips: "Poden aparèixer talls o desajustos del ratolí en certes resolucions. Prova a canviar la resolució de l'amfitrió remot o desactiva el mode automàtic.",
|
||||
fps: "FPS",
|
||||
customizeFps: "Personalitzat",
|
||||
quality: "Qualitat",
|
||||
qualityLossless: "Sense pèrdua",
|
||||
qualityHigh: "Alta",
|
||||
qualityMedium: "Mitjana",
|
||||
qualityLow: "Baixa",
|
||||
frameDetect: "Detecció de fotogrames",
|
||||
frameDetectTip: "Calcula la diferència entre fotogrames. S'atura la transmissió si no hi ha canvis a la pantalla de l'amfitrió remot.",
|
||||
resetHdmi: "Restablir HDMI"
|
||||
},
|
||||
keyboard: {
|
||||
title: "Teclat",
|
||||
paste: "Enganxa",
|
||||
tips: "Només es permeten lletres i símbols estàndard",
|
||||
placeholder: "Escriu aquí",
|
||||
submit: "Envia",
|
||||
virtual: "Teclat",
|
||||
ctrlaltdel: "Ctrl+Alt+Del"
|
||||
},
|
||||
mouse: {
|
||||
title: "Ratolí",
|
||||
cursor: "Estil del cursor",
|
||||
default: "Cursor per defecte",
|
||||
pointer: "Cursor punter",
|
||||
cell: "Cursor de cel·la",
|
||||
text: "Cursor de text",
|
||||
grab: "Cursor de mà",
|
||||
hide: "Amaga el cursor",
|
||||
mode: "Mode de ratolí",
|
||||
absolute: "Mode absolut",
|
||||
relative: "Mode relatiu",
|
||||
speed: "Velocitat de la roda",
|
||||
fast: "Ràpida",
|
||||
slow: "Lenta",
|
||||
requestPointer: "Estàs usant el mode relatiu. Fes clic a l'escriptori per obtenir el punter.",
|
||||
resetHid: "Restablir HID",
|
||||
hidOnly: {
|
||||
title: "Mode només HID",
|
||||
desc: "Si el teclat o ratolí deixen de funcionar i restablir HID no ajuda, pot ser un problema de compatibilitat. Prova d'activar el mode només HID.",
|
||||
tip1: "Activar aquest mode desmuntarà el disc virtual i la xarxa virtual",
|
||||
tip2: "En mode només HID, no es pot muntar imatges",
|
||||
tip3: "El NanoKVM es reiniciarà automàticament en canviar de mode",
|
||||
enable: "Activa mode només HID",
|
||||
disable: "Desactiva mode només HID"
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: "Imatges",
|
||||
loading: "Carregant...",
|
||||
empty: "No s'ha trobat res",
|
||||
cdrom: "Munta la imatge en mode CD-ROM",
|
||||
mountFailed: "Error en muntar",
|
||||
mountDesc: "En alguns sistemes cal expulsar el disc virtual abans de muntar la imatge.",
|
||||
refresh: "Actualitza la llista",
|
||||
tips: {
|
||||
title: "Com pujar imatges",
|
||||
usb1: "Connecta el NanoKVM al teu ordinador via USB.",
|
||||
usb2: "Assegura't que el disc virtual està muntat (Configuració - Disc Virtual).",
|
||||
usb3: "Obre el disc virtual i copia la imatge a l'arrel.",
|
||||
scp1: "Comprova que el NanoKVM i el teu ordinador estan a la mateixa xarxa.",
|
||||
scp2: "Obre un terminal i usa SCP per pujar la imatge al directori /data del NanoKVM.",
|
||||
scp3: "Exemple: scp ruta-de-la-imatge root@ip-del-nanokvm:/data",
|
||||
tfCard: "Targeta TF",
|
||||
tf1: "Mètode disponible a sistemes GNU/Linux",
|
||||
tf2: "Extreu la targeta TF del NanoKVM (versió FULL, cal obrir la carcassa).",
|
||||
tf3: "Introdueix la targeta en un lector i connecta'l a l'ordinador.",
|
||||
tf4: "Copia la imatge al directori /data de la targeta.",
|
||||
tf5: "Reintrodueix la targeta al NanoKVM."
|
||||
}
|
||||
},
|
||||
script: {
|
||||
title: "Scripts",
|
||||
upload: "Puja",
|
||||
run: "Executa",
|
||||
runBackground: "Executa en segon pla",
|
||||
runFailed: "Error en l'execució",
|
||||
attention: "Atenció",
|
||||
delDesc: "Estàs segur que vols eliminar aquest fitxer?",
|
||||
confirm: "Sí",
|
||||
cancel: "No",
|
||||
delete: "Esborra",
|
||||
close: "Tanca"
|
||||
},
|
||||
terminal: {
|
||||
title: "Terminal",
|
||||
nanokvm: "Terminal NanoKVM",
|
||||
serial: "Terminal de port sèrie",
|
||||
serialPort: "Port sèrie",
|
||||
serialPortPlaceholder: "Introdueix el port sèrie",
|
||||
baudrate: "Velocitat (baudrate)",
|
||||
parity: "Paritat",
|
||||
parityNone: "Cap",
|
||||
parityEven: "Parell",
|
||||
parityOdd: "Senar",
|
||||
flowControl: "Control de flux",
|
||||
flowControlNone: "Cap",
|
||||
flowControlSoft: "Suau",
|
||||
flowControlHard: "Dur",
|
||||
dataBits: "Bits de dades",
|
||||
stopBits: "Bits de parada",
|
||||
confirm: "D'acord"
|
||||
},
|
||||
wol: {
|
||||
title: "Wake-on-LAN",
|
||||
sending: "Enviant comanda...",
|
||||
sent: "Comanda enviada",
|
||||
input: "Introdueix la MAC",
|
||||
ok: "D'acord"
|
||||
},
|
||||
download: {
|
||||
title: "Descarregador d'imatges",
|
||||
input: "Introdueix la URL de la imatge",
|
||||
ok: "D'acord",
|
||||
disabled: "La partició /data és només lectura. No es pot descarregar la imatge."
|
||||
},
|
||||
power: {
|
||||
title: "Alimentació",
|
||||
showConfirm: "Confirmació",
|
||||
showConfirmTip: "Les operacions d'alimentació requereixen confirmació",
|
||||
reset: "Reinicia",
|
||||
power: "Encén",
|
||||
powerShort: "Clic curt",
|
||||
powerLong: "Clic llarg",
|
||||
resetConfirm: "Vols realment reiniciar?",
|
||||
powerConfirm: "Vols realment encendre/apagar?",
|
||||
okBtn: "Sí",
|
||||
cancelBtn: "No"
|
||||
},
|
||||
settings: {
|
||||
title: "Configuració",
|
||||
about: {
|
||||
title: "Sobre NanoKVM",
|
||||
information: "Informació",
|
||||
ip: "IP",
|
||||
mdns: "mDNS",
|
||||
application: "Versió aplicació",
|
||||
applicationTip: "Versió de la interfície web de NanoKVM",
|
||||
image: "Versió de la imatge",
|
||||
imageTip: "Versió del sistema NanoKVM",
|
||||
deviceKey: "Clau del dispositiu",
|
||||
community: "Comunitat",
|
||||
hostname: "Nom del dispositiu",
|
||||
hostnameUpdated: "Nom actualitzat. Reinicia per aplicar.",
|
||||
ipType: {
|
||||
Wired: "Cablejada",
|
||||
Wireless: "Sense fil",
|
||||
Other: "Altra"
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: "Aparença",
|
||||
display: "Pantalla",
|
||||
language: "Idioma",
|
||||
menuBar: "Barra de menú",
|
||||
menuBarDesc: "Mostra icones a la barra de menú",
|
||||
webTitle: "Títol web",
|
||||
webTitleDesc: "Personalitza el títol de la pàgina"
|
||||
},
|
||||
device: {
|
||||
title: "Dispositiu",
|
||||
oled: {
|
||||
title: "OLED",
|
||||
description: "Apagar pantalla OLED després de",
|
||||
0: "Mai",
|
||||
15: "15 s",
|
||||
30: "30 s",
|
||||
60: "1 min",
|
||||
180: "3 min",
|
||||
300: "5 min",
|
||||
600: "10 min",
|
||||
1800: "30 min",
|
||||
3600: "1 h"
|
||||
},
|
||||
wifi: {
|
||||
title: "Xarxa sense fils",
|
||||
description: "Configura la xarxa sense fils",
|
||||
setBtn: "Configura"
|
||||
},
|
||||
ssh: {
|
||||
description: "Activa accés remot per SSH",
|
||||
tip: "Configura una contrasenya segura abans (Compte - Canvia contrasenya)"
|
||||
},
|
||||
tls: {
|
||||
description: "Activa el protocol HTTPS",
|
||||
tip: "Atenció: Usar HTTPS pot augmentar la latència, sobretot amb vídeo MJPEG."
|
||||
},
|
||||
advanced: "Configuració avançada",
|
||||
swap: {
|
||||
title: "Swap",
|
||||
disable: "Desactiva",
|
||||
description: "Defineix la mida del fitxer swap",
|
||||
tip: "Pot reduir la vida útil de la targeta SD!"
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: "Mou-ratolí automàtic",
|
||||
description: "Evita que el dispositiu remot entri en repòs",
|
||||
disable: "Desactiva",
|
||||
absolute: "Mode absolut",
|
||||
relative: "Mode relatiu"
|
||||
},
|
||||
mdns: {
|
||||
description: "Activa descobriment mDNS",
|
||||
tip: "Desactiva-ho si no és necessari"
|
||||
},
|
||||
hdmi: {
|
||||
description: "Activa la sortida HDMI"
|
||||
},
|
||||
hidOnly: "Mode només HID",
|
||||
disk: "Disc virtual",
|
||||
diskDesc: "Munta un disc U virtual al dispositiu remot",
|
||||
network: "Xarxa virtual",
|
||||
networkDesc: "Munta una targeta de xarxa virtual al dispositiu remot",
|
||||
reboot: "Reinicia",
|
||||
rebootDesc: "Segur que vols reiniciar el NanoKVM?",
|
||||
okBtn: "Sí",
|
||||
cancelBtn: "No"
|
||||
},
|
||||
tailscale: {
|
||||
title: "Tailscale",
|
||||
memory: {
|
||||
title: "Optimització de memòria",
|
||||
tip: "Quan es supera el límit de memòria, es fa una neteja més agressiva. Recomanat: 75MB si uses Tailscale. Requereix reiniciar Tailscale.",
|
||||
disable: "Desactiva"
|
||||
},
|
||||
restart: "Reiniciar Tailscale?",
|
||||
stop: "Aturar Tailscale?",
|
||||
stopDesc: "Tanca la sessió i desactiva l'inici automàtic",
|
||||
loading: "Carregant...",
|
||||
notInstall: "Tailscale no instal·lat! Instal·la-ho.",
|
||||
install: "Instal·la",
|
||||
installing: "Instal·lant",
|
||||
failed: "Error en la instal·lació",
|
||||
retry: "Actualitza i torna-ho a provar. O instal·la manualment",
|
||||
download: "Descarrega el",
|
||||
package: "paquet d'instal·lació",
|
||||
unzip: "i descomprimeix-lo",
|
||||
upTailscale: "Puja tailscale a /usr/bin/",
|
||||
upTailscaled: "Puja tailscaled a /usr/sbin/",
|
||||
refresh: "Actualitza la pàgina",
|
||||
notLogin: "El dispositiu no està vinculat. Inicia sessió per vincular-lo.",
|
||||
urlPeriod: "Aquesta URL és vàlida durant 10 minuts",
|
||||
login: "Inicia sessió",
|
||||
loginSuccess: "Sessió iniciada correctament",
|
||||
enable: "Activa Tailscale",
|
||||
deviceName: "Nom del dispositiu",
|
||||
deviceIP: "IP del dispositiu",
|
||||
account: "Compte",
|
||||
logout: "Tanca sessió",
|
||||
logoutDesc: "Segur que vols tancar sessió?",
|
||||
uninstall: "Desinstal·la Tailscale",
|
||||
okBtn: "Sí",
|
||||
cancelBtn: "No"
|
||||
},
|
||||
update: {
|
||||
title: "Comprova actualitzacions",
|
||||
queryFailed: "Error en obtenir la versió",
|
||||
updateFailed: "Error en actualitzar. Torna-ho a intentar.",
|
||||
isLatest: "Ja tens la darrera versió.",
|
||||
available: "Hi ha una actualització disponible. Vols actualitzar ara?",
|
||||
updating: "Actualitzant... espera",
|
||||
confirm: "Confirma",
|
||||
cancel: "Cancel·la",
|
||||
preview: "Versió de prova",
|
||||
previewDesc: "Prova noves funcions abans que ningú",
|
||||
previewTip: "Compte: aquestes versions poden tenir errors o funcions inacabades!"
|
||||
},
|
||||
account: {
|
||||
title: "Compte",
|
||||
webAccount: "Nom del compte web",
|
||||
password: "Contrasenya",
|
||||
updateBtn: "Canvia",
|
||||
logoutBtn: "Tanca sessió",
|
||||
logoutDesc: "Segur que vols tancar sessió?",
|
||||
okBtn: "Sí",
|
||||
cancelBtn: "No"
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: "Hi ha hagut un error",
|
||||
refresh: "Actualitza"
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: "Pantalla completa"
|
||||
},
|
||||
menu: {
|
||||
collapse: "Amaga menú",
|
||||
expand: "Mostra menú"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ca;
|
||||
@@ -2,58 +2,60 @@ const de = {
|
||||
translation: {
|
||||
head: {
|
||||
desktop: 'Entfernter Desktop',
|
||||
login: 'Einloggen',
|
||||
login: 'Anmelden',
|
||||
changePassword: 'Passwort ändern',
|
||||
terminal: 'Terminal',
|
||||
wifi: 'Wi-Fi'
|
||||
},
|
||||
auth: {
|
||||
login: 'Einloggen',
|
||||
placeholderUsername: 'Bitte den Benutzernamen eingeben',
|
||||
placeholderPassword: 'Bitte das Passwort eingeben',
|
||||
placeholderPassword2: 'Passwort wiederholen',
|
||||
noEmptyUsername: 'Der Benutzername darf nicht leer sein',
|
||||
noEmptyPassword: 'Das Passwort darf nicht leer sein',
|
||||
login: 'Anmelden',
|
||||
placeholderUsername: 'Benutzername',
|
||||
placeholderPassword: 'Passwort',
|
||||
placeholderPassword2: 'Bitte Passwort erneut eingeben',
|
||||
noEmptyUsername: 'Benutzername benötigt',
|
||||
noEmptyPassword: 'Passwort benötigt',
|
||||
noAccount:
|
||||
'Fehler beim Auslsen der Benutzerinformation, bitte die Seite neu laden oder das Passwort zurücksetzen',
|
||||
'Abfragen der Benutzerdaten fehlgeschlagen, bitte die Seite neu laden oder Passwort zurücksetzen',
|
||||
invalidUser: 'Falscher Benutzername oder falsches Passwort',
|
||||
error: 'Unerwarteter Fehler',
|
||||
changePassword: 'Passwort ändern',
|
||||
changePasswordDesc:
|
||||
'Für die Sicherheit Ihres Geräts ändern Sie bitte das Web-Login-Passwort.',
|
||||
differentPassword: 'Passwörter stimmt nicht überein',
|
||||
illegalUsername: 'Benutzername beinhaltet ungültige Zeichen',
|
||||
illegalPassword: 'Passwort beinhaltet ungültige Zeichen',
|
||||
changePasswordDesc: 'Für die Sicherheit Ihres Geräts ändern Sie bitte das Passwort!',
|
||||
differentPassword: 'Passwörter stimmen nicht überein',
|
||||
illegalUsername: 'Benutzername enthält ungültige Zeichen',
|
||||
illegalPassword: 'Passwort enthält ungültige Zeichen',
|
||||
forgetPassword: 'Passwort vergessen',
|
||||
ok: 'Ok',
|
||||
cancel: 'Abbrechen',
|
||||
loginButtonText: 'Einloggen',
|
||||
loginButtonText: 'Anmelden',
|
||||
tips: {
|
||||
reset1:
|
||||
'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.',
|
||||
reset2: 'For detailed steps, please consult this document:',
|
||||
reset3: 'Web default account:',
|
||||
reset4: 'SSH default account:',
|
||||
change1: 'Please note that this action will change the following passwords:',
|
||||
change2: 'Web login password',
|
||||
change3: 'System root password (SSH login password)',
|
||||
change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.'
|
||||
'Um das Passwort zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM für 10 Sekunden.',
|
||||
reset2: 'Für detailliertere Anweisungen lesen Sie folgendes Dokument:',
|
||||
reset3: 'Web Standard-Account:',
|
||||
reset4: 'SSH Standard-Account:',
|
||||
change1: 'Bitte beachten Sie, dass diese Aktion folgende Passwörter ändert:',
|
||||
change2: 'Web Anmelde-Passwort',
|
||||
change3: 'System root Passwort (SSH Anmelde-Passwort)',
|
||||
change4:
|
||||
'Um die Passwörter zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM.'
|
||||
}
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi for NanoKVM',
|
||||
success: 'Please check the network status of NanoKVM and visit the new IP address.',
|
||||
failed: 'Operation failed, please try again.',
|
||||
description: 'Wi-Fi Konfiguration für NanoKVM',
|
||||
success:
|
||||
'Bitte überprüfen Sie den Netzwerk-Status des NanoKVM und greifen Sie über die neue IP Adresse darauf zu.',
|
||||
failed: 'Aktion fehlgeschlagen, bitte erneut versuchen.',
|
||||
confirmBtn: 'Ok',
|
||||
finishBtn: 'Finished'
|
||||
finishBtn: 'Fertig'
|
||||
},
|
||||
screen: {
|
||||
video: 'Videomodus',
|
||||
title: 'Bildschirm',
|
||||
video: 'Video Modus',
|
||||
resolution: 'Auflösung',
|
||||
auto: 'Automatisch',
|
||||
autoTips:
|
||||
'Falls es bei bestimmten Auflösungen zu Screen-Tearing oder einem Offset des Mauszeigers kommen sollte, bitte die Auflösung des Remote Hosts anpassen oder den automatischen Modus ausschalten.',
|
||||
'Bildverzerrungen oder ein versetzter Mauszeiger können bei bestimmten Auflösungen auftreten. Versuchen Sie, die Auflösung des entfernten Hosts anzupassen oder den automatischen Modus zu deaktivieren.',
|
||||
fps: 'FPS',
|
||||
customizeFps: 'Anpassen',
|
||||
quality: 'Qualität',
|
||||
@@ -61,193 +63,297 @@ const de = {
|
||||
qualityHigh: 'Hoch',
|
||||
qualityMedium: 'Mittel',
|
||||
qualityLow: 'Niedrig',
|
||||
frameDetect: 'Frame Detect',
|
||||
frameDetect: 'Bilderkennung',
|
||||
frameDetectTip:
|
||||
'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.',
|
||||
resetHdmi: 'Reset HDMI'
|
||||
resetHdmi: 'HDMI zurücksetzen'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Tastatur',
|
||||
paste: 'Einfügen',
|
||||
tips: 'Nur Standard-Tastaturbuchstaben und Symbole werden unterstützt.',
|
||||
tips: 'Server Tastaturbelegung',
|
||||
placeholder: 'Bitte eingeben',
|
||||
submit: 'Senden',
|
||||
virtual: 'Virtuelle Tastatur',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
virtual: 'Tastatur',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del',
|
||||
dropdownEnglish: 'Englisch',
|
||||
dropdownGerman: 'Deutsch',
|
||||
dropdownRussian: 'Russisch'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Maus',
|
||||
cursor: 'Cursor',
|
||||
default: 'Standard Cursor',
|
||||
pointer: 'Zeiger Cursor',
|
||||
cell: 'Zellen Cursor',
|
||||
text: 'Text Cursor',
|
||||
grab: 'Greif Cursor',
|
||||
hide: 'Cursor ausblenden',
|
||||
hide: 'Versteckter Cursor',
|
||||
mode: 'Maus Modus',
|
||||
absolute: 'Absoluter Modus',
|
||||
relative: 'Relativer Modus',
|
||||
requestPointer:
|
||||
'Der Relative Modus wird benutzt. Bitte auf den Desktop klicken um einen Mauszeiger anzuzeigen.',
|
||||
resetHid: 'Eingeabegeräte zurücksetzen'
|
||||
'Relativer Modus aktiv. Klicken Sie auf den Desktop um den Mauszeiger zu sehen.',
|
||||
resetHid: 'HID zurücksetzen',
|
||||
hidOnly: {
|
||||
title: 'HID-Only Modus',
|
||||
desc: 'Wenn Ihre Maus und Tastatur nicht mehr reagieren und das Zurücksetzen der HID-Verbindung nicht hilft, könnte es sich um ein Kompatibilitätsproblem zwischen dem NanoKVM und dem Gerät handeln. Versuchen Sie, den HID-Only Modus zu aktivieren, um die Kompatibilität zu verbessern.',
|
||||
tip1: 'Die Aktivierung des HID-Only Modus entfernt das virtuelle U-Laufwerk und das virtuelle Netzwerk.',
|
||||
tip2: 'Im HID-Only Modus ist das Einbinden von Systemabbilder deaktiviert.',
|
||||
tip3: 'NanoKVM wird nach dem Wechsel in den neuen Modus automatisch neu gestartet.',
|
||||
enable: 'HID-Only Modus aktivieren',
|
||||
disable: 'HID-Only Modus deaktivieren'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: 'ISO oder Festplatten Abbilder',
|
||||
loading: 'Laden...',
|
||||
title: 'Bilder',
|
||||
loading: 'Lädt...',
|
||||
empty: 'Nichts gefunden',
|
||||
mountFailed: 'Mounten fehlgeschlagen',
|
||||
cdrom: 'Binden Sie das Abbild im CD-ROM Modus ein',
|
||||
mountFailed: 'Einbinden fehlgeschlagen',
|
||||
mountDesc:
|
||||
'Bei einigen Systemen muss die virtuelle Festplatte auf dem Remote-Host ausgeworfen werden, bevor das Image gemountet werden kann.',
|
||||
'In einigen Systemen ist es notwendig, die virtuelle Festplatte auf dem entfernten Host auszuwerfen, bevor das Image eingebunden werden kann.',
|
||||
refresh: 'Bilder aktualisieren',
|
||||
tips: {
|
||||
title: 'Wie man Abbilder hochlädt',
|
||||
usb1: 'Den NanoKVM via USB mit dem Computer verbinden.',
|
||||
usb2: 'Stellen sie sicher, das das Virtuelle Festplatte eingebunden ist. (Einstellungen - Virtuelle Festplatte).',
|
||||
usb3: 'Öffnen sie die Virtuelle Festplatte auf ihrem Computer und kopieren sie die Abbilddatei in das root-Verzeichnis des USB-Geräts.',
|
||||
scp1: 'Stellen sie sicher, das sich der NanoKVM und ihr Computer im gleichen lokalen Netzwerk befinden.',
|
||||
scp2: 'Öffnen sie ein Terminal auf ihrem Computer und benutzen sie den SCP-Befehl um die Abbilddatei in das /data Verzeichnis des NanoKVM zu kopieren.',
|
||||
scp3: 'Beispiel: scp ihr-abbild-verzeichnis root@ihre-nanokvm-ip:/data',
|
||||
title: 'So laden Sie Dateien hoch',
|
||||
usb1: 'Verbinden Sie den NanoKVM über USB mit Ihrem Computer.',
|
||||
usb2: 'Stellen Sie sicher, dass die virtuelle Festplatte eingebunden ist (Einstellungen – Virtuelle Festplatte).',
|
||||
usb3: 'Öffnen Sie die virtuelle Festplatte auf Ihrem Computer und kopieren Sie die Image-Datei in das Stammverzeichnis der virtuellen Festplatte.',
|
||||
scp1: 'Stellen Sie sicher, dass sich der NanoKVM und Ihr Computer im selben lokalen Netzwerk befinden.',
|
||||
scp2: 'Öffnen Sie ein Terminal auf Ihrem Computer und verwenden Sie den SCP-Befehl, um die Image-Datei in das Verzeichnis /data auf dem NanoKVM hochzuladen.',
|
||||
scp3: 'Beispiel: scp your-image-path root@your-nanokvm-ip:/data',
|
||||
tfCard: 'TF-Karte',
|
||||
tf1: 'Diese Mehthode wird auf Linuxsystemen unterstützt',
|
||||
tf2: 'Entfernen sie die TF-Karte aus dem NanoKVM (gilt nur für die fertig zusammengebaute Version, bitte das Gehäuse vorher auseinanderbauen).',
|
||||
tf3: 'Legen sie die TF-Karte in den Kartenleser ein und verbinden sie diesen mit dem Computer.',
|
||||
tf4: 'Kopieren sie die Abbild-Datei in das /data Verzeichnis der TF-Karte.',
|
||||
tf5: 'Legen sie die TF-Karte in das NanoKVM ein.'
|
||||
tf1: 'Diese Methode wird unter Linux-Systemen unterstützt.',
|
||||
tf2: 'Entnehmen Sie die TF-Karte aus dem NanoKVM (bei der FULL-Version muss zuvor das Gehäuse geöffnet werden).',
|
||||
tf3: 'Stecken Sie die TF-Karte in einen Kartenleser und verbinden Sie diesen mit Ihrem Computer.',
|
||||
tf4: 'Kopieren Sie die Image-Datei in das Verzeichnis /data auf der TF-Karte.',
|
||||
tf5: 'Setzen Sie die TF-Karte wieder in den NanoKVM ein.'
|
||||
}
|
||||
},
|
||||
script: {
|
||||
title: 'Skript',
|
||||
title: 'Skripte',
|
||||
upload: 'Hochladen',
|
||||
run: 'Ausführen',
|
||||
runBackground: 'Im Hintergrund ausführen',
|
||||
runFailed: 'Ausführung abgebrochen',
|
||||
runFailed: '',
|
||||
attention: 'Achtung',
|
||||
delDesc: 'Sind sie sicher, dass sie diese Datei löschen möchten?',
|
||||
delDesc: 'Möchten Sie diese Datei wirklich löschen?',
|
||||
confirm: 'Ja',
|
||||
cancel: 'Nein',
|
||||
delete: 'Löschen',
|
||||
close: 'Schließen'
|
||||
close: 'Schliessen'
|
||||
},
|
||||
terminal: {
|
||||
title: 'Terminal',
|
||||
nanokvm: 'NanoKVM Terminal',
|
||||
serial: 'Terminal über den Seriellen Port',
|
||||
serialPort: 'Serieller Port',
|
||||
serialPortPlaceholder: 'Serielle Portnummer eingeben',
|
||||
serial: 'Serieller Anschluss Terminal',
|
||||
serialPort: 'Serieller Anschluss',
|
||||
serialPortPlaceholder: 'Bitte seriellen Anschluss angeben',
|
||||
baudrate: 'Baudrate',
|
||||
parity: 'Parität',
|
||||
parityNone: 'Keine',
|
||||
parityEven: 'Gerade',
|
||||
parityOdd: 'Ungerade',
|
||||
flowControl: 'Fluss-Kontrolle',
|
||||
flowControlNone: 'Keine',
|
||||
flowControlSoft: 'Wenig',
|
||||
flowControlHard: 'Viel',
|
||||
dataBits: 'Daten bits',
|
||||
stopBits: 'Stopp bits',
|
||||
confirm: 'Ok'
|
||||
},
|
||||
wol: {
|
||||
title: 'Wake-on-LAN',
|
||||
sending: 'Befehl senden...',
|
||||
sending: 'Sende Befehl...',
|
||||
sent: 'Befehl gesendet',
|
||||
input: 'Bitte die MAC eingeben',
|
||||
input: 'Bitte MAC Adresse eingeben',
|
||||
ok: 'Ok'
|
||||
},
|
||||
download: {
|
||||
title: 'Systemabbild Downloader',
|
||||
input: 'Bitte geben Sie die URL für das Remote-Systemabbild ein',
|
||||
ok: 'Ok',
|
||||
disabled:
|
||||
'/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden',
|
||||
uploadbox: 'Datei hier ablegen oder klicken zum Auswählen',
|
||||
inputfile: 'Bitte geben Sie die Datei für das Systemabbild an',
|
||||
NoISO: 'Keine ISO'
|
||||
},
|
||||
power: {
|
||||
title: 'Einschalten',
|
||||
reset: 'Neustart',
|
||||
power: 'Einschalten',
|
||||
powerShort: 'Einschalten (Kurzes Drücken)',
|
||||
powerLong: 'Einschalten (Langes Drücken)'
|
||||
title: 'Power',
|
||||
showConfirm: 'Bestätigung',
|
||||
showConfirmTip: 'Diese Aktionen benötigen eine zusätzliche Bestätigung',
|
||||
reset: 'Zurücksetzen',
|
||||
power: 'Power',
|
||||
powerShort: 'Power (Kurzer Klick)',
|
||||
powerLong: 'Power (Langer Klick)',
|
||||
resetConfirm: 'Reset-Aktion durchführen?',
|
||||
powerConfirm: 'Power-Aktion durchführen?',
|
||||
okBtn: 'Ja',
|
||||
cancelBtn: 'Nein'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
title: 'Einstellungen',
|
||||
about: {
|
||||
title: 'Über NanoKVM',
|
||||
information: 'Information',
|
||||
information: 'Informationen',
|
||||
ip: 'IP',
|
||||
mdns: 'mDNS',
|
||||
application: 'Versionsnummer',
|
||||
applicationTip: 'NanoKVM web application version',
|
||||
image: 'Image Version',
|
||||
imageTip: 'NanoKVM system image version',
|
||||
deviceKey: 'Geräte-Key',
|
||||
community: 'Community'
|
||||
application: 'Applikations-Version',
|
||||
applicationTip: 'NanoKVM Web Applikations-Version',
|
||||
image: 'Systemabbild-Version',
|
||||
imageTip: 'NanoKVM Systemabbild-Version',
|
||||
deviceKey: 'Geräte Schlüssel',
|
||||
community: 'Community',
|
||||
hostname: 'Hostname',
|
||||
hostnameUpdated: 'Hostname aktualisiert. Neustarten um zu übernehmen.',
|
||||
ipType: {
|
||||
Wired: 'Kabel',
|
||||
Wireless: 'Drahtlos',
|
||||
Other: 'Andere'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar'
|
||||
title: 'Erscheinungsbild',
|
||||
display: 'Bildschirm',
|
||||
language: 'Sprache',
|
||||
menuBar: 'Menu-Leiste',
|
||||
menuBarDesc: 'Symbole in der Menu-Leiste anzeigen',
|
||||
webTitle: 'Web Titel',
|
||||
webTitleDesc: 'Passen Sie den Web-Seite Titel an'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
title: 'Gerät',
|
||||
oled: {
|
||||
title: 'OLED',
|
||||
description: 'OLED screen automatically sleep',
|
||||
0: 'Never',
|
||||
15: '15 sec',
|
||||
30: '30 sec',
|
||||
60: '1 min',
|
||||
180: '3 min',
|
||||
300: '5 min',
|
||||
600: '10 min',
|
||||
1800: '30 min',
|
||||
3600: '1 hour'
|
||||
description: 'Schalte OLED Bildschirm aus nach',
|
||||
0: 'Nie',
|
||||
15: '15 Sek',
|
||||
30: '30 Sek',
|
||||
60: '1 Min',
|
||||
180: '3 Min',
|
||||
300: '5 Min',
|
||||
600: '10 Min',
|
||||
1800: '30 Min',
|
||||
3600: '1 Stunde'
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi',
|
||||
setBtn: 'Config'
|
||||
description: 'Wi-Fi konfigurieren',
|
||||
setBtn: 'Konfigurieren'
|
||||
},
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
networkDesc: 'Mount virtual network card on the remote host'
|
||||
ssh: {
|
||||
description: 'Aktiviere entfernten SSH-Zugang',
|
||||
tip: 'Setzten Sie ein starkes Passwort vor dem aktivieren (Konto - Passwort ändern)'
|
||||
},
|
||||
advanced: 'Erweiterte Einstellungen',
|
||||
swap: {
|
||||
title: 'Swap',
|
||||
disable: 'Deaktivieren',
|
||||
description: 'Grösse der Swap-Datei festlegen',
|
||||
tip: 'Das Aktivieren dieser Funktion kann die Lebensdauer Ihrer SD-Karte verkürzen!'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: 'Mausaktivitäts-Simulator',
|
||||
description: 'Verhindert, dass der remote Host in den Energiesparmodus wechselt',
|
||||
disable: 'Deaktivieren',
|
||||
absolute: 'Absoluter Modus',
|
||||
relative: 'Relativer Modus'
|
||||
},
|
||||
mdns: {
|
||||
description: 'mDNS-Erkennungsdienst aktivieren',
|
||||
tip: 'Deaktivieren Sie den Dienst, wenn Sie ihn nicht benötigen'
|
||||
},
|
||||
hidOnly: 'HID-Only Mode',
|
||||
disk: 'Virtuelle Festplatte',
|
||||
diskDesc: 'Binde das virtuelle U-Laufwerk an den entfernten Host',
|
||||
network: 'Virtuelles Netzwerk',
|
||||
networkDesc: 'Binde die virtuelle Netzwerkkarte an den entfernten Host',
|
||||
reboot: 'Neustarten',
|
||||
rebootDesc: 'Sind Sie sicher dass Sie NanoKVM neustarten möchten?',
|
||||
okBtn: 'Ja',
|
||||
cancelBtn: 'Nein'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
memory: {
|
||||
title: 'Memory optimization',
|
||||
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
|
||||
disable: 'Disable'
|
||||
title: 'Speicher Optimierung',
|
||||
tip: 'Wenn die Speichernutzung das Limit überschreitet, wird die Speicherbereinigung aggressiver durchgeführt, um Speicher freizugeben. Es wird empfohlen, den Wert auf 75 MB zu setzen, wenn Tailscale verwendet wird. Ein Neustart von Tailscale ist erforderlich, damit die Änderung wirksam wird.',
|
||||
disable: 'Deaktivieren'
|
||||
},
|
||||
restart: 'Are you sure to restart Tailscale?',
|
||||
stop: 'Are you sure to stop Tailscale?',
|
||||
stopDesc: 'Log out Tailscale and disable its automatic startup on boot.',
|
||||
loading: 'Lade...',
|
||||
notInstall: 'Tailscale wurde nicht gefunden! Bitte installieren.',
|
||||
restart: 'Tailscale neu starten?',
|
||||
stop: 'Tailscale stoppen?',
|
||||
stopDesc: 'Von Tailscale abmelden und automatischen Start beim Booten deaktivieren.',
|
||||
loading: 'Lädt...',
|
||||
notInstall: 'Tailscale nicht gefunden! Bitte installieren.',
|
||||
install: 'Installieren',
|
||||
installing: 'Installation wird durchgeführt',
|
||||
failed: 'Installation gescheitert',
|
||||
retry:
|
||||
'Bitte neu laden und noch einmal versuchen. Oder versuchen sie Tailscale manuell zu installieren',
|
||||
download: 'Laden sie das',
|
||||
package: 'Installationspaket',
|
||||
unzip: 'und entpacken sie es manuell',
|
||||
upTailscale: 'Hochladen von tailscale in das NanoKVM Verzeichnis /usr/bin/',
|
||||
upTailscaled: 'Hochladen von tailscaled in das NanoKVM Verzeichnis /usr/sbin/',
|
||||
refresh: 'Die aktuelle Seite neu laden',
|
||||
installing: 'Installiere',
|
||||
failed: 'Installation fehlgeschlagen',
|
||||
retry: 'Bitte Seite neu laden und erneut versuchen oder manuelle Installation versuchen.',
|
||||
download: 'Laden Sie das',
|
||||
package: 'Installations-Paket herunter',
|
||||
unzip: 'und entpacken Sie es',
|
||||
upTailscale: 'Tailscale nach /usr/bin/ auf NanoKVM hochladen',
|
||||
upTailscaled: 'Tailscaled nach /usr/bin/ auf NanoKVM hochladen',
|
||||
refresh: 'Aktuelle Seite neu laden',
|
||||
notLogin:
|
||||
'Diese Geräte ist bisher noch nicht verknüpft. Bitte loggen sie sich in ihr Konto ein und verknüpfen sie dieses Gerät mit diesem.',
|
||||
'Das Gerät konnte noch nicht gefunden werden. Bitte melden Sie sich an und verknüpfen Sie dieses Gerät mit Ihrem Konto.',
|
||||
urlPeriod: 'Diese URL ist für 10 Minuten gültig',
|
||||
login: 'Einloggen',
|
||||
loginSuccess: 'Das Einloggen war erfolgreich',
|
||||
enable: 'Tailscale aktivieren',
|
||||
deviceName: 'Name des Geräts',
|
||||
deviceIP: 'Geräte-IP',
|
||||
account: 'Benutzerkonto',
|
||||
logout: 'Ausloggen',
|
||||
logout2: 'Wollen Sie sich wirklich ausloggen?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
login: 'Anmelden',
|
||||
loginSuccess: 'Anmeldung erfolgreich',
|
||||
enable: 'Tailscale einschalten',
|
||||
deviceName: 'Geräte Name',
|
||||
deviceIP: 'Geräte IP',
|
||||
account: 'Konto',
|
||||
logout: 'Abmelden',
|
||||
logoutDesc: 'Möchten Sie sich wirklich abmelden?',
|
||||
uninstall: 'Tailscale deinstallieren',
|
||||
okBtn: 'Ja',
|
||||
cancelBtn: 'Nein'
|
||||
},
|
||||
update: {
|
||||
title: 'Nach neuem Update suchen',
|
||||
queryFailed: 'Versionsnummer konnte nicht erkannt werden',
|
||||
updateFailed: 'Update gescheitert. Bitte versuchen sie es erneut.',
|
||||
isLatest: 'Die aktuelle Version ist bereits installiert.',
|
||||
available:
|
||||
'Ein Update ist verfügbar. Sind sie sicher, dass sie diese Version aktualisieren möchten?',
|
||||
updating: 'Update wird gestartet. Bitte warten...',
|
||||
title: 'Nach Aktualisierungen suchen',
|
||||
queryFailed: 'Version konnte nicht abgefragt werden',
|
||||
updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
||||
isLatest: 'Sie haben bereits die aktuellste Version.',
|
||||
available: 'Eine Aktualisierung ist verfügbar. Möchten sie diese jetzt durchführen?',
|
||||
updating: 'Aktualisierung gestartet. Bitte warten...',
|
||||
confirm: 'Bestätigen',
|
||||
cancel: 'Abbrechen'
|
||||
cancel: 'Abbrechen',
|
||||
preview: 'Vorab-Versionen',
|
||||
previewDesc: 'Erhalten Sie vorab Zugriff auf neue Funktionen und Verbesserungen',
|
||||
previewTip:
|
||||
'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!',
|
||||
offline: {
|
||||
title: 'Offline Aktualisierung',
|
||||
desc: 'Über lokales Installationspaket aktualisieren',
|
||||
upload: 'Hochladen',
|
||||
invalidName:
|
||||
'Ungültiges Dateinamenformat. Bitte laden Sie von den GitHub-Releases herunter.',
|
||||
updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.'
|
||||
}
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
webAccount: 'Web Account Name',
|
||||
password: 'Password',
|
||||
updateBtn: 'Update',
|
||||
logoutBtn: 'Logout'
|
||||
title: 'Konto',
|
||||
webAccount: 'Web Konto Name',
|
||||
password: 'Passwort',
|
||||
updateBtn: 'Ändern',
|
||||
logoutBtn: 'Abmelden',
|
||||
logoutDesc: 'Möchten Sie sich wirklich abmelden?',
|
||||
okBtn: 'Ja',
|
||||
cancelBtn: 'Nein'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: 'Wir sind auf ein Problem gestossen',
|
||||
refresh: 'Neuladen'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'Vollbild ein/aus'
|
||||
},
|
||||
menu: {
|
||||
collapse: 'Menu einblenden',
|
||||
expand: 'Menu verbergen'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,14 +41,18 @@ const en = {
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi for NanoKVM',
|
||||
success: 'Please check the network status of NanoKVM and visit the new IP address.',
|
||||
success: 'Please go to the device to check the network status of NanoKVM.',
|
||||
failed: 'Operation failed, please try again.',
|
||||
invalidMode:
|
||||
'The current mode does not support network setup. Please go to your device and enable Wi-Fi configuration mode.',
|
||||
confirmBtn: 'Ok',
|
||||
finishBtn: 'Finished'
|
||||
},
|
||||
screen: {
|
||||
scale: 'Scale',
|
||||
title: 'Screen',
|
||||
video: 'Video Mode',
|
||||
videoDirectTips: 'Enable HTTPS in "Settings > Device" to use this mode',
|
||||
resolution: 'Resolution',
|
||||
auto: 'Automatic',
|
||||
autoTips:
|
||||
@@ -72,7 +76,14 @@ const en = {
|
||||
placeholder: 'Please input',
|
||||
submit: 'Submit',
|
||||
virtual: 'Keyboard',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
ctrlaltdel: 'Ctrl+Alt+Del',
|
||||
readClipboard: 'Read from Clipboard',
|
||||
clipboardPermissionDenied:
|
||||
'Clipboard permission denied. Please allow clipboard access in your browser.',
|
||||
clipboardReadError: 'Failed to read clipboard',
|
||||
dropdownEnglish: 'English',
|
||||
dropdownGerman: 'German',
|
||||
dropdownRussian: 'Russian'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
@@ -86,6 +97,12 @@ const en = {
|
||||
mode: 'Mouse mode',
|
||||
absolute: 'Absolute mode',
|
||||
relative: 'Relative mode',
|
||||
direction: 'Wheel direction',
|
||||
scrollUp: 'Scroll up',
|
||||
scrollDown: 'Scroll down',
|
||||
speed: 'Wheel speed',
|
||||
fast: 'Fast',
|
||||
slow: 'Slow',
|
||||
requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.',
|
||||
resetHid: 'Reset HID',
|
||||
hidOnly: {
|
||||
@@ -102,11 +119,18 @@ const en = {
|
||||
title: 'Images',
|
||||
loading: 'Loading...',
|
||||
empty: 'Nothing Found',
|
||||
cdrom: 'Mount the image in CD-ROM mode',
|
||||
mountFailed: 'Mount Failed',
|
||||
mountMode: 'Mount mode',
|
||||
mountFailed: 'Mount failed',
|
||||
mountDesc:
|
||||
"In some systems, it's necessary to eject the virtual disk on the remote host before mounting the image.",
|
||||
'On some systems, you need to eject the virtual disk from the remote host before mounting the image.',
|
||||
unmountFailed: 'Unmount failed',
|
||||
unmountDesc:
|
||||
'On some systems, you need to manually eject from the remote host before unmounting the image.',
|
||||
refresh: 'Refresh the image list',
|
||||
attention: 'Attention',
|
||||
deleteConfirm: 'Are you sure you want to delete this image?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No',
|
||||
tips: {
|
||||
title: 'How to upload',
|
||||
usb1: 'Connect the NanoKVM to your computer via USB.',
|
||||
@@ -166,7 +190,10 @@ const en = {
|
||||
title: 'Image Downloader',
|
||||
input: 'Please enter a remote image URL',
|
||||
ok: 'Ok',
|
||||
disabled: '/data partition is RO, so we cannot download the image'
|
||||
disabled: '/data partition is RO, so we cannot download the image',
|
||||
uploadbox: 'Drop file here or click to select',
|
||||
inputfile: 'Please enter the image File',
|
||||
NoISO: 'No ISO'
|
||||
},
|
||||
power: {
|
||||
title: 'Power',
|
||||
@@ -195,12 +222,18 @@ const en = {
|
||||
deviceKey: 'Device Key',
|
||||
community: 'Community',
|
||||
hostname: 'Hostname',
|
||||
hostnameUpdated: 'Hostname updated. Reboot to apply.'
|
||||
hostnameUpdated: 'Hostname updated. Reboot to apply.',
|
||||
ipType: {
|
||||
Wired: 'Wired',
|
||||
Wireless: 'Wireless',
|
||||
Other: 'Other'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
languageDesc: 'Select the language for the interface',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar',
|
||||
webTitle: 'Web Title',
|
||||
@@ -224,14 +257,29 @@ const en = {
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi',
|
||||
setBtn: 'Config'
|
||||
apMode: 'AP mode is enabled, connect to Wi-Fi by scanning QR code',
|
||||
connect: 'Join Wi-Fi',
|
||||
connectDesc1: 'Please enter the network ssid and password',
|
||||
connectDesc2: 'Please enter the password to join this network',
|
||||
disconnect: 'Are you sure to disconnect the network?',
|
||||
failed: 'Connection failed, please try again.',
|
||||
ssid: 'Name',
|
||||
password: 'Password',
|
||||
joinBtn: 'Join',
|
||||
confirmBtn: 'Ok',
|
||||
cancelBtn: 'Cancel'
|
||||
},
|
||||
ssh: {
|
||||
description: 'Enable SSH remote access',
|
||||
tip: 'Set a strong password before enabling (Account - Change Password)'
|
||||
},
|
||||
tls: {
|
||||
description: 'Enable HTTPS protocol',
|
||||
tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.'
|
||||
},
|
||||
advanced: 'Advanced Settings',
|
||||
swap: {
|
||||
title: 'Swap',
|
||||
disable: 'Disable',
|
||||
description: 'Set the swap file size',
|
||||
tip: "Enabling this feature could shorten your SD card's usable life!"
|
||||
@@ -247,9 +295,24 @@ const en = {
|
||||
description: 'Enable mDNS discovery service',
|
||||
tip: "Turning it off if it's not needed"
|
||||
},
|
||||
hdmi: {
|
||||
description: 'Enable HDMI/monitor output'
|
||||
},
|
||||
autostart: {
|
||||
title: 'Autostart Scripts Settings',
|
||||
description: 'Manage scripts that run automatically on system startup',
|
||||
new: 'New',
|
||||
deleteConfirm: 'Are you sure you want to delete this file?',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
scriptName: 'Autostart Script Name',
|
||||
scriptContent: 'Autostart Script Content',
|
||||
settings: 'Settings'
|
||||
},
|
||||
hidOnly: 'HID-Only Mode',
|
||||
hidOnlyDesc: 'Stop emulating virtual devices, retaining only basic HID control',
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
diskDesc: 'Mount SD card on the remote host',
|
||||
network: 'Virtual Network',
|
||||
networkDesc: 'Mount virtual network card on the remote host',
|
||||
reboot: 'Reboot',
|
||||
@@ -261,8 +324,11 @@ const en = {
|
||||
title: 'Tailscale',
|
||||
memory: {
|
||||
title: 'Memory optimization',
|
||||
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. It's recommended to set to 75MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
|
||||
disable: 'Disable'
|
||||
tip: 'When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. A Tailscale restart is required for the change to take effect.'
|
||||
},
|
||||
swap: {
|
||||
title: 'Swap memory',
|
||||
tip: 'If issues persist after enabling memory optimization, try enabling swap memory. This sets the swap file size to 256MB by default, which can be adjusted in "Settings > Device".'
|
||||
},
|
||||
restart: 'Restart Tailscale?',
|
||||
stop: 'Stop Tailscale?',
|
||||
@@ -279,6 +345,8 @@ const en = {
|
||||
upTailscale: 'Upload tailscale to NanoKVM directory /usr/bin/',
|
||||
upTailscaled: 'Upload tailscaled to NanoKVM directory /usr/sbin/',
|
||||
refresh: 'Refresh current page',
|
||||
notRunning: 'Tailscale is not running. Please start it to continue.',
|
||||
run: 'Start',
|
||||
notLogin:
|
||||
'The device has not been bound yet. Please login and bind this device to your account.',
|
||||
urlPeriod: 'This url is valid for 10 minutes',
|
||||
@@ -291,6 +359,7 @@ const en = {
|
||||
logout: 'Logout',
|
||||
logoutDesc: 'Are you sure you want to logout?',
|
||||
uninstall: 'Uninstall Tailscale',
|
||||
uninstallDesc: 'Are you sure you want to uninstall Tailscale?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
@@ -306,7 +375,14 @@ const en = {
|
||||
preview: 'Preview Updates',
|
||||
previewDesc: 'Get early access to new features and improvements',
|
||||
previewTip:
|
||||
'Please be aware that preview releases may contain bugs or incomplete functionality!'
|
||||
'Please be aware that preview releases may contain bugs or incomplete functionality!',
|
||||
offline: {
|
||||
title: 'Offline Updates',
|
||||
desc: 'Update through local installation package',
|
||||
upload: 'Upload',
|
||||
invalidName: 'Invalid filename format. Please download from GitHub releases.',
|
||||
updateFailed: 'Update failed. Please retry.'
|
||||
}
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const en = {
|
||||
const es = {
|
||||
translation: {
|
||||
head: {
|
||||
desktop: 'Escritorio remoto',
|
||||
@@ -9,51 +9,49 @@ const en = {
|
||||
},
|
||||
auth: {
|
||||
login: 'Iniciar sesión',
|
||||
placeholderUsername: 'Introduce tu usuario',
|
||||
placeholderUsername: 'Introduce tu nombre de usuario',
|
||||
placeholderPassword: 'Introduce tu contraseña',
|
||||
placeholderPassword2: 'Introduce tu contraseña de nuevo',
|
||||
noEmptyUsername: 'El usuario no puede estar vacío',
|
||||
noEmptyUsername: 'El nombre de usuario no puede estar vacío',
|
||||
noEmptyPassword: 'La contraseña no puede estar vacía',
|
||||
noAccount:
|
||||
'No se ha encontrado la cuenta. Por favor, recarga la página o recupera tu contraseña.',
|
||||
invalidUser: 'Usuario o contraseña incorrectos',
|
||||
noAccount: 'No se ha encontrado la cuenta. Por favor, recarga la página o recupera tu contraseña.',
|
||||
invalidUser: 'Nombre de usuario o contraseña incorrectos',
|
||||
error: 'Error inesperado',
|
||||
changePassword: 'Cambiar contraseña',
|
||||
changePasswordDesc: 'Para la seguridad de su dispositivo, por favor, modifique la contraseña de inicio de sesión en la web.',
|
||||
differentPassword: 'Las contraseñas no coinciden',
|
||||
changePasswordDesc:
|
||||
'Para la seguridad de su dispositivo, por favor modifique la contraseña de inicio de sesión en la web.',
|
||||
illegalUsername: 'El usuario contiene caracteres no permitidos',
|
||||
illegalUsername: 'El nombre de usuario contiene caracteres no permitidos',
|
||||
illegalPassword: 'La contraseña contiene caracteres no permitidos',
|
||||
forgetPassword: 'Contraseña olvidada',
|
||||
ok: 'Aceptar',
|
||||
cancel: 'Cancelar',
|
||||
loginButtonText: 'Iniciar sesión',
|
||||
tips: {
|
||||
reset1:
|
||||
'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.',
|
||||
reset2: 'For detailed steps, please consult this document:',
|
||||
reset3: 'Web default account:',
|
||||
reset4: 'SSH default account:',
|
||||
change1: 'Please note that this action will change the following passwords:',
|
||||
change2: 'Web login password',
|
||||
change3: 'System root password (SSH login password)',
|
||||
change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.'
|
||||
reset1: 'Para restablecer las contraseñas, mantén pulsado el botón BOOT del NanoKVM durante 10 segundos.',
|
||||
reset2: 'Para ver los pasos detallados, consulta este documento:',
|
||||
reset3: 'Cuenta predeterminada de la interfaz web:',
|
||||
reset4: 'Cuenta predeterminada de SSH:',
|
||||
change1: 'Ten en cuenta que esta acción cambiará las siguientes contraseñas:',
|
||||
change2: 'Contraseña de acceso web',
|
||||
change3: 'Contraseña root del sistema (contraseña de acceso por SSH)',
|
||||
change4: 'Para restablecer las contraseñas, mantén pulsado el botón BOOT del NanoKVM.'
|
||||
}
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi for NanoKVM',
|
||||
success: 'Please check the network status of NanoKVM and visit the new IP address.',
|
||||
failed: 'Operation failed, please try again.',
|
||||
confirmBtn: 'Ok',
|
||||
finishBtn: 'Finished'
|
||||
description: 'Configura el Wi-Fi para el NanoKVM',
|
||||
success: 'Comprueba el estado de red del NanoKVM y accede a la nueva dirección IP.',
|
||||
failed: 'La operación ha fallado, vuelve a intentarlo.',
|
||||
confirmBtn: 'Aceptar',
|
||||
finishBtn: 'Finalizado'
|
||||
},
|
||||
screen: {
|
||||
title: 'Pantalla',
|
||||
video: 'Modo de vídeo',
|
||||
videoDirectTips: 'Habilita HTTPS en "Ajustes > Dispositivo" para usar este modo',
|
||||
resolution: 'Resolución',
|
||||
auto: 'Automático',
|
||||
autoTips:
|
||||
'En determinadas resoluciones pueden producirse rasgado de imagen o desplazamiento del ratón. Prueba a ajustar la resolución del host remoto o desactiva el modo automático.',
|
||||
autoTips: 'En determinadas resoluciones pueden producirse rasgado de imagen (tearing) o desplazamiento del ratón. Prueba a ajustar la resolución del host remoto o desactiva el modo automático.',
|
||||
fps: 'FPS',
|
||||
customizeFps: 'Personalizar',
|
||||
quality: 'Calidad',
|
||||
@@ -62,19 +60,26 @@ const en = {
|
||||
qualityMedium: 'Medio',
|
||||
qualityLow: 'Bajo',
|
||||
frameDetect: 'Detectar fotogramas',
|
||||
frameDetectTip:
|
||||
'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
|
||||
resetHdmi: 'Reset HDMI'
|
||||
frameDetectTip: 'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
|
||||
resetHdmi: 'Reiniciar HDMI'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Teclado',
|
||||
paste: 'Pegar',
|
||||
tips: 'Solo están soportadas las letras y símbolos estándar del teclado',
|
||||
tips: 'Sólo están soportadas las letras y símbolos estándar del teclado',
|
||||
placeholder: 'Por favor, introduce el texto',
|
||||
submit: 'Enviar',
|
||||
virtual: 'Teclado virtual',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
ctrlaltdel: 'Ctrl+Alt+Del',
|
||||
readClipboard: 'Leer del portapapeles',
|
||||
clipboardPermissionDenied: 'Permiso de portapapeles denegado. Por favor, permite el acceso al portapapeles en tu navegador.',
|
||||
clipboardReadError: 'Error al leer del portapapeles',
|
||||
dropdownEnglish: 'Inglés',
|
||||
dropdownGerman: 'Alemán',
|
||||
},
|
||||
mouse: {
|
||||
title: 'Ratón',
|
||||
cursor: 'Estilo de cursor',
|
||||
default: 'Cursor por defecto',
|
||||
pointer: 'Cursor de puntero',
|
||||
cell: 'Cursor de celda',
|
||||
@@ -84,17 +89,35 @@ const en = {
|
||||
mode: 'Modo de ratón',
|
||||
absolute: 'Modo absoluto',
|
||||
relative: 'Modo relativo',
|
||||
requestPointer:
|
||||
'Usando modo relativo. Por favor, haz clic en el escritorio para obtener el cursor del ratón.',
|
||||
resetHid: 'Restablecer HID'
|
||||
speed: 'Velocidad de la rueda',
|
||||
fast: 'Rápida',
|
||||
slow: 'Lenta',
|
||||
requestPointer: 'Usando modo relativo. Por favor, haz clic en el escritorio para obtener el cursor del ratón.',
|
||||
resetHid: 'Restablecer HID',
|
||||
hidOnly: {
|
||||
title: 'Modo sólo HID',
|
||||
desc: 'Si tu ratón y teclado dejan de responder y restablecer el HID no ayuda, podría ser un problema de compatibilidad entre el NanoKVM y el dispositivo. Prueba a habilitar el modo sólo HID para mejorar la compatibilidad.',
|
||||
tip1: 'Habilitar el modo sólo HID desmontará el disco virtual y la red virtual',
|
||||
tip2: 'En modo sólo HID, el montaje de imágenes está deshabilitado',
|
||||
tip3: 'El NanoKVM se reiniciará automáticamente después de cambiar de modo',
|
||||
enable: 'Habilitar modo sólo HID',
|
||||
disable: 'Desactivar modo sólo HID'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: 'Imágenes',
|
||||
loading: 'Cargando...',
|
||||
empty: 'No se ha encontrado nada',
|
||||
mountMode: 'Modo de montaje',
|
||||
mountFailed: 'Fallo al montar',
|
||||
mountDesc:
|
||||
'En algunos sistemas, es necesario expulsar el disco virtual en el host remoto antes de montar una imagen.',
|
||||
mountDesc: 'En algunos sistemas, es necesario expulsar el disco virtual del host remoto antes de montar una imagen.',
|
||||
unmountFailed: 'Fallo al desmontar',
|
||||
unmountDesc: 'En algunos sistemas, es necesario expulsar manualmente el disco virtual desde el host remoto antes de desmontar la imagen.',
|
||||
refresh: 'Actualizar la lista de imágenes',
|
||||
attention: 'Atención',
|
||||
deleteConfirm: '¿Estás seguro de que deseas eliminar esta imagen?',
|
||||
okBtn: 'Sí',
|
||||
cancelBtn: 'No',
|
||||
tips: {
|
||||
title: 'Cómo subir imágenes',
|
||||
usb1: 'Conecta el NanoKVM a tu computadora mediante USB.',
|
||||
@@ -103,12 +126,12 @@ const en = {
|
||||
scp1: 'Asegúrate de que el NanoKVM y tu computadora estén en la misma red local.',
|
||||
scp2: 'Abre una terminal en tu computadora y usa el comando SCP para subir el archivo de imagen al directorio /data en el NanoKVM.',
|
||||
scp3: 'Ejemplo: scp tu-ruta-de-imagen root@tu-ip-del-nanokvm:/data',
|
||||
tfCard: 'Tarjeta TF',
|
||||
tfCard: 'Tarjeta SD',
|
||||
tf1: 'Este método es compatible con el sistema Linux',
|
||||
tf2: 'Obtén la tarjeta TF del NanoKVM (para la versión FULL, desmonta la carcasa primero).',
|
||||
tf3: 'Inserta la tarjeta TF en un lector de tarjetas y conéctalo a tu computadora.',
|
||||
tf4: 'Copia el archivo de imagen en el directorio /data de la tarjeta TF.',
|
||||
tf5: 'Inserta la tarjeta TF en el NanoKVM.'
|
||||
tf2: 'Obtén la tarjeta SD del NanoKVM (para la versión FULL, desmonta la carcasa primero).',
|
||||
tf3: 'Inserta la tarjeta SD en un lector de tarjetas y conéctalo a tu computadora.',
|
||||
tf4: 'Copia el archivo de imagen en el directorio /data de la tarjeta SD.',
|
||||
tf5: 'Inserta la tarjeta SD en el NanoKVM.'
|
||||
}
|
||||
},
|
||||
script: {
|
||||
@@ -118,7 +141,7 @@ const en = {
|
||||
runBackground: 'Ejecutar en segundo plano',
|
||||
runFailed: 'Ejecución fallida',
|
||||
attention: 'Atención',
|
||||
delDesc: '¿Estás seguro que deseas eliminar este archivo?',
|
||||
delDesc: '¿Estás seguro de que deseas eliminar este archivo?',
|
||||
confirm: 'Sí',
|
||||
cancel: 'No',
|
||||
delete: 'Eliminar',
|
||||
@@ -131,6 +154,16 @@ const en = {
|
||||
serialPort: 'Puerto Serie',
|
||||
serialPortPlaceholder: 'Por favor, introduce el puerto serie',
|
||||
baudrate: 'Tasa de baudios',
|
||||
parity: 'Paridad',
|
||||
parityNone: 'Ninguna',
|
||||
parityEven: 'Par',
|
||||
parityOdd: 'Impar',
|
||||
flowControl: 'Control de flujo',
|
||||
flowControlNone: 'Ninguno',
|
||||
flowControlSoft: 'Software',
|
||||
flowControlHard: 'Hardware',
|
||||
dataBits: 'Bits de datos',
|
||||
stopBits: 'Bits de parada',
|
||||
confirm: 'Confirmar'
|
||||
},
|
||||
wol: {
|
||||
@@ -138,17 +171,29 @@ const en = {
|
||||
sending: 'Enviando comando...',
|
||||
sent: 'Comando enviado',
|
||||
input: 'Por favor, introduce la dirección MAC',
|
||||
ok: 'Vale'
|
||||
ok: 'Aceptar'
|
||||
},
|
||||
download: {
|
||||
title: 'Descargador de imágenes',
|
||||
input: 'Por favor, introduce la URL de una imagen remota',
|
||||
ok: 'Aceptar',
|
||||
disabled: 'La partición /data es de sólo lectura, no se puede descargar la imagen'
|
||||
},
|
||||
power: {
|
||||
title: 'Encender / Apagar',
|
||||
showConfirm: 'Confirmación',
|
||||
showConfirmTip: 'Las operaciones de encendido requieren confirmación adicional',
|
||||
reset: 'Reiniciar',
|
||||
power: 'Encender / Apagar',
|
||||
powerShort: 'Encender / Apagar (pulsación corta)',
|
||||
powerLong: 'Encender / Apagar (pulsación larga)'
|
||||
powerLong: 'Encendido/Apagado (pulsación larga)',
|
||||
resetConfirm: '¿Desea proceder con la operación de reinicio?',
|
||||
powerConfirm: '¿Desea proceder con la operación de encendido?',
|
||||
okBtn: 'Sí',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
title: 'Ajustes',
|
||||
about: {
|
||||
title: 'Sobre NanoKVM',
|
||||
information: 'Información',
|
||||
@@ -159,65 +204,115 @@ const en = {
|
||||
image: 'Versión de la imagen',
|
||||
imageTip: 'NanoKVM system image version',
|
||||
deviceKey: 'Clave del dispositivo',
|
||||
community: 'Comunidad'
|
||||
community: 'Comunidad',
|
||||
hostname: 'Nombre del host',
|
||||
hostnameUpdated: 'Nombre del host actualizado. Reinicia para aplicar.',
|
||||
ipType: {
|
||||
Wired: 'Cableada',
|
||||
Wireless: 'Inalámbrica',
|
||||
Other: 'Otra'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar'
|
||||
title: 'Apariencia',
|
||||
display: 'Pantalla',
|
||||
language: 'Idioma',
|
||||
menuBar: 'Barra de menú',
|
||||
menuBarDesc: 'Mostrar iconos en la barra de menú',
|
||||
webTitle: 'Título web',
|
||||
webTitleDesc: 'Personaliza el título de la página web'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
title: 'Dispositivo',
|
||||
oled: {
|
||||
title: 'OLED',
|
||||
description: 'OLED screen automatically sleep',
|
||||
0: 'Never',
|
||||
15: '15 sec',
|
||||
30: '30 sec',
|
||||
description: 'La pantalla OLED entra en reposo automáticamente',
|
||||
0: 'Nunca',
|
||||
15: '15 s',
|
||||
30: '30 s',
|
||||
60: '1 min',
|
||||
180: '3 min',
|
||||
300: '5 min',
|
||||
600: '10 min',
|
||||
1800: '30 min',
|
||||
3600: '1 hour'
|
||||
3600: '1 hora'
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi',
|
||||
setBtn: 'Config'
|
||||
description: 'Configura el Wi-Fi',
|
||||
setBtn: 'Configurar'
|
||||
},
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
networkDesc: 'Mount virtual network card on the remote host'
|
||||
ssh: {
|
||||
description: 'Habilitar acceso remoto SSH',
|
||||
tip: 'Establece una contraseña segura antes de habilitar (Cuenta - Cambiar contraseña)'
|
||||
},
|
||||
tls: {
|
||||
description: 'Habilitar protocolo HTTPS',
|
||||
tip: 'Aviso: Usar HTTPS puede aumentar la latencia, especialmente en modo de vídeo MJPEG.'
|
||||
},
|
||||
advanced: 'Ajustes avanzados',
|
||||
swap: {
|
||||
title: 'Memoria Swap',
|
||||
disable: 'Desactivar',
|
||||
description: 'Establece el tamaño del archivo swap',
|
||||
tip: 'Habilitar esta función podría acortar la vida útil de tu tarjeta SD.'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: 'Mouse Jiggler',
|
||||
description: 'Evitar que el host remoto entre en reposo',
|
||||
disable: 'Desactivar',
|
||||
absolute: 'Modo absoluto',
|
||||
relative: 'Modo relativo'
|
||||
},
|
||||
mdns: {
|
||||
description: 'Habilitar servicio de descubrimiento mDNS',
|
||||
tip: 'Desactívalo si no es necesario'
|
||||
},
|
||||
hdmi: {
|
||||
description: 'Habilitar salida HDMI/monitor'
|
||||
},
|
||||
hidOnly: 'Modo sólo HID',
|
||||
disk: 'Disco Virtual',
|
||||
diskDesc: 'Montar disco virtual en el host remoto',
|
||||
media: 'Imagen virtual',
|
||||
mediaDesc: 'Adjuntar dispositivo de imagen virtual al host remoto',
|
||||
network: 'Red Virtual',
|
||||
networkDesc: 'Montar tarjeta de red virtual en el host remoto',
|
||||
reboot: 'Reiniciar',
|
||||
rebootDesc: '¿Estás seguro de que deseas reiniciar el NanoKVM?',
|
||||
usb: 'Interfaz USB',
|
||||
usbDesc: 'Habilitar interfaz USB hacia el host',
|
||||
okBtn: 'Sí',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
memory: {
|
||||
title: 'Memory optimization',
|
||||
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
|
||||
disable: 'Disable'
|
||||
title: 'Optimización de memoria',
|
||||
tip: 'Cuando el uso de memoria supera el límite, la recolección de basura se ejecuta de forma más agresiva para intentar liberar memoria. Es necesario reiniciar Tailscale para que el cambio surta efecto.'
|
||||
},
|
||||
restart: 'Are you sure to restart Tailscale?',
|
||||
stop: 'Are you sure to stop Tailscale?',
|
||||
stopDesc: 'Log out Tailscale and disable its automatic startup on boot.',
|
||||
swap: {
|
||||
title: 'Memoria Swap',
|
||||
tip: 'Si los problemas persisten después de habilitar la optimización de memoria, prueba a activar la memoria swap. Esto establece el tamaño del archivo swap en 256 MB por defecto, que se puede ajustar en "Ajustes > Dispositivo".'
|
||||
},
|
||||
restart: '¿Seguro que deseas reiniciar Tailscale?',
|
||||
stop: '¿Seguro que deseas detener Tailscale?',
|
||||
stopDesc: 'Cerrar sesión en Tailscale y desactivar su inicio automático al arrancar.',
|
||||
loading: 'Cargando...',
|
||||
notInstall: '¡Tailscale no encontrado! Por favor, instálalo.',
|
||||
install: 'Instalar',
|
||||
installing: 'Instalando',
|
||||
failed: 'La instalación falló',
|
||||
retry:
|
||||
'Por favor, actualiza la página e inténtalo de nuevo. O intenta instalarlo manualmente',
|
||||
retry: 'Por favor, actualiza la página e inténtalo de nuevo. O intenta instalarlo manualmente',
|
||||
download: 'Descargar el',
|
||||
package: 'paquete de instalación',
|
||||
unzip: 'y descomprimirlo',
|
||||
upTailscale: 'Sube tailscale al directorio /usr/bin/ del NanoKVM',
|
||||
upTailscaled: 'Sube tailscaled al directorio /usr/sbin/ del NanoKVM',
|
||||
refresh: 'Actualizar la página actual',
|
||||
notLogin:
|
||||
'El dispositivo aún no ha sido vinculado. Por favor, inicia sesión y vincula este dispositivo a tu cuenta.',
|
||||
notRunning: 'Tailscale no se está ejecutando. Por favor, inícialo para continuar.',
|
||||
run: 'Iniciar',
|
||||
notLogin: 'El dispositivo aún no ha sido vinculado. Por favor, inicia sesión y vincula este dispositivo a tu cuenta.',
|
||||
urlPeriod: 'Esta URL es válida por 10 minutos',
|
||||
login: 'Iniciar sesión',
|
||||
loginSuccess: 'Inicio de sesión exitoso',
|
||||
@@ -226,8 +321,10 @@ const en = {
|
||||
deviceIP: 'IP del dispositivo',
|
||||
account: 'Cuenta',
|
||||
logout: 'Cerrar sesión',
|
||||
logout2: '¿Seguro que quieres cerrar sesión?',
|
||||
okBtn: 'Yes',
|
||||
logoutDesc: '¿Estás seguro de que deseas cerrar sesión?',
|
||||
uninstall: 'Desinstalar Tailscale',
|
||||
uninstallDesc: '¿Estás seguro de que deseas desinstalar Tailscale?',
|
||||
okBtn: 'Sí',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
update: {
|
||||
@@ -238,17 +335,34 @@ const en = {
|
||||
available: 'Hay una actualización disponible. ¿Estás seguro de que quieres actualizar?',
|
||||
updating: 'Actualización iniciada. Por favor, espera...',
|
||||
confirm: 'Confirmar',
|
||||
cancel: 'Cancelar'
|
||||
cancel: 'Cancelar',
|
||||
preview: 'Vista previa de actualizaciones',
|
||||
previewDesc: 'Accede anticipadamente a nuevas funciones y mejoras',
|
||||
previewTip: 'Ten en cuenta que las versiones de vista previa pueden contener errores o funcionalidades incompletas'
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
webAccount: 'Web Account Name',
|
||||
password: 'Password',
|
||||
updateBtn: 'Update',
|
||||
logoutBtn: 'Logout'
|
||||
title: 'Cuenta',
|
||||
webAccount: 'Nombre de la cuenta web',
|
||||
password: 'Contraseña',
|
||||
updateBtn: 'Actualizar',
|
||||
logoutBtn: 'Cerrar sesión',
|
||||
logoutDesc: '¿Estás seguro de que deseas cerrar sesión?',
|
||||
okBtn: 'Sí',
|
||||
cancelBtn: 'No'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: 'Hemos encontrado un problema',
|
||||
refresh: 'Actualizar'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'Activar/Desactivar pantalla completa'
|
||||
},
|
||||
menu: {
|
||||
collapse: 'Colapsar menú',
|
||||
expand: 'Expandir menú'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default en;
|
||||
export default es;
|
||||
|
||||
@@ -40,20 +40,22 @@ const ko = {
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Wi-Fi 설정 for NanoKVM',
|
||||
description: 'NanoKVM Wi-Fi 설정',
|
||||
success: 'NanoKVM의 네트워크 상태를 확인하고 새 IP 주소로 접속하세요.',
|
||||
failed: '작업이 실패했습니다. 다시 시도하세요.',
|
||||
failed: '작업에 실패했습니다. 다시 시도하세요.',
|
||||
confirmBtn: '확인',
|
||||
finishBtn: '완료'
|
||||
},
|
||||
screen: {
|
||||
title: '화면',
|
||||
video: '비디오 모드',
|
||||
videoDirectTips: '이 모드를 사용하려면 "설정 > 장치"에서 HTTPS를 활성화하세요',
|
||||
resolution: '해상도',
|
||||
auto: '자동 설정',
|
||||
autoTips:
|
||||
'일부 해상도에서는 화면이 왜곡되거나 마우스 동작이 비정상적으로 나타날 수 있습니다. 원격 컴퓨터의 해상도를 변경하거나 자동 설정 대신 수동 설정을 사용해 보세요.',
|
||||
fps: 'FPS',
|
||||
customizeFps: 'FPS 설정',
|
||||
customizeFps: '사용자 지정',
|
||||
quality: '품질',
|
||||
qualityLossless: '무손실',
|
||||
qualityHigh: '높음',
|
||||
@@ -65,6 +67,7 @@ const ko = {
|
||||
resetHdmi: 'HDMI 초기화'
|
||||
},
|
||||
keyboard: {
|
||||
title: '키보드',
|
||||
paste: '붙여넣기',
|
||||
tips: '표준 키보드 문자 및 기호만 지원됩니다',
|
||||
placeholder: '입력하세요',
|
||||
@@ -73,6 +76,8 @@ const ko = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
title: '마우스',
|
||||
cursor: '커서 스타일',
|
||||
default: '기본 커서',
|
||||
pointer: '포인터 커서',
|
||||
cell: '셀 커서',
|
||||
@@ -82,18 +87,37 @@ const ko = {
|
||||
mode: '마우스 모드',
|
||||
absolute: '절대값 모드',
|
||||
relative: '상대값 모드',
|
||||
speed: '휠 속도',
|
||||
fast: '빠름',
|
||||
slow: '느림',
|
||||
requestPointer: '상대값 모드를 사용 중입니다. 커서를 찾으려면 데스크톱을 클릭하세요.',
|
||||
resetHid: 'HID 초기화'
|
||||
resetHid: 'HID 초기화',
|
||||
hidOnly: {
|
||||
title: 'HID 전용 모드',
|
||||
desc: '마우스와 키보드가 응답하지 않고 HID 초기화도 도움이 되지 않는다면, NanoKVM과 장치 간의 호환성 문제일 수 있습니다. 더 나은 호환성을 위해 HID 전용 모드를 활성화해 보세요.',
|
||||
tip1: 'HID 전용 모드를 활성화하면 가상 USB와 가상 네트워크가 언마운트됩니다',
|
||||
tip2: 'HID 전용 모드에서는 이미지 마운트가 비활성화됩니다',
|
||||
tip3: '모드 전환 후 NanoKVM이 자동으로 재부팅됩니다',
|
||||
enable: 'HID 전용 모드 활성화',
|
||||
disable: 'HID 전용 모드 비활성화'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: '이미지',
|
||||
loading: '불러오는 중...',
|
||||
empty: '아무것도 없습니다.',
|
||||
cdrom: 'CD-ROM 모드로 이미지 마운트',
|
||||
mountFailed: '이미지 마운트 실패',
|
||||
mountMode: '마운트 모드',
|
||||
mountFailed: '마운트 실패',
|
||||
mountDesc:
|
||||
'일부 시스템에서는 이미지를 마운트하기 전에 원격 호스트에서 가상 디스크를 제거해야 합니다.',
|
||||
unmountFailed: '언마운트 실패',
|
||||
unmountDesc:
|
||||
'일부 시스템에서는 이미지를 언마운트하기 전에 원격 호스트에서 수동으로 제거하여야 합니다.',
|
||||
refresh: '이미지 목록 새로고침',
|
||||
attention: '주의',
|
||||
deleteConfirm: '이 이미지를 제거하시겠습니까?',
|
||||
okBtn: '네',
|
||||
cancelBtn: '아니오',
|
||||
tips: {
|
||||
title: '업로드 방법',
|
||||
usb1: 'USB를 통해 NanoKVM을 컴퓨터에 연결하세요.',
|
||||
@@ -129,7 +153,17 @@ const ko = {
|
||||
serial: '시리얼 포트 터미널',
|
||||
serialPort: '시리얼 포트',
|
||||
serialPortPlaceholder: '시리얼 포트를 입력하세요',
|
||||
baudrate: 'Baud rate',
|
||||
baudrate: '전송 속도',
|
||||
parity: '패리티',
|
||||
parityNone: '없음',
|
||||
parityEven: '짝수',
|
||||
parityOdd: '홀수',
|
||||
flowControl: '흐름 제어',
|
||||
flowControlNone: '없음',
|
||||
flowControlSoft: '소프트웨어',
|
||||
flowControlHard: '하드웨어',
|
||||
dataBits: '데이터 비트',
|
||||
stopBits: '정지 비트',
|
||||
confirm: '확인'
|
||||
},
|
||||
wol: {
|
||||
@@ -147,10 +181,16 @@ const ko = {
|
||||
},
|
||||
power: {
|
||||
title: '전원',
|
||||
showConfirm: '확인',
|
||||
showConfirmTip: '전원 작업에는 추가 확인이 필요합니다',
|
||||
reset: '리셋',
|
||||
power: '전원',
|
||||
powerShort: '전원 (짧게 누르기)',
|
||||
powerLong: '전원 (길게 누르기)'
|
||||
powerLong: '전원 (길게 누르기)',
|
||||
resetConfirm: '리셋 작업을 진행하시겠습니까?',
|
||||
powerConfirm: '전원 작업을 진행하시겠습니까?',
|
||||
okBtn: '네',
|
||||
cancelBtn: '아니오'
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
@@ -164,14 +204,23 @@ const ko = {
|
||||
image: '이미지 버전',
|
||||
imageTip: 'NanoKVM 시스템 이미지 버전',
|
||||
deviceKey: '장치 키',
|
||||
community: '커뮤니티'
|
||||
community: '커뮤니티',
|
||||
hostname: '호스트 이름',
|
||||
hostnameUpdated: '호스트 이름이 업데이트되었습니다. 적용하려면 재부팅하세요.',
|
||||
ipType: {
|
||||
Wired: '유선',
|
||||
Wireless: '무선',
|
||||
Other: '기타'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: '디자인',
|
||||
display: '표시',
|
||||
language: '언어',
|
||||
menuBar: '메뉴 바',
|
||||
menuBarDesc: '메뉴 바에 아이콘을 표시'
|
||||
menuBarDesc: '메뉴 바에 아이콘을 표시',
|
||||
webTitle: '웹 제목',
|
||||
webTitleDesc: '웹 페이지 제목 사용자 지정'
|
||||
},
|
||||
device: {
|
||||
title: '장치',
|
||||
@@ -197,17 +246,50 @@ const ko = {
|
||||
description: 'SSH 원격 접속 활성화',
|
||||
tip: '활성화하기 전에 강력한 비밀번호를 설정하세요. (계정 - 비밀번호 변경)'
|
||||
},
|
||||
tls: {
|
||||
description: 'HTTPS 프로토콜 활성화',
|
||||
tip: '주의: HTTPS 사용 시 특히 MJPEG 비디오 모드에서 지연 시간이 증가할 수 있습니다.'
|
||||
},
|
||||
advanced: '고급 설정',
|
||||
swap: {
|
||||
title: '스왑',
|
||||
disable: '비활성화',
|
||||
description: '스왑 파일 크기 설정',
|
||||
tip: '이 기능을 활성화하면 SD 카드의 수명이 단축될 수 있습니다!'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: '마우스 흔들기',
|
||||
description: '원격 호스트가 절전 모드로 진입하는 것을 방지',
|
||||
disable: '비활성화',
|
||||
absolute: '절대값 모드',
|
||||
relative: '상대값 모드'
|
||||
},
|
||||
mdns: {
|
||||
description: 'mDNS 검색 서비스 활성화',
|
||||
tip: '사용하지 않는 경우 끄는 것이 좋습니다'
|
||||
},
|
||||
hdmi: {
|
||||
description: 'HDMI/모니터 출력 활성화'
|
||||
},
|
||||
hidOnly: 'HID 전용 모드',
|
||||
disk: '가상 디스크',
|
||||
diskDesc: '원격 호스트에서 가상 U-디스크를 마운트합니다.',
|
||||
diskDesc: '원격 호스트에서 가상 USB를 마운트합니다.',
|
||||
network: '가상 네트워크',
|
||||
networkDesc: '원격 호스트에서 가상 네트워크 카드를 마운트합니다.'
|
||||
networkDesc: '원격 호스트에서 가상 네트워크 카드를 마운트합니다.',
|
||||
reboot: '재부팅',
|
||||
rebootDesc: 'NanoKVM을 재부팅하시겠습니까?',
|
||||
okBtn: '네',
|
||||
cancelBtn: '아니오'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
memory: {
|
||||
title: '메모리 최적화',
|
||||
tip: '메모리 사용량이 제한을 초과하면 가비지 컬렉션이 더 적극적으로 실행되어 메모리를 확보하려고 시도합니다. Tailscale을 사용할 경우 50MB로 설정하는 것이 좋습니다. 변경 사항을 적용하려면 Tailscale을 다시 시작해야 합니다.',
|
||||
disable: '비활성화'
|
||||
tip: '메모리 사용량이 제한을 초과하면 가비지 컬렉션이 더 적극적으로 실행되어 메모리를 확보하려고 시도합니다. Tailscale을 사용할 경우 50MB로 설정하는 것이 좋습니다. 변경 사항을 적용하려면 Tailscale을 다시 시작해야 합니다.'
|
||||
},
|
||||
swap: {
|
||||
title: '스왑 메모리',
|
||||
tip: '메모리 최적화를 활성화한 후에도 문제가 지속되면 스왑 메모리를 활성화해 보세요. 이 설정은 스왑 파일 크기를 기본값으로 256MB로 설정하며, "설정 > 기기"에서 조정할 수 있습니다.'
|
||||
},
|
||||
restart: '정말로 Tailscale을 다시 시작하시겠습니까?',
|
||||
stop: '정말로 Tailscale을 중지하시겠습니까?',
|
||||
@@ -221,9 +303,11 @@ const ko = {
|
||||
download: '다운로드 중 :',
|
||||
package: '패키지 설치',
|
||||
unzip: '압축 해제',
|
||||
upTailscale: 'tailscale을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/bin/',
|
||||
upTailscaled: 'tailscaled을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/sbin/',
|
||||
upTailscale: 'tailscale을 NanoKVM의 /usr/bin/ 경로에 업로드 했습니다.',
|
||||
upTailscaled: 'tailscaled을 NanoKVM의 /usr/sbin/ 경로에 업로드 했습니다.',
|
||||
refresh: '현재 페이지 새로고침',
|
||||
notRunning: 'Tailscale이 실행되고 있지 않습니다. 계속하려면 시작해 주세요.',
|
||||
run: '시작',
|
||||
notLogin:
|
||||
'이 기기는 현재 연동 되지 않았습니다. 로그인해서 계정에 이 장치를 연동하세요.',
|
||||
urlPeriod: '이 주소는 10분간 유효합니다.',
|
||||
@@ -234,8 +318,10 @@ const ko = {
|
||||
deviceIP: '장치 IP',
|
||||
account: '계정',
|
||||
logout: '로그아웃',
|
||||
logout2: '정말로 로그아웃 합니까?',
|
||||
logoutDesc: '정말로 로그아웃 하시겠습니까?',
|
||||
logout2: '정말로 로그아웃 할까요?',
|
||||
uninstall: 'Tailscale 제거',
|
||||
uninstallDesc: '정말로 Tailscale을 제거할까요?',
|
||||
okBtn: '네',
|
||||
cancelBtn: '아니오'
|
||||
},
|
||||
@@ -247,19 +333,32 @@ const ko = {
|
||||
available: '업데이트가 가능합니다. 정말로 업데이트 할까요?',
|
||||
updating: '업데이트 시작. 잠시 기다려주세요...',
|
||||
confirm: '확인',
|
||||
cancel: '취소'
|
||||
cancel: '취소',
|
||||
preview: '미리보기 업데이트',
|
||||
previewDesc: '새로운 기능과 개선 사항에 미리 접근하세요',
|
||||
previewTip: '미리보기 버전에는 버그나 완성되지 않은 기능이 포함될 수 있으니 주의하세요!'
|
||||
},
|
||||
account: {
|
||||
title: '계정',
|
||||
webAccount: 'Web 계정',
|
||||
webAccount: '웹 계정',
|
||||
password: '비밀번호',
|
||||
updateBtn: '업데이트',
|
||||
logoutBtn: '로그아웃'
|
||||
logoutBtn: '로그아웃',
|
||||
logoutDesc: '정말로 로그아웃 하시겠습니까?',
|
||||
okBtn: '네',
|
||||
cancelBtn: '아니오'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: '문제가 발생했습니다.',
|
||||
refresh: '새로고침'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: '전체 화면 전환'
|
||||
},
|
||||
menu: {
|
||||
collapse: '메뉴 접기',
|
||||
expand: '메뉴 펼치기'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const nl = {
|
||||
login: 'Inloggen',
|
||||
changePassword: 'Wachtwoord wijzigen',
|
||||
terminal: 'Terminal',
|
||||
wifi: 'Wi-Fi'
|
||||
wifi: 'Wifi'
|
||||
},
|
||||
auth: {
|
||||
login: 'Inloggen',
|
||||
@@ -29,24 +29,23 @@ const nl = {
|
||||
cancel: 'Annuleren',
|
||||
loginButtonText: 'Inloggen',
|
||||
tips: {
|
||||
reset1:
|
||||
'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.',
|
||||
reset2: 'For detailed steps, please consult this document:',
|
||||
reset3: 'Web default account:',
|
||||
reset4: 'SSH default account:',
|
||||
change1: 'Please note that this action will change the following passwords:',
|
||||
change2: 'Web login password',
|
||||
change3: 'System root password (SSH login password)',
|
||||
change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.'
|
||||
reset1: 'Om de wachtwoorden opnieuw in te stellen, houdt u de BOOT-knop op de NanoKVM 10 seconden lang ingedrukt.',
|
||||
reset2: 'Voor gedetailleerde stappen kunt u dit document raadplegen:',
|
||||
reset3: 'Standaard webaccount:',
|
||||
reset4: 'Standaard SSH-account:',
|
||||
change1: 'Houd er rekening mee dat deze actie de volgende wachtwoorden zal wijzigen:',
|
||||
change2: 'Web login wachtwoord',
|
||||
change3: 'Systeem root-wachtwoord (SSH-inlogwachtwoord)',
|
||||
change4: 'Om de wachtwoorden opnieuw in te stellen, houdt u de BOOT-knop op de NanoKVM ingedrukt.'
|
||||
}
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi for NanoKVM',
|
||||
success: 'Please check the network status of NanoKVM and visit the new IP address.',
|
||||
failed: 'Operation failed, please try again.',
|
||||
title: 'Wifi',
|
||||
description: 'Wifi configureren voor NanoKVM',
|
||||
success: 'Controleer de netwerkstatus van NanoKVM en bezoek het nieuwe IP-adres.',
|
||||
failed: 'De bewerking is mislukt. Probeer het opnieuw.',
|
||||
confirmBtn: 'Ok',
|
||||
finishBtn: 'Finished'
|
||||
finishBtn: 'Gereed'
|
||||
},
|
||||
screen: {
|
||||
video: 'Videomodus',
|
||||
@@ -162,18 +161,18 @@ const nl = {
|
||||
community: 'Community'
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar'
|
||||
title: 'Uiterlijk',
|
||||
display: 'Beeldscherm',
|
||||
language: 'Taal',
|
||||
menuBar: 'Menubalk',
|
||||
menuBarDesc: 'Toon iconen in de menubalk'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
title: 'Apparaat',
|
||||
oled: {
|
||||
title: 'OLED',
|
||||
description: 'OLED screen automatically sleep',
|
||||
0: 'Never',
|
||||
description: 'OLED scherm automatisch slapen',
|
||||
0: 'Nooit',
|
||||
15: '15 sec',
|
||||
30: '30 sec',
|
||||
60: '1 min',
|
||||
@@ -181,32 +180,32 @@ const nl = {
|
||||
300: '5 min',
|
||||
600: '10 min',
|
||||
1800: '30 min',
|
||||
3600: '1 hour'
|
||||
3600: '1 uur'
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi',
|
||||
title: 'Wifi',
|
||||
description: 'Configureer wifi',
|
||||
setBtn: 'Config'
|
||||
},
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
networkDesc: 'Mount virtual network card on the remote host'
|
||||
disk: 'Virtuele schijf',
|
||||
diskDesc: 'Koppel virtuele U-schijf aan de externe host',
|
||||
network: 'Virtueel Netwerk',
|
||||
networkDesc: 'Koppel virtueel netwerk kaart aan de externe host'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
memory: {
|
||||
title: 'Memory optimization',
|
||||
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
|
||||
disable: 'Disable'
|
||||
title: 'Geheugen optimalisatie',
|
||||
tip: "Wanneer geheugen gebruik de limiet overschreid, garbage collection wordt agressiever uitgevoerd om geheugen vrij te maken. geadviseerd om 50MB te kiezen als Tailscale wordt gebruikt. Tailscale moet worden herstart om de wijziging door te voeren.",
|
||||
disable: 'Uitschakelen'
|
||||
},
|
||||
restart: 'Are you sure to restart Tailscale?',
|
||||
stop: 'Are you sure to stop Tailscale?',
|
||||
stopDesc: 'Log out Tailscale and disable its automatic startup on boot.',
|
||||
restart: 'Weet u zeker dat u Tailscale opnieuw wilt opstarten?',
|
||||
stop: 'Weet u zeker dat u Tailscale wilt stoppen?',
|
||||
stopDesc: 'Meld Tailscale af en schakel het automatisch opstarten bij het opstarten uit.',
|
||||
loading: 'Laden...',
|
||||
notInstall: 'Tailscale niet gevonden! Installeer a.u.b.',
|
||||
install: 'Installeren',
|
||||
installing: 'Installeren',
|
||||
installing: 'Installeren bezig',
|
||||
failed: 'Installatie mislukt',
|
||||
retry: 'Vernieuw en probeer opnieuw. Of probeer handmatig te installeren',
|
||||
download: 'Download het',
|
||||
@@ -226,8 +225,8 @@ const nl = {
|
||||
account: 'Account',
|
||||
logout: 'Uitloggen',
|
||||
logout2: 'Weet u zeker dat u wilt uitloggen?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
okBtn: 'Ja',
|
||||
cancelBtn: 'Nee'
|
||||
},
|
||||
update: {
|
||||
title: 'Controleren op updates',
|
||||
@@ -241,10 +240,10 @@ const nl = {
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
webAccount: 'Web Account Name',
|
||||
password: 'Password',
|
||||
webAccount: 'Web Account Naam',
|
||||
password: 'Wachtwoord',
|
||||
updateBtn: 'Update',
|
||||
logoutBtn: 'Logout'
|
||||
logoutBtn: 'Afmelden'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user