Compare commits

...

71 Commits
2.2.2 ... 2.2.6

Author SHA1 Message Date
wj-xiao
6aafb78680 update CHANGELOG.md 2025-04-17 16:23:44 +08:00
wj-xiao
c83dc5565c Optimize some components
refactor: optimize mouse jiggler
refactor: custom swap file size
refactor: update power buttons ui
refactor: optimize web page title
perf: set the minimum screen size to 640x480
perf: add heartbeat in H.264
chore: upgrade axios and golang.org/x/net
2025-04-17 15:20:25 +08:00
BuGu
59a03cacef Update S30eth
To prevent some regions from not receiving the update package, add some DNS servers. However, please note that after setting a static IP, the /etc/resolv.conf file will be overwritten, and the old file will be stored as /etc/resolv.conf.old.
2025-04-16 16:01:25 +08:00
BuGu
c6af287b11 Merge pull request #421 from xitation/main
Resolve typo in eth config that breaks dns resolving.
2025-04-16 15:46:40 +08:00
BuGu
bc636bd9fd Merge pull request #409 from scpcom/fix-iptables-for-ssh
fix iptables for ssh
2025-04-16 15:02:28 +08:00
BuGu
1f2b6c5d44 Optimize IP display logic.
WiFi version:
Only display the corresponding IP when either ETH or WiFi is present;
otherwise, both IPs will alternate.
2025-04-15 14:31:07 +08:00
BuGu
b42ffe8e0b Add Function: Automatically wake up the screen
OLED will automatically wake up in the following scenarios:
1. Plug and unplug the network cable
2. Plug and unplug USB
3. WiFi connection status has changed
4. Plug and unplug HDMI
5. HDMI input resolution changes
6. The transmission format has changed
7. Change in transmission quality
2025-04-15 11:29:22 +08:00
BuGu
f9244b36df Fix security risks related to MIC
MIC driver will not be automatically loaded when powered on,
and the driver will be directly deleted
2025-04-14 14:51:14 +08:00
wenjie
87f697622c Merge pull request #453 from LingkongSky/main
add the Wol custom name
2025-04-13 23:30:48 -07:00
LingkongSky
52dbc0fe40 add the Wol custom name 2025-04-13 10:46:32 +08:00
wj-xiao
4a69bdcd12 upgrade golang-jwt 2025-04-11 10:41:11 +08:00
wenjie
e9e47d797c Merge pull request #434 from ChokunPlayZ/thai-tl
added missing thai translations
2025-04-10 19:13:46 -07:00
wenjie
66cdb5e376 Merge pull request #433 from LingkongSky/main
Finish the MouseJiggler and add it to WEBUI
2025-04-10 19:13:26 -07:00
wenjie
c7575393c9 Merge pull request #432 from S33G/feature/login-logo
Add logo to login screen
2025-04-10 19:10:43 -07:00
LingkongSky
3b9005e9a8 add the Web Title Edit bar into the Web UI 2025-04-10 19:10:44 +08:00
LingkongSky
e3fd5876d2 add the Hostname Editbar into the Web UI 2025-04-10 13:30:04 +08:00
LingkongSky
f71c2d3f8d add the power operation confirm selection in WEB UI 2025-04-08 22:16:42 +08:00
LingkongSky
7163854185 fix the indentation 2025-04-08 20:02:41 +08:00
LingkongSky
8037081e0c update the MouseJiggler,change to use the goroutine 2025-04-08 14:21:47 +08:00
Chokun Rojanapron
477f7dfa03 added missing thai translations 2025-04-04 23:06:15 +07:00
凌空
d45aaf6cb2 Merge branch 'sipeed:main' into main 2025-04-04 17:55:30 +08:00
LingkongSky
4650da8373 finish the MouseJiggler 2025-04-04 17:53:54 +08:00
s33g
13128a8a90 Add logo to login screen 2025-04-04 01:05:50 +01:00
wj-xiao
23b19015f4 update README.md 2025-04-03 18:05:54 +08:00
wj-xiao
843a1f97ce update README.md 2025-04-03 17:41:39 +08:00
wj-xiao
43d6002542 refactor: show all IP addresses in device info 2025-04-03 15:59:59 +08:00
LingkongSky
f9d9677bc0 add the function of MouseJiggler 2025-04-02 23:02:29 +08:00
BuGu
49220d8024 Update USB Gadget config 2025-04-01 11:44:22 +08:00
BuGu
013d76b006 Merge branch 'main' of https://github.com/sipeed/NanoKVM 2025-04-01 11:43:16 +08:00
XiTatiON
c7d3755149 Merge pull request #1 from xitation/resolv_conf_fix
Update S30eth
2025-03-29 11:54:13 +11:00
XiTatiON
8d67ecd6a0 Update S30eth
Fix typo that creates wrong resolve.conf file and therefore dns doesn't work.
2025-03-29 11:52:48 +11:00
wenjie
6005bb1b8d Merge pull request #415 from LingkongSky/main
add the swap function in WebUI
2025-03-27 19:20:05 -07:00
LingkongSky
e6565fbe4b add the swap function in WebUI 2025-03-27 20:00:46 +08:00
wj-xiao
087dbaded9 update CHANGELOG.md 2025-03-26 17:06:11 +08:00
wj-xiao
6c42c57e59 update CHANGELOG.md 2025-03-26 17:04:13 +08:00
wj-xiao
3286cc2f85 feat: add HID-Only mode
feat: support preview updates
perf: optimize screen parameters for reading image
2025-03-26 15:26:59 +08:00
BuGu
d098a5ef09 Remove compatibility support for version 2.2.3 2025-03-26 10:22:32 +08:00
scpcom
040335ace0 fix iptables for ssh 2025-03-25 01:04:17 +01:00
BuGu
dbc74834cd Fix the issue of the server frequently crashing in version 2.2.4. 2025-03-24 17:22:48 +08:00
wj-xiao
f1b411e3b6 update CHANGELOG.md 2025-03-21 18:46:21 +08:00
BuGu
7a9f5f9e6e Merge branch 'main' of https://github.com/sipeed/NanoKVM 2025-03-21 16:22:03 +08:00
BuGu
cffa9a97f3 Delete S03usbdev 2025-03-21 16:21:22 +08:00
wj-xiao
1bf986d41b update libkvm.so 2025-03-21 16:19:05 +08:00
BuGu
eb343b0d31 Fix build script issues 2025-03-21 16:17:47 +08:00
BuGu
628cb3b8c9 Fix abnormal updates of some models
Add function: If the program does not execute after the update, execute a restart
2025-03-21 16:16:53 +08:00
BuGu
ebd391ec26 Reset USB on new app launch. 2025-03-21 12:02:03 +08:00
BuGu
a3b577e563 Fix USB initialization issue. 2025-03-21 11:03:18 +08:00
BuGu
f60adcc797 Merge branch 'main' of https://github.com/sipeed/NanoKVM 2025-03-21 11:03:03 +08:00
BuGu
ee77f7bed1 Fix USB initialization issue. 2025-03-21 11:03:01 +08:00
wj-xiao
6ef83cb22f feat: support setting H.264 GOP
perf: merge H.264 SPS and PPS into I-frame
perf: refactor MJPEG frame detection
perf: update log timestamp to millisecond precision
perf: update script `update-nanokvm.py`
chore: bump Go to 1.23
chore: bump golang.org/x/net to v0.37.0
2025-03-20 18:31:09 +08:00
BuGu
1a3fd15fdf Merge branch 'main' of https://github.com/sipeed/NanoKVM 2025-03-20 18:02:57 +08:00
wenjie
3e5a46570c Merge pull request #392 from jobsidi/patch-1
Update README.md
2025-03-20 03:00:20 -07:00
wenjie
2c4fbf7179 Merge pull request #382 from mekaris/feature/serial_greater_support
Add more serial features
2025-03-20 02:59:30 -07:00
wenjie
5409fb783c Merge pull request #380 from reishoku/reishoku.202503.improve_update-nanokvm-python3
Improve and optimize update-nanokvm.py
2025-03-20 02:58:53 -07:00
BuGu
4cec610aba Fix the issue of build script copying errors 2025-03-20 17:43:33 +08:00
BuGu
57d88d7889 New products default to disabling mDNS 2025-03-20 17:42:53 +08:00
BuGu
7f305ae3cf Fix deadlock issue when HDMI res error occurs 2025-03-20 17:28:05 +08:00
BuGu
eb4ac33fcd Restore frame difference detection function
Optimize the detection algorithm to reduce the detection time to 3-4ms per frame, using set_frame_detact (xx); Dynamically set the number of detection interval frames
2025-03-20 16:43:42 +08:00
BuGu
7a3c6c1690 Add production testing version restrictions 2025-03-20 16:40:33 +08:00
jobsidi
ca63609545 Update README.md
fixed link
2025-03-17 18:56:53 +01:00
mekaris
bcaa8896ee Add more serial features 2025-03-15 00:14:39 +01:00
KOSHIKAWA Kenichi
13b19e3889 Improve update-nanokvm script 2025-03-15 05:44:05 +09:00
BuGu
99d1fc0a64 Modify the method for retrieving I-frames in H.264.
Use a single data packet to replace the SPS + PPS + I three packets retrieval.
Note: This modification may require synchronization support from the server.
2025-03-12 17:34:35 +08:00
BuGu
86b7d676dd Restore S30eth
The new script has rendered the static address unavailable; reverting to the previous version.
2025-03-12 17:18:13 +08:00
BuGu
1adf6edffc Merge pull request #355 from BlwAvg/init-cleanup
Minor Init.d Cleanup
2025-03-12 16:24:28 +08:00
wj-xiao
a16af99648 update CHANGELOG.md 2025-03-11 11:52:55 +08:00
BlwAvg
9fdcb428c0 No more spacing issues! Should be consistent 2025-02-27 13:12:57 -07:00
BlwAvg
cee9bbb079 i dont know how to space things still 2025-02-27 13:11:04 -07:00
BlwAvg
fcc09e2ef0 spacing cont... 2025-02-27 13:07:44 -07:00
BlwAvg
5d96a8060f spacing 2025-02-27 13:02:14 -07:00
BlwAvg
023fb4b68b S03usbdev: Removed erroneous cat command
S30eth: Refactored to remove subshell

S30eth: Fixed spelling error for /etc/resolv.conf (not /etc/resolve.conf)

S50shd: Optimized the sshd script

S95nanokvm: Checked for IPTables rules before implementing to prevent duplicates.
2025-02-27 12:43:36 -07:00
118 changed files with 3691 additions and 1143 deletions

View File

@@ -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
View File

@@ -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">
![NanoKVM-Cube](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg)
![NanoKVM-PCIe](https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM-PCIe.png)
</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.
![NanoKVM Disassembled](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg)
## 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 |
![NanoKVM PCB Pinout](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_2.jpg)
@@ -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.

View File

@@ -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">
![NanoKVM-Cube](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg)
![NanoKVM-PCIe](https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM-PCIe.png)
</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 为完整版配置,带精致外壳和完整配件,内置开机即用的系统镜像卡,推荐个人用户购买。
![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg)
## 参数
| 产品 | 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
View 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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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.

View File

@@ -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` 重启服务。

View File

@@ -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...")
}

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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_

View File

@@ -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)

View 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)

View File

@@ -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
View 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
}

View File

@@ -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 (

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View 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
}

View File

@@ -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"

View File

@@ -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)

View 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
View 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
}

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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

View 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")
}

View File

@@ -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
View 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
}

View 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
}

View 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
}
}
}

View 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)
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View 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")
}

View File

@@ -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()
}
}

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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/"

View File

@@ -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_

View File

@@ -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();
}
}

View File

@@ -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_

View File

@@ -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");

View File

@@ -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_

View File

@@ -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;
}

View File

@@ -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_

View File

@@ -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();

View File

@@ -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);

View File

@@ -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
View File

@@ -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:

View File

@@ -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');
}

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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',

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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: '帐号',

View File

@@ -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');

View File

@@ -2,3 +2,6 @@ import { atom } from 'jotai';
// menu bar disabled items
export const menuDisabledItemsAtom = atom<string[]>([]);
// web title
export const webTitleAtom = atom('');

View File

@@ -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));
}

View File

@@ -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();
});

View File

@@ -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 }]}

View File

@@ -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);

View 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>
);
};

View 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>
</>
);
};

View File

@@ -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} />;

View 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>
);
};

View 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>
);
};

View File

@@ -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>
);

View 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>
</>
);
};

View 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>
)}
</>
);
};

View 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>
)}
</>
);
};

View File

@@ -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>
);

View 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>
);
};

View File

@@ -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>

View File

@@ -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>

View 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>
)}
</>
);
};

View File

@@ -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 />

View File

@@ -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