├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_JA.md ├── README_ZH.md ├── kvmapp ├── jpg_stream │ ├── S95nanokvm │ └── jpg_stream ├── kvm_new_app ├── kvm_system │ └── kvm_stream └── system │ ├── init.d │ ├── S00kmod │ ├── S01fs │ ├── S03usbdev │ ├── S03usbhid │ ├── S15kvmhwd │ ├── S30eth │ ├── S30wifi │ ├── S50avahi-daemon │ ├── S50ssdpd │ ├── S50sshd │ ├── S80dnsmasq │ ├── S95nanokvm │ └── S98tailscaled │ ├── ko │ └── soph_mipi_rx.ko │ └── update-nanokvm.py ├── server ├── README.md ├── README_JA.md ├── README_ZH.md ├── common │ ├── cgo.go │ └── screen.go ├── config │ ├── config.go │ ├── default.go │ ├── file.go │ ├── hardware.go │ ├── jwt.go │ └── types.go ├── dl_lib │ ├── libaaccomm2.so │ ├── libaacdec2.so │ ├── libaacenc2.so │ ├── libaacsbrdec2.so │ ├── libaacsbrenc2.so │ ├── libae.so │ ├── libaf.so │ ├── libawb.so │ ├── libcli.so │ ├── libcvi_RES1.so │ ├── libcvi_VoiceEngine.so │ ├── libcvi_audio.so │ ├── libcvi_bin.so │ ├── libcvi_bin_isp.so │ ├── libcvi_ispd2.so │ ├── libcvi_ive.so │ ├── libcvi_ssp.so │ ├── libcvi_vqe.so │ ├── libdnvqe.so │ ├── libini.so │ ├── libisp.so │ ├── libisp_algo.so │ ├── libjson-c.so.5 │ ├── libkvm.so │ ├── libkvm_mmf.so │ ├── libmipi_tx.so │ ├── libmisc.so │ ├── libopencv_core.so.409 │ ├── libopencv_highgui.so.409 │ ├── libopencv_imgcodecs.so.409 │ ├── libopencv_imgproc.so.409 │ ├── libosdc.so │ ├── libraw_dump.so │ ├── libsys.so │ ├── libtinyalsa.so │ ├── libvdec.so │ ├── libvenc.so │ └── libvpu.so ├── go.mod ├── go.sum ├── include │ └── kvm_vision.h ├── logger │ ├── formatter.go │ └── logger.go ├── main.go ├── middleware │ ├── jwt.go │ └── tls.go ├── proto │ ├── application.go │ ├── auth.go │ ├── download.go │ ├── hid.go │ ├── network.go │ ├── request.go │ ├── response.go │ ├── storage.go │ ├── stream.go │ └── vm.go ├── router │ ├── application.go │ ├── auth.go │ ├── download.go │ ├── extensions.go │ ├── hid.go │ ├── network.go │ ├── router.go │ ├── storage.go │ ├── stream.go │ ├── vm.go │ └── ws.go ├── service │ ├── application │ │ ├── preview.go │ │ ├── service.go │ │ ├── update.go │ │ └── version.go │ ├── auth │ │ ├── account.go │ │ ├── login.go │ │ ├── password.go │ │ └── service.go │ ├── download │ │ └── service.go │ ├── extensions │ │ └── tailscale │ │ │ ├── cli.go │ │ │ ├── install.go │ │ │ └── service.go │ ├── hid │ │ ├── hid.go │ │ ├── keyboard.go │ │ ├── mode.go │ │ ├── mouse.go │ │ ├── operation.go │ │ ├── paste.go │ │ ├── reset.go │ │ └── service.go │ ├── network │ │ ├── service.go │ │ ├── wifi.go │ │ └── wol.go │ ├── storage │ │ ├── image.go │ │ └── service.go │ ├── stream │ │ ├── direct │ │ │ └── h264.go │ │ ├── frame-rate.go │ │ ├── h264 │ │ │ ├── client.go │ │ │ ├── h264.go │ │ │ └── sender.go │ │ └── mjpeg │ │ │ ├── frame-detect.go │ │ │ └── mjpeg.go │ ├── vm │ │ ├── gpio.go │ │ ├── hdmi.go │ │ ├── hostname.go │ │ ├── info.go │ │ ├── ip.go │ │ ├── jiggler │ │ │ ├── jiggler.go │ │ │ └── mouse.go │ │ ├── mdns.go │ │ ├── memory.go │ │ ├── mouse-jiggler.go │ │ ├── oled.go │ │ ├── screen.go │ │ ├── script.go │ │ ├── service.go │ │ ├── ssh.go │ │ ├── swap.go │ │ ├── system.go │ │ ├── terminal.go │ │ ├── tls.go │ │ ├── virtual-device.go │ │ └── web-title.go │ └── ws │ │ ├── message.go │ │ ├── service.go │ │ └── ws.go └── utils │ ├── cert.go │ ├── chmod.go │ ├── encrypt.go │ ├── http.go │ ├── memory.go │ ├── move-file.go │ ├── permission.go │ ├── untar.go │ └── unzip.go ├── support ├── README.md ├── README_ZH.md └── sg2002 │ ├── README.md │ ├── README_ZH.md │ ├── additional │ ├── kvm │ │ ├── CMakeLists.txt │ │ ├── Kconfig │ │ ├── include │ │ │ └── kvm_vision.h │ │ └── src │ │ │ └── kvm_vision.cpp │ ├── kvm_mmf │ │ ├── CMakeLists.txt │ │ ├── Kconfig │ │ ├── include │ │ │ └── kvm_mmf.hpp │ │ └── src │ │ │ └── kvm_mmf.cpp │ ├── peripheral │ │ └── port │ │ │ └── maixcam │ │ │ └── maix_i2c.cpp │ ├── sophgo-middleware │ │ └── v2 │ │ │ └── sample │ │ │ └── common │ │ │ └── sample_common_sensor.c │ └── vision │ │ ├── CMakeLists.txt │ │ ├── Kconfig │ │ ├── include │ │ ├── base │ │ │ └── maix_camera_base.hpp │ │ ├── maix_camera.hpp │ │ ├── maix_image.hpp │ │ ├── maix_image_color.hpp │ │ ├── maix_image_def.hpp │ │ ├── maix_image_obj.hpp │ │ └── maix_vision.hpp │ │ ├── include_private │ │ └── maix_image_util.hpp │ │ ├── port │ │ └── maixcam │ │ │ └── maix_camera_mmf.hpp │ │ └── src │ │ ├── maix_camera.cpp │ │ ├── maix_image.cpp │ │ └── maix_image_ops.cpp │ ├── build │ ├── kvm_system │ └── main │ │ ├── CMakeLists.txt │ │ ├── Kconfig │ │ ├── include │ │ └── config.h │ │ ├── lib │ │ ├── hdmi │ │ │ ├── hdmi.cpp │ │ │ └── hdmi.h │ │ ├── libqr │ │ │ ├── CMakeLists.txt │ │ │ ├── LICENSE │ │ │ ├── README │ │ │ ├── TODO │ │ │ ├── crc.h │ │ │ ├── qr.c │ │ │ ├── qr.h │ │ │ ├── qr_dwtable.h │ │ │ ├── qr_private.h │ │ │ ├── qr_util.h │ │ │ ├── qrcmd.c │ │ │ ├── qrcmd.h │ │ │ ├── qrcnv.c │ │ │ ├── qrcnv.h │ │ │ └── qrcnv_bmp.c │ │ ├── oled_ctrl │ │ │ ├── oled_ctrl.cpp │ │ │ └── oled_ctrl.h │ │ ├── oled_ui │ │ │ ├── oled_ui.cpp │ │ │ └── oled_ui.h │ │ ├── system_ctrl │ │ │ ├── system_ctrl.cpp │ │ │ └── system_ctrl.h │ │ ├── system_init │ │ │ ├── system_init.cpp │ │ │ └── system_init.h │ │ └── system_state │ │ │ ├── system_state.cpp │ │ │ └── system_state.h │ │ └── src │ │ └── main.cpp │ └── kvm_vision_test │ └── main │ ├── CMakeLists.txt │ ├── Kconfig │ └── src │ └── main.cpp └── web ├── .editorconfig ├── .env.development ├── .eslintrc.cjs ├── .prettierignore ├── .prettierrc.yaml ├── README.md ├── README_JA.md ├── README_ZH.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── mockServiceWorker.js └── sipeed.ico ├── src ├── api │ ├── application.ts │ ├── auth.ts │ ├── download.ts │ ├── extensions │ │ └── tailscale.ts │ ├── hid.ts │ ├── network.ts │ ├── script.ts │ ├── storage.ts │ ├── stream.ts │ ├── virtual-device.ts │ └── vm.ts ├── assets │ ├── images │ │ ├── monitor-x.svg │ │ └── tailscale.svg │ └── styles │ │ ├── index.css │ │ └── keyboard.css ├── components │ ├── auth.tsx │ ├── head.tsx │ ├── icons │ │ └── tailscale.tsx │ ├── main-error.tsx │ ├── menu-item.tsx │ └── root.tsx ├── i18n │ ├── README.md │ ├── index.ts │ ├── languages.ts │ └── locales │ │ ├── cz.ts │ │ ├── da.ts │ │ ├── de.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── fr.ts │ │ ├── hu.ts │ │ ├── id.ts │ │ ├── it.ts │ │ ├── ja.ts │ │ ├── ko.ts │ │ ├── nb.ts │ │ ├── nl.ts │ │ ├── pl.ts │ │ ├── ru.ts │ │ ├── th.ts │ │ ├── uk.ts │ │ ├── vi.ts │ │ ├── zh.ts │ │ └── zh_tw.ts ├── jotai │ ├── keyboard.ts │ ├── mouse.ts │ ├── screen.ts │ └── settings.ts ├── lib │ ├── cookie.ts │ ├── encrypt.ts │ ├── http.ts │ ├── localstorage.ts │ ├── service.ts │ └── websocket.ts ├── main.tsx ├── mocks │ └── browser.ts ├── pages │ ├── auth │ │ ├── login │ │ │ ├── index.tsx │ │ │ └── tips.tsx │ │ └── password │ │ │ └── index.tsx │ ├── desktop │ │ ├── index.tsx │ │ ├── keyboard │ │ │ ├── index.tsx │ │ │ ├── mappings.ts │ │ │ ├── virtual-keyboard.tsx │ │ │ └── virtual-keys.ts │ │ ├── menu │ │ │ ├── download.tsx │ │ │ ├── fullscreen │ │ │ │ └── index.tsx │ │ │ ├── image │ │ │ │ ├── images.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tips.tsx │ │ │ ├── index.tsx │ │ │ ├── keyboard │ │ │ │ ├── ctrl-alt-del.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── paste.tsx │ │ │ │ └── virtual-keyboard.tsx │ │ │ ├── mouse │ │ │ │ ├── cursor.tsx │ │ │ │ ├── hid-mode.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── mouse-mode.tsx │ │ │ │ ├── reset-hid.tsx │ │ │ │ └── speed.tsx │ │ │ ├── power │ │ │ │ ├── index.tsx │ │ │ │ ├── power-long.tsx │ │ │ │ ├── power-short.tsx │ │ │ │ └── reset.tsx │ │ │ ├── screen │ │ │ │ ├── constants.ts │ │ │ │ ├── fps.tsx │ │ │ │ ├── frame-detect.tsx │ │ │ │ ├── gop.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── quality.tsx │ │ │ │ ├── reset.tsx │ │ │ │ ├── resolution.tsx │ │ │ │ └── video-mode.tsx │ │ │ ├── script │ │ │ │ ├── index.tsx │ │ │ │ └── run.tsx │ │ │ ├── settings │ │ │ │ ├── about │ │ │ │ │ ├── community.tsx │ │ │ │ │ ├── hostname.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── information.tsx │ │ │ │ ├── account │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── logout.tsx │ │ │ │ ├── appearance │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── language.tsx │ │ │ │ │ ├── menu-bar.tsx │ │ │ │ │ └── web-title.tsx │ │ │ │ ├── device │ │ │ │ │ ├── advanced │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── swap.tsx │ │ │ │ │ ├── hdmi.tsx │ │ │ │ │ ├── hid-mode.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── mdns.tsx │ │ │ │ │ ├── mouse-jiggler.tsx │ │ │ │ │ ├── oled.tsx │ │ │ │ │ ├── reboot.tsx │ │ │ │ │ ├── ssh.tsx │ │ │ │ │ ├── tls.tsx │ │ │ │ │ ├── virtual-devices.tsx │ │ │ │ │ └── wifi.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tailscale │ │ │ │ │ ├── device.tsx │ │ │ │ │ ├── header.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── install.tsx │ │ │ │ │ ├── login.tsx │ │ │ │ │ ├── memory.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ └── uninstall.tsx │ │ │ │ └── update │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── preview.tsx │ │ │ ├── terminal │ │ │ │ ├── index.tsx │ │ │ │ ├── nanokvm.tsx │ │ │ │ └── serial-port.tsx │ │ │ └── wol │ │ │ │ └── index.tsx │ │ ├── mouse │ │ │ ├── absolute.tsx │ │ │ ├── constants.ts │ │ │ ├── index.tsx │ │ │ └── relative.tsx │ │ ├── notification.tsx │ │ └── screen │ │ │ ├── h264-direct.tsx │ │ │ ├── h264-webrtc.tsx │ │ │ ├── index.tsx │ │ │ └── mjpeg.tsx │ ├── terminal │ │ └── index.tsx │ └── wifi │ │ └── index.tsx ├── router.tsx ├── types │ └── index.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | pnpm-debug.log* 3 | 4 | web/node_modules 5 | web/dist 6 | web/web 7 | 8 | .DS_Store 9 | *.local 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.sw? 15 | 16 | .idea 17 | .vscode 18 | 19 | NanoKVM-Server 20 | 21 | support/sg2002/kvm_system/build 22 | support/sg2002/kvm_system/dist 23 | support/sg2002/kvm_system/CMakeLists.txt 24 | 25 | support/sg2002/kvm_vision_test/build 26 | support/sg2002/kvm_vision_test/dist 27 | support/sg2002/kvm_vision_test/CMakeLists.txt 28 | 29 | support/sg2002/additional/original 30 | kvmapp/server/dl_lib 31 | kvmapp/kvm_system/kvm_system 32 | -------------------------------------------------------------------------------- /kvmapp/jpg_stream/jpg_stream: -------------------------------------------------------------------------------- 1 | rm /etc/init.d/S95webkvm 2 | cp /kvmapp/jpg_stream/S95nanokvm /etc/init.d/ 3 | cp /kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so /kvmapp/kvm_system/dl_lib 4 | rm -r /kvmapp/jpg_stream 5 | /etc/init.d/S95nanokvm restart 6 | -------------------------------------------------------------------------------- /kvmapp/kvm_new_app: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/kvmapp/kvm_new_app -------------------------------------------------------------------------------- /kvmapp/kvm_system/kvm_stream: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/kvmapp/kvm_system/kvm_stream -------------------------------------------------------------------------------- /kvmapp/system/init.d/S00kmod: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "start" ] 4 | then 5 | . /etc/profile 6 | printf "load kernel module: " 7 | cd /mnt/system/ko/ 8 | insmod soph_sys.ko 9 | insmod soph_base.ko 10 | insmod soph_rtos_cmdqu.ko 11 | insmod soph_fast_image.ko 12 | insmod soph_mipi_rx.ko 13 | insmod soph_snsr_i2c.ko 14 | insmod soph_vi.ko 15 | insmod soph_vpss.ko 16 | insmod soph_dwa.ko 17 | insmod soph_vo.ko 18 | insmod soph_rgn.ko 19 | insmod soph_wdt.ko 20 | insmod soph_clock_cooling.ko 21 | insmod soph_tpu.ko 22 | insmod soph_vcodec.ko 23 | insmod soph_jpeg.ko 24 | insmod soph_vc_driver.ko MaxVencChnNum=9 MaxVdecChnNum=9 25 | insmod soph_rtc.ko 26 | insmod soph_ive.ko 27 | insmod soph_mon.ko 28 | insmod soph_pwm.ko 29 | insmod soph_wiegand.ko 30 | echo "OK" 31 | exit 0 32 | fi 33 | -------------------------------------------------------------------------------- /kvmapp/system/init.d/S01fs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "start" ] 4 | then 5 | # use all sdcard free space for data 6 | parted -s /dev/mmcblk0 "resizepart 2 -0" 7 | echo "yes 8 | 8192MB 9 | " | parted ---pretend-input-tty /dev/mmcblk0 "resizepart 2 8192MB" 10 | # resize data filesystem 11 | (resize2fs /dev/mmcblk0p2) & 12 | 13 | 14 | . /etc/profile 15 | printf "mounting filesystem : " 16 | mkdir -p /boot 17 | mount -t vfat /dev/mmcblk0p1 /boot 18 | mount -t configfs configfs /sys/kernel/config 19 | mount -t debugfs debugfs /sys/kernel/debug 20 | 21 | if [ -e /boot/usb.disk0 ] 22 | then 23 | if [ ! -e /etc/kvm.disk0 ] 24 | then 25 | touch /etc/kvm.disk0 26 | # use all sdcard free space for data 27 | parted -s /dev/mmcblk0 "mkpart primary 8193MB 100%" 28 | sleep 1 29 | # resize data filesystem 30 | (mkfs.exfat /dev/mmcblk0p3) & 31 | sleep 1 32 | fi 33 | fi 34 | 35 | if [ -e /dev/mmcblk0p3 ] 36 | then 37 | mkdir -p /data 38 | mount /dev/mmcblk0p3 /data 39 | fi 40 | 41 | echo "OK" 42 | fi 43 | -------------------------------------------------------------------------------- /kvmapp/system/init.d/S50avahi-daemon: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # avahi-daemon init script 4 | 5 | DAEMON=/usr/sbin/avahi-daemon 6 | case "$1" in 7 | start) 8 | $DAEMON -c || $DAEMON -D 9 | ;; 10 | stop) 11 | $DAEMON -c && $DAEMON -k 12 | ;; 13 | reload) 14 | $DAEMON -c && $DAEMON -r 15 | ;; 16 | *) 17 | echo "Usage: S50avahi-daemon {start|stop|reload}" >&2 18 | exit 1 19 | ;; 20 | esac 21 | -------------------------------------------------------------------------------- /kvmapp/system/init.d/S50ssdpd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DAEMON=ssdpd 4 | PIDFILE=/var/run/$DAEMON.pid 5 | CFGFILE=/etc/default/$DAEMON 6 | 7 | DAEMON_ARGS="" 8 | 9 | # Read configuration variable file if it is present 10 | # shellcheck source=/dev/null 11 | [ -r "$CFGFILE" ] && . "$CFGFILE" 12 | 13 | # shellcheck disable=SC2086 14 | start() { 15 | printf 'Starting %s: ' "$DAEMON" 16 | if start-stop-daemon -S -q -p "$PIDFILE" -x "$DAEMON" -- $DAEMON_ARGS; then 17 | echo "OK" 18 | else 19 | echo "FAIL" 20 | fi 21 | } 22 | 23 | stop() { 24 | printf 'Stopping %s: ' "$DAEMON" 25 | if start-stop-daemon -K -q -p "$PIDFILE" -x "$DAEMON"; then 26 | echo "OK" 27 | else 28 | echo "FAIL" 29 | fi 30 | } 31 | 32 | restart() { 33 | stop 34 | start 35 | } 36 | 37 | case "$1" in 38 | start|stop|restart) 39 | "$1" 40 | ;; 41 | reload) 42 | restart 43 | ;; 44 | *) 45 | echo "Usage: $0 {start|stop|restart|reload}" 46 | exit 1 47 | esac 48 | 49 | exit $? -------------------------------------------------------------------------------- /kvmapp/system/init.d/S50sshd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # sshd Starts sshd. 4 | # 5 | 6 | # Ensure required binaries exist 7 | [ -x /usr/bin/ssh-keygen ] || exit 0 8 | [ -x /usr/sbin/sshd ] || exit 1 9 | 10 | PIDFILE="/var/run/sshd.pid" 11 | 12 | umask 077 13 | 14 | startssh() { 15 | # Generate SSH keys if they do not exist 16 | [ ! -f /etc/ssh/ssh_host_rsa_key ] && /usr/bin/ssh-keygen -A 17 | 18 | printf "Starting sshd: " 19 | /usr/sbin/sshd 20 | touch /var/lock/sshd 21 | echo "OK" 22 | } 23 | 24 | start() { 25 | if [ -e /etc/kvm/ssh_stop ]; then 26 | if [ -e /boot/start_ssh_once ]; then 27 | rm -f /boot/start_ssh_once 28 | startssh 29 | else 30 | echo "SSH does not start" 31 | exit 0 32 | fi 33 | else 34 | startssh 35 | fi 36 | } 37 | 38 | stop() { 39 | printf "Stopping sshd: " 40 | killall sshd 2>/dev/null 41 | rm -f /var/lock/sshd 42 | echo "OK" 43 | } 44 | 45 | restart() { 46 | stop 47 | start 48 | } 49 | 50 | case "$1" in 51 | start) 52 | start 53 | ;; 54 | stop) 55 | stop 56 | ;; 57 | restart|reload) 58 | restart 59 | ;; 60 | permanent_on) 61 | rm -f /etc/kvm/ssh_stop 62 | start 63 | ;; 64 | permanent_off) 65 | touch /etc/kvm/ssh_stop 66 | stop 67 | ;; 68 | *) 69 | echo "Usage: $0 {start|stop|restart|permanent_on|permanent_off}" 70 | exit 1 71 | esac 72 | 73 | exit 0 -------------------------------------------------------------------------------- /kvmapp/system/init.d/S80dnsmasq: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DAEMON="dnsmasq" 4 | PIDFILE="/var/run/$DAEMON.pid" 5 | 6 | [ -f /etc/dnsmasq.conf ] || exit 0 7 | 8 | case "$1" in 9 | start) 10 | printf "Starting dnsmasq: " 11 | start-stop-daemon -S -p "$PIDFILE" -x "/usr/sbin/$DAEMON" -- \ 12 | --pid-file="$PIDFILE" 13 | [ $? = 0 ] && echo "OK" || echo "FAIL" 14 | ;; 15 | stop) 16 | printf "Stopping dnsmasq: " 17 | start-stop-daemon -K -q -p "$PIDFILE" -x "/usr/sbin/$DAEMON" 18 | [ $? = 0 ] && echo "OK" || echo "FAIL" 19 | ;; 20 | restart|reload) 21 | $0 stop 22 | $0 start 23 | ;; 24 | *) 25 | echo "Usage: $0 {start|stop|restart}" 26 | exit 1 27 | esac 28 | 29 | exit 0 30 | -------------------------------------------------------------------------------- /kvmapp/system/ko/soph_mipi_rx.ko: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/kvmapp/system/ko/soph_mipi_rx.ko -------------------------------------------------------------------------------- /server/common/screen.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "sync" 4 | 5 | type Screen struct { 6 | Width uint16 7 | Height uint16 8 | FPS int 9 | Quality uint16 10 | BitRate uint16 11 | GOP uint8 12 | } 13 | 14 | var ( 15 | screen *Screen 16 | screenOnce sync.Once 17 | ) 18 | 19 | // ResolutionMap height to width 20 | var ResolutionMap = map[uint16]uint16{ 21 | 1080: 1920, 22 | 720: 1280, 23 | 600: 800, 24 | 480: 640, 25 | 0: 0, 26 | } 27 | 28 | var QualityMap = map[uint16]bool{ 29 | 100: true, 30 | 80: true, 31 | 60: true, 32 | 50: true, 33 | } 34 | 35 | var BitRateMap = map[uint16]bool{ 36 | 5000: true, 37 | 3000: true, 38 | 2000: true, 39 | 1000: true, 40 | } 41 | 42 | func GetScreen() *Screen { 43 | screenOnce.Do(func() { 44 | screen = &Screen{ 45 | Width: 0, 46 | Height: 0, 47 | Quality: 80, 48 | FPS: 30, 49 | BitRate: 3000, 50 | GOP: 30, 51 | } 52 | }) 53 | 54 | return screen 55 | } 56 | 57 | func SetScreen(key string, value int) { 58 | switch key { 59 | case "resolution": 60 | height := uint16(value) 61 | if width, ok := ResolutionMap[height]; ok { 62 | screen.Width = width 63 | screen.Height = height 64 | } 65 | 66 | case "quality": 67 | if value > 100 { 68 | screen.BitRate = uint16(value) 69 | } else { 70 | screen.Quality = uint16(value) 71 | } 72 | 73 | case "fps": 74 | screen.FPS = validateFPS(value) 75 | 76 | case "gop": 77 | screen.GOP = uint8(value) 78 | } 79 | } 80 | 81 | func CheckScreen() { 82 | if _, ok := ResolutionMap[screen.Height]; !ok { 83 | screen.Width = 1920 84 | screen.Height = 1080 85 | } 86 | 87 | if _, ok := QualityMap[screen.Quality]; !ok { 88 | screen.Quality = 80 89 | } 90 | 91 | if _, ok := BitRateMap[screen.BitRate]; !ok { 92 | screen.BitRate = 3000 93 | } 94 | } 95 | 96 | func validateFPS(fps int) int { 97 | if fps > 60 { 98 | return 60 99 | } 100 | if fps < 10 { 101 | return 10 102 | } 103 | 104 | return fps 105 | } 106 | -------------------------------------------------------------------------------- /server/config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var defaultConfig = &Config{ 4 | Proto: "http", 5 | Port: Port{ 6 | Http: 80, 7 | Https: 443, 8 | }, 9 | Cert: Cert{ 10 | Crt: "server.crt", 11 | Key: "server.key", 12 | }, 13 | Logger: Logger{ 14 | Level: "info", 15 | File: "stdout", 16 | }, 17 | JWT: JWT{ 18 | SecretKey: "", 19 | RefreshTokenDuration: 2678400, 20 | RevokeTokensOnLogout: true, 21 | }, 22 | Stun: "stun.l.google.com:19302", 23 | Turn: Turn{ 24 | TurnAddr: "", 25 | TurnUser: "", 26 | TurnCred: "", 27 | }, 28 | Authentication: "enable", 29 | } 30 | 31 | func checkDefaultValue() { 32 | if instance.JWT.SecretKey == "" { 33 | instance.JWT.SecretKey = generateRandomSecretKey() 34 | instance.JWT.RevokeTokensOnLogout = true 35 | } 36 | 37 | if instance.JWT.RefreshTokenDuration == 0 { 38 | instance.JWT.RefreshTokenDuration = 2678400 39 | } 40 | 41 | if instance.Stun == "" { 42 | instance.Stun = "stun.l.google.com:19302" 43 | } 44 | 45 | if instance.Authentication == "" { 46 | instance.Authentication = "enable" 47 | } 48 | 49 | instance.Hardware = getHardware() 50 | } 51 | -------------------------------------------------------------------------------- /server/config/file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | const ConfigurationFile = "/etc/kvm/server.yaml" 11 | 12 | func Read() (*Config, error) { 13 | data, err := os.ReadFile(ConfigurationFile) 14 | if err != nil { 15 | log.Errorf("failed to read config: %v", err) 16 | return nil, err 17 | } 18 | 19 | var conf Config 20 | 21 | if err := yaml.Unmarshal(data, &conf); err != nil { 22 | log.Fatalf("failed to unmarshal config: %v", err) 23 | return nil, err 24 | } 25 | 26 | log.Debugf("read %s successfully", ConfigurationFile) 27 | return &conf, nil 28 | } 29 | 30 | func Write(conf *Config) error { 31 | data, err := yaml.Marshal(&conf) 32 | if err != nil { 33 | log.Errorf("failed to marshal config: %v", err) 34 | return err 35 | } 36 | 37 | err = os.WriteFile(ConfigurationFile, data, 0644) 38 | if err != nil { 39 | log.Errorf("failed to write config: %v", err) 40 | return err 41 | } 42 | 43 | log.Debugf("write to %s successfully", ConfigurationFile) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /server/config/hardware.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type HWVersion int 11 | 12 | const ( 13 | HWVersionAlpha HWVersion = iota 14 | HWVersionBeta 15 | HWVersionPcie 16 | 17 | HWVersionFile = "/etc/kvm/hw" 18 | ) 19 | 20 | var HWAlpha = Hardware{ 21 | Version: HWVersionAlpha, 22 | GPIOReset: "/sys/class/gpio/gpio507/value", 23 | GPIOPower: "/sys/class/gpio/gpio503/value", 24 | GPIOPowerLED: "/sys/class/gpio/gpio504/value", 25 | GPIOHDDLed: "/sys/class/gpio/gpio505/value", 26 | } 27 | 28 | var HWBeta = Hardware{ 29 | Version: HWVersionBeta, 30 | GPIOReset: "/sys/class/gpio/gpio505/value", 31 | GPIOPower: "/sys/class/gpio/gpio503/value", 32 | GPIOPowerLED: "/sys/class/gpio/gpio504/value", 33 | GPIOHDDLed: "", 34 | } 35 | 36 | var HWPcie = Hardware{ 37 | Version: HWVersionPcie, 38 | GPIOReset: "/sys/class/gpio/gpio505/value", 39 | GPIOPower: "/sys/class/gpio/gpio503/value", 40 | GPIOPowerLED: "/sys/class/gpio/gpio504/value", 41 | GPIOHDDLed: "", 42 | } 43 | 44 | func (h HWVersion) String() string { 45 | switch h { 46 | case HWVersionAlpha: 47 | return "Alpha" 48 | case HWVersionBeta: 49 | return "Beta" 50 | case HWVersionPcie: 51 | return "PCIE" 52 | default: 53 | return "Unknown" 54 | } 55 | } 56 | 57 | func getHwVersion() HWVersion { 58 | content, err := os.ReadFile(HWVersionFile) 59 | if err != nil { 60 | return HWVersionAlpha 61 | } 62 | 63 | version := strings.ReplaceAll(string(content), "\n", "") 64 | if version == "beta" { 65 | return HWVersionBeta 66 | } else if version == "pcie" { 67 | return HWVersionPcie 68 | } 69 | 70 | return HWVersionAlpha 71 | } 72 | 73 | func getHardware() (h Hardware) { 74 | version := getHwVersion() 75 | 76 | switch version { 77 | case HWVersionAlpha: 78 | h = HWAlpha 79 | 80 | case HWVersionBeta: 81 | h = HWBeta 82 | 83 | case HWVersionPcie: 84 | h = HWPcie 85 | 86 | default: 87 | h = HWAlpha 88 | log.Errorf("Unsupported hardware version: %s", version) 89 | } 90 | 91 | return 92 | } 93 | -------------------------------------------------------------------------------- /server/config/jwt.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // RegenerateSecretKey regenerate secret key when logout 11 | func RegenerateSecretKey() { 12 | if instance.JWT.RevokeTokensOnLogout { 13 | instance.JWT.SecretKey = generateRandomSecretKey() 14 | } 15 | } 16 | 17 | // Generate random string for secret key. 18 | func generateRandomSecretKey() string { 19 | b := make([]byte, 64) 20 | _, err := rand.Read(b) 21 | if err != nil { 22 | currentTime := time.Now().UnixNano() 23 | timeString := fmt.Sprintf("%d", currentTime) 24 | return fmt.Sprintf("%064s", timeString) 25 | } 26 | 27 | return base64.URLEncoding.EncodeToString(b) 28 | } 29 | -------------------------------------------------------------------------------- /server/config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Proto string `yaml:"proto"` 5 | Port Port `yaml:"port"` 6 | Cert Cert `yaml:"cert"` 7 | Logger Logger `yaml:"logger"` 8 | Authentication string `yaml:"authentication"` 9 | JWT JWT `yaml:"jwt"` 10 | Stun string `yaml:"stun"` 11 | Turn Turn `yaml:"turn"` 12 | 13 | Hardware Hardware `yaml:"-"` 14 | } 15 | 16 | type Logger struct { 17 | Level string `yaml:"level"` 18 | File string `yaml:"file"` 19 | } 20 | 21 | type Port struct { 22 | Http int `yaml:"http"` 23 | Https int `yaml:"https"` 24 | } 25 | 26 | type Cert struct { 27 | Crt string `yaml:"crt"` 28 | Key string `yaml:"key"` 29 | } 30 | 31 | type JWT struct { 32 | SecretKey string `yaml:"secretKey"` 33 | RefreshTokenDuration uint64 `yaml:"refreshTokenDuration"` 34 | RevokeTokensOnLogout bool `yaml:"revokeTokensOnLogout"` 35 | } 36 | 37 | type Turn struct { 38 | TurnAddr string `yaml:"turnAddr"` 39 | TurnUser string `yaml:"turnUser"` 40 | TurnCred string `yaml:"turnCred"` 41 | } 42 | 43 | type Hardware struct { 44 | Version HWVersion `yaml:"-"` 45 | GPIOReset string `yaml:"-"` 46 | GPIOPower string `yaml:"-"` 47 | GPIOPowerLED string `yaml:"-"` 48 | GPIOHDDLed string `yaml:"-"` 49 | } 50 | -------------------------------------------------------------------------------- /server/dl_lib/libaaccomm2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaaccomm2.so -------------------------------------------------------------------------------- /server/dl_lib/libaacdec2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaacdec2.so -------------------------------------------------------------------------------- /server/dl_lib/libaacenc2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaacenc2.so -------------------------------------------------------------------------------- /server/dl_lib/libaacsbrdec2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaacsbrdec2.so -------------------------------------------------------------------------------- /server/dl_lib/libaacsbrenc2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaacsbrenc2.so -------------------------------------------------------------------------------- /server/dl_lib/libae.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libae.so -------------------------------------------------------------------------------- /server/dl_lib/libaf.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libaf.so -------------------------------------------------------------------------------- /server/dl_lib/libawb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libawb.so -------------------------------------------------------------------------------- /server/dl_lib/libcli.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcli.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_RES1.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_RES1.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_VoiceEngine.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_VoiceEngine.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_audio.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_audio.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_bin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_bin.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_bin_isp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_bin_isp.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_ispd2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_ispd2.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_ive.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_ive.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_ssp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_ssp.so -------------------------------------------------------------------------------- /server/dl_lib/libcvi_vqe.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libcvi_vqe.so -------------------------------------------------------------------------------- /server/dl_lib/libdnvqe.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libdnvqe.so -------------------------------------------------------------------------------- /server/dl_lib/libini.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libini.so -------------------------------------------------------------------------------- /server/dl_lib/libisp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libisp.so -------------------------------------------------------------------------------- /server/dl_lib/libisp_algo.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libisp_algo.so -------------------------------------------------------------------------------- /server/dl_lib/libjson-c.so.5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libjson-c.so.5 -------------------------------------------------------------------------------- /server/dl_lib/libkvm.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libkvm.so -------------------------------------------------------------------------------- /server/dl_lib/libkvm_mmf.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libkvm_mmf.so -------------------------------------------------------------------------------- /server/dl_lib/libmipi_tx.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libmipi_tx.so -------------------------------------------------------------------------------- /server/dl_lib/libmisc.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libmisc.so -------------------------------------------------------------------------------- /server/dl_lib/libopencv_core.so.409: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libopencv_core.so.409 -------------------------------------------------------------------------------- /server/dl_lib/libopencv_highgui.so.409: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libopencv_highgui.so.409 -------------------------------------------------------------------------------- /server/dl_lib/libopencv_imgcodecs.so.409: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libopencv_imgcodecs.so.409 -------------------------------------------------------------------------------- /server/dl_lib/libopencv_imgproc.so.409: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libopencv_imgproc.so.409 -------------------------------------------------------------------------------- /server/dl_lib/libosdc.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libosdc.so -------------------------------------------------------------------------------- /server/dl_lib/libraw_dump.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libraw_dump.so -------------------------------------------------------------------------------- /server/dl_lib/libsys.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libsys.so -------------------------------------------------------------------------------- /server/dl_lib/libtinyalsa.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libtinyalsa.so -------------------------------------------------------------------------------- /server/dl_lib/libvdec.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libvdec.so -------------------------------------------------------------------------------- /server/dl_lib/libvenc.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libvenc.so -------------------------------------------------------------------------------- /server/dl_lib/libvpu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/server/dl_lib/libvpu.so -------------------------------------------------------------------------------- /server/logger/formatter.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type formatter struct{} 12 | 13 | func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { 14 | var ( 15 | text string 16 | buffer *bytes.Buffer 17 | ) 18 | 19 | if entry.Buffer != nil { 20 | buffer = entry.Buffer 21 | } else { 22 | buffer = &bytes.Buffer{} 23 | } 24 | 25 | now := entry.Time.Format("2006-01-02 15:04:05.000") 26 | 27 | if entry.HasCaller() { 28 | fileName := filepath.Base(entry.Caller.File) 29 | text = fmt.Sprintf( 30 | "[%s] [%s] [%s:%d] %s\n", 31 | now, entry.Level, fileName, entry.Caller.Line, entry.Message, 32 | ) 33 | } else { 34 | text = fmt.Sprintf( 35 | "[%s] [%s] %s \n", 36 | now, entry.Level, entry.Message, 37 | ) 38 | } 39 | 40 | buffer.WriteString(text) 41 | return buffer.Bytes(), nil 42 | } 43 | -------------------------------------------------------------------------------- /server/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "NanoKVM-Server/config" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func openLogFile(filename string) (*os.File, error) { 13 | absPath, err := filepath.Abs(filename) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | file, err := os.OpenFile(absPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return file, nil 24 | } 25 | 26 | func Init() { 27 | conf := config.GetInstance() 28 | 29 | level, err := logrus.ParseLevel(conf.Logger.Level) 30 | if err != nil { 31 | level = logrus.ErrorLevel 32 | } 33 | 34 | logrus.SetLevel(level) 35 | if conf.Logger.File == "" || conf.Logger.File == "stdout" { 36 | logrus.SetOutput(os.Stdout) 37 | } else { 38 | fh, err := openLogFile(conf.Logger.File) 39 | if err != nil { 40 | logrus.Error("open log file failed:", err) 41 | logrus.SetOutput(os.Stdout) 42 | } else { 43 | logrus.SetOutput(fh) 44 | } 45 | } 46 | 47 | logrus.SetReportCaller(true) 48 | logrus.SetFormatter(&formatter{}) 49 | 50 | logrus.Info("logger set success") 51 | } 52 | -------------------------------------------------------------------------------- /server/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/golang-jwt/jwt/v5" 9 | log "github.com/sirupsen/logrus" 10 | 11 | "NanoKVM-Server/config" 12 | ) 13 | 14 | type Token struct { 15 | Username string `json:"username"` 16 | jwt.RegisteredClaims 17 | } 18 | 19 | func CheckToken() gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | conf := config.GetInstance() 22 | 23 | if conf.Authentication == "disable" { 24 | c.Next() 25 | return 26 | } 27 | 28 | cookie, err := c.Cookie("nano-kvm-token") 29 | if err == nil { 30 | _, err = ParseJWT(cookie) 31 | if err == nil { 32 | c.Next() 33 | return 34 | } 35 | } 36 | 37 | c.JSON(http.StatusUnauthorized, "unauthorized") 38 | c.Abort() 39 | } 40 | } 41 | 42 | func GenerateJWT(username string) (string, error) { 43 | conf := config.GetInstance() 44 | 45 | expireDuration := time.Duration(conf.JWT.RefreshTokenDuration) * time.Second 46 | 47 | claims := Token{ 48 | Username: username, 49 | RegisteredClaims: jwt.RegisteredClaims{ 50 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), 51 | }, 52 | } 53 | 54 | t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 55 | 56 | return t.SignedString([]byte(conf.JWT.SecretKey)) 57 | } 58 | 59 | func ParseJWT(jwtToken string) (*Token, error) { 60 | conf := config.GetInstance() 61 | 62 | t, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) { 63 | return []byte(conf.JWT.SecretKey), nil 64 | }) 65 | if err != nil { 66 | log.Debugf("parse jwt error: %s", err) 67 | return nil, err 68 | } 69 | 70 | if claims, ok := t.Claims.(*Token); ok && t.Valid { 71 | return claims, nil 72 | } else { 73 | return nil, err 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/middleware/tls.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/unrolled/secure" 6 | ) 7 | 8 | func Tls() gin.HandlerFunc { 9 | secureMiddleware := secure.New(secure.Options{ 10 | SSLRedirect: true, 11 | }) 12 | 13 | secureFunc := func(c *gin.Context) { 14 | err := secureMiddleware.Process(c.Writer, c.Request) 15 | if err != nil { 16 | c.Abort() 17 | return 18 | } 19 | 20 | if status := c.Writer.Status(); status > 300 && status < 399 { 21 | c.Abort() 22 | } 23 | } 24 | 25 | return secureFunc 26 | } 27 | -------------------------------------------------------------------------------- /server/proto/application.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type GetVersionRsp struct { 4 | Current string `json:"current"` 5 | Latest string `json:"latest"` 6 | } 7 | 8 | type GetPreviewRsp struct { 9 | Enabled bool `json:"enabled"` 10 | } 11 | 12 | type SetPreviewReq struct { 13 | Enable bool `validate:"omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /server/proto/auth.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type LoginReq struct { 4 | Username string `validate:"required"` 5 | Password string `validate:"required"` 6 | } 7 | 8 | type LoginRsp struct { 9 | Token string `json:"token"` 10 | } 11 | 12 | type GetAccountRsp struct { 13 | Username string `json:"username"` 14 | } 15 | 16 | type ChangePasswordReq struct { 17 | Username string `json:"username" validate:"required"` 18 | Password string `json:"password" validate:"required"` 19 | } 20 | 21 | type IsPasswordUpdatedRsp struct { 22 | IsUpdated bool `json:"isUpdated"` 23 | } 24 | 25 | type ConnectWifiReq struct { 26 | Ssid string `validate:"required"` 27 | Password string `valid:"required"` 28 | } 29 | -------------------------------------------------------------------------------- /server/proto/download.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type ImageEnabledRsp struct { 4 | Enabled bool `json:"enabled"` 5 | } 6 | 7 | type StatusImageRsp struct { 8 | Status string `json:"status"` 9 | File string `json:"file"` 10 | Percentage string `json:"percentage"` 11 | } 12 | -------------------------------------------------------------------------------- /server/proto/hid.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type GetHidModeRsp struct { 4 | Mode string `json:"mode"` // normal or hid-only 5 | } 6 | 7 | type SetHidModeReq struct { 8 | Mode string `validate:"required"` // normal or hid-only 9 | } 10 | -------------------------------------------------------------------------------- /server/proto/network.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type WakeOnLANReq struct { 4 | Mac string `form:"mac" validate:"required"` 5 | } 6 | 7 | type GetMacRsp struct { 8 | Macs []string `json:"macs"` 9 | } 10 | 11 | type DeleteMacReq struct { 12 | Mac string `form:"mac" validate:"required"` 13 | } 14 | 15 | type SetMacNameReq struct { 16 | Mac string `form:"mac" validate:"required"` 17 | Name string `form:"name" validate:"required"` 18 | } 19 | 20 | type TailscaleState string 21 | 22 | const ( 23 | TailscaleNotInstall TailscaleState = "notInstall" 24 | TailscaleNotRunning TailscaleState = "notRunning" 25 | TailscaleNotLogin TailscaleState = "notLogin" 26 | TailscaleStopped TailscaleState = "stopped" 27 | TailscaleRunning TailscaleState = "running" 28 | ) 29 | 30 | type GetTailscaleStatusRsp struct { 31 | State TailscaleState `json:"state"` 32 | Name string `json:"name"` 33 | IP string `json:"ip"` 34 | Account string `json:"account"` 35 | } 36 | 37 | type LoginTailscaleRsp struct { 38 | Url string `json:"url"` 39 | } 40 | 41 | type GetWifiRsp struct { 42 | Supported bool `json:"supported"` 43 | Connected bool `json:"connected"` 44 | } 45 | -------------------------------------------------------------------------------- /server/proto/request.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-playground/validator/v10" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var env = os.Getenv(gin.EnvGinMode) 12 | 13 | // ValidateRequest Validates request parameters. 14 | func ValidateRequest(req interface{}) error { 15 | validate := validator.New() 16 | 17 | if err := validate.Struct(req); err != nil { 18 | log.Errorf("validate request failed, err: %s", err) 19 | return err 20 | } 21 | 22 | if env == "" || env == "debug" { 23 | log.Debugf("request: %+v\n", req) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // ParseQueryRequest Validates GET requests. 30 | func ParseQueryRequest(c *gin.Context, req interface{}) error { 31 | var err error 32 | if err = c.ShouldBindQuery(req); err != nil { 33 | log.Errorf("parse request failed, err: %s", err) 34 | return err 35 | } 36 | 37 | return ValidateRequest(req) 38 | } 39 | 40 | // ParseFormRequest Validates POST Requests. 41 | func ParseFormRequest(c *gin.Context, req interface{}) error { 42 | var err error 43 | if err = c.ShouldBind(req); err != nil { 44 | log.Errorf("parse request failed, err: %s", err) 45 | return err 46 | } 47 | 48 | return ValidateRequest(req) 49 | } 50 | -------------------------------------------------------------------------------- /server/proto/response.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type Response struct { 10 | Code int `json:"code"` // Status code. 0-success, others-failure 11 | Msg string `json:"msg"` // Status details 12 | Data interface{} `json:"data"` // Returned data 13 | } 14 | 15 | func (r *Response) Ok() { 16 | r.Code = 0 17 | r.Msg = "success" 18 | } 19 | 20 | func (r *Response) OkWithData(data interface{}) { 21 | r.Ok() 22 | r.Data = data 23 | } 24 | 25 | func (r *Response) Err(code int, msg string) { 26 | r.Code = code 27 | r.Msg = msg 28 | } 29 | 30 | // OkRsp Successful response without data. 31 | func (r *Response) OkRsp(c *gin.Context) { 32 | r.Ok() 33 | 34 | c.JSON(http.StatusOK, r) 35 | } 36 | 37 | // OkRspWithData Successful response with data. 38 | func (r *Response) OkRspWithData(c *gin.Context, data interface{}) { 39 | r.Ok() 40 | r.Data = data 41 | 42 | c.JSON(http.StatusOK, r) 43 | } 44 | 45 | // ErrRsp Failed response. 46 | func (r *Response) ErrRsp(c *gin.Context, code int, msg string) { 47 | r.Err(code, msg) 48 | 49 | c.JSON(http.StatusOK, r) 50 | } 51 | -------------------------------------------------------------------------------- /server/proto/storage.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type GetImagesRsp struct { 4 | Files []string `json:"files"` 5 | } 6 | 7 | type MountImageReq struct { 8 | File string `json:"file" validate:"omitempty"` 9 | Cdrom bool `json:"cdrom" validate:"omitempty"` 10 | } 11 | 12 | type GetMountedImageRsp struct { 13 | File string `json:"file"` 14 | } 15 | 16 | type GetCdRomRsp struct { 17 | Cdrom int64 `json:"cdrom"` 18 | } 19 | -------------------------------------------------------------------------------- /server/proto/stream.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type UpdateFrameDetectReq struct { 4 | Enabled bool `validate:"omitempty"` 5 | } 6 | 7 | type StopFrameDetectReq struct { 8 | Duration int `validate:"omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /server/router/application.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "NanoKVM-Server/middleware" 5 | "NanoKVM-Server/service/application" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func applicationRouter(r *gin.Engine) { 11 | service := application.NewService() 12 | api := r.Group("/api").Use(middleware.CheckToken()) 13 | 14 | api.GET("/application/version", service.GetVersion) // get application version 15 | api.POST("/application/update", service.Update) // update application 16 | 17 | api.GET("/application/preview", service.GetPreview) // get preview updates state 18 | api.POST("/application/preview", service.SetPreview) // set preview updates state 19 | } 20 | -------------------------------------------------------------------------------- /server/router/auth.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "NanoKVM-Server/middleware" 7 | "NanoKVM-Server/service/auth" 8 | ) 9 | 10 | func authRouter(r *gin.Engine) { 11 | service := auth.NewService() 12 | 13 | r.POST("/api/auth/login", service.Login) // login 14 | 15 | api := r.Group("/api").Use(middleware.CheckToken()) 16 | 17 | api.GET("/auth/password", service.IsPasswordUpdated) // is password updated 18 | api.GET("/auth/account", service.GetAccount) // get account 19 | api.POST("/auth/password", service.ChangePassword) // change password 20 | api.POST("/auth/logout", service.Logout) // logout 21 | } 22 | -------------------------------------------------------------------------------- /server/router/download.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "NanoKVM-Server/service/download" 5 | "github.com/gin-gonic/gin" 6 | 7 | "NanoKVM-Server/middleware" 8 | ) 9 | 10 | func downloadRouter(r *gin.Engine) { 11 | service := download.NewService() 12 | api := r.Group("/api").Use(middleware.CheckToken()) 13 | 14 | api.POST("/download/image", service.DownloadImage) // download image 15 | api.GET("/download/image/status", service.StatusImage) // download image 16 | api.GET("/download/image/enabled", service.ImageEnabled) // download image 17 | } 18 | -------------------------------------------------------------------------------- /server/router/extensions.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "NanoKVM-Server/middleware" 5 | "NanoKVM-Server/service/extensions/tailscale" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func extensionsRouter(r *gin.Engine) { 11 | api := r.Group("/api/extensions").Use(middleware.CheckToken()) 12 | 13 | ts := tailscale.NewService() 14 | 15 | api.POST("/tailscale/install", ts.Install) // install tailscale 16 | api.POST("/tailscale/uninstall", ts.Uninstall) // uninstall tailscale 17 | api.GET("/tailscale/status", ts.GetStatus) // get tailscale status 18 | api.POST("/tailscale/up", ts.Up) // run tailscale up 19 | api.POST("/tailscale/down", ts.Down) // run tailscale down 20 | api.POST("/tailscale/login", ts.Login) // tailscale login 21 | api.POST("/tailscale/logout", ts.Logout) // tailscale logout 22 | api.POST("/tailscale/start", ts.Start) // tailscale start 23 | api.POST("/tailscale/stop", ts.Stop) // tailscale stop 24 | api.POST("/tailscale/restart", ts.Restart) // tailscale restart 25 | } 26 | -------------------------------------------------------------------------------- /server/router/hid.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "NanoKVM-Server/middleware" 7 | "NanoKVM-Server/service/hid" 8 | ) 9 | 10 | func hidRouter(r *gin.Engine) { 11 | service := hid.NewService() 12 | api := r.Group("/api").Use(middleware.CheckToken()) 13 | 14 | api.POST("/hid/reset", service.Reset) // reset hid 15 | api.POST("/hid/paste", service.Paste) // paste 16 | 17 | api.GET("/hid/mode", service.GetHidMode) // get hid mode 18 | api.POST("/hid/mode", service.SetHidMode) // set hid mode 19 | } 20 | -------------------------------------------------------------------------------- /server/router/network.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "NanoKVM-Server/middleware" 7 | "NanoKVM-Server/service/network" 8 | ) 9 | 10 | func networkRouter(r *gin.Engine) { 11 | service := network.NewService() 12 | 13 | r.POST("/api/network/wifi", service.ConnectWifi) // connect Wi-Fi 14 | 15 | api := r.Group("/api").Use(middleware.CheckToken()) 16 | 17 | api.POST("/network/wol", service.WakeOnLAN) // wake on lan 18 | api.GET("/network/wol/mac", service.GetMac) // get mac list 19 | api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac 20 | api.POST("/network/wol/mac/name", service.SetMacName) // set mac name 21 | api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information 22 | } 23 | -------------------------------------------------------------------------------- /server/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/gin-gonic/contrib/static" 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func Init(r *gin.Engine) { 14 | web(r) 15 | server(r) 16 | log.Debugf("router init done") 17 | } 18 | 19 | func web(r *gin.Engine) { 20 | execPath, err := os.Executable() 21 | if err != nil { 22 | panic("invalid executable path") 23 | } 24 | 25 | execDir := filepath.Dir(execPath) 26 | webPath := fmt.Sprintf("%s/web", execDir) 27 | 28 | r.Use(static.Serve("/", static.LocalFile(webPath, true))) 29 | } 30 | 31 | func server(r *gin.Engine) { 32 | authRouter(r) 33 | applicationRouter(r) 34 | vmRouter(r) 35 | streamRouter(r) 36 | storageRouter(r) 37 | networkRouter(r) 38 | hidRouter(r) 39 | wsRouter(r) 40 | downloadRouter(r) 41 | extensionsRouter(r) 42 | } 43 | -------------------------------------------------------------------------------- /server/router/storage.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "NanoKVM-Server/middleware" 7 | "NanoKVM-Server/service/storage" 8 | ) 9 | 10 | func storageRouter(r *gin.Engine) { 11 | service := storage.NewService() 12 | api := r.Group("/api").Use(middleware.CheckToken()) 13 | 14 | api.GET("/storage/image", service.GetImages) // get image list 15 | api.GET("/storage/image/mounted", service.GetMountedImage) // get mounted image 16 | api.POST("/storage/image/mount", service.MountImage) // mount image 17 | api.GET("/storage/cdrom", service.GetCdRom) // get CD-ROM flag 18 | } 19 | -------------------------------------------------------------------------------- /server/router/stream.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "NanoKVM-Server/middleware" 5 | "NanoKVM-Server/service/stream/direct" 6 | "NanoKVM-Server/service/stream/h264" 7 | "NanoKVM-Server/service/stream/mjpeg" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func streamRouter(r *gin.Engine) { 13 | api := r.Group("/api").Use(middleware.CheckToken()) 14 | 15 | api.GET("/stream/mjpeg", mjpeg.Connect) // mjpeg stream 16 | api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect 17 | api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect 18 | 19 | api.GET("/stream/h264", h264.Connect) // h264 stream (webrtc) 20 | api.GET("/stream/h264/direct", direct.Connect) // h264 stream (http) 21 | } 22 | -------------------------------------------------------------------------------- /server/router/ws.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "NanoKVM-Server/middleware" 7 | "NanoKVM-Server/service/ws" 8 | ) 9 | 10 | func wsRouter(r *gin.Engine) { 11 | service := ws.NewService() 12 | api := r.Group("/api").Use(middleware.CheckToken()) 13 | 14 | api.GET("/ws", service.Connect) 15 | } 16 | -------------------------------------------------------------------------------- /server/service/application/preview.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "os" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const ( 12 | PreviewUpdatesFlag = "/etc/kvm/preview_updates" 13 | ) 14 | 15 | func (s *Service) GetPreview(c *gin.Context) { 16 | var rsp proto.Response 17 | 18 | isEnabled := isPreviewEnabled() 19 | 20 | rsp.OkRspWithData(c, &proto.GetPreviewRsp{ 21 | Enabled: isEnabled, 22 | }) 23 | } 24 | 25 | func (s *Service) SetPreview(c *gin.Context) { 26 | var req proto.SetPreviewReq 27 | var rsp proto.Response 28 | 29 | if err := proto.ParseFormRequest(c, &req); err != nil { 30 | rsp.ErrRsp(c, -1, "invalid arguments") 31 | return 32 | } 33 | 34 | if req.Enable == isPreviewEnabled() { 35 | rsp.OkRsp(c) 36 | return 37 | } 38 | 39 | if req.Enable { 40 | if err := os.WriteFile(PreviewUpdatesFlag, []byte("1"), 0o644); err != nil { 41 | log.Errorf("failed to write %s: %s", PreviewUpdatesFlag, err) 42 | rsp.ErrRsp(c, -2, "enable failed") 43 | return 44 | } 45 | } else { 46 | if err := os.Remove(PreviewUpdatesFlag); err != nil { 47 | log.Errorf("failed to remove %s: %s", PreviewUpdatesFlag, err) 48 | rsp.ErrRsp(c, -3, "disable failed") 49 | return 50 | } 51 | } 52 | 53 | rsp.OkRsp(c) 54 | log.Debugf("set preview updates state: %t", req.Enable) 55 | } 56 | 57 | func isPreviewEnabled() bool { 58 | _, err := os.Stat(PreviewUpdatesFlag) 59 | return err == nil 60 | } 61 | -------------------------------------------------------------------------------- /server/service/application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | const ( 4 | StableURL = "https://cdn.sipeed.com/nanokvm" 5 | PreviewURL = "https://cdn.sipeed.com/nanokvm/preview" 6 | 7 | AppDir = "/kvmapp" 8 | BackupDir = "/root/old" 9 | CacheDir = "/root/.kvmcache" 10 | ) 11 | 12 | type Service struct{} 13 | 14 | func NewService() *Service { 15 | return &Service{} 16 | } 17 | -------------------------------------------------------------------------------- /server/service/auth/login.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "NanoKVM-Server/config" 5 | "NanoKVM-Server/middleware" 6 | "NanoKVM-Server/proto" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func (s *Service) Login(c *gin.Context) { 13 | var req proto.LoginReq 14 | var rsp proto.Response 15 | 16 | // authentication disabled 17 | conf := config.GetInstance() 18 | if conf.Authentication == "disable" { 19 | rsp.OkRspWithData(c, &proto.LoginRsp{ 20 | Token: "disabled", 21 | }) 22 | return 23 | } 24 | 25 | if err := proto.ParseFormRequest(c, &req); err != nil { 26 | rsp.ErrRsp(c, -1, "invalid parameters") 27 | return 28 | } 29 | 30 | if ok := CompareAccount(req.Username, req.Password); !ok { 31 | rsp.ErrRsp(c, -2, "invalid username or password") 32 | return 33 | } 34 | 35 | token, err := middleware.GenerateJWT(req.Username) 36 | if err != nil { 37 | rsp.ErrRsp(c, -3, "generate token failed") 38 | return 39 | } 40 | 41 | rsp.OkRspWithData(c, &proto.LoginRsp{ 42 | Token: token, 43 | }) 44 | 45 | log.Debugf("login success, username: %s", req.Username) 46 | } 47 | 48 | func (s *Service) Logout(c *gin.Context) { 49 | conf := config.GetInstance() 50 | 51 | if conf.JWT.RevokeTokensOnLogout { 52 | config.RegenerateSecretKey() 53 | } 54 | 55 | var rsp proto.Response 56 | rsp.OkRsp(c) 57 | } 58 | 59 | func (s *Service) GetAccount(c *gin.Context) { 60 | var rsp proto.Response 61 | 62 | account, err := GetAccount() 63 | if err != nil { 64 | rsp.ErrRsp(c, -1, "get account failed") 65 | return 66 | } 67 | 68 | rsp.OkRspWithData(c, &proto.GetAccountRsp{ 69 | Username: account.Username, 70 | }) 71 | log.Debugf("get account successful") 72 | } 73 | -------------------------------------------------------------------------------- /server/service/auth/service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type Service struct{} 4 | 5 | func NewService() *Service { 6 | return &Service{} 7 | } 8 | -------------------------------------------------------------------------------- /server/service/hid/hid.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | hid *Hid 10 | hidOnce sync.Once 11 | ) 12 | 13 | type Hid struct { 14 | g0 *os.File 15 | g1 *os.File 16 | g2 *os.File 17 | kbMutex sync.Mutex 18 | mouseMutex sync.Mutex 19 | } 20 | 21 | func (h *Hid) Lock() { 22 | h.kbMutex.Lock() 23 | h.mouseMutex.Lock() 24 | } 25 | 26 | func (h *Hid) Unlock() { 27 | h.kbMutex.Unlock() 28 | h.mouseMutex.Unlock() 29 | } 30 | 31 | func GetHid() *Hid { 32 | hidOnce.Do(func() { 33 | hid = &Hid{} 34 | }) 35 | return hid 36 | } 37 | -------------------------------------------------------------------------------- /server/service/hid/keyboard.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | func (h *Hid) Keyboard(queue <-chan []int) { 4 | for event := range queue { 5 | h.kbMutex.Lock() 6 | h.writeKeyboard(event) 7 | h.kbMutex.Unlock() 8 | } 9 | } 10 | 11 | func (h *Hid) writeKeyboard(event []int) { 12 | code := byte(event[0]) 13 | 14 | var modifier byte = 0x00 15 | if code > 0 { 16 | modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4]) 17 | } 18 | 19 | data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00} 20 | h.Write(h.g0, data) 21 | } 22 | -------------------------------------------------------------------------------- /server/service/hid/operation.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (h *Hid) OpenNoLock() { 11 | var err error 12 | h.CloseNoLock() 13 | 14 | h.g0, err = os.OpenFile("/dev/hidg0", os.O_WRONLY, 0o666) 15 | if err != nil { 16 | log.Errorf("open /dev/hidg0 failed: %s", err) 17 | } 18 | 19 | h.g1, err = os.OpenFile("/dev/hidg1", os.O_WRONLY, 0o666) 20 | if err != nil { 21 | log.Errorf("open /dev/hidg1 failed: %s", err) 22 | } 23 | 24 | h.g2, err = os.OpenFile("/dev/hidg2", os.O_WRONLY, 0o666) 25 | if err != nil { 26 | log.Errorf("open /dev/hidg2 failed: %s", err) 27 | } 28 | } 29 | 30 | func (h *Hid) Open() { 31 | h.kbMutex.Lock() 32 | defer h.kbMutex.Unlock() 33 | h.mouseMutex.Lock() 34 | defer h.mouseMutex.Unlock() 35 | 36 | h.CloseNoLock() 37 | 38 | h.OpenNoLock() 39 | } 40 | 41 | func (h *Hid) CloseNoLock() { 42 | for _, file := range []*os.File{h.g0, h.g1, h.g2} { 43 | if file != nil { 44 | _ = file.Sync() 45 | _ = file.Close() 46 | } 47 | } 48 | } 49 | 50 | func (h *Hid) Close() { 51 | h.kbMutex.Lock() 52 | defer h.kbMutex.Unlock() 53 | h.mouseMutex.Lock() 54 | defer h.mouseMutex.Unlock() 55 | 56 | h.CloseNoLock() 57 | } 58 | 59 | func (h *Hid) Write(file *os.File, data []byte) { 60 | _, err := file.Write(data) 61 | if err != nil { 62 | if errors.Is(err, os.ErrClosed) { 63 | log.Debugf("hid already closed, reopen it...") 64 | h.OpenNoLock() 65 | } else { 66 | log.Errorf("write to hid failed: %s", err) 67 | } 68 | 69 | return 70 | } 71 | 72 | log.Debugf("write to hid: %+v", data) 73 | } 74 | -------------------------------------------------------------------------------- /server/service/hid/reset.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "os" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func (s *Service) Reset(c *gin.Context) { 13 | var rsp proto.Response 14 | 15 | // reset USB 16 | f, err := os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644) 17 | if err != nil { 18 | log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) 19 | rsp.ErrRsp(c, -1, "open usb gadget file failed") 20 | return 21 | } 22 | err = f.Truncate(0) 23 | if err != nil { 24 | _ = f.Close() 25 | log.Errorf("truncate /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) 26 | rsp.ErrRsp(c, -1, "truncate usb gadget file failed") 27 | return 28 | } 29 | _, err = f.Seek(0, 0) 30 | if err != nil { 31 | _ = f.Close() 32 | log.Errorf("seek to 0 failed: %s", err) 33 | rsp.ErrRsp(c, -1, "seek to 0 in usb gadget file failed") 34 | return 35 | } 36 | _, err = f.WriteString("\n") 37 | if err != nil { 38 | _ = f.Close() 39 | log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) 40 | rsp.ErrRsp(c, -1, "write to usb gadget file failed") 41 | return 42 | } 43 | _ = f.Close() 44 | 45 | time.Sleep(1 * time.Second) 46 | 47 | devices, err := os.ReadDir("/sys/class/udc/") 48 | if err != nil { 49 | log.Errorf("read udc directory failed: %s", err) 50 | rsp.ErrRsp(c, -1, "read udc directory failed") 51 | return 52 | } 53 | 54 | f, err = os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644) 55 | if err != nil { 56 | log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) 57 | rsp.ErrRsp(c, -1, "open usb gadget file failed") 58 | return 59 | } 60 | for _, device := range devices { 61 | _, err = f.WriteString(device.Name() + "\n") 62 | if err != nil { 63 | _ = f.Close() 64 | log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) 65 | rsp.ErrRsp(c, -1, "write to usb gadget file failed") 66 | return 67 | } 68 | } 69 | 70 | _ = f.Close() 71 | 72 | rsp.OkRsp(c) 73 | log.Debugf("reset hid success") 74 | } 75 | -------------------------------------------------------------------------------- /server/service/hid/service.go: -------------------------------------------------------------------------------- 1 | package hid 2 | 3 | type Service struct { 4 | hid *Hid 5 | } 6 | 7 | func NewService() *Service { 8 | return &Service{ 9 | hid: GetHid(), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/service/network/service.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | type Service struct{} 4 | 5 | func NewService() *Service { 6 | return &Service{} 7 | } 8 | -------------------------------------------------------------------------------- /server/service/network/wifi.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "os" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const ( 13 | WiFiExistFile = "/etc/kvm/wifi_exist" 14 | WiFiSSID = "/etc/kvm/wifi.ssid" 15 | WiFiPasswd = "/etc/kvm/wifi.pass" 16 | WiFiConnect = "/kvmapp/kvm/wifi_try_connect" 17 | WiFiStateFile = "/kvmapp/kvm/wifi_state" 18 | ) 19 | 20 | func (s *Service) GetWifi(c *gin.Context) { 21 | var rsp proto.Response 22 | 23 | data := &proto.GetWifiRsp{} 24 | 25 | _, err := os.Stat(WiFiExistFile) 26 | if err != nil { 27 | rsp.OkRspWithData(c, data) 28 | return 29 | } 30 | 31 | data.Supported = true 32 | 33 | content, err := os.ReadFile(WiFiStateFile) 34 | if err != nil { 35 | rsp.OkRspWithData(c, data) 36 | return 37 | } 38 | 39 | state := strings.ReplaceAll(string(content), "\n", "") 40 | data.Connected = state == "1" 41 | 42 | rsp.OkRspWithData(c, data) 43 | log.Debugf("get wifi state: %s", state) 44 | } 45 | 46 | func (s *Service) ConnectWifi(c *gin.Context) { 47 | var req proto.ConnectWifiReq 48 | var rsp proto.Response 49 | 50 | if err := proto.ParseFormRequest(c, &req); err != nil { 51 | rsp.ErrRsp(c, -1, "invalid parameters") 52 | return 53 | } 54 | 55 | if err := os.WriteFile(WiFiSSID, []byte(req.Ssid), 0o644); err != nil { 56 | log.Errorf("failed to save wifi ssid: %s", err) 57 | rsp.ErrRsp(c, -2, "failed to save wifi") 58 | return 59 | } 60 | 61 | if err := os.WriteFile(WiFiPasswd, []byte(req.Password), 0o644); err != nil { 62 | log.Errorf("failed to save wifi password: %s", err) 63 | rsp.ErrRsp(c, -3, "failed to save wifi") 64 | return 65 | } 66 | 67 | if err := os.WriteFile(WiFiConnect, nil, 0o644); err != nil { 68 | log.Errorf("failed to connect wifi: %s", err) 69 | rsp.ErrRsp(c, -4, "failed to connect wifi") 70 | return 71 | } 72 | 73 | rsp.OkRsp(c) 74 | log.Debugf("set wifi successfully: %s", req.Ssid) 75 | } 76 | -------------------------------------------------------------------------------- /server/service/storage/service.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type Service struct{} 4 | 5 | func NewService() *Service { 6 | return &Service{} 7 | } 8 | -------------------------------------------------------------------------------- /server/service/stream/frame-rate.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | counter *FrameRateCounter 15 | counterOnce sync.Once 16 | ) 17 | 18 | type FrameRateCounter struct { 19 | frameCount int32 20 | fps int32 21 | mutex sync.Mutex 22 | } 23 | 24 | func GetFrameRateCounter() *FrameRateCounter { 25 | counterOnce.Do(func() { 26 | counter = &FrameRateCounter{} 27 | 28 | go func() { 29 | ticker := time.NewTicker(3 * time.Second) 30 | defer ticker.Stop() 31 | 32 | for range ticker.C { 33 | counter.mutex.Lock() 34 | 35 | currentCount := atomic.LoadInt32(&counter.frameCount) 36 | 37 | counter.fps = currentCount / 3 38 | atomic.StoreInt32(&counter.frameCount, 0) 39 | 40 | counter.mutex.Unlock() 41 | 42 | data := fmt.Sprintf("%d", counter.fps) 43 | err := os.WriteFile("/kvmapp/kvm/now_fps", []byte(data), 0o666) 44 | if err != nil { 45 | log.Errorf("failed to write fps: %s", err) 46 | } 47 | } 48 | }() 49 | }) 50 | 51 | return counter 52 | } 53 | 54 | func (f *FrameRateCounter) Update() { 55 | atomic.AddInt32(&f.frameCount, 1) 56 | } 57 | 58 | func (f *FrameRateCounter) GetFPS() int32 { 59 | f.mutex.Lock() 60 | defer f.mutex.Unlock() 61 | 62 | return f.fps 63 | } 64 | -------------------------------------------------------------------------------- /server/service/stream/h264/h264.go: -------------------------------------------------------------------------------- 1 | package h264 2 | 3 | import ( 4 | "NanoKVM-Server/config" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/gorilla/websocket" 11 | "github.com/pion/webrtc/v4" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ( 16 | upgrader = websocket.Upgrader{ 17 | CheckOrigin: func(r *http.Request) bool { 18 | return true 19 | }, 20 | } 21 | trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample) 22 | isSending = false 23 | ) 24 | 25 | func Connect(c *gin.Context) { 26 | wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 27 | if err != nil { 28 | log.Errorf("failed to create websocket: %s", err) 29 | return 30 | } 31 | 32 | defer func() { 33 | _ = wsConn.Close() 34 | log.Debugf("h264 websocket disconnected") 35 | }() 36 | 37 | var zeroTime time.Time 38 | _ = wsConn.SetReadDeadline(zeroTime) 39 | 40 | conf := config.GetInstance() 41 | 42 | var iceServers []webrtc.ICEServer 43 | 44 | if conf.Stun != "" && conf.Stun != "disable" { 45 | iceServers = append(iceServers, webrtc.ICEServer{ 46 | URLs: []string{"stun:" + conf.Stun}, 47 | }) 48 | } 49 | 50 | if conf.Turn.TurnAddr != "" && conf.Turn.TurnUser != "" && conf.Turn.TurnCred != "" { 51 | iceServers = append(iceServers, webrtc.ICEServer{ 52 | URLs: []string{"turn:" + conf.Turn.TurnAddr}, 53 | Username: conf.Turn.TurnUser, 54 | Credential: conf.Turn.TurnCred, 55 | }) 56 | } 57 | 58 | peerConn, err := webrtc.NewPeerConnection(webrtc.Configuration{ 59 | ICEServers: iceServers, 60 | }) 61 | if err != nil { 62 | log.Errorf("failed to create PeerConnection: %s", err) 63 | return 64 | } 65 | 66 | defer func() { 67 | _ = peerConn.Close() 68 | log.Debugf("PeerConnection disconnected") 69 | }() 70 | 71 | client := &Client{ 72 | ws: wsConn, 73 | pc: peerConn, 74 | mutex: sync.Mutex{}, 75 | } 76 | 77 | client.addTrack() 78 | client.register() 79 | client.readMessage() 80 | } 81 | -------------------------------------------------------------------------------- /server/service/stream/h264/sender.go: -------------------------------------------------------------------------------- 1 | package h264 2 | 3 | import ( 4 | "NanoKVM-Server/common" 5 | "time" 6 | 7 | "github.com/pion/webrtc/v4/pkg/media" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func send() { 12 | screen := common.GetScreen() 13 | common.CheckScreen() 14 | 15 | fps := screen.FPS 16 | duration := time.Second / time.Duration(fps) 17 | 18 | ticker := time.NewTicker(duration) 19 | defer ticker.Stop() 20 | 21 | vision := common.GetKvmVision() 22 | for range ticker.C { 23 | if !isSending && len(trackMap) == 0 { 24 | return 25 | } 26 | 27 | data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate) 28 | if result < 0 { 29 | continue 30 | } 31 | 32 | sample := media.Sample{ 33 | Data: data, 34 | Duration: duration, 35 | } 36 | 37 | for _, track := range trackMap { 38 | if err := track.WriteSample(sample); err != nil { 39 | log.Errorf("failed to send h264 data: %s", err) 40 | } 41 | } 42 | 43 | log.Debugf("send h264 data: %d", len(data)) 44 | 45 | if screen.FPS != fps { 46 | fps = screen.FPS 47 | duration = time.Second / time.Duration(fps) 48 | ticker.Reset(duration) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/service/stream/mjpeg/frame-detect.go: -------------------------------------------------------------------------------- 1 | package mjpeg 2 | 3 | import ( 4 | "NanoKVM-Server/common" 5 | "NanoKVM-Server/proto" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const FrameDetectInterval uint8 = 60 13 | 14 | func UpdateFrameDetect(c *gin.Context) { 15 | var req proto.UpdateFrameDetectReq 16 | var rsp proto.Response 17 | 18 | if err := proto.ParseFormRequest(c, &req); err != nil { 19 | rsp.ErrRsp(c, -1, "invalid parameters") 20 | return 21 | } 22 | 23 | var frame uint8 = 0 24 | if req.Enabled { 25 | frame = FrameDetectInterval 26 | } 27 | 28 | common.GetKvmVision().SetFrameDetect(frame) 29 | 30 | rsp.OkRsp(c) 31 | log.Debugf("update frame detect: %t", req.Enabled) 32 | } 33 | 34 | func StopFrameDetect(c *gin.Context) { 35 | var req proto.StopFrameDetectReq 36 | var rsp proto.Response 37 | 38 | if err := proto.ParseFormRequest(c, &req); err != nil { 39 | rsp.ErrRsp(c, -1, "invalid parameters") 40 | return 41 | } 42 | 43 | duration := 10 * time.Second 44 | if req.Duration > 0 { 45 | duration = time.Duration(req.Duration) * time.Second 46 | } 47 | 48 | vision := common.GetKvmVision() 49 | 50 | vision.SetFrameDetect(0) 51 | time.Sleep(duration) 52 | vision.SetFrameDetect(FrameDetectInterval) 53 | 54 | rsp.OkRsp(c) 55 | } 56 | -------------------------------------------------------------------------------- /server/service/vm/hdmi.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "time" 5 | 6 | "NanoKVM-Server/common" 7 | "NanoKVM-Server/proto" 8 | 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var hdmiEnabled = true 14 | 15 | func (s *Service) ResetHdmi(c *gin.Context) { 16 | var rsp proto.Response 17 | 18 | vision := common.GetKvmVision() 19 | 20 | vision.SetHDMI(false) 21 | time.Sleep(1 * time.Second) 22 | vision.SetHDMI(true) 23 | hdmiEnabled = true 24 | 25 | rsp.OkRsp(c) 26 | log.Debug("reset hdmi") 27 | } 28 | 29 | func (s *Service) EnableHdmi(c *gin.Context) { 30 | var rsp proto.Response 31 | 32 | vision := common.GetKvmVision() 33 | 34 | vision.SetHDMI(true) 35 | hdmiEnabled = true 36 | 37 | rsp.OkRsp(c) 38 | log.Debug("enable hdmi") 39 | } 40 | 41 | func (s *Service) DisableHdmi(c *gin.Context) { 42 | var rsp proto.Response 43 | 44 | vision := common.GetKvmVision() 45 | 46 | vision.SetHDMI(false) 47 | hdmiEnabled = false 48 | 49 | rsp.OkRsp(c) 50 | log.Debug("disable hdmi") 51 | } 52 | 53 | func (s *Service) GetHdmiState(c *gin.Context) { 54 | var rsp proto.Response 55 | 56 | rsp.OkRspWithData(c, &proto.GetGetHdmiStateRsp{ 57 | Enabled: hdmiEnabled, 58 | }) 59 | 60 | log.Debug("get hdmi state") 61 | } 62 | -------------------------------------------------------------------------------- /server/service/vm/hostname.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "NanoKVM-Server/proto" 9 | 10 | "github.com/gin-gonic/gin" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | BootHostnameFile = "/boot/hostname" 16 | EtcHostname = "/etc/hostname" 17 | ) 18 | 19 | func (s *Service) SetHostname(c *gin.Context) { 20 | var req proto.SetHostnameReq 21 | var rsp proto.Response 22 | 23 | if err := proto.ParseFormRequest(c, &req); err != nil { 24 | rsp.ErrRsp(c, -1, "invalid arguments") 25 | return 26 | } 27 | 28 | data := []byte(fmt.Sprintf("%s", req.Hostname)) 29 | 30 | if err := os.WriteFile(BootHostnameFile, data, 0o644); err != nil { 31 | rsp.ErrRsp(c, -2, "failed to write data") 32 | return 33 | } 34 | 35 | if err := os.WriteFile(EtcHostname, data, 0o644); err != nil { 36 | rsp.ErrRsp(c, -3, "failed to write data") 37 | return 38 | } 39 | 40 | rsp.OkRsp(c) 41 | log.Debugf("set Hostname: %s", req.Hostname) 42 | } 43 | 44 | func (s *Service) GetHostname(c *gin.Context) { 45 | var rsp proto.Response 46 | 47 | data, err := os.ReadFile(EtcHostname) 48 | if err != nil { 49 | rsp.ErrRsp(c, -1, "read Hostname failed") 50 | return 51 | } 52 | 53 | rsp.OkRspWithData(c, &proto.GetHostnameRsp{ 54 | Hostname: strings.Replace(string(data), "\n", "", -1), 55 | }) 56 | log.Debugf("get Hostname successful") 57 | } 58 | -------------------------------------------------------------------------------- /server/service/vm/jiggler/mouse.go: -------------------------------------------------------------------------------- 1 | package jiggler 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func move(mode string) { 11 | var ( 12 | hid string 13 | data [][]byte 14 | ) 15 | 16 | if mode == "absolute" { 17 | hid = "/dev/hidg2" 18 | data = [][]byte{ 19 | {0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00}, 20 | {0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00}, 21 | } 22 | } else { 23 | hid = "/dev/hidg1" 24 | data = [][]byte{ 25 | {0x00, 0x0a, 0x0a, 0x00}, 26 | {0x00, 0xf6, 0xf6, 0x00}, 27 | } 28 | } 29 | 30 | write(hid, data) 31 | } 32 | 33 | func write(hid string, data [][]byte) { 34 | file, err := os.OpenFile(hid, os.O_WRONLY, 0o666) 35 | if err != nil { 36 | log.Errorf("failed to open %s: %s", hid, err) 37 | return 38 | } 39 | defer func() { 40 | _ = file.Close() 41 | }() 42 | 43 | for _, b := range data { 44 | deadline := time.Now().Add(8 * time.Millisecond) 45 | if err := file.SetWriteDeadline(deadline); err != nil { 46 | log.Errorf("failed to set deadline: %s", err) 47 | return 48 | } 49 | 50 | if _, err := file.Write(b); err != nil { 51 | log.Errorf("failed to write: %s", err) 52 | return 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/service/vm/memory.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "NanoKVM-Server/utils" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (s *Service) SetMemoryLimit(c *gin.Context) { 12 | var req proto.SetMemoryLimitReq 13 | var rsp proto.Response 14 | 15 | err := proto.ParseFormRequest(c, &req) 16 | if err != nil { 17 | rsp.ErrRsp(c, -1, "invalid arguments") 18 | return 19 | } 20 | 21 | if req.Enabled { 22 | err = utils.SetGoMemLimit(req.Limit) 23 | } else { 24 | err = utils.DelGoMemLimit() 25 | } 26 | 27 | if err != nil { 28 | rsp.ErrRsp(c, -2, "failed to set memory limit") 29 | return 30 | } 31 | 32 | rsp.OkRsp(c) 33 | log.Debugf("set memory limit successful, enabled: %t, limit: %d", req.Enabled, req.Limit) 34 | } 35 | 36 | func (s *Service) GetMemoryLimit(c *gin.Context) { 37 | var rsp proto.Response 38 | 39 | exist := utils.IsGoMemLimitExist() 40 | if !exist { 41 | rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{ 42 | Enabled: false, 43 | Limit: 0, 44 | }) 45 | return 46 | } 47 | 48 | limit, err := utils.GetGoMemLimit() 49 | if err != nil { 50 | rsp.ErrRsp(c, -1, "failed to get memory limit") 51 | return 52 | } 53 | 54 | rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{ 55 | Enabled: true, 56 | Limit: limit, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /server/service/vm/mouse-jiggler.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "NanoKVM-Server/service/vm/jiggler" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (s *Service) GetMouseJiggler(c *gin.Context) { 12 | var rsp proto.Response 13 | 14 | mouseJiggler := jiggler.GetJiggler() 15 | 16 | data := &proto.GetMouseJigglerRsp{ 17 | Enabled: mouseJiggler.IsEnabled(), 18 | Mode: mouseJiggler.GetMode(), 19 | } 20 | 21 | rsp.OkRspWithData(c, data) 22 | } 23 | 24 | func (s *Service) SetMouseJiggler(c *gin.Context) { 25 | var req proto.SetMouseJigglerReq 26 | var rsp proto.Response 27 | 28 | err := proto.ParseFormRequest(c, &req) 29 | if err != nil { 30 | rsp.ErrRsp(c, -1, "invalid arguments") 31 | return 32 | } 33 | 34 | mouseJiggler := jiggler.GetJiggler() 35 | 36 | if req.Enabled { 37 | err = mouseJiggler.Enable(req.Mode) 38 | } else { 39 | err = mouseJiggler.Disable() 40 | } 41 | 42 | if err != nil { 43 | rsp.ErrRsp(c, -2, "operation failed") 44 | return 45 | } 46 | 47 | rsp.OkRsp(c) 48 | log.Debugf("set mouse jiggler: %t", req.Enabled) 49 | } 50 | -------------------------------------------------------------------------------- /server/service/vm/oled.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | OLEDExistFile = "/etc/kvm/oled_exist" 16 | OLEDSleepFile = "/etc/kvm/oled_sleep" 17 | ) 18 | 19 | func (s *Service) SetOLED(c *gin.Context) { 20 | var req proto.SetOledReq 21 | var rsp proto.Response 22 | 23 | if err := proto.ParseFormRequest(c, &req); err != nil { 24 | rsp.ErrRsp(c, -1, "invalid arguments") 25 | return 26 | } 27 | 28 | data := []byte(fmt.Sprintf("%d", req.Sleep)) 29 | err := os.WriteFile(OLEDSleepFile, data, 0o644) 30 | if err != nil { 31 | rsp.ErrRsp(c, -2, "failed to write data") 32 | return 33 | } 34 | 35 | rsp.OkRsp(c) 36 | log.Debugf("set OLED sleep: %d", req.Sleep) 37 | } 38 | 39 | func (s *Service) GetOLED(c *gin.Context) { 40 | var rsp proto.Response 41 | 42 | if _, err := os.Stat(OLEDExistFile); err != nil { 43 | rsp.OkRspWithData(c, &proto.GetOLEDRsp{ 44 | Exist: false, 45 | Sleep: 0, 46 | }) 47 | return 48 | } 49 | 50 | data, err := os.ReadFile(OLEDSleepFile) 51 | if err != nil { 52 | rsp.OkRspWithData(c, &proto.GetOLEDRsp{ 53 | Exist: true, 54 | Sleep: 0, 55 | }) 56 | return 57 | } 58 | 59 | content := strings.TrimSpace(string(data)) 60 | sleep, err := strconv.Atoi(content) 61 | if err != nil { 62 | log.Errorf("failed to parse OLED: %s", err) 63 | rsp.ErrRsp(c, -1, "failed to parse OLED config") 64 | return 65 | } 66 | 67 | rsp.OkRspWithData(c, &proto.GetOLEDRsp{ 68 | Exist: true, 69 | Sleep: sleep, 70 | }) 71 | log.Debugf("get OLED config successful, sleep %d", sleep) 72 | } 73 | -------------------------------------------------------------------------------- /server/service/vm/screen.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/common" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "NanoKVM-Server/proto" 13 | ) 14 | 15 | var screenFileMap = map[string]string{ 16 | "type": "/kvmapp/kvm/type", 17 | "fps": "/kvmapp/kvm/fps", 18 | "quality": "/kvmapp/kvm/qlty", 19 | "resolution": "/kvmapp/kvm/res", 20 | } 21 | 22 | func (s *Service) SetScreen(c *gin.Context) { 23 | var req proto.SetScreenReq 24 | var rsp proto.Response 25 | 26 | err := proto.ParseFormRequest(c, &req) 27 | if err != nil { 28 | rsp.ErrRsp(c, -1, "invalid arguments") 29 | return 30 | } 31 | 32 | switch req.Type { 33 | case "type": 34 | data := "h264" 35 | if req.Value == 0 { 36 | data = "mjpeg" 37 | } 38 | err = writeScreen("type", data) 39 | 40 | case "gop": 41 | gop := 30 42 | if req.Value >= 1 && req.Value <= 100 { 43 | gop = req.Value 44 | } 45 | common.GetKvmVision().SetGop(uint8(gop)) 46 | 47 | default: 48 | data := strconv.Itoa(req.Value) 49 | err = writeScreen(req.Type, data) 50 | } 51 | 52 | if err != nil { 53 | rsp.ErrRsp(c, -2, "update screen failed") 54 | return 55 | } 56 | 57 | common.SetScreen(req.Type, req.Value) 58 | 59 | log.Debugf("update screen: %+v", req) 60 | rsp.OkRsp(c) 61 | } 62 | 63 | func writeScreen(key string, value string) error { 64 | file, ok := screenFileMap[key] 65 | if !ok { 66 | return fmt.Errorf("invalid argument %s", key) 67 | } 68 | 69 | err := os.WriteFile(file, []byte(value), 0o666) 70 | if err != nil { 71 | log.Errorf("write kvm %s failed: %s", file, err) 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /server/service/vm/service.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | type Service struct { 4 | } 5 | 6 | func NewService() *Service { 7 | return &Service{} 8 | } 9 | -------------------------------------------------------------------------------- /server/service/vm/ssh.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/gin-gonic/gin" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | SSHScript = "/etc/init.d/S50sshd" 16 | SSHStopFlag = "/etc/kvm/ssh_stop" 17 | ) 18 | 19 | func (s *Service) GetSSHState(c *gin.Context) { 20 | var rsp proto.Response 21 | 22 | enabled := isSSHEnabled() 23 | rsp.OkRspWithData(c, &proto.GetSSHStateRsp{ 24 | Enabled: enabled, 25 | }) 26 | } 27 | 28 | func (s *Service) EnableSSH(c *gin.Context) { 29 | var rsp proto.Response 30 | 31 | command := fmt.Sprintf("%s permanent_on", SSHScript) 32 | err := exec.Command("sh", "-c", command).Run() 33 | if err != nil { 34 | log.Errorf("failed to run SSH script: %s", err) 35 | rsp.ErrRsp(c, -1, "operation failed") 36 | return 37 | } 38 | 39 | rsp.OkRsp(c) 40 | log.Debugf("SSH enabled") 41 | } 42 | 43 | func (s *Service) DisableSSH(c *gin.Context) { 44 | var rsp proto.Response 45 | 46 | command := fmt.Sprintf("%s permanent_off", SSHScript) 47 | err := exec.Command("sh", "-c", command).Run() 48 | if err != nil { 49 | log.Errorf("failed to run SSH script: %s", err) 50 | rsp.ErrRsp(c, -1, "operation failed") 51 | return 52 | } 53 | 54 | rsp.OkRsp(c) 55 | log.Debugf("SSH disabled") 56 | } 57 | 58 | func isSSHEnabled() bool { 59 | _, err := os.Stat(SSHStopFlag) 60 | if err != nil { 61 | if errors.Is(err, os.ErrNotExist) { 62 | return true 63 | } 64 | } 65 | 66 | return false 67 | } 68 | -------------------------------------------------------------------------------- /server/service/vm/system.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "NanoKVM-Server/proto" 5 | "os/exec" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (s *Service) Reboot(c *gin.Context) { 12 | var rsp proto.Response 13 | 14 | log.Println("reboot system...") 15 | 16 | err := exec.Command("reboot").Run() 17 | if err != nil { 18 | rsp.ErrRsp(c, -1, "operation failed") 19 | log.Errorf("failed to reboot: %s", err) 20 | return 21 | } 22 | 23 | rsp.OkRsp(c) 24 | log.Debug("system rebooted") 25 | } 26 | -------------------------------------------------------------------------------- /server/service/vm/tls.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "NanoKVM-Server/config" 11 | "NanoKVM-Server/proto" 12 | "NanoKVM-Server/utils" 13 | ) 14 | 15 | func (s *Service) SetTls(c *gin.Context) { 16 | var req proto.SetTlsReq 17 | var rsp proto.Response 18 | 19 | err := proto.ParseFormRequest(c, &req) 20 | if err != nil { 21 | rsp.ErrRsp(c, -1, fmt.Sprintf("invalid arguments: %s", err)) 22 | return 23 | } 24 | 25 | if req.Enabled { 26 | err = enableTls() 27 | } else { 28 | err = disableTls() 29 | } 30 | 31 | if err != nil { 32 | log.Errorf("failed to set TLS: %s", err) 33 | rsp.ErrRsp(c, -2, "operation failed") 34 | return 35 | } 36 | 37 | rsp.OkRsp(c) 38 | 39 | _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() 40 | } 41 | 42 | func enableTls() error { 43 | if err := utils.GenerateCert(); err != nil { 44 | return err 45 | } 46 | 47 | conf, err := config.Read() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | conf.Proto = "https" 53 | conf.Cert.Crt = "/etc/kvm/server.crt" 54 | conf.Cert.Key = "/etc/kvm/server.key" 55 | 56 | if err := config.Write(conf); err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func disableTls() error { 64 | conf, err := config.Read() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | conf.Proto = "http" 70 | 71 | if err := config.Write(conf); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /server/service/vm/web-title.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "NanoKVM-Server/proto" 8 | 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const ( 14 | WebTitleFile = "/etc/kvm/web-title" 15 | ) 16 | 17 | func (s *Service) SetWebTitle(c *gin.Context) { 18 | var req proto.SetWebTitleReq 19 | var rsp proto.Response 20 | 21 | if err := proto.ParseFormRequest(c, &req); err != nil { 22 | rsp.ErrRsp(c, -1, "invalid arguments") 23 | return 24 | } 25 | 26 | if req.Title == "" || req.Title == "NanoKVM" { 27 | err := os.Remove(WebTitleFile) 28 | if err != nil { 29 | rsp.ErrRsp(c, -2, "reset failed") 30 | return 31 | } 32 | } else { 33 | err := os.WriteFile(WebTitleFile, []byte(req.Title), 0o644) 34 | if err != nil { 35 | rsp.ErrRsp(c, -3, "write failed") 36 | return 37 | } 38 | } 39 | 40 | rsp.OkRsp(c) 41 | log.Debugf("set web title: %s", req.Title) 42 | } 43 | 44 | func (s *Service) GetWebTitle(c *gin.Context) { 45 | var rsp proto.Response 46 | 47 | data, err := os.ReadFile(WebTitleFile) 48 | if err != nil { 49 | rsp.ErrRsp(c, -1, "read web title failed") 50 | return 51 | } 52 | 53 | rsp.OkRspWithData(c, &proto.GetWebTitleRsp{ 54 | Title: strings.Replace(string(data), "\n", "", -1), 55 | }) 56 | 57 | log.Debugf("get web title successful") 58 | } 59 | -------------------------------------------------------------------------------- /server/service/ws/message.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | type Stream struct { 4 | Type string `json:"type"` 5 | State int `json:"state"` 6 | } 7 | -------------------------------------------------------------------------------- /server/service/ws/service.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | type Service struct{} 4 | 5 | func NewService() *Service { 6 | return &Service{} 7 | } 8 | -------------------------------------------------------------------------------- /server/utils/chmod.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func ChmodRecursively(path string, mode uint32) error { 9 | return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 10 | if err != nil { 11 | return err 12 | } 13 | 14 | if !info.IsDir() { 15 | err = os.Chmod(path, os.FileMode(mode)) 16 | if err != nil { 17 | return err 18 | } 19 | } 20 | 21 | return nil 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /server/utils/encrypt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/mervick/aes-everywhere/go/aes256" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // SecretKey is only used to prevent the data from being transmitted in plaintext. 11 | const SecretKey = "nanokvm-sipeed-2024" 12 | 13 | func Decrypt(ciphertext string) (string, error) { 14 | if ciphertext == "" { 15 | return "", nil 16 | } 17 | 18 | decrypt := aes256.Decrypt(ciphertext, SecretKey) 19 | return decrypt, nil 20 | } 21 | 22 | func DecodeDecrypt(data string) (string, error) { 23 | ciphertext, err := url.QueryUnescape(data) 24 | if err != nil { 25 | log.Errorf("decode ciphertext failed: %s", err) 26 | return "", err 27 | } 28 | 29 | return Decrypt(ciphertext) 30 | } 31 | -------------------------------------------------------------------------------- /server/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func Download(req *http.Request, target string) error { 14 | log.Debugf("downloading %s to %s", req.URL.String(), target) 15 | err := os.MkdirAll(filepath.Dir(target), 0o755) 16 | if err != nil { 17 | log.Errorf("create dir %s err: %s", filepath.Dir(target), err) 18 | return err 19 | } 20 | out, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) 21 | if err != nil { 22 | log.Errorf("cannot create file '%s', error: %s", target, err) 23 | return err 24 | } 25 | defer func() { 26 | _ = out.Close() 27 | }() 28 | 29 | resp, err := (&http.Client{}).Do(req) 30 | if err != nil { 31 | log.Errorf("request error: %s", err) 32 | return err 33 | } 34 | defer func() { 35 | _ = resp.Body.Close() 36 | }() 37 | 38 | if resp.StatusCode != http.StatusOK { 39 | log.Errorf("request failed, status code: %d", resp.StatusCode) 40 | return errors.New("update website is inaccessible right now") 41 | } 42 | 43 | contentType := resp.Header.Get("Content-Type") 44 | if contentType != "application/octet-stream" && contentType != "application/zip" && contentType != "application/gzip" { 45 | log.Debugf("unexpected content-type, it should be either octet-stream or (g)zip, but got: %s", contentType) 46 | return errors.New("unsupported content type") 47 | } 48 | 49 | _, err = io.Copy(out, resp.Body) 50 | if err != nil { 51 | log.Errorf("download file to %s err: %s", target, err) 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /server/utils/memory.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | "strconv" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const GoMemLimitFile = "/etc/kvm/GOMEMLIMIT" 14 | 15 | func InitGoMemLimit() { 16 | if !IsGoMemLimitExist() { 17 | return 18 | } 19 | 20 | limit, err := GetGoMemLimit() 21 | if err != nil { 22 | return 23 | } 24 | 25 | debug.SetMemoryLimit(limit * 1024 * 1024) 26 | log.Debugf("set GOMEMLIMIT to %d MB", limit) 27 | } 28 | 29 | func SetGoMemLimit(limit int64) error { 30 | memoryLimit := max(limit, 50) 31 | debug.SetMemoryLimit(memoryLimit * 1024 * 1024) 32 | 33 | log.Debugf("set GOMEMLIMIT to %d MB", limit) 34 | 35 | data := []byte(fmt.Sprintf("%d", limit)) 36 | err := os.WriteFile(GoMemLimitFile, data, 0o644) 37 | if err != nil { 38 | log.Errorf("failed to write GOMEMLIMIT: %s", err) 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func GetGoMemLimit() (int64, error) { 46 | data, err := os.ReadFile(GoMemLimitFile) 47 | if err != nil { 48 | log.Errorf("failed to read GOMEMLIMIT: %s", err) 49 | return 0, err 50 | } 51 | 52 | content := strings.TrimSpace(string(data)) 53 | limit, err := strconv.ParseInt(content, 10, 64) 54 | if err != nil { 55 | log.Errorf("failed to parse GOMEMLIMIT: %s", err) 56 | return 0, err 57 | } 58 | 59 | return limit, nil 60 | } 61 | 62 | func DelGoMemLimit() error { 63 | debug.SetMemoryLimit(1024 * 1024 * 1024) 64 | 65 | err := os.Remove(GoMemLimitFile) 66 | if err != nil { 67 | log.Errorf("failed to delete GOMEMLIMIT: %s", err) 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func IsGoMemLimitExist() bool { 75 | _, err := os.Stat(GoMemLimitFile) 76 | return err == nil 77 | } 78 | -------------------------------------------------------------------------------- /server/utils/move-file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func MoveFile(src, dst string) error { 11 | if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { 12 | return err 13 | } 14 | err := os.Rename(src, dst) 15 | if err != nil { 16 | if strings.Contains(err.Error(), "invalid cross-device link") { 17 | return MoveFileCrossFS(src, dst) 18 | } 19 | return err 20 | } 21 | return nil 22 | } 23 | 24 | func MoveFileCrossFS(src, dst string) error { 25 | tmp := dst + ".tmp" 26 | srcFile, err := os.Open(src) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | tmpFile, err := os.Create(tmp) 32 | if err != nil { 33 | _ = srcFile.Close() 34 | return err 35 | } 36 | _, err = io.Copy(tmpFile, srcFile) 37 | if err != nil { 38 | _ = srcFile.Close() 39 | _ = tmpFile.Close() 40 | return err 41 | } 42 | _ = srcFile.Close() 43 | _ = tmpFile.Close() 44 | fi, err := os.Stat(src) 45 | if err != nil { 46 | return err 47 | } 48 | err = os.Chmod(tmp, fi.Mode()) 49 | if err != nil { 50 | return err 51 | } 52 | _ = os.Remove(src) 53 | err = os.Rename(tmp, dst) 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | func MoveFilesRecursively(src, dst string) error { 61 | return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { 62 | if err != nil { 63 | return err 64 | } 65 | 66 | fileName := strings.Replace(path, src, "", 1) 67 | dstName := dst + fileName 68 | fileInfo, err := os.Stat(path) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if fileInfo.IsDir() { 74 | return os.MkdirAll(dstName, fileInfo.Mode()) 75 | } 76 | return MoveFile(path, dstName) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /server/utils/permission.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | func HasPermission(filePath string, perm os.FileMode) (bool, error) { 6 | fileInfo, err := os.Stat(filePath) 7 | if err != nil { 8 | return false, err 9 | } 10 | 11 | mode := fileInfo.Mode().Perm() 12 | if mode&perm == perm { 13 | return true, nil 14 | } 15 | 16 | return false, nil 17 | } 18 | 19 | func AddPermission(filePath string, perm os.FileMode) error { 20 | fileInfo, err := os.Stat(filePath) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | mode := fileInfo.Mode() | perm 26 | err = os.Chmod(filePath, mode) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func EnsurePermission(filePath string, perm os.FileMode) error { 35 | hasPerm, err := HasPermission(filePath, perm) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if !hasPerm { 41 | err = AddPermission(filePath, perm) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /server/utils/untar.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func UnTarGz(srcFile string, destDir string) (string, error) { 13 | if err := os.MkdirAll(destDir, 0755); err != nil { 14 | return "", err 15 | } 16 | 17 | fr, err := os.Open(srcFile) 18 | if err != nil { 19 | return "", err 20 | } 21 | defer func() { 22 | _ = fr.Close() 23 | }() 24 | 25 | gr, err := gzip.NewReader(fr) 26 | if err != nil { 27 | return "", err 28 | } 29 | defer func() { 30 | _ = gr.Close() 31 | }() 32 | 33 | tr := tar.NewReader(gr) 34 | 35 | targetFile := "" 36 | for { 37 | header, err := tr.Next() 38 | 39 | if err == io.EOF { 40 | break 41 | } 42 | 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | if targetFile == "" { 48 | parts := strings.Split(header.Name, "/") 49 | if len(parts) > 0 { 50 | targetFile = filepath.Join(destDir, parts[0]) 51 | } 52 | } 53 | 54 | filename := filepath.Join(destDir, header.Name) 55 | 56 | switch header.Typeflag { 57 | case tar.TypeDir: 58 | if err := os.MkdirAll(filename, os.FileMode(header.Mode)); err != nil { 59 | return "", err 60 | } 61 | 62 | case tar.TypeReg: 63 | file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | if _, err := io.Copy(file, tr); err != nil { 69 | _ = file.Close() 70 | return "", err 71 | } 72 | _ = file.Close() 73 | 74 | case tar.TypeSymlink: 75 | if err := os.Symlink(header.Linkname, filename); err != nil { 76 | return "", err 77 | } 78 | } 79 | } 80 | 81 | return targetFile, nil 82 | } 83 | -------------------------------------------------------------------------------- /server/utils/unzip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func Unzip(filename string, dest string) error { 11 | r, err := zip.OpenReader(filename) 12 | if err != nil { 13 | return err 14 | } 15 | defer func() { 16 | _ = r.Close() 17 | }() 18 | 19 | for _, f := range r.File { 20 | dstPath := filepath.Join(dest, filepath.Clean("/"+f.Name)) 21 | if f.FileInfo().IsDir() { 22 | err = os.MkdirAll(dstPath, 0o755) 23 | if err != nil { 24 | return err 25 | } 26 | } else { 27 | err = unzipFile(dstPath, f) 28 | if err != nil { 29 | return err 30 | } 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func unzipFile(dstPath string, f *zip.File) error { 37 | err := os.MkdirAll(filepath.Dir(dstPath), 0o755) 38 | if err != nil { 39 | return err 40 | } 41 | out, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) 42 | if err != nil { 43 | return err 44 | } 45 | defer func() { 46 | _ = out.Close() 47 | }() 48 | 49 | archivedFile, err := f.Open() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if _, err = io.Copy(out, archivedFile); err != nil { 55 | return err 56 | } 57 | if err = os.Chmod(dstPath, f.Mode()); err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /support/README.md: -------------------------------------------------------------------------------- 1 | # NanoKVM Support Instructions 2 | 3 | `/support` contains auxiliary functions for NanoKVM, such as image subsystem, system status monitoring, system updates, screen key drivers, and a few system functions. 4 | 5 | Currently, NanoKVM is divided into two versions based on the main control chip: SG2002 (which includes NanoKVM-Lite/Full/PCIe) and H618 (including NanoKVM-Pro). Different chips have significantly different projects and compilation environments. To distinguish between them, they are stored separately in `/support/sg2002` and `/support/h618`. 6 | -------------------------------------------------------------------------------- /support/README_ZH.md: -------------------------------------------------------------------------------- 1 | # NanoKVM support 说明 2 | 3 | `/support`包含NanoKVM辅助性的功能,如图像子系统、系统状态监控、系统更新、屏幕按键驱动和少部分的系统功能 4 | 5 | 当前 NanoKVM 根据主控芯片的不同分为两个版本:SG2002(包含NanoKVM-Lite/Full/PCIe)和H618(包括NanoKVM-Pro),不同的芯片有差异较大的工程和编译环境,为做区分,将它们分别存放在`/support/sg2002`和`/support/h618` 6 | -------------------------------------------------------------------------------- /support/sg2002/README.md: -------------------------------------------------------------------------------- 1 | # NanoKVM Support Instructions 2 | 3 | ## Environment Preparation 4 | 1. NanoKVM-Lite/Full/PCIe is based on the SG2002 as the main control chip. The projects in the support section are compiled under the [MaixCDK](https://github.com/sipeed/MaixCDK) framework. Before compiling, please ensure that the `MaixCDK` environment is correctly configured. For configuration instructions, click [here](https://github.com/sipeed/MaixCDK/blob/main/docs/doc_zh/README.md). 5 | 6 | ## kvm_system Compilation Instructions 7 | 8 | > The `kvm_system` is responsible for monitoring the NanoKVM system status, system updates, screen key drivers, and a few system functions, compiled with MaixCDK. 9 | 10 | 1. Before compiling, please ensure that the above-mentioned `MaixCDK` environment is correctly configured. 11 | 2. Modify the paths of `MAIXCDK_PATH` and `NanoKVM_PATH` in `./build`. 12 | 3. Execute `./build kvm_system` to compile `kvm_system`. 13 | 4. Use `scp ./kvm_system/dist/kvm_system_release/kvm_system root@192.168.x.x:/kvmapp/kvm_system` to copy it to NanoKVM for testing. 14 | 5. Use `./build add_to_kvmapp` to place the executable file into the `/kvmapp` installation package. 15 | 6. Use `./build kvm_system clean` to clean the compilation of `kvm_system`. 16 | 17 | ## kvm_vision Compilation Instructions 18 | 19 | > `kvm_vision` refers to the image acquisition and encoding subsystem of NanoKVM, compiled with MaixCDK to produce dynamic libraries for Go calls. Use `kvm_vision_test` to compile and test the dynamic library. 20 | 21 | 1. Before compiling, please ensure that the above-mentioned `MaixCDK` environment is correctly configured. 22 | 2. Modify the paths of `MAIXCDK_PATH` and `NanoKVM_PATH` in `./build`. 23 | 3. Execute `./build kvm_vision` to compile `kvm_vision_test`. 24 | 4. You can test the dynamic libraries in `kvm_vision_test/dist/kvm_vision_test_release/dl_lib/`. 25 | 5. Use `./build add_to_kvmapp` to place the dynamic libraries into the `/kvmapp` installation package. 26 | 6. Use `./build kvm_vision clean` to clean the compilation of `kvm_vision`. -------------------------------------------------------------------------------- /support/sg2002/README_ZH.md: -------------------------------------------------------------------------------- 1 | # NanoKVM support 说明 2 | 3 | ## 环境准备 4 | 1. NanoKVM-Lite/Full/PCIe 以SG2002为主控芯片,support 部分的工程在[MaixCDK](https://github.com/sipeed/MaixCDK) 框架下编译。编译前,请保证`MaixCDK`环境已正确配置,配置教程点击[这里](https://github.com/sipeed/MaixCDK/blob/main/docs/doc_zh/README.md) 5 | 6 | ## kvm_system 编译说明 7 | 8 | > `kvm_system` 负责 NanoKVM 系统状态监控、系统更新、屏幕按键驱动和少部分的系统功能,借助 MaixCDK 编译 9 | 10 | 1. 编译前,请保证上述 1.`MaixCDK`环境已正确配置 11 | 2. 修改 `./build` 中 `MAIXCDK_PATH` 和 `NanoKVM_PATH`的路径 12 | 3. 执行 `./build kvm_system` 编译 kvm_system 13 | 4. 使用 `scp ./kvm_system/dist/kvm_system_release/kvm_system root@192.168.x.x:/kvmapp/kvm_system` 拷贝入 NanoKVM 测试 14 | 5. 使用 `./build add_to_kvmapp` 可以将可执行文件放入 `/kvmapp` 安装包内 15 | 6. 使用 `./build kvm_system clean` 可以清除 kvm_system 的编译 16 | 17 | ## kvm_vision 编译说明 18 | 19 | > `kvm_vision` 是 NanoKVM 图像获取编码子系统的统称,使用MaixCDK编译出动态库供Go调用,使用 `kvm_vision_test` 编译和测试动态库 20 | 21 | 1. 编译前,请保证上述 1.`MaixCDK`环境已正确配置 22 | 2. 修改 `./build` 中 `MAIXCDK_PATH` 和 `NanoKVM_PATH`的路径 23 | 3. 执行 `./build kvm_vision` 编译 kvm_vision_test 24 | 4. 可使用 `kvm_vision_test/dist/kvm_vision_test_release/dl_lib/` 中的动态库测试 25 | 5. 使用 `./build add_to_kvmapp` 可以将动态库放入 `/kvmapp` 安装包内 26 | 6. 使用 `./build kvm_vision clean` 可以清除 kvm_system 的编译 27 | -------------------------------------------------------------------------------- /support/sg2002/additional/kvm/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | list(APPEND ADD_REQUIREMENTS vision basic peripheral) 2 | list(APPEND ADD_INCLUDE "include") 3 | 4 | append_srcs_dir(ADD_SRCS "src") 5 | 6 | register_component(DYNAMIC) -------------------------------------------------------------------------------- /support/sg2002/additional/kvm/Kconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/support/sg2002/additional/kvm/Kconfig -------------------------------------------------------------------------------- /support/sg2002/additional/kvm_mmf/Kconfig: -------------------------------------------------------------------------------- 1 | # sophgo middleware version major minor patch 2 | menu "sophgo middleware version" 3 | config SOPHGO_MIDDLEWARE_CHIP 4 | string "sophgo chip" 5 | default "cv181x" 6 | help 7 | sophgo chip. cv180x or cv181x 8 | 9 | config SOPHGO_MIDDLEWARE_C_LIBRARY 10 | string "sophgo c standard library" 11 | default "musl" 12 | help 13 | sophgo c standard library. glibc or musl 14 | 15 | config SOPHGO_MIDDLEWARE_VERSION_MAJOR 16 | int "sophgo middleware package major version" 17 | default 0 18 | help 19 | sophgo middleware package major version, 0 means auto select according to board 20 | 21 | config SOPHGO_MIDDLEWARE_VERSION_MINOR 22 | int "sophgo middleware package minor version" 23 | default 0 24 | help 25 | sophgo middleware package minor version 26 | 27 | config SOPHGO_MIDDLEWARE_VERSION_PATCH 28 | int "sophgo middleware package patch version" 29 | default 4 30 | help 31 | sophgo middleware package patch version 32 | endmenu 33 | -------------------------------------------------------------------------------- /support/sg2002/additional/vision/Kconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/support/sg2002/additional/vision/Kconfig -------------------------------------------------------------------------------- /support/sg2002/additional/vision/include/maix_vision.hpp: -------------------------------------------------------------------------------- 1 | #include "maix_image.hpp" 2 | #include "maix_display.hpp" 3 | #include "maix_camera.hpp" 4 | #include "maix_video.hpp" 5 | -------------------------------------------------------------------------------- /support/sg2002/additional/vision/include_private/maix_image_util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "maix_image.hpp" 4 | #include "omv.hpp" 5 | 6 | namespace maix::image 7 | { 8 | /** 9 | * Convert image to openmv image format 10 | * @param image image 11 | * @param imlib_image openmv image 12 | * @return 13 | */ 14 | extern void convert_to_imlib_image(image::Image *image, image_t *imlib_image); 15 | extern void _convert_to_lab_thresholds(std::vector> &in, list_t *out); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | list(APPEND ADD_INCLUDE 3 | "include" 4 | "lib/libqr" 5 | "lib/system_ctrl" 6 | "lib/system_state" 7 | "lib/system_init" 8 | "lib/oled_ctrl" 9 | "lib/oled_ui" 10 | "lib/hdmi" 11 | ) 12 | 13 | list(APPEND ADD_PRIVATE_INCLUDE "") 14 | 15 | append_srcs_dir(ADD_SRCS 16 | "src" 17 | "lib/libqr" 18 | "lib/system_ctrl" 19 | "lib/system_state" 20 | "lib/system_init" 21 | "lib/oled_ctrl" 22 | "lib/oled_ui" 23 | "lib/hdmi" 24 | ) # append source file in src dir to var ADD_SRCS 25 | 26 | list(APPEND ADD_REQUIREMENTS basic peripheral) 27 | 28 | register_component() 29 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/Kconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/support/sg2002/kvm_system/main/Kconfig -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/hdmi/hdmi.h: -------------------------------------------------------------------------------- 1 | #ifndef HDMI_H_ 2 | #define HDMI_H_ 3 | 4 | #include "maix_basic.hpp" 5 | #include "maix_time.hpp" 6 | #include "maix_gpio.hpp" 7 | #include "maix_pinmap.hpp" 8 | #include "maix_i2c.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define LT6911_ADDR 0x2B 16 | #define LT6911_READ 0xFF 17 | #define LT6911_WRITE 0x00 18 | 19 | void lt6911_enable(); 20 | void lt6911_disable(); 21 | void lt6911_start(); 22 | void lt6911_stop(); 23 | void lt6911_reset(); 24 | void lt6911_get_hdmi_errer(); 25 | uint8_t lt6911_get_hdmi_res(); 26 | void lt6911_get_hdmi_clk(); 27 | uint8_t lt6911_get_csi_res(); 28 | 29 | #endif // HDMI_H_ -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/libqr/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(qr) 2 | cmake_minimum_required(VERSION 2.6.0) 3 | 4 | set(QR_VERSION "1.0.0") 5 | set(QR_SOVERSION "1") 6 | 7 | set(QR_COMMAND_SOURCES qrcmd.c) 8 | set(QR_LIBRARY_SOURCES 9 | qr.c qrcnv.c qrcnv_bmp.c qrcnv_png.c qrcnv_svg.c qrcnv_tiff.c 10 | ) 11 | set(QR_PUBLIC_HEADERS qr.h) 12 | 13 | set(bindir bin) 14 | set(incdir include) 15 | set(libdir lib) 16 | 17 | set(CMAKE_SKIP_BUILD_RPATH OFF) 18 | set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF) 19 | set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${libdir}") 20 | set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/${libdir}") 21 | 22 | find_package(ZLIB) 23 | 24 | add_definitions(-Wall -Wextra) 25 | 26 | include_directories(${ZLIB_INCLUDE_DIRS}) 27 | 28 | add_executable(qrcmd ${QR_COMMAND_SOURCES}) 29 | add_executable(qrcmd_multi ${QR_COMMAND_SOURCES}) 30 | 31 | add_library(libqr_shared SHARED ${QR_LIBRARY_SOURCES}) 32 | add_library(libqr_static STATIC ${QR_LIBRARY_SOURCES}) 33 | 34 | target_link_libraries(qrcmd libqr_shared) 35 | target_link_libraries(qrcmd_multi libqr_shared) 36 | target_link_libraries(libqr_shared m ${ZLIB_LIBRARIES}) 37 | 38 | set_target_properties(qrcmd PROPERTIES 39 | OUTPUT_NAME qr 40 | ) 41 | set_target_properties(qrcmd_multi PROPERTIES 42 | OUTPUT_NAME qrs 43 | COMPILE_FLAGS -DQRCMD_STRUCTURED_APPEND 44 | ) 45 | set_target_properties(libqr_shared PROPERTIES 46 | OUTPUT_NAME qr 47 | VERSION ${QR_VERSION} 48 | SOVERSION ${QR_SOVERSION} 49 | ) 50 | set_target_properties(libqr_static PROPERTIES 51 | OUTPUT_NAME qr 52 | ) 53 | 54 | install(TARGETS qrcmd qrcmd_multi libqr_shared libqr_static 55 | RUNTIME DESTINATION ${bindir} 56 | LIBRARY DESTINATION ${libdir} 57 | ARCHIVE DESTINATION ${libdir} 58 | ) 59 | install(FILES ${QR_PUBLIC_HEADERS} DESTINATION ${incdir}) 60 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/libqr/LICENSE: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/libqr/README: -------------------------------------------------------------------------------- 1 | This is a C library and a command line tool to make a QR Code. 2 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/libqr/TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | - test on Win32 3 | - test on Cygwin (if I could) 4 | - write tests 5 | - write manpage of qr(1), qrs(1) and libqr(3) 6 | - localization support by ICU4C 7 | - pkg-config (.pc file) support 8 | - rcfile support for command line tool 9 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/libqr/crc.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is taken from: 3 | * PNG (Portable Network Graphics) Specification, Version 1.1 4 | * 15. Appendix: Sample CRC Code 5 | * http://www.libpng.org/pub/png/spec/1.1/PNG-CRCAppendix.html 6 | */ 7 | 8 | #ifdef uint32_t 9 | typedef uint32_t crc_t; 10 | #else 11 | typedef unsigned long crc_t; 12 | #endif 13 | 14 | /* Table of CRCs of all 8-bit messages. */ 15 | static crc_t crc_table[256]; 16 | 17 | /* Flag: has the table been computed? Initially false. */ 18 | static int crc_table_computed = 0; 19 | 20 | /* Make the table for a fast CRC. */ 21 | static void make_crc_table(void) 22 | { 23 | crc_t c; 24 | int n, k; 25 | 26 | for (n = 0; n < 256; n++) { 27 | c = (crc_t) n; 28 | for (k = 0; k < 8; k++) { 29 | if (c & 1) { 30 | c = 0xedb88320L ^ (c >> 1); 31 | } else { 32 | c = c >> 1; 33 | } 34 | } 35 | crc_table[n] = c; 36 | } 37 | crc_table_computed = 1; 38 | } 39 | 40 | /* Update a running CRC with the bytes buf[0..len-1]--the CRC 41 | should be initialized to all 1's, and the transmitted value 42 | is the 1's complement of the final running CRC (see the 43 | crc() routine below)). */ 44 | 45 | static crc_t update_crc(crc_t crc, const unsigned char *buf, int len) 46 | { 47 | crc_t c = crc; 48 | int n; 49 | 50 | if (!crc_table_computed) { 51 | make_crc_table(); 52 | } 53 | for (n = 0; n < len; n++) { 54 | c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); 55 | } 56 | return c; 57 | } 58 | 59 | /* Return the CRC of the bytes buf[0..len-1]. */ 60 | static crc_t crc(const unsigned char *buf, int len) 61 | { 62 | return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL; 63 | } 64 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/oled_ui/oled_ui.h: -------------------------------------------------------------------------------- 1 | #ifndef OLED_UI_H_ 2 | #define OLED_UI_H_ 3 | #include "config.h" 4 | #include "oled_ctrl.h" 5 | 6 | void kvm_main_ui_disp(uint8_t first_disp, uint8_t subpage_changed); 7 | void kvm_wifi_config_ui_disp(uint8_t first_disp, uint8_t subpage_changed); 8 | void oled_auto_sleep_time_update(void); 9 | void oled_auto_sleep(void); 10 | void kvm_show_UE(void); 11 | 12 | #endif // OLED_UI_H_ -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/system_ctrl/system_ctrl.h: -------------------------------------------------------------------------------- 1 | #ifndef SYSTEM_CTRL_H_ 2 | #define SYSTEM_CTRL_H_ 3 | #include "config.h" 4 | 5 | void gen_hostapd_conf(char* ap_ssid); 6 | void gen_udhcpd_conf(); 7 | void gen_dnsmasq_conf(); 8 | uint8_t sta_connect_ap(void); 9 | uint8_t ssid_pass_ok(void); 10 | uint8_t wifi_connected(void); 11 | void kvm_start_wifi_config_process(void); 12 | void kvm_wifi_web_config_process(); 13 | void kvm_wifi_config_process(); 14 | uint8_t kvm_reset_password(void); 15 | 16 | #endif // SYSTEM_CTRL_H_ -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/system_init/system_init.h: -------------------------------------------------------------------------------- 1 | #ifndef SYSTEM_INIT_H_ 2 | #define SYSTEM_INIT_H_ 3 | #include "config.h" 4 | 5 | void new_img_init(void); 6 | void new_app_init(void); 7 | void Production_testing_patch(void); 8 | 9 | #endif // SYSTEM_INIT_H_ 10 | -------------------------------------------------------------------------------- /support/sg2002/kvm_system/main/lib/system_state/system_state.h: -------------------------------------------------------------------------------- 1 | #ifndef SYSTEM_STATE_H_ 2 | #define SYSTEM_STATE_H_ 3 | #include "config.h" 4 | 5 | enum ip_addr_t 6 | { 7 | ETH_IP=1, WiFi_IP, Tailscale_IP, RNDIS_IP, ETH_ROUTE, WiFi_ROUTE, NULL_IP 8 | }; 9 | 10 | #define NIC_STATE_UP 1 11 | #define NIC_STATE_DOWN 0 12 | #define NIC_STATE_RUNNING 2 13 | #define NIC_STATE_UNKNOWN -1 14 | #define NIC_STATE_NO_EXIST -2 15 | 16 | #define watchdog_mode_path "/etc/kvm/watchdog" 17 | #define watchdog_temp_path "/tmp/watchdog" 18 | #define watchdog_file "/tmp/nanokvm_wd" 19 | 20 | // net_port 21 | int get_ip_addr(ip_addr_t ip_type); 22 | int chack_net_state(ip_addr_t use_ip_type); 23 | int get_ping_allow_state(void); 24 | void patch_eth_wifi(void); 25 | int kvm_wifi_exist(void); 26 | void kvm_update_usb_state(void); 27 | void kvm_update_hdmi_state(void); 28 | void kvm_update_stream_fps(void); 29 | void kvm_update_stream_type(void); 30 | void kvm_update_stream_qlty(void); 31 | void kvm_update_hdmi_res(void); 32 | void kvm_update_eth_state(void); 33 | void kvm_update_wifi_state(void); 34 | void kvm_update_rndis_state(void); 35 | void kvm_update_tailscale_state(void); 36 | uint8_t ion_free_space(void); 37 | int get_nic_state(const char* interface_name); 38 | int create_temp_watchdog(void); 39 | void rm_temp_watchdog(void); 40 | void auto_remove_temp_watchdog(void); 41 | uint8_t watchdog_sf_is_open(void); 42 | int check_watchdog(); 43 | 44 | #endif // SYSTEM_STATE_H_ 45 | -------------------------------------------------------------------------------- /support/sg2002/kvm_vision_test/main/Kconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/support/sg2002/kvm_vision_test/main/Kconfig -------------------------------------------------------------------------------- /support/sg2002/kvm_vision_test/main/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "kvm_vision.h" 2 | #include "maix_basic.hpp" 3 | 4 | using namespace maix; 5 | using namespace maix::sys; 6 | 7 | // #define NOT_GET_IMG 8 | 9 | int main(int argc, char* argv[]) 10 | { 11 | uint64_t __attribute__((unused)) start_time; 12 | // Catch SIGINT signal(e.g. Ctrl + C), and set exit flag to true. 13 | signal(SIGINT, [](int sig){ app::set_exit_flag(true); 14 | log::info("========================\n"); 15 | }); 16 | 17 | kvmv_hdmi_control(0); 18 | kvmv_hdmi_control(1); 19 | 20 | kvmv_init(0); 21 | set_h264_gop(30); 22 | 23 | uint16_t get_fream_count = 0; 24 | 25 | while(!app::need_exit()){ 26 | #ifdef NOT_GET_IMG 27 | printf("NOT_GET_IMG ...\n"); 28 | time::sleep_ms(1000); 29 | #else 30 | uint8_t* p_kvmv_img_data; 31 | uint32_t kvmv_img_data_size; 32 | int ret; 33 | 34 | printf("KVM-Vison Get Fream ...\n"); 35 | 36 | start_time = time::time_ms(); 37 | if(get_fream_count < 1){ 38 | get_fream_count ++; 39 | ret = kvmv_read_img(1920, 1080, 1, 3000, &p_kvmv_img_data, &kvmv_img_data_size); 40 | } else if(get_fream_count >= 1 && get_fream_count < 2){ 41 | get_fream_count ++; 42 | ret = kvmv_read_img(0, 0, 0, 60, &p_kvmv_img_data, &kvmv_img_data_size); 43 | } else { 44 | get_fream_count = 0; 45 | } 46 | 47 | // printf("kvmv_read_img(): %d \r\n", (int)(time::time_ms() - start_time)); 48 | 49 | // ret = kvmv_read_img(1920, 1080, 1, 3000, &p_kvmv_img_data, &kvmv_img_data_size); 50 | // ret = kvmv_read_img(1920, 1080, 0, 60, &p_kvmv_img_data, &kvmv_img_data_size); 51 | 52 | printf("kvmv_read_img ret = %d\n", ret); 53 | 54 | // send... 55 | // if(ret >= 0){ 56 | free_kvmv_data(&p_kvmv_img_data); 57 | // } 58 | #endif 59 | } 60 | kvmv_deinit(); 61 | } -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | VITE_SERVER_IP=192.168.0.65 2 | VITE_SERVER_PORT=80 3 | VITE_WITH_CREDENTIALS=false 4 | -------------------------------------------------------------------------------- /web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'prettier', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh'], 13 | rules: { 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | *.hbs 2 | -------------------------------------------------------------------------------- /web/.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: none 3 | printWidth: 100 4 | tabWidth: 2 5 | bracketSpacing: true 6 | importOrder: 7 | - ^(react/(.*)$)|^(react$) 8 | - ^(next/(.*)$)|^(next$) 9 | - 10 | - '' 11 | - ^types$ 12 | - ^@/assets(.*)$ 13 | - ^@/api/(.*)$ 14 | - ^@/i18n/(.*)$ 15 | - ^@/types$ 16 | - ^@/lib/(.*)$ 17 | - ^@/jotai/(.*)$ 18 | - ^@/hooks/(.*)$ 19 | - ^@/components/(.*)$ 20 | - ^@/pages/(.*)$ 21 | - ^@/styles/(.*)$ 22 | - '' 23 | - '^[./]' 24 | importOrderSeparation: false 25 | importOrderSortSpecifiers: true 26 | importOrderBuiltinModulesToTop: true 27 | importOrderParserPlugins: 28 | - typescript 29 | - jsx 30 | - tsx 31 | - decorators-legacy 32 | importOrderMergeDuplicateImports: true 33 | importOrderCombineTypeAndValueImports: true 34 | plugins: 35 | - '@ianvs/prettier-plugin-sort-imports' 36 | - prettier-plugin-tailwindcss 37 | -------------------------------------------------------------------------------- /web/README_JA.md: -------------------------------------------------------------------------------- 1 | # NanoKVM フロントエンド 2 | 3 | これは NanoKVM のウェブプロジェクトです。詳細なドキュメントについては、[Wiki](https://wiki.sipeed.com/nanokvm) を参照してください。 4 | 5 | ## 構造 6 | 7 | ```shell 8 | src 9 | ├── api // バックエンド API 10 | ├── assets // 静的リソース 11 | ├── components // 公共コンポーネント 12 | ├── i18n // 言語リソース 13 | ├── jotai // グローバル jotai 変数 14 | ├── lib // ユーティリティライブラリ 15 | ├── pages // ウェブページ 16 | │ ├── auth // ログインとパスワード 17 | │ ├── desktop // リモートデスクトップ 18 | │ └── terminal // ウェブターミナル 19 | ├── router.tsx // ルーター 20 | └── types // 型定義 21 | ``` 22 | 23 | ## ローカル開発 24 | 25 | > 開発には SSH が必要です。Web 設定で有効にすることができます: `設定 > SSH` 26 | 27 | CORS 制限のため、ローカル開発中は認証を無効にする必要があります。 28 | 29 | 認証機能を開発するには、プロジェクトをビルドして NanoKVM でテストする必要があります。 30 | 31 | 1. SSH を介して NanoKVM にログインします:`ssh root@your-nanokvm-ip`(デフォルトのパスワードは root です)。 32 | 2. 設定ファイル `/etc/kvm/server.yaml/` を開き、`authentication: disable` を追加します。⚠️注意:このオプションはすべての認証を無効にし、本番環境では有効にしないでください! 33 | 3. サービスを再起動します:`/etc/init.d/S95nanokvm restart`。 34 | 4. `.env.development` ファイルを編集し、`VITE_SERVER_IP` を NanoKVM の IP アドレスに変更します。 35 | 5. `pnpm dev` を実行してサーバーを起動し、ブラウザで http://localhost:3001/ にアクセスします。 36 | 37 | 38 | 開発中のアクセス問題を避けるため、ブラウザのキャッシュを無効にすることをお勧めします: 39 | 40 | 1. ブラウザの開発者ツールを開きます; 41 | 2. `Network` タブに移動します; 42 | 3. `Disable cache` オプションをチェックします; 43 | 4. ページをリフレッシュします。 44 | 45 | ## デプロイ 46 | 47 | ビルド: 48 | 49 | ```shell 50 | cd web 51 | pnpm install 52 | pnpm build 53 | ``` 54 | 55 | 1. コンパイルが完了すると、`dist` フォルダが生成されます。 56 | 2. フォルダの名前を `web` に変更します。 57 | 3. `web` を NanoKVM の `/kvmapp/server/` にアップロードします。 58 | 4. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。 59 | 60 | Tips: 61 | 62 | 1. ファイルのアップロードには SSH が必要です。Web 設定で有効にすることができます: `設定 > SSH` 63 | 2. ブラウザに古いバージョンのキャッシュが残っている可能性があります。ページが開かない場合は、強制リフレッシュまたはキャッシュのクリアを試してください。 64 | -------------------------------------------------------------------------------- /web/README_ZH.md: -------------------------------------------------------------------------------- 1 | # NanoKVM 前端页面 2 | 3 | NanoKVM 前端页面的代码。更多文档请参考 [Wiki](https://wiki.sipeed.com/nanokvm) 。 4 | 5 | ## 目录结构 6 | 7 | ```shell 8 | src 9 | ├── api // 后端接口 10 | ├── assets // 资源文件 11 | ├── components // 公共组件 12 | ├── i18n // 多语言 13 | ├── jotai // 全局 jotai 变量 14 | ├── lib // lib 15 | ├── pages // 页面 16 | │ ├── auth // 鉴权页面 17 | │ ├── desktop // 远程桌面 18 | │ └── terminal // 终端 19 | ├── router.tsx // 路由 20 | └── types // 类型定义 21 | ``` 22 | 23 | ## 本地开发 24 | 25 | > 开发需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用。 26 | 27 | 由于 CORS 的限制,在本地开发时,需要关闭鉴权功能。 28 | 29 | 如果想要开发鉴权相关的功能,需要编译后在 NanoKVM 中进行测试。 30 | 31 | 1. 通过 SSH 登录到 NanoKVM:`ssh root@your-nanokvm-ip`(默认密码为 root); 32 | 2. 修改配置文件 `/etc/kvm/server.yaml`,添加一行 `authentication: disable`。⚠️注意:该选项会禁用所有鉴权功能,生产环境请勿开启该选项! 33 | 3. 执行 `/etc/init.d/S95nanokvm restart` 重启服务。 34 | 4. 编辑 `.env.development` 文件,将 `VITE_SERVER_IP` 修改为你的 NanoKVM IP 地址。 35 | 5. 执行 `pnpm dev` 启动服务,然后在浏览器中访问 http://localhost:3001/ 。 36 | 37 | 38 | 建议在浏览器中禁用缓存,防止在开发过程中出现无法访问的情况。 39 | 40 | 1. 打开开发者工具; 41 | 2. 点击 `Network` 选项卡; 42 | 3. 勾选 `Disable cache` 选项; 43 | 4. 刷新页面。 44 | 45 | ## 部署 46 | 47 | 编译: 48 | 49 | ```shell 50 | cd web 51 | pnpm install 52 | pnpm build 53 | ``` 54 | 55 | 1. 编译完成后会生成 `dist` 文件夹; 56 | 2. 将该文件夹重命名为 `web`; 57 | 3. 将 `web` 文件夹上传到 NanoKVM 的 `/kvmapp/server/` 目录下; 58 | 4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。 59 | 60 | 61 | 注意: 62 | 63 | 1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用。 64 | 2. 更新 web 目录后,浏览器可能会有缓存。如果遇到打不开页面的情况,请强制刷新或清空缓存。 65 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NanoKVM 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web/public/sipeed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipeed/NanoKVM/34746afea3bc0a770de8dc6e964f9936b31af363/web/public/sipeed.ico -------------------------------------------------------------------------------- /web/src/api/application.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // get application version 4 | export function getVersion() { 5 | return http.get('/api/application/version'); 6 | } 7 | 8 | // update application to latest version 9 | export function update() { 10 | return http.request({ 11 | method: 'post', 12 | url: '/api/application/update', 13 | timeout: 15 * 60 * 1000 14 | }); 15 | } 16 | 17 | // enable/disable preview updates 18 | export function setPreviewUpdates(enable: boolean) { 19 | const data = { 20 | enable 21 | }; 22 | return http.post('/api/application/preview', data); 23 | } 24 | 25 | // get preview updates state 26 | export function getPreviewUpdates() { 27 | return http.get('/api/application/preview'); 28 | } 29 | -------------------------------------------------------------------------------- /web/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http'; 2 | 3 | export function login(username: string, password: string) { 4 | const data = { 5 | username, 6 | password 7 | }; 8 | return http.post('/api/auth/login', data); 9 | } 10 | 11 | export function logout() { 12 | return http.post('/api/auth/logout'); 13 | } 14 | 15 | export function getAccount() { 16 | return http.get('/api/auth/account'); 17 | } 18 | 19 | export function changePassword(username: string, password: string) { 20 | const data = { 21 | username, 22 | password 23 | }; 24 | return http.post('/api/auth/password', data); 25 | } 26 | 27 | export function isPasswordUpdated() { 28 | return http.get('/api/auth/password'); 29 | } 30 | -------------------------------------------------------------------------------- /web/src/api/download.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // Download image 4 | export function downloadImage(file?: string) { 5 | const data = { 6 | file: file ? file : '' 7 | }; 8 | return http.post('/api/download/image', data); 9 | } 10 | 11 | export function statusImage() { 12 | return http.get('/api/download/image/status'); 13 | } 14 | 15 | export function imageEnabled() { 16 | return http.get('/api/download/image/enabled'); 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/extensions/tailscale.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // install tailscale 4 | export function install() { 5 | return http.post('/api/extensions/tailscale/install'); 6 | } 7 | 8 | // uninstall tailscale 9 | export function uninstall() { 10 | return http.post('/api/extensions/tailscale/uninstall'); 11 | } 12 | 13 | // get tailscale status 14 | export function getStatus() { 15 | return http.get('/api/extensions/tailscale/status'); 16 | } 17 | 18 | // start tailscale 19 | export function start() { 20 | return http.post('/api/extensions/tailscale/start'); 21 | } 22 | 23 | // restart tailscale 24 | export function restart() { 25 | return http.post('/api/extensions/tailscale/restart'); 26 | } 27 | 28 | // stop tailscale 29 | export function stop() { 30 | return http.post('/api/extensions/tailscale/stop'); 31 | } 32 | 33 | // run tailscale up 34 | export function up() { 35 | return http.post('/api/extensions/tailscale/up'); 36 | } 37 | 38 | // run tailscale down 39 | export function down() { 40 | return http.post('/api/extensions/tailscale/down'); 41 | } 42 | 43 | // login tailscale 44 | export function login() { 45 | return http.post('/api/extensions/tailscale/login'); 46 | } 47 | 48 | // logout tailscale 49 | export function logout() { 50 | return http.post('/api/extensions/tailscale/logout'); 51 | } 52 | -------------------------------------------------------------------------------- /web/src/api/hid.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // paste 4 | export function paste(content: string) { 5 | return http.post('/api/hid/paste', { content }); 6 | } 7 | 8 | // reset hid 9 | export function reset() { 10 | return http.post('/api/hid/reset'); 11 | } 12 | 13 | // get hid mode 14 | export function getHidMode() { 15 | return http.get('/api/hid/mode'); 16 | } 17 | 18 | // set hid mode 19 | export function setHidMode(mode: string) { 20 | const data = { 21 | mode 22 | }; 23 | return http.post('/api/hid/mode', data); 24 | } 25 | -------------------------------------------------------------------------------- /web/src/api/network.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // wake on lan 4 | export function wol(mac: string) { 5 | const data = { 6 | mac 7 | }; 8 | return http.post('/api/network/wol', data); 9 | } 10 | 11 | // get wake-on-lan macs history 12 | export function getWolMacs() { 13 | return http.get('/api/network/wol/mac'); 14 | } 15 | 16 | export function deleteWolMac(mac: string) { 17 | return http.request({ 18 | method: 'delete', 19 | url: '/api/network/wol/mac', 20 | data: { mac } 21 | }); 22 | } 23 | 24 | // set Mac name 25 | export function setWolMacName(mac: string, name: string) { 26 | return http.post('/api/network/wol/mac/name', { mac, name }); 27 | } 28 | 29 | // get wifi information 30 | export function getWiFi() { 31 | return http.get('/api/network/wifi'); 32 | } 33 | 34 | // connect wifi 35 | export function connectWifi(ssid: string, password: string) { 36 | const data = { 37 | ssid, 38 | password 39 | }; 40 | return http.post('/api/network/wifi', data); 41 | } 42 | -------------------------------------------------------------------------------- /web/src/api/script.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | export function uploadScript(formData: FormData) { 4 | return http.request({ 5 | url: '/api/vm/script/upload', 6 | method: 'post', 7 | headers: { 8 | 'Content-Type': 'multipart/form-data' 9 | }, 10 | data: formData 11 | }); 12 | } 13 | 14 | export function runScript(name: string, type: string) { 15 | return http.post('/api/vm/script/run', { name, type }); 16 | } 17 | 18 | export function getScripts() { 19 | return http.get('/api/vm/script'); 20 | } 21 | 22 | export function deleteScript(name: string) { 23 | return http.request({ 24 | url: '/api/vm/script', 25 | method: 'delete', 26 | data: { name } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /web/src/api/storage.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // get image list 4 | export function getImages() { 5 | return http.get('/api/storage/image'); 6 | } 7 | 8 | // get mounted image 9 | export function getMountedImage() { 10 | return http.get('/api/storage/image/mounted'); 11 | } 12 | 13 | // mount/unmount image 14 | export function mountImage(file?: string, cdrom?: boolean) { 15 | const data = { 16 | file: file ? file : '', 17 | cdrom: cdrom 18 | }; 19 | return http.post('/api/storage/image/mount', data); 20 | } 21 | 22 | // get CD-ROM flag 23 | export function getCdRom() { 24 | return http.get('/api/storage/cdrom'); 25 | } 26 | -------------------------------------------------------------------------------- /web/src/api/stream.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // enable/disable frame detect 4 | export function updateFrameDetect(enabled: boolean) { 5 | const data = { 6 | enabled 7 | }; 8 | return http.post('/api/stream/mjpeg/detect', data); 9 | } 10 | 11 | // pause frame detect for a while (prevent a black screen when opening the page for the first time) 12 | export function stopFrameDetect(duration: number) { 13 | const data = { 14 | duration 15 | }; 16 | return http.post('/api/stream/mjpeg/detect/stop', data); 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/virtual-device.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/lib/http.ts'; 2 | 3 | // get virtual devices status 4 | export function getVirtualDevice() { 5 | return http.get('/api/vm/device/virtual'); 6 | } 7 | 8 | // mount/unmount virtual device 9 | export function updateVirtualDevice(device: string) { 10 | const data = { 11 | device 12 | }; 13 | 14 | return http.post('/api/vm/device/virtual', data); 15 | } 16 | -------------------------------------------------------------------------------- /web/src/assets/images/monitor-x.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/assets/images/tailscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body { 6 | padding: 0; 7 | margin: 0; 8 | background: #000; 9 | } 10 | 11 | ::-webkit-scrollbar { 12 | width: 10px; 13 | } 14 | ::-webkit-scrollbar-track { 15 | background: #000000; 16 | } 17 | ::-webkit-scrollbar-thumb { 18 | background: #555; 19 | border-radius: 5px; 20 | } 21 | ::-webkit-scrollbar-thumb:hover { 22 | background: #444; 23 | } 24 | 25 | .spin { 26 | animation: spin 1s linear; 27 | } 28 | 29 | @keyframes spin { 30 | from { 31 | transform: rotate(0deg); 32 | } 33 | to { 34 | transform: rotate(360deg); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/components/auth.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | 4 | import { existToken } from '@/lib/cookie.ts'; 5 | 6 | export const ProtectedRoute = ({ children }: { children: ReactNode }) => { 7 | const hasToken = existToken(); 8 | 9 | if (!hasToken) { 10 | return ; 11 | } 12 | 13 | return children; 14 | }; 15 | -------------------------------------------------------------------------------- /web/src/components/head.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAtom } from 'jotai'; 3 | import { Helmet, HelmetData } from 'react-helmet-async'; 4 | 5 | import { getWebTitle } from '@/api/vm.ts'; 6 | import { existToken } from '@/lib/cookie.ts'; 7 | import { webTitleAtom } from '@/jotai/settings.ts'; 8 | 9 | type HeadProps = { 10 | title?: string; 11 | description?: string; 12 | }; 13 | 14 | const helmetData = new HelmetData({}); 15 | 16 | export const Head = ({ title = '', description = '' }: HeadProps = {}) => { 17 | const [webTitle, setWebTitle] = useAtom(webTitleAtom); 18 | 19 | useEffect(() => { 20 | if (!existToken()) return; 21 | 22 | getWebTitle().then((rsp) => { 23 | if (rsp.data?.title) { 24 | setWebTitle(rsp.data.title); 25 | } 26 | }); 27 | }, []); 28 | 29 | return ( 30 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /web/src/components/icons/tailscale.tsx: -------------------------------------------------------------------------------- 1 | import icon from '@/assets/images/tailscale.svg'; 2 | 3 | export const Tailscale = () => { 4 | return tailscale; 5 | }; 6 | -------------------------------------------------------------------------------- /web/src/components/main-error.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | export const MainError = () => { 5 | const { t } = useTranslation(); 6 | 7 | return ( 8 |
12 |

{t('error.title')}

13 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /web/src/components/menu-item.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from 'react'; 2 | import { Popover, Tooltip } from 'antd'; 3 | import { useMediaQuery } from 'react-responsive'; 4 | 5 | type MenuItemProps = { 6 | title: string; 7 | icon: ReactNode; 8 | content: ReactNode; 9 | className?: string; 10 | fresh?: boolean; 11 | onOpenChange?: (open: boolean) => void; 12 | }; 13 | 14 | export const MenuItem = ({ 15 | title, 16 | icon, 17 | content, 18 | className, 19 | fresh, 20 | onOpenChange 21 | }: MenuItemProps) => { 22 | const isBigScreen = useMediaQuery({ minWidth: 640 }); 23 | 24 | const [isPopoverOpen, setIsPopoverOpen] = useState(false); 25 | const [isTooltipOpen, setIsTooltipOpen] = useState(false); 26 | 27 | function togglePopover(open: boolean) { 28 | setIsTooltipOpen(false); 29 | setIsPopoverOpen(open); 30 | 31 | if (onOpenChange) { 32 | onOpenChange(open); 33 | } 34 | } 35 | 36 | function toggleTooltip(open: boolean) { 37 | if (isPopoverOpen) { 38 | return; 39 | } 40 | setIsTooltipOpen(open); 41 | } 42 | 43 | return ( 44 | 53 | 60 |
67 | {icon} 68 |
69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /web/src/components/root.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | export const Root = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /web/src/i18n/README.md: -------------------------------------------------------------------------------- 1 | # How to add a language 2 | 3 | 1. Add a language file in i18n/locales folder (for example: en.ts). 4 | 2. Add language key and name in i18n/languages.ts (for example: { key: 'en', name: 'English' }). 5 | -------------------------------------------------------------------------------- /web/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import type { Resource } from 'i18next'; 3 | import { initReactI18next } from 'react-i18next'; 4 | 5 | import { getLanguage } from '@/lib/localstorage.ts'; 6 | 7 | function getResources(): Resource { 8 | const resources: Resource = {}; 9 | 10 | const modules: Record = import.meta.glob('./locales/*.ts', { eager: true }); 11 | 12 | for (const path in modules) { 13 | const moduleName = path.split('/').pop()?.replace('.ts', ''); 14 | if (moduleName) { 15 | resources[moduleName] = modules[path].default; 16 | } 17 | } 18 | 19 | return resources; 20 | } 21 | 22 | function getCurrentLanguage(): string { 23 | const languages = Object.keys(resources); 24 | 25 | const cookieLng = getLanguage(); 26 | if (cookieLng && languages.includes(cookieLng)) { 27 | return cookieLng; 28 | } 29 | 30 | const navigatorLng = navigator.language.split('-')[0]; 31 | if (languages.includes(navigatorLng)) { 32 | return navigatorLng; 33 | } 34 | 35 | return 'en'; 36 | } 37 | 38 | const resources = getResources(); 39 | const lng = getCurrentLanguage(); 40 | 41 | i18n 42 | .use(initReactI18next) 43 | .init({ 44 | resources, 45 | lng, 46 | fallbackLng: 'en', 47 | interpolation: { 48 | escapeValue: false 49 | } 50 | }) 51 | .then(); 52 | 53 | export default i18n; 54 | -------------------------------------------------------------------------------- /web/src/i18n/languages.ts: -------------------------------------------------------------------------------- 1 | const languages = [ 2 | { key: 'nl', name: 'Nederlands' }, 3 | { key: 'da', name: 'Danish' }, 4 | { key: 'de', name: 'Deutsch' }, 5 | { key: 'en', name: 'English' }, 6 | { key: 'es', name: 'Español' }, 7 | { key: 'fr', name: 'Français' }, 8 | { key: 'id', name: 'Indonesia' }, 9 | { key: 'it', name: 'Italian' }, 10 | { key: 'pl', name: 'Polski' }, 11 | { key: 'ru', name: 'Русский' }, 12 | { key: 'ko', name: '한국어' }, 13 | { key: 'zh', name: '简体中文' }, 14 | { key: 'zh_tw', name: '繁體中文' }, 15 | { key: 'hu', name: 'Magyar' }, 16 | { key: 'vi', name: 'Tiếng Việt' }, 17 | { key: 'ja', name: '日本語' }, 18 | { key: 'cz', name: 'Česky' }, 19 | { key: 'uk', name: 'Українська' }, 20 | { key: 'nb', name: 'Norsk, bokmål' }, 21 | { key: 'th', name: 'ภาษาไทย' } 22 | ]; 23 | 24 | languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })); 25 | 26 | export default languages; 27 | -------------------------------------------------------------------------------- /web/src/jotai/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | // is the keyboard enabled (Disable keyboard events when input is required) 4 | export const isKeyboardEnableAtom = atom(true); 5 | 6 | // is the virtual keyboard opened 7 | export const isKeyboardOpenAtom = atom(false); 8 | -------------------------------------------------------------------------------- /web/src/jotai/mouse.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | // mouse cursor style 4 | export const mouseStyleAtom = atom('cursor-default'); 5 | 6 | // mouse mode: absolute or relative 7 | export const mouseModeAtom = atom('absolute'); 8 | 9 | // mouse scroll interval (unit: ms) 10 | export const scrollIntervalAtom = atom(0); 11 | 12 | // hid mode: normal or hid-only 13 | export const hidModeAtom = atom<'normal' | 'hid-only'>('normal'); 14 | -------------------------------------------------------------------------------- /web/src/jotai/screen.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | import { Resolution } from '@/types'; 4 | 5 | export const isHdmiEnabledAtom = atom(true); 6 | 7 | // video mode 8 | // direct: stream H.264 over HTTP 9 | // h264: stream H.264 over WebRTC 10 | // mjpeg: stream JPEG over HTTP 11 | export const videoModeAtom = atom(''); 12 | 13 | // browser screen resolution 14 | export const resolutionAtom = atom(null); 15 | -------------------------------------------------------------------------------- /web/src/jotai/settings.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | // menu bar disabled items 4 | export const menuDisabledItemsAtom = atom([]); 5 | 6 | // web title 7 | export const webTitleAtom = atom(''); 8 | -------------------------------------------------------------------------------- /web/src/lib/cookie.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | const COOKIE_TOKEN_KEY = 'nano-kvm-token'; 4 | 5 | export function existToken() { 6 | const token = Cookies.get(COOKIE_TOKEN_KEY); 7 | return !!token; 8 | } 9 | 10 | export function getToken() { 11 | const token = Cookies.get(COOKIE_TOKEN_KEY); 12 | if (!token) return null; 13 | 14 | return token; 15 | } 16 | 17 | export function setToken(token: string) { 18 | Cookies.set(COOKIE_TOKEN_KEY, token, { expires: 30 }); 19 | } 20 | 21 | export function removeToken() { 22 | Cookies.remove(COOKIE_TOKEN_KEY); 23 | } 24 | -------------------------------------------------------------------------------- /web/src/lib/encrypt.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | // This key is only used to prevent the data from being transmitted in plaintext. 4 | const SECRET_KEY = 'nanokvm-sipeed-2024'; 5 | 6 | export function encrypt(data: string) { 7 | const dataEncrypt = CryptoJS.AES.encrypt(data, SECRET_KEY).toString(); 8 | return encodeURIComponent(dataEncrypt); 9 | } 10 | -------------------------------------------------------------------------------- /web/src/lib/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | 3 | import { removeToken } from '@/lib/cookie.ts'; 4 | import { getBaseUrl } from '@/lib/service.ts'; 5 | 6 | type Response = { 7 | code: number; 8 | msg: string; 9 | data: any; 10 | }; 11 | 12 | class Http { 13 | private instance: AxiosInstance; 14 | 15 | constructor() { 16 | const baseURL = getBaseUrl('http'); 17 | const withCredentials = (import.meta.env.VITE_WITH_CREDENTIALS as string) !== 'false'; 18 | 19 | this.instance = axios.create({ 20 | baseURL, 21 | withCredentials, 22 | timeout: 60 * 1000 23 | }); 24 | 25 | this.setInterceptors(); 26 | } 27 | 28 | private setInterceptors() { 29 | this.instance.interceptors.request.use((config) => { 30 | if (config.headers) { 31 | config.headers.Accept = 'application/json'; 32 | } 33 | 34 | return config; 35 | }); 36 | 37 | this.instance.interceptors.response.use( 38 | (response) => { 39 | return response.data; 40 | }, 41 | (error) => { 42 | console.log(error); 43 | const code = error.response?.status; 44 | if (code === 401) { 45 | removeToken(); 46 | window.location.reload(); 47 | } 48 | return Promise.reject(error); 49 | } 50 | ); 51 | } 52 | 53 | public get(url: string, params?: any): Promise { 54 | return this.instance.request({ 55 | method: 'get', 56 | url, 57 | params 58 | }); 59 | } 60 | 61 | public post(url: string, data?: any): Promise { 62 | return this.instance.request({ 63 | method: 'post', 64 | url, 65 | data 66 | }); 67 | } 68 | 69 | public request(config: AxiosRequestConfig): Promise { 70 | return this.instance.request(config); 71 | } 72 | } 73 | 74 | export const http = new Http(); 75 | -------------------------------------------------------------------------------- /web/src/lib/service.ts: -------------------------------------------------------------------------------- 1 | export function getHostname(): string { 2 | const ip = import.meta.env.VITE_SERVER_IP as string; 3 | return ip ? ip : window.location.hostname; 4 | } 5 | 6 | export function getPort(): string { 7 | const port = import.meta.env.VITE_SERVER_PORT as string; 8 | return port ? port : window.location.port; 9 | } 10 | 11 | export function getBaseUrl(type: 'http' | 'ws'): string { 12 | let protocol = window.location.protocol; 13 | if (type === 'ws') { 14 | protocol = protocol === 'https:' ? 'wss:' : 'ws:'; 15 | } 16 | 17 | const hostname = getHostname(); 18 | const port = getPort(); 19 | 20 | return `${protocol}//${hostname}:${port}`; 21 | } 22 | -------------------------------------------------------------------------------- /web/src/lib/websocket.ts: -------------------------------------------------------------------------------- 1 | import { IMessageEvent, w3cwebsocket as W3cWebSocket } from 'websocket'; 2 | 3 | import { getBaseUrl } from '@/lib/service.ts'; 4 | 5 | type Event = (message: IMessageEvent) => void; 6 | 7 | const eventMap: Map = new Map(); 8 | 9 | class WsClient { 10 | private readonly url: string; 11 | private instance: W3cWebSocket; 12 | 13 | constructor() { 14 | this.url = `${getBaseUrl('ws')}/api/ws`; 15 | this.instance = new W3cWebSocket(this.url); 16 | this.setEvents(); 17 | } 18 | 19 | public connect() { 20 | this.close(); 21 | 22 | this.instance = new W3cWebSocket(this.url); 23 | this.setEvents(); 24 | } 25 | 26 | public send(data: number[]) { 27 | if (this.instance.readyState !== W3cWebSocket.OPEN) { 28 | return; 29 | } 30 | 31 | const message = JSON.stringify(data); 32 | this.instance.send(message); 33 | } 34 | 35 | public close() { 36 | if (this.instance.readyState === W3cWebSocket.OPEN) { 37 | this.instance.close(); 38 | } 39 | } 40 | 41 | public register(type: string, fn: (message: IMessageEvent) => void) { 42 | eventMap.set(type, fn); 43 | 44 | this.setEvents(); 45 | } 46 | 47 | public unregister(type: string) { 48 | eventMap.delete(type); 49 | 50 | this.setEvents(); 51 | } 52 | 53 | private setEvents() { 54 | this.instance.onmessage = (message) => { 55 | const data = JSON.parse(message.data as string); 56 | if (!data) return; 57 | 58 | const fn = eventMap.get(data.type); 59 | if (!fn) return; 60 | 61 | fn(message); 62 | }; 63 | } 64 | } 65 | 66 | export const client = new WsClient(); 67 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { ConfigProvider, Spin, theme } from 'antd'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { ErrorBoundary } from 'react-error-boundary'; 5 | import { HelmetProvider } from 'react-helmet-async'; 6 | import { RouterProvider } from 'react-router-dom'; 7 | 8 | import { MainError } from './components/main-error.tsx'; 9 | import { router } from './router'; 10 | 11 | import './i18n'; 12 | import './assets/styles/index.css'; 13 | 14 | const renderApp = () => { 15 | const themeConfig = { 16 | algorithm: theme.darkAlgorithm, 17 | components: { 18 | Collapse: { 19 | headerPadding: 0, 20 | contentPadding: 0 21 | } 22 | } 23 | }; 24 | 25 | return ReactDOM.createRoot(document.getElementById('root')!).render( 26 | 27 | 30 | 31 | 32 | } 33 | > 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | if (import.meta.env.MODE === 'mocked') { 47 | const { worker } = await import('./mocks/browser'); 48 | worker.start().then(() => { 49 | return renderApp(); 50 | }); 51 | } 52 | 53 | renderApp(); 54 | -------------------------------------------------------------------------------- /web/src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser' 2 | import { http, HttpResponse } from 'msw' 3 | 4 | export const handlers = [ 5 | http.post('/api/auth/login', () => { 6 | return HttpResponse.json({ 7 | code: 0, 8 | data: { 9 | token: 'mocked_token', 10 | }, 11 | }) 12 | }), 13 | ] 14 | export const worker = setupWorker(...handlers) 15 | -------------------------------------------------------------------------------- /web/src/pages/auth/login/tips.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, Card, Modal, Typography } from 'antd'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const { Text } = Typography; 6 | 7 | export const Tips = () => { 8 | const { t } = useTranslation(); 9 | const [isModalOpen, setIsModalOpen] = useState(false); 10 | 11 | const showModal = () => { 12 | setIsModalOpen(true); 13 | }; 14 | 15 | const hideModal = () => { 16 | setIsModalOpen(false); 17 | }; 18 | 19 | return ( 20 | <> 21 | 25 | {t('auth.forgetPassword')} 26 | 27 | 28 | 36 | 37 |
38 |
{t('auth.tips.reset1')}
39 | 40 |
41 | {t('auth.tips.reset2')} 42 | 43 | wiki 44 | 45 |
46 | 47 |
    48 |
  • 49 | {t('auth.tips.reset3')} 50 | admin/admin 51 |
  • 52 |
  • 53 | {t('auth.tips.reset4')} 54 | root/root 55 |
  • 56 |
57 |
58 |
59 | 60 |
61 | 64 |
65 |
66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/fullscreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import { MaximizeIcon, MinimizeIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | export const Fullscreen = () => { 7 | const { t } = useTranslation(); 8 | const [isFullscreen, setIsFullscreen] = useState(false); 9 | 10 | useEffect(() => { 11 | function onFullscreenChange() { 12 | setIsFullscreen(!!document.fullscreenElement); 13 | } 14 | onFullscreenChange(); 15 | 16 | document.addEventListener('fullscreenchange', onFullscreenChange); 17 | 18 | return () => { 19 | document.removeEventListener('fullscreenchange', onFullscreenChange); 20 | }; 21 | }, []); 22 | 23 | function handleFullscreen() { 24 | if (!document.fullscreenElement) { 25 | const element = document.documentElement; 26 | element.requestFullscreen(); 27 | } else { 28 | document.exitFullscreen(); 29 | } 30 | } 31 | 32 | return ( 33 | 34 |
38 | {isFullscreen ? : } 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/keyboard/ctrl-alt-del.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { OctagonMinus } from 'lucide-react'; 4 | 5 | import { KeyboardCodes, ModifierCodes } from '@/pages/desktop/keyboard/mappings.ts'; 6 | import { client } from '@/lib/websocket.ts'; 7 | 8 | export const CtrlAltDel = () => { 9 | const { t } = useTranslation(); 10 | 11 | function sendCtrlAltDel() { 12 | const ctrl = ModifierCodes.get('ControlLeft')!; 13 | const alt = ModifierCodes.get('AltLeft')!; 14 | const del = KeyboardCodes.get('Delete')!; 15 | 16 | client.send([1, del, ctrl, 0, alt, 0]); 17 | client.send([1, 0, 0, 0, 0, 0]); 18 | }; 19 | 20 | return ( 21 |
27 | 28 | {t('keyboard.ctrlaltdel')} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/keyboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { KeyboardIcon } from 'lucide-react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { MenuItem } from '@/components/menu-item.tsx'; 5 | 6 | import { CtrlAltDel } from './ctrl-alt-del.tsx'; 7 | import { Paste } from './paste.tsx'; 8 | import { VirtualKeyboard } from './virtual-keyboard.tsx'; 9 | 10 | export const Keyboard = () => { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | } 17 | content={ 18 | <> 19 | 20 | 21 | 22 | 23 | } 24 | /> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useSetAtom } from 'jotai'; 3 | import { KeyboardIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts'; 7 | 8 | export const VirtualKeyboard = () => { 9 | const { t } = useTranslation(); 10 | const setIsKeyboardOpen = useSetAtom(isKeyboardOpenAtom); 11 | 12 | return ( 13 |
setIsKeyboardOpen((o) => !o)} 18 | > 19 | 20 | {t('keyboard.virtual')} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/mouse/cursor.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from 'antd'; 2 | import clsx from 'clsx'; 3 | import { useAtom } from 'jotai'; 4 | import { EyeOffIcon, HandIcon, MousePointerIcon, PlusIcon, TextCursorIcon } from 'lucide-react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | import * as ls from '@/lib/localstorage.ts'; 8 | import { mouseStyleAtom } from '@/jotai/mouse.ts'; 9 | 10 | export const Cursor = () => { 11 | const { t } = useTranslation(); 12 | 13 | const [mouseStyle, setMouseStyle] = useAtom(mouseStyleAtom); 14 | 15 | const mouseStyles = [ 16 | { name: t('mouse.default'), icon: , value: 'cursor-default' }, 17 | { name: t('mouse.grab'), icon: , value: 'cursor-grab' }, 18 | { name: t('mouse.cell'), icon: , value: 'cursor-cell' }, 19 | { name: t('mouse.text'), icon: , value: 'cursor-text' }, 20 | { name: t('mouse.hide'), icon: , value: 'cursor-none' } 21 | ]; 22 | 23 | function updateMouseStyle(style: string) { 24 | setMouseStyle(style); 25 | ls.setMouseStyle(style); 26 | } 27 | 28 | const content = ( 29 | <> 30 | {mouseStyles.map((style) => ( 31 |
updateMouseStyle(style.value)} 38 | > 39 |
{style.icon}
40 | {style.name} 41 |
42 | ))} 43 | 44 | ); 45 | 46 | return ( 47 | 48 |
49 | 50 | {t('mouse.cursor')} 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/mouse/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Divider } from 'antd'; 3 | import { useSetAtom } from 'jotai'; 4 | import { MouseIcon } from 'lucide-react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | import * as ls from '@/lib/localstorage'; 8 | import { mouseModeAtom, mouseStyleAtom, scrollIntervalAtom } from '@/jotai/mouse'; 9 | import { MenuItem } from '@/components/menu-item.tsx'; 10 | 11 | import { Cursor } from './cursor.tsx'; 12 | import { HidMode } from './hid-mode.tsx'; 13 | import { MouseMode } from './mouse-mode.tsx'; 14 | import { ResetHid } from './reset-hid.tsx'; 15 | import { Speed } from './speed.tsx'; 16 | 17 | export const Mouse = () => { 18 | const { t } = useTranslation(); 19 | 20 | const setMouseStyle = useSetAtom(mouseStyleAtom); 21 | const setMouseMode = useSetAtom(mouseModeAtom); 22 | const setScrollInterval = useSetAtom(scrollIntervalAtom); 23 | 24 | useEffect(() => { 25 | const mouseStyle = ls.getMouseStyle(); 26 | if (mouseStyle) { 27 | setMouseStyle(mouseStyle); 28 | } 29 | 30 | const mouseMode = ls.getMouseMode(); 31 | if (mouseMode) { 32 | setMouseMode(mouseMode); 33 | } 34 | 35 | const interval = ls.getMouseScrollInterval(); 36 | if (interval) { 37 | setScrollInterval(interval); 38 | } 39 | }, []); 40 | 41 | const content = ( 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | ); 52 | 53 | return } content={content} />; 54 | }; 55 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/mouse/mouse-mode.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from 'antd'; 2 | import { useAtom } from 'jotai'; 3 | import { CheckIcon, SquareDashedMousePointerIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as ls from '@/lib/localstorage.ts'; 7 | import { client } from '@/lib/websocket.ts'; 8 | import { mouseModeAtom } from '@/jotai/mouse.ts'; 9 | 10 | export const MouseMode = () => { 11 | const { t } = useTranslation(); 12 | 13 | const [mouseMode, setMouseMode] = useAtom(mouseModeAtom); 14 | 15 | const mouseModes = [ 16 | { name: t('mouse.absolute'), value: 'absolute' }, 17 | { name: t('mouse.relative'), value: 'relative' } 18 | ]; 19 | 20 | function updateMouseMode(mode: string) { 21 | setMouseMode(mode); 22 | ls.setMouseMode(mode); 23 | 24 | if (mode === 'relative') { 25 | client.close(); 26 | setTimeout(() => { 27 | client.connect(); 28 | }, 500); 29 | } 30 | } 31 | 32 | const content = ( 33 | <> 34 | {mouseModes.map((mode) => ( 35 |
updateMouseMode(mode.value)} 39 | > 40 |
41 | {mode.value === mouseMode && } 42 |
43 | {mode.name} 44 |
45 | ))} 46 | 47 | ); 48 | 49 | return ( 50 | 51 |
52 | 53 | {t('mouse.mode')} 54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/mouse/reset-hid.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import clsx from 'clsx'; 3 | import { RefreshCwIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/hid.ts'; 7 | import { client } from '@/lib/websocket.ts'; 8 | 9 | export const ResetHid = () => { 10 | const { t } = useTranslation(); 11 | 12 | const [isResetting, setIsResetting] = useState(false); 13 | 14 | function resetHid() { 15 | if (isResetting) return; 16 | setIsResetting(true); 17 | 18 | client.send([1, 0, 0, 0, 0, 0]); 19 | client.close(); 20 | 21 | api.reset().finally(() => { 22 | client.connect(); 23 | setIsResetting(false); 24 | }); 25 | } 26 | 27 | return ( 28 |
32 | 33 | {t('mouse.resetHid')} 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/mouse/speed.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Popover, Slider } from 'antd'; 3 | import { useAtom } from 'jotai'; 4 | import { GaugeIcon } from 'lucide-react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | import * as storage from '@/lib/localstorage.ts'; 8 | import { scrollIntervalAtom } from '@/jotai/mouse.ts'; 9 | 10 | const MAX_INTERVAL = 300; 11 | 12 | export const Speed = () => { 13 | const { t } = useTranslation(); 14 | 15 | const [scrollInterval, setScrollInterval] = useAtom(scrollIntervalAtom); 16 | 17 | const [scrollSpeed, setScrollSpeed] = useState(100); 18 | 19 | useEffect(() => { 20 | const speed = interval2Speed(scrollInterval); 21 | setScrollSpeed(speed); 22 | }, [scrollInterval]); 23 | 24 | function update(speed: number): void { 25 | const interval = speed2Interval(speed); 26 | setScrollInterval(interval); 27 | storage.setMouseScrollInterval(interval); 28 | } 29 | 30 | function interval2Speed(interval: number) { 31 | if (interval === MAX_INTERVAL) { 32 | return 0; 33 | } 34 | return ((MAX_INTERVAL - interval) * 100) / MAX_INTERVAL; 35 | } 36 | 37 | function speed2Interval(speed: number) { 38 | return MAX_INTERVAL - speed * (MAX_INTERVAL / 100); 39 | } 40 | 41 | const content = ( 42 |
43 | {t('mouse.slow')}, 47 | 100: {t('mouse.fast')} 48 | }} 49 | range={false} 50 | included={false} 51 | step={10} 52 | defaultValue={scrollSpeed} 53 | onChange={update} 54 | /> 55 |
56 | ); 57 | 58 | return ( 59 | 60 |
61 | 62 | {t('mouse.speed')} 63 |
64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/power/power-long.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Popconfirm, Slider } from 'antd'; 3 | import { CirclePowerIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/vm.ts'; 7 | 8 | type PowerLongProps = { 9 | showConfirm: boolean; 10 | isLoading: boolean; 11 | setIsLoading: (loading: boolean) => void; 12 | }; 13 | 14 | export const PowerLong = ({ showConfirm, isLoading, setIsLoading }: PowerLongProps) => { 15 | const { t } = useTranslation(); 16 | 17 | const [duration, setDuration] = useState(8); 18 | 19 | function power() { 20 | if (isLoading) return; 21 | setIsLoading(true); 22 | 23 | api.setGpio('power', duration * 1000).finally(() => { 24 | setIsLoading(false); 25 | }); 26 | } 27 | 28 | return ( 29 | <> 30 | {showConfirm ? ( 31 | 39 |
40 | 41 | {t('power.powerLong')} 42 |
{`${duration}s`}
43 |
44 |
45 | ) : ( 46 |
50 | 51 | {t('power.powerLong')} 52 |
{`${duration}s`}
53 |
54 | )} 55 | 56 |
57 | 64 |
65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/power/power-short.tsx: -------------------------------------------------------------------------------- 1 | import { Popconfirm } from 'antd'; 2 | import { PowerIcon } from 'lucide-react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import * as api from '@/api/vm.ts'; 6 | 7 | type PowerShortProps = { 8 | showConfirm: boolean; 9 | isLoading: boolean; 10 | setIsLoading: (loading: boolean) => void; 11 | }; 12 | 13 | export const PowerShort = ({ showConfirm, isLoading, setIsLoading }: PowerShortProps) => { 14 | const { t } = useTranslation(); 15 | 16 | function power() { 17 | if (isLoading) return; 18 | setIsLoading(true); 19 | 20 | api.setGpio('power', 800).finally(() => { 21 | setIsLoading(false); 22 | }); 23 | } 24 | 25 | return ( 26 | <> 27 | {showConfirm ? ( 28 | 36 |
37 | 38 | {t('power.powerShort')} 39 |
40 |
41 | ) : ( 42 |
46 | 47 | {t('power.powerShort')} 48 |
49 | )} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/power/reset.tsx: -------------------------------------------------------------------------------- 1 | import { Popconfirm } from 'antd'; 2 | import { RotateCcwIcon } from 'lucide-react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { setGpio } from '@/api/vm.ts'; 6 | 7 | type ResetProps = { 8 | showConfirm: boolean; 9 | isLoading: boolean; 10 | setIsLoading: (loading: boolean) => void; 11 | }; 12 | 13 | export const Reset = ({ showConfirm, isLoading, setIsLoading }: ResetProps) => { 14 | const { t } = useTranslation(); 15 | 16 | function reset() { 17 | if (isLoading) return; 18 | setIsLoading(true); 19 | 20 | setGpio('reset', 800).finally(() => { 21 | setIsLoading(false); 22 | }); 23 | } 24 | 25 | return ( 26 | <> 27 | {showConfirm ? ( 28 | 36 |
37 | 38 | {t('power.reset')} 39 |
40 |
41 | ) : ( 42 |
46 | 47 | {t('power.reset')} 48 |
49 | )} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/screen/constants.ts: -------------------------------------------------------------------------------- 1 | export const QualityMap = new Map([ 2 | [1, 100], 3 | [2, 80], 4 | [3, 60], 5 | [4, 50] 6 | ]); 7 | 8 | export const BitRateMap = new Map([ 9 | [1, 5000], 10 | [2, 3000], 11 | [3, 2000], 12 | [4, 1000] 13 | ]); 14 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/screen/frame-detect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import clsx from 'clsx'; 4 | import { LoaderCircleIcon, Tally4Icon, Tally5Icon } from 'lucide-react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | import * as api from '@/api/stream.ts'; 8 | import * as ls from '@/lib/localstorage.ts'; 9 | 10 | export const FrameDetect = () => { 11 | const { t } = useTranslation(); 12 | 13 | const [isLoading, setIsLoading] = useState(false); 14 | const [isEnabled, setIsEnabled] = useState(false); 15 | 16 | useEffect(() => { 17 | const enabled = ls.getFrameDetect(); 18 | if (enabled) { 19 | setIsEnabled(true); 20 | } else { 21 | api.updateFrameDetect(false); 22 | } 23 | }, []); 24 | 25 | function update() { 26 | if (isLoading) return; 27 | setIsLoading(true); 28 | 29 | const enabled = !isEnabled; 30 | 31 | api 32 | .updateFrameDetect(enabled) 33 | .then((rsp) => { 34 | if (rsp.code === 0) { 35 | setIsEnabled(enabled); 36 | ls.setFrameDetect(enabled); 37 | } 38 | }) 39 | .finally(() => { 40 | setIsLoading(false); 41 | }); 42 | } 43 | 44 | return ( 45 | 46 |
50 | {isLoading ? ( 51 | 52 | ) : ( 53 | <> 54 | {isEnabled ? : } 55 | 56 | 62 | {t('screen.frameDetect')} 63 | 64 | 65 | )} 66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/screen/gop.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from 'antd'; 2 | import { CheckIcon, SquareKanbanIcon } from 'lucide-react'; 3 | 4 | import { updateScreen } from '@/api/vm.ts'; 5 | import { setGop as setCookie } from '@/lib/localstorage'; 6 | 7 | type GopProps = { 8 | gop: number; 9 | setGop: (gop: number) => void; 10 | }; 11 | 12 | const gopList = [ 13 | { key: 10, label: '10' }, 14 | { key: 30, label: '30' }, 15 | { key: 50, label: '50' }, 16 | { key: 100, label: '100' } 17 | ]; 18 | 19 | export const Gop = ({ gop, setGop }: GopProps) => { 20 | async function update(value: number) { 21 | if (value === gop) return; 22 | 23 | const rsp = await updateScreen('gop', value); 24 | if (rsp.code !== 0) { 25 | return; 26 | } 27 | 28 | setGop(value); 29 | setCookie(value); 30 | } 31 | 32 | const content = ( 33 | <> 34 | {gopList.map((item) => ( 35 |
update(item.key)} 39 | > 40 |
41 | {item.key === gop && } 42 |
43 | {item.label} 44 |
45 | ))} 46 | 47 | ); 48 | 49 | return ( 50 | 51 |
52 | 53 | GOP 54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/screen/reset.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import clsx from 'clsx'; 3 | import { RefreshCwIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/vm.ts'; 7 | 8 | export const Reset = () => { 9 | const { t } = useTranslation(); 10 | 11 | const [isPcie, setIsPcie] = useState(true); 12 | const [isResetting, setIsResetting] = useState(false); 13 | 14 | useEffect(() => { 15 | api.getHardware().then((rsp) => { 16 | if (rsp.code === 0) { 17 | setIsPcie(rsp.data?.version === 'PCIE'); 18 | } 19 | }); 20 | }, []); 21 | 22 | function reset() { 23 | if (isResetting) return; 24 | setIsResetting(true); 25 | 26 | api.resetHdmi().finally(() => { 27 | setTimeout(() => { 28 | setIsResetting(false); 29 | }, 1000); 30 | }); 31 | } 32 | 33 | return ( 34 | <> 35 | {isPcie && ( 36 |
40 | 44 | {t('screen.resetHdmi')} 45 |
46 | )} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/script/run.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { LoadingOutlined } from '@ant-design/icons'; 3 | import { Button, Card, Modal, Spin } from 'antd'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/script'; 7 | 8 | type RunProps = { 9 | script: string; 10 | setIsRunning: (isRunning: boolean) => void; 11 | }; 12 | 13 | export const Run = ({ script, setIsRunning }: RunProps) => { 14 | const { t } = useTranslation(); 15 | const [state, setState] = useState(''); 16 | const [log, setLog] = useState(''); 17 | 18 | useEffect(() => { 19 | setState('running'); 20 | 21 | api 22 | .runScript(script, 'foreground') 23 | .then((rsp) => { 24 | if (rsp.code !== 0) { 25 | setLog(rsp.msg); 26 | setState('failed'); 27 | return; 28 | } 29 | 30 | setState('success'); 31 | setLog(rsp.data.log); 32 | }) 33 | .catch(() => { 34 | setLog(t('script.runFailed')); 35 | setState('failed'); 36 | }); 37 | }, []); 38 | 39 | return ( 40 | 48 | {state === 'running' ? ( 49 |
50 | } size="large" /> 51 |
52 | ) : ( 53 | {log} 54 | )} 55 | 56 |
57 | 60 |
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/about/community.tsx: -------------------------------------------------------------------------------- 1 | import { GithubOutlined, XOutlined } from '@ant-design/icons'; 2 | import { BookOpenIcon, MessageCircleQuestionIcon, MessageSquareIcon } from 'lucide-react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | export const Community = () => { 6 | const { t } = useTranslation(); 7 | 8 | const communities = [ 9 | { name: 'Document', icon: , url: 'https://wiki.sipeed.com/nanokvm' }, 10 | { 11 | name: 'GitHub', 12 | icon: , 13 | url: 'https://github.com/sipeed/NanoKVM' 14 | }, 15 | { 16 | name: 'X', 17 | icon: , 18 | url: 'https://twitter.com/SipeedIO' 19 | }, 20 | { 21 | name: 'Discussion', 22 | icon: , 23 | url: 'https://maixhub.com/discussion/nanokvm' 24 | }, 25 | { 26 | name: 'FAQ', 27 | icon: , 28 | url: 'https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/faq.html' 29 | } 30 | ]; 31 | 32 | return ( 33 | <> 34 |
{t('settings.about.community')}
35 | 36 |
37 | {communities.map((community) => ( 38 | 44 | {community.icon} 45 | {community.name} 46 | 47 | ))} 48 |
49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/about/index.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { Community } from './community.tsx'; 5 | import { Hostname } from './hostname.tsx'; 6 | import { Information } from './information.tsx'; 7 | 8 | export const About = () => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 | <> 13 |
{t('settings.about.title')}
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/account/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Divider } from 'antd'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | import * as api from '@/api/auth.ts'; 7 | 8 | import { Logout } from './logout.tsx'; 9 | 10 | export const Account = () => { 11 | const { t } = useTranslation(); 12 | const navigate = useNavigate(); 13 | 14 | const [username, setUsername] = useState(''); 15 | 16 | useEffect(() => { 17 | api.getAccount().then((rsp) => { 18 | if (rsp.code === 0) { 19 | setUsername(rsp.data.username); 20 | } 21 | }); 22 | }, []); 23 | 24 | function changePassword() { 25 | navigate('/auth/password'); 26 | } 27 | 28 | return ( 29 | <> 30 |
{t('settings.account.title')}
31 | 32 | 33 |
34 |
35 | {t('settings.account.webAccount')} 36 | {username ? username : '-'} 37 |
38 | 39 |
40 | {t('settings.account.password')} 41 | 45 | {t('settings.account.updateBtn')} 46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/account/logout.tsx: -------------------------------------------------------------------------------- 1 | import { LogoutOutlined } from '@ant-design/icons'; 2 | import { Button, Popconfirm } from 'antd'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | import * as api from '@/api/auth.ts'; 7 | import { removeToken } from '@/lib/cookie.ts'; 8 | 9 | export const Logout = () => { 10 | const { t } = useTranslation(); 11 | const navigate = useNavigate(); 12 | 13 | function logout() { 14 | api.logout().then((rsp) => { 15 | if (rsp.code !== 0) { 16 | console.log(rsp.msg); 17 | return; 18 | } 19 | 20 | removeToken(); 21 | navigate('/auth/login'); 22 | }); 23 | } 24 | 25 | return ( 26 |
27 | 34 | 37 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/appearance/index.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { Language } from './language.tsx'; 5 | import { MenuBar } from './menu-bar.tsx'; 6 | import { WebTitle } from './web-title.tsx'; 7 | 8 | export const Appearance = () => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 | <> 13 |
{t('settings.appearance.title')}
14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/appearance/language.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'antd'; 2 | import { LanguagesIcon } from 'lucide-react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import languages from '@/i18n/languages.ts'; 6 | import { setLanguage } from '@/lib/localstorage.ts'; 7 | 8 | export const Language = () => { 9 | const { t, i18n } = useTranslation(); 10 | 11 | const options = languages.map((language) => ({ 12 | value: language.key, 13 | label: language.name 14 | })); 15 | 16 | function changeLanguage(value: string) { 17 | if (i18n.language === value) return; 18 | 19 | i18n.changeLanguage(value); 20 | setLanguage(value); 21 | } 22 | 23 | return ( 24 |
25 |
26 | 27 | {t('settings.appearance.language')} 28 |
29 | 30 | setIsKeyboardEnable(false)} 65 | onBlur={() => setIsKeyboardEnable(true)} 66 | style={{ width: 180 }} 67 | value={title} 68 | onChange={(e) => setTitle(e.target.value)} 69 | onPressEnter={update} 70 | /> 71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/advanced/index.tsx: -------------------------------------------------------------------------------- 1 | import { Collapse } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { Swap } from './swap.tsx'; 5 | 6 | const children = ( 7 |
8 | 9 |
10 | ); 11 | 12 | export const Advanced = () => { 13 | const { t } = useTranslation(); 14 | 15 | return ( 16 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/hdmi.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Switch } from 'antd'; 3 | import { useAtom } from 'jotai'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/vm.ts'; 7 | import { isHdmiEnabledAtom } from '@/jotai/screen.ts'; 8 | 9 | export const Hdmi = () => { 10 | const { t } = useTranslation(); 11 | 12 | const [isHdmiEnabled, setIsHdmiEnabled] = useAtom(isHdmiEnabledAtom); 13 | 14 | const [isPcie, setIsPcie] = useState(false); 15 | const [isLoading, setIsLoading] = useState(false); 16 | 17 | useEffect(() => { 18 | getHardware(); 19 | getHdmiState(); 20 | }, []); 21 | 22 | async function getHardware() { 23 | const rsp = await api.getHardware(); 24 | if (rsp.code !== 0) { 25 | return; 26 | } 27 | 28 | setIsPcie(rsp.data?.version === 'PCIE'); 29 | } 30 | 31 | async function getHdmiState() { 32 | setIsLoading(true); 33 | 34 | const rsp = await api.getHdmiState(); 35 | if (rsp.code === 0) { 36 | setIsHdmiEnabled(rsp.data.enabled); 37 | } 38 | 39 | setIsLoading(false); 40 | } 41 | 42 | async function setHdmiState() { 43 | if (isLoading) return; 44 | setIsLoading(true); 45 | 46 | const enabled = !isHdmiEnabled; 47 | 48 | const rsp = await api.setHdmiState(enabled); 49 | if (rsp.code !== 0) { 50 | setIsLoading(false); 51 | return; 52 | } 53 | 54 | setTimeout(() => { 55 | setIsHdmiEnabled(enabled); 56 | setIsLoading(false); 57 | }, 1000); 58 | } 59 | 60 | return ( 61 | <> 62 | {isPcie && ( 63 |
64 |
65 | HDMI 66 | 67 | 68 | {t('settings.device.hdmi.description')} 69 | 70 |
71 | 72 | 73 |
74 | )} 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/hid-mode.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | export const HidMode = () => { 5 | const { t } = useTranslation(); 6 | 7 | return ( 8 |
9 |
10 | {t('settings.device.hidOnly')} 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Divider } from 'antd'; 3 | import { useAtom } from 'jotai'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/hid.ts'; 7 | import { hidModeAtom } from '@/jotai/mouse.ts'; 8 | 9 | import { Advanced } from './advanced'; 10 | import { Hdmi } from './hdmi.tsx'; 11 | import { HidMode } from './hid-mode.tsx'; 12 | import { Mdns } from './mdns.tsx'; 13 | import { MouseJiggler } from './mouse-jiggler.tsx'; 14 | import { Oled } from './oled.tsx'; 15 | import { Reboot } from './reboot.tsx'; 16 | import { Ssh } from './ssh.tsx'; 17 | import { Tls } from './tls.tsx'; 18 | import { VirtualDevices } from './virtual-devices.tsx'; 19 | import { Wifi } from './wifi.tsx'; 20 | 21 | export const Device = () => { 22 | const { t } = useTranslation(); 23 | 24 | const [hidMode, setHidMode] = useAtom(hidModeAtom); 25 | 26 | useEffect(() => { 27 | api.getHidMode().then((rsp) => { 28 | if (rsp.code === 0) { 29 | setHidMode(rsp.data.mode); 30 | } 31 | }); 32 | }, []); 33 | 34 | return ( 35 | <> 36 |
{t('settings.device.title')}
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | {hidMode === 'normal' ? : } 46 |
47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/mdns.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Switch, Tooltip } from 'antd'; 3 | import { CircleAlertIcon } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/vm.ts'; 7 | 8 | export const Mdns = () => { 9 | const { t } = useTranslation(); 10 | 11 | const [isEnabled, setIsEnabled] = useState(false); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | useEffect(() => { 15 | setIsLoading(true); 16 | 17 | api 18 | .getMdnsState() 19 | .then((rsp) => { 20 | if (rsp.data?.enabled) { 21 | setIsEnabled(true); 22 | } 23 | }) 24 | .finally(() => { 25 | setIsLoading(false); 26 | }); 27 | }, []); 28 | 29 | async function update() { 30 | if (isLoading) return; 31 | setIsLoading(true); 32 | 33 | const rsp = isEnabled ? await api.disableMdns() : await api.enableMdns(); 34 | setIsLoading(false); 35 | 36 | if (rsp.code !== 0) { 37 | console.log(rsp.msg); 38 | return; 39 | } 40 | 41 | setIsEnabled(!isEnabled); 42 | } 43 | 44 | return ( 45 |
46 |
47 |
48 | mDNS 49 | 50 | 56 | 57 | 58 |
59 | 60 | {t('settings.device.mdns.description')} 61 |
62 | 63 | 64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /web/src/pages/desktop/menu/settings/device/oled.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Select } from 'antd'; 3 | import { ScreenShareOff } from 'lucide-react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import * as api from '@/api/vm.ts'; 7 | 8 | export const Oled = () => { 9 | const { t } = useTranslation(); 10 | 11 | const [isOLEDExist, setIsOLEDExist] = useState(false); 12 | const [isLoading, setIsLoading] = useState(false); 13 | const [sleep, setSleep] = useState(''); 14 | 15 | useEffect(() => { 16 | api.getOLED().then((rsp) => { 17 | if (rsp.code !== 0) { 18 | console.log(rsp.msg); 19 | return; 20 | } 21 | 22 | if (!rsp.data.exist) { 23 | return; 24 | } 25 | 26 | setIsOLEDExist(true); 27 | setSleep(rsp.data.sleep.toString()); 28 | }); 29 | }, []); 30 | 31 | const options = [0, 15, 30, 60, 180, 300, 600, 1800, 3600].map((duration) => ({ 32 | value: duration.toString(), 33 | label: t(`settings.device.oled.${duration}`) 34 | })); 35 | 36 | function update(value: string) { 37 | if (isLoading) return; 38 | setIsLoading(true); 39 | 40 | api 41 | .setOLED(parseInt(value)) 42 | .then((rsp) => { 43 | if (rsp.code !== 0) { 44 | console.log(rsp.msg); 45 | return; 46 | } 47 | 48 | setSleep(value); 49 | }) 50 | .finally(() => { 51 | setIsLoading(false); 52 | }); 53 | } 54 | 55 | return ( 56 |
57 |
58 | {t('settings.device.oled.title')} 59 | {t('settings.device.oled.description')} 60 |
61 | 62 | {isOLEDExist ? ( 63 |