mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-10 01:49:00 -06:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6aafb78680 | ||
|
|
c83dc5565c | ||
|
|
59a03cacef | ||
|
|
c6af287b11 | ||
|
|
bc636bd9fd | ||
|
|
1f2b6c5d44 | ||
|
|
b42ffe8e0b | ||
|
|
f9244b36df | ||
|
|
87f697622c | ||
|
|
52dbc0fe40 | ||
|
|
4a69bdcd12 | ||
|
|
e9e47d797c | ||
|
|
66cdb5e376 | ||
|
|
c7575393c9 | ||
|
|
3b9005e9a8 | ||
|
|
e3fd5876d2 | ||
|
|
f71c2d3f8d | ||
|
|
7163854185 | ||
|
|
8037081e0c | ||
|
|
642a00051e | ||
|
|
7f9bd1c457 | ||
|
|
477f7dfa03 | ||
|
|
d45aaf6cb2 | ||
|
|
4650da8373 | ||
|
|
13128a8a90 | ||
|
|
23b19015f4 | ||
|
|
843a1f97ce | ||
|
|
43d6002542 | ||
|
|
f9d9677bc0 | ||
|
|
49220d8024 | ||
|
|
013d76b006 | ||
|
|
c7d3755149 | ||
|
|
8d67ecd6a0 | ||
|
|
6005bb1b8d | ||
|
|
c4e8d19649 | ||
|
|
82be18c469 | ||
|
|
e6565fbe4b | ||
|
|
087dbaded9 | ||
|
|
6c42c57e59 | ||
|
|
3286cc2f85 | ||
|
|
d098a5ef09 | ||
|
|
040335ace0 | ||
|
|
dbc74834cd | ||
|
|
f1b411e3b6 | ||
|
|
7a9f5f9e6e | ||
|
|
cffa9a97f3 | ||
|
|
1bf986d41b | ||
|
|
eb343b0d31 | ||
|
|
628cb3b8c9 | ||
|
|
ebd391ec26 | ||
|
|
a3b577e563 | ||
|
|
f60adcc797 | ||
|
|
ee77f7bed1 | ||
|
|
6ef83cb22f | ||
|
|
1a3fd15fdf | ||
|
|
3e5a46570c | ||
|
|
2c4fbf7179 | ||
|
|
5409fb783c | ||
|
|
4cec610aba | ||
|
|
57d88d7889 | ||
|
|
7f305ae3cf | ||
|
|
eb4ac33fcd | ||
|
|
7a3c6c1690 | ||
|
|
ca63609545 | ||
|
|
bcaa8896ee | ||
|
|
13b19e3889 | ||
|
|
99d1fc0a64 | ||
|
|
86b7d676dd | ||
|
|
1adf6edffc | ||
|
|
a16af99648 | ||
|
|
9fdcb428c0 | ||
|
|
cee9bbb079 | ||
|
|
fcc09e2ef0 | ||
|
|
5d96a8060f | ||
|
|
023fb4b68b |
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,3 +1,89 @@
|
||||
## 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
|
||||
* feat: add support for swap memory
|
||||
* feat: add support for customizing the device hostname
|
||||
* feat: add support for customizing the web page title
|
||||
* 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
|
||||
* perf: improve H.264 streaming reliability by adding a heartbeat mechanism
|
||||
* perf: set the minimum screen size to 640x480
|
||||
* perf: add display of both wired and wireless IPv4 addresses in the settings page
|
||||
* perf: update the Thai language translations (thanks to [ChokunPlayZ](https://github.com/ChokunPlayZ))
|
||||
* chore: bump axios to 1.8.4
|
||||
* chore: bump golang.org/x/net to v0.39.0
|
||||
* chore: bump github.com/golang-jwt/jwt/v5 to to v5.2.2
|
||||
|
||||
## 2.2.5 [3286cc2](https://github.com/sipeed/NanoKVM/commit/3286cc2f85a14133d65935cb476c833dcf151459) (2025-03-26)
|
||||
|
||||
* fix: server crash caused by MJPEG frame detection error
|
||||
* feat: add HID-Only mode
|
||||
* feat: support preview updates
|
||||
* perf: improve image reading performance by optimizing screen parameters
|
||||
|
||||
## 2.2.4 [1bf986d](https://github.com/sipeed/NanoKVM/commit/1bf986d41b34d568c1ffee5df90ce61b6b08456b) (2025-03-21)
|
||||
|
||||
* fix: resolve USB initialization issue
|
||||
* fix: correct abnormal updates in certain models
|
||||
* perf: add version restrictions for production testing
|
||||
|
||||
## 2.2.3 [6ef83cb](https://github.com/sipeed/NanoKVM/commit/6ef83cb22fcd77f721d32c97c85d12f2bfc3035a) (2025-03-21)
|
||||
|
||||
* feat: add support for setting H.264 GOP
|
||||
* fix: resolve deadlock caused by HDMI resolution errors
|
||||
* perf: merge H.264 SPS and PPS into I-frame
|
||||
* perf: refactor MJPEG frame detection
|
||||
* perf: added more configuration options to the serial port terminal (thanks to [@mekaris](https://github.com/mekaris))
|
||||
* perf: improve the `update-nanokvm.py` script (thanks to [@reishoku](https://github.com/reishoku))
|
||||
* perf: disable mDNS by default in new products
|
||||
* perf: update log timestamp to millisecond precision
|
||||
* chore: bump Go to 1.23
|
||||
* chore: bump `golang.org/x/net` to v0.37.0
|
||||
|
||||
## 2.2.2 [58d5ab2](https://github.com/sipeed/NanoKVM/commit/58d5ab2d37244b1e1a68b925a5c23c324c489ad3) (2025-03-11)
|
||||
|
||||
* feat: add watchdog for NanoKVM-Server
|
||||
* feat: add support for UE chip
|
||||
* feat: support system reboot
|
||||
* feat: support enable/disable mDNS
|
||||
* fix: resolve UE chip cannot start server
|
||||
* perf: refactor automatic resolution detection
|
||||
* perf: add output prompt for unsupported resolutions
|
||||
* perf: add lock to kvmv_read_img
|
||||
* perf: configurable VENC automatic recycling feature
|
||||
* perf: add maximum limit for vi
|
||||
* perf: add tooltips to menu bar (thanks to [@S33G](https://github.com/S33G))
|
||||
* perf: menu bar is now draggable (thanks to [@forumi0721](https://github.com/forumi0721))
|
||||
* perf: image list support auto-refresh (thanks to [@forumi0721](https://github.com/forumi0721))
|
||||
* perf: update translations
|
||||
|
||||
## 2.2.1 [b5e48a0](https://github.com/sipeed/NanoKVM/commit/b5e48a07e82df3aedd60442342ae50b95684a697) (2025-02-21)
|
||||
|
||||
* fix: mounted image were not being detected correctly
|
||||
@@ -30,9 +116,9 @@ NanoKVM [Image v1.4.0](https://github.com/sipeed/NanoKVM/releases/tag/v1.4.0) ha
|
||||
* perf: download Tailscale installation package from the official source
|
||||
* perf: automatic enable/disable GOMEMLIMIT on tailscale start/stop
|
||||
* perf: add JWT configuration
|
||||
* secretKey: customize secret key. If empty, generated a random 64-byte secret key by default
|
||||
* refreshTokenDuration: customize token expiration time
|
||||
* revokeTokensOnLogout: invalidate all JWT tokens on logout
|
||||
* secretKey: customize secret key. If empty, generated a random 64-byte secret key by default
|
||||
* refreshTokenDuration: customize token expiration time
|
||||
* revokeTokensOnLogout: invalidate all JWT tokens on logout
|
||||
* perf: implement secure password storage using bcrypt hashing
|
||||
* perf: implement integrity checks for online updates
|
||||
* refactor: refactor HDMI module and remove the dependency `libmaixcam_lib.so`
|
||||
|
||||
156
README.md
156
README.md
@@ -18,10 +18,30 @@ English | [中文](./README_ZH.md) | [日本語](./README_JA.md)
|
||||
> 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).
|
||||
|
||||
## Opensource & Contribution
|
||||
## Introduction
|
||||
|
||||
### 2025.03.04 Update Content
|
||||
+ Updated directory structure
|
||||
Lichee NanoKVM is an IP-KVM product based on LicheeRV Nano, inheriting the extreme size and powerful features of LicheeRV Nano.
|
||||
|
||||
The Lichee NanoKVM is now available in three versions:
|
||||
|
||||
* NanoKVM-Cube Lite is a basic configuration that is suitable for individual users with certain DIY capabilities and enterprise users with bulk requirements.
|
||||
|
||||
* 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.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</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!
|
||||
|
||||
## Project Structure
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP update package
|
||||
@@ -30,69 +50,37 @@ English | [中文](./README_ZH.md) | [日本語](./README_JA.md)
|
||||
│ ├── 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
|
||||
├── web # NanoKVM front-end
|
||||
├── LICENSE
|
||||
├── README_JA.md
|
||||
├── README.md
|
||||
├── README_ZH.md
|
||||
└── CHANGELOG.md
|
||||
├── ...
|
||||
```
|
||||
|
||||
+ Add NanoKVM app installation package `kvmapp`
|
||||
+ Add compilation scripts and compilation instructions
|
||||
|
||||
### Update 2025.02.19
|
||||
All components of NanoKVM were open-sourced, including the [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), the [kvmapp update package](https://github.com/sipeed/NanoKVM-System/tree/main/kvmapp), [system sdk](https://github.com/sipeed/LicheeRV-Nano-Build/tree/NanoKVM), and [packaging methods](https://github.com/sipeed/LicheeRV-Nano-Build/blob/NanoKVM/kvm/NanoKVM_img.sh).
|
||||
|
||||
### Update 2025.02.14
|
||||
Newest APP v2.1.6 add most security enhancements&bug fix, recommend regular users upgrade to this version or newer.
|
||||
|
||||
### Update 2025.02.05
|
||||
For users concerned about security, you can read this issue: 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!
|
||||
|
||||
### Update 2024.10.18
|
||||
|
||||
We have open-sourced the backend code in 10.8, and thank you for civil PR it immediately, we will give you newest NanoKVM-PCIe as a reward!
|
||||
The mid-Oct batch is starting testing and packaging, most orders before 10.1 will ship out next and next next week.
|
||||
The aliexpress shipping date is change to Dec since 10.1, it is a conservative delivery time. the order in Oct. should send out during 11.15~12.15.
|
||||
|
||||
## Introduction
|
||||
|
||||
Lichee NanoKVM is an IP-KVM product based on LicheeRV Nano, inheriting the extreme size and powerful features of LicheeRV Nano.
|
||||
The Lichee NanoKVM is available in two versions:
|
||||
NanoKVM Lite is a basic configuration that is suitable for individual users with certain DIY capabilities and enterprise users with bulk requirements.
|
||||
The NanoKVM 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.
|
||||
|
||||

|
||||
|
||||
## Tech Specs
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||

|
||||
|
||||
@@ -106,38 +94,50 @@ The NanoKVM image is built on LicheeRV Nano SDK and MaixCDK, and is compatible w
|
||||
|
||||
Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the multimedia subsystem, which NanoKVM will use for video image acquisition and processing.
|
||||
|
||||
+ [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_HDMI_MIPI_31011.pdf)
|
||||
+ [NanoKVM img](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
* [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 img](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Q1 2025
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- [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
|
||||
* [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
|
||||
|
||||
**Distribution:**
|
||||
|
||||
- [x] Distribute applications via GitHub
|
||||
- [x] Implement integrity checks for online updates
|
||||
- [ ] Support offline updates
|
||||
* [x] Distribute applications via GitHub
|
||||
* [x] Implement integrity checks for online updates
|
||||
* [ ] Support offline updates
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
- [x] Fix the DNS issue
|
||||
- [x] Fix the CSRF vulnerability
|
||||
* [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
|
||||
* [ ] [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
|
||||
|
||||
@@ -149,3 +149,7 @@ Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the mu
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
|
||||
|
||||
57
README_ZH.md
57
README_ZH.md
@@ -18,10 +18,27 @@ NanoKVM
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> 如果您在使用过程中有任何问题或建议,请在这里提交 issue, 或在 [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)告诉我们.
|
||||
|
||||
## 开源与贡献
|
||||
## 简介
|
||||
|
||||
### 2025.03.04 更新内容
|
||||
+ 更新目录结构
|
||||
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更新包
|
||||
@@ -42,33 +59,6 @@ NanoKVM
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
+ 添加 NanoKVM app安装包`kvmapp`
|
||||
+ 添加编译脚本和编译说明
|
||||
|
||||
### 2025.02.19 更新内容
|
||||
所有 NanoKVM 组件已开源,包括
|
||||
[前端](https://github.com/sipeed/NanoKVM/tree/main/web)、[后端](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 更新包](https://github.com/sipeed/NanoKVM-System/tree/main/kvmapp)、[系统 SDK](https://github.com/sipeed/LicheeRV-Nano-Build/tree/NanoKVM) 和 [固件打包方法](https://github.com/sipeed/LicheeRV-Nano-Build/blob/NanoKVM/kvm/NanoKVM_img.sh)。
|
||||
|
||||
### 2025.02.14 更新内容
|
||||
最新的 APP v2.1.6 添加了大多数安全增强和 bug 修复,建议普通用户升级到此版本或更新版本。
|
||||
|
||||
### 2025.02.05 更新内容
|
||||
对于关注安全的用户,可以阅读此问题:https://github.com/sipeed/NanoKVM/issues/301,您会找到有关所有安全问题的解释。GitHub 作为一个开放和透明的平台,为讨论所谓的“后门”担忧提供了空间。开源使产品更加安全!
|
||||
|
||||
### 2024.10.18 更新内容
|
||||
我们在 10.8 中开源了后端代码,感谢您立即提交的文明 PR,我们将赠送您最新的 NanoKVM-PCIe 作为奖励!
|
||||
十月中旬的批次正在开始测试和打包,大多数在 10.1 之前的订单将在下周和下下周发货。
|
||||
自 10.1 起,AliExpress 的发货日期已更改为 12 月,这是一个保守的交货时间。十月的订单应在 11.15~12.15 期间发出。
|
||||
|
||||
## 简介
|
||||
|
||||
Lichee NanoKVM 是基于 LicheeRV Nano 的 IP-KVM 产品,继承了 LicheeRV Nano的极致体积 和 强大功能。
|
||||
Lichee NanoKVM分为两个版本:
|
||||
NanoKVM Lite 为基础版配置,适合 具有一定DIY能力的个人用户 和 有批量需求的企业用户。
|
||||
NanoKVM Full 为完整版配置,带精致外壳和完整配件,内置开机即用的系统镜像卡,推荐个人用户购买。
|
||||
|
||||

|
||||
|
||||
## 参数
|
||||
|
||||
| 产品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
@@ -105,10 +95,9 @@ NanoKVM 镜像在LicheeRV Nano SDK 和 MaixCDK 基础上构建,可以兼容使
|
||||
|
||||
注: 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)
|
||||
|
||||
* [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)
|
||||
|
||||
## 购买渠道
|
||||
|
||||
|
||||
32
kvmapp/system/init.d/S00kmod
Executable file
32
kvmapp/system/init.d/S00kmod
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$1" = "start" ]
|
||||
then
|
||||
. /etc/profile
|
||||
printf "load kernel module: "
|
||||
cd /mnt/system/ko/
|
||||
insmod soph_sys.ko
|
||||
insmod soph_base.ko
|
||||
insmod soph_rtos_cmdqu.ko
|
||||
insmod soph_fast_image.ko
|
||||
insmod soph_mipi_rx.ko
|
||||
insmod soph_snsr_i2c.ko
|
||||
insmod soph_vi.ko
|
||||
insmod soph_vpss.ko
|
||||
insmod soph_dwa.ko
|
||||
insmod soph_vo.ko
|
||||
insmod soph_rgn.ko
|
||||
insmod soph_wdt.ko
|
||||
insmod soph_clock_cooling.ko
|
||||
insmod soph_tpu.ko
|
||||
insmod soph_vcodec.ko
|
||||
insmod soph_jpeg.ko
|
||||
insmod soph_vc_driver.ko MaxVencChnNum=9 MaxVdecChnNum=9
|
||||
insmod soph_rtc.ko
|
||||
insmod soph_ive.ko
|
||||
insmod soph_mon.ko
|
||||
insmod soph_pwm.ko
|
||||
insmod soph_wiegand.ko
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# kvmhwd Rev2.0
|
||||
# kvmhwd Rev2.1
|
||||
|
||||
start_usb_dev(){
|
||||
. /etc/profile
|
||||
@@ -47,7 +47,7 @@ start_usb_dev(){
|
||||
echo 1 > functions/hid.GS0/wakeup_on_write
|
||||
fi
|
||||
echo 1 > functions/hid.GS0/protocol
|
||||
echo 6 > functions/hid.GS0/report_length
|
||||
echo 8 > functions/hid.GS0/report_length
|
||||
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.GS0/report_desc
|
||||
ln -s functions/hid.GS0 configs/c.1
|
||||
|
||||
@@ -62,7 +62,7 @@ start_usb_dev(){
|
||||
echo 1 > functions/hid.GS1/wakeup_on_write
|
||||
fi
|
||||
echo 2 > functions/hid.GS1/protocol
|
||||
echo -ne \\x34 > functions/hid.GS1/report_length
|
||||
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
|
||||
ln -s functions/hid.GS1 configs/c.1
|
||||
|
||||
|
||||
93
kvmapp/system/init.d/S03usbhid
Executable file
93
kvmapp/system/init.d/S03usbhid
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/sh
|
||||
# kvmhwd hid only
|
||||
|
||||
start_usb_dev(){
|
||||
. /etc/profile
|
||||
echo "usb mode: device"
|
||||
cd /sys/kernel/config/usb_gadget
|
||||
|
||||
mkdir g0
|
||||
cd g0
|
||||
|
||||
echo 0x0101 > bcdUSB
|
||||
echo 0x0623 > bcdDevice
|
||||
echo 0x3346 > idVendor
|
||||
echo 0x1009 > idProduct
|
||||
mkdir strings/0x409
|
||||
# echo '0' > strings/0x409/serialnumber
|
||||
echo 'sipeed' > strings/0x409/manufacturer
|
||||
echo 'NanoKVM' > strings/0x409/product
|
||||
|
||||
mkdir configs/c.1
|
||||
echo 0xA0 > configs/c.1/bmAttributes
|
||||
echo 200 > configs/c.1/MaxPower
|
||||
mkdir configs/c.1/strings/0x409
|
||||
echo "NanoKVM" > configs/c.1/strings/0x409/configuration
|
||||
|
||||
# keyboard
|
||||
mkdir functions/hid.GS0
|
||||
echo 1 > functions/hid.GS0/subclass
|
||||
if [ ! -e /boot/usb.notwakeup ]
|
||||
then
|
||||
echo 1 > functions/hid.GS0/wakeup_on_write
|
||||
fi
|
||||
echo 1 > functions/hid.GS0/protocol
|
||||
echo 8 > functions/hid.GS0/report_length
|
||||
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.GS0/report_desc
|
||||
ln -s functions/hid.GS0 configs/c.1
|
||||
|
||||
# mouse
|
||||
mkdir functions/hid.GS1
|
||||
echo 1 > functions/hid.GS1/subclass
|
||||
if [ ! -e /boot/usb.notwakeup ]
|
||||
then
|
||||
echo 1 > functions/hid.GS1/wakeup_on_write
|
||||
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
|
||||
ln -s functions/hid.GS1 configs/c.1
|
||||
|
||||
# touchpad
|
||||
mkdir functions/hid.GS2
|
||||
echo 1 > functions/hid.GS2/subclass
|
||||
if [ ! -e /boot/usb.notwakeup ]
|
||||
then
|
||||
echo 1 > functions/hid.GS2/wakeup_on_write
|
||||
fi
|
||||
echo 2 > functions/hid.GS2/protocol
|
||||
echo 6 > functions/hid.GS2/report_length
|
||||
echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\x05\\x01\\x09\\x38\\x15\\x81\\x25\\x7f\\x35\\x00\\x45\\x00\\x75\\x08\\x95\\x01\\x81\\x06\\xc0\\xc0 > functions/hid.GS2/report_desc
|
||||
ln -s functions/hid.GS2 configs/c.1
|
||||
|
||||
ls /sys/class/udc/ | cat > UDC
|
||||
echo device > /proc/cviusb/otg_role
|
||||
}
|
||||
|
||||
start_usb_host(){
|
||||
echo '' > /sys/kernel/config/usb_gadget/g0/UDC
|
||||
echo host > /proc/cviusb/otg_role
|
||||
}
|
||||
|
||||
restart_usb_dev(){
|
||||
echo > /sys/kernel/config/usb_gadget/g0/UDC
|
||||
sleep 1
|
||||
ls /sys/class/udc/ | cat > /sys/kernel/config/usb_gadget/g0/UDC
|
||||
echo "USB Restart OK!"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_usb_dev
|
||||
;;
|
||||
restart)
|
||||
restart_usb_dev
|
||||
;;
|
||||
stop)
|
||||
start_usb_host
|
||||
;;
|
||||
stop_start)
|
||||
start_usb_host
|
||||
start_usb_dev
|
||||
;;
|
||||
esac
|
||||
@@ -29,10 +29,14 @@ start() {
|
||||
arping -Dqc2 -Ieth0 $addr || continue
|
||||
ip a add $inet brd + dev eth0
|
||||
ip r add default via $gw dev eth0
|
||||
cat > /etc/resolve.conf << EOF
|
||||
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 &&
|
||||
@@ -112,4 +116,4 @@ EOF
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
esac
|
||||
esac
|
||||
|
||||
@@ -3,67 +3,71 @@
|
||||
# sshd Starts sshd.
|
||||
#
|
||||
|
||||
# Make sure the ssh-keygen progam exists
|
||||
[ -f /usr/bin/ssh-keygen ] || exit 0
|
||||
# Ensure required binaries exist
|
||||
[ -x /usr/bin/ssh-keygen ] || exit 0
|
||||
[ -x /usr/sbin/sshd ] || exit 1
|
||||
|
||||
PIDFILE="/var/run/sshd.pid"
|
||||
|
||||
umask 077
|
||||
|
||||
startssh() {
|
||||
/usr/bin/ssh-keygen -A
|
||||
# Generate SSH keys if they do not exist
|
||||
[ ! -f /etc/ssh/ssh_host_rsa_key ] && /usr/bin/ssh-keygen -A
|
||||
|
||||
printf "Starting sshd: "
|
||||
/usr/sbin/sshd
|
||||
touch /var/lock/sshd
|
||||
echo "OK"
|
||||
printf "Starting sshd: "
|
||||
/usr/sbin/sshd
|
||||
touch /var/lock/sshd
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
start() {
|
||||
# Create any missing keys
|
||||
if [ -e /etc/kvm/ssh_stop ]
|
||||
then
|
||||
if [ -e /boot/start_ssh_once ]
|
||||
then
|
||||
rm /boot/start_ssh_once
|
||||
startssh
|
||||
else
|
||||
echo "SSH does not start"
|
||||
fi
|
||||
else
|
||||
startssh
|
||||
fi
|
||||
if [ -e /etc/kvm/ssh_stop ]; then
|
||||
if [ -e /boot/start_ssh_once ]; then
|
||||
rm -f /boot/start_ssh_once
|
||||
startssh
|
||||
else
|
||||
echo "SSH does not start"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
startssh
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
printf "Stopping sshd: "
|
||||
killall sshd
|
||||
rm -f /var/lock/sshd
|
||||
echo "OK"
|
||||
printf "Stopping sshd: "
|
||||
killall sshd 2>/dev/null
|
||||
rm -f /var/lock/sshd
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart|reload)
|
||||
restart
|
||||
;;
|
||||
permanent_on)
|
||||
rm /etc/kvm/ssh_stop
|
||||
start
|
||||
;;
|
||||
permanent_off)
|
||||
touch /etc/kvm/ssh_stop
|
||||
stop
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|permanent_on|permanent_off}"
|
||||
exit 1
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart|reload)
|
||||
restart
|
||||
;;
|
||||
permanent_on)
|
||||
rm -f /etc/kvm/ssh_stop
|
||||
start
|
||||
;;
|
||||
permanent_off)
|
||||
touch /etc/kvm/ssh_stop
|
||||
stop
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|permanent_on|permanent_off}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit $?
|
||||
|
||||
exit 0
|
||||
@@ -1,42 +1,57 @@
|
||||
#!/bin/sh
|
||||
# nanokvm Rev3.0
|
||||
# nanokvm Rev3.1
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo -n kvm > /boot/hostname.prefix
|
||||
cp /mnt/data/sensor_cfg.ini.LT /mnt/data/sensor_cfg.ini
|
||||
|
||||
str_value=$(cat /sys/class/cvi-base/base_uid | awk '{print $2}')
|
||||
first_uint=$(echo $str_value | cut -d'_' -f1)
|
||||
second_uint=$(echo $str_value | cut -d'_' -f2)
|
||||
result="$first_uint$second_uint"
|
||||
echo $result > /device_key
|
||||
# Copy sensor config if the file exists
|
||||
[ -f /mnt/data/sensor_cfg.ini.LT ] && cp /mnt/data/sensor_cfg.ini.LT /mnt/data/sensor_cfg.ini
|
||||
|
||||
# Generate unique device key
|
||||
if [ -f /sys/class/cvi-base/base_uid ]; then
|
||||
str_value=$(awk '{print $2}' /sys/class/cvi-base/base_uid)
|
||||
first_uint=$(echo "$str_value" | cut -d'_' -f1)
|
||||
second_uint=$(echo "$str_value" | cut -d'_' -f2)
|
||||
echo "$first_uint$second_uint" > /device_key
|
||||
fi
|
||||
|
||||
# Set iptables rules (skip if already present)
|
||||
iptables -C INPUT -i eth0 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
|
||||
iptables -C OUTPUT -o eth0 -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -A OUTPUT -o eth0 -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -o eth0 -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
|
||||
|
||||
iptables -C INPUT -i eth0 -p tcp --sport 22 -m state --state NEW,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -A INPUT -i eth0 -p tcp --sport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
|
||||
iptables -C OUTPUT -o eth0 -p tcp --dport 22 -m state --state ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -A OUTPUT -o eth0 -p tcp --dport 22 -m state --state ESTABLISHED -j ACCEPT
|
||||
|
||||
iptables -C OUTPUT -o eth0 -p tcp --sport 8000 -m state --state ESTABLISHED -j DROP 2>/dev/null || \
|
||||
iptables -A OUTPUT -o eth0 -p tcp --sport 8000 -m state --state ESTABLISHED -j DROP
|
||||
|
||||
# Start services
|
||||
cp -r /kvmapp/kvm_system /tmp/
|
||||
/tmp/kvm_system/kvm_system &
|
||||
|
||||
|
||||
cp -r /kvmapp/server /tmp/
|
||||
/tmp/server/NanoKVM-Server &
|
||||
;;
|
||||
;;
|
||||
|
||||
stop)
|
||||
killall kvm_system
|
||||
killall NanoKVM-Server
|
||||
rm -r /tmp/kvm_system
|
||||
rm -r /tmp/server
|
||||
rm -r /tmp/kvm_system /tmp/server
|
||||
echo "OK"
|
||||
;;
|
||||
;;
|
||||
|
||||
restart)
|
||||
killall kvm_system
|
||||
killall NanoKVM-Server
|
||||
rm -r /tmp/kvm_system
|
||||
rm -r /tmp/server
|
||||
|
||||
rm -r /tmp/kvm_system /tmp/server
|
||||
|
||||
cp -r /kvmapp/kvm_system /tmp/
|
||||
/tmp/kvm_system/kvm_system &
|
||||
|
||||
@@ -44,7 +59,11 @@ case "$1" in
|
||||
/tmp/server/NanoKVM-Server &
|
||||
|
||||
sync
|
||||
|
||||
echo "OK"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -2,41 +2,38 @@ import os
|
||||
import shutil
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
import pathlib
|
||||
import requests
|
||||
|
||||
temporary = "/root/.kvm-cache/"
|
||||
temporary: str = "/root/.kvm-cache/"
|
||||
|
||||
|
||||
def mkdir():
|
||||
is_exists = os.path.exists(temporary)
|
||||
if is_exists:
|
||||
def mkdir() -> None:
|
||||
if pathlib.Path(temporary).exists():
|
||||
shutil.rmtree(temporary)
|
||||
os.mkdir(temporary)
|
||||
print(f"create temporary directory {temporary}")
|
||||
pathlib.Path(temporary).mkdir()
|
||||
print(f"Created temporary directory {temporary}")
|
||||
|
||||
|
||||
def read(file):
|
||||
with open(file, "r") as f:
|
||||
content = f.read()
|
||||
return content.replace("\n", "")
|
||||
def read(file: str) -> str:
|
||||
return pathlib.Path(file).read_text().replace("\n", "")
|
||||
|
||||
|
||||
def download_firmware():
|
||||
print("download firmware...")
|
||||
def download_firmware() -> None:
|
||||
print("Downloading firmware...")
|
||||
|
||||
now = int(time.time())
|
||||
url = f"https://cdn.sipeed.com/nanokvm/latest.zip?n={now}"
|
||||
print(f"download from {url}")
|
||||
print(f"Downloading firmware from {url}")
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"download firmware failed, status: {response.status_code}")
|
||||
raise Exception(f"Failed to download firmware, status: {response.status_code}")
|
||||
|
||||
content_type = response.headers.get("content-type")
|
||||
if content_type != "application/zip":
|
||||
raise Exception(f"download firmware failed, content_type: {content_type}")
|
||||
raise Exception(f"Failed to download firmware, content_type: {content_type}")
|
||||
|
||||
zip_file = f"{temporary}/latest.zip"
|
||||
with open(zip_file, "wb") as f:
|
||||
@@ -46,49 +43,23 @@ def download_firmware():
|
||||
with zipfile.ZipFile(zip_file, "r") as f:
|
||||
f.extractall(temporary)
|
||||
|
||||
print("download firmware done")
|
||||
print("Completed downloading firmware.")
|
||||
|
||||
|
||||
def download_lib():
|
||||
print("download lib...")
|
||||
def update() -> None:
|
||||
backup_dir = pathlib.Path("/root/old")
|
||||
firmware_dir = pathlib.Path("/kvmapp")
|
||||
|
||||
device_key = read("/device_key")
|
||||
if backup_dir.exists():
|
||||
shutil.rmtree(backup_dir)
|
||||
|
||||
url = f"https://maixvision.sipeed.com/api/v1/nanokvm/encryption?uid={device_key}"
|
||||
headers = {"token": "MaixVision2024"}
|
||||
response = requests.get(url, headers=headers, stream=True)
|
||||
if firmware_dir.exists():
|
||||
firmware_dir.rename(backup_dir)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"download lib failed, status: {response.status_code}")
|
||||
|
||||
content_type = response.headers.get("content-type")
|
||||
if content_type != "application/octet-stream":
|
||||
raise Exception(f"download lib failed, content_type: {content_type}")
|
||||
|
||||
lib_file = f"{temporary}/libmaixcam_lib.so"
|
||||
with open(lib_file, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
lib_dir = f"{temporary}/latest/kvm_system/dl_lib/"
|
||||
shutil.copy(lib_file, lib_dir)
|
||||
|
||||
print("download lib done")
|
||||
pathlib.Path(f"{temporary}/latest").rename(firmware_dir)
|
||||
|
||||
|
||||
def update():
|
||||
backup_dir = "/root/old"
|
||||
firmware_dir = "/kvmapp"
|
||||
|
||||
if os.path.exists(backup_dir):
|
||||
shutil.rmtree(backup_dir)
|
||||
|
||||
if os.path.exists(firmware_dir):
|
||||
shutil.move(firmware_dir, backup_dir)
|
||||
|
||||
shutil.move(f"{temporary}/latest", firmware_dir)
|
||||
|
||||
|
||||
def change_permissions():
|
||||
def change_permissions() -> None:
|
||||
for root, dirs, files in os.walk("/kvmapp"):
|
||||
os.chmod(root, 0o755)
|
||||
|
||||
@@ -97,23 +68,22 @@ def change_permissions():
|
||||
os.chmod(file_path, 0o755)
|
||||
|
||||
print("change permissions done")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
print("stop service...")
|
||||
print("Stopping nanokvm service...")
|
||||
os.system("/etc/init.d/S95nanokvm stop")
|
||||
|
||||
print("start update......")
|
||||
print("Staring update...")
|
||||
|
||||
mkdir()
|
||||
download_firmware()
|
||||
# download_lib()
|
||||
update()
|
||||
change_permissions()
|
||||
|
||||
version = read("/kvmapp/version")
|
||||
print(f"update to {version} success.")
|
||||
print(f"Successfully updated to version: {version}")
|
||||
print("restart service\nthe nanokvm will reboot")
|
||||
except Exception as e:
|
||||
print(f"update failed\n{e}")
|
||||
@@ -123,4 +93,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -13,7 +13,7 @@ server
|
||||
├── dl_lib // Shared object libraries
|
||||
├── include // Header files for shared objects
|
||||
├── logger // Logging system
|
||||
├── middleware // Server middleware components
|
||||
├── middleware // Server middleware components
|
||||
├── proto // API request/response definitions
|
||||
├── router // API route handlers
|
||||
├── service // Core service implementations
|
||||
@@ -39,7 +39,7 @@ cert:
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
|
||||
# Authentication setting (enable/disable)
|
||||
# Note: Only disable authentication in development environment
|
||||
authentication: enable
|
||||
@@ -75,8 +75,9 @@ Note: Use Linux operating system (x86-64). This build process is not compatible
|
||||
2. Compile the Project
|
||||
1. Run `cd server` from the project root directory.
|
||||
2. Run `go mod tidy` to install Go dependencies.
|
||||
3. Run `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` to compile the project.
|
||||
4. After compilation, an executable file named `NanoKVM-Server` will be generated.
|
||||
3. (Optional) If you compiled `libkvm.so` yourself, you need to modify its RPATH by `patchelf --add-rpath \$ORIGIN ./dl_lib/libkvm.so`.
|
||||
4. Run `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` to compile the project.
|
||||
5. After compilation, an executable file named `NanoKVM-Server` will be generated.
|
||||
|
||||
3. Modify RPATH
|
||||
1. Run `sudo apt install patchelf` or `pip install patchelf` to install patchelf.
|
||||
|
||||
@@ -31,13 +31,13 @@ port:
|
||||
cert:
|
||||
crt: server.crt
|
||||
key: server.key
|
||||
|
||||
|
||||
# 日志级别(debug/info/warn/error)
|
||||
# 注意:在生产环境中使用 info 或 error。debug 模式仅在开发环境中使用。
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
|
||||
# 鉴权设置(enable/disable)
|
||||
# 注意:生产环境中请勿使用 disable。
|
||||
authentication: enable
|
||||
@@ -72,8 +72,9 @@ turn:
|
||||
2. 编译
|
||||
1. 在项目根目录下执行 `cd server` 进入 server 目录;
|
||||
2. 执行 `go mod tidy` 安装 Go 依赖包;
|
||||
3. 执行 `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` 进行编译;
|
||||
4. 编译完成后,会生成可执行文件 `NanoKVM-Server`。
|
||||
3. (可选)如果您手动编译了 `libkvm.so`,则需要通过 `patchelf --add-rpath \$ORIGIN ./dl_lib/libkvm.so` 修改其 RPATH 属性。
|
||||
4. 执行 `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` 进行编译;
|
||||
5. 编译完成后,会生成可执行文件 `NanoKVM-Server`。
|
||||
|
||||
3. 修改 RPATH
|
||||
1. 执行 `sudo apt install patchelf` 或 `pip install patchelf` 安装 patchelf;
|
||||
@@ -81,8 +82,8 @@ turn:
|
||||
3. 执行 `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` 修改可执行文件的 RPATH 属性。
|
||||
|
||||
4. 部署
|
||||
1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用;
|
||||
2. 使用编译生成的 `NanoKVM-Server` 文件,替换 NanoKVM 中 `/kvmapp/server/` 目录下的原始文件;
|
||||
1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用;
|
||||
2. 使用编译生成的 `NanoKVM-Server` 文件,替换 NanoKVM 中 `/kvmapp/server/` 目录下的原始文件;
|
||||
3. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。
|
||||
|
||||
## 手动更新
|
||||
@@ -92,4 +93,4 @@ turn:
|
||||
1. 从 [GitHub](https://github.com/sipeed/NanoKVM/releases) 下载最新的应用安装包;
|
||||
2. 解压缩下载的安装包,并将解压后的文件夹重命名为 `kvmapp`;
|
||||
3. 备份 NanoKVM 系统中的 `/kvmapp` 目录,然后用解压后的 `kvmapp` 文件夹替换现有目录。
|
||||
4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。
|
||||
4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。
|
||||
|
||||
@@ -20,9 +20,7 @@ var (
|
||||
kvmVisionOnce sync.Once
|
||||
)
|
||||
|
||||
type KvmVision struct {
|
||||
mutex sync.Mutex
|
||||
}
|
||||
type KvmVision struct{}
|
||||
|
||||
func GetKvmVision() *KvmVision {
|
||||
kvmVisionOnce.Do(func() {
|
||||
@@ -44,9 +42,6 @@ func GetKvmVision() *KvmVision {
|
||||
}
|
||||
|
||||
func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data []byte, result int) {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
|
||||
var (
|
||||
kvmData *C.uint8_t
|
||||
dataSize C.uint32_t
|
||||
@@ -72,10 +67,7 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data
|
||||
return
|
||||
}
|
||||
|
||||
func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, sps []byte, pps []byte, result int) {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
|
||||
func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, result int) {
|
||||
var (
|
||||
kvmData *C.uint8_t
|
||||
dataSize C.uint32_t
|
||||
@@ -97,48 +89,10 @@ func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data
|
||||
|
||||
data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
if result == 3 {
|
||||
sps, _ = k.ReadH264SPS()
|
||||
pps, _ = k.ReadH264PPS()
|
||||
}
|
||||
|
||||
log.Debugf("read kvm image: %v", result)
|
||||
return
|
||||
}
|
||||
|
||||
func (k *KvmVision) ReadH264SPS() ([]byte, int) {
|
||||
var (
|
||||
kvmData *C.uint8_t
|
||||
dataSize C.uint32_t
|
||||
)
|
||||
|
||||
result := int(C.kvmv_get_sps_frame(&kvmData, &dataSize))
|
||||
if result < 0 {
|
||||
log.Errorf("failed to read sps: %v", result)
|
||||
return nil, result
|
||||
}
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
return data, result
|
||||
}
|
||||
|
||||
func (k *KvmVision) ReadH264PPS() ([]byte, int) {
|
||||
var (
|
||||
kvmData *C.uint8_t
|
||||
dataSize C.uint32_t
|
||||
)
|
||||
|
||||
result := int(C.kvmv_get_pps_frame(&kvmData, &dataSize))
|
||||
if result < 0 {
|
||||
log.Errorf("failed to read pps: %v", result)
|
||||
return nil, result
|
||||
}
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
return data, result
|
||||
}
|
||||
|
||||
func (k *KvmVision) SetHDMI(enable bool) int {
|
||||
hdmiEnable := C.uint8_t(0)
|
||||
if enable {
|
||||
@@ -154,10 +108,17 @@ func (k *KvmVision) SetHDMI(enable bool) int {
|
||||
return result
|
||||
}
|
||||
|
||||
func (k *KvmVision) Close() {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
func (k *KvmVision) SetGop(gop uint8) {
|
||||
_gop := C.uint8_t(gop)
|
||||
C.set_h264_gop(_gop)
|
||||
}
|
||||
|
||||
func (k *KvmVision) SetFrameDetect(frame uint8) {
|
||||
_frame := C.uint8_t(frame)
|
||||
C.set_frame_detact(_frame)
|
||||
}
|
||||
|
||||
func (k *KvmVision) Close() {
|
||||
C.kvmv_deinit()
|
||||
log.Debugf("stop kvm vision...")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type Screen struct {
|
||||
FPS int
|
||||
Quality uint16
|
||||
BitRate uint16
|
||||
GOP uint8
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -46,6 +47,7 @@ func GetScreen() *Screen {
|
||||
Quality: 80,
|
||||
FPS: 30,
|
||||
BitRate: 3000,
|
||||
GOP: 30,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,6 +72,24 @@ func SetScreen(key string, value int) {
|
||||
|
||||
case "fps":
|
||||
screen.FPS = validateFPS(value)
|
||||
|
||||
case "gop":
|
||||
screen.GOP = uint8(value)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckScreen() {
|
||||
if _, ok := ResolutionMap[screen.Height]; !ok {
|
||||
screen.Width = 1920
|
||||
screen.Height = 1080
|
||||
}
|
||||
|
||||
if _, ok := QualityMap[screen.Quality]; !ok {
|
||||
screen.Quality = 80
|
||||
}
|
||||
|
||||
if _, ok := BitRateMap[screen.BitRate]; !ok {
|
||||
screen.BitRate = 3000
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
Binary file not shown.
@@ -1,13 +1,15 @@
|
||||
module NanoKVM-Server
|
||||
|
||||
go 1.22.1
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
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/webrtc/v4 v4.0.1
|
||||
@@ -15,7 +17,7 @@ require (
|
||||
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.31.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -71,9 +73,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.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // 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
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ 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=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -160,17 +162,33 @@ 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/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/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=
|
||||
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=
|
||||
|
||||
@@ -50,20 +50,20 @@ void set_venc_auto_recyc(uint8_t _enable);
|
||||
-5: Retrieving image, please wait
|
||||
-4: Modifying image resolution, please wait
|
||||
-3: img buffer full
|
||||
-2: VENC Errorl
|
||||
-2: VENC Error
|
||||
-1: No images were acquired
|
||||
0: Acquire MJPEG encoded images
|
||||
1: Acquire H264 encoded images(SPS)
|
||||
2: Acquire H264 encoded images(PPS)
|
||||
1: Acquire H264 encoded images(SPS)[Deprecated]
|
||||
2: Acquire H264 encoded images(PPS)[Deprecated]
|
||||
3: Acquire H264 encoded images(I)
|
||||
4: Acquire H264 encoded images(P)
|
||||
5: IMG not changed
|
||||
**********************************************************************************/
|
||||
int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int free_kvmv_data(uint8_t ** _pp_kvm_data);
|
||||
void free_all_kvmv_data();
|
||||
void set_h264_gop(uint8_t _gop);
|
||||
void set_frame_detact(uint8_t _frame_detact);
|
||||
void kvmv_deinit();
|
||||
uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||
|
||||
@@ -71,4 +71,4 @@ uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // KVM_VISION_H_
|
||||
#endif // KVM_VISION_H_
|
||||
|
||||
@@ -22,7 +22,7 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
buffer = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
now := entry.Time.Format("2006-01-02 15:04:05")
|
||||
now := entry.Time.Format("2006-01-02 15:04:05.000")
|
||||
|
||||
if entry.HasCaller() {
|
||||
fileName := filepath.Base(entry.Caller.File)
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/logger"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/utils"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
@@ -37,7 +40,8 @@ func initialize() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
vision.SetHDMI(true)
|
||||
|
||||
utils.InitGoMemLimit()
|
||||
// run mouse jiggler
|
||||
jiggler.GetJiggler().Run()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
@@ -56,7 +60,6 @@ func run() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
if conf.Authentication == "disable" {
|
||||
r.Use(cors.AllowAll())
|
||||
}
|
||||
@@ -65,20 +68,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")
|
||||
@@ -86,6 +86,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()
|
||||
}
|
||||
|
||||
@@ -4,3 +4,11 @@ type GetVersionRsp struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
type GetPreviewRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type SetPreviewReq struct {
|
||||
Enable bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
9
server/proto/hid.go
Normal file
9
server/proto/hid.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package proto
|
||||
|
||||
type GetHidModeRsp struct {
|
||||
Mode string `json:"mode"` // normal or hid-only
|
||||
}
|
||||
|
||||
type SetHidModeReq struct {
|
||||
Mode string `validate:"required"` // normal or hid-only
|
||||
}
|
||||
@@ -12,6 +12,11 @@ type DeleteMacReq struct {
|
||||
Mac string `form:"mac" validate:"required"`
|
||||
}
|
||||
|
||||
type SetMacNameReq struct {
|
||||
Mac string `form:"mac" validate:"required"`
|
||||
Name string `form:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type TailscaleState string
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package proto
|
||||
|
||||
type GetFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
type UpdateFrameDetectReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type UpdateFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
type StopFrameDetectReq struct {
|
||||
Duration int `validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package proto
|
||||
|
||||
type IP struct {
|
||||
Name string `json:"name"`
|
||||
Addr string `json:"addr"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GetInfoRsp struct {
|
||||
Ip string `json:"ip"`
|
||||
IPs []IP `json:"ips"`
|
||||
Mdns string `json:"mdns"`
|
||||
Image string `json:"image"`
|
||||
Application string `json:"application"`
|
||||
@@ -62,8 +69,8 @@ type UpdateVirtualDeviceRsp struct {
|
||||
}
|
||||
|
||||
type SetMemoryLimitReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Limit int64 `json:"limit"`
|
||||
Enabled bool `validate:"omitempty"`
|
||||
Limit int64 `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMemoryLimitRsp struct {
|
||||
@@ -72,7 +79,7 @@ type GetMemoryLimitRsp struct {
|
||||
}
|
||||
|
||||
type SetOledReq struct {
|
||||
Sleep int `json:"sleep"`
|
||||
Sleep int `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetOLEDRsp struct {
|
||||
@@ -80,10 +87,52 @@ type GetOLEDRsp struct {
|
||||
Sleep int `json:"sleep"`
|
||||
}
|
||||
|
||||
type GetGetHdmiStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetSSHStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetSwapRsp struct {
|
||||
Size int64 `json:"size"` // unit: MB
|
||||
}
|
||||
|
||||
type SetSwapReq struct {
|
||||
Size int64 `validate:"omitempty"` // unit: MB
|
||||
}
|
||||
|
||||
type GetMouseJigglerRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type SetMouseJigglerReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
Mode string `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMdnsStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type SetHostnameReq struct {
|
||||
Hostname string `validate:"required"`
|
||||
}
|
||||
|
||||
type GetHostnameRsp struct {
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
type SetWebTitleReq struct {
|
||||
Title string `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetWebTitleRsp struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type SetTlsReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -13,4 +13,7 @@ func applicationRouter(r *gin.Engine) {
|
||||
|
||||
api.GET("/application/version", service.GetVersion) // get application version
|
||||
api.POST("/application/update", service.Update) // update application
|
||||
|
||||
api.GET("/application/preview", service.GetPreview) // get preview updates state
|
||||
api.POST("/application/preview", service.SetPreview) // set preview updates state
|
||||
}
|
||||
|
||||
@@ -13,4 +13,7 @@ func hidRouter(r *gin.Engine) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ func networkRouter(r *gin.Engine) {
|
||||
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.POST("/network/wol", service.WakeOnLAN) // wake on lan
|
||||
api.GET("/network/wol/mac", service.GetMac) // get mac list
|
||||
api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac
|
||||
|
||||
api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information
|
||||
api.POST("/network/wol", service.WakeOnLAN) // wake on lan
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/service/stream/direct"
|
||||
"NanoKVM-Server/service/stream/h264"
|
||||
"NanoKVM-Server/service/stream/mjpeg"
|
||||
|
||||
@@ -12,9 +13,9 @@ func streamRouter(r *gin.Engine) {
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/stream/mjpeg", mjpeg.Connect) // mjpeg stream
|
||||
api.GET("/stream/mjpeg/detect", mjpeg.GetFrameDetect) // get frame detect state
|
||||
api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect state
|
||||
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", h264.Connect) // h264 stream (webrtc)
|
||||
api.GET("/stream/h264/direct", direct.Connect) // h264 stream (http)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func vmRouter(r *gin.Engine) {
|
||||
service := vm.NewService()
|
||||
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/vm/info", service.GetInfo) // get device information
|
||||
@@ -34,15 +35,33 @@ 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
|
||||
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
|
||||
|
||||
api.GET("/vm/swap", service.GetSwap) // get swap file size
|
||||
api.POST("/vm/swap", service.SetSwap) // set swap file size
|
||||
|
||||
api.GET("/vm/mouse-jiggler", service.GetMouseJiggler) // get mouse jiggler
|
||||
api.POST("/vm/mouse-jiggler/", service.SetMouseJiggler) // set mouse jiggler
|
||||
|
||||
api.GET("/vm/hostname", service.GetHostname) // Get Hostname
|
||||
api.POST("/vm/hostname", service.SetHostname) // Set Hostname
|
||||
|
||||
api.GET("/vm/web-title", service.GetWebTitle) // Get web title
|
||||
api.POST("/vm/web-title", service.SetWebTitle) // Set web title
|
||||
|
||||
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
|
||||
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.POST("/vm/system/reboot", service.Reboot) // reboot system
|
||||
}
|
||||
|
||||
60
server/service/application/preview.go
Normal file
60
server/service/application/preview.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
PreviewUpdatesFlag = "/etc/kvm/preview_updates"
|
||||
)
|
||||
|
||||
func (s *Service) GetPreview(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled := isPreviewEnabled()
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetPreviewRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetPreview(c *gin.Context) {
|
||||
var req proto.SetPreviewReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enable == isPreviewEnabled() {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enable {
|
||||
if err := os.WriteFile(PreviewUpdatesFlag, []byte("1"), 0o644); err != nil {
|
||||
log.Errorf("failed to write %s: %s", PreviewUpdatesFlag, err)
|
||||
rsp.ErrRsp(c, -2, "enable failed")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(PreviewUpdatesFlag); err != nil {
|
||||
log.Errorf("failed to remove %s: %s", PreviewUpdatesFlag, err)
|
||||
rsp.ErrRsp(c, -3, "disable failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set preview updates state: %t", req.Enable)
|
||||
}
|
||||
|
||||
func isPreviewEnabled() bool {
|
||||
_, err := os.Stat(PreviewUpdatesFlag)
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
BaseURL = "https://cdn.sipeed.com/nanokvm"
|
||||
StableURL = "https://cdn.sipeed.com/nanokvm"
|
||||
PreviewURL = "https://cdn.sipeed.com/nanokvm/preview"
|
||||
|
||||
AppDir = "/kvmapp"
|
||||
BackupDir = "/root/old"
|
||||
|
||||
@@ -3,13 +3,11 @@ package application
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,39 +21,6 @@ const (
|
||||
maxTries = 3
|
||||
)
|
||||
|
||||
type Latest struct {
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Sha512 string `json:"sha512"`
|
||||
Size uint `json:"size"`
|
||||
}
|
||||
|
||||
func (s *Service) GetVersion(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// current version
|
||||
currentVersion := "1.0.0"
|
||||
|
||||
versionFile := fmt.Sprintf("%s/version", AppDir)
|
||||
if version, err := os.ReadFile(versionFile); err == nil {
|
||||
currentVersion = strings.ReplaceAll(string(version), "\n", "")
|
||||
}
|
||||
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get latest version failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Update(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
@@ -87,10 +52,9 @@ func update() error {
|
||||
}
|
||||
|
||||
// download
|
||||
url := fmt.Sprintf("%s/%s", BaseURL, latest.Name)
|
||||
target := fmt.Sprintf("%s/%s", CacheDir, latest.Name)
|
||||
|
||||
if err := download(url, target); err != nil {
|
||||
if err := download(latest.Url, target); err != nil {
|
||||
log.Errorf("download app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -135,39 +99,6 @@ func update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLatest() (*Latest, error) {
|
||||
url := fmt.Sprintf("%s/latest.json?now=%d", BaseURL, time.Now().Unix())
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var latest Latest
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
log.Errorf("failed to unmarshal response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("get application latest version: %s", latest.Version)
|
||||
return &latest, nil
|
||||
}
|
||||
|
||||
func download(url string, target string) (err error) {
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
|
||||
89
server/service/application/version.go
Normal file
89
server/service/application/version.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Latest struct {
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Sha512 string `json:"sha512"`
|
||||
Size uint `json:"size"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (s *Service) GetVersion(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// current version
|
||||
currentVersion := "1.0.0"
|
||||
|
||||
versionFile := fmt.Sprintf("%s/version", AppDir)
|
||||
if version, err := os.ReadFile(versionFile); err == nil {
|
||||
currentVersion = strings.ReplaceAll(string(version), "\n", "")
|
||||
}
|
||||
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get latest version failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func getLatest() (*Latest, error) {
|
||||
baseURL := StableURL
|
||||
if isPreviewEnabled() {
|
||||
baseURL = PreviewURL
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/latest.json?now=%d", baseURL, time.Now().Unix())
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var latest Latest
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
log.Errorf("failed to unmarshal response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latest.Url = fmt.Sprintf("%s/%s", baseURL, latest.Name)
|
||||
|
||||
log.Debugf("get application latest version: %s", latest.Version)
|
||||
return &latest, nil
|
||||
}
|
||||
158
server/service/hid/mode.go
Normal file
158
server/service/hid/mode.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeNormal = "normal"
|
||||
ModeHidOnly = "hid-only"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var modeMap = map[string]string{
|
||||
"0x0510": ModeNormal,
|
||||
"0x0623": ModeHidOnly,
|
||||
}
|
||||
|
||||
func (s *Service) GetHidMode(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
mode, err := getHidMode()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get HID mode failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetHidModeRsp{
|
||||
Mode: mode,
|
||||
})
|
||||
log.Debugf("get hid mode: %s", mode)
|
||||
}
|
||||
|
||||
func (s *Service) SetHidMode(c *gin.Context) {
|
||||
var req proto.SetHidModeReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
if req.Mode != ModeNormal && req.Mode != ModeHidOnly {
|
||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if mode, _ := getHidMode(); req.Mode == mode {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
srcScript := NormalModeScript
|
||||
if req.Mode == ModeHidOnly {
|
||||
srcScript = HidOnlyModeScript
|
||||
}
|
||||
|
||||
if err := copyModeFile(srcScript); err != nil {
|
||||
rsp.ErrRsp(c, -3, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
|
||||
log.Println("reboot system...")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_ = exec.Command("reboot").Run()
|
||||
}
|
||||
|
||||
func copyModeFile(srcScript string) error {
|
||||
// open the source file
|
||||
srcFile, err := os.Open(srcScript)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open %s: %s", srcScript, err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = srcFile.Close()
|
||||
}()
|
||||
|
||||
srcInfo, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get %s info: %s", srcScript, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
_ = os.Remove(tmpPath)
|
||||
}()
|
||||
log.Debugf("create temporary file: %s", tmpPath)
|
||||
|
||||
if err := tmpFile.Chmod(srcInfo.Mode()); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
log.Errorf("failed to set %s mode: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpFile, srcFile); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
log.Errorf("failed to copy %s: %s", srcScript, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpFile.Sync(); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
log.Errorf("failed to sync %s: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
log.Errorf("failed to close %s: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// replace the target file with the temporary file
|
||||
if err := os.Rename(tmpPath, TargetModeScript); err != nil {
|
||||
log.Errorf("failed to rename %s: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("copy %s to %s successful", srcScript, TargetModeScript)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHidMode() (string, error) {
|
||||
data, err := os.ReadFile(ModeFlag)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read %s: %s", ModeFlag, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(string(data))
|
||||
mode, ok := modeMap[key]
|
||||
if !ok {
|
||||
log.Errorf("invalid mode flag: %s", key)
|
||||
return "", errors.New("invalid mode flag")
|
||||
}
|
||||
|
||||
return mode, nil
|
||||
}
|
||||
@@ -21,152 +21,94 @@ type PasteReq struct {
|
||||
func (s *Service) Paste(c *gin.Context) {
|
||||
var req PasteReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Content) > 1024 {
|
||||
rsp.ErrRsp(c, -2, "")
|
||||
rsp.ErrRsp(c, -2, "content too long")
|
||||
return
|
||||
}
|
||||
|
||||
s.hid.kbMutex.Lock()
|
||||
defer s.hid.kbMutex.Unlock()
|
||||
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
for _, char := range req.Content {
|
||||
key, ok := charMap[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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,54 @@ func (s *Service) GetMac(c *gin.Context) {
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func (s *Service) SetMacName(c *gin.Context) {
|
||||
var req proto.SetMacNameReq // Mac:string Name:string
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -2, "read failed")
|
||||
return
|
||||
}
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
var newLines []string
|
||||
macFound := false
|
||||
|
||||
for _, line := range macs {
|
||||
parts := strings.Split(line, " ")
|
||||
if req.Mac != parts[0] {
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
newLines = append(newLines, parts[0]+" "+req.Name)
|
||||
macFound = true
|
||||
}
|
||||
|
||||
if !macFound {
|
||||
log.Errorf("failed to found mac %s: %s", req.Mac, err)
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
}
|
||||
|
||||
data := strings.Join(newLines, "\n")
|
||||
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set wol mac name: %s %s", req.Mac, req.Name)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteMac(c *gin.Context) {
|
||||
var req proto.DeleteMacReq
|
||||
var rsp proto.Response
|
||||
@@ -85,7 +133,8 @@ func (s *Service) DeleteMac(c *gin.Context) {
|
||||
var newMacs []string
|
||||
|
||||
for _, mac := range macs {
|
||||
if req.Mac != mac {
|
||||
parts := strings.Split(mac, " ")
|
||||
if req.Mac != parts[0] {
|
||||
newMacs = append(newMacs, mac)
|
||||
}
|
||||
}
|
||||
@@ -164,7 +213,8 @@ func isMacExist(mac string) bool {
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
for _, item := range macs {
|
||||
if mac == item {
|
||||
parts := strings.Split(item, " ")
|
||||
if mac == parts[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
107
server/service/stream/direct/h264.go
Normal file
107
server/service/stream/direct/h264.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
)
|
||||
|
||||
type Frame struct {
|
||||
IsKeyFrame bool `json:"isKeyFrame"`
|
||||
Data string `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var (
|
||||
mutex = sync.Mutex{}
|
||||
wsMap = make(map[*websocket.Conn]bool)
|
||||
isSending = false
|
||||
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 create websocket: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = ws.Close()
|
||||
log.Debugf("h264 websocket disconnected")
|
||||
}()
|
||||
|
||||
var zeroTime time.Time
|
||||
_ = ws.SetReadDeadline(zeroTime)
|
||||
|
||||
mutex.Lock()
|
||||
wsMap[ws] = true
|
||||
if len(wsMap) == 1 && !isSending {
|
||||
go send()
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
_, _, err = ws.ReadMessage()
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
delete(wsMap, ws)
|
||||
mutex.Unlock()
|
||||
log.Debugf("failed to read message: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func send() {
|
||||
isSending = true
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
startTime := time.Now()
|
||||
|
||||
for range ticker.C {
|
||||
if len(wsMap) == 0 {
|
||||
isSending = false
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
frameMsg := Frame{
|
||||
IsKeyFrame: result == 3,
|
||||
Data: base64.StdEncoding.EncodeToString(data),
|
||||
Timestamp: time.Since(startTime).Microseconds(),
|
||||
}
|
||||
|
||||
frameJSON, err := json.Marshal(frameMsg)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for ws := range wsMap {
|
||||
if err := ws.WriteMessage(websocket.TextMessage, frameJSON); err != nil {
|
||||
log.Debugf("failed to write message: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package h264
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4"
|
||||
@@ -9,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
pc *webrtc.PeerConnection
|
||||
ws *websocket.Conn
|
||||
pc *webrtc.PeerConnection
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
@@ -36,9 +38,7 @@ func (c *Client) addTrack() {
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
trackMap[c.ws] = videoTrack
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// register callback events
|
||||
@@ -61,13 +61,9 @@ func (c *Client) register() {
|
||||
// ICE connection state has changed
|
||||
c.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state == webrtc.ICEConnectionStateConnected && !isSending {
|
||||
mutex.Lock()
|
||||
if !isSending {
|
||||
// start sending h264 data
|
||||
go send()
|
||||
isSending = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
// start sending h264 data
|
||||
go send()
|
||||
isSending = true
|
||||
}
|
||||
|
||||
log.Debugf("ice connection state has changed to %s", state.String())
|
||||
@@ -81,13 +77,11 @@ func (c *Client) readMessage() {
|
||||
for {
|
||||
_, raw, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
delete(trackMap, c.ws)
|
||||
if len(trackMap) == 0 && isSending {
|
||||
if isSending && len(trackMap) == 0 {
|
||||
// stop sending when all websocket connections are closed
|
||||
exitSig <- true
|
||||
isSending = false
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
log.Debugf("failed to read message: %s", err)
|
||||
return
|
||||
@@ -155,8 +149,8 @@ func (c *Client) readMessage() {
|
||||
|
||||
// send websocket message
|
||||
func (c *Client) sendMessage(event string, data string) error {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
message := &Message{
|
||||
Event: event,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/config"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -18,9 +19,7 @@ var (
|
||||
},
|
||||
}
|
||||
trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample)
|
||||
mutex = sync.RWMutex{}
|
||||
isSending = false
|
||||
exitSig = make(chan bool, 1)
|
||||
)
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
@@ -35,6 +34,9 @@ func Connect(c *gin.Context) {
|
||||
log.Debugf("h264 websocket disconnected")
|
||||
}()
|
||||
|
||||
var zeroTime time.Time
|
||||
_ = wsConn.SetReadDeadline(zeroTime)
|
||||
|
||||
conf := config.GetInstance()
|
||||
|
||||
var iceServers []webrtc.ICEServer
|
||||
@@ -67,8 +69,9 @@ func Connect(c *gin.Context) {
|
||||
}()
|
||||
|
||||
client := &Client{
|
||||
ws: wsConn,
|
||||
pc: peerConn,
|
||||
ws: wsConn,
|
||||
pc: peerConn,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
|
||||
client.addTrack()
|
||||
|
||||
@@ -2,7 +2,6 @@ package h264
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
|
||||
func send() {
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
@@ -18,63 +19,33 @@ func send() {
|
||||
defer ticker.Stop()
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
height := screen.Height
|
||||
width, ok := common.ResolutionMap[height]
|
||||
if !ok {
|
||||
width = 0
|
||||
height = 0
|
||||
}
|
||||
|
||||
bitRate := screen.BitRate
|
||||
if _, ok := common.BitRateMap[bitRate]; !ok {
|
||||
bitRate = 3000
|
||||
}
|
||||
|
||||
data, sps, pps, result := vision.ReadH264(width, height, bitRate)
|
||||
if result < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if result == 3 {
|
||||
writeSample(sps, duration)
|
||||
writeSample(pps, duration)
|
||||
}
|
||||
writeSample(data, duration)
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
|
||||
case <-exitSig:
|
||||
mutex.Lock()
|
||||
isSending = false
|
||||
mutex.Unlock()
|
||||
for range ticker.C {
|
||||
if !isSending && len(trackMap) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSample(data []byte, duration time.Duration) {
|
||||
sample := media.Sample{
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
sample := media.Sample{
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
for _, track := range trackMap {
|
||||
if err := track.WriteSample(sample); err != nil {
|
||||
log.Errorf("failed to send h264 data: %s", err)
|
||||
for _, track := range trackMap {
|
||||
if err := track.WriteSample(sample); err != nil {
|
||||
log.Errorf("failed to send h264 data: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("send h264 data: %d", len(data))
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("send h264 data: %d", len(data))
|
||||
}
|
||||
|
||||
@@ -1,132 +1,55 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
frameDetect = "/etc/kvm/frame_detact"
|
||||
frameDetectTmp = "/etc/kvm/frame_detact_tmp"
|
||||
)
|
||||
|
||||
type UpdateFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func GetFrameDetect(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled, err := isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame detect status")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetFrameDetectRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
log.Debugf("get frame detect success, enabled: %t", isEnabled)
|
||||
}
|
||||
const FrameDetectInterval uint8 = 60
|
||||
|
||||
func UpdateFrameDetect(c *gin.Context) {
|
||||
var req proto.UpdateFrameDetectReq
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled, err := isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if isEnabled {
|
||||
_ = os.Remove(frameDetect)
|
||||
_ = os.Remove(frameDetectTmp)
|
||||
} else {
|
||||
file, err2 := os.OpenFile(frameDetect, os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err2 != nil {
|
||||
rsp.ErrRsp(c, -3, "enable frame detect failed")
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
var frame uint8 = 0
|
||||
if req.Enabled {
|
||||
frame = FrameDetectInterval
|
||||
}
|
||||
|
||||
isEnabled, err = isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -4, "unknown frame status")
|
||||
return
|
||||
}
|
||||
common.GetKvmVision().SetFrameDetect(frame)
|
||||
|
||||
rsp.OkRspWithData(c, &UpdateFrameDetectRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
log.Debugf("update frame detect success, enabled: %t", isEnabled)
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("update frame detect: %t", req.Enabled)
|
||||
}
|
||||
|
||||
func StopFrameDetect(c *gin.Context) {
|
||||
var req proto.StopFrameDetectReq
|
||||
var rsp proto.Response
|
||||
|
||||
exist, err := isFileExist(frameDetect)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if !exist {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
duration := 10 * time.Second
|
||||
if req.Duration > 0 {
|
||||
duration = time.Duration(req.Duration) * time.Second
|
||||
}
|
||||
|
||||
err = os.Rename(frameDetect, frameDetectTmp)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "stop operation failed")
|
||||
return
|
||||
}
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Second)
|
||||
_ = os.Rename(frameDetectTmp, frameDetect)
|
||||
log.Debug("frame detect started")
|
||||
}()
|
||||
vision.SetFrameDetect(0)
|
||||
time.Sleep(duration)
|
||||
vision.SetFrameDetect(FrameDetectInterval)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("frame detect stopped")
|
||||
}
|
||||
|
||||
func isFrameDetectEnabled() (bool, error) {
|
||||
exist, err := isFileExist(frameDetect)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if exist {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
exist, err = isFileExist(frameDetectTmp)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exist, nil
|
||||
}
|
||||
|
||||
func isFileExist(name string) (bool, error) {
|
||||
_, err := os.Stat(name)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
chanMap = make(map[*gin.Context]chan struct{})
|
||||
mutex = sync.RWMutex{}
|
||||
chanMap = make(map[*gin.Context]bool)
|
||||
mutex = sync.Mutex{}
|
||||
exitSig = make(chan bool, 1)
|
||||
)
|
||||
|
||||
@@ -23,13 +23,13 @@ func Connect(c *gin.Context) {
|
||||
c.Header("Pragma", "no-cache")
|
||||
|
||||
mutex.Lock()
|
||||
chanMap[c] = make(chan struct{}, 1)
|
||||
chanMap[c] = true
|
||||
if len(chanMap) == 1 {
|
||||
go send()
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
<-chanMap[c]
|
||||
<-c.Request.Context().Done()
|
||||
|
||||
mutex.Lock()
|
||||
delete(chanMap, c)
|
||||
@@ -41,6 +41,8 @@ func Connect(c *gin.Context) {
|
||||
|
||||
func send() {
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
|
||||
fps := screen.FPS
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(fps))
|
||||
@@ -50,27 +52,14 @@ func send() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
height := screen.Height
|
||||
width, ok := common.ResolutionMap[height]
|
||||
if !ok {
|
||||
width = 0
|
||||
height = 0
|
||||
}
|
||||
|
||||
quality := screen.Quality
|
||||
if _, ok := common.QualityMap[quality]; !ok {
|
||||
quality = 80
|
||||
}
|
||||
|
||||
data, result := vision.ReadMjpeg(width, height, quality)
|
||||
if result < 0 {
|
||||
data, result := vision.ReadMjpeg(screen.Width, screen.Height, screen.Quality)
|
||||
if result < 0 || result == 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
for c, ch := range chanMap {
|
||||
for c := range chanMap {
|
||||
if err := write(c, data); err != nil {
|
||||
log.Debugf("failed to write mjpeg data: %s", err)
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
log.Debugf("send mjpeg data: %d", len(data))
|
||||
|
||||
@@ -23,12 +23,13 @@ func (s *Service) SetGpio(c *gin.Context) {
|
||||
}
|
||||
|
||||
device := ""
|
||||
conf := config.GetInstance().Hardware
|
||||
|
||||
switch req.Type {
|
||||
case "power":
|
||||
device = s.config.Hardware.GPIOPower
|
||||
device = conf.GPIOPower
|
||||
case "reset":
|
||||
device = s.config.Hardware.GPIOReset
|
||||
device = conf.GPIOReset
|
||||
default:
|
||||
rsp.ErrRsp(c, -2, fmt.Sprintf("invalid power event: %s", req.Type))
|
||||
return
|
||||
@@ -53,15 +54,17 @@ func (s *Service) SetGpio(c *gin.Context) {
|
||||
func (s *Service) GetGpio(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
pwr, err := readGpio(s.config.Hardware.GPIOPowerLED)
|
||||
conf := config.GetInstance().Hardware
|
||||
|
||||
pwr, err := readGpio(conf.GPIOPowerLED)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read power led: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
hdd := false
|
||||
if s.config.Hardware.Version == config.HWVersionAlpha {
|
||||
hdd, err = readGpio(s.config.Hardware.GPIOHDDLed)
|
||||
if conf.Version == config.HWVersionAlpha {
|
||||
hdd, err = readGpio(conf.GPIOHDDLed)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read hdd led: %s", err))
|
||||
return
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var hdmiEnabled = true
|
||||
|
||||
func (s *Service) ResetHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
@@ -17,7 +20,42 @@ func (s *Service) ResetHdmi(c *gin.Context) {
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(1 * time.Second)
|
||||
vision.SetHDMI(true)
|
||||
hdmiEnabled = true
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("reset hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) EnableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(true)
|
||||
hdmiEnabled = true
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("enable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) DisableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(false)
|
||||
hdmiEnabled = false
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("disable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) GetHdmiState(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetGetHdmiStateRsp{
|
||||
Enabled: hdmiEnabled,
|
||||
})
|
||||
|
||||
log.Debug("get hdmi state")
|
||||
}
|
||||
|
||||
57
server/service/vm/hostname.go
Normal file
57
server/service/vm/hostname.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
BootHostnameFile = "/boot/hostname"
|
||||
EtcHostname = "/etc/hostname"
|
||||
)
|
||||
|
||||
func (s *Service) SetHostname(c *gin.Context) {
|
||||
var req proto.SetHostnameReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte(fmt.Sprintf("%s", req.Hostname))
|
||||
|
||||
if err := os.WriteFile(BootHostnameFile, data, 0o644); err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to write data")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(EtcHostname, data, 0o644); err != nil {
|
||||
rsp.ErrRsp(c, -3, "failed to write data")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set Hostname: %s", req.Hostname)
|
||||
}
|
||||
|
||||
func (s *Service) GetHostname(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
data, err := os.ReadFile(EtcHostname)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hostname failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetHostnameRsp{
|
||||
Hostname: strings.Replace(string(data), "\n", "", -1),
|
||||
})
|
||||
log.Debugf("get Hostname successful")
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package vm
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -19,13 +18,14 @@ var imageVersionMap = map[string]string{
|
||||
"2024-08-08-19-44-bef2ca.img": "v1.2.0",
|
||||
"2024-11-13-09-59-9c961a.img": "v1.3.0",
|
||||
"2025-02-17-19-08-3649fe.img": "v1.4.0",
|
||||
"2025-04-17-14-21-98d17d.img": "v1.4.1",
|
||||
}
|
||||
|
||||
func (s *Service) GetInfo(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
data := &proto.GetInfoRsp{
|
||||
Ip: getIp(),
|
||||
IPs: getIPs(),
|
||||
Mdns: getMdns(),
|
||||
Image: getImageVersion(),
|
||||
Application: getApplicationVersion(),
|
||||
@@ -36,20 +36,24 @@ func (s *Service) GetInfo(c *gin.Context) {
|
||||
log.Debug("get vm information success")
|
||||
}
|
||||
|
||||
func getIp() string {
|
||||
addressList, err := net.InterfaceAddrs()
|
||||
func getIPs() (ips []proto.IP) {
|
||||
interfaces, err := GetInterfaceInfos()
|
||||
if err != nil {
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
for _, address := range addressList {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
|
||||
ip := ipnet.IP.String()
|
||||
return ip
|
||||
for _, iface := range interfaces {
|
||||
if iface.IP.To4() != nil {
|
||||
ips = append(ips, proto.IP{
|
||||
Name: iface.Name,
|
||||
Addr: iface.IP.String(),
|
||||
Version: "IPv4",
|
||||
Type: iface.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
func getMdns() string {
|
||||
|
||||
107
server/service/vm/ip.go
Normal file
107
server/service/vm/ip.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
Wired = "Wired"
|
||||
Wireless = "Wireless"
|
||||
Other = "Other"
|
||||
)
|
||||
|
||||
type InterfaceInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
func GetInterfaceInfos() ([]*InterfaceInfo, error) {
|
||||
var interfaceInfos []*InterfaceInfo
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get net interfaces: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
info := getInterfaceInfo(iface)
|
||||
if info != nil {
|
||||
interfaceInfos = append(interfaceInfos, info)
|
||||
}
|
||||
}
|
||||
|
||||
if len(interfaceInfos) == 0 {
|
||||
return nil, fmt.Errorf("no valid IP address")
|
||||
}
|
||||
|
||||
return interfaceInfos, nil
|
||||
}
|
||||
|
||||
func getInterfaceInfo(iface net.Interface) *InterfaceInfo {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
interfaceType := getInterfaceType(iface)
|
||||
if interfaceType == Other {
|
||||
return nil
|
||||
}
|
||||
|
||||
interfaceIP := getInterfaceIP(iface)
|
||||
if interfaceIP == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &InterfaceInfo{
|
||||
Name: iface.Name,
|
||||
Type: interfaceType,
|
||||
IP: interfaceIP,
|
||||
}
|
||||
}
|
||||
|
||||
func getInterfaceType(iface net.Interface) string {
|
||||
if strings.HasPrefix(iface.Name, "eth") || strings.HasPrefix(iface.Name, "en") {
|
||||
return Wired
|
||||
}
|
||||
|
||||
if strings.HasPrefix(iface.Name, "wlan") || strings.HasPrefix(iface.Name, "wl") {
|
||||
return Wireless
|
||||
}
|
||||
|
||||
return Other
|
||||
}
|
||||
|
||||
func getInterfaceIP(iface net.Interface) net.IP {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get interface addresses: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
server/service/vm/jiggler/jiggler.go
Normal file
119
server/service/vm/jiggler/jiggler.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package jiggler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigFile = "/etc/kvm/mouse-jiggler"
|
||||
Interval = 15 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
jiggler Jiggler
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
type Jiggler struct {
|
||||
mutex sync.Mutex
|
||||
enabled bool
|
||||
running bool
|
||||
mode string
|
||||
lastUpdated time.Time
|
||||
}
|
||||
|
||||
func GetJiggler() *Jiggler {
|
||||
once.Do(func() {
|
||||
jiggler = Jiggler{
|
||||
mutex: sync.Mutex{},
|
||||
enabled: false,
|
||||
running: false,
|
||||
mode: "relative",
|
||||
lastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(ConfigFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mode := strings.ReplaceAll(string(content), "\n", "")
|
||||
if mode != "" {
|
||||
jiggler.mode = mode
|
||||
}
|
||||
|
||||
jiggler.enabled = true
|
||||
})
|
||||
|
||||
return &jiggler
|
||||
}
|
||||
|
||||
func (j *Jiggler) Enable(mode string) error {
|
||||
err := os.WriteFile(ConfigFile, []byte(mode), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.enabled = true
|
||||
j.mode = mode
|
||||
j.Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Jiggler) Disable() error {
|
||||
if err := os.Remove(ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.enabled = false
|
||||
j.mode = "relative"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Jiggler) Run() {
|
||||
if !j.enabled || j.running {
|
||||
return
|
||||
}
|
||||
|
||||
j.mutex.Lock()
|
||||
j.running = true
|
||||
j.mutex.Unlock()
|
||||
|
||||
j.Update()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if !j.enabled {
|
||||
j.running = false
|
||||
return
|
||||
}
|
||||
|
||||
if time.Since(j.lastUpdated) > Interval {
|
||||
move(j.mode)
|
||||
j.Update()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (j *Jiggler) Update() {
|
||||
if j.running {
|
||||
j.lastUpdated = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Jiggler) IsEnabled() bool {
|
||||
return j.enabled
|
||||
}
|
||||
|
||||
func (j *Jiggler) GetMode() string {
|
||||
return j.mode
|
||||
}
|
||||
55
server/service/vm/jiggler/mouse.go
Normal file
55
server/service/vm/jiggler/mouse.go
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
49
server/service/vm/mouse-jiggler.go
Normal file
49
server/service/vm/mouse-jiggler.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) GetMouseJiggler(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
mouseJiggler := jiggler.GetJiggler()
|
||||
|
||||
data := &proto.GetMouseJigglerRsp{
|
||||
Enabled: mouseJiggler.IsEnabled(),
|
||||
Mode: mouseJiggler.GetMode(),
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func (s *Service) SetMouseJiggler(c *gin.Context) {
|
||||
var req proto.SetMouseJigglerReq
|
||||
var rsp proto.Response
|
||||
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
mouseJiggler := jiggler.GetJiggler()
|
||||
|
||||
if req.Enabled {
|
||||
err = mouseJiggler.Enable(req.Mode)
|
||||
} else {
|
||||
err = mouseJiggler.Disable()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set mouse jiggler: %t", req.Enabled)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/common"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -22,30 +23,34 @@ func (s *Service) SetScreen(c *gin.Context) {
|
||||
var req proto.SetScreenReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
file, ok := screenFileMap[req.Type]
|
||||
if !ok {
|
||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("%d", req.Value)
|
||||
if req.Type == "type" {
|
||||
switch req.Type {
|
||||
case "type":
|
||||
data := "h264"
|
||||
if req.Value == 0 {
|
||||
data = "mjpeg"
|
||||
} else {
|
||||
data = "h264"
|
||||
}
|
||||
err = writeScreen("type", data)
|
||||
|
||||
case "gop":
|
||||
gop := 30
|
||||
if req.Value >= 1 && req.Value <= 100 {
|
||||
gop = req.Value
|
||||
}
|
||||
common.GetKvmVision().SetGop(uint8(gop))
|
||||
|
||||
default:
|
||||
data := strconv.Itoa(req.Value)
|
||||
err = writeScreen(req.Type, data)
|
||||
}
|
||||
|
||||
err := os.WriteFile(file, []byte(data), 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("write kvm %s failed: %s", file, err)
|
||||
rsp.ErrRsp(c, -3, "update screen failed")
|
||||
rsp.ErrRsp(c, -2, "update screen failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,3 +59,18 @@ func (s *Service) SetScreen(c *gin.Context) {
|
||||
log.Debugf("update screen: %+v", req)
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func writeScreen(key string, value string) error {
|
||||
file, ok := screenFileMap[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid argument %s", key)
|
||||
}
|
||||
|
||||
err := os.WriteFile(file, []byte(value), 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("write kvm %s failed: %s", file, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{
|
||||
config: config.GetInstance(),
|
||||
}
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
176
server/service/vm/swap.go
Normal file
176
server/service/vm/swap.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
SwapFile = "/swapfile"
|
||||
InittabPath = "/etc/inittab"
|
||||
TempInittab = "/etc/.inittab.tmp"
|
||||
)
|
||||
|
||||
func (s *Service) GetSwap(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetSwapRsp{
|
||||
Size: getSwapSize(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetSwap(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
var req proto.SetSwapReq
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
size := getSwapSize()
|
||||
if req.Size == size {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
if err := disableSwap(); err != nil {
|
||||
rsp.ErrRsp(c, -2, "disable swap failed")
|
||||
return
|
||||
}
|
||||
if err := disableInittab(); err != nil {
|
||||
rsp.ErrRsp(c, -3, "disable inittab failed")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := enableSwap(req.Size); err != nil {
|
||||
rsp.ErrRsp(c, -4, "enable swap failed")
|
||||
return
|
||||
}
|
||||
if err := enableInittab(); err != nil {
|
||||
rsp.ErrRsp(c, -5, "enable inittab failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func getSwapSize() int64 {
|
||||
fileInfo, err := os.Stat(SwapFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return fileInfo.Size() / 1024 / 1024
|
||||
}
|
||||
|
||||
func enableSwap(size int64) error {
|
||||
if getSwapSize() > 0 {
|
||||
if err := disableSwap(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("fallocate -l %dM %s", size, SwapFile),
|
||||
fmt.Sprintf("chmod 600 %s", SwapFile),
|
||||
fmt.Sprintf("mkswap %s", SwapFile),
|
||||
fmt.Sprintf("swapon %s", SwapFile),
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to execute %s: %s", command, err)
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Debugf("set swap file size: %d", size)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableSwap() error {
|
||||
command := "swapoff -a"
|
||||
if err := exec.Command("sh", "-c", command).Run(); err != nil {
|
||||
log.Errorf("failed to execute swapoff: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(SwapFile); err != nil {
|
||||
log.Errorf("failed to delete %s: %s", SwapFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableInittab() error {
|
||||
f, err := os.OpenFile(InittabPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("read inittab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
content := fmt.Sprintf("\nsi11::sysinit:/sbin/swapon %s", SwapFile)
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
log.Errorf("write inittab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %s", InittabPath, content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableInittab() error {
|
||||
defer func() {
|
||||
_ = os.Remove(TempInittab)
|
||||
}()
|
||||
|
||||
input, err := os.ReadFile(InittabPath)
|
||||
if err != nil {
|
||||
log.Errorf("read fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(input), "\n")
|
||||
output := make([]string, 0)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasSuffix(line, SwapFile) {
|
||||
log.Debugf("%s delete line: %s", InittabPath, line)
|
||||
} else {
|
||||
output = append(output, line)
|
||||
}
|
||||
}
|
||||
|
||||
content := strings.Join(output, "\n")
|
||||
content = strings.TrimSuffix(content, "\n")
|
||||
if err := os.WriteFile(TempInittab, []byte(content), 0644); err != nil {
|
||||
log.Errorf("write temp fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(TempInittab, InittabPath); err != nil {
|
||||
log.Errorf("replace fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
58
server/service/vm/web-title.go
Normal file
58
server/service/vm/web-title.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
WebTitleFile = "/etc/kvm/web-title"
|
||||
)
|
||||
|
||||
func (s *Service) SetWebTitle(c *gin.Context) {
|
||||
var req proto.SetWebTitleReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title == "" || req.Title == "NanoKVM" {
|
||||
err := os.Remove(WebTitleFile)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "reset failed")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := os.WriteFile(WebTitleFile, []byte(req.Title), 0o644)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set web title: %s", req.Title)
|
||||
}
|
||||
|
||||
func (s *Service) GetWebTitle(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
data, err := os.ReadFile(WebTitleFile)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read web title failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetWebTitleRsp{
|
||||
Title: strings.Replace(string(data), "\n", "", -1),
|
||||
})
|
||||
|
||||
log.Debugf("get web title successful")
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/service/hid"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -85,6 +86,9 @@ func (c *WsClient) Read() error {
|
||||
} else if event[0] == MouseEvent {
|
||||
c.mouse <- event[1:]
|
||||
}
|
||||
|
||||
// update latest HID operation time
|
||||
jiggler.GetJiggler().Update()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
@@ -61,17 +61,17 @@ void set_venc_auto_recyc(uint8_t _enable);
|
||||
-2: VENC Errorl
|
||||
-1: No images were acquired
|
||||
0: Acquire MJPEG encoded images
|
||||
1: Acquire H264 encoded images(SPS)
|
||||
2: Acquire H264 encoded images(PPS)
|
||||
1: Acquire H264 encoded images(SPS)[Deprecated]
|
||||
2: Acquire H264 encoded images(PPS)[Deprecated]
|
||||
3: Acquire H264 encoded images(I)
|
||||
4: Acquire H264 encoded images(P)
|
||||
5: IMG not changed
|
||||
**********************************************************************************/
|
||||
int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size);
|
||||
int free_kvmv_data(uint8_t ** _pp_kvm_data);
|
||||
void free_all_kvmv_data();
|
||||
void set_h264_gop(uint8_t _gop);
|
||||
void set_frame_detact(uint8_t _frame_detact);
|
||||
void kvmv_deinit();
|
||||
uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#define vi_max_width 1920
|
||||
#define vi_max_height 1080
|
||||
|
||||
#define Farame_sample_size 76800 // 320*240
|
||||
|
||||
#define default_vi_width 1920
|
||||
#define default_vi_height 1080
|
||||
#define default_vpss_width 1920
|
||||
@@ -32,12 +34,14 @@
|
||||
#define default_h264_gop 30
|
||||
|
||||
#define kvmv_data_buffer_size 4
|
||||
#define Try_rounds_HDMI_err_res 5
|
||||
|
||||
#define vi_width_path "/kvmapp/kvm/width"
|
||||
#define vi_height_path "/kvmapp/kvm/height"
|
||||
#define hdmi_mode_path "/etc/kvm/hdmi_mode"
|
||||
#define hdmi_state_path "/proc/lt_int"
|
||||
#define watchdog_mode_path "/etc/kvm/watchdog"
|
||||
#define watchdog_temp_path "/tmp/watchdog"
|
||||
#define watchdog_file "/tmp/nanokvm_wd"
|
||||
|
||||
#define LT6911_ADDR 0x2B
|
||||
@@ -71,7 +75,7 @@ typedef struct {
|
||||
uint16_t qlty;
|
||||
uint8_t cam_state = 0;
|
||||
uint8_t stream_stop;
|
||||
uint8_t frame_detact;
|
||||
uint8_t frame_detact = 0;
|
||||
uint8_t display;
|
||||
uint8_t reinit_flag = 1;
|
||||
uint8_t reopen_cam_flag = 0;
|
||||
@@ -86,6 +90,7 @@ typedef struct {
|
||||
uint8_t hdmi_mode = 0;
|
||||
uint8_t hdmi_res_type = 0;
|
||||
uint8_t hdmi_res_err = 0;
|
||||
uint8_t hdmi_try_rounds = 0;
|
||||
uint8_t vi_detect_state = 0;
|
||||
uint8_t venc_auto_recyc = 0;
|
||||
} kvmv_cfg_t;
|
||||
@@ -109,8 +114,6 @@ camera::Camera *cam = new camera::Camera(default_vpss_width, default_vpss_height
|
||||
kvmv_cfg_t kvmv_cfg;
|
||||
|
||||
kvmv_data_t kvmv_data_buffer[kvmv_data_buffer_size];
|
||||
kvmv_data_t kvmv_SPS_buffer = {0};
|
||||
kvmv_data_t kvmv_PPS_buffer = {0};
|
||||
|
||||
uint8_t kvmv_data_buffer_index = 0;
|
||||
|
||||
@@ -307,9 +310,10 @@ int get_hdmi_mode(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t watchdog_sf_is_open()
|
||||
uint8_t watchdog_sf_is_open(void)
|
||||
{
|
||||
if(access(watchdog_mode_path, F_OK) == 0) return 1;
|
||||
else if(access(watchdog_temp_path, F_OK) == 0) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
@@ -1015,7 +1019,7 @@ void* watchdog_sf_feed(void * arg)
|
||||
debug("[kvmv] Ion memory is full reboot now!\n");
|
||||
system("reboot");
|
||||
}
|
||||
debug("[kvmv] watchdog_sf_feed now!\n");
|
||||
// debug("[kvmv] watchdog_sf_feed now!\n");
|
||||
vision_update_watchdog();
|
||||
}
|
||||
}
|
||||
@@ -1065,6 +1069,7 @@ void* vi_subsystem_detection(void * arg)
|
||||
uint8_t rising_times = 0;
|
||||
uint8_t falling_times = 0;
|
||||
uint8_t cam_need_restart = 0;
|
||||
uint8_t auto_change_mode = 0;
|
||||
kvmv_cfg.thread_is_running = 1;
|
||||
if(access("/proc/lt_int", F_OK) != 0){
|
||||
time::sleep_ms(10);
|
||||
@@ -1128,15 +1133,10 @@ void* vi_subsystem_detection(void * arg)
|
||||
kvmv_cfg.hdmi_res_type = lt6911_get_csi_res(&kvmv_cfg.vi_width, &kvmv_cfg.vi_height);
|
||||
if (kvmv_cfg.hdmi_res_type == NEW_RES) kvmv_cfg.reopen_cam_flag = 1;
|
||||
else if (kvmv_cfg.hdmi_res_type == UNKNOWN_RES){
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
if(auto_try_res() == 1){
|
||||
printf("[hdmi] auto get res\n");
|
||||
kvmv_cfg.hdmi_res_err = NORMAL_RES;
|
||||
} else {
|
||||
// Potential deadlock may occur
|
||||
kvmv_cfg.hdmi_res_err = ERROR_RES;
|
||||
}
|
||||
kvmv_cfg.vi_detect_state = 0;
|
||||
/* Move HDMI resolution modification directly
|
||||
to mode1 to solve deadlock problem */
|
||||
auto_change_mode = 1;
|
||||
set_hdmi_mode(1);
|
||||
}
|
||||
} else {
|
||||
// HDMI res = 0*0/x*0
|
||||
@@ -1158,15 +1158,10 @@ void* vi_subsystem_detection(void * arg)
|
||||
kvmv_cfg.hdmi_res_type = lt6911_get_csi_res(&kvmv_cfg.vi_width, &kvmv_cfg.vi_height);
|
||||
if (kvmv_cfg.hdmi_res_type == NEW_RES) kvmv_cfg.reopen_cam_flag = 1;
|
||||
else if (kvmv_cfg.hdmi_res_type == UNKNOWN_RES){
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
if(auto_try_res() == 1){
|
||||
printf("[hdmi] auto get res\n");
|
||||
kvmv_cfg.hdmi_res_err = NORMAL_RES;
|
||||
} else {
|
||||
// Potential deadlock may occur
|
||||
kvmv_cfg.hdmi_res_err = ERROR_RES;
|
||||
}
|
||||
kvmv_cfg.vi_detect_state = 0;
|
||||
/* Move HDMI resolution modification directly
|
||||
to mode1 to solve deadlock problem */
|
||||
auto_change_mode = 1;
|
||||
set_hdmi_mode(1);
|
||||
}
|
||||
} else {
|
||||
// HDMI res = 0*0/x*0
|
||||
@@ -1197,20 +1192,35 @@ void* vi_subsystem_detection(void * arg)
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
}
|
||||
if(kvmv_cfg.vi_detect_state == 1){
|
||||
|
||||
try_res = auto_try_res();
|
||||
if (try_res == 1) {
|
||||
kvmv_cfg.hdmi_res_err = NORMAL_RES;
|
||||
kvmv_cfg.hdmi_try_rounds = 0;
|
||||
kvmv_cfg.vi_detect_state = 2;
|
||||
} else if (try_res == 0) {
|
||||
printf("[kvmv] Suitable resolution not found, switching to manual input mode automatically\n");
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
set_hdmi_mode(2);
|
||||
/* There may be a situation where the correct resolution cannot be recognized
|
||||
after one round of detection. By default, Try_rounds_HDMI_err_res rounds will be detected,
|
||||
and if it cannot be detected, jump to the next mode */
|
||||
kvmv_cfg.hdmi_res_err = ERROR_RES;
|
||||
kvmv_cfg.hdmi_try_rounds++;
|
||||
if(kvmv_cfg.hdmi_try_rounds >= Try_rounds_HDMI_err_res){
|
||||
kvmv_cfg.hdmi_try_rounds = 0;
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
// printf("[kvmv] Suitable resolution not found, switching to manual input mode automatically\n");
|
||||
if(auto_change_mode == 1){
|
||||
auto_change_mode = 0;
|
||||
set_hdmi_mode(0);
|
||||
} else {
|
||||
set_hdmi_mode(2);
|
||||
}
|
||||
}
|
||||
} else if (try_res == 2) {
|
||||
kvmv_cfg.hdmi_try_rounds = 0;
|
||||
// Cannot obtain HDMI input / No signal on HDMI
|
||||
}
|
||||
} else if (kvmv_cfg.vi_detect_state == 2){
|
||||
// Low-frequency detection of HDMI status, no log output
|
||||
printf("[kvmv] kvmv_cfg.vi_detect_state == 2\n");
|
||||
printf("[kvmv] kvmv_cfg.vi_detect_state == 2\n");
|
||||
err_code = get_vi_state();
|
||||
if (err_code != 1) {
|
||||
kvmv_cfg.vi_detect_state = 1;
|
||||
@@ -1332,6 +1342,31 @@ int sync_vi_res()
|
||||
return res;
|
||||
}
|
||||
|
||||
uint8_t frame_changed(image::Image *raw)
|
||||
{
|
||||
static int raw_size = 0;
|
||||
static uint8_t Farame_sample[Farame_sample_size] = {0};
|
||||
uint8_t ret = 0;
|
||||
uint8_t sample_byte;
|
||||
if(raw->data_size() != raw_size){
|
||||
raw_size = raw->data_size();
|
||||
ret = 1;
|
||||
}
|
||||
int Detection_Pixel_Interval = raw_size/(Farame_sample_size*1.5);
|
||||
for(int i = 0; i < Farame_sample_size; i ++){
|
||||
// printf("[kvmv] i = %d\n", i);
|
||||
if(i >= raw_size){
|
||||
ret = 0;
|
||||
}
|
||||
sample_byte = *(uint8_t*)(raw->data()+(i*Detection_Pixel_Interval));
|
||||
if(sample_byte != Farame_sample[i]){
|
||||
Farame_sample[i] = sample_byte;
|
||||
ret = 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void jpg_dump(kvmv_data_t* dump_to, image::Image *raw)
|
||||
{
|
||||
dump_to->p_img_data = (uint8_t *)malloc(raw->data_size());
|
||||
@@ -1376,29 +1411,17 @@ int h264_stream_dump(kvmv_data_t* dump_to, mmf_stream_t* dump_from)
|
||||
static int8_t I_Frame_index = -1;
|
||||
// debug("[kvmv]dump_from->count = %d\n", dump_from->count);
|
||||
if (dump_from->count == 3) {
|
||||
// debug("[kvmv]dump I-Frame\r\n");
|
||||
if(kvmv_SPS_buffer.p_img_data != NULL || kvmv_PPS_buffer.p_img_data != NULL)
|
||||
return IMG_BUFFER_FULL;
|
||||
|
||||
kvmv_SPS_buffer.p_img_data = (uint8_t *)malloc(dump_from->data_size[0]);
|
||||
kvmv_PPS_buffer.p_img_data = (uint8_t *)malloc(dump_from->data_size[1]);
|
||||
dump_to->p_img_data = (uint8_t *)malloc(dump_from->data_size[2]);
|
||||
dump_to->p_img_data = (uint8_t *)malloc(dump_from->data_size[0]+dump_from->data_size[1]+dump_from->data_size[2]);
|
||||
dump_to->img_data_size = dump_from->data_size[0]+dump_from->data_size[1]+dump_from->data_size[2];
|
||||
dump_to->img_data_type = IMG_H264_TYPE_IF;
|
||||
memcpy(dump_to->p_img_data, dump_from->data[0], dump_from->data_size[0]);
|
||||
memcpy(dump_to->p_img_data+dump_from->data_size[0], dump_from->data[1], dump_from->data_size[1]);
|
||||
memcpy(dump_to->p_img_data+dump_from->data_size[0]+dump_from->data_size[1], dump_from->data[2], dump_from->data_size[2]);
|
||||
|
||||
kvmv_SPS_buffer.img_data_size = dump_from->data_size[0];
|
||||
kvmv_PPS_buffer.img_data_size = dump_from->data_size[1];
|
||||
dump_to->img_data_size = dump_from->data_size[2];
|
||||
|
||||
kvmv_SPS_buffer.img_data_type = IMG_H264_TYPE_SPS;
|
||||
kvmv_PPS_buffer.img_data_type = IMG_H264_TYPE_PPS;
|
||||
dump_to->img_data_type = IMG_H264_TYPE_IF;
|
||||
|
||||
memcpy(kvmv_SPS_buffer.p_img_data, dump_from->data[0], dump_from->data_size[0]);
|
||||
memcpy(kvmv_PPS_buffer.p_img_data, dump_from->data[1], dump_from->data_size[1]);
|
||||
memcpy(dump_to->p_img_data, dump_from->data[2], dump_from->data_size[2]);
|
||||
|
||||
debug("[kvmv]SPS size = %d\n", kvmv_SPS_buffer.img_data_size);
|
||||
debug("[kvmv]PPS size = %d\n", kvmv_PPS_buffer.img_data_size);
|
||||
debug("[kvmv]I-Frame size = %d\n", dump_to->img_data_size);
|
||||
debug("[kvmv]SPS size = %d\n", dump_from->data_size[0]);
|
||||
debug("[kvmv]PPS size = %d\n", dump_from->data_size[1]);
|
||||
debug("[kvmv]I-Frame size = %d\n", dump_from->data_size[2]);
|
||||
|
||||
return IMG_H264_TYPE_IF;
|
||||
|
||||
@@ -1419,18 +1442,17 @@ int h264_stream_dump(kvmv_data_t* dump_to, mmf_stream_t* dump_from)
|
||||
void set_h264_gop(uint8_t _gop)
|
||||
{
|
||||
kvm_venc.enc_h264_init = 0; // call
|
||||
kvmvenc_gop = maxmin_data(100, 10, (int)_gop);
|
||||
kvmvenc_gop = maxmin_data(100, 1, (int)_gop);
|
||||
debug("[kvmv] set_h264_gop = %d\n", kvmvenc_gop);
|
||||
}
|
||||
|
||||
// uint8_t get_h264_gop(void)
|
||||
// {
|
||||
// return kvm_venc.kvm_venc_cfg.gop;
|
||||
// }
|
||||
|
||||
// uint8_t get_hdmi_width(void)
|
||||
// {
|
||||
// return kvm_venc.kvm_venc_cfg.gop;
|
||||
// }
|
||||
void set_frame_detact(uint8_t _frame_detact)
|
||||
{
|
||||
uint8_t frame_detact = maxmin_data(100, 0, (int)_frame_detact);
|
||||
kvmv_cfg.frame_detact = frame_detact;
|
||||
debug("[kvmv] set_frame_detact = %d\n", kvmv_cfg.frame_detact);
|
||||
kvmv_cfg.stream_stop = 0;
|
||||
}
|
||||
|
||||
int8_t raw_to_h264(image::Image *raw, kvmv_data_t* ret_stream, uint16_t _qlty)
|
||||
{
|
||||
@@ -1513,8 +1535,6 @@ void kvmv_init(uint8_t _debug_info_en)
|
||||
for(int i = 0; i < kvmv_data_buffer_size; i++){
|
||||
kvmv_data_buffer[i].p_img_data = NULL;
|
||||
}
|
||||
kvmv_SPS_buffer.p_img_data = NULL;
|
||||
kvmv_PPS_buffer.p_img_data = NULL;
|
||||
|
||||
kvmv_cfg.try_exit_thread = 0;
|
||||
// debug("[kvmv]kvmv_init - 2\r\n");
|
||||
@@ -1585,13 +1605,15 @@ void set_venc_auto_recyc(uint8_t _enable)
|
||||
-2: VENC Errorl
|
||||
-1: No images were acquired
|
||||
0: Acquire MJPEG encoded images
|
||||
1: Acquire H264 encoded images(SPS)
|
||||
2: Acquire H264 encoded images(PPS)
|
||||
1: Acquire H264 encoded images(SPS)[Deprecated]
|
||||
2: Acquire H264 encoded images(PPS)[Deprecated]
|
||||
3: Acquire H264 encoded images(I)
|
||||
4: Acquire H264 encoded images(P)
|
||||
5: IMG not changed
|
||||
**********************************************************************************/
|
||||
int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size)
|
||||
{
|
||||
static uint8_t frame_undetact_count = 0;
|
||||
// uint64_t __attribute__((unused)) start_time = time::time_ms();
|
||||
debug("[kvmv]kvmv_read_img type = %d...\n", _type);
|
||||
struct timespec ts;
|
||||
@@ -1648,7 +1670,30 @@ int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _ql
|
||||
// debug("[kvmv]befor read img: %d \r\n", (int)(time::time_ms() - start_time));
|
||||
image::Image *img = cam->read();
|
||||
// debug("[kvmv]read img: %d \r\n", (int)(time::time_ms() - start_time));
|
||||
|
||||
if(img != NULL){
|
||||
// frame detect
|
||||
if(_type == VENC_MJPEG && kvmv_cfg.frame_detact != 0){
|
||||
if(kvmv_cfg.stream_stop == 0){
|
||||
frame_undetact_count++;
|
||||
} else {
|
||||
frame_undetact_count = kvmv_cfg.frame_detact;
|
||||
}
|
||||
|
||||
if(frame_undetact_count == kvmv_cfg.frame_detact){
|
||||
frame_undetact_count = 0;
|
||||
|
||||
if(frame_changed(img) == 0){
|
||||
debug("[kvmv]frame not changed...\n");
|
||||
kvmv_cfg.stream_stop = 1;
|
||||
delete img;
|
||||
pthread_mutex_unlock(&vi_mutex);
|
||||
return 5;
|
||||
} else {
|
||||
kvmv_cfg.stream_stop = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(kvmv_cfg.cam_state == 0) {
|
||||
kvmv_cfg.cam_state = 1;
|
||||
kvmv_cfg.hdmi_cable_state = 1;
|
||||
@@ -1729,30 +1774,6 @@ int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _ql
|
||||
return IMG_NOT_EXIST;
|
||||
}
|
||||
|
||||
int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size)
|
||||
{
|
||||
if(kvmv_SPS_buffer.p_img_data == NULL){
|
||||
return IMG_NOT_EXIST;
|
||||
} else {
|
||||
*_pp_kvm_data = kvmv_SPS_buffer.p_img_data;
|
||||
*_p_kvmv_data_size = kvmv_SPS_buffer.img_data_size;
|
||||
return IMG_H264_TYPE_SPS;
|
||||
}
|
||||
return IMG_NOT_EXIST;
|
||||
}
|
||||
|
||||
int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size)
|
||||
{
|
||||
if(kvmv_PPS_buffer.p_img_data == NULL){
|
||||
return IMG_NOT_EXIST;
|
||||
} else {
|
||||
*_pp_kvm_data = kvmv_PPS_buffer.p_img_data;
|
||||
*_p_kvmv_data_size = kvmv_PPS_buffer.img_data_size;
|
||||
return IMG_H264_TYPE_PPS;
|
||||
}
|
||||
return IMG_NOT_EXIST;
|
||||
}
|
||||
|
||||
int free_kvmv_data(uint8_t ** _pp_kvm_data)
|
||||
{
|
||||
// debug("[kvmv]free_kvmv_data - 1\r\n");
|
||||
@@ -1765,12 +1786,6 @@ int free_kvmv_data(uint8_t ** _pp_kvm_data)
|
||||
// debug("[kvmv]free_kvmv_data - 3\r\n");
|
||||
kvmv_data_buffer[i].p_img_data = NULL;
|
||||
uint8_t _type = kvmv_data_buffer[i].img_data_type;
|
||||
if(_type == IMG_H264_TYPE_IF){
|
||||
free(kvmv_SPS_buffer.p_img_data);
|
||||
free(kvmv_PPS_buffer.p_img_data);
|
||||
kvmv_SPS_buffer.p_img_data = NULL;
|
||||
kvmv_PPS_buffer.p_img_data = NULL;
|
||||
}
|
||||
return _type;
|
||||
} else {
|
||||
return IMG_NOT_EXIST;
|
||||
@@ -1788,10 +1803,6 @@ void free_all_kvmv_data()
|
||||
kvmv_data_buffer[i].p_img_data = NULL;
|
||||
}
|
||||
}
|
||||
if(kvmv_SPS_buffer.p_img_data != NULL) free(kvmv_SPS_buffer.p_img_data);
|
||||
if(kvmv_PPS_buffer.p_img_data != NULL) free(kvmv_PPS_buffer.p_img_data);
|
||||
kvmv_SPS_buffer.p_img_data = NULL;
|
||||
kvmv_PPS_buffer.p_img_data = NULL;
|
||||
}
|
||||
|
||||
void kvmv_deinit()
|
||||
@@ -1844,24 +1855,3 @@ uint8_t kvmv_hdmi_control(uint8_t _en)
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
todo list:
|
||||
- [x] 添加芯片型号
|
||||
- [-] 写一个不支持的分辨率列表,如果不支持就发出错误提醒,不输出/不获取图像
|
||||
- mode0:
|
||||
- 获取到支持的分辨率:直接初始化
|
||||
- 不支持的分辨率:输出对应错误
|
||||
- 不在列表的分辨率:自动进入模式1
|
||||
- mode1:
|
||||
- 根据/proc/cvitek
|
||||
- [ ] 写一个文档说明获取USB HDMI ETH WiFi的状态
|
||||
- [ ] mode0:添加功能,获取到一个错误的res自动切换为探索模式
|
||||
- [ ] add mode3:仅i2c获取模式
|
||||
- [ ] 创建守护server进程
|
||||
- [ ] mDNS默认配置问题
|
||||
- [ ] 关于看门狗:kvm_vision和kvm_system相互在/tmp里狗叫,同时vision中添加检测是否read卡死,system中检测ion是否够用,否则重启
|
||||
VI_SDK_IOC_S_CTRL - vi_sdk_enable_chn NG, No buffer space available ion是否够用也放在vision里吧,前面是log
|
||||
|
||||
*/
|
||||
@@ -149,9 +149,8 @@ then
|
||||
if [ "$Project_Name" = "kvm_system" ]
|
||||
then
|
||||
SOURCE_PATH="$Project_PATH/dist/kvm_system_release/kvm_system"
|
||||
TARGET_PATH="$KVMAPP_PATH/kvm_system/kvm_system"
|
||||
TARGET_PATH="$KVMAPP_PATH/kvm_system"
|
||||
chmod +x $SOURCE_PATH
|
||||
rm -f $TARGET_PATH
|
||||
else
|
||||
SOURCE_PATH="$Project_PATH/dist/kvm_vision_test_release/dl_lib"
|
||||
TARGET_PATH="$KVMAPP_PATH/server/"
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#define OLED_SLEEP_DELAY_MIN 10
|
||||
#define OLED_SLEEP_DELAY_DEFAULT 30
|
||||
#define KVM_WD_COUNT_MAX 10
|
||||
#define RM_Watchdog_times 60
|
||||
|
||||
typedef struct {
|
||||
int8_t page = 0;
|
||||
@@ -97,6 +98,7 @@ typedef struct {
|
||||
uint8_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_
|
||||
|
||||
@@ -140,6 +140,8 @@ int qrencode(char *string)
|
||||
ip_addr_t show_which_ip(void)
|
||||
{
|
||||
if(kvm_sys_state.wifi_state == -2) return ETH_IP;
|
||||
if(kvm_oled_state.eth_state == 3 && kvm_oled_state.wifi_state != 1) return ETH_IP;
|
||||
if(kvm_oled_state.eth_state != 3 && kvm_oled_state.wifi_state == 1) return WiFi_IP;
|
||||
static uint8_t run_count = 0;
|
||||
static ip_addr_t ip_type = ETH_IP;
|
||||
run_count++;
|
||||
@@ -182,6 +184,22 @@ uint8_t ip_changed(ip_addr_t ip_type)
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t kvm_state_is_changed()
|
||||
{
|
||||
if( (kvm_sys_state.eth_state != kvm_oled_state.eth_state) ||
|
||||
(kvm_sys_state.wifi_state != kvm_oled_state.wifi_state) ||
|
||||
(kvm_sys_state.usb_state != kvm_oled_state.usb_state) ||
|
||||
(kvm_sys_state.hdmi_state != kvm_oled_state.hdmi_state) ||
|
||||
/* (kvm_sys_state.now_fps != kvm_oled_state.now_fps) || */
|
||||
(kvm_sys_state.hdmi_width != kvm_oled_state.hdmi_width) ||
|
||||
(kvm_sys_state.hdmi_height != kvm_oled_state.hdmi_height) ||
|
||||
(kvm_sys_state.type != kvm_oled_state.type) ||
|
||||
(kvm_sys_state.qlty != kvm_oled_state.qlty) )
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
void kvm_eth_state_disp(ip_addr_t _ip_type, uint8_t first_disp)
|
||||
{
|
||||
static ip_addr_t _ip_type_old = NULL_IP;
|
||||
@@ -337,6 +355,11 @@ void kvm_main_ui_disp(uint8_t first_disp, uint8_t subpage_changed)
|
||||
ip_addr_t now_ip_type;
|
||||
// if(kvm_oled_state.sub_page == 0)
|
||||
// if(kvm_oled_state.oled_sleep_state == 1){
|
||||
|
||||
// Any operation will update the OLED sleep time
|
||||
if (kvm_state_is_changed())
|
||||
oled_auto_sleep_time_update();
|
||||
|
||||
if(kvm_oled_state.sub_page == 1){
|
||||
// main page (oled sleep)
|
||||
kvm_oled_clear(first_disp || subpage_changed);
|
||||
@@ -505,3 +528,15 @@ void oled_auto_sleep(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void kvm_show_UE(void)
|
||||
{
|
||||
if(kvm_hw_ver != 2){
|
||||
OLED_ShowString(0, 0, "HDMI: UE", 16);
|
||||
} else {
|
||||
OLED_Revolve();
|
||||
OLED_ShowString(0, 0, "HDMI: UE", 16);
|
||||
// OLED_ShowString_AlignRight(AlignRightEND_P, 3, " FPS", 4);
|
||||
// kvm_init_pcie_ui();
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@ void kvm_main_ui_disp(uint8_t first_disp, uint8_t subpage_changed);
|
||||
void kvm_wifi_config_ui_disp(uint8_t first_disp, uint8_t subpage_changed);
|
||||
void oled_auto_sleep_time_update(void);
|
||||
void oled_auto_sleep(void);
|
||||
void kvm_show_UE(void);
|
||||
|
||||
#endif // OLED_UI_H_
|
||||
@@ -7,34 +7,82 @@ using namespace maix::sys;
|
||||
extern kvm_sys_state_t kvm_sys_state;
|
||||
extern kvm_oled_state_t kvm_oled_state;
|
||||
|
||||
uint8_t get_hdmi_version()
|
||||
{
|
||||
FILE *fp;
|
||||
uint8_t RW_Data[2];
|
||||
system("/kvmapp/system/init.d/S15kvmhwd get_hdmi_version");
|
||||
if(access("/etc/kvm/hdmi_version", F_OK) == 0){
|
||||
fp = fopen("/etc/kvm/hdmi_version", "r");
|
||||
fread(RW_Data, sizeof(char), 2, fp);
|
||||
fclose(fp);
|
||||
if(RW_Data[0] == 'u'){
|
||||
// 6911uxc
|
||||
if(RW_Data[1] == 'e'){
|
||||
return 2;
|
||||
} else if(RW_Data[1] == 'x') {
|
||||
return 1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
// 6911c
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Production_testing_patch(void)
|
||||
{
|
||||
// Product UE version detecte
|
||||
if(get_hdmi_version() == 2){
|
||||
printf("ue_patch_state = 1;\n");
|
||||
kvm_oled_state.ue_patch_state = 1;
|
||||
} else {
|
||||
printf("ue_patch_state = 0;\n");
|
||||
kvm_oled_state.ue_patch_state = 0;
|
||||
}
|
||||
|
||||
// New products default to disabling mDNS functionality
|
||||
system("rm -f /etc/init.d/S50ssdpd");
|
||||
system("sync");
|
||||
}
|
||||
|
||||
void new_app_init(void)
|
||||
{
|
||||
// Update the necessary scripts
|
||||
system("cp -f /kvmapp/system/update-nanokvm.py /etc/kvm/");
|
||||
system("rm /etc/init.d/S02udisk");
|
||||
system("cp /kvmapp/system/init.d/S01fs /etc/init.d/");
|
||||
system("cp /kvmapp/system/init.d/S03usbdev /etc/init.d/");
|
||||
system("cp /kvmapp/system/init.d/S15kvmhwd /etc/init.d/");
|
||||
system("cp /kvmapp/system/init.d/S30eth /etc/init.d/");
|
||||
system("cp /kvmapp/system/init.d/S50sshd /etc/init.d/");
|
||||
system("rm -f /etc/init.d/S02udisk");
|
||||
system("cp -f /kvmapp/system/init.d/S00kmod /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S01fs /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S03usbdev /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S15kvmhwd /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S30eth /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S50sshd /etc/init.d/");
|
||||
if(kvm_wifi_exist()) {
|
||||
system("cp /kvmapp/system/init.d/S30wifi /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S30wifi /etc/init.d/");
|
||||
} else {
|
||||
system("rm /etc/init.d/S30wifi");
|
||||
system("rm -f /etc/init.d/S30wifi");
|
||||
}
|
||||
|
||||
// rmmod soph_saradc
|
||||
system("rmmod soph_saradc");
|
||||
system("rm -f /mnt/system/ko/soph_saradc.ko");
|
||||
|
||||
// PCIe Patch
|
||||
// system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/");
|
||||
if(access("/kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so", F_OK) != 0){
|
||||
system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/");
|
||||
system("cp -f /kvmapp/system/init.d/S95nanokvm /etc/init.d/");
|
||||
}
|
||||
|
||||
// Remove unnecessary components to speed up boot time
|
||||
system("rm /etc/init.d/S04backlight");
|
||||
system("rm /etc/init.d/S05tp");
|
||||
system("rm /etc/init.d/S40bluetoothd");
|
||||
system("rm /etc/init.d/S50ssdpd");
|
||||
system("rm /etc/init.d/S99*");
|
||||
system("rm -f /etc/init.d/S04backlight");
|
||||
system("rm -f /etc/init.d/S05tp");
|
||||
system("rm -f /etc/init.d/S40bluetoothd");
|
||||
system("rm -f /etc/init.d/S50ssdpd");
|
||||
system("rm -f /etc/init.d/S99*");
|
||||
|
||||
// Add necessary configuration files for program execution
|
||||
system("mkdir /kvmapp/kvm");
|
||||
@@ -48,10 +96,10 @@ void new_app_init(void)
|
||||
system("touch /etc/kvm/frame_detact");
|
||||
|
||||
// rm jpg_stream & kvm_stream
|
||||
system("rm -r /kvmapp/jpg_stream");
|
||||
system("rm /kvmapp/kvm_system/kvm_stream"); // Cannot delete temporarily; the old production test script uses this file to determine if the download is complete
|
||||
system("rm -rf /kvmapp/jpg_stream");
|
||||
system("rm -f /kvmapp/kvm_system/kvm_stream"); // Cannot delete temporarily; the old production test script uses this file to determine if the download is complete
|
||||
|
||||
system("rm /kvmapp/kvm_new_app");
|
||||
system("rm -f /kvmapp/kvm_new_app");
|
||||
system("sync");
|
||||
// system("/etc/init.d/S95nanokvm restart");
|
||||
|
||||
@@ -87,6 +135,9 @@ void new_app_init(void)
|
||||
else if(RW_Data[0] == 'u') hdmi_ver = 2;
|
||||
}
|
||||
|
||||
// system("/etc/init.d/S03usbdev stop_start");
|
||||
create_temp_watchdog();
|
||||
|
||||
if(hdmi_ver == 2){
|
||||
if(RW_Data_1[0] != '/'){
|
||||
system("cp /kvmapp/system/ko/soph_mipi_rx.ko /mnt/system/ko/soph_mipi_rx.ko");
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#define SYSTEM_INIT_H_
|
||||
#include "config.h"
|
||||
|
||||
void init_upadte(void);
|
||||
void new_img_init(void);
|
||||
void new_app_init(void);
|
||||
void Production_testing_patch(void);
|
||||
|
||||
#endif // SYSTEM_INIT_H_
|
||||
|
||||
@@ -470,9 +470,43 @@ uint8_t ion_free_space(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t watchdog_sf_is_open()
|
||||
int create_temp_watchdog(void)
|
||||
{
|
||||
FILE *file;
|
||||
|
||||
file = fopen(watchdog_temp_path, "w");
|
||||
if (file == NULL) {
|
||||
printf("[kvmv] Temp watchdog create error\n");
|
||||
return -1;
|
||||
}
|
||||
// fprintf(file, "%s", 'v');
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void rm_temp_watchdog(void)
|
||||
{
|
||||
if(access(watchdog_temp_path, F_OK) == 0) {
|
||||
remove(watchdog_temp_path);
|
||||
}
|
||||
}
|
||||
|
||||
void auto_remove_temp_watchdog(void)
|
||||
{
|
||||
static uint8_t run_times = 0;
|
||||
static uint8_t temp_watchdog_removed = 0;
|
||||
if(temp_watchdog_removed) return;
|
||||
if(run_times++ >= RM_Watchdog_times){
|
||||
run_times = 0;
|
||||
temp_watchdog_removed = 1;
|
||||
rm_temp_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t watchdog_sf_is_open(void)
|
||||
{
|
||||
if(access(watchdog_mode_path, F_OK) == 0) return 1;
|
||||
if(access(watchdog_temp_path, F_OK) == 0) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ enum ip_addr_t
|
||||
#define NIC_STATE_NO_EXIST -2
|
||||
|
||||
#define watchdog_mode_path "/etc/kvm/watchdog"
|
||||
#define watchdog_temp_path "/tmp/watchdog"
|
||||
#define watchdog_file "/tmp/nanokvm_wd"
|
||||
|
||||
// net_port
|
||||
@@ -34,7 +35,10 @@ void kvm_update_rndis_state(void);
|
||||
void kvm_update_tailscale_state(void);
|
||||
uint8_t ion_free_space(void);
|
||||
int get_nic_state(const char* interface_name);
|
||||
uint8_t watchdog_sf_is_open();
|
||||
int create_temp_watchdog(void);
|
||||
void rm_temp_watchdog(void);
|
||||
void auto_remove_temp_watchdog(void);
|
||||
uint8_t watchdog_sf_is_open(void);
|
||||
int check_watchdog();
|
||||
|
||||
#endif // SYSTEM_STATE_H_
|
||||
|
||||
@@ -7,13 +7,6 @@ using namespace maix::peripheral;
|
||||
kvm_sys_state_t kvm_sys_state;
|
||||
kvm_oled_state_t kvm_oled_state;
|
||||
|
||||
/*
|
||||
init.d:
|
||||
/etc/init.d/S00kmod
|
||||
/etc/init.d/S01fs
|
||||
|
||||
*/
|
||||
|
||||
void build_recovery(void)
|
||||
{
|
||||
uint8_t need_recovery = 0;
|
||||
@@ -33,6 +26,13 @@ void* thread_oled_handle(void * arg)
|
||||
OLED_ColorTurn(0); //0正常显示 1 反色显示
|
||||
OLED_DisplayTurn(0); //0正常显示 1 屏幕翻转显示
|
||||
OLED_Clear();
|
||||
if(kvm_oled_state.ue_patch_state == 1){
|
||||
kvm_show_UE();
|
||||
while(kvm_sys_state.oled_thread_running){
|
||||
time::sleep_ms(100);
|
||||
}
|
||||
OLED_Clear();
|
||||
}
|
||||
|
||||
while(kvm_sys_state.oled_thread_running)
|
||||
{
|
||||
@@ -167,6 +167,7 @@ void* thread_sys_handle(void * arg)
|
||||
}
|
||||
|
||||
time::sleep_ms(STATE_DELAY);
|
||||
auto_remove_temp_watchdog();
|
||||
}
|
||||
kvm_sys_state.sys_thread_running = 0;
|
||||
}
|
||||
@@ -196,6 +197,12 @@ int main(int argc, char* argv[])
|
||||
new_app_init();
|
||||
}
|
||||
|
||||
// only for production testing
|
||||
if (access("/tmp/S49kvmtest", F_OK) == 0){
|
||||
printf("Production testing patch\n");
|
||||
Production_testing_patch();
|
||||
}
|
||||
|
||||
system("sync");
|
||||
|
||||
OLED_state = oled_exist();
|
||||
|
||||
@@ -50,25 +50,7 @@ int main(int argc, char* argv[])
|
||||
// ret = kvmv_read_img(1920, 1080, 0, 60, &p_kvmv_img_data, &kvmv_img_data_size);
|
||||
|
||||
printf("kvmv_read_img ret = %d\n", ret);
|
||||
if(ret == IMG_H264_TYPE_IF){
|
||||
uint8_t* p_kvmv_sps_data;
|
||||
uint8_t* p_kvmv_pps_data;
|
||||
uint32_t kvmv_sps_data_size;
|
||||
uint32_t kvmv_pps_data_size;
|
||||
|
||||
ret = kvmv_get_sps_frame(&p_kvmv_sps_data, &kvmv_sps_data_size);
|
||||
if(ret == IMG_H264_TYPE_SPS){
|
||||
printf("Get SPS data size = %d\n", kvmv_sps_data_size);
|
||||
} else {
|
||||
printf("Get SPS data error!\n");
|
||||
}
|
||||
ret = kvmv_get_pps_frame(&p_kvmv_pps_data, &kvmv_pps_data_size);
|
||||
if(ret == IMG_H264_TYPE_PPS){
|
||||
printf("Get PPS data size = %d\n", kvmv_pps_data_size);
|
||||
} else {
|
||||
printf("Get PPS data error!\n");
|
||||
}
|
||||
}
|
||||
// send...
|
||||
// if(ret >= 0){
|
||||
free_kvmv_data(&p_kvmv_img_data);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"antd": "^5.21.6",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.8.4",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"i18next": "^23.16.4",
|
||||
@@ -25,7 +25,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^14.1.3",
|
||||
@@ -34,7 +34,8 @@
|
||||
"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",
|
||||
|
||||
31
web/pnpm-lock.yaml
generated
31
web/pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^5.21.6
|
||||
version: 5.21.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.7.7
|
||||
specifier: ^1.8.4
|
||||
version: 1.8.4
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -51,7 +51,7 @@ importers:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-draggable:
|
||||
specifier: ^4.4.0
|
||||
specifier: ^4.4.6
|
||||
version: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-error-boundary:
|
||||
specifier: ^4.1.2
|
||||
@@ -80,6 +80,9 @@ importers:
|
||||
websocket:
|
||||
specifier: ^1.0.35
|
||||
version: 1.0.35
|
||||
yocto-queue:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
devDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: ^4.3.1
|
||||
@@ -1118,8 +1121,8 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axios@1.7.7:
|
||||
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
|
||||
axios@1.8.4:
|
||||
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
@@ -1159,8 +1162,8 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
caniuse-lite@1.0.30001677:
|
||||
resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==}
|
||||
caniuse-lite@1.0.30001713:
|
||||
resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -2916,6 +2919,10 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yocto-queue@1.2.1:
|
||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
yoctocolors-cjs@2.1.2:
|
||||
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3917,7 +3924,7 @@ snapshots:
|
||||
autoprefixer@10.4.20(postcss@8.4.47):
|
||||
dependencies:
|
||||
browserslist: 4.24.2
|
||||
caniuse-lite: 1.0.30001677
|
||||
caniuse-lite: 1.0.30001713
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.1.1
|
||||
@@ -3928,7 +3935,7 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
|
||||
axios@1.7.7:
|
||||
axios@1.8.4:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.1
|
||||
@@ -3955,7 +3962,7 @@ snapshots:
|
||||
|
||||
browserslist@4.24.2:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001677
|
||||
caniuse-lite: 1.0.30001713
|
||||
electron-to-chromium: 1.5.51
|
||||
node-releases: 2.0.18
|
||||
update-browserslist-db: 1.1.1(browserslist@4.24.2)
|
||||
@@ -3976,7 +3983,7 @@ snapshots:
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001677: {}
|
||||
caniuse-lite@1.0.30001713: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
@@ -5899,4 +5906,6 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yocto-queue@1.2.1: {}
|
||||
|
||||
yoctocolors-cjs@2.1.2: {}
|
||||
|
||||
@@ -13,3 +13,16 @@ export function update() {
|
||||
timeout: 15 * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
// enable/disable preview updates
|
||||
export function setPreviewUpdates(enable: boolean) {
|
||||
const data = {
|
||||
enable
|
||||
};
|
||||
return http.post('/api/application/preview', data);
|
||||
}
|
||||
|
||||
// get preview updates state
|
||||
export function getPreviewUpdates() {
|
||||
return http.get('/api/application/preview');
|
||||
}
|
||||
|
||||
@@ -9,3 +9,16 @@ export function paste(content: string) {
|
||||
export function reset() {
|
||||
return http.post('/api/hid/reset');
|
||||
}
|
||||
|
||||
// get hid mode
|
||||
export function getHidMode() {
|
||||
return http.get('/api/hid/mode');
|
||||
}
|
||||
|
||||
// set hid mode
|
||||
export function setHidMode(mode: string) {
|
||||
const data = {
|
||||
mode
|
||||
};
|
||||
return http.post('/api/hid/mode', data);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ export function deleteWolMac(mac: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// set Mac name
|
||||
export function setWolMacName(mac: string, name: string) {
|
||||
return http.post('/api/network/wol/mac/name', { mac, name });
|
||||
}
|
||||
|
||||
// get wifi information
|
||||
export function getWiFi() {
|
||||
return http.get('/api/network/wifi');
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { http } from '@/lib/http.ts';
|
||||
|
||||
// get whether the frame detect is enabled
|
||||
export function getFrameDetect() {
|
||||
return http.get('/api/stream/mjpeg/detect');
|
||||
}
|
||||
|
||||
// enable/disable frame detect
|
||||
export function updateFrameDetect() {
|
||||
return http.post('/api/stream/mjpeg/detect');
|
||||
export function updateFrameDetect(enabled: boolean) {
|
||||
const data = {
|
||||
enabled
|
||||
};
|
||||
return http.post('/api/stream/mjpeg/detect', data);
|
||||
}
|
||||
|
||||
// pause frame detect for a while (prevent a black screen when opening the page for the first time)
|
||||
export function stopFrameDetect() {
|
||||
return http.post('/api/stream/mjpeg/detect/stop');
|
||||
export function stopFrameDetect(duration: number) {
|
||||
const data = {
|
||||
duration
|
||||
};
|
||||
return http.post('/api/stream/mjpeg/detect/stop', 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');
|
||||
@@ -77,6 +100,46 @@ export function disableSSH() {
|
||||
return http.post('/api/vm/ssh/disable');
|
||||
}
|
||||
|
||||
// get swap file size
|
||||
export function getSwap() {
|
||||
return http.get('/api/vm/swap');
|
||||
}
|
||||
|
||||
// set swap file size
|
||||
export function setSwap(size: number) {
|
||||
return http.post('/api/vm/swap', { size });
|
||||
}
|
||||
|
||||
// get mouse jiggler
|
||||
export function getMouseJiggler() {
|
||||
return http.get('/api/vm/mouse-jiggler');
|
||||
}
|
||||
|
||||
// set mouse jiggler
|
||||
export function setMouseJiggler(enabled: boolean, mode: string) {
|
||||
return http.post('/api/vm/mouse-jiggler', { enabled, mode });
|
||||
}
|
||||
|
||||
// get Hostname
|
||||
export function getHostname() {
|
||||
return http.get('/api/vm/hostname');
|
||||
}
|
||||
|
||||
// set Hostname
|
||||
export function setHostname(hostname: string) {
|
||||
return http.post('/api/vm/hostname', { hostname });
|
||||
}
|
||||
|
||||
// get WebTitle
|
||||
export function getWebTitle() {
|
||||
return http.get('/api/vm/web-title');
|
||||
}
|
||||
|
||||
// set WebTitle
|
||||
export function setWebTitle(title: string) {
|
||||
return http.post('/api/vm/web-title', { title });
|
||||
}
|
||||
|
||||
// get mDNS state
|
||||
export function getMdnsState() {
|
||||
return http.get('/api/vm/mdns');
|
||||
@@ -92,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');
|
||||
|
||||
@@ -21,3 +21,16 @@ html, body {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,5 +1,11 @@
|
||||
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 = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -8,11 +14,23 @@ type HeadProps = {
|
||||
const helmetData = new HelmetData({});
|
||||
|
||||
export const Head = ({ title = '', description = '' }: HeadProps = {}) => {
|
||||
const [webTitle, setWebTitle] = useAtom(webTitleAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!existToken()) return;
|
||||
|
||||
getWebTitle().then((rsp) => {
|
||||
if (rsp.data?.title) {
|
||||
setWebTitle(rsp.data.title);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
helmetData={helmetData}
|
||||
title={title ? `${title} - NanoKVM` : undefined}
|
||||
defaultTitle="NanoKVM"
|
||||
title={webTitle ? webTitle : title ? `${title} - NanoKVM` : undefined}
|
||||
defaultTitle={webTitle}
|
||||
>
|
||||
<meta name="description" content={description} />
|
||||
</Helmet>
|
||||
|
||||
@@ -2,58 +2,57 @@ 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',
|
||||
noAccount:
|
||||
'Fehler beim Auslsen der Benutzerinformation, bitte die Seite neu laden oder das Passwort zurücksetzen',
|
||||
login: 'Anmelden',
|
||||
placeholderUsername: 'Benutzername',
|
||||
placeholderPassword: 'Passwort',
|
||||
placeholderPassword2: 'Bitte Passwort erneut eingeben',
|
||||
noEmptyUsername: 'Benutzername benötigt',
|
||||
noEmptyPassword: 'Passwort benötigt',
|
||||
noAccount: '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 +60,281 @@ 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: 'Nur Standard-Tastaturbuchstaben und Symbole werden unterstützt',
|
||||
placeholder: 'Bitte eingeben',
|
||||
submit: 'Senden',
|
||||
virtual: 'Virtuelle Tastatur',
|
||||
virtual: 'Tastatur',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
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'
|
||||
requestPointer: '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',
|
||||
mountDesc:
|
||||
'Bei einigen Systemen muss die virtuelle Festplatte auf dem Remote-Host ausgeworfen werden, bevor das Image gemountet werden kann.',
|
||||
cdrom: 'Binden Sie das Abbild im CD-ROM Modus ein',
|
||||
mountFailed: 'Einbinden fehlgeschlagen',
|
||||
mountDesc:
|
||||
"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',
|
||||
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.'
|
||||
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 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'
|
||||
},
|
||||
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!'
|
||||
},
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ const en = {
|
||||
screen: {
|
||||
title: 'Screen',
|
||||
video: 'Video Mode',
|
||||
videoDirectTips: 'Enable HTTPS in "Settings > Device" to use this mode',
|
||||
resolution: 'Resolution',
|
||||
auto: 'Automatic',
|
||||
autoTips:
|
||||
@@ -76,6 +77,7 @@ const en = {
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
cursor: 'Cursor style',
|
||||
default: 'Default cursor',
|
||||
pointer: 'Pointer cursor',
|
||||
cell: 'Cell cursor',
|
||||
@@ -85,8 +87,20 @@ const en = {
|
||||
mode: 'Mouse mode',
|
||||
absolute: 'Absolute mode',
|
||||
relative: 'Relative mode',
|
||||
speed: 'Wheel speed',
|
||||
fast: 'Fast',
|
||||
slow: 'Slow',
|
||||
requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.',
|
||||
resetHid: 'Reset HID'
|
||||
resetHid: 'Reset HID',
|
||||
hidOnly: {
|
||||
title: 'HID-Only mode',
|
||||
desc: "If your mouse and keyboard stop responding and resetting HID doesn't help, it could be a compatibility issue between the NanoKVM and the device. Try to enable HID-Only mode for better compatibility.",
|
||||
tip1: 'Enabling HID-Only mode will unmount the virtual U-disk and virtual network',
|
||||
tip2: 'In HID-Only mode, image mounting is disabled',
|
||||
tip3: 'NanoKVM will automatically reboot after switching modes',
|
||||
enable: 'Enable HID-Only mode',
|
||||
disable: 'Disable HID-Only mode'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: 'Images',
|
||||
@@ -133,6 +147,16 @@ const en = {
|
||||
serialPort: 'Serial Port',
|
||||
serialPortPlaceholder: 'Please enter the serial port',
|
||||
baudrate: 'Baud rate',
|
||||
parity: 'Parity',
|
||||
parityNone: 'None',
|
||||
parityEven: 'Even',
|
||||
parityOdd: 'Odd',
|
||||
flowControl: 'Flow control',
|
||||
flowControlNone: 'None',
|
||||
flowControlSoft: 'Soft',
|
||||
flowControlHard: 'Hard',
|
||||
dataBits: 'Data bits',
|
||||
stopBits: 'Stop bits',
|
||||
confirm: 'Ok'
|
||||
},
|
||||
wol: {
|
||||
@@ -150,10 +174,16 @@ const en = {
|
||||
},
|
||||
power: {
|
||||
title: 'Power',
|
||||
showConfirm: 'Confirmation',
|
||||
showConfirmTip: 'Power operations require an extra confirmation',
|
||||
reset: 'Reset',
|
||||
power: 'Power',
|
||||
powerShort: 'Power (short click)',
|
||||
powerLong: 'Power (long click)'
|
||||
powerLong: 'Power (long click)',
|
||||
resetConfirm: 'Proceed reset operation?',
|
||||
powerConfirm: 'Proceed power operation?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
@@ -167,14 +197,23 @@ const en = {
|
||||
image: 'Image Version',
|
||||
imageTip: 'NanoKVM system image version',
|
||||
deviceKey: 'Device Key',
|
||||
community: 'Community'
|
||||
community: 'Community',
|
||||
hostname: 'Hostname',
|
||||
hostnameUpdated: 'Hostname updated. Reboot to apply.',
|
||||
ipType: {
|
||||
Wired: 'Wired',
|
||||
Wireless: 'Wireless',
|
||||
Other: 'Other'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar'
|
||||
menuBarDesc: 'Display icons in the menu bar',
|
||||
webTitle: 'Web Title',
|
||||
webTitleDesc: 'Customize the web page title'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
@@ -200,10 +239,32 @@ const en = {
|
||||
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!"
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: 'Mouse Jiggler',
|
||||
description: 'Prevent the remote host from sleeping',
|
||||
disable: 'Disable',
|
||||
absolute: 'Absolute Mode',
|
||||
relative: 'Relative Mode'
|
||||
},
|
||||
mdns: {
|
||||
description: 'Enable mDNS discovery service',
|
||||
tip: "Turning it off if it's not needed"
|
||||
},
|
||||
hdmi: {
|
||||
description: 'Enable HDMI/monitor output'
|
||||
},
|
||||
hidOnly: 'HID-Only Mode',
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
@@ -258,7 +319,11 @@ const en = {
|
||||
available: 'An update is available. Are you sure you want to update now?',
|
||||
updating: 'Update started. Please wait...',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel'
|
||||
cancel: 'Cancel',
|
||||
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!'
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
|
||||
@@ -40,9 +40,9 @@ const ko = {
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Wi-Fi 설정 for NanoKVM',
|
||||
description: 'NanoKVM Wi-Fi 설정',
|
||||
success: 'NanoKVM의 네트워크 상태를 확인하고 새 IP 주소로 접속하세요.',
|
||||
failed: '작업이 실패했습니다. 다시 시도하세요.',
|
||||
failed: '작업에 실패했습니다. 다시 시도하세요.',
|
||||
confirmBtn: '확인',
|
||||
finishBtn: '완료'
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,16 @@ const pl = {
|
||||
serialPort: 'Port szeregowy',
|
||||
serialPortPlaceholder: 'Wprowadź port szeregowy',
|
||||
baudrate: 'Szybkość transmisji',
|
||||
parity: 'Kontrola parzystości',
|
||||
parityNone: 'Brak kontroli',
|
||||
parityEven: 'Parzystość',
|
||||
parityOdd: 'Nieparzystość',
|
||||
flowControl: 'Kontrola przepływu',
|
||||
flowControlNone: 'Brak kontroli',
|
||||
flowControlSoft: 'Miękka',
|
||||
flowControlHard: 'Twarda',
|
||||
dataBits: 'Bity danych',
|
||||
stopBits: 'Bity stopu',
|
||||
confirm: 'Ok'
|
||||
},
|
||||
wol: {
|
||||
|
||||
@@ -14,8 +14,7 @@ const ru = {
|
||||
placeholderPassword2: 'Введите пароль снова',
|
||||
noEmptyUsername: 'Имя пользователя не может быть пустым',
|
||||
noEmptyPassword: 'Пароль не может быть пустым',
|
||||
noAccount:
|
||||
'Не удалось получить информацию о пользователе, пожалуйста, обновите веб-страницу или сбросьте пароль',
|
||||
noAccount: 'Не удалось получить информацию о пользователе, пожалуйста, обновите веб-страницу или сбросьте пароль',
|
||||
invalidUser: 'Неверное имя пользователя или пароль',
|
||||
error: 'Непредвиденная ошибка',
|
||||
changePassword: 'Изменить пароль',
|
||||
@@ -48,7 +47,9 @@ const ru = {
|
||||
finishBtn: 'Готово'
|
||||
},
|
||||
screen: {
|
||||
title: 'Экран',
|
||||
video: 'Видеорежим',
|
||||
videoDirectTips: 'Включите HTTPS в "Настройки > Устройство", чтобы использовать этот режим',
|
||||
resolution: 'Разрешение',
|
||||
auto: 'Автоматическое',
|
||||
autoTips:
|
||||
@@ -66,6 +67,7 @@ const ru = {
|
||||
resetHdmi: 'Перезагрузить HDMI подсистему'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Клавиатура',
|
||||
paste: 'Вставка текста',
|
||||
tips: 'Поддерживаются только стандартные буквы и символы клавиатуры',
|
||||
placeholder: 'Текст для ввода',
|
||||
@@ -74,6 +76,8 @@ const ru = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Мышь',
|
||||
cursor: 'Стиль курсора',
|
||||
default: 'Курсор по умолчанию',
|
||||
pointer: 'Курсор: указатель',
|
||||
cell: 'Курсор: крест',
|
||||
@@ -84,7 +88,16 @@ const ru = {
|
||||
absolute: 'Абсолютный режим',
|
||||
relative: 'Относительный режим',
|
||||
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: 'Образы',
|
||||
@@ -94,6 +107,7 @@ const ru = {
|
||||
mountFailed: 'Монтирование образа не удалось',
|
||||
mountDesc:
|
||||
'В некоторых системах необходимо отсоединить виртуальный диск на удаленном хосте перед монтированием образа.',
|
||||
refresh: 'Обновить список образов',
|
||||
tips: {
|
||||
title: 'Как загрузить',
|
||||
usb1: 'Подключите NanoKVM к вашему компьютеру через USB.',
|
||||
@@ -130,6 +144,16 @@ const ru = {
|
||||
serialPort: 'COM-порт',
|
||||
serialPortPlaceholder: 'Введите COM-порт',
|
||||
baudrate: 'Скорость передачи',
|
||||
parity: 'Чётность',
|
||||
parityNone: 'Нет',
|
||||
parityEven: 'Чётная',
|
||||
parityOdd: 'Нечётная',
|
||||
flowControl: 'Управление потоком',
|
||||
flowControlNone: 'Нет',
|
||||
flowControlSoft: 'Программное',
|
||||
flowControlHard: 'Аппаратное',
|
||||
dataBits: 'Бит данных',
|
||||
stopBits: 'Стоп-бит',
|
||||
confirm: 'ОК'
|
||||
},
|
||||
wol: {
|
||||
@@ -147,10 +171,16 @@ const ru = {
|
||||
},
|
||||
power: {
|
||||
title: 'Питание',
|
||||
showConfirm: 'Подтверждение',
|
||||
showConfirmTip: 'Если включено, операции с питанием потребуют дополнительного подтверждения',
|
||||
reset: 'Экстренная перезагрузка',
|
||||
power: 'Питание',
|
||||
powerShort: 'Питание (короткое нажатие)',
|
||||
powerLong: 'Питание (длинное нажатие)'
|
||||
powerLong: 'Питание (длинное нажатие)',
|
||||
resetConfirm: 'Вы уверены, что хотите выполнить экстренную перезагрузку?',
|
||||
powerConfirm: 'Вы уверены, что хотите нажать кнопку питания?',
|
||||
okBtn: 'Да',
|
||||
cancelBtn: 'Нет'
|
||||
},
|
||||
settings: {
|
||||
title: 'Настройки',
|
||||
@@ -164,14 +194,23 @@ const ru = {
|
||||
image: 'Версия прошивки',
|
||||
imageTip: 'Версия системного образа NanoKVM',
|
||||
deviceKey: 'Ключ устройства',
|
||||
community: 'Сообщество'
|
||||
community: 'Сообщество',
|
||||
hostname: 'Имя хоста',
|
||||
hostnameUpdated: 'Имя хоста изменено. Перезагрузите NanoKVM, чтобы настройки вступили в силу.',
|
||||
ipType: {
|
||||
'Wired': 'Проводное',
|
||||
'Wireless': 'Беспроводное',
|
||||
'Other': 'Другое'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Внешний вид',
|
||||
display: 'Отображение',
|
||||
language: 'Язык',
|
||||
menuBar: 'Панель меню',
|
||||
menuBarDesc: 'Иконки для отображения в панели меню'
|
||||
menuBarDesc: 'Иконки для отображения в панели меню',
|
||||
webTitle: 'Заголовок страницы',
|
||||
webTitleDesc: 'Изменить заголовок страницы'
|
||||
},
|
||||
device: {
|
||||
title: 'Устройство',
|
||||
@@ -197,10 +236,37 @@ const ru = {
|
||||
description: 'Включить удаленный доступ по SSH',
|
||||
tip: 'Задайте сильный пароль перед включением (Аккаунт - Пароль - Обновить)'
|
||||
},
|
||||
tls: {
|
||||
description: 'Включить HTTPS Protocol',
|
||||
tip: 'Имейте в виду: использование HTTPS может увеличить задержку, особенно в режиме видео MJPEG.'
|
||||
},
|
||||
advanced: 'Дополнительные настройки',
|
||||
swap: {
|
||||
title: 'Файл подкачки',
|
||||
disable: 'Отключить',
|
||||
description: 'Задать размер файла подкачки',
|
||||
tip: 'Включение этой опции может негативно сказаться на долговечности SD-карты!',
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: 'Дрожалка для мыши',
|
||||
description: 'Включите данную опцию, если хост засыпает',
|
||||
disable: 'Отключить',
|
||||
absolute: 'Абсолютный режим',
|
||||
relative: 'Относительный режим'
|
||||
},
|
||||
mdns: {
|
||||
description: 'Включить сервис обнаружения mDNS',
|
||||
tip: "Выключите, если нет необходимости"
|
||||
},
|
||||
hidOnly: 'Режим только HID',
|
||||
disk: 'Виртуальный диск',
|
||||
diskDesc: 'Смонтировать виртуальный USB диск на удаленном хосте',
|
||||
network: 'Виртуальная сеть',
|
||||
networkDesc: 'Смонтировать виртуальное сетевое устройство на удаленном хосте'
|
||||
networkDesc: 'Смонтировать виртуальное сетевое устройство на удаленном хосте',
|
||||
reboot: 'Перезагрузка',
|
||||
rebootDesc: 'Вы уверены, что хотите перезагрузить NanoKVM?',
|
||||
okBtn: 'Да',
|
||||
cancelBtn: 'Нет'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
@@ -224,7 +290,8 @@ const ru = {
|
||||
upTailscale: 'Переместите tailscale в каталог /usr/bin/ на NanoKVM',
|
||||
upTailscaled: 'Переместите tailscaled в каталог /usr/sbin/ на NanoKVM',
|
||||
refresh: 'Обновите текущую страницу',
|
||||
notLogin: 'Устройство не привязано. Войдите, чтобы привязать его к аккаунту.',
|
||||
notLogin:
|
||||
'Устройство не привязано. Войдите, чтобы привязать его к аккаунту.',
|
||||
urlPeriod: 'Этот адрес действителен в течение 10 минут',
|
||||
login: 'Войти',
|
||||
loginSuccess: 'Вход выполнен',
|
||||
@@ -233,7 +300,7 @@ const ru = {
|
||||
deviceIP: 'IP адрес устройства',
|
||||
account: 'Аккаунт',
|
||||
logout: 'Выход',
|
||||
logout2: 'Вы действительно хотите выйти?',
|
||||
logoutDesc: 'Вы действительно хотите выйти?',
|
||||
uninstall: 'Удалить Tailscale',
|
||||
okBtn: 'Да',
|
||||
cancelBtn: 'Нет'
|
||||
@@ -246,19 +313,33 @@ const ru = {
|
||||
available: 'Доступно обновление. Вы уверены, что хотите обновить?',
|
||||
updating: 'Начато обновление. Пожалуйста, подождите...',
|
||||
confirm: 'Подтвердить',
|
||||
cancel: 'Отмена'
|
||||
cancel: 'Отмена',
|
||||
preview: 'Ранние обновления',
|
||||
previewDesc: 'Получайте ранний доступ к новым функциям и улучшениям',
|
||||
previewTip:
|
||||
'Обратите внимание: в ранних версиях могут быть ошибки или незавершённый функционал!'
|
||||
},
|
||||
account: {
|
||||
title: 'Аккаунт',
|
||||
webAccount: 'Имя веб-аккаунта',
|
||||
password: 'Пароль',
|
||||
updateBtn: 'Обновить',
|
||||
logoutBtn: 'Выйти'
|
||||
logoutBtn: 'Выйти',
|
||||
logoutDesc: 'Вы действительно хотите выйти?',
|
||||
okBtn: 'Да',
|
||||
cancelBtn: 'Нет'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: "У нас возникла проблема",
|
||||
refresh: 'Обновить страницу'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'Полноэкранный режим'
|
||||
},
|
||||
menu: {
|
||||
collapse: 'Свернуть меню',
|
||||
expand: 'Развернуть меню'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const en = {
|
||||
const th = {
|
||||
translation: {
|
||||
head: {
|
||||
desktop: 'Remote Desktop',
|
||||
@@ -83,7 +83,16 @@ const en = {
|
||||
absolute: 'โหมดสัมบูรณ์',
|
||||
relative: 'โหมดสัมพัทธ์',
|
||||
requestPointer: 'กำลังใช้โหมดสัมพัทธ์ กรุณาคลิกที่เดสก์ท็อปเพื่อรับตัวชี้เมาส์',
|
||||
resetHid: 'รีเซ็ต HID'
|
||||
resetHid: 'รีเซ็ต HID',
|
||||
hidOnly: {
|
||||
title: 'โหมด HID-Only',
|
||||
desc: 'หากเมาส์และคีย์บอร์ดของคุณหยุดตอบสนองและการรีเซ็ต HID ไม่ช่วย อาจเป็นปัญหาความเข้ากันได้ระหว่าง NanoKVM และอุปกรณ์ ลองเปิดโหมด HID-Only เพื่อความเข้ากันได้ที่ดีขึ้น',
|
||||
tip1: 'การเปิดโหมด HID-Only จะปิดการทำงานดิสก์จำลองและ NIC จำลอง',
|
||||
tip2: 'ในโหมด HID-Only ดิสก์จะลองจะถูกปิดใช้งาน',
|
||||
tip3: 'NanoKVM จะรีบูตอัตโนมัติหลังจากเปลี่ยนโหมด',
|
||||
enable: 'เปิดใช้งานโหมด HID-Only',
|
||||
disable: 'ปิดใช้งานโหมด HID-Only'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: 'ดิสก์จำลอง',
|
||||
@@ -128,7 +137,17 @@ const en = {
|
||||
serialPort: 'Serial Port',
|
||||
serialPortPlaceholder: 'กรุณาเลือก serial port',
|
||||
baudrate: 'Baud rate',
|
||||
confirm: 'ยืนยัน'
|
||||
confirm: 'ยืนยัน',
|
||||
parity: 'Parity',
|
||||
parityNone: 'ไม่มี',
|
||||
parityEven: 'คู่',
|
||||
parityOdd: 'คี่',
|
||||
flowControl: 'การควบคุมการไหล',
|
||||
flowControlNone: 'ไม่มี',
|
||||
flowControlSoft: 'ซอฟต์',
|
||||
flowControlHard: 'ฮาร์ด',
|
||||
dataBits: 'บิตข้อมูล',
|
||||
stopBits: 'บิตหยุด'
|
||||
},
|
||||
wol: {
|
||||
title: 'Wake-on-LAN',
|
||||
@@ -244,18 +263,34 @@ const en = {
|
||||
available: 'มีการอัปเดตใหม่ คุณแน่ใจที่จะอัปเดตหรือไม่?',
|
||||
updating: 'กำลังเริ่มอัปเดต กรุณารอสักครู่...',
|
||||
confirm: 'ยืนยัน',
|
||||
cancel: 'ยกเลิก'
|
||||
cancel: 'ยกเลิก',
|
||||
preview: 'ดูตัวอย่างการอัปเดต',
|
||||
previewDesc: 'เข้าถึงฟีเจอร์และการปรับปรุงใหม่ก่อนใคร',
|
||||
previewTip: 'โปรดทราบว่าการเผยแพร่ตัวอย่างอาจมีข้อบกพร่องหรือฟังก์ชันการทำงานที่ไม่สมบูรณ์!'
|
||||
},
|
||||
account: {
|
||||
title: 'บัญชี',
|
||||
webAccount: 'ชื่อผู้ใช้',
|
||||
password: 'รหัสผ่าน',
|
||||
updateBtn: 'ยืนยัน',
|
||||
logoutBtn: 'ออกจากระบบ'
|
||||
logoutBtn: 'ออกจากระบบ',
|
||||
logoutDesc: 'คุณแน่ใจที่จะออกจากระบบหรือไม่?',
|
||||
okBtn: 'ใช่',
|
||||
cancelBtn: 'ไม่ใช่'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: 'เราเจอปัญหา',
|
||||
refresh: 'รีเฟรช'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'โหมดเต็มหน้าจอ'
|
||||
},
|
||||
menu: {
|
||||
collapse: 'ย่อเมนู',
|
||||
expand: 'ขยายเมนู'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
export default th;
|
||||
|
||||
@@ -10,46 +10,46 @@ const uk = {
|
||||
auth: {
|
||||
login: 'Вхід',
|
||||
placeholderUsername: "Введіть ім'я користувача",
|
||||
placeholderPassword: 'введіть пароль',
|
||||
placeholderPassword2: 'введіть пароль ще раз',
|
||||
noEmptyUsername: "ім'я користувача не може бути порожнім",
|
||||
noEmptyPassword: 'пароль не може бути порожнім',
|
||||
noAccount:
|
||||
'Не вдалося отримати інформацію про користувача, оновіть веб-сторінку або скиньте пароль',
|
||||
invalidUser: "недійсне ім'я користувача або пароль",
|
||||
error: 'якась халепа! непередбачена помилка :(',
|
||||
placeholderPassword: 'Введіть пароль',
|
||||
placeholderPassword2: 'Введіть пароль ще раз',
|
||||
noEmptyUsername: "Ім'я користувача не може бути порожнім",
|
||||
noEmptyPassword: 'Пароль не може бути порожнім',
|
||||
noAccount: 'Не вдалося отримати інформацію про користувача, оновіть веб-сторінку або скиньте пароль',
|
||||
invalidUser: "Недійсне ім'я користувача або пароль",
|
||||
error: 'Якась халепа! Непередбачена помилка :(',
|
||||
changePassword: 'Змінити пароль',
|
||||
changePasswordDesc:
|
||||
'Для безпеки вашого пристрою, будь ласка, змініть пароль для входу в веб-інтерфейс.',
|
||||
differentPassword: 'паролі не збігаються',
|
||||
illegalUsername: "ім'я користувача містить недопустимі символи",
|
||||
illegalPassword: 'пароль містить недопустимі символи',
|
||||
changePasswordDesc: 'Для безпеки вашого пристрою, будь ласка, змініть пароль для входу в веб-інтерфейс.',
|
||||
differentPassword: 'Паролі не збігаються',
|
||||
illegalUsername: "Ім'я користувача містить недопустимі символи",
|
||||
illegalPassword: 'Пароль містить недопустимі символи',
|
||||
forgetPassword: 'Забули пароль',
|
||||
ok: 'Ок',
|
||||
cancel: 'Скасувати',
|
||||
loginButtonText: 'Увійти',
|
||||
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.'
|
||||
'Щоб скинути паролі, натискаючи та тримаючи кнопку завантаження на Nanokvm протягом 10 секунд.',
|
||||
reset2: 'Щоб отримати детальні кроки, зверніться до цього документа:',
|
||||
reset3: 'Обліковий запис вебінтерфейс за замовчуванням:',
|
||||
reset4: 'Обліковий запис SSH за замовчуванням:',
|
||||
change1: 'Зверніть увагу, що ця дія змінить наступні паролі:',
|
||||
change2: 'Пароль для входу у веб-інтерфейс',
|
||||
change3: 'Системний root пароль (пароль для входу в систему по SSH)',
|
||||
change4: 'Щоб скинути паролі, натисніть та утримуйте кнопку "Завантаження" на 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: 'Налаштування Wi-Fi для Nanokvm',
|
||||
success: 'Будь ласка, перевірте статус мережі NANOKVM та відвідайте нову IP -адресу.',
|
||||
failed: 'Операція не вдалася, спробуйте ще раз.',
|
||||
confirmBtn: 'Так',
|
||||
finishBtn: 'Готово'
|
||||
},
|
||||
screen: {
|
||||
title: 'Екран',
|
||||
video: 'Відеорежим',
|
||||
videoDirectTips: 'Увімкніть HTTPS у "Налаштування > Пристрій", щоб використовувати цей режим',
|
||||
resolution: 'Роздільна здатність',
|
||||
auto: 'Автоматично',
|
||||
autoTips:
|
||||
@@ -64,9 +64,10 @@ const uk = {
|
||||
frameDetect: 'Виявлення кадрів',
|
||||
frameDetectTip:
|
||||
'Обчислює різницю між кадрами. Зупиняє передачу відеопотоку, коли на екрані віддаленого хоста не виявлено змін.',
|
||||
resetHdmi: 'Reset HDMI'
|
||||
resetHdmi: 'Перезавантажити HDMI підсистему'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Клавіатура',
|
||||
paste: 'Вставити',
|
||||
tips: 'Підтримуються лише стандартні літери та символи клавіатури',
|
||||
placeholder: 'Будь ласка, введіть',
|
||||
@@ -75,6 +76,8 @@ const uk = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Миша',
|
||||
cursor: 'Стиль курсору',
|
||||
default: 'Курсор за замовчуванням',
|
||||
pointer: 'Курсор-стрілка',
|
||||
cell: 'Курсор-таблиця',
|
||||
@@ -84,17 +87,27 @@ const uk = {
|
||||
mode: 'Режим миші',
|
||||
absolute: 'Абсолютний режим',
|
||||
relative: 'Відносний режим',
|
||||
requestPointer:
|
||||
'Використовується відносний режим. Будь ласка, натисніть на робочий стіл, щоб отримати курсор миші.',
|
||||
resetHid: 'Скинути HID'
|
||||
requestPointer: 'Використовується відносний режим. Будь ласка, натисніть на робочий стіл, щоб отримати курсор миші.',
|
||||
resetHid: 'Скинути HID',
|
||||
hidOnly: {
|
||||
title: 'Режим лише HID',
|
||||
desc: 'Якщо ваша миша та клавіатура перестають відповідати та скинути HID, це не допомагає, це може бути проблемою сумісності між NanoKVM та пристроєм. Спробуйте ввімкнути режим "лише HID" лише для кращої сумісності.',
|
||||
tip1: 'Увімкнення режиму "лише HID" демонтує віртуальний U-диск і віртуальну мережу',
|
||||
tip2: 'У режимі "лише HID" монтування образів буде вимкнено',
|
||||
tip3: 'NANOKVM автоматично перезавантажиться після перемикання режимів',
|
||||
enable: 'Увімкнути режим "лише HID"',
|
||||
disable: 'Ввимкнути режим "лише HID"'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: 'Зображення',
|
||||
title: 'Образи',
|
||||
loading: 'Завантаження...',
|
||||
empty: 'Нічого не знайдено',
|
||||
mountFailed: 'Не вдалося змонтувати',
|
||||
cdrom: 'Встановіть образ в режимі CD-ROM',
|
||||
mountFailed: 'Не вдалося змонтувати образ у режимі CD-ROM',
|
||||
mountDesc:
|
||||
'У деяких системах необхідно витягнути віртуальний диск на віддаленому хості перед монтуванням файлу образа.',
|
||||
refresh: 'Оновіть список образів',
|
||||
tips: {
|
||||
title: 'Як завантажити',
|
||||
usb1: "Під'єднайте NanoKVM до вашого комп'ютера через USB.",
|
||||
@@ -112,7 +125,7 @@ const uk = {
|
||||
}
|
||||
},
|
||||
script: {
|
||||
title: 'Скрипт',
|
||||
title: 'Скрипти',
|
||||
upload: 'Завантажити',
|
||||
run: 'Запустити',
|
||||
runBackground: 'Запустити у фоновому режимі',
|
||||
@@ -131,6 +144,16 @@ const uk = {
|
||||
serialPort: 'Послідовний порт',
|
||||
serialPortPlaceholder: 'Будь ласка, введіть послідовний порт',
|
||||
baudrate: 'Швидкість передачі',
|
||||
parity: 'Парність',
|
||||
parityNone: 'Без парності',
|
||||
parityEven: 'Парний',
|
||||
parityOdd: 'Непарний',
|
||||
flowControl: 'Керування потоком',
|
||||
flowControlNone: 'Без керування',
|
||||
flowControlSoft: "М'яке керування",
|
||||
flowControlHard: 'Жорстке керування',
|
||||
dataBits: 'Біти даних',
|
||||
stopBits: 'Стопові біти',
|
||||
confirm: 'Ок'
|
||||
},
|
||||
wol: {
|
||||
@@ -140,71 +163,123 @@ const uk = {
|
||||
input: 'Будь ласка, введіть MAC',
|
||||
ok: 'Ок'
|
||||
},
|
||||
download: {
|
||||
title: 'Завантажувач образів',
|
||||
input: 'Введіть URL-адресу віддаленого образу',
|
||||
ok: 'Так',
|
||||
disabled: 'Розділ даних /data у режимі лише для читання, тому ми не можемо завантажити образ'
|
||||
},
|
||||
power: {
|
||||
title: 'Живлення',
|
||||
showConfirm: 'Підтвердження',
|
||||
showConfirmTip: 'Енергетичні операції потребують додаткового підтвердження',
|
||||
reset: 'Скидання',
|
||||
power: 'Живлення',
|
||||
powerShort: 'Живлення (коротке натискання)',
|
||||
powerLong: 'Живлення (довге натискання)'
|
||||
powerLong: 'Живлення (довге натискання)',
|
||||
resetConfirm: 'Продовжити роботу зі скиданням?',
|
||||
powerConfirm: 'Продовжувати роботу живлення?',
|
||||
okBtn: 'Так',
|
||||
cancelBtn: 'Ні'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
title: 'Налаштування',
|
||||
about: {
|
||||
title: 'Про NanoKVM',
|
||||
information: 'Інформація',
|
||||
ip: 'IP',
|
||||
mdns: 'mDNS',
|
||||
application: 'Версія додатку',
|
||||
applicationTip: 'NanoKVM web application version',
|
||||
application: 'Версія додатка',
|
||||
applicationTip: 'Версія веб-додатка NanoKVM',
|
||||
image: 'Версія образу',
|
||||
imageTip: 'NanoKVM system image version',
|
||||
imageTip: 'Версія системного образу NanoKVM',
|
||||
deviceKey: 'Ключ пристрою',
|
||||
community: 'Спільнота'
|
||||
community: 'Спільнота',
|
||||
hostname: "Ім'я вузла",
|
||||
hostnameUpdated: "Ім'я вузла оновлено. Перезавантажте систему, щоб застосувати зміни.",
|
||||
ipType: {
|
||||
Wired: 'Дротове',
|
||||
Wireless: 'Бездротове',
|
||||
Other: 'Інший'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar'
|
||||
title: 'Зовнішній вигляд',
|
||||
display: 'Відображення',
|
||||
language: 'Мова',
|
||||
menuBar: 'Панель меню',
|
||||
menuBarDesc: 'Піктограми відображення в панелі меню',
|
||||
webTitle: 'Веб-назва',
|
||||
webTitleDesc: 'Налаштуйте назву веб-сторінки'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
title: 'Пристрій',
|
||||
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'
|
||||
title: 'OLED-екран',
|
||||
description: 'Автоматичне відключення OLED-екрана',
|
||||
0: 'Ніколи',
|
||||
15: '15 сек',
|
||||
30: '30 сек',
|
||||
60: '1 хв',
|
||||
180: '3 хв',
|
||||
300: '5 хв',
|
||||
600: '10 хв',
|
||||
1800: '30 хв',
|
||||
3600: '1 година'
|
||||
},
|
||||
wifi: {
|
||||
title: 'Wi-Fi',
|
||||
description: 'Configure Wi-Fi',
|
||||
setBtn: 'Config'
|
||||
description: 'Налаштування Wi-Fi',
|
||||
setBtn: 'Конфігурація'
|
||||
},
|
||||
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: 'Увімкніть віддалений доступ по SSH',
|
||||
tip: 'Перед ввімкненням встановіть надійний пароль (Обліковий запис - Зміна пароля)'
|
||||
},
|
||||
tls: {
|
||||
description: 'Увімкнути протокол HTTPS',
|
||||
tip: 'Будьте в курсі: Використання HTTPS може збільшити затримку, особливо в режимі відео MJPEG.'
|
||||
},
|
||||
advanced: 'Розширені налаштування',
|
||||
swap: {
|
||||
title: 'Swap (файл підкачки)',
|
||||
disable: 'Відключено',
|
||||
description: 'Встановіть розмір файлу свопу',
|
||||
tip: "Увімкнення цієї опції може вивести з ладу SD карту!"
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: 'Налаштування пацюка',
|
||||
description: 'Не дозволяйте віддаленому хосту спати',
|
||||
disable: 'Відключено',
|
||||
absolute: 'Абсолютний режим',
|
||||
relative: 'Відносний режим'
|
||||
},
|
||||
mdns: {
|
||||
description: 'Увімкнути службу MDNS Discovery',
|
||||
tip: "Вимкнути, якщо це не потрібно"
|
||||
},
|
||||
hidOnly: 'Режим лише HID',
|
||||
disk: 'Віртуальний диск',
|
||||
diskDesc: 'Монтувати віртуальний U-диск на віддаленому хості',
|
||||
network: 'Віртуальна мережа',
|
||||
networkDesc: 'Встановити віртуальну мережеву карту на віддаленому хості',
|
||||
reboot: 'Перезавантажити',
|
||||
rebootDesc: 'Ви впевнені, що хочете перезавантажити Nanokvm?',
|
||||
okBtn: 'Так',
|
||||
cancelBtn: 'Ні'
|
||||
},
|
||||
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: "Оптимізація пам'яті",
|
||||
tip: "Коли використання пам’яті перевищує обмеження, збирання сміття проводиться більш агресивно, щоб спробувати звільнити пам’ять. Рекомендується встановити 50 Мб, якщо використовувати Tailscale. Після зміни налаштувань потрібно перезавантажити Tailscale.",
|
||||
disable: 'Відключено'
|
||||
},
|
||||
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: 'Ви впевнені, що бажаєте перезавантажити Tailscale?',
|
||||
stop: 'Ви впевнені, що бажаєте зупинити Tailscale?',
|
||||
stopDesc: 'Вийдіть з Tailscale і вимкніть автоматичний запуск при завантаженні.',
|
||||
loading: 'Завантаження...',
|
||||
notInstall: 'Tailscale не знайдено! Будь ласка, встановіть клієнт Tailscale.',
|
||||
notInstall: 'Tailscale не знайдено! Будь ласка, встановіть його.',
|
||||
install: 'Встановити',
|
||||
installing: 'Встановлення',
|
||||
failed: 'Не вдалося встановити',
|
||||
@@ -212,8 +287,8 @@ const uk = {
|
||||
download: 'Завантажте',
|
||||
package: 'пакет встановлення',
|
||||
unzip: 'та розпакуйте його',
|
||||
upTailscale: 'Завантажте tailscale до каталогу /usr/bin/ на NanoKVM',
|
||||
upTailscaled: 'Завантажте tailscaled до каталогу /usr/sbin/ на NanoKVM',
|
||||
upTailscale: 'Завантажте Tailscale до каталогу /usr/bin/ на NanoKVM',
|
||||
upTailscaled: 'Завантажте Tailscaled до каталогу /usr/sbin/ на NanoKVM',
|
||||
refresh: 'Оновіть поточну сторінку',
|
||||
notLogin:
|
||||
"Пристрій ще не прив'язаний. Будь ласка, увійдіть і прив'яжіть цей пристрій до вашого облікового запису.",
|
||||
@@ -225,9 +300,10 @@ const uk = {
|
||||
deviceIP: 'IP пристрою',
|
||||
account: 'Обліковий запис',
|
||||
logout: 'Вийти',
|
||||
logout2: 'Ви впевнені, що хочете вийти?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
logoutDesc: 'Ви впевнені, що хочете вийти?',
|
||||
uninstall: 'Видалити Tailscale',
|
||||
okBtn: 'Так',
|
||||
cancelBtn: 'Ні'
|
||||
},
|
||||
update: {
|
||||
title: 'Перевірити оновлення',
|
||||
@@ -237,15 +313,33 @@ const uk = {
|
||||
available: 'Доступне оновлення. Ви впевнені, що хочете оновити?',
|
||||
updating: 'Оновлення розпочато. Будь ласка, зачекайте...',
|
||||
confirm: 'Підтвердити',
|
||||
cancel: 'Скасувати'
|
||||
cancel: 'Скасувати',
|
||||
preview: 'О бета релізах',
|
||||
previewDesc: 'Отримайте ранній доступ до нових функцій та вдосконалень',
|
||||
previewTip:
|
||||
'Будь ласка, майте на увазі, що випуски бета релізів можуть містити помилки або неповну функціональність!'
|
||||
},
|
||||
account: {
|
||||
title: 'Account',
|
||||
webAccount: 'Web Account Name',
|
||||
password: 'Password',
|
||||
updateBtn: 'Update',
|
||||
logoutBtn: 'Logout'
|
||||
title: 'Обліковий запис',
|
||||
webAccount: "Ім'я облыкового запису у веб-інтерфейсі",
|
||||
password: 'Пароль',
|
||||
updateBtn: 'Зміна',
|
||||
logoutBtn: 'Вийти',
|
||||
logoutDesc: 'Ви впевнені, що хочете вийти?',
|
||||
okBtn: 'Так',
|
||||
cancelBtn: 'Ні'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: "Ми зіткнулися з проблемою",
|
||||
refresh: 'Оновити'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'Перемикатися на повному екрані'
|
||||
},
|
||||
menu: {
|
||||
collapse: 'Згорнути меню',
|
||||
expand: 'Розгорнути меню'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +46,9 @@ const zh = {
|
||||
finishBtn: '完成'
|
||||
},
|
||||
screen: {
|
||||
title: '屏幕',
|
||||
video: '视频模式',
|
||||
videoDirectTips: '该模式需启用 HTTPS,请前往「设置 - 设备」中开启',
|
||||
resolution: '分辨率',
|
||||
auto: '自动',
|
||||
autoTips:
|
||||
@@ -63,6 +65,7 @@ const zh = {
|
||||
resetHdmi: '重置 HDMI'
|
||||
},
|
||||
keyboard: {
|
||||
title: '键盘',
|
||||
paste: '粘贴',
|
||||
tips: '仅支持标准键盘的字母和符号',
|
||||
placeholder: '请输入内容',
|
||||
@@ -71,7 +74,9 @@ const zh = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
default: '默认指针',
|
||||
title: '鼠标',
|
||||
cursor: '光标样式',
|
||||
default: '默认光标',
|
||||
pointer: '悬浮指针',
|
||||
cell: '单元指针',
|
||||
text: '文本指针',
|
||||
@@ -80,8 +85,20 @@ const zh = {
|
||||
mode: '鼠标模式',
|
||||
absolute: '绝对模式',
|
||||
relative: '相对模式',
|
||||
speed: '滚轮速度',
|
||||
fast: '快',
|
||||
slow: '慢',
|
||||
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
|
||||
resetHid: '重置 HID'
|
||||
resetHid: '重置 HID',
|
||||
hidOnly: {
|
||||
title: 'HID-Only 模式',
|
||||
desc: '若使用过程中遇到鼠标键盘无响应,且重置 HID 无效,可能是 NanoKVM 与您的设备存在兼容性问题。建议尝试启用 HID-Only 模式以提升兼容性。',
|
||||
tip1: '启用该模式会禁用虚拟U盘和虚拟网卡',
|
||||
tip2: '该模式下无法使用镜像挂载功能',
|
||||
tip3: '切换模式后将自动重启 NanoKVM',
|
||||
enable: '启用 HID-Only 模式',
|
||||
disable: '关闭 HID-Only 模式'
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title: '镜像',
|
||||
@@ -143,10 +160,16 @@ const zh = {
|
||||
},
|
||||
power: {
|
||||
title: '电源',
|
||||
showConfirm: '显示确认框',
|
||||
showConfirmTip: '电源操作需要二次确认',
|
||||
reset: '重启',
|
||||
power: '电源',
|
||||
powerShort: '电源(短按)',
|
||||
powerLong: '电源(长按)'
|
||||
powerLong: '电源(长按)',
|
||||
resetConfirm: '确认执行重启操作吗?',
|
||||
powerConfirm: '确认执行电源操作吗?',
|
||||
okBtn: '确认',
|
||||
cancelBtn: '取消'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
@@ -160,14 +183,18 @@ const zh = {
|
||||
image: '镜像版本',
|
||||
imageTip: 'NanoKVM 系统镜像版本',
|
||||
deviceKey: '设备码',
|
||||
community: '社区'
|
||||
community: '社区',
|
||||
hostname: '主机名',
|
||||
hostnameUpdated: '主机名修改成功,重启后生效'
|
||||
},
|
||||
appearance: {
|
||||
title: '外观',
|
||||
display: '显示',
|
||||
language: '语言',
|
||||
menuBar: '菜单栏',
|
||||
menuBarDesc: '是否在菜单栏中显示图标'
|
||||
menuBarDesc: '是否在菜单栏中显示图标',
|
||||
webTitle: '网站标题',
|
||||
webTitleDesc: '自定义网站页面标题'
|
||||
},
|
||||
device: {
|
||||
title: '设备',
|
||||
@@ -193,14 +220,37 @@ const zh = {
|
||||
description: '启用 SSH 远程访问',
|
||||
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
|
||||
},
|
||||
tls: {
|
||||
description: '启用 HTTPS 协议',
|
||||
tip: '注意:使用 HTTPS 可能导致延迟增加,特别是在 MJPEG 视频模式下。'
|
||||
},
|
||||
advanced: '高级设置',
|
||||
swap: {
|
||||
disable: '禁用',
|
||||
description: '设置交换文件大小',
|
||||
tip: '启用该功能可能会减少SD卡使用寿命!'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: '鼠标抖动',
|
||||
description: '防止远程主机休眠',
|
||||
disable: '关闭',
|
||||
absolute: '绝对模式',
|
||||
relative: '相对模式'
|
||||
},
|
||||
mdns: {
|
||||
description: '启用 mDNS 发现服务',
|
||||
tip: '如果您未使用此功能,建议将其关闭'
|
||||
},
|
||||
hdmi: {
|
||||
description: '启用 HDMI/显示器 输出功能'
|
||||
},
|
||||
hidOnly: 'HID-Only 模式',
|
||||
disk: '虚拟U盘',
|
||||
diskDesc: '在远程主机中挂载虚拟U盘',
|
||||
network: '虚拟网卡',
|
||||
networkDesc: '在远程主机中挂载虚拟网卡'
|
||||
networkDesc: '在远程主机中挂载虚拟网卡',
|
||||
okBtn: '是',
|
||||
cancelBtn: '否'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
@@ -235,6 +285,8 @@ const zh = {
|
||||
logout: '退出',
|
||||
logoutDesc: '确定要退出吗?',
|
||||
uninstall: '卸载 Tailscale',
|
||||
reboot: '重启',
|
||||
rebootDesc: '你确定要重启 NanoKVM 吗?',
|
||||
okBtn: '确认',
|
||||
cancelBtn: '取消'
|
||||
},
|
||||
@@ -246,7 +298,10 @@ const zh = {
|
||||
available: '有新的可用版本,确定要更新吗?',
|
||||
updating: '更新中,请稍候...',
|
||||
confirm: '确定',
|
||||
cancel: '取消'
|
||||
cancel: '取消',
|
||||
preview: '预览更新',
|
||||
previewDesc: '率先体验即将推出的新功能和优化',
|
||||
previewTip: '预览版更新可能包含一些不稳定因素或未完善的功能!'
|
||||
},
|
||||
account: {
|
||||
title: '帐号',
|
||||
|
||||
@@ -5,3 +5,9 @@ export const mouseStyleAtom = atom('cursor-default');
|
||||
|
||||
// mouse mode: absolute or relative
|
||||
export const mouseModeAtom = atom('absolute');
|
||||
|
||||
// mouse scroll interval (unit: ms)
|
||||
export const scrollIntervalAtom = atom(0);
|
||||
|
||||
// hid mode: normal or hid-only
|
||||
export const hidModeAtom = atom<'normal' | 'hid-only'>('normal');
|
||||
|
||||
@@ -2,7 +2,12 @@ import { atom } from 'jotai';
|
||||
|
||||
import { Resolution } from '@/types';
|
||||
|
||||
// video mode: h264 or mjpeg
|
||||
export const isHdmiEnabledAtom = atom(true);
|
||||
|
||||
// video mode
|
||||
// direct: stream H.264 over HTTP
|
||||
// h264: stream H.264 over WebRTC
|
||||
// mjpeg: stream JPEG over HTTP
|
||||
export const videoModeAtom = atom('');
|
||||
|
||||
// browser screen resolution
|
||||
|
||||
@@ -2,3 +2,6 @@ import { atom } from 'jotai';
|
||||
|
||||
// menu bar disabled items
|
||||
export const menuDisabledItemsAtom = atom<string[]>([]);
|
||||
|
||||
// web title
|
||||
export const webTitleAtom = atom('');
|
||||
|
||||
@@ -5,12 +5,17 @@ const VIDEO_MODE_KEY = 'nano-kvm-vide-mode';
|
||||
const WEB_RESOLUTION_KEY = 'nano-kvm-web-resolution';
|
||||
const FPS_KEY = 'nano-kvm-fps';
|
||||
const QUALITY_KEY = 'nano-kvm-quality';
|
||||
const GOP_KEY = 'nano-kvm-gop';
|
||||
const FRAME_DETECT_KEY = 'nano-kvm-frame-detect';
|
||||
const MOUSE_STYLE_KEY = 'nano-kvm-mouse-style';
|
||||
const MOUSE_MODE_KEY = 'nano-kvm-mouse-mode';
|
||||
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-kvm-mouse-scroll-interval';
|
||||
const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
||||
const KEYBOARD_LAYOUT_KEY = 'nano-kvm-keyboard-layout';
|
||||
const KEYBOARD_SYSTEM_KEY = 'nano-kvm-keyboard-system';
|
||||
const KEYBOARD_LANGUAGE_KEY = 'nano-kvm-keyboard-language';
|
||||
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
|
||||
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
|
||||
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
|
||||
|
||||
type ItemWithExpiry = {
|
||||
value: string;
|
||||
@@ -92,6 +97,24 @@ export function setQuality(quality: number) {
|
||||
localStorage.setItem(QUALITY_KEY, String(quality));
|
||||
}
|
||||
|
||||
export function getGop() {
|
||||
const gop = localStorage.getItem(GOP_KEY);
|
||||
return gop ? Number(gop) : null;
|
||||
}
|
||||
|
||||
export function setGop(gop: number) {
|
||||
localStorage.setItem(GOP_KEY, String(gop));
|
||||
}
|
||||
|
||||
export function getFrameDetect(): boolean {
|
||||
const enabled = localStorage.getItem(FRAME_DETECT_KEY);
|
||||
return enabled === 'true';
|
||||
}
|
||||
|
||||
export function setFrameDetect(enabled: boolean) {
|
||||
localStorage.setItem(FRAME_DETECT_KEY, String(enabled));
|
||||
}
|
||||
|
||||
export function getMouseStyle() {
|
||||
return localStorage.getItem(MOUSE_STYLE_KEY);
|
||||
}
|
||||
@@ -108,9 +131,18 @@ export function setMouseMode(mouse: string) {
|
||||
localStorage.setItem(MOUSE_MODE_KEY, mouse);
|
||||
}
|
||||
|
||||
export function getMouseScrollInterval() {
|
||||
const interval = localStorage.getItem(MOUSE_SCROLL_INTERVAL_KEY);
|
||||
return interval ? Number(interval) : null;
|
||||
}
|
||||
|
||||
export function setMouseScrollInterval(interval: number): void {
|
||||
localStorage.setItem(MOUSE_SCROLL_INTERVAL_KEY, String(interval));
|
||||
}
|
||||
|
||||
export function getSkipUpdate() {
|
||||
const skip = getWithExpiry(SKIP_UPDATE_KEY);
|
||||
return skip ? Boolean(skip) : false;
|
||||
return skip === 'true';
|
||||
}
|
||||
|
||||
export function setSkipUpdate(skip: boolean) {
|
||||
@@ -118,12 +150,20 @@ export function setSkipUpdate(skip: boolean) {
|
||||
setWithExpiry(SKIP_UPDATE_KEY, String(skip), expiry);
|
||||
}
|
||||
|
||||
export function setKeyboardLayout(layout: string) {
|
||||
localStorage.setItem(KEYBOARD_LAYOUT_KEY, layout);
|
||||
export function setKeyboardSystem(system: string) {
|
||||
localStorage.setItem(KEYBOARD_SYSTEM_KEY, system);
|
||||
}
|
||||
|
||||
export function getKeyboardLayout() {
|
||||
return localStorage.getItem(KEYBOARD_LAYOUT_KEY);
|
||||
export function getKeyboardSystem() {
|
||||
return localStorage.getItem(KEYBOARD_SYSTEM_KEY);
|
||||
}
|
||||
|
||||
export function setKeyboardLanguage(language: string) {
|
||||
localStorage.setItem(KEYBOARD_LANGUAGE_KEY, language);
|
||||
}
|
||||
|
||||
export function getKeyboardLanguage() {
|
||||
return localStorage.getItem(KEYBOARD_LANGUAGE_KEY);
|
||||
}
|
||||
|
||||
export function setSkipModifyPassword(skip: boolean) {
|
||||
@@ -133,7 +173,7 @@ export function setSkipModifyPassword(skip: boolean) {
|
||||
|
||||
export function getSkipModifyPassword() {
|
||||
const skip = getWithExpiry(SKIP_MODIFY_PASSWORD_KEY);
|
||||
return skip ? Boolean(skip) : false;
|
||||
return skip === 'true';
|
||||
}
|
||||
|
||||
export function setMenuDisabledItems(items: string[]) {
|
||||
@@ -145,3 +185,12 @@ export function getMenuDisabledItems(): string[] {
|
||||
const value = localStorage.getItem(MENU_DISABLED_ITEMS_KEY);
|
||||
return value ? JSON.parse(value) : [];
|
||||
}
|
||||
|
||||
export function getPowerConfirm() {
|
||||
const enabled = localStorage.getItem(POWER_CONFIRM_KEY);
|
||||
return enabled === 'true';
|
||||
}
|
||||
|
||||
export function setPowerConfirm(enabled: boolean) {
|
||||
localStorage.setItem(POWER_CONFIRM_KEY, String(enabled));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@ import './i18n';
|
||||
import './assets/styles/index.css';
|
||||
|
||||
const renderApp = () => {
|
||||
const themeConfig = {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
components: {
|
||||
Collapse: {
|
||||
headerPadding: 0,
|
||||
contentPadding: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Suspense
|
||||
@@ -23,7 +33,7 @@ const renderApp = () => {
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={MainError}>
|
||||
<HelmetProvider>
|
||||
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<RouterProvider router={router} />
|
||||
</ConfigProvider>
|
||||
</HelmetProvider>
|
||||
@@ -34,7 +44,7 @@ const renderApp = () => {
|
||||
};
|
||||
|
||||
if (import.meta.env.MODE === 'mocked') {
|
||||
const { worker } = await import('./mocks/browser')
|
||||
const { worker } = await import('./mocks/browser');
|
||||
worker.start().then(() => {
|
||||
return renderApp();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, ReactElement } from 'react';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,7 +11,7 @@ import { Head } from '@/components/head.tsx';
|
||||
|
||||
import { Tips } from './tips.tsx';
|
||||
|
||||
export const Login = () => {
|
||||
export const Login = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -69,8 +69,19 @@ export const Login = () => {
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={login}
|
||||
>
|
||||
<h2 className="text-center text-xl font-semibold text-neutral-100">{t('auth.login')}</h2>
|
||||
|
||||
<div className="flex flex-col items-center justify-center pb-4">
|
||||
<img
|
||||
id="logo"
|
||||
src="/sipeed.ico"
|
||||
alt="Sipeed"
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
(evt.target as HTMLImageElement).classList.add('animate-spin');
|
||||
setTimeout(() => {
|
||||
(evt.target as HTMLImageElement).classList.remove('animate-spin');
|
||||
}, 1000);
|
||||
}} />
|
||||
</div>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: t('auth.noEmptyUsername'), min: 1 }]}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getResolution, getVideoMode } from '@/lib/localstorage.ts';
|
||||
import * as storage from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
|
||||
@@ -25,11 +25,11 @@ export const Desktop = () => {
|
||||
const isKeyboardEnable = useAtomValue(isKeyboardEnableAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const cookieVideoMode = getVideoMode();
|
||||
setVideoMode(cookieVideoMode ? cookieVideoMode : window.RTCPeerConnection ? 'h264' : 'mjpeg');
|
||||
const mode = getVideoMode();
|
||||
setVideoMode(mode);
|
||||
|
||||
const cookieResolution = getResolution();
|
||||
setResolution(cookieResolution ? cookieResolution : { width: 0, height: 0 });
|
||||
const res = storage.getResolution() || { width: 0, height: 0 };
|
||||
setResolution(res);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
client.send([0]);
|
||||
@@ -42,6 +42,20 @@ export const Desktop = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
function getVideoMode() {
|
||||
const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg';
|
||||
|
||||
const cookieVideoMode = storage.getVideoMode();
|
||||
if (cookieVideoMode) {
|
||||
if (cookieVideoMode === 'direct' && !window.VideoDecoder) {
|
||||
return defaultVideoMode;
|
||||
}
|
||||
return cookieVideoMode;
|
||||
}
|
||||
|
||||
return defaultVideoMode;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={t('head.desktop')} />
|
||||
|
||||
@@ -26,6 +26,42 @@ export const KeyboardCodes: Map<string, number> = new Map([
|
||||
['KeyY', 28],
|
||||
['KeyZ', 29],
|
||||
|
||||
['RusA', 4],
|
||||
['RusB', 5],
|
||||
['RusC', 6],
|
||||
['RusD', 7],
|
||||
['RusE', 8],
|
||||
['RusF', 9],
|
||||
['RusG', 10],
|
||||
['RusH', 11],
|
||||
['RusI', 12],
|
||||
['RusJ', 13],
|
||||
['RusK', 14],
|
||||
['RusL', 15],
|
||||
['RusM', 16],
|
||||
['RusN', 17],
|
||||
['RusO', 18],
|
||||
['RusP', 19],
|
||||
['RusQ', 20],
|
||||
['RusR', 21],
|
||||
['RusS', 22],
|
||||
['RusT', 23],
|
||||
['RusU', 24],
|
||||
['RusV', 25],
|
||||
['RusW', 26],
|
||||
['RusX', 27],
|
||||
['RusY', 28],
|
||||
['RusZ', 29],
|
||||
|
||||
['RusBracketLeft', 47],
|
||||
['RusBracketRight', 48],
|
||||
['RusBackslash', 49],
|
||||
['RusSemicolon', 51],
|
||||
['RusQuote', 52],
|
||||
['RusComma', 54],
|
||||
['RusPeriod', 55],
|
||||
['RusSlash', 56],
|
||||
|
||||
['Digit1', 30],
|
||||
['Digit2', 31],
|
||||
['Digit3', 32],
|
||||
@@ -48,6 +84,7 @@ export const KeyboardCodes: Map<string, number> = new Map([
|
||||
['BracketRight', 48],
|
||||
['Backslash', 49],
|
||||
['IntlBackslash', 49],
|
||||
['Backquote_azerty', 100],
|
||||
|
||||
['Semicolon', 51],
|
||||
['Quote', 52],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { AppleOutlined, WindowsOutlined } from '@ant-design/icons';
|
||||
import { ConfigProvider, Segmented, theme } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { XIcon } from 'lucide-react';
|
||||
@@ -10,9 +9,10 @@ import { Drawer } from 'vaul';
|
||||
import 'react-simple-keyboard/build/css/index.css';
|
||||
import '@/assets/styles/keyboard.css';
|
||||
|
||||
import { ConfigProvider, Segmented, Select, theme } from 'antd';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getKeyboardLayout, setKeyboardLayout } from '@/lib/localstorage.ts';
|
||||
import * as storage from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
|
||||
|
||||
@@ -31,18 +31,56 @@ export const VirtualKeyboard = () => {
|
||||
|
||||
const [isKeyboardOpen, setIsKeyboardOpen] = useAtom(isKeyboardOpenAtom);
|
||||
|
||||
const [layout, setLayout] = useState('default');
|
||||
const [keyboardLayout, setKeyboardLayout] = useState('default');
|
||||
const [keyboardSystem, setKeyboardSystem] = useState('win');
|
||||
const [keyboardLanguage, setKeyboardLanguage] = useState('en');
|
||||
const [activeModifierKeys, setActiveModifierKeys] = useState<string[]>([]);
|
||||
|
||||
const keyboardRef = useRef<any>(null);
|
||||
|
||||
const systems = [
|
||||
{ value: 'win', icon: <WindowsOutlined /> },
|
||||
{ value: 'mac', icon: <AppleOutlined /> }
|
||||
];
|
||||
|
||||
const languages = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'ru', label: 'Russian' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardLayout = getKeyboardLayout();
|
||||
if (keyboardLayout && ['default', 'mac'].includes(keyboardLayout)) {
|
||||
setLayout(keyboardLayout);
|
||||
const system = storage.getKeyboardSystem();
|
||||
if (system && ['win', 'mac'].includes(system)) {
|
||||
setKeyboardSystem(system);
|
||||
}
|
||||
|
||||
const language = storage.getKeyboardLanguage();
|
||||
if (language && languages.some((lng) => lng.value === language)) {
|
||||
setKeyboardLanguage(language);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const layoutMap = new Map([
|
||||
['en', 'default'],
|
||||
['ru', 'rus'],
|
||||
['fr', 'azerty']
|
||||
]);
|
||||
|
||||
if (keyboardLanguage === 'en' && keyboardSystem === 'mac') {
|
||||
setKeyboardLayout('mac');
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutMap.has(keyboardLanguage)) {
|
||||
setKeyboardLayout(layoutMap.get(keyboardLanguage)!);
|
||||
return;
|
||||
}
|
||||
|
||||
setKeyboardLayout('default');
|
||||
}, [keyboardSystem, keyboardLanguage]);
|
||||
|
||||
function onKeyPress(key: string) {
|
||||
if (modifierKeys.includes(key)) {
|
||||
if (activeModifierKeys.includes(key)) {
|
||||
@@ -66,8 +104,7 @@ export const VirtualKeyboard = () => {
|
||||
}
|
||||
|
||||
function sendKeydown(key: string) {
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
const code = KeyboardCodes.get(specialKey ? specialKey : key);
|
||||
const code = getKeyCode(key);
|
||||
if (!code) {
|
||||
console.log('unknown code: ', key);
|
||||
return;
|
||||
@@ -78,6 +115,26 @@ export const VirtualKeyboard = () => {
|
||||
client.send([1, code, ...modifiers]);
|
||||
}
|
||||
|
||||
function getKeyCode(key: string) {
|
||||
// AZERTY: swap A↔Q and Z↔W on French physical positions
|
||||
if (keyboardLanguage === 'fr' && key.endsWith('_azerty')) {
|
||||
const base = key.replace('_azerty', '');
|
||||
if (base === 'KeyA') return KeyboardCodes.get('KeyQ');
|
||||
if (base === 'KeyQ') return KeyboardCodes.get('KeyA');
|
||||
if (base === 'KeyZ') return KeyboardCodes.get('KeyW');
|
||||
if (base === 'KeyW') return KeyboardCodes.get('KeyZ');
|
||||
// all other labels use their own code
|
||||
return KeyboardCodes.get(base);
|
||||
}
|
||||
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
if (specialKey) {
|
||||
return KeyboardCodes.get(specialKey);
|
||||
}
|
||||
|
||||
return KeyboardCodes.get(key);
|
||||
}
|
||||
|
||||
function sendKeyup() {
|
||||
sendModifierKeyUp();
|
||||
client.send([1, 0, 0, 0, 0, 0]);
|
||||
@@ -121,6 +178,16 @@ export const VirtualKeyboard = () => {
|
||||
setActiveModifierKeys([]);
|
||||
}
|
||||
|
||||
function selectSystem(system: string) {
|
||||
setKeyboardSystem(system);
|
||||
storage.setKeyboardSystem(system);
|
||||
}
|
||||
|
||||
function selectLanguage(language: string) {
|
||||
setKeyboardLanguage(language);
|
||||
storage.setKeyboardLanguage(language);
|
||||
}
|
||||
|
||||
function getButtonTheme(): KeyboardButtonTheme[] {
|
||||
const theme = [{ class: 'hg-double', buttons: doubleKeys.join(' ') }];
|
||||
|
||||
@@ -132,11 +199,6 @@ export const VirtualKeyboard = () => {
|
||||
return theme;
|
||||
}
|
||||
|
||||
function selectLayout(value: string) {
|
||||
setKeyboardLayout(value);
|
||||
setLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer.Root open={isKeyboardOpen} onOpenChange={setIsKeyboardOpen} modal={false}>
|
||||
<Drawer.Portal>
|
||||
@@ -148,22 +210,30 @@ export const VirtualKeyboard = () => {
|
||||
>
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between px-3 py-1">
|
||||
<div className="flex flex-row">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm
|
||||
}}
|
||||
>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'Win', value: 'default', icon: <WindowsOutlined /> },
|
||||
{ label: 'Mac', value: 'mac', icon: <AppleOutlined /> }
|
||||
]}
|
||||
value={layout}
|
||||
onChange={selectLayout}
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Select
|
||||
size="small"
|
||||
style={{ minWidth: 90 }}
|
||||
defaultValue={keyboardLanguage}
|
||||
options={languages}
|
||||
onChange={selectLanguage}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
|
||||
{keyboardLanguage === 'en' && (
|
||||
<Segmented
|
||||
size="small"
|
||||
options={systems}
|
||||
value={keyboardSystem}
|
||||
onChange={selectSystem}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
|
||||
<div className="flex w-[100px] items-center justify-end">
|
||||
<div
|
||||
@@ -184,7 +254,7 @@ export const VirtualKeyboard = () => {
|
||||
keyboardRef={(r) => (keyboardRef.current = r)}
|
||||
onKeyPress={onKeyPress}
|
||||
onKeyReleased={onKeyReleased}
|
||||
layoutName={layout}
|
||||
layoutName={keyboardLayout}
|
||||
{...keyboardOptions}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: refactor it
|
||||
|
||||
// main keys
|
||||
export const keyboardOptions = {
|
||||
theme: 'simple-keyboard hg-theme-default',
|
||||
@@ -18,6 +20,22 @@ export const keyboardOptions = {
|
||||
'{capslock} KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote {enter}',
|
||||
'{shiftleft} KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash {shiftright}',
|
||||
'{controlleft} {altleft} {metaleft} {space} {metaright} {altright}'
|
||||
],
|
||||
rus: [
|
||||
'{escape} F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12',
|
||||
'Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal {backspace}',
|
||||
'{tab} RusQ RusW RusE RusR RusT RusY RusU RusI RusO RusP RusBracketLeft RusBracketRight RusBackslash',
|
||||
'{capslock} RusA RusS RusD RusF RusG RusH RusJ RusK RusL RusSemicolon RusQuote {enter}',
|
||||
'{shiftleft} RusZ RusX RusC RusV RusB RusN RusM RusComma RusPeriod RusSlash {shiftright}',
|
||||
'{controlleft} {winleft} {altleft} {space} {altright} {winright} {menu} {controlright}'
|
||||
],
|
||||
azerty: [
|
||||
'{escape} F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12',
|
||||
'Backquote_azerty Digit1_azerty Digit2_azerty Digit3_azerty Digit4_azerty Digit5_azerty Digit6_azerty Digit7_azerty Digit8_azerty Digit9_azerty Digit0_azerty Minus_azerty Equal_azerty {backspace}',
|
||||
'{tab} KeyA_azerty KeyZ_azerty KeyE_azerty KeyR_azerty KeyT_azerty KeyY_azerty KeyU_azerty KeyI_azerty KeyO_azerty KeyP_azerty BracketLeft_azerty BracketRight_azerty Backslash_azerty',
|
||||
'{capslock} KeyQ_azerty KeyS_azerty KeyD_azerty KeyF_azerty KeyG_azerty KeyH_azerty KeyJ_azerty KeyK_azerty KeyL_azerty Semicolon_azerty Quote_azerty {enter}',
|
||||
'{shiftleft} KeyW_azerty KeyX_azerty KeyC_azerty KeyV_azerty KeyB_azerty KeyN_azerty KeyM_azerty Comma_azerty Period_azerty Slash_azerty {shiftright}',
|
||||
'{controlleft} {winleft} {altleft} {space} {altright} {winright} {menu} {controlright}'
|
||||
]
|
||||
},
|
||||
display: {
|
||||
@@ -88,8 +106,109 @@ export const keyboardOptions = {
|
||||
'{winright}': 'Win',
|
||||
'{altright}': 'Alt',
|
||||
'{menu}': 'Menu',
|
||||
'{controlright}': 'Ctrl'
|
||||
'{controlright}': 'Ctrl',
|
||||
|
||||
RusQ: 'Й',
|
||||
RusW: 'Ц',
|
||||
RusE: 'У',
|
||||
RusR: 'К',
|
||||
RusT: 'Е',
|
||||
RusY: 'Н',
|
||||
RusU: 'Г',
|
||||
RusI: 'Ш',
|
||||
RusO: 'Щ',
|
||||
RusP: 'З',
|
||||
RusBracketLeft: 'Х',
|
||||
RusBracketRight: 'Ъ',
|
||||
RusBackslash: '/<br>\\',
|
||||
|
||||
RusA: 'Ф',
|
||||
RusS: 'Ы',
|
||||
RusD: 'В',
|
||||
RusF: 'А',
|
||||
RusG: 'П',
|
||||
RusH: 'Р',
|
||||
RusJ: 'О',
|
||||
RusK: 'Л',
|
||||
RusL: 'Д',
|
||||
RusSemicolon: 'Ж',
|
||||
RusQuote: 'Э',
|
||||
|
||||
RusZ: 'Я',
|
||||
RusX: 'Ч',
|
||||
RusC: 'С',
|
||||
RusV: 'М',
|
||||
RusB: 'И',
|
||||
RusN: 'Т',
|
||||
RusM: 'Ь',
|
||||
RusComma: 'Б',
|
||||
RusPeriod: 'Ю',
|
||||
RusSlash: ',<br/>.',
|
||||
|
||||
// AZERTY specific display keys
|
||||
// Row 1
|
||||
Backquote_azerty: '<<br/>>',
|
||||
Digit1_azerty: '&<br/>1',
|
||||
Digit2_azerty: 'é<br/>2',
|
||||
Digit3_azerty: '"<br/>#',
|
||||
Digit4_azerty: "'<br/>{",
|
||||
Digit5_azerty: '(<br/>[',
|
||||
Digit6_azerty: '-<br/>|',
|
||||
Digit7_azerty: 'è<br/>`',
|
||||
Digit8_azerty: '_<br/>\\',
|
||||
Digit9_azerty: 'ç<br/>^',
|
||||
Digit0_azerty: 'à<br/>@',
|
||||
Minus_azerty: ')<br/>]',
|
||||
Equal_azerty: '=<br/>}',
|
||||
|
||||
// Row 2
|
||||
KeyA_azerty: 'A',
|
||||
KeyZ_azerty: 'Z',
|
||||
KeyE_azerty: 'E<br/>€',
|
||||
KeyR_azerty: 'R',
|
||||
KeyT_azerty: 'T',
|
||||
KeyY_azerty: 'Y',
|
||||
KeyU_azerty: 'U',
|
||||
KeyI_azerty: 'I',
|
||||
KeyO_azerty: 'O',
|
||||
KeyP_azerty: 'P',
|
||||
BracketLeft_azerty: '¨<br/>^',
|
||||
BracketRight_azerty: '£<br/>$',
|
||||
Backslash_azerty: 'µ<br/>*',
|
||||
|
||||
// Row 3
|
||||
KeyQ_azerty: 'Q',
|
||||
KeyS_azerty: 'S',
|
||||
KeyD_azerty: 'D',
|
||||
KeyF_azerty: 'F',
|
||||
KeyG_azerty: 'G',
|
||||
KeyH_azerty: 'H',
|
||||
KeyJ_azerty: 'J',
|
||||
KeyK_azerty: 'K',
|
||||
KeyL_azerty: 'L',
|
||||
Semicolon_azerty: 'M',
|
||||
Quote_azerty: '%<br/>ù',
|
||||
|
||||
// Row 4
|
||||
KeyW_azerty: 'W',
|
||||
KeyX_azerty: 'X',
|
||||
KeyC_azerty: 'C',
|
||||
KeyV_azerty: 'V',
|
||||
KeyB_azerty: 'B',
|
||||
KeyN_azerty: 'N',
|
||||
KeyM_azerty: '?<br/>,',
|
||||
Comma_azerty: '.<br/>;',
|
||||
Period_azerty: '/<br/>:',
|
||||
Slash_azerty: '§<br/>!'
|
||||
},
|
||||
// Enable layout-specific display
|
||||
mergeDisplay: true,
|
||||
layoutCandidates: {
|
||||
default: 'default',
|
||||
shift: 'shift',
|
||||
azerty: 'azerty'
|
||||
}
|
||||
// ...remaining options...
|
||||
};
|
||||
|
||||
// control keys
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user