mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-11 02:19:24 -06:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aafb78680 | ||
|
|
c83dc5565c | ||
|
|
59a03cacef | ||
|
|
c6af287b11 | ||
|
|
bc636bd9fd | ||
|
|
1f2b6c5d44 | ||
|
|
b42ffe8e0b | ||
|
|
f9244b36df | ||
|
|
87f697622c | ||
|
|
52dbc0fe40 | ||
|
|
4a69bdcd12 | ||
|
|
e9e47d797c | ||
|
|
66cdb5e376 | ||
|
|
c7575393c9 | ||
|
|
3b9005e9a8 | ||
|
|
e3fd5876d2 | ||
|
|
f71c2d3f8d | ||
|
|
7163854185 | ||
|
|
8037081e0c | ||
|
|
477f7dfa03 | ||
|
|
d45aaf6cb2 | ||
|
|
4650da8373 | ||
|
|
13128a8a90 | ||
|
|
23b19015f4 | ||
|
|
843a1f97ce | ||
|
|
43d6002542 | ||
|
|
f9d9677bc0 | ||
|
|
49220d8024 | ||
|
|
013d76b006 | ||
|
|
c7d3755149 | ||
|
|
8d67ecd6a0 | ||
|
|
6005bb1b8d | ||
|
|
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 |
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,3 +1,66 @@
|
||||
## 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 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 +93,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,12 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/logger"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/utils"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -14,6 +8,13 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/logger"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
)
|
||||
@@ -37,7 +38,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)
|
||||
|
||||
@@ -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 {
|
||||
@@ -84,6 +91,40 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
@@ -40,6 +41,18 @@ func vmRouter(r *gin.Engine) {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
22
web/pnpm-lock.yaml
generated
22
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
|
||||
@@ -1118,8 +1118,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 +1159,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==}
|
||||
@@ -3917,7 +3917,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 +3928,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 +3955,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 +3976,7 @@ snapshots:
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001677: {}
|
||||
caniuse-lite@1.0.30001713: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Helmet, HelmetData } from 'react-helmet-async';
|
||||
|
||||
import { webTitleAtom } from '@/jotai/settings.ts';
|
||||
|
||||
type HeadProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -8,11 +11,13 @@ type HeadProps = {
|
||||
const helmetData = new HelmetData({});
|
||||
|
||||
export const Head = ({ title = '', description = '' }: HeadProps = {}) => {
|
||||
const webTitle = useAtomValue(webTitleAtom);
|
||||
|
||||
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>
|
||||
|
||||
@@ -76,6 +76,7 @@ const en = {
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
cursor: 'Cursor style',
|
||||
default: 'Default cursor',
|
||||
pointer: 'Pointer cursor',
|
||||
cell: 'Cell cursor',
|
||||
@@ -86,7 +87,16 @@ const en = {
|
||||
absolute: 'Absolute mode',
|
||||
relative: 'Relative mode',
|
||||
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 +143,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 +170,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 +193,18 @@ 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.'
|
||||
},
|
||||
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 +230,24 @@ const en = {
|
||||
description: 'Enable SSH remote access',
|
||||
tip: 'Set a strong password before enabling (Account - Change Password)'
|
||||
},
|
||||
advanced: 'Advanced Settings',
|
||||
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"
|
||||
},
|
||||
hidOnly: 'HID-Only Mode',
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
@@ -258,7 +302,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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -71,7 +71,9 @@ const zh = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
default: '默认指针',
|
||||
title: '鼠标',
|
||||
cursor: '光标样式',
|
||||
default: '默认光标',
|
||||
pointer: '悬浮指针',
|
||||
cell: '单元指针',
|
||||
text: '文本指针',
|
||||
@@ -81,7 +83,16 @@ const zh = {
|
||||
absolute: '绝对模式',
|
||||
relative: '相对模式',
|
||||
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 +154,16 @@ const zh = {
|
||||
},
|
||||
power: {
|
||||
title: '电源',
|
||||
showConfirm: '显示确认框',
|
||||
showConfirmTip: '电源操作需要二次确认',
|
||||
reset: '重启',
|
||||
power: '电源',
|
||||
powerShort: '电源(短按)',
|
||||
powerLong: '电源(长按)'
|
||||
powerLong: '电源(长按)',
|
||||
resetConfirm: '确认执行重启操作吗?',
|
||||
powerConfirm: '确认执行电源操作吗?',
|
||||
okBtn: '确认',
|
||||
cancelBtn: '取消'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
@@ -160,14 +177,18 @@ const zh = {
|
||||
image: '镜像版本',
|
||||
imageTip: 'NanoKVM 系统镜像版本',
|
||||
deviceKey: '设备码',
|
||||
community: '社区'
|
||||
community: '社区',
|
||||
hostname: '主机名',
|
||||
hostnameUpdated: '主机名修改成功,重启后生效'
|
||||
},
|
||||
appearance: {
|
||||
title: '外观',
|
||||
display: '显示',
|
||||
language: '语言',
|
||||
menuBar: '菜单栏',
|
||||
menuBarDesc: '是否在菜单栏中显示图标'
|
||||
menuBarDesc: '是否在菜单栏中显示图标',
|
||||
webTitle: '网站标题',
|
||||
webTitleDesc: '自定义网站页面标题'
|
||||
},
|
||||
device: {
|
||||
title: '设备',
|
||||
@@ -193,14 +214,30 @@ const zh = {
|
||||
description: '启用 SSH 远程访问',
|
||||
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
|
||||
},
|
||||
advanced: '高级设置',
|
||||
swap: {
|
||||
disable: '禁用',
|
||||
description: '设置交换文件大小',
|
||||
tip: '启用该功能可能会减少SD卡使用寿命!'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: '鼠标抖动',
|
||||
description: '防止远程主机休眠',
|
||||
disable: '关闭',
|
||||
absolute: '绝对模式',
|
||||
relative: '相对模式'
|
||||
},
|
||||
mdns: {
|
||||
description: '启用 mDNS 发现服务',
|
||||
tip: '如果您未使用此功能,建议将其关闭'
|
||||
},
|
||||
hidOnly: 'HID-Only 模式',
|
||||
disk: '虚拟U盘',
|
||||
diskDesc: '在远程主机中挂载虚拟U盘',
|
||||
network: '虚拟网卡',
|
||||
networkDesc: '在远程主机中挂载虚拟网卡'
|
||||
networkDesc: '在远程主机中挂载虚拟网卡',
|
||||
okBtn: '是',
|
||||
cancelBtn: '否'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
@@ -246,7 +283,10 @@ const zh = {
|
||||
available: '有新的可用版本,确定要更新吗?',
|
||||
updating: '更新中,请稍候...',
|
||||
confirm: '确定',
|
||||
cancel: '取消'
|
||||
cancel: '取消',
|
||||
preview: '预览更新',
|
||||
previewDesc: '率先体验即将推出的新功能和优化',
|
||||
previewTip: '预览版更新可能包含一些不稳定因素或未完善的功能!'
|
||||
},
|
||||
account: {
|
||||
title: '帐号',
|
||||
|
||||
@@ -5,3 +5,6 @@ export const mouseStyleAtom = atom('cursor-default');
|
||||
|
||||
// mouse mode: absolute or relative
|
||||
export const mouseModeAtom = atom('absolute');
|
||||
|
||||
// hid mode: normal or hid-only
|
||||
export const hidModeAtom = atom<'normal' | 'hid-only'>('normal');
|
||||
|
||||
@@ -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,15 @@ 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 SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
||||
const KEYBOARD_LAYOUT_KEY = 'nano-kvm-keyboard-layout';
|
||||
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 +95,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 ? Boolean(enabled) : true;
|
||||
}
|
||||
|
||||
export function setFrameDetect(enabled: boolean) {
|
||||
localStorage.setItem(FRAME_DETECT_KEY, String(enabled));
|
||||
}
|
||||
|
||||
export function getMouseStyle() {
|
||||
return localStorage.getItem(MOUSE_STYLE_KEY);
|
||||
}
|
||||
@@ -145,3 +166,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 ? Boolean(enabled) : false;
|
||||
}
|
||||
|
||||
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 }]}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getWebTitle } from '@/api/vm.ts';
|
||||
import { getResolution, getVideoMode } from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
|
||||
import { webTitleAtom } from '@/jotai/settings.ts';
|
||||
import { Head } from '@/components/head.tsx';
|
||||
|
||||
import { Keyboard } from './keyboard';
|
||||
@@ -23,6 +25,7 @@ export const Desktop = () => {
|
||||
const [videoMode, setVideoMode] = useAtom(videoModeAtom);
|
||||
const [resolution, setResolution] = useAtom(resolutionAtom);
|
||||
const isKeyboardEnable = useAtomValue(isKeyboardEnableAtom);
|
||||
const setWebTitle = useSetAtom(webTitleAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const cookieVideoMode = getVideoMode();
|
||||
@@ -31,6 +34,12 @@ export const Desktop = () => {
|
||||
const cookieResolution = getResolution();
|
||||
setResolution(cookieResolution ? cookieResolution : { width: 0, height: 0 });
|
||||
|
||||
getWebTitle().then((rsp) => {
|
||||
if (rsp.data?.title) {
|
||||
setWebTitle(rsp.data.title);
|
||||
}
|
||||
});
|
||||
|
||||
const timer = setInterval(() => {
|
||||
client.send([0]);
|
||||
}, 60 * 1000);
|
||||
|
||||
54
web/src/pages/desktop/menu/mouse/cursor.tsx
Normal file
54
web/src/pages/desktop/menu/mouse/cursor.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Popover } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { EyeOffIcon, HandIcon, MousePointerIcon, PlusIcon, TextCursorIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
import { mouseStyleAtom } from '@/jotai/mouse.ts';
|
||||
|
||||
export const Cursor = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mouseStyle, setMouseStyle] = useAtom(mouseStyleAtom);
|
||||
|
||||
const mouseStyles = [
|
||||
{ name: t('mouse.default'), icon: <MousePointerIcon size={14} />, value: 'cursor-default' },
|
||||
{ name: t('mouse.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
|
||||
{ name: t('mouse.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
|
||||
{ name: t('mouse.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
|
||||
{ name: t('mouse.hide'), icon: <EyeOffIcon size={14} />, value: 'cursor-none' }
|
||||
];
|
||||
|
||||
function updateMouseStyle(style: string) {
|
||||
setMouseStyle(style);
|
||||
ls.setMouseStyle(style);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{mouseStyles.map((style) => (
|
||||
<div
|
||||
key={style.value}
|
||||
className={clsx(
|
||||
'flex cursor-pointer select-none items-center space-x-1 rounded py-1.5 pl-3 pr-6 hover:bg-neutral-700/70',
|
||||
style.value === mouseStyle && 'text-green-500'
|
||||
)}
|
||||
onClick={() => updateMouseStyle(style.value)}
|
||||
>
|
||||
<div className="flex h-[14px] w-[20px] items-end">{style.icon}</div>
|
||||
<span>{style.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} placement="rightTop" arrow={true} trigger="hover">
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70">
|
||||
<MousePointerIcon size={18} />
|
||||
<span>{t('mouse.cursor')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
112
web/src/pages/desktop/menu/mouse/hid-mode.tsx
Normal file
112
web/src/pages/desktop/menu/mouse/hid-mode.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Divider, Modal, Typography } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { PenIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/hid.ts';
|
||||
import { hidModeAtom } from '@/jotai/mouse.ts';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
export const HidMode = () => {
|
||||
const { t } = useTranslation();
|
||||
const [hidMode, setHidMode] = useAtom(hidModeAtom);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getHidMode();
|
||||
}, []);
|
||||
|
||||
function getHidMode() {
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.getHidMode()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setHidMode(rsp.data.mode);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHidMode() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 30000);
|
||||
|
||||
const mode = hidMode === 'normal' ? 'hid-only' : 'normal';
|
||||
|
||||
api
|
||||
.setHidMode(mode)
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-[30px] cursor-pointer select-none items-center space-x-2 rounded px-3 hover:bg-neutral-700/70',
|
||||
hidMode === 'hid-only' ? 'text-blue-500' : 'text-neutral-300'
|
||||
)}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<PenIcon size={18} />
|
||||
<span>{t('mouse.hidOnly.title')}</span>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
title={t('mouse.hidOnly.title')}
|
||||
width={580}
|
||||
centered={false}
|
||||
footer={false}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<Divider />
|
||||
|
||||
<Paragraph>{t('mouse.hidOnly.desc')}</Paragraph>
|
||||
|
||||
<Paragraph type="secondary">
|
||||
<ul>
|
||||
<li>{t('mouse.hidOnly.tip1')}</li>
|
||||
<li>{t('mouse.hidOnly.tip2')}</li>
|
||||
<li>{t('mouse.hidOnly.tip3')}</li>
|
||||
</ul>
|
||||
</Paragraph>
|
||||
|
||||
{errMsg && <div className="pt-1 text-sm text-red-500">{errMsg}</div>}
|
||||
|
||||
<div className="flex justify-center pt-5">
|
||||
<Button danger type="primary" loading={isLoading} onClick={updateHidMode}>
|
||||
{hidMode === 'normal' ? t('mouse.hidOnly.enable') : t('mouse.hidOnly.disable')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,149 +1,44 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Divider, Popover } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import {
|
||||
CheckIcon,
|
||||
EyeOffIcon,
|
||||
HandIcon,
|
||||
MouseIcon,
|
||||
MousePointerIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
SquareMousePointerIcon,
|
||||
TextCursorIcon
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { Divider } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { MouseIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/hid';
|
||||
import * as ls from '@/lib/localstorage';
|
||||
import { client } from '@/lib/websocket';
|
||||
import { mouseModeAtom, mouseStyleAtom } from '@/jotai/mouse';
|
||||
import { MenuItem } from '@/components/menu-item.tsx';
|
||||
|
||||
import { Cursor } from './cursor.tsx';
|
||||
import { HidMode } from './hid-mode.tsx';
|
||||
import { MouseMode } from './mouse-mode.tsx';
|
||||
import { ResetHid } from './reset-hid.tsx';
|
||||
|
||||
export const Mouse = () => {
|
||||
const { t } = useTranslation();
|
||||
const [mouseStyle, setMouseStyle] = useAtom(mouseStyleAtom);
|
||||
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const mouseStyles = [
|
||||
{ name: t('mouse.default'), icon: <MousePointerIcon size={14} />, value: 'cursor-default' },
|
||||
{ name: t('mouse.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
|
||||
{ name: t('mouse.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
|
||||
{ name: t('mouse.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
|
||||
{ name: t('mouse.hide'), icon: <EyeOffIcon size={14} />, value: 'cursor-none' }
|
||||
];
|
||||
|
||||
const mouseModes = [
|
||||
{ name: t('mouse.absolute'), value: 'absolute' },
|
||||
{ name: t('mouse.relative'), value: 'relative' }
|
||||
];
|
||||
const setMouseStyle = useSetAtom(mouseStyleAtom);
|
||||
const setMouseMode = useSetAtom(mouseModeAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const style = ls.getMouseStyle();
|
||||
if (style && style !== mouseStyle) {
|
||||
setMouseStyle(style);
|
||||
const mouseStyle = ls.getMouseStyle();
|
||||
if (mouseStyle) {
|
||||
setMouseStyle(mouseStyle);
|
||||
}
|
||||
|
||||
const mode = ls.getMouseMode();
|
||||
if (mode && mode !== mouseMode) {
|
||||
setMouseMode(mode);
|
||||
const mouseMode = ls.getMouseMode();
|
||||
if (mouseMode) {
|
||||
setMouseMode(mouseMode);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function updateStyle(style: string) {
|
||||
setMouseStyle(style);
|
||||
ls.setMouseStyle(style);
|
||||
}
|
||||
|
||||
function updateMode(mode: string) {
|
||||
setMouseMode(mode);
|
||||
ls.setMouseMode(mode);
|
||||
|
||||
if (mode === 'relative') {
|
||||
client.close();
|
||||
setTimeout(() => {
|
||||
client.connect();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function resetHid() {
|
||||
if (isResetting) return;
|
||||
setIsResetting(true);
|
||||
|
||||
client.close();
|
||||
|
||||
api.reset().finally(() => {
|
||||
client.connect();
|
||||
setIsResetting(false);
|
||||
});
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{/* mouse cursor */}
|
||||
{mouseStyles.map((style) => (
|
||||
<div
|
||||
key={style.value}
|
||||
className={clsx(
|
||||
'flex cursor-pointer select-none items-center space-x-1 rounded py-1.5 pl-3 pr-6 hover:bg-neutral-700/70',
|
||||
style.value === mouseStyle && 'text-green-500'
|
||||
)}
|
||||
onClick={() => updateStyle(style.value)}
|
||||
>
|
||||
<div className="flex h-[14px] w-[20px] items-end">{style.icon}</div>
|
||||
<span>{style.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Cursor />
|
||||
<MouseMode />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
|
||||
{/* mouse mode */}
|
||||
<Popover
|
||||
content={
|
||||
<>
|
||||
{mouseModes.map((mode) => (
|
||||
<div
|
||||
key={mode.value}
|
||||
className="flex cursor-pointer items-center space-x-1 rounded py-1.5 pl-2 pr-5 hover:bg-neutral-700/70"
|
||||
onClick={() => updateMode(mode.value)}
|
||||
>
|
||||
<div className="flex h-[16px] w-[16px] items-end text-blue-500">
|
||||
{mode.value === mouseMode && <CheckIcon strokeWidth={3} size={16} />}
|
||||
</div>
|
||||
<span>{mode.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
placement="rightTop"
|
||||
arrow={true}
|
||||
trigger="hover"
|
||||
>
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded pl-3 pr-5 text-neutral-300 hover:bg-neutral-700/70">
|
||||
<div className="flex h-[14px] w-[20px] items-end">
|
||||
<SquareMousePointerIcon size={14} />
|
||||
</div>
|
||||
<span>{t('mouse.mode')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
{/* reset HID */}
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-1 rounded px-3 py-1 hover:bg-neutral-700/70"
|
||||
onClick={resetHid}
|
||||
>
|
||||
<div className="flex h-[14px] w-[20px] items-end">
|
||||
<RefreshCwIcon
|
||||
className={clsx({ 'animate-spin text-blue-500': isResetting })}
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
<span>{t('mouse.resetHid')}</span>
|
||||
</div>
|
||||
</>
|
||||
<HidMode />
|
||||
<ResetHid />
|
||||
</div>
|
||||
);
|
||||
|
||||
return <MenuItem title={t('mouse.title')} icon={<MouseIcon size={18} />} content={content} />;
|
||||
|
||||
57
web/src/pages/desktop/menu/mouse/mouse-mode.tsx
Normal file
57
web/src/pages/desktop/menu/mouse/mouse-mode.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Popover } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { CheckIcon, SquareDashedMousePointerIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { mouseModeAtom } from '@/jotai/mouse.ts';
|
||||
|
||||
export const MouseMode = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
|
||||
|
||||
const mouseModes = [
|
||||
{ name: t('mouse.absolute'), value: 'absolute' },
|
||||
{ name: t('mouse.relative'), value: 'relative' }
|
||||
];
|
||||
|
||||
function updateMouseMode(mode: string) {
|
||||
setMouseMode(mode);
|
||||
ls.setMouseMode(mode);
|
||||
|
||||
if (mode === 'relative') {
|
||||
client.close();
|
||||
setTimeout(() => {
|
||||
client.connect();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{mouseModes.map((mode) => (
|
||||
<div
|
||||
key={mode.value}
|
||||
className="flex cursor-pointer items-center space-x-1 rounded py-1.5 pl-2 pr-5 hover:bg-neutral-700/70"
|
||||
onClick={() => updateMouseMode(mode.value)}
|
||||
>
|
||||
<div className="flex h-[16px] w-[16px] items-end text-blue-500">
|
||||
{mode.value === mouseMode && <CheckIcon strokeWidth={3} size={16} />}
|
||||
</div>
|
||||
<span>{mode.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} placement="rightTop" arrow={true} trigger="hover">
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70">
|
||||
<SquareDashedMousePointerIcon size={18} />
|
||||
<span>{t('mouse.mode')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
35
web/src/pages/desktop/menu/mouse/reset-hid.tsx
Normal file
35
web/src/pages/desktop/menu/mouse/reset-hid.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { RefreshCwIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/hid.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
|
||||
export const ResetHid = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
function resetHid() {
|
||||
if (isResetting) return;
|
||||
setIsResetting(true);
|
||||
|
||||
client.close();
|
||||
|
||||
api.reset().finally(() => {
|
||||
client.connect();
|
||||
setIsResetting(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[30px] cursor-pointer select-none items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70"
|
||||
onClick={resetHid}
|
||||
>
|
||||
<RefreshCwIcon className={clsx({ 'animate-spin text-blue-500': isResetting })} size={18} />
|
||||
<span>{t('mouse.resetHid')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,31 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Slider } from 'antd';
|
||||
import { Divider, Switch, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { CirclePowerIcon, LoaderCircleIcon, PowerIcon, RotateCcwIcon } from 'lucide-react';
|
||||
import { LoaderCircleIcon, PowerIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm';
|
||||
import * as localstorage from '@/lib/localstorage.ts';
|
||||
import { MenuItem } from '@/components/menu-item.tsx';
|
||||
|
||||
import { PowerLong } from './power-long.tsx';
|
||||
import { PowerShort } from './power-short.tsx';
|
||||
import { Reset } from './reset.tsx';
|
||||
|
||||
export const Power = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isPowerOn, setIsPowerOn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [powerDuration, setPowerDuration] = useState(8);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getLed();
|
||||
const interval = setInterval(getLed, 5000);
|
||||
|
||||
const powerConfirm = localstorage.getPowerConfirm();
|
||||
setShowConfirm(powerConfirm);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
@@ -24,28 +33,15 @@ export const Power = () => {
|
||||
|
||||
function getLed() {
|
||||
api.getGpio().then((rsp: any) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
if (rsp.code === 0) {
|
||||
setIsPowerOn(rsp.data.pwr);
|
||||
}
|
||||
|
||||
setIsPowerOn(rsp.data.pwr);
|
||||
});
|
||||
}
|
||||
|
||||
function click(button: 'reset' | 'power', duration?: number) {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const millisecond = Math.floor((duration ? duration : powerDuration) * 1000);
|
||||
|
||||
api.setGpio(button, millisecond).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function onChange(value: number) {
|
||||
setPowerDuration(value);
|
||||
function updateShowConfirm(value: boolean) {
|
||||
setShowConfirm(value);
|
||||
localstorage.setPowerConfirm(value);
|
||||
}
|
||||
|
||||
const icon = (
|
||||
@@ -59,39 +55,27 @@ export const Power = () => {
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('reset', 0.8)}
|
||||
>
|
||||
<RotateCcwIcon size={16} />
|
||||
<span>{t('power.reset')}</span>
|
||||
<div className="min-w-[200px]">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-base font-bold text-neutral-300">{t('power.title')}</span>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Tooltip title={t('power.showConfirmTip')} placement="right">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400">{t('power.showConfirm')}</span>
|
||||
|
||||
<Switch size="small" checked={showConfirm} onChange={updateShowConfirm} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('power', 0.8)}
|
||||
>
|
||||
<PowerIcon size={16} />
|
||||
<span>{t('power.powerShort')}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '10px 0 15px 0' }} />
|
||||
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('power')}
|
||||
>
|
||||
<CirclePowerIcon size={16} />
|
||||
<span>{t('power.powerLong')}</span>
|
||||
<div className="flex h-full items-start text-xs text-neutral-500">{`${powerDuration}s`}</div>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
defaultValue={8}
|
||||
min={1}
|
||||
max={30}
|
||||
tooltip={{ placement: 'bottom' }}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Reset showConfirm={showConfirm} isLoading={isLoading} setIsLoading={setIsLoading} />
|
||||
<PowerShort showConfirm={showConfirm} isLoading={isLoading} setIsLoading={setIsLoading} />
|
||||
<PowerLong showConfirm={showConfirm} isLoading={isLoading} setIsLoading={setIsLoading} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
67
web/src/pages/desktop/menu/power/power-long.tsx
Normal file
67
web/src/pages/desktop/menu/power/power-long.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState } from 'react';
|
||||
import { Popconfirm, Slider } from 'antd';
|
||||
import { CirclePowerIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
|
||||
type PowerLongProps = {
|
||||
showConfirm: boolean;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
export const PowerLong = ({ showConfirm, isLoading, setIsLoading }: PowerLongProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [duration, setDuration] = useState(8);
|
||||
|
||||
function power() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
api.setGpio('power', duration * 1000).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showConfirm ? (
|
||||
<Popconfirm
|
||||
placement="bottomLeft"
|
||||
title={t('power.powerConfirm')}
|
||||
okText={t('power.okBtn')}
|
||||
cancelText={t('power.cancelBtn')}
|
||||
onConfirm={power}
|
||||
color="#404040"
|
||||
>
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70">
|
||||
<CirclePowerIcon size={16} />
|
||||
<span>{t('power.powerLong')}</span>
|
||||
<div className="flex h-full items-start text-xs text-neutral-500">{`${duration}s`}</div>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={power}
|
||||
>
|
||||
<CirclePowerIcon size={16} />
|
||||
<span>{t('power.powerLong')}</span>
|
||||
<div className="flex h-full items-start text-xs text-neutral-500">{`${duration}s`}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
defaultValue={8}
|
||||
min={1}
|
||||
max={30}
|
||||
tooltip={{ placement: 'bottom' }}
|
||||
onChange={setDuration}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
52
web/src/pages/desktop/menu/power/power-short.tsx
Normal file
52
web/src/pages/desktop/menu/power/power-short.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Popconfirm } from 'antd';
|
||||
import { PowerIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
|
||||
type PowerShortProps = {
|
||||
showConfirm: boolean;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
export const PowerShort = ({ showConfirm, isLoading, setIsLoading }: PowerShortProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function power() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
api.setGpio('power', 800).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showConfirm ? (
|
||||
<Popconfirm
|
||||
placement="bottomLeft"
|
||||
title={t('power.powerConfirm')}
|
||||
okText={t('power.okBtn')}
|
||||
cancelText={t('power.cancelBtn')}
|
||||
onConfirm={power}
|
||||
color="#404040"
|
||||
>
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70">
|
||||
<PowerIcon size={16} />
|
||||
<span>{t('power.powerShort')}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={power}
|
||||
>
|
||||
<PowerIcon size={16} />
|
||||
<span>{t('power.powerShort')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
52
web/src/pages/desktop/menu/power/reset.tsx
Normal file
52
web/src/pages/desktop/menu/power/reset.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Popconfirm } from 'antd';
|
||||
import { RotateCcwIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { setGpio } from '@/api/vm.ts';
|
||||
|
||||
type ResetProps = {
|
||||
showConfirm: boolean;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
export const Reset = ({ showConfirm, isLoading, setIsLoading }: ResetProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function reset() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
setGpio('reset', 800).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showConfirm ? (
|
||||
<Popconfirm
|
||||
placement="bottomLeft"
|
||||
title={t('power.resetConfirm')}
|
||||
okText={t('power.okBtn')}
|
||||
cancelText={t('power.cancelBtn')}
|
||||
onConfirm={reset}
|
||||
color="#404040"
|
||||
>
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70">
|
||||
<RotateCcwIcon size={16} />
|
||||
<span>{t('power.reset')}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={reset}
|
||||
>
|
||||
<RotateCcwIcon size={16} />
|
||||
<span>{t('power.reset')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +1,39 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { LoaderCircleIcon, Tally4Icon, Tally5Icon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getFrameDetect, updateFrameDetect } from '@/api/stream.ts';
|
||||
import * as api from '@/api/stream.ts';
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
|
||||
export const FrameDetect = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getFrameDetect().then((rsp) => {
|
||||
if (rsp.code === 0) {
|
||||
setIsEnabled(rsp.data.enabled);
|
||||
}
|
||||
});
|
||||
const enabled = ls.getFrameDetect();
|
||||
if (enabled) {
|
||||
setIsEnabled(true);
|
||||
} else {
|
||||
api.updateFrameDetect(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function update() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
updateFrameDetect()
|
||||
const enabled = !isEnabled;
|
||||
|
||||
api
|
||||
.updateFrameDetect(enabled)
|
||||
.then((rsp) => {
|
||||
if (rsp.code === 0) {
|
||||
setIsEnabled(rsp.data.enabled);
|
||||
setIsEnabled(enabled);
|
||||
ls.setFrameDetect(enabled);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -41,18 +49,20 @@ export const FrameDetect = () => {
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
) : isEnabled ? (
|
||||
<div>
|
||||
<Tally4Icon color="#22c55e" size={18} className="block group-hover:hidden" />
|
||||
<Tally5Icon color="#ef4444" size={18} className="hidden group-hover:block" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Tally5Icon size={18} className="block group-hover:hidden" />
|
||||
<Tally4Icon color="#22c55e" size={18} className="hidden group-hover:block" />
|
||||
</div>
|
||||
<>
|
||||
{isEnabled ? <Tally4Icon color="#22c55e" size={18} /> : <Tally5Icon size={18} />}
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
'select-none text-sm',
|
||||
isEnabled ? 'group-hover:text-red-500' : 'group-hover:text-green-500'
|
||||
)}
|
||||
>
|
||||
{t('screen.frameDetect')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="select-none text-sm">{t('screen.frameDetect')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
57
web/src/pages/desktop/menu/screen/gop.tsx
Normal file
57
web/src/pages/desktop/menu/screen/gop.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Popover } from 'antd';
|
||||
import { CheckIcon, SquareKanbanIcon } from 'lucide-react';
|
||||
|
||||
import { updateScreen } from '@/api/vm.ts';
|
||||
import { setGop as setCookie } from '@/lib/localstorage';
|
||||
|
||||
type GopProps = {
|
||||
gop: number;
|
||||
setGop: (gop: number) => void;
|
||||
};
|
||||
|
||||
const gopList = [
|
||||
{ key: 10, label: '10' },
|
||||
{ key: 30, label: '30' },
|
||||
{ key: 50, label: '50' },
|
||||
{ key: 100, label: '100' }
|
||||
];
|
||||
|
||||
export const Gop = ({ gop, setGop }: GopProps) => {
|
||||
async function update(value: number) {
|
||||
if (value === gop) return;
|
||||
|
||||
const rsp = await updateScreen('gop', value);
|
||||
if (rsp.code !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGop(value);
|
||||
setCookie(value);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{gopList.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="flex cursor-pointer select-none items-center rounded py-1 pl-1 pr-6 hover:bg-neutral-700/70"
|
||||
onClick={() => update(item.key)}
|
||||
>
|
||||
<div className="flex h-[14px] w-[20px] items-end text-blue-500">
|
||||
{item.key === gop && <CheckIcon size={14} />}
|
||||
</div>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} placement="rightTop">
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70">
|
||||
<SquareKanbanIcon size={18} />
|
||||
<span className="select-none text-sm">GOP</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { MenuItem } from '@/components/menu-item.tsx';
|
||||
import { BitRateMap, QualityMap } from './constants.ts';
|
||||
import { Fps } from './fps';
|
||||
import { FrameDetect } from './frame-detect';
|
||||
import { Gop } from './gop.tsx';
|
||||
import { Quality } from './quality';
|
||||
import { Reset } from './reset.tsx';
|
||||
import { Resolution } from './resolution';
|
||||
@@ -24,37 +25,47 @@ export const Screen = () => {
|
||||
|
||||
const [fps, setFps] = useState(30);
|
||||
const [quality, setQuality] = useState(2);
|
||||
const [gop, setGop] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
updateScreen('type', videoMode === 'mjpeg' ? 0 : 1);
|
||||
updateScreen('resolution', resolution!.height);
|
||||
updateQuality();
|
||||
updateFps();
|
||||
updateGop();
|
||||
}, []);
|
||||
|
||||
function updateQuality() {
|
||||
async function updateQuality() {
|
||||
const cookieQuality = ls.getQuality();
|
||||
if (!cookieQuality) return;
|
||||
|
||||
const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2;
|
||||
const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!;
|
||||
|
||||
updateScreen('quality', value).then((rsp) => {
|
||||
if (rsp.code === 0) {
|
||||
setQuality(key);
|
||||
}
|
||||
});
|
||||
const rsp = await updateScreen('quality', value);
|
||||
if (rsp.code === 0) {
|
||||
setQuality(key);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFps() {
|
||||
async function updateFps() {
|
||||
const cookieFps = ls.getFps();
|
||||
if (!cookieFps) return;
|
||||
|
||||
updateScreen('fps', cookieFps).then((rsp) => {
|
||||
if (rsp.code === 0) {
|
||||
setFps(cookieFps);
|
||||
}
|
||||
});
|
||||
const rsp = await updateScreen('fps', cookieFps);
|
||||
if (rsp.code === 0) {
|
||||
setFps(cookieFps);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGop() {
|
||||
const cookieGop = ls.getGop();
|
||||
if (!cookieGop) return;
|
||||
|
||||
const rsp = await updateScreen('gop', cookieGop);
|
||||
if (rsp.code === 0) {
|
||||
setGop(cookieGop);
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
@@ -63,6 +74,7 @@ export const Screen = () => {
|
||||
<Resolution />
|
||||
<Quality quality={quality} setQuality={setQuality} />
|
||||
<Fps fps={fps} setFps={setFps} />
|
||||
{videoMode === 'h264' && <Gop gop={gop} setGop={setGop} />}
|
||||
{videoMode === 'mjpeg' && <FrameDetect />}
|
||||
<Reset />
|
||||
</div>
|
||||
|
||||
@@ -55,9 +55,9 @@ export const Resolution = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex w-[32px]">{res.width}</span>
|
||||
<span>x</span>
|
||||
<span className="w-[36px]">{res.height}</span>
|
||||
<span>{res.width}</span>
|
||||
<span className="px-1">x</span>
|
||||
<span>{res.height}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
108
web/src/pages/desktop/menu/settings/about/hostname.tsx
Normal file
108
web/src/pages/desktop/menu/settings/about/hostname.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Input } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { ClipboardPenIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
|
||||
export const Hostname = () => {
|
||||
const { t } = useTranslation();
|
||||
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hostname, setHostname] = useState('');
|
||||
|
||||
const [editState, setEditState] = useState<'' | 'editing' | 'edited'>('');
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getHostname();
|
||||
}, []);
|
||||
|
||||
function getHostname() {
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.getHostname()
|
||||
.then((rsp) => {
|
||||
if (rsp.data?.hostname) {
|
||||
setHostname(rsp.data?.hostname);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function showInput() {
|
||||
setInput(hostname);
|
||||
setEditState('editing');
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (input === hostname) {
|
||||
setEditState('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.setHostname(input)
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setHostname(input);
|
||||
setEditState('edited');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between pt-4">
|
||||
<span>{t('settings.about.hostname')}</span>
|
||||
|
||||
{editState === 'editing' ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
onFocus={() => setIsKeyboardEnable(false)}
|
||||
onBlur={() => setIsKeyboardEnable(true)}
|
||||
style={{ width: 150 }}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
<Button size="small" icon={<CheckOutlined />} onClick={update} />
|
||||
<Button size="small" icon={<CloseOutlined />} onClick={() => setEditState('')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start space-x-2">
|
||||
<span>{hostname}</span>
|
||||
<div
|
||||
className="cursor-pointer text-blue-500 hover:text-blue-500/60"
|
||||
onClick={showInput}
|
||||
>
|
||||
<ClipboardPenIcon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editState === 'edited' && (
|
||||
<div className="flex w-full justify-end pt-1 text-xs text-green-500">
|
||||
{t('settings.about.hostnameUpdated')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Community } from './community.tsx';
|
||||
import { Hostname } from './hostname.tsx';
|
||||
import { Information } from './information.tsx';
|
||||
|
||||
export const About = () => {
|
||||
@@ -13,6 +14,7 @@ export const About = () => {
|
||||
<Divider />
|
||||
|
||||
<Information />
|
||||
<Hostname />
|
||||
<Divider />
|
||||
|
||||
<Community />
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { CircleHelpIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
|
||||
type IP = {
|
||||
name: string;
|
||||
addr: string;
|
||||
version: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type Info = {
|
||||
ip: string;
|
||||
ips: IP[];
|
||||
mdns: string;
|
||||
image: string;
|
||||
application: string;
|
||||
@@ -34,11 +41,24 @@ export const Information = () => {
|
||||
<div className="pb-5 text-neutral-400">{t('settings.about.information')}</div>
|
||||
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{/* IP list */}
|
||||
<div className="flex w-full items-start justify-between">
|
||||
<span>{t('settings.about.ip')}</span>
|
||||
<span>{information ? information.ip : '-'}</span>
|
||||
{information?.ips && information.ips.length > 0 ? (
|
||||
<div className="flex flex-col space-y-1">
|
||||
{information.ips.map((ip) => (
|
||||
<div key={ip.addr} className="flex items-center">
|
||||
<Tag>{ip.type}</Tag>
|
||||
<span>{ip.addr}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* mDNS */}
|
||||
{!!information?.mdns && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span>{t('settings.about.mdns')}</span>
|
||||
@@ -46,6 +66,7 @@ export const Information = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* image version */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t('settings.about.image')}</span>
|
||||
@@ -61,6 +82,7 @@ export const Information = () => {
|
||||
<span>{information ? information.image : '-'}</span>
|
||||
</div>
|
||||
|
||||
{/* application version */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t('settings.about.application')}</span>
|
||||
@@ -76,10 +98,11 @@ export const Information = () => {
|
||||
<span>{information ? information.application : '-'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span>{t('settings.about.deviceKey')}</span>
|
||||
<span>{information ? information.deviceKey : '-'}</span>
|
||||
</div>
|
||||
{/* device key */}
|
||||
{/*<div className="flex w-full items-center justify-between">*/}
|
||||
{/* <span>{t('settings.about.deviceKey')}</span>*/}
|
||||
{/* <span>{information ? information.deviceKey : '-'}</span>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user