mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-10 01:49:00 -06:00
Compare commits
308 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9311b898f2 | ||
|
|
0e30a26db4 | ||
|
|
24a460e64a | ||
|
|
f62320f3e1 | ||
|
|
0d73e8b52a | ||
|
|
11d66da064 | ||
|
|
f24fecf01c | ||
|
|
1c16444614 | ||
|
|
8b7b1236ba | ||
|
|
c44bf06cc9 | ||
|
|
8230d59354 | ||
|
|
0f270c8823 | ||
|
|
95970f87fa | ||
|
|
469c1693bd | ||
|
|
2a45a8a8fa | ||
|
|
2180fa70f7 | ||
|
|
b093e11aeb | ||
|
|
140812606f | ||
|
|
14239b8a6f | ||
|
|
dfee0934f0 | ||
|
|
42c332a406 | ||
|
|
4c1ebc84e9 | ||
|
|
81a6e5d152 | ||
|
|
145a43d688 | ||
|
|
386c5fa86c | ||
|
|
1499edd83c | ||
|
|
db719c32ad | ||
|
|
340477a31f | ||
|
|
fd57047e86 | ||
|
|
496b9f981e | ||
|
|
76dc2312b4 | ||
|
|
ade6d80f10 | ||
|
|
574b41a0a1 | ||
|
|
f907a340b6 | ||
|
|
0498b39f0e | ||
|
|
5d196ff8d8 | ||
|
|
84d65fb167 | ||
|
|
0f22d53e7c | ||
|
|
f44bc72201 | ||
|
|
310d550ff6 | ||
|
|
044292ccb4 | ||
|
|
0dc0f9ea73 | ||
|
|
5fcdf888b3 | ||
|
|
f209cb1e0e | ||
|
|
7d3df10915 | ||
|
|
fb9f127289 | ||
|
|
bc64084516 | ||
|
|
e86d385521 | ||
|
|
6ca0fa7626 | ||
|
|
f8d8b04de3 | ||
|
|
305a75d1a7 | ||
|
|
c3a9e9d402 | ||
|
|
48c941fb86 | ||
|
|
1016853d25 | ||
|
|
90068a3c2d | ||
|
|
8fc71a2e36 | ||
|
|
8310e43b31 | ||
|
|
0e30b62894 | ||
|
|
3de21f295b | ||
|
|
d8c4de9a01 | ||
|
|
acf366d61e | ||
|
|
b9c85fad7b | ||
|
|
196b9555ab | ||
|
|
e9cccd58ea | ||
|
|
00d30efa45 | ||
|
|
309b9665cb | ||
|
|
759abc92c9 | ||
|
|
ae8f0e2236 | ||
|
|
815770fabf | ||
|
|
e82069dea2 | ||
|
|
396aee7d0a | ||
|
|
3299c202b0 | ||
|
|
755cbe113d | ||
|
|
bab729ed18 | ||
|
|
350dbdfe4b | ||
|
|
1b5ca8eef6 | ||
|
|
ba242be427 | ||
|
|
fce974c146 | ||
|
|
a82aa792d8 | ||
|
|
105f77d9ec | ||
|
|
b6d87b9799 | ||
|
|
6279086b5a | ||
|
|
61ea4bd385 | ||
|
|
7f3e039d1b | ||
|
|
f2c91c09c7 | ||
|
|
32c81f220d | ||
|
|
c77981cc0c | ||
|
|
cd767d16df | ||
|
|
8591aecb9c | ||
|
|
34746afea3 | ||
|
|
9d6cf49c3f | ||
|
|
dcb87d0d89 | ||
|
|
6e45e6b135 | ||
|
|
7f7e31bae9 | ||
|
|
4499f9ed44 | ||
|
|
9a5ad1dff2 | ||
|
|
2f8f77cb89 | ||
|
|
a4978d93d6 | ||
|
|
01e28f10ae | ||
|
|
ea9068fd0d | ||
|
|
d85823abcf | ||
|
|
4c0027ffd3 | ||
|
|
afb868af30 | ||
|
|
66b1d5dc30 | ||
|
|
7d8cdc74c6 | ||
|
|
809581df49 | ||
|
|
5d47a8d48d | ||
|
|
6242f1931e | ||
|
|
b768219c18 | ||
|
|
6935923bd1 | ||
|
|
559e6e2556 | ||
|
|
f354fdfbf4 | ||
|
|
f242b5a378 | ||
|
|
e18ec2219d | ||
|
|
d075ae58d9 | ||
|
|
e8ccac8e61 | ||
|
|
7c8f9f3cd8 | ||
|
|
66e434db24 | ||
|
|
0be553c4c8 | ||
|
|
1dda0f29be | ||
|
|
fbdedc7d8a | ||
|
|
e325352309 | ||
|
|
bf51b50346 | ||
|
|
96b52f3ac0 | ||
|
|
ad44db254b | ||
|
|
736b89d7d1 | ||
|
|
3122531cae | ||
|
|
cbb47ab735 | ||
|
|
fa8d4fd5ca | ||
|
|
423d05a087 | ||
|
|
7036e94dab | ||
|
|
3a19180a51 | ||
|
|
f8f8920fc5 | ||
|
|
507b53c10e | ||
|
|
6aafb78680 | ||
|
|
c83dc5565c | ||
|
|
59a03cacef | ||
|
|
c6af287b11 | ||
|
|
bc636bd9fd | ||
|
|
1f2b6c5d44 | ||
|
|
b42ffe8e0b | ||
|
|
f9244b36df | ||
|
|
87f697622c | ||
|
|
52dbc0fe40 | ||
|
|
4a69bdcd12 | ||
|
|
e9e47d797c | ||
|
|
66cdb5e376 | ||
|
|
c7575393c9 | ||
|
|
3b9005e9a8 | ||
|
|
e3fd5876d2 | ||
|
|
f71c2d3f8d | ||
|
|
7163854185 | ||
|
|
8037081e0c | ||
|
|
642a00051e | ||
|
|
7f9bd1c457 | ||
|
|
477f7dfa03 | ||
|
|
d45aaf6cb2 | ||
|
|
4650da8373 | ||
|
|
13128a8a90 | ||
|
|
23b19015f4 | ||
|
|
843a1f97ce | ||
|
|
43d6002542 | ||
|
|
f9d9677bc0 | ||
|
|
49220d8024 | ||
|
|
013d76b006 | ||
|
|
c7d3755149 | ||
|
|
8d67ecd6a0 | ||
|
|
6005bb1b8d | ||
|
|
c4e8d19649 | ||
|
|
82be18c469 | ||
|
|
e6565fbe4b | ||
|
|
087dbaded9 | ||
|
|
6c42c57e59 | ||
|
|
3286cc2f85 | ||
|
|
d098a5ef09 | ||
|
|
040335ace0 | ||
|
|
dbc74834cd | ||
|
|
f1b411e3b6 | ||
|
|
7a9f5f9e6e | ||
|
|
cffa9a97f3 | ||
|
|
1bf986d41b | ||
|
|
eb343b0d31 | ||
|
|
628cb3b8c9 | ||
|
|
ebd391ec26 | ||
|
|
a3b577e563 | ||
|
|
f60adcc797 | ||
|
|
ee77f7bed1 | ||
|
|
6ef83cb22f | ||
|
|
1a3fd15fdf | ||
|
|
3e5a46570c | ||
|
|
2c4fbf7179 | ||
|
|
5409fb783c | ||
|
|
4cec610aba | ||
|
|
57d88d7889 | ||
|
|
7f305ae3cf | ||
|
|
eb4ac33fcd | ||
|
|
7a3c6c1690 | ||
|
|
ca63609545 | ||
|
|
bcaa8896ee | ||
|
|
13b19e3889 | ||
|
|
99d1fc0a64 | ||
|
|
86b7d676dd | ||
|
|
1adf6edffc | ||
|
|
a16af99648 | ||
|
|
58d5ab2d37 | ||
|
|
9aebabfd59 | ||
|
|
21b119294c | ||
|
|
e3a79daf89 | ||
|
|
4813e8bd50 | ||
|
|
93c0f3d353 | ||
|
|
be5a9f7515 | ||
|
|
36a1eb8eb8 | ||
|
|
20b452f3c0 | ||
|
|
b062569aab | ||
|
|
c91ce5ab62 | ||
|
|
602615e489 | ||
|
|
0e11855011 | ||
|
|
c5c768b143 | ||
|
|
5c0f098069 | ||
|
|
8bd7e02ec5 | ||
|
|
9233bd7c79 | ||
|
|
7d2664acb5 | ||
|
|
8c09cd7c44 | ||
|
|
aef8a7a851 | ||
|
|
8ed6e2eca3 | ||
|
|
a7b5091024 | ||
|
|
cd6da8b588 | ||
|
|
2d755fdb98 | ||
|
|
f231df4f65 | ||
|
|
95c8edcd3d | ||
|
|
d94eba0ab1 | ||
|
|
b48729c594 | ||
|
|
b9ba107587 | ||
|
|
c2d3224552 | ||
|
|
592939895b | ||
|
|
8d8c57bc48 | ||
|
|
8330b1b1fe | ||
|
|
9fdcb428c0 | ||
|
|
cee9bbb079 | ||
|
|
fcc09e2ef0 | ||
|
|
5d96a8060f | ||
|
|
023fb4b68b | ||
|
|
594c205a29 | ||
|
|
fc436e2743 | ||
|
|
125eddffe1 | ||
|
|
aad3d718e4 | ||
|
|
e3575e629c | ||
|
|
7bd7b77fa9 | ||
|
|
ba620db901 | ||
|
|
3259379345 | ||
|
|
08eb89dc5c | ||
|
|
1376929897 | ||
|
|
614ef4031c | ||
|
|
d9a19e132a | ||
|
|
7795191a46 | ||
|
|
5e3c4d98f2 | ||
|
|
de28273def | ||
|
|
148fb4e37b | ||
|
|
a920fcc58a | ||
|
|
5021ab8d2f | ||
|
|
0ae97e6dd7 | ||
|
|
8c2e744d42 | ||
|
|
1ea9dc0561 | ||
|
|
e4367f0b69 | ||
|
|
f973316f8f | ||
|
|
b5e48a07e8 | ||
|
|
2ec5393bd0 | ||
|
|
01c837f6ac | ||
|
|
899aa48875 | ||
|
|
29dd37852d | ||
|
|
9c0c50140b | ||
|
|
33d0eb3297 | ||
|
|
922a7772b8 | ||
|
|
38ddd89e56 | ||
|
|
b664411992 | ||
|
|
7a301f588a | ||
|
|
e5ebb36f78 | ||
|
|
01e4802247 | ||
|
|
66b1ea26c5 | ||
|
|
fbf2531dbb | ||
|
|
3debfc4cf1 | ||
|
|
b35542a446 | ||
|
|
dcf5bc99dd | ||
|
|
f984b9f870 | ||
|
|
8c547b8baa | ||
|
|
a265eaef15 | ||
|
|
f8eaa7f615 | ||
|
|
c47fc483a2 | ||
|
|
64254052c0 | ||
|
|
b99a8358a7 | ||
|
|
ed0a6f6681 | ||
|
|
055df124b3 | ||
|
|
06097068a2 | ||
|
|
ff5398ed67 | ||
|
|
c8dcdf7921 | ||
|
|
e09a0b14e0 | ||
|
|
0dbf8c007f | ||
|
|
a24c1b0cef | ||
|
|
42dc6ad079 | ||
|
|
d670cf19a1 | ||
|
|
7fe7c49e65 | ||
|
|
5c1e617862 | ||
|
|
9ff9d59aa5 | ||
|
|
7556d154d3 | ||
|
|
df808da5d6 | ||
|
|
06bd869fb1 | ||
|
|
7343ca3811 | ||
|
|
59d5944398 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -16,4 +16,16 @@ web/web
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
NanoKVM-Server
|
||||
NanoKVM-Server
|
||||
|
||||
support/sg2002/kvm_system/build
|
||||
support/sg2002/kvm_system/dist
|
||||
support/sg2002/kvm_system/CMakeLists.txt
|
||||
|
||||
support/sg2002/kvm_vision_test/build
|
||||
support/sg2002/kvm_vision_test/dist
|
||||
support/sg2002/kvm_vision_test/CMakeLists.txt
|
||||
|
||||
support/sg2002/additional/original
|
||||
kvmapp/server/dl_lib
|
||||
kvmapp/kvm_system/kvm_system
|
||||
|
||||
212
CHANGELOG.md
Normal file
212
CHANGELOG.md
Normal file
@@ -0,0 +1,212 @@
|
||||
## 2.3.0 [0e30a26](https://github.com/sipeed/NanoKVM/commit/0e30a26db42cbc08416662b49a88a5d16ad93424) (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* Added an EDID editor in the terminal with a built-in 1080P EDID template
|
||||
* Added German virtual keyboard and German clipboard support (thanks to [@Alexander-Ger-Reich](https://github.com/Alexander-Ger-Reich))
|
||||
* Resetting the HID also resets the USB hardware
|
||||
* Added support for deleting ISO images
|
||||
|
||||
### Refactoring & Improvements
|
||||
|
||||
* Video (H.264 WebRTC): Refactored the module to significantly reduce video latency
|
||||
* Video (H.264 Direct): Refactored to optimize data transmission and support data parsing even when the page is in the background
|
||||
* Video (MJPEG): Refactored and ensure correct data length transmission
|
||||
* Added locking before resetting USB to prevent HID deadlock (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Optimized HID writing logic
|
||||
* Made HDMI toggle state persist across reboots (thanks to [@tpretz](https://github.com/tpretz))
|
||||
* Enhanced the paste feature with browser clipboard API integration (thanks to [@patrickpilon](https://github.com/patrickpilon))
|
||||
* Set the inquiry string for the virtual storage device (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Hostname now updates `/etc/hosts` simultaneously and takes effect immediately (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* Improved compatibility for static IP configuration on Windows
|
||||
* Optimized the web page title update logic
|
||||
* Added a confirmation dialog when uninstalling Tailscale
|
||||
* Improved UI for Clipboard, Image Mounting, and settings pages
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Tailscale: Fixed an issue where Tailscale would turn the device into a router
|
||||
* Tailscale: Fixed an issue where Tailscale would disable IPv6
|
||||
* Tailscale: Added a swap memory option to prevent Tailscale Out-Of-Memory errors
|
||||
* Tailscale: Fixed Tailscale accept-dns configuration (thanks to [@lazydba247](https://github.com/lazydba247))
|
||||
* Fixed an issue where certain modifier keys were not recognized
|
||||
* Fixed vertical mouse drift when the page is zoomed in or out
|
||||
|
||||
### Localization
|
||||
|
||||
* Update Korean translation (thanks to [@xenix4845](https://github.com/xenix4845))
|
||||
* Update Traditional Chinese translation (thanks to [@protonchang](https://github.com/protonchang))
|
||||
* Add Brazilian Portuguese translation (thanks to [@chiconws](https://github.com/chiconws) and [@Luccas-LF](https://github.com/Luccas-LF))
|
||||
* Add Swedish translation (thanks to [@acidflash](https://github.com/acidflash))
|
||||
* Add Catalan translation (thanks to [@Zagur](https://github.com/Zagur))
|
||||
* Add Turkish translation (thanks to [@Keylem](https://github.com/Keylem))
|
||||
|
||||
### Security
|
||||
|
||||
* Implemented a delay after failed login attempts to mitigate brute-force attacks
|
||||
* Upgraded dependencies to fix known security vulnerabilities
|
||||
|
||||
## 2.2.9 [c77981c](https://github.com/sipeed/NanoKVM/commit/c77981cc0ceebd8f6705b6c5d8c3cf4edf4f6717) (2025-06-13)
|
||||
|
||||
* fix(security): resolve parameter injection in serial port terminal
|
||||
|
||||
## 2.2.8 [01e28f1](https://github.com/sipeed/NanoKVM/commit/01e28f10ae8b581d484bb6077ddfe7bbe4e57919) (2025-05-22)
|
||||
|
||||
* feat: add AZERTY virtual keyboard Layout (thanks to [@felix068](https://github.com/felix068))
|
||||
* feat: add support for enabling/disabling HDMI output (PCIe version only) (thanks to [@tpretz](https://github.com/tpretz))
|
||||
* feat: add support for custom mouse wheel speed
|
||||
* fix: prevent direct H.264 stream buffer overflow and replay issues
|
||||
* perf: improve keyboard paste performance (thanks to [@ethanperrine](https://github.com/ethanperrine))
|
||||
* Localization
|
||||
* update Korean translation (thanks to [@kmw0410](https://github.com/kmw0410))
|
||||
* update Ukrainian translation (thanks to [@arbdevml](https://github.com/arbdevml))
|
||||
* update Russian translation (thanks to [@arbdevml](https://github.com/arbdevml))
|
||||
|
||||
## 2.2.7 [e18ec22](https://github.com/sipeed/NanoKVM/commit/e18ec2219d22886529575d1fdaad5c320e05f5b2) (2025-05-08)
|
||||
|
||||
* feat: add HTTPS support
|
||||
* feat: support direct H.264 streaming over HTTP
|
||||
* Localization
|
||||
* update Russian translation (thanks to [polyzium](https://github.com/polyzium))
|
||||
* update Dutch translation (thanks to [LeonStraathof](https://github.com/LeonStraathof))
|
||||
* update Ukrainian translation (thanks to [click0](https://github.com/click0))
|
||||
* update German translation (thanks to [3limin4tor](https://github.com/3limin4tor))
|
||||
|
||||
## 2.2.6 [c83dc55](https://github.com/sipeed/NanoKVM/commit/c83dc5565c9dbed22336661a8832edbd93a06d11) (2025-04-17)
|
||||
|
||||
* feat: add mouse jiggler to prevent system sleep
|
||||
* feat: add support for swap memory
|
||||
* feat: add support for customizing the device hostname
|
||||
* feat: add support for customizing the web page title
|
||||
* feat: add support for assigning custom names to Wake-on-LAN MAC addresses
|
||||
* feat: add confirmation prompts for power operations
|
||||
* feat: add logo to the login page (thanks to [S33G](https://github.com/S33G))
|
||||
* fix: fix possible privacy issues with MIC drivers. [Repair Record](https://github.com/sipeed/NanoKVM/commit/f9244b36df090a05cd59ba11ea4fd01e9b638995)
|
||||
* fix: fix iptables rule that could interfere with SSH connections (thanks to [scpcom](https://github.com/scpcom))
|
||||
* fix: fix the static IP gateway configuration might not apply correctly (thanks to [xitation](https://github.com/xitation))
|
||||
* perf: optimized OLED display handling and sleep logic
|
||||
* perf: improve H.264 streaming reliability by adding a heartbeat mechanism
|
||||
* perf: set the minimum screen size to 640x480
|
||||
* perf: add display of both wired and wireless IPv4 addresses in the settings page
|
||||
* perf: update the Thai language translations (thanks to [ChokunPlayZ](https://github.com/ChokunPlayZ))
|
||||
* chore: bump axios to 1.8.4
|
||||
* chore: bump golang.org/x/net to v0.39.0
|
||||
* chore: bump github.com/golang-jwt/jwt/v5 to to v5.2.2
|
||||
|
||||
## 2.2.5 [3286cc2](https://github.com/sipeed/NanoKVM/commit/3286cc2f85a14133d65935cb476c833dcf151459) (2025-03-26)
|
||||
|
||||
* fix: server crash caused by MJPEG frame detection error
|
||||
* feat: add HID-Only mode
|
||||
* feat: support preview updates
|
||||
* perf: improve image reading performance by optimizing screen parameters
|
||||
|
||||
## 2.2.4 [1bf986d](https://github.com/sipeed/NanoKVM/commit/1bf986d41b34d568c1ffee5df90ce61b6b08456b) (2025-03-21)
|
||||
|
||||
* fix: resolve USB initialization issue
|
||||
* fix: correct abnormal updates in certain models
|
||||
* perf: add version restrictions for production testing
|
||||
|
||||
## 2.2.3 [6ef83cb](https://github.com/sipeed/NanoKVM/commit/6ef83cb22fcd77f721d32c97c85d12f2bfc3035a) (2025-03-21)
|
||||
|
||||
* feat: add support for setting H.264 GOP
|
||||
* fix: resolve deadlock caused by HDMI resolution errors
|
||||
* perf: merge H.264 SPS and PPS into I-frame
|
||||
* perf: refactor MJPEG frame detection
|
||||
* perf: added more configuration options to the serial port terminal (thanks to [@mekaris](https://github.com/mekaris))
|
||||
* perf: improve the `update-nanokvm.py` script (thanks to [@reishoku](https://github.com/reishoku))
|
||||
* perf: disable mDNS by default in new products
|
||||
* perf: update log timestamp to millisecond precision
|
||||
* chore: bump Go to 1.23
|
||||
* chore: bump `golang.org/x/net` to v0.37.0
|
||||
|
||||
## 2.2.2 [58d5ab2](https://github.com/sipeed/NanoKVM/commit/58d5ab2d37244b1e1a68b925a5c23c324c489ad3) (2025-03-11)
|
||||
|
||||
* feat: add watchdog for NanoKVM-Server
|
||||
* feat: add support for UE chip
|
||||
* feat: support system reboot
|
||||
* feat: support enable/disable mDNS
|
||||
* fix: resolve UE chip cannot start server
|
||||
* perf: refactor automatic resolution detection
|
||||
* perf: add output prompt for unsupported resolutions
|
||||
* perf: add lock to kvmv_read_img
|
||||
* perf: configurable VENC automatic recycling feature
|
||||
* perf: add maximum limit for vi
|
||||
* perf: add tooltips to menu bar (thanks to [@S33G](https://github.com/S33G))
|
||||
* perf: menu bar is now draggable (thanks to [@forumi0721](https://github.com/forumi0721))
|
||||
* perf: image list support auto-refresh (thanks to [@forumi0721](https://github.com/forumi0721))
|
||||
* perf: update translations
|
||||
|
||||
## 2.2.1 [b5e48a0](https://github.com/sipeed/NanoKVM/commit/b5e48a07e82df3aedd60442342ae50b95684a697) (2025-02-21)
|
||||
|
||||
* fix: mounted image were not being detected correctly
|
||||
* perf: add support for CD-ROM mode when mounting image (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* perf: add a loading state during login
|
||||
* perf: add changelog link in settings
|
||||
* perf: update translation and cleanup the code (thanks to [@ChokunPlayZ](https://github.com/ChokunPlayZ) [@Stoufiler](https://github.com/Stoufiler) [@polyzium](https://github.com/polyzium) [@Jonher937](https://github.com/Jonher937) [@S33G](https://github.com/S33G))
|
||||
|
||||
## 2.2.0 [0dbf8c0](https://github.com/sipeed/NanoKVM/commit/0dbf8c007f2d0183d0f0601c3da6d3c3fccd8b31) (2025-02-17)
|
||||
|
||||
NanoKVM [Image v1.4.0](https://github.com/sipeed/NanoKVM/releases/tag/v1.4.0) has been released!
|
||||
|
||||
> Please refer to the [wiki](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/introduction.html) for more details about the image and application.
|
||||
|
||||
* fix: improve password update notification logic (thanks to [@li20034](https://github.com/li20034))
|
||||
* perf: increase update wait time to 10s (from 6s)
|
||||
* perf: update Korean translation (thanks to [@forumi0721](https://github.com/forumi0721))
|
||||
* perf: update Traditional Chinese translation (thanks to [@protonchang](https://github.com/protonchang))
|
||||
* refactor: update `libkvm.so` and `libkvm_mmf.so` libraries
|
||||
|
||||
## 2.1.6 [6eb4a4e](https://github.com/sipeed/NanoKVM/commit/6eb4a4ea6254f465a47f9881d13934c686649061) (2025-02-14)
|
||||
|
||||
* feat: support downloading image from online URL (thanks to [@Itxaka](https://github.com/Itxaka))
|
||||
* feat: add keyboard shortcut `Ctrl+Alt+Del` (thanks to [@CaffeeLake](https://github.com/CaffeeLake))
|
||||
* fix: fix the CSRF issue
|
||||
* perf: add an option to configure custom ICE servers (thanks to [@VMFortress](https://github.com/VMFortress))
|
||||
* perf: removed unnecessary modifications to DNS configuration
|
||||
* perf: add an SSH enable/disable toggle in the web UI
|
||||
* perf: add a Tailscale enable/disable toggle in the web UI
|
||||
* 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
|
||||
* 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`
|
||||
* refactor: web terminal use pty instead of SSH
|
||||
* refactor: move Tailscale APIs from the `network` module to the `extensions` module
|
||||
|
||||
## 2.1.5 [85f6447](https://github.com/sipeed/NanoKVM/commit/85f6447a16cc2591c6459b7d3dfda4d4cb75e98c) (2025-01-14)
|
||||
|
||||
* feat: add HDMI reset for NanoKVM-PCIe
|
||||
* fix: remove unnecessary lock acquisition during HID reset
|
||||
* refactor: refactor Tailscale
|
||||
|
||||
## 2.1.4 [d7ca7c4](https://github.com/sipeed/NanoKVM/commit/d7ca7c453d821ad099bf79b463969419041279cb) (2025-01-10)
|
||||
|
||||
* feat: support configuring OLED sleep settings
|
||||
* feat: support setting the `GOMEMLIMIT` environment variable
|
||||
* fix: fix Wi-Fi configuration
|
||||
* perf: password changes now update both the web user and the system root user
|
||||
* perf: add MAC address verification for Wake-on-LAN
|
||||
* refactor: a lot update to web UI
|
||||
* refactor: refactor Tailscale
|
||||
|
||||
## 2.1.3 [26078fe](https://github.com/sipeed/NanoKVM/commit/26078fe46e43d4543d7b09901b4992e4fbe4f01f) (2024-12-27)
|
||||
|
||||
* feat: add API to retrieve Wi-Fi information
|
||||
* fix: fix keyboard modifier keys
|
||||
* fix: update keyboard and mouse HID codes
|
||||
* fix: update hardware version information
|
||||
|
||||
## 2.1.2 [5a39562](https://github.com/sipeed/NanoKVM/commit/5a39562f2d32695933f4e7e86866136236cc9903) (2024-12-04)
|
||||
|
||||
* feat: add hardware version to configuration
|
||||
* feat: add Wi-Fi configuration support for NanoKVM-PCIe
|
||||
* perf: update web UI
|
||||
* chore: add dependency libraries
|
||||
|
||||
## 2.1.1 [74a303b](https://github.com/sipeed/NanoKVM/commit/74a303bd5cbb58f9d8ddd81abaaf4919dbbfb71b) (2024-11-06)
|
||||
|
||||
* feat: support h264
|
||||
160
README.md
160
README.md
@@ -6,66 +6,94 @@ NanoKVM
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> Quick Start </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> Hardware </a>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html">🚀 Quick Start</a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM">🛠️ Hardware Details</a>
|
||||
|
|
||||
<a href="https://github.com/sipeed/NanoKVM/releases/latest">💾 Firmware Releases</a>
|
||||
</h3>
|
||||
|
||||
English | [中文](./README_ZH.md) | [日本語](./README_JA.md)
|
||||
English | [中文](./README_ZH.md) | [日本語](./README_JA.md) | [한국어](./README_KO.md)
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> If you have any issues or suggestions, creating issue here, or tell us with [MaixHub Discussion](https://maixhub.com/discussion/nanokvm).
|
||||
## 🌟 What is NanoKVM?
|
||||
|
||||
## Opensource & Contribution
|
||||
NanoKVM is a series of compact, open-source IP-KVM devices. Built upon the LicheeRV Nano (RISC-V), NanoKVM allows you to remotely access and control computers as if you were sitting right in front of them – perfect for managing servers, embedded systems, or any headless machine.
|
||||
|
||||
### 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!
|
||||
## 📦 Product Family
|
||||
|
||||
### Update 2024.10.18
|
||||
We offer several NanoKVM versions to suit your needs:
|
||||
|
||||
We have opensourced 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 order 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.
|
||||
* **NanoKVM-Cube Lite:** A barebones kit for DIY enthusiasts and enterprise users needing bulk deployment.
|
||||
* **NanoKVM-Cube Full:** A complete package with a sleek case, accessories, and a pre-flashed system SD card. Ready to use out-of-the-box, ideal for individual users.
|
||||
* **NanoKVM-PCIe:** A unique form factor with a PCIe bracket for internal chassis mounting, drawing power directly from a PCIe slot. And add optional WiFi and PoE functions.
|
||||
|
||||
## Introduction
|
||||
### 🚀 NanoKVM-Pro: The 4K Powerhouse
|
||||
|
||||
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.
|
||||
Based on popular demand, we've upgraded NanoKVM to the **NanoKVM-Pro**:
|
||||
|
||||

|
||||
* **Resolution Upgrade:** From 1080P to stunning **4K@30fps / 2K@60fps**.
|
||||
* **Network Upgrade:** Blazing fast **GbE + PoE + WiFi 6** (up from 100M Ethernet).
|
||||
* **Latency Upgrade:** Encoder accelerated latency reduced from 100-150ms to **50-100ms**.
|
||||
|
||||
## Tech Specs
|
||||
> The NanoKVM-Pro is now available for pre-sale! [Click here for product details](https://sipeed.com/nanokvm/pro).
|
||||
|
||||
| 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(developing) | MJPEG, H264(developing) | 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 <br> ~1/30 PiKVM V4 size | 40x36x36mm <br/> ~1/7 PiKVM V4 size | 120x68x44mm |
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
## Hardware platform NanoKVM
|
||||
</div>
|
||||
|
||||
## 🛠️ Technical Specifications
|
||||
|
||||
| Product | NanoKVM-Pro | NanoKVM (Cube/PCIe) | GxxKVM | JxxKVM |
|
||||
|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |------------------------------------ |
|
||||
| Core | AX631 2xA53 1.5G | SG2002 1xC906 1.0G | RV1126 4xA7 1.5G | RV1106 1xA7 1.2G |
|
||||
| Memory & Storage | 1G LPDDR4X + 32G eMMC | 256M DDR3 + 32G microSD | 1G DDR3 + 8G eMMc | 256M DDR3 + 16G eMMC |
|
||||
| System | NanoKVM / PiKVM | NanoKVM | GxxKVM | JxxKVM |
|
||||
| Resolution | 4K@30fps / 2K@60fps | 1080P@60fps | 4K@30fps / 2K@60fps | 1080P@60fps |
|
||||
| HDMI Loopout | 4K loopout | x | x | x |
|
||||
| Video Encoding | MJPEG / H.264 / H.265 | MJPEG / H264 | MJPEG / H264 | MJPEG / H264 |
|
||||
| Audio Transmit | ✓ | x | ✓ | x |
|
||||
| UEFI / BIOS | ✓ | ✓ | ✓ | ✓ |
|
||||
| Emulated USB Keyboard & Mouse | ✓ | ✓ | ✓ | ✓ |
|
||||
| Emulated USB ISO | ✓ | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ | x |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ | ✓ |
|
||||
| Web Terminal | ✓ | ✓ | ✓ | ✓ |
|
||||
| Serial Terminal | 3 channels | 2 channels | x | 1 channel |
|
||||
| Custom Scripts | ✓ | ✓ | x | x |
|
||||
| Storage | 32G eMMC 300MB/s | 32G MicroSD 12MB/s | 8G eMMC 120MB/s | 8G eMMC 60MB/s |
|
||||
| Ethernet | 1000M | 100M | 1000M | 100M |
|
||||
| PoE | Optional | Optional | x | x |
|
||||
| WiFi | Optional WiFi6 | Optional WiFi6 | x | x |
|
||||
| ATX Power Control | ✓ | ✓ | Extra $15 | Extra $10 |
|
||||
| Display | 1.47" 320x172 LCD / 0.96" 128x64 OLED | 0.96" 128x64 OLED | - | 1.68" 280x240 |
|
||||
| More Features | Sync LED Strip / Smart Assistant | - | - | - |
|
||||
| Power Consumption | 0.4A@5V | 0.2A@5V | 0.4A@5V | 0.2A@5V |
|
||||
| Power Input | USB-C or PoE | USB-C | USB-C | USB-C |
|
||||
| Dimensions | 65x65x26mm | 40x36x36mm | 80x60x17.5mm | 60x43x(24~31)mm |
|
||||
| Price | ~~$79~~ $69 ATX / ~~$89~~ $79 Desk | $25 Lite / $50 Full(with ATX) | $89 no ATX / $102 with ATX | $69 no ATX / $79 with ATX |
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP update package
|
||||
│ ├── jpg_stream # Compatible for direct updates from very old versions
|
||||
│ ├── kvm_new_app # Triggers necessary components for kvm_system update
|
||||
│ ├── kvm_system # kvm_system application
|
||||
│ ├── server # NanoKVM front-end and back-end applications
|
||||
│ └── system # Necessary system components
|
||||
├── web # NanoKVM front-end
|
||||
├── server # NanoKVM back-end
|
||||
├── support # Auxiliary functions (image subsystem, system status, system updates, screen, keys, etc.)
|
||||
│ ├── sg2002 # NanoKVM-Lite/Full/PCIe
|
||||
│ └── h618 # NanoKVM-Pro
|
||||
├── ...
|
||||
```
|
||||
|
||||
## 🔩 Hardware Platform (NanoKVM Cube/PCIe)
|
||||
|
||||
NanoKVM is based on Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html),you can find specifications, schematics, and dimensional drawings [here](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano).
|
||||
|
||||
@@ -73,45 +101,39 @@ The NanoKVM Lite is constructed by the LicheeRV Nano and HDMItoCSI board, and th
|
||||
|
||||
The NanoKVM image is built on LicheeRV Nano SDK and MaixCDK, and is compatible with materials that use the LicheeRV Nano, opposite the KVM software cannot be used with the LicheeRV Nano or other SG2002 products. If you would like to build an HDMI input application on LicheeRV Nano or MaixCam, please contact us for technical support.
|
||||
|
||||
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
|
||||
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
|
||||
## 🤝 Contributing
|
||||
|
||||
**Enhancements:**
|
||||
We welcome contributions! Here's how you can help:
|
||||
|
||||
- [ ] Refactor the HDMI module and remove the libmaixcam_lib.so dependency
|
||||
- [ ] Disable SSH by default and add an enable/disable toggle in the web UI
|
||||
- [ ] Disable Tailscale by default and add an enable/disable toggle in the web UI
|
||||
- [ ] Add JWT configuration options to the configuration file
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch.
|
||||
3. Commit your changes.
|
||||
4. Push to the branch.
|
||||
5. Open a Pull Request.
|
||||
|
||||
**Distribution:**
|
||||
Please keep your pull requests small and focused to facilitate easier review and merging.
|
||||
|
||||
- [ ] Distribute applications via GitHub
|
||||
- [ ] Implement integrity checks for online updates
|
||||
- [ ] Support offline updates
|
||||
> 🎁 **Contributors who submit high-quality Pull Requests may receive a NanoKVM Cube, PCIe, or Pro as a token of our appreciation!**
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
- [ ] Fix the DNS issue
|
||||
- [ ] Fix the CSRF vulnerability
|
||||
|
||||
## Where to buy
|
||||
## 🛒 Where to Buy
|
||||
|
||||
* [Aliexpress(global except USA&Russia)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [Preorder (anyother country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm)
|
||||
* [Preorder (any other country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm)
|
||||
|
||||
|
||||
## Community
|
||||
## 💬 Community & Support
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
|
||||
## 📜 License
|
||||
|
||||
|
||||
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
|
||||
|
||||
112
README_JA.md
112
README_JA.md
@@ -1,30 +1,43 @@
|
||||
# NanoKVM
|
||||
NanoKVM
|
||||
======
|
||||
|
||||
<div align="center">
|
||||
|
||||

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

|
||||

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

|
||||

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

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html">🚀 빠른 시작</a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM">🛠️ 하드웨어 정보</a>
|
||||
|
|
||||
<a href="https://github.com/sipeed/NanoKVM/releases/latest">💾 펌웨어 릴리즈</a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | [中文](./README_ZH.md) | [日本語](./README_JA.md) | 한국어
|
||||
|
||||
</div>
|
||||
|
||||
## 🌟 NanoKVM이 무엇인가요?
|
||||
|
||||
NanoKVM은 소형 오픈 소스 IP-KVM 장치 시리즈입니다. LicheeRV Nano(RISC-V)를 기반으로 구축된 NanoKVM을 사용하면 마치 컴퓨터 앞에 바로 앉아있는 것처럼 원격으로 컴퓨터에 접근하고 제어할 수 있어 서버, 임베디드 시스템 또는 모든 헤드리스 머신을 관리하는 데 완벽합니다.
|
||||
|
||||
## 📦 제품군
|
||||
|
||||
다양한 요구를 충족할 수 있도록 여러 NanoKVM 버전을 제공합니다:
|
||||
|
||||
* **NanoKVM-Cube Lite:** DIY 사용자 및 대량 배포가 필요한 기업용을 위한 최소 구성 키트입니다.
|
||||
* **NanoKVM-Cube Full:** 세련된 케이스, 액세서리, 사전 플래시된 시스템 SD 카드가 포함된 완전 구성 패키지입니다. 개봉 후 즉시 사용할 수 있어 개인 사용자에게 적합합니다.
|
||||
* **NanoKVM-PCIe:** 독특한 폼팩터로, PCIe 브래킷을 통해 섀시 내부에 장착하며, 전원인 PCIe 슬롯에서 직접 공급받습니다. 선택적으로 WiFi와 PoE 기능을 추가할 수 있습니다.
|
||||
|
||||
### 🚀 NanoKVM-Pro: 4K를 위한 파워 머신
|
||||
|
||||
대중적인 수요를 기반으로, NanoKVM을 **NanoKVM-Pro**로 업그레이드했습니다:
|
||||
|
||||
* **해상도 업그레이드:** 1080P에서 놀라운 **4K@30fps / 2K@60fps**까지.
|
||||
* **네트워크 업그레이드:** 초고속 **GbE + PoE + WiFi 6** (100M 이더넷 대비 향상).
|
||||
* **지연 시간 업그레이드:** 인코더 가속 지연시간이 100-150ms에서 **50-100ms**로 감소.
|
||||
|
||||
> NanoKVM-Pro가 예약 판매 중입니다! [제품 세부 정보는 여기를 클릭하세요](https://sipeed.com/nanokvm/pro).
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 🛠️ 기술 사양
|
||||
|
||||
| 제품 | NanoKVM-Pro | NanoKVM (Cube/PCIe) | GxxKVM | JxxKVM |
|
||||
|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |------------------------------------ |
|
||||
| 코어 | AX631 2xA53 1.5G | SG2002 1xC906 1.0G | RV1126 4xA7 1.5G | RV1106 1xA7 1.2G |
|
||||
| 메모리 & 저장 공간 | 1G LPDDR4X + 32G eMMC | 256M DDR3 + 32G microSD | 1G DDR3 + 8G eMMc | 256M DDR3 + 16G eMMC |
|
||||
| 시스템 | NanoKVM / PiKVM | NanoKVM | GxxKVM | JxxKVM |
|
||||
| 해상도 | 4K@30fps / 2K@60fps | 1080P@60fps | 4K@30fps / 2K@60fps | 1080P@60fps |
|
||||
| HDMI 루프아웃 | 4K 루프아웃 | x | x | x |
|
||||
| 영상 인코딩 | MJPEG / H.264 / H.265 | MJPEG / H264 | MJPEG / H264 | MJPEG / H264 |
|
||||
| 오디오 전송 | ✓ | x | ✓ | x |
|
||||
| UEFI / BIOS | ✓ | ✓ | ✓ | ✓ |
|
||||
| USB 키보드 & 마우스 에뮬레이션 | ✓ | ✓ | ✓ | ✓ |
|
||||
| USB ISO 에뮬레이션 | ✓ | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ | x |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ | ✓ |
|
||||
| 웹 터미널 | ✓ | ✓ | ✓ | ✓ |
|
||||
| 시리얼 터미널 | 3 채널 | 2 채널 | x | 1 채널 |
|
||||
| 사용자 정의 스크립트 | ✓ | ✓ | x | x |
|
||||
| 저장 공간 | 32G eMMC 300MB/s | 32G MicroSD 12MB/s | 8G eMMC 120MB/s | 8G eMMC 60MB/s |
|
||||
| 이더넷 | 1000M | 100M | 1000M | 100M |
|
||||
| PoE | 옵션 | 옵션 | x | x |
|
||||
| WiFi | WiFi6 옵션 | WiFi6 옵션 | x | x |
|
||||
| ATX 전원 조작 | ✓ | ✓ | $15 추가 | $10 추가 |
|
||||
| 디스플레이 | 1.47" 320x172 LCD / 0.96" 128x64 OLED | 0.96" 128x64 OLED | - | 1.68" 280x240 |
|
||||
| 기타 기능 | LED 줄 동기화 / 스마트 어시스턴트 | - | - | - |
|
||||
| 전력 소비량 | 0.4A@5V | 0.2A@5V | 0.4A@5V | 0.2A@5V |
|
||||
| 전원 입력 | USB-C 또는 PoE | USB-C | USB-C | USB-C |
|
||||
| 크기 | 65x65x26mm | 40x36x36mm | 80x60x17.5mm | 60x43x(24~31)mm |
|
||||
| 가격 | ~~$79~~ $69 ATX / ~~$89~~ $79 Desk | $25 Lite / $50 Full(with ATX) | ATX 미사용 시 $89 / ATX 사용 시 $102 | ATX 미사용 시 $69 / ATX 사용 시 $79 |
|
||||
|
||||
## 📂 프로젝트 구조
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP 업데이트 패키지
|
||||
│ ├── jpg_stream # 매우 오래된 버전에서도 직접 업데이트 가능하도록 호환됨
|
||||
│ ├── kvm_new_app # kvm_system 업데이트에 필요한 구성 요소를 트리거함
|
||||
│ ├── kvm_system # kvm_system 애플리케이션
|
||||
│ ├── server # NanoKVM 프론트 엔드와 백엔드 애플리케이션
|
||||
│ └── system # 필요한 시스템 구성 요소
|
||||
├── web # NanoKVM 프론트엔드
|
||||
├── server # NanoKVM 백엔드
|
||||
├── support # 보조 기능 (이미지 하위 시스템, 시스템 상태, 시스템 업데이트, 화면, 키, 기타.)
|
||||
│ ├── sg2002 # NanoKVM-Lite/Full/PCIe
|
||||
│ └── h618 # NanoKVM-Pro
|
||||
├── ...
|
||||
```
|
||||
|
||||
## 🔩 하드웨어 플랫폼 (NanoKVM Cube/PCIe)
|
||||
|
||||
NanoKVM은 Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html)를 기반으로 하며,[여기](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)에서 사양, 도식, 치수, 도면은 여기에서 찾을 수 있습니다.
|
||||
|
||||
NanoKVM Lite는 LicheeRV Nano와 HDMItoCSI 보드로 구성되어 있으며, NanoKVM FULL은 NanoKVM Lite에 NasnoKVM-A/B 보드와 케이스를 추가한 구성입니다. HDMItoCSI 보드는 HDMI 신호를 변환하는 역할을 하고 NanoKVM-A는 OLED, ATX 전원 출력 (USB Type-C 인터페이스), 보조 전원 공급 장치, ATX 전원 켜기/끄기 및 리셋 버튼을 포함합니다. NanoKVM-B는 한쪽 끝이 컴퓨터와 연결되고 다른 한쪽 끝은 컴퓨터의 ATX 핀에 연결되어 컴퓨터의 전원을 원격으로 제어하는 데 사용됩니다.
|
||||
|
||||
NanoKVM 이미지는 LicheeRV Nano SDK와 MaixCDK로 구축되었으며, LicheeRV Nano를 사용하는 자재와 호환됩니다. 반면에 KVM 소프트웨어는 LicheeRV Nano나 다른 SG2002 제품과 함께 사용할 수 없습니다. 만약 LicheeRV Nano나 MaixCam에서 HDMI 입력 애플리케이션을 구축하고 싶으시다면, 기술 지원에 문의해 주세요.
|
||||
|
||||
참고: SG2002의 256MB 메모리 중에 158MB는 멀티미디어 하위 폴더에 할당되어 있으며, NanoKVM은 비디오 이미지 획득과 처리에 사용됩니다.
|
||||
|
||||
* [NanoKVM-A 도식](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
* [NanoKVM-B 도식](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_B_30131.pdf)
|
||||
* [NanoKVM 이미지](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||

|
||||
|
||||
## 🤝 기여
|
||||
|
||||
여러분의 기여를 환영합니다! 도움을 줄 수 있는 방법은 다음과 같습니다:
|
||||
|
||||
1. 레포지토리를 포크하세요
|
||||
2. 기능 브랜치를 만드세요
|
||||
3. 변경한 것을 커밋하세요
|
||||
4. 브랜치로 푸시하세요
|
||||
5. 풀 리퀘스트를 생성하세요
|
||||
|
||||
Please keep your pull requests small and focused to facilitate easier review and merging.
|
||||
|
||||
> 🎁 **고품질 풀 리퀘스트를 제출한 기여자는 감사의 표시로 NanoKVM Cube, PCIe, 또는 Pro를 받을 수 있습니다!**
|
||||
|
||||
## 🛒 어디서 구매하나요?
|
||||
|
||||
* [Aliexpress(미국&러시아를 제외한 전 세계)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [사전 주문 (Aliexpress나 Taobao를 지원하지 않는 국가)](https://sipeed.com/nanokvm)
|
||||
|
||||
## 💬 커뮤니티 & 지원
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
|
||||
## 📜 라이센스
|
||||
|
||||
이 프로젝트는 GPL-3.0 라이센스에 따라 라이센스가 부여됩니다. 자세한 사항은 LICENSE 파일을 참조하세요.
|
||||
139
README_ZH.md
139
README_ZH.md
@@ -6,54 +6,98 @@ NanoKVM
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html"> 快速开始 </a> |
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> 硬件资料 </a>
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html">🚀 快速开始</a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM">🛠️ 硬件信息</a>
|
||||
|
|
||||
<a href="https://github.com/sipeed/NanoKVM/releases/latest">💾 固件发布</a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | 中文 | [日本語](./README_JA.md)
|
||||
[English](./README.md) | 中文 | [日本語](./README_JA.md) | [한국어](./README_KO.md)
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> 如果您在使用过程中有任何问题或建议,请在这里提交 issue, 或在 [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)告诉我们.
|
||||
## 🌟 NanoKVM 是什么?
|
||||
|
||||
NanoKVM 是一系列紧凑型开源 IP-KVM 设备。NanoKVM 基于 LicheeRV Nano (RISC-V) 构建,让您能够身临其境般的远程访问和控制计算机,是管理服务器、嵌入式系统或任何无头设备的理想之选。
|
||||
|
||||
## 简介
|
||||
## 📦 产品系列
|
||||
|
||||
Lichee NanoKVM 是基于 LicheeRV Nano 的 IP-KVM 产品,继承了 LicheeRV Nano的极致体积 和 强大功能。
|
||||
Lichee NanoKVM分为两个版本:
|
||||
NanoKVM Lite 为基础版配置,适合 具有一定DIY能力的个人用户 和 有批量需求的企业用户。
|
||||
NanoKVM Full 为完整版配置,带精致外壳和完整配件,内置开机即用的系统镜像卡,推荐个人用户购买。
|
||||
我们提供多种 NanoKVM 版本以满足您的需求:
|
||||
|
||||

|
||||
* **NanoKVM-Cube Lite**:为 DIY 爱好者和需要批量部署的企业用户提供的准系统套件。
|
||||
* **NanoKVM-Cube Full**:包含外壳、配件以及预装系统 SD 卡的完整套装。开箱即用,非常适合个人用户。
|
||||
* **NanoKVM-PCIe**:独特的外形设计,配备 PCIe 支架,可安装在机箱内部,并直接从 PCIe 插槽供电。可选配 WiFi 和 PoE 功能。
|
||||
|
||||
## 参数
|
||||
### 🚀 NanoKVM-Pro
|
||||
|
||||
| 产品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
| --- | --- | --- | --- |
|
||||
| 计算单元 | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 分辨率 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| 视频编码 | MJPEG, H264(developing) | MJPEG, H264(developing) | MJPEG, H264 |
|
||||
| 视频延迟 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 模拟USB键鼠 | ✓ | ✓ | ✓ |
|
||||
| 模拟USB存储 | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX电源控制 | 无,用户可自行连接 | USB接口IO控制板 | RJ45接口IO控制板 |
|
||||
| OLED显示 | 无,用户可自行扩展 | 128x64 0.96" white | 128x32 0.91" white |
|
||||
| 外接串口 | 2路 | 2路 | 1路 |
|
||||
| TF卡 | 无,用户自备 | 有,开机即用 | 有 |
|
||||
| 扩展配件 | 无 | PoE | WiFi/LTE |
|
||||
| 功耗 | 0.2A@5V | 0.2A@5V | Peak 2.6A@5V |
|
||||
| 电源输入 | PC USB即可供电 | PC USB即可供电 <br> 也支持额外辅助供电 | 需要DC 5V 3A供电 |
|
||||
| 散热 | 静音无风扇 | 静音无风扇 | 需要风扇主动散热 |
|
||||
| 尺寸 | 23x37x15mm <br> ~1/30 PiKVM V4 体积 | 40x36x36mm <br/> ~1/7 PiKVM V4 体积 | 120x68x44mm |
|
||||
根据大众的需求,我们将 NanoKVM 升级到了 **NanoKVM-Pro**:
|
||||
|
||||

|
||||
* **分辨率升级**:从 1080P 升级到 **4K@30fps / 2K@60fps**。
|
||||
* **网络升级**:从百兆网口升级到超快的 **GbE + WiFi 6**。
|
||||
* **延迟升级**:编码器加速延迟从 100-150 毫秒减少到 **50-100 毫秒**。
|
||||
|
||||
## NanoKVM 硬件平台
|
||||
> NanoKVM-Pro 现已开启预售。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 🛠️ 技术规格
|
||||
|
||||
| 产品 | NanoKVM-Pro | NanoKVM (Cube/PCIe) | GxxKVM | JxxKVM |
|
||||
|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |------------------------------------ |
|
||||
| 核心 | AX631 2xA53 1.5G | SG2002 1xC906 1.0G | RV1126 4xA7 1.5G | RV1106 1xA7 1.2G |
|
||||
| 内存&存储 | 1G LPDDR4X + 32G eMMC | 256M DDR3 + 32G microSD | 1G DDR3 + 8G eMMc | 256M DDR3 + 16G eMMC |
|
||||
| 系统 | NanoKVM / PiKVM | NanoKVM | GxxKVM | JxxKVM |
|
||||
| 分辨率 | 4K@30fps / 2K@60fps | 1080P@60fps | 4K@30fps / 2K@60fps | 1080P@60fps |
|
||||
| HDMI 环出 | 4K 环出 | x | x | x |
|
||||
| 视频编码 | MJPEG / H.264 / H.265 | MJPEG / H264 | MJPEG / H264 | MJPEG / H264 |
|
||||
| 音频输出 | ✓ | x | ✓ | x |
|
||||
| UEFI / BIOS | ✓ | ✓ | ✓ | ✓ |
|
||||
| 模拟 USB 键鼠 | ✓ | ✓ | ✓ | ✓ |
|
||||
| 模拟 USB ISO | ✓ | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ | x |
|
||||
| 局域网唤醒 | ✓ | ✓ | ✓ | ✓ |
|
||||
| 网页终端 | ✓ | ✓ | ✓ | ✓ |
|
||||
| 串口终端 | 3 channels | 2 channels | x | 1 channel |
|
||||
| 用户脚本 | ✓ | ✓ | x | x |
|
||||
| 存储 | 32G eMMC 300MB/s | 32G MicroSD 12MB/s | 8G eMMC 120MB/s | 8G eMMC 60MB/s |
|
||||
| 网口 | 1000M | 100M | 1000M | 100M |
|
||||
| PoE | 可选 | 可选 | x | x |
|
||||
| WiFi | 可选 WiFi6 | 可选 WiFi6 | x | x |
|
||||
| ATX 电源控制 | ✓ | ✓ | ✓ | ✓ |
|
||||
| 显示屏 | 1.47" 320x172 LCD / 0.96" 128x64 OLED | 0.96" 128x64 OLED | - | 1.68" 280x240 |
|
||||
| 特色功能 | LED 同步灯带 / 智能助手 | - | - | - |
|
||||
| 功耗 | 0.4A@5V | 0.2A@5V | 0.4A@5V | 0.2A@5V |
|
||||
| 电源输入 | USB-C or PoE | USB-C | USB-C | USB-C |
|
||||
| 尺寸 | 65x65x26mm | 40x36x36mm | 80x60x17.5mm | 60x43x(24~31)mm |
|
||||
| 价格 | ~~$79~~ $69 ATX / ~~$89~~ $79 Desk | $25 Lite / $50 Full(带ATX) | $89 无ATX / $102 带ATX | $69 无ATX / $79 带ATX |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
``` shell
|
||||
├── kvmapp # APP更新包
|
||||
│ ├── jpg_stream # 兼容从非常老的版本中直接更新
|
||||
│ ├── kvm_new_app # 触发 kvm_system 更新必要组件
|
||||
│ ├── kvm_system # kvm_system 应用
|
||||
│ ├── server # NanoKVM 前后端应用
|
||||
│ └── system # 必要系统组件
|
||||
├── server # NanoKVM 后端
|
||||
├── support # 辅助功能(图像子系统、系统状态、系统更新、屏幕、按键……)
|
||||
│ ├── sg2002 # NanoKVM-Lite/Full/PCIe
|
||||
│ └── h618 # NanoKVM-Pro
|
||||
├── web # NanoKVM 前端
|
||||
├── LICENSE
|
||||
├── README_JA.md
|
||||
├── README.md
|
||||
├── README_ZH.md
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
## 🔩 硬件平台(NanoKVM Cube/PCIe)
|
||||
|
||||
NanoKVM 基于 Sipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html) 核心板搭建,这部分硬件的规格书、原理图、尺寸图等均可在这里找到:[点击这里](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)
|
||||
|
||||
@@ -63,18 +107,33 @@ 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)
|
||||
|
||||

|
||||
|
||||
## 购买渠道
|
||||
## 🤝 贡献代码
|
||||
|
||||
我们十分欢迎任何人来贡献代码!您可以通过以下方式提供帮助:
|
||||
|
||||
1. Fork 本仓库.
|
||||
2. 创建一个分支.
|
||||
3. 提交你的代码.
|
||||
4. 推送代码到分支.
|
||||
5. 创建一个PR.
|
||||
|
||||
请保持你的 PR 尽量简单且集中,以便于更轻松地审查和合并。
|
||||
|
||||
> 🎁 **提交高质量 PR 的贡献者可能会收到 NanoKVM Cube、PCIe 或 Pro 作为感谢礼物!**
|
||||
|
||||
## 🛒 购买渠道
|
||||
|
||||
* [Aliexpress(全球,除了美国和俄罗斯)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [Preorder (其它不支持淘宝和速卖通的国家)](https://sipeed.com/nanokvm)
|
||||
|
||||
## 社区
|
||||
## 💬 社区
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
|
||||
91
kvmapp/jpg_stream/S95nanokvm
Executable file
91
kvmapp/jpg_stream/S95nanokvm
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/bin/sh
|
||||
# nanokvm Rev2.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
|
||||
|
||||
# if [ ! -e /etc/kvm/hw ]
|
||||
# then
|
||||
|
||||
# fi
|
||||
|
||||
# echo 504 > /sys/class/gpio/export # pwr led
|
||||
# echo 505 > /sys/class/gpio/export # hdd led
|
||||
# echo 503 > /sys/class/gpio/export # pwr key
|
||||
# echo 507 > /sys/class/gpio/export # rst key
|
||||
|
||||
# echo in > /sys/class/gpio/gpio504/direction # pwr led
|
||||
# echo in > /sys/class/gpio/gpio505/direction # hdd led
|
||||
# echo out > /sys/class/gpio/gpio503/direction # pwr key
|
||||
# echo out > /sys/class/gpio/gpio507/direction # rst key
|
||||
|
||||
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
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 -A OUTPUT -o eth0 -p tcp --sport 8000 -m state --state ESTABLISHED -j DROP
|
||||
|
||||
cp -r /kvmapp/kvm_system /tmp/
|
||||
/tmp/kvm_system/kvm_system &
|
||||
|
||||
# if [ -e /kvmapp/kvm_stream ]
|
||||
# then
|
||||
# cp -r /kvmapp/kvm_stream /tmp/
|
||||
# /tmp/kvm_stream/kvm_stream &
|
||||
# fi
|
||||
|
||||
cp -r /kvmapp/server /tmp/
|
||||
/tmp/server/NanoKVM-Server &
|
||||
;;
|
||||
stop)
|
||||
killall kvm_system
|
||||
# killall kvm_stream
|
||||
killall NanoKVM-Server
|
||||
rm -r /tmp/kvm_system
|
||||
# rm -r /tmp/kvm_stream
|
||||
rm -r /tmp/server
|
||||
echo "OK"
|
||||
;;
|
||||
# restart_stream)
|
||||
# killall kvm_stream
|
||||
# rm -r /tmp/kvm_stream
|
||||
# if [ -e /kvmapp/kvm_stream ]
|
||||
# then
|
||||
# cp -r /kvmapp/kvm_stream /tmp/
|
||||
# /tmp/kvm_stream/kvm_stream &
|
||||
# echo "OK"
|
||||
# fi
|
||||
# ;;
|
||||
restart)
|
||||
killall kvm_system
|
||||
# killall kvm_stream
|
||||
killall NanoKVM-Server
|
||||
rm -r /tmp/kvm_system
|
||||
# rm -r /tmp/kvm_stream
|
||||
rm -r /tmp/server
|
||||
|
||||
cp -r /kvmapp/kvm_system /tmp/
|
||||
/tmp/kvm_system/kvm_system &
|
||||
|
||||
# if [ -e /kvmapp/kvm_stream ]
|
||||
# then
|
||||
# cp -r /kvmapp/kvm_stream /tmp/
|
||||
# /tmp/kvm_stream/kvm_stream &
|
||||
# fi
|
||||
|
||||
cp -r /kvmapp/server /tmp/
|
||||
/tmp/server/NanoKVM-Server &
|
||||
|
||||
sync
|
||||
|
||||
echo "OK"
|
||||
;;
|
||||
esac
|
||||
5
kvmapp/jpg_stream/jpg_stream
Executable file
5
kvmapp/jpg_stream/jpg_stream
Executable file
@@ -0,0 +1,5 @@
|
||||
rm /etc/init.d/S95webkvm
|
||||
cp /kvmapp/jpg_stream/S95nanokvm /etc/init.d/
|
||||
cp /kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so /kvmapp/kvm_system/dl_lib
|
||||
rm -r /kvmapp/jpg_stream
|
||||
/etc/init.d/S95nanokvm restart
|
||||
0
kvmapp/kvm_system/kvm_stream
Executable file
0
kvmapp/kvm_system/kvm_stream
Executable file
32
kvmapp/system/init.d/S00kmod
Executable file
32
kvmapp/system/init.d/S00kmod
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$1" = "start" ]
|
||||
then
|
||||
. /etc/profile
|
||||
printf "load kernel module: "
|
||||
cd /mnt/system/ko/
|
||||
insmod soph_sys.ko
|
||||
insmod soph_base.ko
|
||||
insmod soph_rtos_cmdqu.ko
|
||||
insmod soph_fast_image.ko
|
||||
insmod soph_mipi_rx.ko
|
||||
insmod soph_snsr_i2c.ko
|
||||
insmod soph_vi.ko
|
||||
insmod soph_vpss.ko
|
||||
insmod soph_dwa.ko
|
||||
insmod soph_vo.ko
|
||||
insmod soph_rgn.ko
|
||||
insmod soph_wdt.ko
|
||||
insmod soph_clock_cooling.ko
|
||||
insmod soph_tpu.ko
|
||||
insmod soph_vcodec.ko
|
||||
insmod soph_jpeg.ko
|
||||
insmod soph_vc_driver.ko MaxVencChnNum=9 MaxVdecChnNum=9
|
||||
insmod soph_rtc.ko
|
||||
insmod soph_ive.ko
|
||||
insmod soph_mon.ko
|
||||
insmod soph_pwm.ko
|
||||
insmod soph_wiegand.ko
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
42
kvmapp/system/init.d/S01fs
Executable file
42
kvmapp/system/init.d/S01fs
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$1" = "start" ]
|
||||
then
|
||||
# use all sdcard free space for data
|
||||
parted -s /dev/mmcblk0 "resizepart 2 -0"
|
||||
echo "yes
|
||||
8192MB
|
||||
" | parted ---pretend-input-tty /dev/mmcblk0 "resizepart 2 8192MB"
|
||||
# resize data filesystem
|
||||
(resize2fs /dev/mmcblk0p2) &
|
||||
|
||||
|
||||
. /etc/profile
|
||||
printf "mounting filesystem : "
|
||||
mkdir -p /boot
|
||||
mount -t vfat /dev/mmcblk0p1 /boot
|
||||
mount -t configfs configfs /sys/kernel/config
|
||||
mount -t debugfs debugfs /sys/kernel/debug
|
||||
|
||||
if [ -e /boot/usb.disk0 ]
|
||||
then
|
||||
if [ ! -e /etc/kvm.disk0 ]
|
||||
then
|
||||
touch /etc/kvm.disk0
|
||||
# use all sdcard free space for data
|
||||
parted -s /dev/mmcblk0 "mkpart primary 8193MB 100%"
|
||||
sleep 1
|
||||
# resize data filesystem
|
||||
(mkfs.exfat /dev/mmcblk0p3) &
|
||||
sleep 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -e /dev/mmcblk0p3 ]
|
||||
then
|
||||
mkdir -p /data
|
||||
mount /dev/mmcblk0p3 /data
|
||||
fi
|
||||
|
||||
echo "OK"
|
||||
fi
|
||||
152
kvmapp/system/init.d/S03usbdev
Executable file
152
kvmapp/system/init.d/S03usbdev
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/bin/sh
|
||||
# kvmhwd Rev2.1
|
||||
|
||||
start_usb_dev(){
|
||||
. /etc/profile
|
||||
echo "usb mode: device"
|
||||
cd /sys/kernel/config/usb_gadget
|
||||
|
||||
mkdir g0
|
||||
cd g0
|
||||
|
||||
# echo 0x3346 > idVendor
|
||||
# echo 0x1009 > idProduct
|
||||
if [ -e /boot/usb.vid ]
|
||||
then
|
||||
cat /boot/usb.vid > idVendor
|
||||
else
|
||||
echo 0x3346 > idVendor
|
||||
fi
|
||||
if [ -e /boot/usb.pid ]
|
||||
then
|
||||
cat /boot/usb.pid > idProduct
|
||||
else
|
||||
echo 0x1009 > idProduct
|
||||
fi
|
||||
mkdir strings/0x409
|
||||
echo '0123456789ABCDEF' > strings/0x409/serialnumber
|
||||
echo 'sipeed' > strings/0x409/manufacturer
|
||||
echo 'NanoKVM' > strings/0x409/product
|
||||
|
||||
mkdir configs/c.1
|
||||
echo 0xE0 > configs/c.1/bmAttributes
|
||||
echo 120 > configs/c.1/MaxPower
|
||||
mkdir configs/c.1/strings/0x409
|
||||
echo "NanoKVM" > configs/c.1/strings/0x409/configuration
|
||||
|
||||
if [ -e /boot/usb.ncm ]
|
||||
then
|
||||
mkdir functions/ncm.usb0
|
||||
ln -s functions/ncm.usb0 configs/c.1/
|
||||
else
|
||||
if [ -e /boot/usb.rndis0 ]
|
||||
then
|
||||
mkdir functions/rndis.usb0
|
||||
ln -s functions/rndis.usb0 configs/c.1/
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -e /boot/disable_hid ]
|
||||
then
|
||||
# keyboard
|
||||
mkdir functions/hid.GS0
|
||||
if [ -e /boot/BIOS ]
|
||||
then
|
||||
echo 1 > functions/hid.GS0/subclass
|
||||
fi
|
||||
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
|
||||
if [ -e /boot/BIOS ]
|
||||
then
|
||||
echo 1 > functions/hid.GS1/subclass
|
||||
fi
|
||||
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
|
||||
if [ -e /boot/BIOS ]
|
||||
then
|
||||
echo 1 > functions/hid.GS2/subclass
|
||||
fi
|
||||
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
|
||||
fi
|
||||
|
||||
if [ -e /boot/usb.disk0 ]
|
||||
then
|
||||
mkdir functions/mass_storage.disk0
|
||||
ln -s functions/mass_storage.disk0 configs/c.1/
|
||||
echo 1 > functions/mass_storage.disk0/lun.0/removable
|
||||
if [ -e /boot/usb.disk0.ro ]
|
||||
then
|
||||
echo 1 > functions/mass_storage.disk0/lun.0/ro
|
||||
echo 0 > functions/mass_storage.disk0/lun.0/cdrom
|
||||
fi
|
||||
echo "NanoKVM USB Mass Storage0520" > functions/mass_storage.disk0/lun.0/inquiry_string
|
||||
disk=$(cat /boot/usb.disk0)
|
||||
if [ -z "${disk}" ]
|
||||
then
|
||||
# if [ ! -e /mnt/usbdisk.img ]
|
||||
# then
|
||||
# fallocate -l 8G /mnt/usbdisk.img
|
||||
# mkfs.vfat /mnt/usbdisk.img
|
||||
# fi
|
||||
echo /dev/mmcblk0p3 > functions/mass_storage.disk0/lun.0/file
|
||||
else
|
||||
cat /boot/usb.disk0 > functions/mass_storage.disk0/lun.0/file
|
||||
fi
|
||||
fi
|
||||
|
||||
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
|
||||
105
kvmapp/system/init.d/S03usbhid
Executable file
105
kvmapp/system/init.d/S03usbhid
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/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
|
||||
if [ -e /boot/usb.vid ]
|
||||
then
|
||||
cat /boot/usb.vid > idVendor
|
||||
else
|
||||
echo 0x3346 > idVendor
|
||||
fi
|
||||
if [ -e /boot/usb.pid ]
|
||||
then
|
||||
cat /boot/usb.pid > idProduct
|
||||
else
|
||||
echo 0x1009 > idProduct
|
||||
fi
|
||||
mkdir strings/0x409
|
||||
# echo '0' > strings/0x409/serialnumber
|
||||
echo 'sipeed' > strings/0x409/manufacturer
|
||||
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
|
||||
234
kvmapp/system/init.d/S15kvmhwd
Executable file
234
kvmapp/system/init.d/S15kvmhwd
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
# kvmhwd Rev2.4
|
||||
|
||||
Alpha_OLED_RST_Pin=371
|
||||
Beta_OLED_RST_Pin=502
|
||||
Beta_OLED_SCL=595
|
||||
Beta_OLED_SDA=507
|
||||
WiFi_EN_Pin=506
|
||||
Alpha_PWR_LED_Pin=504
|
||||
Alpha_HDD_LED_Pin=505
|
||||
Alpha_PWR_KEY_Pin=503
|
||||
Alpha_RST_KEY_Pin=507
|
||||
Beta_PWR_LED_Pin=504
|
||||
Beta_PWR_KEY_Pin=503
|
||||
Beta_RST_KEY_Pin=505
|
||||
PCIe_HDMI_RST_Pin=451
|
||||
|
||||
init_alpha_hw(){
|
||||
devmem 0x030010D0 32 0x2 # I2C1_SCL
|
||||
devmem 0x030010DC 32 0x2 # I2C1_SDA
|
||||
devmem 0x030010D4 32 0x3 # OLED_RST
|
||||
devmem 0x0300103C 32 0x3 # GPIOA15
|
||||
devmem 0x03001050 32 0x3 # GPIOA22
|
||||
devmem 0x0300105C 32 0x3 # GPIOA23
|
||||
devmem 0x03001060 32 0x3 # GPIOA24
|
||||
devmem 0x03001054 32 0x3 # GPIOA25
|
||||
devmem 0x03001058 32 0x3 # GPIOA27
|
||||
|
||||
devmem 0x03001068 32 0x6 # GPIOA 18 UART1 RX
|
||||
devmem 0x03001064 32 0x6 # GPIOA 19 UART1 TX
|
||||
devmem 0x03001070 32 0x2 # GPIOA 28 UART2 TX
|
||||
devmem 0x03001074 32 0x2 # GPIOA 29 UART2 RX
|
||||
|
||||
echo ${Alpha_OLED_RST_Pin} > /sys/class/gpio/export # OLED_RST
|
||||
echo out > /sys/class/gpio/gpio${Alpha_OLED_RST_Pin}/direction
|
||||
echo 1 > /sys/class/gpio/gpio${Alpha_OLED_RST_Pin}/value
|
||||
|
||||
echo ${Alpha_PWR_LED_Pin} > /sys/class/gpio/export # pwr led
|
||||
echo ${Alpha_HDD_LED_Pin} > /sys/class/gpio/export # hdd led
|
||||
echo ${Alpha_PWR_KEY_Pin} > /sys/class/gpio/export # pwr key
|
||||
echo ${Alpha_RST_KEY_Pin} > /sys/class/gpio/export # rst key
|
||||
|
||||
echo in > /sys/class/gpio/gpio${Alpha_PWR_LED_Pin}/direction # pwr led
|
||||
echo in > /sys/class/gpio/gpio${Alpha_HDD_LED_Pin}/direction # hdd led
|
||||
echo out > /sys/class/gpio/gpio${Alpha_PWR_KEY_Pin}/direction # pwr key
|
||||
echo out > /sys/class/gpio/gpio${Alpha_RST_KEY_Pin}/direction # rst key
|
||||
|
||||
rmmod /mnt/system/ko/i2c-gpio.ko
|
||||
rmmod /mnt/system/ko/i2c-algo-bit.ko
|
||||
|
||||
# rm /etc/init.d/S25wifimod
|
||||
# rm /etc/init.d/S30wifi
|
||||
}
|
||||
|
||||
init_beta_pcie_hw(){
|
||||
devmem 0x0300103C 32 0x3 # GPIOA15
|
||||
devmem 0x03001050 32 0x3 # GPIOA22
|
||||
devmem 0x0300105C 32 0x3 # GPIOA23
|
||||
devmem 0x03001060 32 0x3 # GPIOA24
|
||||
devmem 0x03001054 32 0x3 # GPIOA25
|
||||
devmem 0x03001058 32 0x3 # GPIOA27
|
||||
|
||||
devmem 0x030010E4 32 0x0 # SDIO CLK
|
||||
devmem 0x030010E0 32 0x0 # SDIO CMD
|
||||
devmem 0x030010DC 32 0x0 # SDIO D0
|
||||
devmem 0x030010D8 32 0x0 # SDIO D1
|
||||
devmem 0x030010D4 32 0x0 # SDIO D2
|
||||
devmem 0x030010D0 32 0x0 # SDIO D3
|
||||
|
||||
devmem 0x03001068 32 0x6 # GPIOA 18 UART1 RX
|
||||
devmem 0x03001064 32 0x6 # GPIOA 19 UART1 TX
|
||||
devmem 0x03001070 32 0x2 # GPIOA 28 UART2 TX
|
||||
devmem 0x03001074 32 0x2 # GPIOA 29 UART2 RX
|
||||
|
||||
echo ${Beta_OLED_RST_Pin} > /sys/class/gpio/export # Beta OLED_RST
|
||||
echo out > /sys/class/gpio/gpio${Beta_OLED_RST_Pin}/direction
|
||||
echo 1 > /sys/class/gpio/gpio${Beta_OLED_RST_Pin}/value
|
||||
|
||||
echo ${Beta_PWR_LED_Pin} > /sys/class/gpio/export # pwr led
|
||||
echo ${Beta_PWR_KEY_Pin} > /sys/class/gpio/export # pwr key
|
||||
echo ${Beta_RST_KEY_Pin} > /sys/class/gpio/export # rst key
|
||||
echo ${PCIe_HDMI_RST_Pin} > /sys/class/gpio/export # hdmi rst key
|
||||
|
||||
echo in > /sys/class/gpio/gpio${Beta_PWR_LED_Pin}/direction # pwr led
|
||||
echo out > /sys/class/gpio/gpio${Beta_PWR_KEY_Pin}/direction # pwr key
|
||||
echo out > /sys/class/gpio/gpio${Beta_RST_KEY_Pin}/direction # rst key
|
||||
echo out > /sys/class/gpio/gpio${PCIe_HDMI_RST_Pin}/direction # rst key
|
||||
|
||||
echo 1 > /sys/class/gpio/gpio${PCIe_HDMI_RST_Pin}/value # rst key
|
||||
|
||||
rmmod /mnt/system/ko/i2c-gpio.ko
|
||||
rmmod /mnt/system/ko/i2c-algo-bit.ko
|
||||
insmod /mnt/system/ko/i2c-algo-bit.ko
|
||||
insmod /mnt/system/ko/i2c-gpio.ko
|
||||
}
|
||||
|
||||
kvm_hw_detect(){
|
||||
if [ ! -e /etc/kvm/hdmi_version ]
|
||||
then
|
||||
rm /etc/kvm/hw
|
||||
fi
|
||||
if [ -e /etc/kvm/hw ]
|
||||
then
|
||||
echo "/etc/kvm/hw exist"
|
||||
else
|
||||
echo "/etc/kvm/hw not exist"
|
||||
if [ -d "/etc/kvm/" ]
|
||||
then
|
||||
echo "/etc/kvm/ exist"
|
||||
else
|
||||
mkdir /etc/kvm/
|
||||
fi
|
||||
devmem 0x0300104C 32 0x3 # GPIOA26 / WiFi_EN
|
||||
echo ${WiFi_EN_Pin} > /sys/class/gpio/export # WiFi_EN
|
||||
echo out > /sys/class/gpio/gpio${WiFi_EN_Pin}/direction
|
||||
echo 0 > /sys/class/gpio/gpio${WiFi_EN_Pin}/value
|
||||
|
||||
devmem 0x0300103C 32 0x3 # GPIOA15
|
||||
devmem 0x03001050 32 0x3 # GPIOA22
|
||||
devmem 0x0300105C 32 0x3 # GPIOA23
|
||||
devmem 0x03001060 32 0x3 # GPIOA24
|
||||
devmem 0x03001054 32 0x3 # GPIOA25
|
||||
devmem 0x03001058 32 0x3 # GPIOA27
|
||||
|
||||
devmem 0x030010D0 32 0x2 # I2C1_SCL
|
||||
devmem 0x030010DC 32 0x2 # I2C1_SDA
|
||||
devmem 0x030010D4 32 0x3 # OLED_RST
|
||||
echo ${Alpha_OLED_RST_Pin} > /sys/class/gpio/export # OLED_RST
|
||||
echo out > /sys/class/gpio/gpio${Alpha_OLED_RST_Pin}/direction
|
||||
echo 1 > /sys/class/gpio/gpio${Alpha_OLED_RST_Pin}/value
|
||||
|
||||
kvm_tmp=$(i2cdetect -ry 1 0x3d 0x3d | grep 3d)
|
||||
if [ -n "$kvm_tmp" ]
|
||||
then
|
||||
# alpha hw
|
||||
echo "alpha" > /etc/kvm/hw
|
||||
else
|
||||
# beta/pcie hw
|
||||
echo ${Alpha_OLED_RST_Pin} > /sys/class/gpio/unexport # OLED_RST
|
||||
echo ${Beta_OLED_RST_Pin} > /sys/class/gpio/export # Beta_OLED_RST_Pin
|
||||
echo out > /sys/class/gpio/gpio${Beta_OLED_RST_Pin}/direction
|
||||
echo 1 > /sys/class/gpio/gpio${Beta_OLED_RST_Pin}/value
|
||||
|
||||
rmmod /mnt/system/ko/i2c-gpio.ko
|
||||
rmmod /mnt/system/ko/i2c-algo-bit.ko
|
||||
insmod /mnt/system/ko/i2c-algo-bit.ko
|
||||
insmod /mnt/system/ko/i2c-gpio.ko
|
||||
|
||||
kvm_tmp=$(i2cdetect -ry 4 0x2c 0x2c | grep 2c)
|
||||
if [ -n "$kvm_tmp" ]
|
||||
then
|
||||
echo "c" > /etc/kvm/hdmi_version
|
||||
else
|
||||
kvm_tmp1=$(i2cdetect -ry 4 0x2b 0x2b | grep 2b)
|
||||
if [ -n "$kvm_tmp1" ]
|
||||
then
|
||||
echo "ux" > /etc/kvm/hdmi_version
|
||||
else
|
||||
echo "ue" > /etc/kvm/hdmi_version
|
||||
fi
|
||||
fi
|
||||
|
||||
kvm_tmp=$(i2cdetect -ry 5 0x3c 0x3c | grep 3c)
|
||||
if [ -n "$kvm_tmp" ]
|
||||
then
|
||||
echo "pcie" > /etc/kvm/hw
|
||||
sync
|
||||
reboot
|
||||
else
|
||||
echo "beta" > /etc/kvm/hw
|
||||
# rm /etc/init.d/S25wifimod
|
||||
# rm /etc/init.d/S30wifi
|
||||
sync
|
||||
fi
|
||||
|
||||
echo 1 > /sys/class/gpio/gpio${WiFi_EN_Pin}/value
|
||||
echo ${WiFi_EN_Pin} > /sys/class/gpio/unexport # WiFi_EN
|
||||
fi
|
||||
fi
|
||||
sync
|
||||
}
|
||||
|
||||
kvm_hw_init(){
|
||||
FIND_FILE="/etc/kvm/hw"
|
||||
if [ `grep -c "alpha" $FIND_FILE` -ne '0' ]
|
||||
then
|
||||
echo "hw = alpha!"
|
||||
init_alpha_hw
|
||||
fi
|
||||
if [ `grep -c "beta" $FIND_FILE` -ne '0' ]
|
||||
then
|
||||
echo "hw = beta!"
|
||||
init_beta_pcie_hw
|
||||
fi
|
||||
if [ `grep -c "pcie" $FIND_FILE` -ne '0' ]
|
||||
then
|
||||
echo "hw = pcie!"
|
||||
init_beta_pcie_hw
|
||||
fi
|
||||
}
|
||||
|
||||
kvm_hdmi_version_detect(){
|
||||
hdmi_tmp=$(i2cdetect -ry 4 0x2c 0x2c | grep 2c)
|
||||
if [ -n "$hdmi_tmp" ]
|
||||
then
|
||||
echo "c" > /etc/kvm/hdmi_version
|
||||
else
|
||||
hdmi_tmp1=$(i2cdetect -ry 4 0x2b 0x2b | grep 2b)
|
||||
if [ -n "$hdmi_tmp1" ]
|
||||
then
|
||||
echo "ux" > /etc/kvm/hdmi_version
|
||||
else
|
||||
echo "ue" > /etc/kvm/hdmi_version
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
kvm_hw_detect
|
||||
kvm_hw_init
|
||||
;;
|
||||
re-detect)
|
||||
rm /etc/kvm/hw
|
||||
kvm_hw_detect
|
||||
kvm_hw_init
|
||||
;;
|
||||
re-init)
|
||||
kvm_hw_init
|
||||
;;
|
||||
get_hdmi_version)
|
||||
kvm_hdmi_version_detect
|
||||
;;
|
||||
esac
|
||||
67
kvmapp/system/init.d/S30eth
Executable file
67
kvmapp/system/init.d/S30eth
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
|
||||
RESERVE_INET="192.168.90.1/22"
|
||||
|
||||
clean_line() {
|
||||
echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/\r//g'
|
||||
}
|
||||
|
||||
calc_gw() {
|
||||
addr="$1"
|
||||
netid="$2"
|
||||
IFS=. read a b c d <<EOF
|
||||
$addr
|
||||
EOF
|
||||
ipnum=$(( (a<<24) + (b<<16) + (c<<8) + d ))
|
||||
mask=$(( 0xFFFFFFFF << (32-netid) & 0xFFFFFFFF ))
|
||||
gwnum=$(( (ipnum & mask) | 1 ))
|
||||
echo "$(( (gwnum>>24)&255 )).$(( (gwnum>>16)&255 )).$(( (gwnum>>8)&255 )).$(( gwnum&255 ))"
|
||||
}
|
||||
|
||||
start() {
|
||||
if [ -e /boot/eth.nodhcp ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line=$(clean_line "$line")
|
||||
[ -z "$line" ] && continue
|
||||
set -- $line
|
||||
inet="$1"
|
||||
gw="$2"
|
||||
[ -z "$inet" ] && continue
|
||||
addr=${inet%/*}
|
||||
netid=${inet#*/}
|
||||
[ -z "$gw" ] && gw=$(calc_gw "$addr" "$netid")
|
||||
arping -Dqc2 -Ieth0 "$addr" || continue
|
||||
ip a add "$inet" brd + dev eth0
|
||||
ip r add default via "$gw" dev eth0
|
||||
echo -e "nameserver $gw" >> /etc/resolv.conf
|
||||
break
|
||||
done < /boot/eth.nodhcp
|
||||
|
||||
ip a show dev eth0 | grep inet > /dev/null || {
|
||||
udhcpc -i eth0 -t 3 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &>/dev/null
|
||||
ip a show dev eth0 | grep inet > /dev/null
|
||||
} || {
|
||||
inet=$RESERVE_INET
|
||||
addr=${inet%/*}
|
||||
ip a add "$inet" brd + dev eth0
|
||||
} || exit 1
|
||||
else
|
||||
udhcpc -i eth0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
[ ! -e "/run/udhcpc.eth0.pid" ] && exit 1
|
||||
kill "$(cat /run/udhcpc.eth0.pid)"
|
||||
rm /run/udhcpc.eth0.pid
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
175
kvmapp/system/init.d/S30wifi
Executable file
175
kvmapp/system/init.d/S30wifi
Executable file
@@ -0,0 +1,175 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /etc/profile
|
||||
|
||||
gen_hostapd_conf() {
|
||||
ssid="${1}"
|
||||
pass="${2}"
|
||||
echo "ctrl_interface=/var/run/hostapd"
|
||||
echo "ctrl_interface_group=0"
|
||||
echo "ssid=${ssid}"
|
||||
echo "hw_mode=g"
|
||||
echo "channel=1"
|
||||
echo "beacon_int=100"
|
||||
echo "dtim_period=2"
|
||||
echo "max_num_sta=255"
|
||||
echo "rts_threshold=-1"
|
||||
echo "fragm_threshold=-1"
|
||||
echo "macaddr_acl=0"
|
||||
echo "auth_algs=3"
|
||||
echo "wpa=2"
|
||||
echo "wpa_passphrase=${pass}"
|
||||
echo "ieee80211n=1"
|
||||
}
|
||||
|
||||
gen_udhcpd_conf() {
|
||||
interface=${1}
|
||||
ipv4_prefix=${2}
|
||||
echo "start ${ipv4_prefix}.100"
|
||||
echo "end ${ipv4_prefix}.200"
|
||||
echo "interface ${interface}"
|
||||
echo "pidfile /var/run/udhcpd.${interface}.pid"
|
||||
echo "lease_file /var/lib/misc/udhcpd.${interface}.leases"
|
||||
echo "option subnet 255.255.255.0"
|
||||
echo "option lease 864000"
|
||||
}
|
||||
|
||||
gen_dnsmasq_conf() {
|
||||
ipv4_prefix=${1}
|
||||
echo "bind-interfaces"
|
||||
echo "interface=wlan0"
|
||||
echo "address=/config.kvm/${ipv4_prefix}.1"
|
||||
echo "no-hosts"
|
||||
echo "dhcp-range=${ipv4_prefix}.2,${ipv4_prefix}.254"
|
||||
echo "dhcp-option=6,${ipv4_prefix}.1"
|
||||
echo "dhcp-option=3,${ipv4_prefix}.1"
|
||||
echo "dhcp-authoritative"
|
||||
}
|
||||
|
||||
start() {
|
||||
echo "wifi mode: sta"
|
||||
ssid=""
|
||||
pass=""
|
||||
if [ -e /boot/wifi.ssid ] && [ -e /boot/wifi.pass ]; then
|
||||
echo "Updating WiFi credentials from /boot to /etc/kvm/"
|
||||
|
||||
rm -f /etc/kvm/wifi.ssid /etc/kvm/wifi.pass
|
||||
|
||||
mv /boot/wifi.ssid /etc/kvm/wifi.ssid
|
||||
mv /boot/wifi.pass /etc/kvm/wifi.pass
|
||||
|
||||
chown root:root /etc/kvm/wifi.ssid /etc/kvm/wifi.pass
|
||||
chmod 644 /etc/kvm/wifi.ssid /etc/kvm/wifi.pass
|
||||
fi
|
||||
if [ -e /etc/kvm/wifi.ssid ]
|
||||
then
|
||||
ssid=`cat /etc/kvm/wifi.ssid`
|
||||
fi
|
||||
if [ -e /etc/kvm/wifi.pass ]
|
||||
then
|
||||
pass=`cat /etc/kvm/wifi.pass`
|
||||
fi
|
||||
echo "ctrl_interface=/var/run/wpa_supplicant" > /etc/wpa_supplicant.conf
|
||||
wpa_passphrase "$ssid" "$pass" >> /etc/wpa_supplicant.conf
|
||||
wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf
|
||||
if [ ! -e /boot/wifi.nodhcp ]
|
||||
then
|
||||
(udhcpc -i wlan0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.wlan0.pid) &
|
||||
fi
|
||||
}
|
||||
|
||||
ap_start() {
|
||||
echo "wifi mode: ap"
|
||||
id2=$(printf "%d" 0x$(sha512sum /sys/class/cvi-base/base_uid | head -c 2))
|
||||
id3=$(printf "%d" 0x$(sha512sum /sys/class/cvi-base/base_uid | head -c 4 | tail -c 2))
|
||||
if [ "$id2" = "$id3" ]
|
||||
then
|
||||
id2=$((id2 + 1))
|
||||
fi
|
||||
if [ "$id2" -ge 255 ]
|
||||
then
|
||||
id2=253
|
||||
fi
|
||||
if [ "$id3" -ge 255 ]
|
||||
then
|
||||
id3=254
|
||||
fi
|
||||
ssid=""
|
||||
pass=""
|
||||
if [ -e /kvmapp/kvm/ap.ssid ]
|
||||
then
|
||||
ssid=`cat /kvmapp/kvm/ap.ssid`
|
||||
fi
|
||||
if [ -e /kvmapp/kvm/ap.pass ]
|
||||
then
|
||||
pass=`cat /kvmapp/kvm/ap.pass`
|
||||
fi
|
||||
gen_hostapd_conf "$ssid" "$pass" > /etc/hostapd.conf
|
||||
ipv4_prefix=10.$id3.$id2
|
||||
if [ ! -e /etc/udhcpd.wlan0.conf ]
|
||||
then
|
||||
gen_udhcpd_conf wlan0 "${ipv4_prefix}" > /etc/udhcpd.wlan0.conf
|
||||
fi
|
||||
# gen_dnsmasq_conf "${ipv4_prefix}" > /etc/dnsmasq.conf
|
||||
|
||||
# iptables --policy INPUT ACCEPT
|
||||
# iptables --policy FORWARD ACCEPT
|
||||
# iptables --policy OUTPUT ACCEPT
|
||||
# iptables -F
|
||||
# iptables -t nat -F
|
||||
# iptables -t nat -A PREROUTING -i wlan0 -p udp --dport 53 -j DNAT --to ${ipv4_prefix}.1
|
||||
|
||||
ifconfig wlan0 up
|
||||
ip route del default || true
|
||||
ip add flush dev wlan0
|
||||
ip addr add $ipv4_prefix.1/24 dev wlan0
|
||||
# dnsmasq --pid-file=/tmp/dnsmasq.run -C /etc/dnsmasq.conf
|
||||
hostapd -B -i wlan0 /etc/hostapd.conf
|
||||
udhcpd -S /etc/udhcpd.wlan0.conf
|
||||
}
|
||||
|
||||
stop() {
|
||||
ps -ef|grep hostapd|grep -v grep|awk '{print $1}'|xargs kill -2 || true
|
||||
ps -ef|grep "udhcpd -S /etc/udhcpd.wlan0.conf" |grep -v grep|awk '{print $1}'|xargs kill -2 || true
|
||||
killall wpa_supplicant || true
|
||||
if [ -e /run/udhcpc.wlan0.pid ]
|
||||
then
|
||||
kill `cat /run/udhcpc.wlan0.pid` || true
|
||||
rm -f /run/udhcpc.wlan0.pid
|
||||
fi
|
||||
if [ -e /var/run/udhcpd.wlan0.pid ]
|
||||
then
|
||||
kill `cat /var/run/udhcpd.wlan0.pid` || true
|
||||
rm -f /var/run/udhcpd.wlan0.pid
|
||||
fi
|
||||
if [ -e /tmp/dnsmasq.run ]
|
||||
then
|
||||
kill `cat /tmp/dnsmasq.run` || true
|
||||
rm -f /tmp/dnsmasq.run
|
||||
fi
|
||||
# if [ -e /etc/wpa_supplicant.conf]
|
||||
# then
|
||||
# echo > /etc/wpa_supplicant.conf
|
||||
# fi
|
||||
airmon-ng stop wlan0mon || true
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
if [ "${1}" = "start" ]
|
||||
then
|
||||
start
|
||||
elif [ "${1}" = "stop" ]
|
||||
then
|
||||
stop
|
||||
elif [ "${1}" = "ap" ]
|
||||
then
|
||||
stop
|
||||
ap_start
|
||||
elif [ "${1}" = "restart" ]
|
||||
then
|
||||
restart
|
||||
fi
|
||||
20
kvmapp/system/init.d/S50avahi-daemon
Executable file
20
kvmapp/system/init.d/S50avahi-daemon
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# avahi-daemon init script
|
||||
|
||||
DAEMON=/usr/sbin/avahi-daemon
|
||||
case "$1" in
|
||||
start)
|
||||
$DAEMON -c || $DAEMON -D
|
||||
;;
|
||||
stop)
|
||||
$DAEMON -c && $DAEMON -k
|
||||
;;
|
||||
reload)
|
||||
$DAEMON -c && $DAEMON -r
|
||||
;;
|
||||
*)
|
||||
echo "Usage: S50avahi-daemon {start|stop|reload}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
49
kvmapp/system/init.d/S50ssdpd
Executable file
49
kvmapp/system/init.d/S50ssdpd
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
DAEMON=ssdpd
|
||||
PIDFILE=/var/run/$DAEMON.pid
|
||||
CFGFILE=/etc/default/$DAEMON
|
||||
|
||||
DAEMON_ARGS=""
|
||||
|
||||
# Read configuration variable file if it is present
|
||||
# shellcheck source=/dev/null
|
||||
[ -r "$CFGFILE" ] && . "$CFGFILE"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
start() {
|
||||
printf 'Starting %s: ' "$DAEMON"
|
||||
if start-stop-daemon -S -q -p "$PIDFILE" -x "$DAEMON" -- $DAEMON_ARGS; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
printf 'Stopping %s: ' "$DAEMON"
|
||||
if start-stop-daemon -K -q -p "$PIDFILE" -x "$DAEMON"; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start|stop|restart)
|
||||
"$1"
|
||||
;;
|
||||
reload)
|
||||
restart
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|reload}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit $?
|
||||
73
kvmapp/system/init.d/S50sshd
Executable file
73
kvmapp/system/init.d/S50sshd
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# sshd Starts sshd.
|
||||
#
|
||||
|
||||
# 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() {
|
||||
# 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"
|
||||
}
|
||||
|
||||
start() {
|
||||
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 2>/dev/null
|
||||
rm -f /var/lock/sshd
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
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 0
|
||||
29
kvmapp/system/init.d/S80dnsmasq
Executable file
29
kvmapp/system/init.d/S80dnsmasq
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
DAEMON="dnsmasq"
|
||||
PIDFILE="/var/run/$DAEMON.pid"
|
||||
|
||||
[ -f /etc/dnsmasq.conf ] || exit 0
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
printf "Starting dnsmasq: "
|
||||
start-stop-daemon -S -p "$PIDFILE" -x "/usr/sbin/$DAEMON" -- \
|
||||
--pid-file="$PIDFILE"
|
||||
[ $? = 0 ] && echo "OK" || echo "FAIL"
|
||||
;;
|
||||
stop)
|
||||
printf "Stopping dnsmasq: "
|
||||
start-stop-daemon -K -q -p "$PIDFILE" -x "/usr/sbin/$DAEMON"
|
||||
[ $? = 0 ] && echo "OK" || echo "FAIL"
|
||||
;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
69
kvmapp/system/init.d/S95nanokvm
Executable file
69
kvmapp/system/init.d/S95nanokvm
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# nanokvm Rev3.1
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo -n kvm > /boot/hostname.prefix
|
||||
|
||||
# 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 -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 /tmp/server
|
||||
echo "OK"
|
||||
;;
|
||||
|
||||
restart)
|
||||
killall kvm_system
|
||||
killall NanoKVM-Server
|
||||
rm -r /tmp/kvm_system /tmp/server
|
||||
|
||||
cp -r /kvmapp/kvm_system /tmp/
|
||||
/tmp/kvm_system/kvm_system &
|
||||
|
||||
cp -r /kvmapp/server /tmp/
|
||||
/tmp/server/NanoKVM-Server &
|
||||
|
||||
sync
|
||||
echo "OK"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
120
kvmapp/system/init.d/S98tailscaled
Executable file
120
kvmapp/system/init.d/S98tailscaled
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/bin/sh
|
||||
|
||||
DAEMON="tailscaled"
|
||||
PIDFILE="/var/run/$DAEMON.pid"
|
||||
|
||||
# Set the port to listen on for incoming VPN packets.
|
||||
# Remote nodes will automatically be informed about the new port number,
|
||||
# but you might want to configure this in order to set external firewall
|
||||
# settings.
|
||||
PORT="41641"
|
||||
|
||||
# Extra flags you might want to pass to tailscaled.
|
||||
FLAGS=""
|
||||
|
||||
# You need tailscaled at /usr/sbin to server, and tailscale at /usr/bin to operate
|
||||
# STATIC version needed. Download page at https://pkgs.tailscale.com/stable/#static
|
||||
PKG_URL_LATEST="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
[ ! -x /usr/sbin/$DAEMON ] &&
|
||||
echo "/usr/sbin/$DAEMON not found, please download it from $PKG_URL_LATEST" &&
|
||||
echo "Then unpack it, copy $DAEMON to /usr/sbin and copy tailscale to /usr/bin" &&
|
||||
exit 1
|
||||
VERSION=$(/usr/sbin/$DAEMON --version|sed -n '1p'|xargs echo -n)
|
||||
|
||||
[ -x /usr/bin/tailscale ] || echo "/usr/bin/tailscale not found, your installation of tailscale may be broken"
|
||||
|
||||
# just for those need forwarding
|
||||
[ ! -f /etc/sysctl.d/99-tailscale.conf ] &&
|
||||
mkdir -p /etc/sysctl.d/ &&
|
||||
echo "missing /etc/sysctl.d/99-tailscale.conf, try make it below:" &&
|
||||
(tee /etc/sysctl.d/99-tailscale.conf <<EOF
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
EOF
|
||||
) &&
|
||||
echo "if this message repeats showing, please look at $0 if you need forwarding"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if [ -f /etc/kvm/GOMEMLIMIT ]; then
|
||||
value=$(cat /etc/kvm/GOMEMLIMIT)
|
||||
export GOMEMLIMIT="${value}MiB"
|
||||
else
|
||||
export GOMEMLIMIT=1024MiB
|
||||
fi
|
||||
|
||||
echo "GOMEMLIMIT set to ${GOMEMLIMIT}"
|
||||
printf "Starting $DAEMON[$VERSION]: "
|
||||
start-stop-daemon -S -bmq -p "$PIDFILE" -x "/usr/sbin/$DAEMON" -- \
|
||||
--state=/var/lib/tailscale/tailscaled.state \
|
||||
--socket=/var/run/tailscale/tailscaled.sock \
|
||||
--port=${PORT} \
|
||||
$FLAGS
|
||||
if [ $? = 0 ]; then
|
||||
echo "OK"
|
||||
tailscale set --accept-dns=false
|
||||
# example to make your nanoKVM an exit node
|
||||
# tailscale set --advertise-exit-node --advertise-routes=192.168.0.0/16
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
printf "Stopping $DAEMON: "
|
||||
start-stop-daemon -K -p "$PIDFILE"
|
||||
[ $? = 0 ] && (echo "OK"; rm -f "$PIDFILE") || echo "FAIL"
|
||||
printf "cleaning tailscaled: "
|
||||
/usr/sbin/$DAEMON --cleanup &>/dev/null
|
||||
[ $? = 0 ] && echo "OK" || echo "FAIL"
|
||||
;;
|
||||
restart|reload)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
doc)
|
||||
cat <<EOF
|
||||
To use tailscale, you need to meet these conditions below:
|
||||
1. Both tailscale and tailscaled were installed.
|
||||
2. At least one tailscale's account were registered.
|
||||
(https://login.tailscale.com/start)
|
||||
3. Your NanoKVM device must keep online.
|
||||
|
||||
Then, follow the steps below:
|
||||
1. Login in https://login.tailscale.com on your PC
|
||||
to enter into yout admin console.
|
||||
2. Run 'tailscale login' on your device, and copy the link showed
|
||||
to open on your PC, and confirm the auth.
|
||||
3. Congratulations, your NanoKVM device is now on the tailnet, try to
|
||||
visit it from all your device which on the same tailnet.
|
||||
4. run 'tailscale set --webclient' and manage your device
|
||||
on the web interface from localnet and tailenet.
|
||||
|
||||
Added: no more action to be repeat after your device reboot.
|
||||
EOF
|
||||
echo ""
|
||||
cat <<EOF
|
||||
为了正常使用 tailscale, 你需要满足以下几个条件:
|
||||
1. tailscale 和 tailscaled 这两个静态链接的二进制程序
|
||||
都已经正确安装在系统上。
|
||||
2. 至少注册一个 tailscale 的帐号才能使用其服务。
|
||||
(https://login.tailscale.com/start)
|
||||
3. 你的 NanoKVM 设备必须保持在线。
|
||||
|
||||
然后,跟着以下步骤操作:
|
||||
1. 在你的电脑上登录 https://login.tailscale.com 以进入控制台。
|
||||
2. 在你的 NanoKVM ssh 终端上执行 'tailscale login', 然后复制
|
||||
出现的链接到浏览器上,登录并通过验证。
|
||||
3. 祝贺,你的 NanoKVM 现在已经加入了 tailnet 然后你可以从任意
|
||||
同样已加入同一 tailnet 的设备去访问你的 NanoKVM。
|
||||
4. 执行 'tailscale set --webclient' 然后你就可以从 '局域网' 和
|
||||
'tailnet' 来访问特定网页,用来管理你的设备。
|
||||
|
||||
补充: 不需要重复上面的操作,哪怕你的设备重启过。
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
BIN
kvmapp/system/ko/soph_mipi_rx.ko
Normal file
BIN
kvmapp/system/ko/soph_mipi_rx.ko
Normal file
Binary file not shown.
96
kvmapp/system/update-nanokvm.py
Executable file
96
kvmapp/system/update-nanokvm.py
Executable file
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import zipfile
|
||||
import pathlib
|
||||
import requests
|
||||
|
||||
temporary: str = "/root/.kvm-cache/"
|
||||
|
||||
|
||||
def mkdir() -> None:
|
||||
if pathlib.Path(temporary).exists():
|
||||
shutil.rmtree(temporary)
|
||||
pathlib.Path(temporary).mkdir()
|
||||
print(f"Created temporary directory {temporary}")
|
||||
|
||||
|
||||
def read(file: str) -> str:
|
||||
return pathlib.Path(file).read_text().replace("\n", "")
|
||||
|
||||
|
||||
def download_firmware() -> None:
|
||||
print("Downloading firmware...")
|
||||
|
||||
now = int(time.time())
|
||||
url = f"https://cdn.sipeed.com/nanokvm/latest.zip?n={now}"
|
||||
print(f"Downloading firmware from {url}")
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
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"Failed to download firmware, content_type: {content_type}")
|
||||
|
||||
zip_file = f"{temporary}/latest.zip"
|
||||
with open(zip_file, "wb") as f:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
f.write(chunk)
|
||||
|
||||
with zipfile.ZipFile(zip_file, "r") as f:
|
||||
f.extractall(temporary)
|
||||
|
||||
print("Completed downloading firmware.")
|
||||
|
||||
|
||||
def update() -> None:
|
||||
backup_dir = pathlib.Path("/root/old")
|
||||
firmware_dir = pathlib.Path("/kvmapp")
|
||||
|
||||
if backup_dir.exists():
|
||||
shutil.rmtree(backup_dir)
|
||||
|
||||
if firmware_dir.exists():
|
||||
firmware_dir.rename(backup_dir)
|
||||
|
||||
pathlib.Path(f"{temporary}/latest").rename(firmware_dir)
|
||||
|
||||
|
||||
def change_permissions() -> None:
|
||||
for root, dirs, files in os.walk("/kvmapp"):
|
||||
os.chmod(root, 0o755)
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
os.chmod(file_path, 0o755)
|
||||
|
||||
print("change permissions done")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
print("Stopping nanokvm service...")
|
||||
os.system("/etc/init.d/S95nanokvm stop")
|
||||
|
||||
print("Staring update...")
|
||||
|
||||
mkdir()
|
||||
download_firmware()
|
||||
update()
|
||||
change_permissions()
|
||||
|
||||
version = read("/kvmapp/version")
|
||||
print(f"Successfully updated to version: {version}")
|
||||
print("restart service\nthe nanokvm will reboot")
|
||||
except Exception as e:
|
||||
print(f"update failed\n{e}")
|
||||
finally:
|
||||
shutil.rmtree(temporary)
|
||||
os.system("/etc/init.d/S95nanokvm restart")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,7 +13,7 @@ server
|
||||
├── dl_lib // Shared object libraries
|
||||
├── include // Header files for shared objects
|
||||
├── logger // Logging system
|
||||
├── middleware // Server middleware components
|
||||
├── middleware // Server middleware components
|
||||
├── proto // API request/response definitions
|
||||
├── router // API route handlers
|
||||
├── service // Core service implementations
|
||||
@@ -39,7 +39,7 @@ cert:
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
|
||||
# Authentication setting (enable/disable)
|
||||
# Note: Only disable authentication in development environment
|
||||
authentication: enable
|
||||
@@ -53,16 +53,16 @@ jwt:
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# Address for custom STUN server
|
||||
# Note: You can disable the STUN service by setting it to 'disable' (e.g., in a LAN environment)
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
# Address and authentication for custom TURN server
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnAddr: example_addr
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
|
||||
## Compile & Deploy
|
||||
|
||||
Note: Use Linux operating system (x86-64). This build process is not compatible with ARM, Windows or macOS.
|
||||
@@ -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.
|
||||
|
||||
@@ -16,7 +16,7 @@ server
|
||||
├── middleware // サーバーミドルウェアコンポーネント
|
||||
├── proto // API リクエスト/レスポンス定義
|
||||
├── router // API ルートハンドラ
|
||||
├── service // コアサービスの実<EFBFBD><EFBFBD>
|
||||
├── service // コアサービスの実装
|
||||
├── utils // ユーティリティ関数
|
||||
└── main.go
|
||||
```
|
||||
@@ -39,17 +39,23 @@ cert:
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
|
||||
# 認証設定 (enable/disable)
|
||||
# 注意: 認証を無効にするのは開発環境でのみ行ってください
|
||||
authentication: enable
|
||||
|
||||
# JWT 秘密鍵の設定
|
||||
# 空のままにすると、サーバー起動時にランダムな鍵が生成されます
|
||||
secretKey: ""
|
||||
jwt:
|
||||
# JWT 秘密鍵の設定。 空のままにすると、サーバー起動時にランダムな 64 バイトの鍵が自動的に生成されます。
|
||||
secretKey: ""
|
||||
# JWT トークンの有効期限(秒単位)。 デフォルト: 2678400 (31 日)
|
||||
refreshTokenDuration: 2678400
|
||||
# ユーザーがログアウトすると、すべての JWT トークンが無効になります。 デフォルト: true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# カスタム STUN サーバーのアドレス
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
# カスタム TURN サーバーのアドレスと認証情報
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnUser: example_user
|
||||
@@ -80,3 +86,12 @@ turn:
|
||||
1. デプロイ前に、ブラウザでアプリケーションを最新バージョンに更新します。手順は[こちら](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/updating.html)を参照してください。
|
||||
2. コンパイルして生成された `NanoKVM-Server` ファイルを使用して、NanoKVM の `/kvmapp/server/` ディレクトリ内の元のファイルを置き換えます。
|
||||
3. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
|
||||
## 手動更新
|
||||
|
||||
> ファイルのアップロードには SSH が必要です。Web 設定で有効にしてください: `設定 > SSH`
|
||||
|
||||
1. [GitHub](https://github.com/sipeed/NanoKVM/releases) から最新のアプリケーションをダウンロードします。
|
||||
2. ダウンロードしたファイルを解凍し、解凍したフォルダーの名前を `kvmapp` に変更します。
|
||||
3. NanoKVM 上の既存の `/kvmapp` ディレクトリをバックアップし、新しい `kvmapp` フォルダーに置き換えます。
|
||||
4. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
|
||||
@@ -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
|
||||
@@ -50,10 +50,12 @@ jwt:
|
||||
# 在帐号登出时是否使所有 jwt token 失效。默认为 true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# 自定义 STUN 服务器的地址
|
||||
# 注意:可以设置为“disable”来禁用 STUN 服务(例如在局域网环境中使用时)
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnAddr: example_addr
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
@@ -70,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;
|
||||
@@ -79,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` 重启服务。
|
||||
|
||||
## 手动更新
|
||||
@@ -90,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` 重启服务。
|
||||
|
||||
64
server/build.sh
Executable file
64
server/build.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration Variables
|
||||
BINARY_NAME="NanoKVM-Server"
|
||||
CC_COMPILER="riscv64-unknown-linux-musl-gcc"
|
||||
CGO_CFLAGS_OPTS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d"
|
||||
|
||||
# Define colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper function to check if a command exists
|
||||
check_dependency() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
echo -e "${RED}[ERROR] Required command '$1' not found.${NC}"
|
||||
echo "Please install it or ensure it is in your PATH."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 1: Check Prerequisites
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Checking build environment...${NC}"
|
||||
|
||||
check_dependency "go"
|
||||
check_dependency "patchelf"
|
||||
check_dependency "$CC_COMPILER"
|
||||
|
||||
echo -e "${GREEN}[OK] All dependencies found.${NC}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 2: Build the Binary
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Starting cross-compilation for RISC-V 64-bit (BoringCrypto enabled)...${NC}"
|
||||
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=linux
|
||||
export GOARCH=riscv64
|
||||
export GOEXPERIMENT=boringcrypto
|
||||
export CC="$CC_COMPILER"
|
||||
export CGO_CFLAGS="$CGO_CFLAGS_OPTS"
|
||||
|
||||
go build -o "$BINARY_NAME" -v
|
||||
|
||||
if [ -f "$BINARY_NAME" ]; then
|
||||
echo -e "${GREEN}[SUCCESS] Binary '$BINARY_NAME' created successfully.${NC}"
|
||||
else
|
||||
echo -e "${RED}[ERROR] Build failed. Binary not found.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Step 3: Patch RPATH
|
||||
# ------------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}[INFO] Patching RPATH with patchelf...${NC}"
|
||||
|
||||
patchelf --add-rpath '$ORIGIN/dl_lib' "$BINARY_NAME"
|
||||
|
||||
echo -e "${GREEN}[DONE] Build script completed successfully!${NC}"
|
||||
@@ -7,8 +7,6 @@ package common
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
@@ -20,23 +18,14 @@ var (
|
||||
kvmVisionOnce sync.Once
|
||||
)
|
||||
|
||||
type KvmVision struct {
|
||||
mutex sync.Mutex
|
||||
}
|
||||
type KvmVision struct{}
|
||||
|
||||
func GetKvmVision() *KvmVision {
|
||||
kvmVisionOnce.Do(func() {
|
||||
kvmVision = &KvmVision{}
|
||||
|
||||
conf := config.GetInstance()
|
||||
logLevel := strings.ToLower(conf.Logger.Level)
|
||||
|
||||
logEnable := C.uint8_t(0)
|
||||
if logLevel == "debug" {
|
||||
logEnable = C.uint8_t(1)
|
||||
}
|
||||
|
||||
C.kvmv_init(logEnable)
|
||||
logLevel := C.uint8_t(0)
|
||||
C.kvmv_init(logLevel)
|
||||
log.Debugf("kvm vision initialized")
|
||||
})
|
||||
|
||||
@@ -44,9 +33,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
|
||||
@@ -67,15 +53,10 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data
|
||||
defer C.free_kvmv_data(&kvmData)
|
||||
|
||||
data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
log.Debugf("read kvm image: %v", result)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -96,49 +77,9 @@ func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data
|
||||
defer C.free_kvmv_data(&kvmData)
|
||||
|
||||
data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
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 +95,17 @@ func (k *KvmVision) SetHDMI(enable bool) int {
|
||||
return result
|
||||
}
|
||||
|
||||
func (k *KvmVision) Close() {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
func (k *KvmVision) SetGop(gop uint8) {
|
||||
_gop := C.uint8_t(gop)
|
||||
C.set_h264_gop(_gop)
|
||||
}
|
||||
|
||||
func (k *KvmVision) SetFrameDetect(frame uint8) {
|
||||
_frame := C.uint8_t(frame)
|
||||
C.set_frame_detact(_frame)
|
||||
}
|
||||
|
||||
func (k *KvmVision) Close() {
|
||||
C.kvmv_deinit()
|
||||
log.Debugf("stop kvm vision...")
|
||||
}
|
||||
@@ -8,6 +8,7 @@ type Screen struct {
|
||||
FPS int
|
||||
Quality uint16
|
||||
BitRate uint16
|
||||
GOP uint8
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -46,6 +47,7 @@ func GetScreen() *Screen {
|
||||
Quality: 80,
|
||||
FPS: 30,
|
||||
BitRate: 3000,
|
||||
GOP: 30,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,6 +72,24 @@ func SetScreen(key string, value int) {
|
||||
|
||||
case "fps":
|
||||
screen.FPS = validateFPS(value)
|
||||
|
||||
case "gop":
|
||||
screen.GOP = uint8(value)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckScreen() {
|
||||
if _, ok := ResolutionMap[screen.Height]; !ok {
|
||||
screen.Width = 1920
|
||||
screen.Height = 1080
|
||||
}
|
||||
|
||||
if _, ok := QualityMap[screen.Quality]; !ok {
|
||||
screen.Quality = 80
|
||||
}
|
||||
|
||||
if _, ok := BitRateMap[screen.BitRate]; !ok {
|
||||
screen.BitRate = 3000
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ var defaultConfig = &Config{
|
||||
},
|
||||
Stun: "stun.l.google.com:19302",
|
||||
Turn: Turn{
|
||||
TurnAddr: "turn.cloudflare.com:3478",
|
||||
TurnAddr: "",
|
||||
TurnUser: "",
|
||||
TurnCred: "",
|
||||
},
|
||||
|
||||
45
server/config/file.go
Normal file
45
server/config/file.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const ConfigurationFile = "/etc/kvm/server.yaml"
|
||||
|
||||
func Read() (*Config, error) {
|
||||
data, err := os.ReadFile(ConfigurationFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read config: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conf Config
|
||||
|
||||
if err := yaml.Unmarshal(data, &conf); err != nil {
|
||||
log.Fatalf("failed to unmarshal config: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("read %s successfully", ConfigurationFile)
|
||||
return &conf, nil
|
||||
}
|
||||
|
||||
func Write(conf *Config) error {
|
||||
data, err := yaml.Marshal(&conf)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(ConfigurationFile, data, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("write to %s successfully", ConfigurationFile)
|
||||
return nil
|
||||
}
|
||||
@@ -54,24 +54,27 @@ func (h HWVersion) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func getHwVersion() HWVersion {
|
||||
func GetHwVersion() HWVersion {
|
||||
content, err := os.ReadFile(HWVersionFile)
|
||||
if err != nil {
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
version := strings.ReplaceAll(string(content), "\n", "")
|
||||
if version == "beta" {
|
||||
switch version {
|
||||
case "alpha":
|
||||
return HWVersionAlpha
|
||||
case "beta":
|
||||
return HWVersionBeta
|
||||
} else if version == "pcie" {
|
||||
case "pcie":
|
||||
return HWVersionPcie
|
||||
default:
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
func getHardware() (h Hardware) {
|
||||
version := getHwVersion()
|
||||
version := GetHwVersion()
|
||||
|
||||
switch version {
|
||||
case HWVersionAlpha:
|
||||
@@ -85,7 +88,7 @@ func getHardware() (h Hardware) {
|
||||
|
||||
default:
|
||||
h = HWAlpha
|
||||
log.Error("Unsupported hardware version: %s", version)
|
||||
log.Errorf("Unsupported hardware version: %s", version)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,21 +1,23 @@
|
||||
module NanoKVM-Server
|
||||
|
||||
go 1.22.1
|
||||
go 1.24.0
|
||||
|
||||
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/dtls/v3 v3.0.3
|
||||
github.com/pion/rtp v1.8.18
|
||||
github.com/pion/webrtc/v4 v4.0.1
|
||||
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/unrolled/secure v1.15.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -42,14 +44,12 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||
github.com/pion/interceptor v0.1.37 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/interceptor v0.1.39 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.9 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
@@ -71,9 +71,9 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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=
|
||||
@@ -78,18 +78,18 @@ github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/interceptor v0.1.39 h1:Y6k0bN9Y3Lg/Wb21JBWp480tohtns8ybJ037AGr9UuA=
|
||||
github.com/pion/interceptor v0.1.39/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
@@ -139,8 +139,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -158,19 +159,19 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -13,6 +13,7 @@ extern "C" {
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
#define IMG_BUFFER_FULL -3
|
||||
#define IMG_VENC_ERROR -2
|
||||
@@ -23,7 +24,14 @@ extern "C" {
|
||||
#define IMG_H264_TYPE_IF 3
|
||||
#define IMG_H264_TYPE_PF 4
|
||||
|
||||
#define NORMAL_RES 0
|
||||
#define NEW_RES 1
|
||||
#define UNSUPPORT_RES 2
|
||||
#define UNKNOWN_RES 3
|
||||
#define ERROR_RES 4
|
||||
|
||||
void kvmv_init(uint8_t _debug_info_en);
|
||||
void set_venc_auto_recyc(uint8_t _enable);
|
||||
/**********************************************************************************
|
||||
* @name kvmv_read_img
|
||||
* @author Sipeed BuGu
|
||||
@@ -37,22 +45,25 @@ void kvmv_init(uint8_t _debug_info_en);
|
||||
* @param _pp_kvm_data @output: Encode data
|
||||
* @param _p_kvmv_data_size @output: Encode data size
|
||||
* @return
|
||||
-7: HDMI INPUT RES ERROR
|
||||
-6: Unsupported resolution, please modify it in the host settings.
|
||||
-5: Retrieving image, please wait
|
||||
-4: Modifying image resolution, please wait
|
||||
-3: img buffer full
|
||||
-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);
|
||||
|
||||
@@ -60,4 +71,4 @@ uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // KVM_VISION_H_
|
||||
#endif // KVM_VISION_H_
|
||||
|
||||
@@ -22,7 +22,7 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
buffer = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
now := entry.Time.Format("2006-01-02 15:04:05")
|
||||
now := entry.Time.Format("2006-01-02 15:04:05.000")
|
||||
|
||||
if entry.HasCaller() {
|
||||
fileName := filepath.Base(entry.Caller.File)
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/logger"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
"NanoKVM-Server/utils"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
@@ -35,9 +39,12 @@ func initialize() {
|
||||
vision := common.GetKvmVision()
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
vision.SetHDMI(true)
|
||||
if !utils.IsHdmiDisabled() {
|
||||
vision.SetHDMI(true)
|
||||
}
|
||||
|
||||
utils.InitGoMemLimit()
|
||||
// run mouse jiggler
|
||||
jiggler.GetJiggler().Run()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
@@ -56,7 +63,6 @@ func run() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
if conf.Authentication == "disable" {
|
||||
r.Use(cors.AllowAll())
|
||||
}
|
||||
@@ -65,20 +71,17 @@ func run() {
|
||||
|
||||
httpAddr := fmt.Sprintf(":%d", conf.Port.Http)
|
||||
httpsAddr := fmt.Sprintf(":%d", conf.Port.Https)
|
||||
log.Printf("proto: %s, port: %d %d\n", conf.Proto, conf.Port.Http, conf.Port.Https)
|
||||
|
||||
if conf.Proto == "https" {
|
||||
r.Use(middleware.Tls())
|
||||
|
||||
go func() {
|
||||
if err := r.Run(httpAddr); err != nil {
|
||||
panic("start http server failed")
|
||||
r.Use(middleware.Tls())
|
||||
err := r.RunTLS(httpsAddr, conf.Cert.Crt, conf.Cert.Key)
|
||||
if err != nil {
|
||||
panic("start https server failed")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := r.RunTLS(httpsAddr, conf.Cert.Crt, conf.Cert.Key); err != nil {
|
||||
panic("start https server failed")
|
||||
}
|
||||
runRedirect(httpAddr, httpsAddr)
|
||||
} else {
|
||||
if err := r.Run(httpAddr); err != nil {
|
||||
panic("start http server failed")
|
||||
@@ -86,6 +89,26 @@ func run() {
|
||||
}
|
||||
}
|
||||
|
||||
func runRedirect(httpPort string, httpsPort string) {
|
||||
err := http.ListenAndServe(httpPort, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
host := req.Host
|
||||
if strings.Contains(host, httpPort) {
|
||||
host = strings.Split(host, httpPort)[0]
|
||||
}
|
||||
|
||||
targetURL := "https://" + host + req.URL.String()
|
||||
if httpsPort != ":443" {
|
||||
targetURL = "https://" + host + httpsPort + req.URL.String()
|
||||
}
|
||||
|
||||
http.Redirect(w, req, targetURL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
panic("start http server failed")
|
||||
}
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
common.GetKvmVision().Close()
|
||||
}
|
||||
|
||||
@@ -4,3 +4,11 @@ type GetVersionRsp struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
type GetPreviewRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type SetPreviewReq struct {
|
||||
Enable bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
9
server/proto/hid.go
Normal file
9
server/proto/hid.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package proto
|
||||
|
||||
type GetHidModeRsp struct {
|
||||
Mode string `json:"mode"` // normal or hid-only
|
||||
}
|
||||
|
||||
type SetHidModeReq struct {
|
||||
Mode string `validate:"required"` // normal or hid-only
|
||||
}
|
||||
@@ -12,6 +12,11 @@ type DeleteMacReq struct {
|
||||
Mac string `form:"mac" validate:"required"`
|
||||
}
|
||||
|
||||
type SetMacNameReq struct {
|
||||
Mac string `form:"mac" validate:"required"`
|
||||
Name string `form:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type TailscaleState string
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,9 +5,18 @@ type GetImagesRsp struct {
|
||||
}
|
||||
|
||||
type MountImageReq struct {
|
||||
File string `json:"file" validate:"omitempty"`
|
||||
File string `json:"file" validate:"omitempty"`
|
||||
Cdrom bool `json:"cdrom" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMountedImageRsp struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
type GetCdRomRsp struct {
|
||||
Cdrom int64 `json:"cdrom"`
|
||||
}
|
||||
|
||||
type DeleteImageReq struct {
|
||||
File string `json:"file" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package proto
|
||||
|
||||
type GetFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
type UpdateFrameDetectReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type UpdateFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
type StopFrameDetectReq struct {
|
||||
Duration int `validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package proto
|
||||
|
||||
type IP struct {
|
||||
Name string `json:"name"`
|
||||
Addr string `json:"addr"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GetInfoRsp struct {
|
||||
Ip string `json:"ip"`
|
||||
IPs []IP `json:"ips"`
|
||||
Mdns string `json:"mdns"`
|
||||
Image string `json:"image"`
|
||||
Application string `json:"application"`
|
||||
@@ -62,8 +69,8 @@ type UpdateVirtualDeviceRsp struct {
|
||||
}
|
||||
|
||||
type SetMemoryLimitReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Limit int64 `json:"limit"`
|
||||
Enabled bool `validate:"omitempty"`
|
||||
Limit int64 `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMemoryLimitRsp struct {
|
||||
@@ -72,7 +79,7 @@ type GetMemoryLimitRsp struct {
|
||||
}
|
||||
|
||||
type SetOledReq struct {
|
||||
Sleep int `json:"sleep"`
|
||||
Sleep int `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetOLEDRsp struct {
|
||||
@@ -80,6 +87,52 @@ type GetOLEDRsp struct {
|
||||
Sleep int `json:"sleep"`
|
||||
}
|
||||
|
||||
type GetGetHdmiStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetSSHStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetSwapRsp struct {
|
||||
Size int64 `json:"size"` // unit: MB
|
||||
}
|
||||
|
||||
type SetSwapReq struct {
|
||||
Size int64 `validate:"omitempty"` // unit: MB
|
||||
}
|
||||
|
||||
type GetMouseJigglerRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type SetMouseJigglerReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
Mode string `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMdnsStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type SetHostnameReq struct {
|
||||
Hostname string `validate:"required"`
|
||||
}
|
||||
|
||||
type GetHostnameRsp struct {
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
type SetWebTitleReq struct {
|
||||
Title string `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetWebTitleRsp struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type SetTlsReq struct {
|
||||
Enabled bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -13,4 +13,7 @@ func applicationRouter(r *gin.Engine) {
|
||||
|
||||
api.GET("/application/version", service.GetVersion) // get application version
|
||||
api.POST("/application/update", service.Update) // update application
|
||||
|
||||
api.GET("/application/preview", service.GetPreview) // get preview updates state
|
||||
api.POST("/application/preview", service.SetPreview) // set preview updates state
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ func hidRouter(r *gin.Engine) {
|
||||
service := hid.NewService()
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.POST("/hid/reset", service.Reset) // reset hid
|
||||
api.POST("/hid/paste", service.Paste) // paste
|
||||
|
||||
api.GET("/hid/mode", service.GetHidMode) // get hid mode
|
||||
api.POST("/hid/mode", service.SetHidMode) // set hid mode
|
||||
api.POST("/hid/reset", service.ResetHid) // reset hid
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ func storageRouter(r *gin.Engine) {
|
||||
api.GET("/storage/image", service.GetImages) // get image list
|
||||
api.GET("/storage/image/mounted", service.GetMountedImage) // get mounted image
|
||||
api.POST("/storage/image/mount", service.MountImage) // mount image
|
||||
api.GET("/storage/cdrom", service.GetCdRom) // get CD-ROM flag
|
||||
api.POST("/storage/image/delete", service.DeleteImage) // delete image
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package router
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/service/stream/h264"
|
||||
"NanoKVM-Server/service/stream/direct"
|
||||
"NanoKVM-Server/service/stream/mjpeg"
|
||||
"NanoKVM-Server/service/stream/webrtc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -12,9 +13,9 @@ func streamRouter(r *gin.Engine) {
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/stream/mjpeg", mjpeg.Connect) // mjpeg stream
|
||||
api.GET("/stream/mjpeg/detect", mjpeg.GetFrameDetect) // get frame detect state
|
||||
api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect state
|
||||
api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect
|
||||
api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect
|
||||
|
||||
api.GET("/stream/h264", h264.Connect) // h264 stream
|
||||
api.GET("/stream/h264", webrtc.Connect) // h264 stream (webrtc)
|
||||
api.GET("/stream/h264/direct", direct.Connect) // h264 stream (http)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func vmRouter(r *gin.Engine) {
|
||||
service := vm.NewService()
|
||||
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/vm/info", service.GetInfo) // get device information
|
||||
@@ -34,9 +35,33 @@ func vmRouter(r *gin.Engine) {
|
||||
api.GET("/vm/oled", service.GetOLED) // get OLED configuration
|
||||
api.POST("/vm/oled", service.SetOLED) // set OLED configuration
|
||||
|
||||
api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi (pcie only)
|
||||
// Only supported by PCIe version
|
||||
api.GET("/vm/hdmi", service.GetHdmiState) // get HDMI state
|
||||
api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi
|
||||
api.POST("/vm/hdmi/enable", service.EnableHdmi) // enable hdmi
|
||||
api.POST("/vm/hdmi/disable", service.DisableHdmi) // disable hdmi
|
||||
|
||||
api.GET("/vm/ssh", service.GetSSHState) // get SSH state
|
||||
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
|
||||
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
|
||||
|
||||
api.GET("/vm/swap", service.GetSwap) // get swap file size
|
||||
api.POST("/vm/swap", service.SetSwap) // set swap file size
|
||||
|
||||
api.GET("/vm/mouse-jiggler", service.GetMouseJiggler) // get mouse jiggler
|
||||
api.POST("/vm/mouse-jiggler/", service.SetMouseJiggler) // set mouse jiggler
|
||||
|
||||
api.GET("/vm/hostname", service.GetHostname) // Get Hostname
|
||||
api.POST("/vm/hostname", service.SetHostname) // Set Hostname
|
||||
|
||||
api.GET("/vm/web-title", service.GetWebTitle) // Get web title
|
||||
api.POST("/vm/web-title", service.SetWebTitle) // Set web title
|
||||
|
||||
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
|
||||
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
|
||||
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS
|
||||
|
||||
api.POST("/vm/tls", service.SetTls) // enable/disable TLS
|
||||
|
||||
api.POST("/vm/system/reboot", service.Reboot) // reboot system
|
||||
}
|
||||
|
||||
60
server/service/application/preview.go
Normal file
60
server/service/application/preview.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
PreviewUpdatesFlag = "/etc/kvm/preview_updates"
|
||||
)
|
||||
|
||||
func (s *Service) GetPreview(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled := isPreviewEnabled()
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetPreviewRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetPreview(c *gin.Context) {
|
||||
var req proto.SetPreviewReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enable == isPreviewEnabled() {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enable {
|
||||
if err := os.WriteFile(PreviewUpdatesFlag, []byte("1"), 0o644); err != nil {
|
||||
log.Errorf("failed to write %s: %s", PreviewUpdatesFlag, err)
|
||||
rsp.ErrRsp(c, -2, "enable failed")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(PreviewUpdatesFlag); err != nil {
|
||||
log.Errorf("failed to remove %s: %s", PreviewUpdatesFlag, err)
|
||||
rsp.ErrRsp(c, -3, "disable failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set preview updates state: %t", req.Enable)
|
||||
}
|
||||
|
||||
func isPreviewEnabled() bool {
|
||||
_, err := os.Stat(PreviewUpdatesFlag)
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
BaseURL = "https://cdn.sipeed.com/nanokvm"
|
||||
StableURL = "https://cdn.sipeed.com/nanokvm"
|
||||
PreviewURL = "https://cdn.sipeed.com/nanokvm/preview"
|
||||
|
||||
AppDir = "/kvmapp"
|
||||
BackupDir = "/root/old"
|
||||
|
||||
@@ -3,13 +3,12 @@ package application
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,42 +22,29 @@ 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,
|
||||
})
|
||||
}
|
||||
var (
|
||||
updateMutex sync.Mutex
|
||||
isUpdating bool
|
||||
)
|
||||
|
||||
func (s *Service) Update(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
updateMutex.Lock()
|
||||
if isUpdating {
|
||||
updateMutex.Unlock()
|
||||
rsp.ErrRsp(c, -1, "update already in progress")
|
||||
return
|
||||
}
|
||||
isUpdating = true
|
||||
updateMutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
updateMutex.Lock()
|
||||
isUpdating = false
|
||||
updateMutex.Unlock()
|
||||
}()
|
||||
|
||||
if err := update(); err != nil {
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err))
|
||||
return
|
||||
@@ -87,10 +73,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 +120,6 @@ func update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLatest() (*Latest, error) {
|
||||
url := fmt.Sprintf("%s/latest.json?now=%d", BaseURL, time.Now().Unix())
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var latest Latest
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
log.Errorf("failed to unmarshal response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("get application latest version: %s", latest.Version)
|
||||
return &latest, nil
|
||||
}
|
||||
|
||||
func download(url string, target string) (err error) {
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
|
||||
89
server/service/application/version.go
Normal file
89
server/service/application/version.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Latest struct {
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Sha512 string `json:"sha512"`
|
||||
Size uint `json:"size"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (s *Service) GetVersion(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// current version
|
||||
currentVersion := "1.0.0"
|
||||
|
||||
versionFile := fmt.Sprintf("%s/version", AppDir)
|
||||
if version, err := os.ReadFile(versionFile); err == nil {
|
||||
currentVersion = strings.ReplaceAll(string(version), "\n", "")
|
||||
}
|
||||
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get latest version failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func getLatest() (*Latest, error) {
|
||||
baseURL := StableURL
|
||||
if isPreviewEnabled() {
|
||||
baseURL = PreviewURL
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/latest.json?now=%d", baseURL, time.Now().Unix())
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var latest Latest
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
log.Errorf("failed to unmarshal response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latest.Url = fmt.Sprintf("%s/%s", baseURL, latest.Name)
|
||||
|
||||
log.Debugf("get application latest version: %s", latest.Version)
|
||||
return &latest, nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const AccountFile = "/etc/kvm/pwd"
|
||||
|
||||
type Account struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Password string `json:"password"` // should be named HashedPassword for clarity
|
||||
}
|
||||
|
||||
func GetAccount() (*Account, error) {
|
||||
@@ -40,10 +40,10 @@ func GetAccount() (*Account, error) {
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func SetAccount(username string, password string) error {
|
||||
func SetAccount(username string, hashedPassword string) error {
|
||||
account, err := json.Marshal(&Account{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Password: hashedPassword,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal account information to json: %s", err)
|
||||
@@ -65,7 +65,7 @@ func SetAccount(username string, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CompareAccount(username string, password string) bool {
|
||||
func CompareAccount(username string, plainPassword string) bool {
|
||||
account, err := GetAccount()
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -75,16 +75,16 @@ func CompareAccount(username string, password string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
decryptedPassword, err := utils.DecodeDecrypt(password)
|
||||
if err != nil || decryptedPassword == "" {
|
||||
hashedPassword, err := utils.DecodeDecrypt(plainPassword)
|
||||
if err != nil || hashedPassword == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(decryptedPassword))
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(hashedPassword))
|
||||
if err != nil {
|
||||
// Compatible with old versions
|
||||
accountDecryptedPassword, _ := utils.DecodeDecrypt(account.Password)
|
||||
if accountDecryptedPassword == decryptedPassword {
|
||||
accountHashedPassword, _ := utils.DecodeDecrypt(account.Password)
|
||||
if accountHashedPassword == hashedPassword {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -104,10 +104,10 @@ func DelAccount() error {
|
||||
}
|
||||
|
||||
func getDefaultAccount() *Account {
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
|
||||
return &Account{
|
||||
Username: "admin",
|
||||
Password: string(password),
|
||||
Password: string(hashedPassword),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -23,17 +24,20 @@ func (s *Service) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
time.Sleep(3 * time.Second)
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if ok := CompareAccount(req.Username, req.Password); !ok {
|
||||
time.Sleep(2 * time.Second)
|
||||
rsp.ErrRsp(c, -2, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateJWT(req.Username)
|
||||
if err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
rsp.ErrRsp(c, -3, "generate token failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -70,7 +71,9 @@ func (s *Service) IsPasswordUpdated(c *gin.Context) {
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte("admin"))
|
||||
|
||||
rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{
|
||||
IsUpdated: err == nil,
|
||||
// If the hash is not valid, still assume it's not updated
|
||||
// The error we want to see is password and hash not matching
|
||||
IsUpdated: errors.Is(err, bcrypt.ErrMismatchedHashAndPassword),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,6 @@ func (c *Cli) Stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
return os.Remove(ScriptPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ type Service struct{}
|
||||
const (
|
||||
TailscalePath = "/usr/bin/tailscale"
|
||||
TailscaledPath = "/usr/sbin/tailscaled"
|
||||
ConfigPath = "etc/sysctl.d/99-tailscale.conf"
|
||||
|
||||
GoMemLimit int64 = 75
|
||||
)
|
||||
|
||||
var StateMap = map[string]proto.TailscaleState{
|
||||
@@ -38,6 +39,8 @@ func (s *Service) Install(c *gin.Context) {
|
||||
rsp.ErrRsp(c, -1, "install failed")
|
||||
return
|
||||
}
|
||||
|
||||
_ = NewCli().Start()
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
@@ -52,7 +55,6 @@ func (s *Service) Uninstall(c *gin.Context) {
|
||||
|
||||
_ = os.Remove(TailscalePath)
|
||||
_ = os.Remove(TailscaledPath)
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("uninstall tailscale successfully")
|
||||
@@ -69,7 +71,7 @@ func (s *Service) Start(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !utils.IsGoMemLimitExist() {
|
||||
_ = utils.SetGoMemLimit(50)
|
||||
_ = utils.SetGoMemLimit(GoMemLimit)
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
@@ -165,7 +167,7 @@ func (s *Service) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !utils.IsGoMemLimitExist() {
|
||||
_ = utils.SetGoMemLimit(50)
|
||||
_ = utils.SetGoMemLimit(GoMemLimit)
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
"time"
|
||||
|
||||
var (
|
||||
hid *Hid
|
||||
hidOnce sync.Once
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Hid struct {
|
||||
@@ -18,6 +17,24 @@ type Hid struct {
|
||||
mouseMutex sync.Mutex
|
||||
}
|
||||
|
||||
const (
|
||||
HID0 = "/dev/hidg0"
|
||||
HID1 = "/dev/hidg1"
|
||||
HID2 = "/dev/hidg2"
|
||||
)
|
||||
|
||||
var (
|
||||
hid *Hid
|
||||
hidOnce sync.Once
|
||||
)
|
||||
|
||||
func GetHid() *Hid {
|
||||
hidOnce.Do(func() {
|
||||
hid = &Hid{}
|
||||
})
|
||||
return hid
|
||||
}
|
||||
|
||||
func (h *Hid) Lock() {
|
||||
h.kbMutex.Lock()
|
||||
h.mouseMutex.Lock()
|
||||
@@ -28,9 +45,117 @@ func (h *Hid) Unlock() {
|
||||
h.mouseMutex.Unlock()
|
||||
}
|
||||
|
||||
func GetHid() *Hid {
|
||||
hidOnce.Do(func() {
|
||||
hid = &Hid{}
|
||||
})
|
||||
return hid
|
||||
func (h *Hid) OpenNoLock() {
|
||||
var err error
|
||||
h.CloseNoLock()
|
||||
|
||||
h.g0, err = os.OpenFile(HID0, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID0, err)
|
||||
}
|
||||
|
||||
h.g1, err = os.OpenFile(HID1, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID1, err)
|
||||
}
|
||||
|
||||
h.g2, err = os.OpenFile(HID2, os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", HID2, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) CloseNoLock() {
|
||||
for _, file := range []*os.File{h.g0, h.g1, h.g2} {
|
||||
if file != nil {
|
||||
_ = file.Sync()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Open() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
|
||||
h.OpenNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) Close() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid0(data []byte) {
|
||||
h.kbMutex.Lock()
|
||||
_, err := h.g0.Write(data)
|
||||
h.kbMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
} else {
|
||||
log.Debugf("write to %s failed: %s", HID0, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID0, data)
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid1(data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
|
||||
h.mouseMutex.Lock()
|
||||
_ = h.g1.SetWriteDeadline(deadline)
|
||||
_, err := h.g1.Write(data)
|
||||
h.mouseMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to %s timeout", HID1)
|
||||
default:
|
||||
log.Errorf("write to %s failed: %s", HID1, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID1, data)
|
||||
}
|
||||
|
||||
func (h *Hid) WriteHid2(data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
|
||||
h.mouseMutex.Lock()
|
||||
_ = h.g2.SetWriteDeadline(deadline)
|
||||
_, err := h.g2.Write(data)
|
||||
h.mouseMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Errorf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to %s timeout", HID2)
|
||||
default:
|
||||
log.Errorf("write to %s failed: %s", HID2, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %v", HID2, data)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,14 @@ package hid
|
||||
|
||||
func (h *Hid) Keyboard(queue <-chan []int) {
|
||||
for event := range queue {
|
||||
h.kbMutex.Lock()
|
||||
h.writeKeyboard(event)
|
||||
h.kbMutex.Unlock()
|
||||
code := byte(event[0])
|
||||
|
||||
var modifier byte = 0x00
|
||||
if code > 0 {
|
||||
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
h.WriteHid0(data)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) writeKeyboard(event []int) {
|
||||
code := byte(event[0])
|
||||
|
||||
var modifier byte = 0x00
|
||||
if code > 0 {
|
||||
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
h.Write(h.g0, data)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package hid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -25,7 +22,6 @@ var mouseButtonMap = map[byte]bool{
|
||||
|
||||
func (h *Hid) Mouse(queue <-chan []int) {
|
||||
for event := range queue {
|
||||
h.mouseMutex.Lock()
|
||||
|
||||
switch event[0] {
|
||||
case MouseDown:
|
||||
@@ -39,10 +35,8 @@ func (h *Hid) Mouse(queue <-chan []int) {
|
||||
case MouseScroll:
|
||||
h.mouseScroll(event)
|
||||
default:
|
||||
log.Debugf("invalid mouse event: %+v", event)
|
||||
log.Debugf("invalid mouse event: %v", event)
|
||||
}
|
||||
|
||||
h.mouseMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,17 +44,17 @@ func (h *Hid) mouseDown(event []int) {
|
||||
button := byte(event[1])
|
||||
|
||||
if _, ok := mouseButtonMap[button]; !ok {
|
||||
log.Debugf("invalid mouse button: %+v", event)
|
||||
log.Errorf("invalid mouse button: %v", event)
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte{button, 0, 0, 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseUp() {
|
||||
data := []byte{0, 0, 0, 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseScroll(event []int) {
|
||||
@@ -70,9 +64,8 @@ func (h *Hid) mouseScroll(event []int) {
|
||||
}
|
||||
|
||||
data := []byte{0, 0, 0, byte(direction)}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseMoveAbsolute(event []int) {
|
||||
x := make([]byte, 2)
|
||||
y := make([]byte, 2)
|
||||
@@ -80,32 +73,10 @@ func (h *Hid) mouseMoveAbsolute(event []int) {
|
||||
binary.LittleEndian.PutUint16(y, uint16(event[3]))
|
||||
|
||||
data := []byte{0, x[0], x[1], y[0], y[1], 0}
|
||||
h.writeWithTimeout(h.g2, data)
|
||||
h.WriteHid2(data)
|
||||
}
|
||||
|
||||
func (h *Hid) mouseMoveRelative(event []int) {
|
||||
data := []byte{byte(event[1]), byte(event[2]), byte(event[3]), 0}
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
}
|
||||
|
||||
func (h *Hid) writeWithTimeout(file *os.File, data []byte) {
|
||||
deadline := time.Now().Add(8 * time.Millisecond)
|
||||
_ = file.SetWriteDeadline(deadline)
|
||||
|
||||
_, err := file.Write(data)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
log.Debugf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
log.Debugf("write to hid timeout")
|
||||
default:
|
||||
log.Errorf("write to hid failed: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to hid: %+v", data)
|
||||
h.WriteHid1(data)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *Hid) OpenNoLock() {
|
||||
var err error
|
||||
h.CloseNoLock()
|
||||
|
||||
h.g0, err = os.OpenFile("/dev/hidg0", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg0 failed: %s", err)
|
||||
}
|
||||
|
||||
h.g1, err = os.OpenFile("/dev/hidg1", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg1 failed: %s", err)
|
||||
}
|
||||
|
||||
h.g2, err = os.OpenFile("/dev/hidg2", os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("open /dev/hidg2 failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Open() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
|
||||
h.OpenNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) CloseNoLock() {
|
||||
for _, file := range []*os.File{h.g0, h.g1, h.g2} {
|
||||
if file != nil {
|
||||
_ = file.Sync()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) Close() {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
h.mouseMutex.Lock()
|
||||
defer h.mouseMutex.Unlock()
|
||||
|
||||
h.CloseNoLock()
|
||||
}
|
||||
|
||||
func (h *Hid) Write(file *os.File, data []byte) {
|
||||
_, err := file.Write(data)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
log.Debugf("hid already closed, reopen it...")
|
||||
h.OpenNoLock()
|
||||
} else {
|
||||
log.Errorf("write to hid failed: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("write to hid: %+v", data)
|
||||
}
|
||||
@@ -16,6 +16,75 @@ type Char struct {
|
||||
|
||||
type PasteReq struct {
|
||||
Content string `form:"content" validate:"required"`
|
||||
Langue string `form:"langue"`
|
||||
}
|
||||
|
||||
func LangueSwitch(base map[rune]Char, lang string) map[rune]Char {
|
||||
// wenn kein lang angegeben → Standardmap zurück
|
||||
if lang == "" {
|
||||
return base
|
||||
}
|
||||
|
||||
// immer Kopie erstellen
|
||||
m := copyMap(base)
|
||||
|
||||
switch lang {
|
||||
case "de":
|
||||
// Y tauschen
|
||||
m['y'] = Char{0, 29}
|
||||
m['Y'] = Char{2, 29}
|
||||
|
||||
// Z tauschen
|
||||
m['z'] = Char{0, 28}
|
||||
m['Z'] = Char{2, 28}
|
||||
|
||||
// deutsche Sonderzeichen hinzufügen oder remappen
|
||||
m['\u00E4'] = Char{0, 52} // ä
|
||||
m['\u00C4'] = Char{2, 52} // Ä
|
||||
m['\u00F6'] = Char{0, 51} // ö
|
||||
m['\u00D6'] = Char{2, 51} // Ö
|
||||
m['\u00FC'] = Char{0, 47} // ü
|
||||
m['\u00DC'] = Char{2, 47} // Ü
|
||||
m['\u00DF'] = Char{0, 45} // ß
|
||||
|
||||
//Tauschen
|
||||
m['^'] = Char{0, 53} // muss doppelt sein
|
||||
m['/'] = Char{2, 36} // Shift + 7
|
||||
m['('] = Char{2, 37} // Shift + 8
|
||||
m['&'] = Char{2, 35} // Shift + 6
|
||||
m[')'] = Char{2, 38} // Shift + 9
|
||||
m['`'] = Char{2, 46} // Grave Accent / Backtick
|
||||
m['"'] = Char{2, 31} // Shift + 2
|
||||
m['?'] = Char{2, 45} // Shift + ß
|
||||
m['{'] = Char{0x40, 36} // ALt Gr + 7
|
||||
m['['] = Char{0x40, 37} // ALt Gr + 8
|
||||
m[']'] = Char{0x40, 38} // ALt Gr + 6
|
||||
m['}'] = Char{0x40, 39} // ALt Gr + 0
|
||||
m['\\'] = Char{0x40, 45} // ALt Gr + ß
|
||||
m['@'] = Char{0x40, 20} // ALt Gr + q
|
||||
m['+'] = Char{0, 48} // Shift + +
|
||||
m['*'] = Char{2, 48} // Shift + +
|
||||
m['~'] = Char{0x40, 48} // Shift + +
|
||||
m['#'] = Char{0, 49} // Shift + #
|
||||
m['\''] = Char{2, 49} // Shift + #
|
||||
m['<'] = Char{0, 100} // Shift + <
|
||||
m['>'] = Char{2, 100} // Shift + <
|
||||
m['|'] = Char{0x40, 100} // ALt Gr + <
|
||||
m[';'] = Char{2, 54} // Shift + ,
|
||||
m[':'] = Char{2, 55} // Shift + .
|
||||
m['-'] = Char{0, 56} // Shift + -
|
||||
m['_'] = Char{2, 56} // Shift + -
|
||||
|
||||
//neu
|
||||
m['\u00B4'] = Char{0, 46} // ´
|
||||
m['\u00B0'] = Char{2, 53} // °
|
||||
m['\u00A7'] = Char{2, 32} // §
|
||||
m['\u20AC'] = Char{0x40, 8} // €
|
||||
m['\u00B2'] = Char{0x40, 31} // ²
|
||||
m['\u00B3'] = Char{0x40, 32} // ³
|
||||
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *Service) Paste(c *gin.Context) {
|
||||
@@ -28,145 +97,101 @@ func (s *Service) Paste(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(req.Content) > 1024 {
|
||||
rsp.ErrRsp(c, -2, "")
|
||||
rsp.ErrRsp(c, -2, "content too long")
|
||||
return
|
||||
}
|
||||
|
||||
s.hid.kbMutex.Lock()
|
||||
charMapLocal := LangueSwitch(charMap, req.Langue)
|
||||
|
||||
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
for _, char := range req.Content {
|
||||
key, ok := charMap[char]
|
||||
key, ok := charMapLocal[char]
|
||||
if !ok {
|
||||
log.Debugf("unknown key %c", char)
|
||||
log.Debugf("unknown key '%c' (rune: %d)", char, char)
|
||||
continue
|
||||
}
|
||||
|
||||
keyDown := []byte{byte(key.Modifiers), 0x00, byte(key.Code), 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
// only handle shift. Need to handle all modifiers?
|
||||
if key.Modifiers > 0 {
|
||||
keyShift := []byte{0x00, 0x00, byte(225), 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
s.hid.Write(s.hid.g0, keyShift)
|
||||
}
|
||||
|
||||
s.hid.Write(s.hid.g0, keyDown)
|
||||
s.hid.Write(s.hid.g0, keyUp)
|
||||
|
||||
if key.Modifiers > 0 {
|
||||
s.hid.Write(s.hid.g0, keyUp)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
hid.WriteHid0(keyDown)
|
||||
hid.WriteHid0(keyUp)
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
}
|
||||
s.hid.kbMutex.Unlock()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("hid paste success, total %d", len(req.Content))
|
||||
log.Debugf("hid paste success, total %d characters processed", len(req.Content))
|
||||
}
|
||||
|
||||
func copyMap(src map[rune]Char) map[rune]Char {
|
||||
dst := make(map[rune]Char, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
var charMap = map[rune]Char{
|
||||
97: {0, 4}, // a
|
||||
98: {0, 5}, // b
|
||||
99: {0, 6}, // c
|
||||
100: {0, 7}, // d
|
||||
101: {0, 8}, // e
|
||||
102: {0, 9}, // f
|
||||
103: {0, 10}, // g
|
||||
104: {0, 11}, // h
|
||||
105: {0, 12}, // i
|
||||
106: {0, 13}, // j
|
||||
107: {0, 14}, // k
|
||||
108: {0, 15}, // l
|
||||
109: {0, 16}, // m
|
||||
110: {0, 17}, // n
|
||||
111: {0, 18}, // o
|
||||
112: {0, 19}, // p
|
||||
113: {0, 20}, // q
|
||||
114: {0, 21}, // r
|
||||
115: {0, 22}, // s
|
||||
116: {0, 23}, // t
|
||||
117: {0, 24}, // u
|
||||
118: {0, 25}, // v
|
||||
119: {0, 26}, // w
|
||||
120: {0, 27}, // x
|
||||
121: {0, 28}, // y
|
||||
122: {0, 29}, // z
|
||||
// Lowercase letters
|
||||
'a': {0, 4}, 'b': {0, 5}, 'c': {0, 6}, 'd': {0, 7}, 'e': {0, 8},
|
||||
'f': {0, 9}, 'g': {0, 10}, 'h': {0, 11}, 'i': {0, 12}, 'j': {0, 13},
|
||||
'k': {0, 14}, 'l': {0, 15}, 'm': {0, 16}, 'n': {0, 17}, 'o': {0, 18},
|
||||
'p': {0, 19}, 'q': {0, 20}, 'r': {0, 21}, 's': {0, 22}, 't': {0, 23},
|
||||
'u': {0, 24}, 'v': {0, 25}, 'w': {0, 26}, 'x': {0, 27}, 'y': {0, 28},
|
||||
'z': {0, 29},
|
||||
|
||||
49: {0, 30}, // 1
|
||||
50: {0, 31}, // 2
|
||||
51: {0, 32}, // 3
|
||||
52: {0, 33}, // 4
|
||||
53: {0, 34}, // 5
|
||||
54: {0, 35}, // 6
|
||||
55: {0, 36}, // 7
|
||||
56: {0, 37}, // 8
|
||||
57: {0, 38}, // 9
|
||||
48: {0, 39}, // 0
|
||||
// Uppercase letters (Modifier 2 typically means Left Shift)
|
||||
'A': {2, 4}, 'B': {2, 5}, 'C': {2, 6}, 'D': {2, 7}, 'E': {2, 8},
|
||||
'F': {2, 9}, 'G': {2, 10}, 'H': {2, 11}, 'I': {2, 12}, 'J': {2, 13},
|
||||
'K': {2, 14}, 'L': {2, 15}, 'M': {2, 16}, 'N': {2, 17}, 'O': {2, 18},
|
||||
'P': {2, 19}, 'Q': {2, 20}, 'R': {2, 21}, 'S': {2, 22}, 'T': {2, 23},
|
||||
'U': {2, 24}, 'V': {2, 25}, 'W': {2, 26}, 'X': {2, 27}, 'Y': {2, 28},
|
||||
'Z': {2, 29},
|
||||
|
||||
10: {0, 40}, // Enter
|
||||
9: {0, 43}, // Tab
|
||||
32: {0, 44}, // Space
|
||||
45: {0, 45}, // -
|
||||
61: {0, 46}, // =
|
||||
91: {0, 47}, // [
|
||||
93: {0, 48}, // ]
|
||||
92: {0, 49}, // \
|
||||
// Numbers
|
||||
'1': {0, 30}, '2': {0, 31}, '3': {0, 32}, '4': {0, 33}, '5': {0, 34},
|
||||
'6': {0, 35}, '7': {0, 36}, '8': {0, 37}, '9': {0, 38}, '0': {0, 39},
|
||||
|
||||
59: {0, 51}, // ;
|
||||
39: {0, 52}, // '
|
||||
96: {0, 53}, // `
|
||||
44: {0, 54}, // ,
|
||||
46: {0, 55}, // .
|
||||
47: {0, 56}, // /
|
||||
// Shifted numbers / Symbols
|
||||
'!': {2, 30}, // Shift + 1
|
||||
'@': {2, 31}, // Shift + 2
|
||||
'#': {2, 32}, // Shift + 3
|
||||
'$': {2, 33}, // Shift + 4
|
||||
'%': {2, 34}, // Shift + 5
|
||||
'^': {2, 35}, // Shift + 6
|
||||
'&': {2, 36}, // Shift + 7
|
||||
'*': {2, 37}, // Shift + 8
|
||||
'(': {2, 38}, // Shift + 9
|
||||
')': {2, 39}, // Shift + 0
|
||||
|
||||
65: {2, 4}, // A
|
||||
66: {2, 5}, // B
|
||||
67: {2, 6}, // C
|
||||
68: {2, 7}, // D
|
||||
69: {2, 8}, // E
|
||||
70: {2, 9}, // F
|
||||
71: {2, 10}, // G
|
||||
72: {2, 11}, // H
|
||||
73: {2, 12}, // I
|
||||
74: {2, 13}, // J
|
||||
75: {2, 14}, // K
|
||||
76: {2, 15}, // L
|
||||
77: {2, 16}, // M
|
||||
78: {2, 17}, // N
|
||||
79: {2, 18}, // O
|
||||
80: {2, 19}, // P
|
||||
81: {2, 20}, // Q
|
||||
82: {2, 21}, // R
|
||||
83: {2, 22}, // S
|
||||
84: {2, 23}, // T
|
||||
85: {2, 24}, // U
|
||||
86: {2, 25}, // V
|
||||
87: {2, 26}, // W
|
||||
88: {2, 27}, // X
|
||||
89: {2, 28}, // Y
|
||||
90: {2, 29}, // Z
|
||||
// Other common characters
|
||||
'\n': {0, 40}, // Enter (Return)
|
||||
'\t': {0, 43}, // Tab
|
||||
' ': {0, 44}, // Space
|
||||
'-': {0, 45}, // Hyphen / Minus
|
||||
'=': {0, 46}, // Equals
|
||||
'[': {0, 47}, // Left Square Bracket
|
||||
']': {0, 48}, // Right Square Bracket
|
||||
'\\': {0, 49}, // Backslash
|
||||
|
||||
33: {2, 30}, // !
|
||||
64: {2, 31}, // @
|
||||
35: {2, 32}, // #
|
||||
36: {2, 33}, // $
|
||||
37: {2, 34}, // %
|
||||
94: {2, 35}, // ^
|
||||
38: {2, 36}, // &
|
||||
42: {2, 37}, // *
|
||||
40: {2, 38}, // (
|
||||
41: {2, 39}, // )
|
||||
';': {0, 51}, // Semicolon
|
||||
'\'': {0, 52}, // Apostrophe / Single Quote
|
||||
'`': {0, 53}, // Grave Accent / Backtick
|
||||
',': {0, 54}, // Comma
|
||||
'.': {0, 55}, // Period / Dot
|
||||
'/': {0, 56}, // Slash
|
||||
|
||||
95: {2, 45}, // _
|
||||
43: {2, 46}, // +
|
||||
123: {2, 47}, // {
|
||||
125: {2, 48}, // }
|
||||
124: {2, 49}, // |
|
||||
// Shifted symbols
|
||||
'_': {2, 45}, // Underscore (Shift + Hyphen)
|
||||
'+': {2, 46}, // Plus (Shift + Equals)
|
||||
'{': {2, 47}, // Left Curly Brace (Shift + Left Square Bracket)
|
||||
'}': {2, 48}, // Right Curly Brace (Shift + Right Square Bracket)
|
||||
'|': {2, 49}, // Pipe (Shift + Backslash)
|
||||
|
||||
58: {2, 51}, // :
|
||||
34: {2, 52}, // "
|
||||
126: {2, 53}, // ~
|
||||
60: {2, 54}, // <
|
||||
62: {2, 55}, // >
|
||||
63: {2, 56}, // ?
|
||||
':': {2, 51}, // Colon (Shift + Semicolon)
|
||||
'"': {2, 52}, // Double Quote (Shift + Apostrophe)
|
||||
'~': {2, 53}, // Tilde (Shift + Grave Accent)
|
||||
'<': {2, 54}, // Less Than (Shift + Comma)
|
||||
'>': {2, 55}, // Greater Than (Shift + Period)
|
||||
'?': {2, 56}, // Question Mark (Shift + Slash)
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) Reset(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// reset USB
|
||||
f, err := os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "open usb gadget file failed")
|
||||
return
|
||||
}
|
||||
err = f.Truncate(0)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("truncate /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "truncate usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("seek to 0 failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "seek to 0 in usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_, err = f.WriteString("\n")
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "write to usb gadget file failed")
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
devices, err := os.ReadDir("/sys/class/udc/")
|
||||
if err != nil {
|
||||
log.Errorf("read udc directory failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "read udc directory failed")
|
||||
return
|
||||
}
|
||||
|
||||
f, err = os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "open usb gadget file failed")
|
||||
return
|
||||
}
|
||||
for _, device := range devices {
|
||||
_, err = f.WriteString(device.Name() + "\n")
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "write to usb gadget file failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = f.Close()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("reset hid success")
|
||||
}
|
||||
191
server/service/hid/status.go
Normal file
191
server/service/hid/status.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
ModeNormalScript = "/kvmapp/system/init.d/S03usbdev"
|
||||
ModeHidOnlyScript = "/kvmapp/system/init.d/S03usbhid"
|
||||
|
||||
USBDevScript = "/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
|
||||
}
|
||||
|
||||
h := GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
srcScript := ModeNormalScript
|
||||
if req.Mode == ModeHidOnly {
|
||||
srcScript = ModeHidOnlyScript
|
||||
}
|
||||
|
||||
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 (s *Service) ResetHid(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
h := GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
command := fmt.Sprintf("%s restart_phy", USBDevScript)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset hid: %v", err)
|
||||
rsp.ErrRsp(c, -1, "failed to reset hid")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("reset hid success")
|
||||
}
|
||||
|
||||
func copyModeFile(srcScript string) error {
|
||||
// open the source file
|
||||
srcFile, err := os.Open(srcScript)
|
||||
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", USBDevScript, 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, USBDevScript); err != nil {
|
||||
log.Errorf("failed to rename %s: %s", tmpPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("copy %s to %s successful", srcScript, USBDevScript)
|
||||
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
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("ether-wake %s", mac)
|
||||
command := fmt.Sprintf("ether-wake -b %s", mac)
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/service/hid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,9 +21,8 @@ const (
|
||||
imageNone = "/dev/mmcblk0p3"
|
||||
cdromFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/cdrom"
|
||||
mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file"
|
||||
removableFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/removable"
|
||||
inquiryString = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/inquiry_string"
|
||||
roFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/ro"
|
||||
roDisk = "/boot/usb.disk0.ro"
|
||||
)
|
||||
|
||||
func (s *Service) GetImages(c *gin.Context) {
|
||||
@@ -61,61 +63,71 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom and ro flag
|
||||
// set to 0 when unmount image
|
||||
// set to 1 when mount image and the CD-ROM is enabled
|
||||
if req.File == "" || req.Cdrom {
|
||||
flag := "0"
|
||||
if req.File != "" && req.Cdrom {
|
||||
flag = "1"
|
||||
}
|
||||
|
||||
// unmount
|
||||
if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil {
|
||||
log.Errorf("unmount file failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "unmount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
// ro flag
|
||||
if err := os.WriteFile(roFlag, []byte(flag), 0o666); err != nil {
|
||||
log.Errorf("set ro flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set ro flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom flag
|
||||
if err := os.WriteFile(cdromFlag, []byte(flag), 0o666); err != nil {
|
||||
log.Errorf("set cdrom flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set cdrom flag failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
inquiryVen := "NanoKVM"
|
||||
inquiryPrd := "USB Mass Storage"
|
||||
inquiryVer := 0x0520
|
||||
if req.Cdrom {
|
||||
inquiryPrd = "USB CD/DVD-ROM"
|
||||
}
|
||||
inquiryData := fmt.Sprintf("%-8s%-16s%04x", inquiryVen, inquiryPrd, inquiryVer)
|
||||
|
||||
if err := os.WriteFile(inquiryString, []byte(inquiryData), 0o666); err != nil {
|
||||
log.Errorf("set inquiry %s failed: %s", inquiryData, err)
|
||||
rsp.ErrRsp(c, -2, "set inquiry failed")
|
||||
return
|
||||
}
|
||||
|
||||
// mount
|
||||
image := req.File
|
||||
if image == "" {
|
||||
image = imageNone
|
||||
}
|
||||
|
||||
imageRemovable := "1"
|
||||
|
||||
imageRo := "0"
|
||||
isImageRo, _ := isFlagExist(roDisk)
|
||||
if isImageRo {
|
||||
imageRo = "1"
|
||||
}
|
||||
|
||||
imageLow := strings.ToLower(image)
|
||||
imageCdrom := "0"
|
||||
if strings.HasSuffix(imageLow, ".iso") {
|
||||
imageRo = "1"
|
||||
imageCdrom = "1"
|
||||
}
|
||||
|
||||
// unmount
|
||||
if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil {
|
||||
log.Errorf("unmount file failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "unmount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
// removable flag
|
||||
if err := os.WriteFile(removableFlag, []byte(imageRemovable), 0o666); err != nil {
|
||||
log.Errorf("set removable flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set removable flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// ro flag
|
||||
if err := os.WriteFile(roFlag, []byte(imageRo), 0o666); err != nil {
|
||||
log.Errorf("set ro flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set ro flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom flag
|
||||
if err := os.WriteFile(cdromFlag, []byte(imageCdrom), 0o666); err != nil {
|
||||
log.Errorf("set cdrom flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set cdrom flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// mount
|
||||
if err := os.WriteFile(mountDevice, []byte(image), 0o666); err != nil {
|
||||
log.Errorf("mount file %s failed: %s", image, err)
|
||||
rsp.ErrRsp(c, -2, "mount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
h := hid.GetHid()
|
||||
h.Lock()
|
||||
h.CloseNoLock()
|
||||
defer func() {
|
||||
h.OpenNoLock()
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
// reset usb
|
||||
commands := []string{
|
||||
"echo > /sys/kernel/config/usb_gadget/g0/UDC",
|
||||
@@ -128,6 +140,7 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
rsp.ErrRsp(c, -2, "execute command failed")
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
@@ -155,17 +168,53 @@ func (s *Service) GetMountedImage(c *gin.Context) {
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func isFlagExist(flag string) (bool, error) {
|
||||
_, err := os.Stat(flag)
|
||||
func (s *Service) GetCdRom(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if err == nil {
|
||||
return true, nil
|
||||
content, err := os.ReadFile(cdromFlag)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read failed")
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
flag := strings.ReplaceAll(string(content), "\n", "")
|
||||
flatInt, err := strconv.ParseInt(flag, 10, 64)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "parse failed")
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("check file %s err: %s", flag, err)
|
||||
return false, err
|
||||
data := &proto.GetCdRomRsp{
|
||||
Cdrom: flatInt,
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteImage(c *gin.Context) {
|
||||
var req proto.DeleteImageReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.ToLower(req.File)
|
||||
validPrefix := strings.HasPrefix(filename, imageDirectory)
|
||||
validSuffix := strings.HasSuffix(filename, ".iso") || strings.HasSuffix(filename, ".img")
|
||||
|
||||
if !validPrefix || !validSuffix {
|
||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(req.File); err != nil {
|
||||
rsp.ErrRsp(c, -3, "remove file failed")
|
||||
log.Errorf("failed to remove file %s: %s", req.File, err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("delete image %s success", req.File)
|
||||
}
|
||||
|
||||
44
server/service/stream/direct/h264.go
Normal file
44
server/service/stream/direct/h264.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
streamer = newStreamer()
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to upgrade to websocket: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = ws.Close()
|
||||
log.Debugf("h264 websocket disconnected: %s", ws.RemoteAddr())
|
||||
}()
|
||||
log.Debugf("h264 websocket connected: %s", ws.RemoteAddr())
|
||||
|
||||
_ = ws.SetReadDeadline(time.Time{})
|
||||
|
||||
streamer.addClient(ws)
|
||||
defer streamer.removeClient(ws)
|
||||
|
||||
for {
|
||||
if _, _, err := ws.ReadMessage(); err != nil {
|
||||
log.Debugf("failed to read message (client disconnected): %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
12
server/service/stream/direct/pool.go
Normal file
12
server/service/stream/direct/pool.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var BufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
123
server/service/stream/direct/streamer.go
Normal file
123
server/service/stream/direct/streamer.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Streamer struct {
|
||||
mutex sync.RWMutex
|
||||
clients map[*websocket.Conn]bool
|
||||
running int32
|
||||
}
|
||||
|
||||
func newStreamer() *Streamer {
|
||||
return &Streamer{
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) addClient(ws *websocket.Conn) {
|
||||
s.mutex.Lock()
|
||||
s.clients[ws] = true
|
||||
s.mutex.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||
go s.run()
|
||||
log.Debug("h264 stream started")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) removeClient(ws *websocket.Conn) {
|
||||
s.mutex.Lock()
|
||||
delete(s.clients, ws)
|
||||
s.mutex.Unlock()
|
||||
|
||||
log.Debugf("h264 websocket disconnected, remaining clients: %d", len(s.clients))
|
||||
}
|
||||
|
||||
func (s *Streamer) getClientCount() int {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return len(s.clients)
|
||||
}
|
||||
|
||||
func (s *Streamer) run() {
|
||||
defer atomic.StoreInt32(&s.running, 0)
|
||||
|
||||
duration := time.Second / time.Duration(120)
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
screen := common.GetScreen()
|
||||
vision := common.GetKvmVision()
|
||||
startTime := time.Now()
|
||||
|
||||
for range ticker.C {
|
||||
if s.getClientCount() == 0 {
|
||||
log.Debug("h264 stream stopped due to no clients")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
isKeyFrame := byte(0)
|
||||
if result == 3 {
|
||||
isKeyFrame = byte(1)
|
||||
}
|
||||
|
||||
timestamp := time.Since(startTime).Microseconds()
|
||||
|
||||
if err := s.send(isKeyFrame, timestamp, data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) send(isKeyFrame byte, timestamp int64, data []byte) error {
|
||||
buf := BufferPool.Get().(*bytes.Buffer)
|
||||
defer BufferPool.Put(buf)
|
||||
|
||||
buf.Reset()
|
||||
|
||||
if err := buf.WriteByte(isKeyFrame); err != nil {
|
||||
log.Errorf("failed to write keyframe flag: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
tsBytes := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(tsBytes, uint64(timestamp))
|
||||
if _, err := buf.Write(tsBytes); err != nil {
|
||||
log.Errorf("failed to write timestamp: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Write(data); err != nil {
|
||||
log.Errorf("failed to write h264 data: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for client := range s.clients {
|
||||
if err := client.WriteMessage(websocket.BinaryMessage, buf.Bytes()); err != nil {
|
||||
log.Errorf("failed to write message to client %s: %s.", client.RemoteAddr(), err)
|
||||
|
||||
s.removeClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func GetFrameRateCounter() *FrameRateCounter {
|
||||
data := fmt.Sprintf("%d", counter.fps)
|
||||
err := os.WriteFile("/kvmapp/kvm/now_fps", []byte(data), 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("failed to wirte fps: %s", err)
|
||||
log.Errorf("failed to write fps: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -2,6 +2,7 @@ package h264
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4"
|
||||
@@ -9,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
pc *webrtc.PeerConnection
|
||||
ws *websocket.Conn
|
||||
pc *webrtc.PeerConnection
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
@@ -36,9 +38,7 @@ func (c *Client) addTrack() {
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
trackMap[c.ws] = videoTrack
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// register callback events
|
||||
@@ -61,13 +61,9 @@ func (c *Client) register() {
|
||||
// ICE connection state has changed
|
||||
c.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state == webrtc.ICEConnectionStateConnected && !isSending {
|
||||
mutex.Lock()
|
||||
if !isSending {
|
||||
// start sending h264 data
|
||||
go send()
|
||||
isSending = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
// start sending h264 data
|
||||
go send()
|
||||
isSending = true
|
||||
}
|
||||
|
||||
log.Debugf("ice connection state has changed to %s", state.String())
|
||||
@@ -81,13 +77,11 @@ func (c *Client) readMessage() {
|
||||
for {
|
||||
_, raw, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
delete(trackMap, c.ws)
|
||||
if len(trackMap) == 0 && isSending {
|
||||
if isSending && len(trackMap) == 0 {
|
||||
// stop sending when all websocket connections are closed
|
||||
exitSig <- true
|
||||
isSending = false
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
log.Debugf("failed to read message: %s", err)
|
||||
return
|
||||
@@ -155,8 +149,8 @@ func (c *Client) readMessage() {
|
||||
|
||||
// send websocket message
|
||||
func (c *Client) sendMessage(event string, data string) error {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
message := &Message{
|
||||
Event: event,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/config"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -18,9 +19,7 @@ var (
|
||||
},
|
||||
}
|
||||
trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample)
|
||||
mutex = sync.RWMutex{}
|
||||
isSending = false
|
||||
exitSig = make(chan bool, 1)
|
||||
)
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
@@ -35,11 +34,14 @@ func Connect(c *gin.Context) {
|
||||
log.Debugf("h264 websocket disconnected")
|
||||
}()
|
||||
|
||||
var zeroTime time.Time
|
||||
_ = wsConn.SetReadDeadline(zeroTime)
|
||||
|
||||
conf := config.GetInstance()
|
||||
|
||||
var iceServers []webrtc.ICEServer
|
||||
|
||||
if conf.Stun != "" {
|
||||
if conf.Stun != "" && conf.Stun != "disable" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"stun:" + conf.Stun},
|
||||
})
|
||||
@@ -53,11 +55,9 @@ func Connect(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
rtc_config := webrtc.Configuration{
|
||||
peerConn, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
}
|
||||
|
||||
peerConn, err := webrtc.NewPeerConnection(rtc_config)
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to create PeerConnection: %s", err)
|
||||
return
|
||||
@@ -69,8 +69,9 @@ func Connect(c *gin.Context) {
|
||||
}()
|
||||
|
||||
client := &Client{
|
||||
ws: wsConn,
|
||||
pc: peerConn,
|
||||
ws: wsConn,
|
||||
pc: peerConn,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
|
||||
client.addTrack()
|
||||
|
||||
@@ -2,7 +2,6 @@ package h264
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
|
||||
func send() {
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
@@ -18,63 +19,31 @@ 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 || len(data) == 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)
|
||||
}
|
||||
}
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("send h264 data: %d", len(data))
|
||||
}
|
||||
|
||||
@@ -1,132 +1,55 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
frameDetect = "/etc/kvm/frame_detact"
|
||||
frameDetectTmp = "/etc/kvm/frame_detact_tmp"
|
||||
)
|
||||
|
||||
type UpdateFrameDetectRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func GetFrameDetect(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled, err := isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame detect status")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetFrameDetectRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
log.Debugf("get frame detect success, enabled: %t", isEnabled)
|
||||
}
|
||||
const FrameDetectInterval uint8 = 60
|
||||
|
||||
func UpdateFrameDetect(c *gin.Context) {
|
||||
var req proto.UpdateFrameDetectReq
|
||||
var rsp proto.Response
|
||||
|
||||
isEnabled, err := isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if isEnabled {
|
||||
_ = os.Remove(frameDetect)
|
||||
_ = os.Remove(frameDetectTmp)
|
||||
} else {
|
||||
file, err2 := os.OpenFile(frameDetect, os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err2 != nil {
|
||||
rsp.ErrRsp(c, -3, "enable frame detect failed")
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
var frame uint8 = 0
|
||||
if req.Enabled {
|
||||
frame = FrameDetectInterval
|
||||
}
|
||||
|
||||
isEnabled, err = isFrameDetectEnabled()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -4, "unknown frame status")
|
||||
return
|
||||
}
|
||||
common.GetKvmVision().SetFrameDetect(frame)
|
||||
|
||||
rsp.OkRspWithData(c, &UpdateFrameDetectRsp{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
log.Debugf("update frame detect success, enabled: %t", isEnabled)
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("update frame detect: %t", req.Enabled)
|
||||
}
|
||||
|
||||
func StopFrameDetect(c *gin.Context) {
|
||||
var req proto.StopFrameDetectReq
|
||||
var rsp proto.Response
|
||||
|
||||
exist, err := isFileExist(frameDetect)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if !exist {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
duration := 10 * time.Second
|
||||
if req.Duration > 0 {
|
||||
duration = time.Duration(req.Duration) * time.Second
|
||||
}
|
||||
|
||||
err = os.Rename(frameDetect, frameDetectTmp)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "stop operation failed")
|
||||
return
|
||||
}
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Second)
|
||||
_ = os.Rename(frameDetectTmp, frameDetect)
|
||||
log.Debugf("frame detect started")
|
||||
}()
|
||||
vision.SetFrameDetect(0)
|
||||
time.Sleep(duration)
|
||||
vision.SetFrameDetect(FrameDetectInterval)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("frame detect stoped")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,111 +1,22 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
chanMap = make(map[*gin.Context]chan struct{})
|
||||
mutex = sync.RWMutex{}
|
||||
exitSig = make(chan bool, 1)
|
||||
)
|
||||
var streamer = NewStreamer()
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
c.Header("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=frame"))
|
||||
c.Header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("X-Server-Date", time.Now().Format(time.RFC1123))
|
||||
|
||||
mutex.Lock()
|
||||
chanMap[c] = make(chan struct{}, 1)
|
||||
if len(chanMap) == 1 {
|
||||
go send()
|
||||
}
|
||||
mutex.Unlock()
|
||||
streamer.AddClient(c)
|
||||
defer streamer.RemoveClient(c)
|
||||
|
||||
<-chanMap[c]
|
||||
|
||||
mutex.Lock()
|
||||
delete(chanMap, c)
|
||||
if len(chanMap) == 0 {
|
||||
exitSig <- true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
func send() {
|
||||
screen := common.GetScreen()
|
||||
fps := screen.FPS
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(fps))
|
||||
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
|
||||
}
|
||||
|
||||
quality := screen.Quality
|
||||
if _, ok := common.QualityMap[quality]; !ok {
|
||||
quality = 80
|
||||
}
|
||||
|
||||
data, result := vision.ReadMjpeg(width, height, quality)
|
||||
if result < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for c, ch := 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))
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
|
||||
if screen.FPS != fps {
|
||||
fps = screen.FPS
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
}
|
||||
|
||||
case <-exitSig:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func write(c *gin.Context, data []byte) (err error) {
|
||||
if _, err = c.Writer.Write([]byte("--frame\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("Content-Type: image/jpeg\r\n\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.Flush()
|
||||
return
|
||||
<-c.Request.Context().Done()
|
||||
}
|
||||
|
||||
131
server/service/stream/mjpeg/streamer.go
Normal file
131
server/service/stream/mjpeg/streamer.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Streamer struct {
|
||||
mutex sync.RWMutex
|
||||
clients map[*gin.Context]bool
|
||||
running int32
|
||||
}
|
||||
|
||||
func NewStreamer() *Streamer {
|
||||
return &Streamer{
|
||||
clients: make(map[*gin.Context]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) AddClient(c *gin.Context) {
|
||||
s.mutex.Lock()
|
||||
s.clients[c] = true
|
||||
s.mutex.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||
go s.run()
|
||||
log.Debug("mjpeg stream started")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) RemoveClient(c *gin.Context) {
|
||||
s.mutex.Lock()
|
||||
delete(s.clients, c)
|
||||
s.mutex.Unlock()
|
||||
|
||||
log.Debugf("mjpeg connection removed, remaining clients: %d", len(s.clients))
|
||||
}
|
||||
|
||||
func (s *Streamer) getClients() []*gin.Context {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
clients := make([]*gin.Context, 0, len(s.clients))
|
||||
for c := range s.clients {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (s *Streamer) getClientCount() int {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return len(s.clients)
|
||||
}
|
||||
|
||||
func (s *Streamer) run() {
|
||||
defer atomic.StoreInt32(&s.running, 0)
|
||||
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
fps := screen.FPS
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(fps))
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if s.getClientCount() == 0 {
|
||||
log.Debug("mjpeg stream stopped due to no clients")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadMjpeg(screen.Width, screen.Height, screen.Quality)
|
||||
if result < 0 || result == 5 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
clients := s.getClients()
|
||||
for _, client := range clients {
|
||||
if err := writeFrame(client, data); err != nil {
|
||||
log.Errorf("failed to write mjpeg frame for client %s: %s", client.Request.RemoteAddr, err)
|
||||
s.RemoveClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
if screen.FPS != fps && screen.FPS != 0 {
|
||||
fps = screen.FPS
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
|
||||
func writeFrame(c *gin.Context, data []byte) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = c.Request.Context().Err()
|
||||
if err == nil {
|
||||
err = fmt.Errorf("panic recovered in writeFrame: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
header := "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + strconv.Itoa(len(data)) + "\r\n\r\n"
|
||||
if _, err = c.Writer.WriteString(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = c.Writer.Write([]byte("\r\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writer.Flush()
|
||||
return nil
|
||||
}
|
||||
112
server/service/stream/webrtc/client.go
Normal file
112
server/service/stream/webrtc/client.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewClient(ws *websocket.Conn, videoConn *webrtc.PeerConnection) *Client {
|
||||
return &Client{
|
||||
ws: ws,
|
||||
video: videoConn,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WriteMessage(event string, data string) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
message := &Message{
|
||||
Event: event,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if err := c.ws.WriteJSON(message); err != nil {
|
||||
log.Errorf("failed to send message %s: %v", event, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("sent message %s", event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadMessage() (*Message, error) {
|
||||
_, raw, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read message: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
log.Errorf("failed to unmarshal message: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack() error {
|
||||
// video track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
|
||||
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264},
|
||||
"video",
|
||||
"pion-video",
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create video track: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
videoPacketizer := rtp.NewPacketizer(
|
||||
1200,
|
||||
100,
|
||||
0x1234ABCD,
|
||||
&codecs.H264Payloader{},
|
||||
rtp.NewRandomSequencer(),
|
||||
90000,
|
||||
)
|
||||
if videoPacketizer == nil {
|
||||
err := errors.New("failed to create rtp packetizer")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
videoSender, err := c.video.AddTrack(videoTrack)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add video track: %s", err)
|
||||
return err
|
||||
}
|
||||
go startRTCPReader(videoSender)
|
||||
|
||||
track := &Track{
|
||||
videoPacketizer: videoPacketizer,
|
||||
video: videoTrack,
|
||||
}
|
||||
track.updateExtension()
|
||||
|
||||
c.mutex.Lock()
|
||||
c.track = track
|
||||
c.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startRTCPReader(sender *webrtc.RTPSender) {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
if _, _, err := sender.Read(rtcpBuf); err != nil {
|
||||
log.Debugf("RTCP reader error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
158
server/service/stream/webrtc/h264.go
Normal file
158
server/service/stream/webrtc/h264.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
globalManager *WebRTCManager
|
||||
managerOnce sync.Once
|
||||
)
|
||||
|
||||
func getManager() *WebRTCManager {
|
||||
managerOnce.Do(func() {
|
||||
globalManager = NewWebRTCManager()
|
||||
})
|
||||
return globalManager
|
||||
}
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
// create WebSocket connection
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 websocket: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = wsConn.Close()
|
||||
log.Debugf("h264 websocket disconnected: %s", c.ClientIP())
|
||||
}()
|
||||
log.Debugf("h264 websocket connected: %s", c.ClientIP())
|
||||
|
||||
var zeroTime time.Time
|
||||
_ = wsConn.SetReadDeadline(zeroTime)
|
||||
|
||||
// create video connection
|
||||
iceServers := createICEServers()
|
||||
|
||||
mediaEngine, err := createMediaEngine()
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 media engine: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
videoConn, err := createPeerConnection(iceServers, mediaEngine)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create h264 video peer connection: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = videoConn.Close()
|
||||
log.Debugf("h264 video peer disconnected: %s", c.ClientIP())
|
||||
}()
|
||||
|
||||
// create client
|
||||
client := NewClient(wsConn, videoConn)
|
||||
if err := client.AddTrack(); err != nil {
|
||||
log.Errorf("failed to add track: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
manager := getManager()
|
||||
manager.AddClient(wsConn, client)
|
||||
defer manager.RemoveClient(wsConn)
|
||||
|
||||
// handle signaling
|
||||
signalingHandler := NewSignalingHandler(client)
|
||||
signalingHandler.RegisterCallbacks()
|
||||
|
||||
// read and wait
|
||||
for {
|
||||
message, err := client.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if message != nil {
|
||||
if err := signalingHandler.HandleMessage(message); err != nil {
|
||||
log.Errorf("failed to handle signaling message: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createICEServers() []webrtc.ICEServer {
|
||||
var iceServers []webrtc.ICEServer
|
||||
|
||||
conf := config.GetInstance()
|
||||
|
||||
if conf.Stun != "" && conf.Stun != "disable" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"stun:" + conf.Stun},
|
||||
})
|
||||
}
|
||||
|
||||
if conf.Turn.TurnAddr != "" && conf.Turn.TurnUser != "" && conf.Turn.TurnCred != "" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"turn:" + conf.Turn.TurnAddr},
|
||||
Username: conf.Turn.TurnUser,
|
||||
Credential: conf.Turn.TurnCred,
|
||||
})
|
||||
}
|
||||
|
||||
return iceServers
|
||||
}
|
||||
|
||||
func createMediaEngine() (*webrtc.MediaEngine, error) {
|
||||
mediaEngine := &webrtc.MediaEngine{}
|
||||
|
||||
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
||||
log.Errorf("failed to register default codecs: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mediaEngine.RegisterHeaderExtension(
|
||||
webrtc.RTPHeaderExtensionCapability{URI: "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"},
|
||||
webrtc.RTPCodecTypeVideo,
|
||||
); err != nil {
|
||||
log.Errorf("failed to register header extension: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mediaEngine, nil
|
||||
}
|
||||
|
||||
func createPeerConnection(iceServers []webrtc.ICEServer, mediaEngine *webrtc.MediaEngine) (*webrtc.PeerConnection, error) {
|
||||
settingEngine := webrtc.SettingEngine{}
|
||||
settingEngine.SetSRTPProtectionProfiles(
|
||||
dtls.SRTP_AEAD_AES_128_GCM,
|
||||
dtls.SRTP_AES128_CM_HMAC_SHA1_80,
|
||||
)
|
||||
|
||||
apiOptions := []func(api *webrtc.API){
|
||||
webrtc.WithSettingEngine(settingEngine),
|
||||
}
|
||||
if mediaEngine != nil {
|
||||
apiOptions = append(apiOptions, webrtc.WithMediaEngine(mediaEngine))
|
||||
}
|
||||
|
||||
api := webrtc.NewAPI(apiOptions...)
|
||||
|
||||
return api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
||||
})
|
||||
}
|
||||
96
server/service/stream/webrtc/manager.go
Normal file
96
server/service/stream/webrtc/manager.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewWebRTCManager() *WebRTCManager {
|
||||
return &WebRTCManager{
|
||||
clients: make(map[*websocket.Conn]*Client),
|
||||
videoSending: 0,
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) AddClient(ws *websocket.Conn, client *Client) {
|
||||
client.track.updateExtension()
|
||||
|
||||
m.mutex.Lock()
|
||||
m.clients[ws] = client
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("added client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) RemoveClient(ws *websocket.Conn) {
|
||||
m.mutex.Lock()
|
||||
delete(m.clients, ws)
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("removed client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) GetClientCount() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return len(m.clients)
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) StartVideoStream() {
|
||||
if atomic.CompareAndSwapInt32(&m.videoSending, 0, 1) {
|
||||
go m.sendVideoStream()
|
||||
log.Debugf("start sending h264 stream")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) sendVideoStream() {
|
||||
defer atomic.StoreInt32(&m.videoSending, 0)
|
||||
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if m.GetClientCount() == 0 {
|
||||
log.Debugf("stop sending h264 stream")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sample := media.Sample{
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
for _, client := range m.clients {
|
||||
client.track.writeVideo(sample)
|
||||
}
|
||||
|
||||
if screen.FPS != fps && screen.FPS != 0 {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
149
server/service/stream/webrtc/signaling.go
Normal file
149
server/service/stream/webrtc/signaling.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewSignalingHandler(client *Client) *SignalingHandler {
|
||||
return &SignalingHandler{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCallbacks Register callback functions
|
||||
func (s *SignalingHandler) RegisterCallbacks() {
|
||||
// video ICE candidate
|
||||
s.client.video.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidateByte, err := json.Marshal(candidate.ToJSON())
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal video candidate: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.client.WriteMessage("video-candidate", string(candidateByte)); err != nil {
|
||||
log.Errorf("failed to send video candidate: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
manager := getManager()
|
||||
|
||||
// video connection state change
|
||||
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state == webrtc.ICEConnectionStateConnected {
|
||||
manager.StartVideoStream()
|
||||
}
|
||||
|
||||
log.Debugf("video connection state changed to %s", state.String())
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMessage handle the received message
|
||||
func (s *SignalingHandler) HandleMessage(message *Message) error {
|
||||
switch message.Event {
|
||||
case "video-offer":
|
||||
return s.handleVideoOffer(message.Data)
|
||||
case "video-candidate":
|
||||
return s.handleVideoCandidate(message.Data)
|
||||
case "heartbeat":
|
||||
return s.handleHeartbeat()
|
||||
default:
|
||||
log.Debugf("Unhandled message event: %s", message.Event)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalingHandler) handleVideoOffer(data string) error {
|
||||
if s.client.video.SignalingState() != webrtc.SignalingStateStable {
|
||||
err := errors.New("video signaling is not stable")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
offer := webrtc.SessionDescription{}
|
||||
if err := json.Unmarshal([]byte(data), &offer); err != nil {
|
||||
log.Errorf("failed to unmarshal video offer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetRemoteDescription(offer); err != nil {
|
||||
log.Errorf("failed to set remote description: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := s.client.video.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetLocalDescription(answer); err != nil {
|
||||
log.Errorf("failed to set local description: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.updateHeaderExtensionID(); err != nil {
|
||||
log.Errorf("could not update header extension ID: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
answerByte, err := json.Marshal(answer)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.WriteMessage("video-answer", string(answerByte))
|
||||
}
|
||||
|
||||
// set extension ID
|
||||
func (s *SignalingHandler) updateHeaderExtensionID() error {
|
||||
receivers := s.client.video.GetReceivers()
|
||||
if len(receivers) == 0 {
|
||||
return errors.New("no RTP receiver found for video")
|
||||
}
|
||||
|
||||
params := receivers[0].GetParameters()
|
||||
if len(params.HeaderExtensions) == 0 {
|
||||
return errors.New("no header extensions found in negotiated parameters")
|
||||
}
|
||||
|
||||
for _, ext := range params.HeaderExtensions {
|
||||
if ext.URI == "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" {
|
||||
s.client.track.playoutDelayExtensionID = uint8(ext.ID)
|
||||
log.Debugf("found and set playout delay extension ID to: %d", ext.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("no track extension found in negotiated parameters, use default value 5")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle video candidate
|
||||
func (s *SignalingHandler) handleVideoCandidate(data string) error {
|
||||
candidate := webrtc.ICECandidateInit{}
|
||||
if err := json.Unmarshal([]byte(data), &candidate); err != nil {
|
||||
log.Errorf("failed to unmarshal candidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.AddICECandidate(candidate); err != nil {
|
||||
log.Errorf("failed to add ICECandidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle heartbeat
|
||||
func (s *SignalingHandler) handleHeartbeat() error {
|
||||
return s.client.WriteMessage("heartbeat", "")
|
||||
}
|
||||
53
server/service/stream/webrtc/track.go
Normal file
53
server/service/stream/webrtc/track.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (t *Track) updateExtension() {
|
||||
if t.playoutDelayExtensionID == 0 {
|
||||
t.playoutDelayExtensionID = 5
|
||||
}
|
||||
|
||||
if t.playoutDelayExtensionData == nil || len(t.playoutDelayExtensionData) == 0 {
|
||||
playoutDelay := &rtp.PlayoutDelayExtension{
|
||||
MinDelay: 0,
|
||||
MaxDelay: 0,
|
||||
}
|
||||
playoutDelayExtensionData, err := playoutDelay.Marshal()
|
||||
if err == nil {
|
||||
t.playoutDelayExtensionData = playoutDelayExtensionData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Track) writeVideoSample(sample media.Sample) error {
|
||||
samples := uint32(sample.Duration.Seconds() * 90000)
|
||||
packets := t.videoPacketizer.Packetize(sample.Data, samples)
|
||||
|
||||
for _, p := range packets {
|
||||
p.Header.Extension = true
|
||||
p.Header.ExtensionProfile = 0xBEDE
|
||||
|
||||
if err := p.Header.SetExtension(t.playoutDelayExtensionID, t.playoutDelayExtensionData); err != nil {
|
||||
log.Errorf("Failed to set extension: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.video.WriteRTP(p); err != nil {
|
||||
log.Errorf("failed to write RTP: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) writeVideo(sample media.Sample) {
|
||||
err := t.writeVideoSample(sample)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write h264 video: %s", err)
|
||||
}
|
||||
}
|
||||
38
server/service/stream/webrtc/types.go
Normal file
38
server/service/stream/webrtc/types.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type WebRTCManager struct {
|
||||
clients map[*websocket.Conn]*Client
|
||||
videoSending int32
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
video *webrtc.PeerConnection
|
||||
track *Track
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type SignalingHandler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
playoutDelayExtensionID uint8
|
||||
playoutDelayExtensionData []byte
|
||||
videoPacketizer rtp.Packetizer
|
||||
video *webrtc.TrackLocalStaticRTP
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
@@ -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
|
||||
@@ -46,22 +47,24 @@ func (s *Service) SetGpio(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("gpio %s set sucessfully", device)
|
||||
log.Debugf("gpio %s set successfully", device)
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func (s *Service) GetGpio(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
pwr, err := readGpio(s.config.Hardware.GPIOPowerLED)
|
||||
conf := config.GetInstance().Hardware
|
||||
|
||||
pwr, err := readGpio(conf.GPIOPowerLED)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read power led: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
hdd := false
|
||||
if s.config.Hardware.Version == config.HWVersionAlpha {
|
||||
hdd, err = readGpio(s.config.Hardware.GPIOHDDLed)
|
||||
if conf.Version == config.HWVersionAlpha {
|
||||
hdd, err = readGpio(conf.GPIOHDDLed)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read hdd led: %s", err))
|
||||
return
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
"NanoKVM-Server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -17,7 +19,42 @@ func (s *Service) ResetHdmi(c *gin.Context) {
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(1 * time.Second)
|
||||
vision.SetHDMI(true)
|
||||
utils.PersistHDMIEnabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("reset hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) EnableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(true)
|
||||
utils.PersistHDMIEnabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("enable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) DisableHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(false)
|
||||
utils.PersistHDMIDisabled()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("disable hdmi")
|
||||
}
|
||||
|
||||
func (s *Service) GetHdmiState(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetGetHdmiStateRsp{
|
||||
Enabled: !utils.IsHdmiDisabled(),
|
||||
})
|
||||
|
||||
log.Debug("get hdmi state")
|
||||
}
|
||||
|
||||
84
server/service/vm/hostname.go
Normal file
84
server/service/vm/hostname.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
BootHostnameFile = "/boot/hostname"
|
||||
EtcHostname = "/etc/hostname"
|
||||
EtcHosts = "/etc/hosts"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
dataRead, err := os.ReadFile(EtcHostname)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hostname failed")
|
||||
return
|
||||
}
|
||||
|
||||
oldHostname := strings.Replace(string(dataRead), "\n", "", -1)
|
||||
|
||||
if (oldHostname != req.Hostname) {
|
||||
dataRead, err = os.ReadFile(EtcHosts)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hosts failed")
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte(strings.Replace(string(dataRead), oldHostname, req.Hostname, -1))
|
||||
|
||||
if err := os.WriteFile(EtcHosts, data, 0o644); err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to write data")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := []byte(fmt.Sprintf("%s", req.Hostname))
|
||||
|
||||
if err := os.WriteFile(BootHostnameFile, data, 0o644); err != nil {
|
||||
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)
|
||||
|
||||
_ = exec.Command("hostname", "-F", EtcHostname).Run()
|
||||
}
|
||||
|
||||
func (s *Service) GetHostname(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
data, err := os.ReadFile(EtcHostname)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read Hostname failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetHostnameRsp{
|
||||
Hostname: strings.Replace(string(data), "\n", "", -1),
|
||||
})
|
||||
log.Debugf("get Hostname successful")
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package vm
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -18,13 +17,15 @@ var imageVersionMap = map[string]string{
|
||||
"2024-07-23-20-18-587710.img": "v1.1.0",
|
||||
"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(),
|
||||
@@ -35,23 +36,31 @@ 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 {
|
||||
if pid := getAvahiDaemonPid(); pid == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
content, err := os.ReadFile("/etc/hostname")
|
||||
if err != nil {
|
||||
return ""
|
||||
|
||||
107
server/service/vm/ip.go
Normal file
107
server/service/vm/ip.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
Wired = "Wired"
|
||||
Wireless = "Wireless"
|
||||
Other = "Other"
|
||||
)
|
||||
|
||||
type InterfaceInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
func GetInterfaceInfos() ([]*InterfaceInfo, error) {
|
||||
var interfaceInfos []*InterfaceInfo
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get net interfaces: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
info := getInterfaceInfo(iface)
|
||||
if info != nil {
|
||||
interfaceInfos = append(interfaceInfos, info)
|
||||
}
|
||||
}
|
||||
|
||||
if len(interfaceInfos) == 0 {
|
||||
return nil, fmt.Errorf("no valid IP address")
|
||||
}
|
||||
|
||||
return interfaceInfos, nil
|
||||
}
|
||||
|
||||
func getInterfaceInfo(iface net.Interface) *InterfaceInfo {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
interfaceType := getInterfaceType(iface)
|
||||
if interfaceType == Other {
|
||||
return nil
|
||||
}
|
||||
|
||||
interfaceIP := getInterfaceIP(iface)
|
||||
if interfaceIP == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &InterfaceInfo{
|
||||
Name: iface.Name,
|
||||
Type: interfaceType,
|
||||
IP: interfaceIP,
|
||||
}
|
||||
}
|
||||
|
||||
func getInterfaceType(iface net.Interface) string {
|
||||
if strings.HasPrefix(iface.Name, "eth") || strings.HasPrefix(iface.Name, "en") {
|
||||
return Wired
|
||||
}
|
||||
|
||||
if strings.HasPrefix(iface.Name, "wlan") || strings.HasPrefix(iface.Name, "wl") {
|
||||
return Wireless
|
||||
}
|
||||
|
||||
return Other
|
||||
}
|
||||
|
||||
func getInterfaceIP(iface net.Interface) net.IP {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get interface addresses: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
134
server/service/vm/jiggler/jiggler.go
Normal file
134
server/service/vm/jiggler/jiggler.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package jiggler
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/service/hid"
|
||||
"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
|
||||
}
|
||||
|
||||
func move(mode string) {
|
||||
h := hid.GetHid()
|
||||
|
||||
if mode == "absolute" {
|
||||
h.WriteHid2([]byte{0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00})
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
h.WriteHid2([]byte{0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00})
|
||||
} else {
|
||||
h.WriteHid1([]byte{0x00, 0xa, 0xa, 0x00})
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
h.WriteHid1([]byte{0x00, 0xf6, 0xf6, 0x00})
|
||||
}
|
||||
}
|
||||
92
server/service/vm/mdns.go
Normal file
92
server/service/vm/mdns.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
AvahiDaemonPid = "/run/avahi-daemon/pid"
|
||||
AvahiDaemonScript = "/etc/init.d/S50avahi-daemon"
|
||||
AvahiDaemonBackupScript = "/kvmapp/system/init.d/S50avahi-daemon"
|
||||
)
|
||||
|
||||
func (s *Service) GetMdnsState(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
pid := getAvahiDaemonPid()
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetMdnsStateRsp{
|
||||
Enabled: pid != "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) EnableMdns(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
pid := getAvahiDaemonPid()
|
||||
if pid != "" {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("cp -f %s %s", AvahiDaemonBackupScript, AvahiDaemonScript),
|
||||
fmt.Sprintf("%s start", AvahiDaemonScript),
|
||||
}
|
||||
|
||||
command := strings.Join(commands, " && ")
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to start avahi-daemon: %s", err)
|
||||
rsp.ErrRsp(c, -1, "failed to enable mdns")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("avahi-daemon started")
|
||||
}
|
||||
|
||||
func (s *Service) DisableMdns(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
pid := getAvahiDaemonPid()
|
||||
if pid == "" {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("kill -9 %s", pid)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to stop avahi-daemon: %s", err)
|
||||
rsp.ErrRsp(c, -1, "failed to disable mdns")
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.Remove(AvahiDaemonPid)
|
||||
_ = os.Remove(AvahiDaemonScript)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("avahi-daemon stopped")
|
||||
}
|
||||
|
||||
func getAvahiDaemonPid() string {
|
||||
if _, err := os.Stat(AvahiDaemonPid); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(AvahiDaemonPid)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read mdns pid: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(string(content), "\n", "")
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (s *Service) SetMemoryLimit(c *gin.Context) {
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("set memory limit successful, enabled: %t, limit: %s", req.Enabled, req.Limit)
|
||||
log.Debugf("set memory limit successful, enabled: %t, limit: %d", req.Enabled, req.Limit)
|
||||
}
|
||||
|
||||
func (s *Service) GetMemoryLimit(c *gin.Context) {
|
||||
|
||||
49
server/service/vm/mouse_jiggler.go
Normal file
49
server/service/vm/mouse_jiggler.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/service/vm/jiggler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) GetMouseJiggler(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
mouseJiggler := jiggler.GetJiggler()
|
||||
|
||||
data := &proto.GetMouseJigglerRsp{
|
||||
Enabled: mouseJiggler.IsEnabled(),
|
||||
Mode: mouseJiggler.GetMode(),
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func (s *Service) SetMouseJiggler(c *gin.Context) {
|
||||
var req proto.SetMouseJigglerReq
|
||||
var rsp proto.Response
|
||||
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
mouseJiggler := jiggler.GetJiggler()
|
||||
|
||||
if req.Enabled {
|
||||
err = mouseJiggler.Enable(req.Mode)
|
||||
} else {
|
||||
err = mouseJiggler.Disable()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set mouse jiggler: %t", req.Enabled)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"NanoKVM-Server/common"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -22,30 +23,34 @@ func (s *Service) SetScreen(c *gin.Context) {
|
||||
var req proto.SetScreenReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
file, ok := screenFileMap[req.Type]
|
||||
if !ok {
|
||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("%d", req.Value)
|
||||
if req.Type == "type" {
|
||||
switch req.Type {
|
||||
case "type":
|
||||
data := "h264"
|
||||
if req.Value == 0 {
|
||||
data = "mjpeg"
|
||||
} else {
|
||||
data = "h264"
|
||||
}
|
||||
err = writeScreen("type", data)
|
||||
|
||||
case "gop":
|
||||
gop := 30
|
||||
if req.Value >= 1 && req.Value <= 100 {
|
||||
gop = req.Value
|
||||
}
|
||||
common.GetKvmVision().SetGop(uint8(gop))
|
||||
|
||||
default:
|
||||
data := strconv.Itoa(req.Value)
|
||||
err = writeScreen(req.Type, data)
|
||||
}
|
||||
|
||||
err := os.WriteFile(file, []byte(data), 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("write kvm %s failed: %s", file, err)
|
||||
rsp.ErrRsp(c, -3, "update screen failed")
|
||||
rsp.ErrRsp(c, -2, "update screen failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,3 +59,18 @@ func (s *Service) SetScreen(c *gin.Context) {
|
||||
log.Debugf("update screen: %+v", req)
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func writeScreen(key string, value string) error {
|
||||
file, ok := screenFileMap[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid argument %s", key)
|
||||
}
|
||||
|
||||
err := os.WriteFile(file, []byte(value), 0o666)
|
||||
if err != nil {
|
||||
log.Errorf("write kvm %s failed: %s", file, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (s *Service) RunScript(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("run script %s faile: %s", req.Name, err)
|
||||
log.Errorf("run script %s failed: %s", req.Name, err.Error())
|
||||
rsp.ErrRsp(c, -2, "run script failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{
|
||||
config: config.GetInstance(),
|
||||
}
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
176
server/service/vm/swap.go
Normal file
176
server/service/vm/swap.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
SwapFile = "/swapfile"
|
||||
InittabPath = "/etc/inittab"
|
||||
TempInittab = "/etc/.inittab.tmp"
|
||||
)
|
||||
|
||||
func (s *Service) GetSwap(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetSwapRsp{
|
||||
Size: getSwapSize(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetSwap(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
var req proto.SetSwapReq
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
size := getSwapSize()
|
||||
if req.Size == size {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
if err := disableSwap(); err != nil {
|
||||
rsp.ErrRsp(c, -2, "disable swap failed")
|
||||
return
|
||||
}
|
||||
if err := disableInittab(); err != nil {
|
||||
rsp.ErrRsp(c, -3, "disable inittab failed")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := enableSwap(req.Size); err != nil {
|
||||
rsp.ErrRsp(c, -4, "enable swap failed")
|
||||
return
|
||||
}
|
||||
if err := enableInittab(); err != nil {
|
||||
rsp.ErrRsp(c, -5, "enable inittab failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func getSwapSize() int64 {
|
||||
fileInfo, err := os.Stat(SwapFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return fileInfo.Size() / 1024 / 1024
|
||||
}
|
||||
|
||||
func enableSwap(size int64) error {
|
||||
if getSwapSize() > 0 {
|
||||
if err := disableSwap(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("fallocate -l %dM %s", size, SwapFile),
|
||||
fmt.Sprintf("chmod 600 %s", SwapFile),
|
||||
fmt.Sprintf("mkswap %s", SwapFile),
|
||||
fmt.Sprintf("swapon %s", SwapFile),
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to execute %s: %s", command, err)
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Debugf("set swap file size: %d", size)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableSwap() error {
|
||||
command := "swapoff -a"
|
||||
if err := exec.Command("sh", "-c", command).Run(); err != nil {
|
||||
log.Errorf("failed to execute swapoff: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(SwapFile); err != nil {
|
||||
log.Errorf("failed to delete %s: %s", SwapFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableInittab() error {
|
||||
f, err := os.OpenFile(InittabPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("read inittab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
content := fmt.Sprintf("\nsi11::sysinit:/sbin/swapon %s", SwapFile)
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
log.Errorf("write inittab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("write to %s: %s", InittabPath, content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableInittab() error {
|
||||
defer func() {
|
||||
_ = os.Remove(TempInittab)
|
||||
}()
|
||||
|
||||
input, err := os.ReadFile(InittabPath)
|
||||
if err != nil {
|
||||
log.Errorf("read fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(input), "\n")
|
||||
output := make([]string, 0)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasSuffix(line, SwapFile) {
|
||||
log.Debugf("%s delete line: %s", InittabPath, line)
|
||||
} else {
|
||||
output = append(output, line)
|
||||
}
|
||||
}
|
||||
|
||||
content := strings.Join(output, "\n")
|
||||
content = strings.TrimSuffix(content, "\n")
|
||||
if err := os.WriteFile(TempInittab, []byte(content), 0644); err != nil {
|
||||
log.Errorf("write temp fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(TempInittab, InittabPath); err != nil {
|
||||
log.Errorf("replace fstab failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user