mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-17 19:22:15 +01:00
Update App version to 0.0.2
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,5 +3,9 @@ static/*
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
package-lock.json
|
||||
package.json
|
||||
node_modules/
|
||||
|
||||
|
||||
device-tests.tar.gz
|
||||
@@ -5,6 +5,8 @@ linters:
|
||||
- misspell
|
||||
- whitespace
|
||||
- gochecknoinits
|
||||
disable:
|
||||
- unused
|
||||
settings:
|
||||
forbidigo:
|
||||
forbid:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tailwindCSS.classFunctions": ["cva", "cx"]
|
||||
}
|
||||
4
Makefile
4
Makefile
@@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||
BUILDTS ?= $(shell date -u +%s)
|
||||
REVISION ?= $(shell git rev-parse HEAD)
|
||||
VERSION_DEV ?= 0.0.1-dev
|
||||
VERSION ?= 0.0.1
|
||||
VERSION_DEV ?= 0.0.2-dev
|
||||
VERSION ?= 0.0.2
|
||||
|
||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||
KVM_PKG_NAME := kvm
|
||||
|
||||
7
audio.go
7
audio.go
@@ -99,7 +99,10 @@ func StartNtpAudioServer(handleClient func(net.Conn)) {
|
||||
}
|
||||
|
||||
func StopNtpAudioServer() {
|
||||
CallAudioCtrlAction("set_audio_enable", map[string]interface{}{"audio_enable": false})
|
||||
_, err := CallAudioCtrlAction("set_audio_enable", map[string]interface{}{"audio_enable": false})
|
||||
if err != nil {
|
||||
audioLogger.Error().Err(err).Msg("failed to set audio enable")
|
||||
}
|
||||
|
||||
if audioListener != nil {
|
||||
audioListener.Close()
|
||||
@@ -138,7 +141,7 @@ func handleAudioClient(conn net.Conn) {
|
||||
}
|
||||
|
||||
timestamp += timestampStep
|
||||
packet.Header.Timestamp = timestamp
|
||||
packet.Timestamp = timestamp
|
||||
buf, err := packet.Marshal()
|
||||
if err != nil {
|
||||
audioLogger.Warn().Err(err).Msg("error marshalling packet")
|
||||
|
||||
3
cloud.go
3
cloud.go
@@ -118,8 +118,7 @@ func handleSessionRequest(
|
||||
source string,
|
||||
scopedLogger *zerolog.Logger,
|
||||
) error {
|
||||
var sourceType string
|
||||
sourceType = "local"
|
||||
var sourceType = "local"
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v)
|
||||
|
||||
190
config.go
190
config.go
@@ -1,9 +1,14 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"kvm/internal/logging"
|
||||
@@ -75,8 +80,7 @@ func (m *KeyboardMacro) Validate() error {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
CloudToken string `json:"cloud_token"`
|
||||
GoogleIdentity string `json:"google_identity"`
|
||||
STUN string `json:"stun"`
|
||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||
IncludePreRelease bool `json:"include_pre_release"`
|
||||
@@ -102,6 +106,8 @@ type Config struct {
|
||||
TailScaleXEdge bool `json:"tailscale_xedge"`
|
||||
ZeroTierNetworkID string `json:"zerotier_network_id"`
|
||||
ZeroTierAutoStart bool `json:"zerotier_autostart"`
|
||||
FrpcAutoStart bool `json:"frpc_autostart"`
|
||||
FrpcToml string `json:"frpc_toml"`
|
||||
IO0Status bool `json:"io0_status"`
|
||||
IO1Status bool `json:"io1_status"`
|
||||
AudioMode string `json:"audio_mode"`
|
||||
@@ -111,8 +117,10 @@ type Config struct {
|
||||
}
|
||||
|
||||
const configPath = "/userdata/kvm_config.json"
|
||||
const sdConfigPath = "/mnt/sdcard/kvm_config.json"
|
||||
|
||||
var defaultConfig = &Config{
|
||||
STUN: "stun:stun.l.google.com:19302",
|
||||
AutoUpdateEnabled: false, // Set a default value
|
||||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
@@ -135,13 +143,15 @@ var defaultConfig = &Config{
|
||||
RelativeMouse: true,
|
||||
Keyboard: true,
|
||||
MassStorage: true,
|
||||
Audio: true,
|
||||
Audio: false, //At any given time, only one of Audio and Mtp can be set to true
|
||||
Mtp: false,
|
||||
},
|
||||
NetworkConfig: &network.NetworkConfig{},
|
||||
DefaultLogLevel: "INFO",
|
||||
ZeroTierAutoStart: false,
|
||||
TailScaleAutoStart: false,
|
||||
TailScaleXEdge: false,
|
||||
FrpcAutoStart: false,
|
||||
IO0Status: true,
|
||||
IO1Status: true,
|
||||
AudioMode: "disabled",
|
||||
@@ -165,6 +175,14 @@ func LoadConfig() {
|
||||
|
||||
// load the default config
|
||||
config = defaultConfig
|
||||
if config.UsbConfig.SerialNumber == "" {
|
||||
serialNumber, err := extractSerialNumber()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to extract serial number")
|
||||
} else {
|
||||
config.UsbConfig.SerialNumber = serialNumber
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
@@ -177,6 +195,10 @@ func LoadConfig() {
|
||||
loadedConfig := *defaultConfig
|
||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||
logger.Warn().Err(err).Msg("config file JSON parsing failed")
|
||||
os.Remove(configPath)
|
||||
if _, err := os.Stat(sdConfigPath); err == nil {
|
||||
os.Remove(sdConfigPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,6 +222,74 @@ func LoadConfig() {
|
||||
logger.Info().Str("path", configPath).Msg("config loaded")
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := out.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := out.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncConfigSD(isUpdate bool) {
|
||||
resp, err := rpcGetSDMountStatus()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get sd mount status")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Status == SDMountOK {
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if err := SaveConfig(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to create kvm_config.json")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if isUpdate {
|
||||
if _, err := os.Stat(sdConfigPath); err == nil {
|
||||
if err := copyFile(sdConfigPath, configPath); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to copy kvm_config.json from sdcard to userdata")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(configPath, sdConfigPath); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to copy kvm_config.json from userdata to sdcard")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(configPath, sdConfigPath); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to copy kvm_config.json from userdata to sdcard")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SaveConfig() error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
@@ -218,6 +308,8 @@ func SaveConfig() error {
|
||||
return fmt.Errorf("failed to encode config: %w", err)
|
||||
}
|
||||
|
||||
SyncConfigSD(false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,3 +318,95 @@ func ensureConfigLoaded() {
|
||||
LoadConfig()
|
||||
}
|
||||
}
|
||||
|
||||
var systemInfoWriteLock sync.Mutex
|
||||
|
||||
func writeSystemInfoImg() error {
|
||||
systemInfoWriteLock.Lock()
|
||||
defer systemInfoWriteLock.Unlock()
|
||||
|
||||
imgPath := filepath.Join(imagesFolder, "system_info.img")
|
||||
unverifiedimgPath := filepath.Join(imagesFolder, "system_info.img") + ".unverified"
|
||||
mountPoint := "/mnt/system_info"
|
||||
|
||||
run := func(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(unverifiedimgPath); err == nil {
|
||||
err := os.Rename(unverifiedimgPath, imgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename %s to %s: %v", unverifiedimgPath, imgPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
isMounted := false
|
||||
if f, err := os.Open("/proc/mounts"); err == nil {
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) >= 2 && fields[1] == mountPoint {
|
||||
isMounted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isMounted {
|
||||
logger.Info().Msgf("%s is mounted, umounting...\n", mountPoint)
|
||||
_ = run("umount", mountPoint)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mountPoint); err == nil {
|
||||
if err := os.Remove(mountPoint); err != nil {
|
||||
return fmt.Errorf("failed to remove %s: %v", mountPoint, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(imgPath); err == nil {
|
||||
if err := copyFile(imgPath, unverifiedimgPath); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to copy system_info.img")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := run("dd", "if=/dev/zero", "of="+unverifiedimgPath, "bs=1M", "count=4"); err != nil {
|
||||
return fmt.Errorf("dd failed: %v", err)
|
||||
}
|
||||
|
||||
if err := run("mkfs.vfat", unverifiedimgPath); err != nil {
|
||||
return fmt.Errorf("mkfs.vfat failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(mountPoint, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir failed: %v", err)
|
||||
}
|
||||
|
||||
if err := run("mount", "-o", "loop", unverifiedimgPath, mountPoint); err != nil {
|
||||
return fmt.Errorf("mount failed: %v", err)
|
||||
}
|
||||
|
||||
if err := run("cp", "/etc/hostname", mountPoint+"/hostname.txt"); err != nil {
|
||||
return fmt.Errorf("copy hostname failed: %v", err)
|
||||
}
|
||||
if err := run("sh", "-c", "ip addr show > "+mountPoint+"/network_info.txt"); err != nil {
|
||||
return fmt.Errorf("write network info failed: %v", err)
|
||||
}
|
||||
|
||||
_ = run("umount", mountPoint)
|
||||
if err := os.RemoveAll(mountPoint); err != nil {
|
||||
return fmt.Errorf("failed to remove %s: %v", mountPoint, err)
|
||||
}
|
||||
|
||||
if err := os.Rename(unverifiedimgPath, imgPath); err != nil {
|
||||
return fmt.Errorf("failed to rename %s to %s: %v", unverifiedimgPath, imgPath, err)
|
||||
}
|
||||
|
||||
logger.Info().Msg("system_info.img update successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -109,10 +109,14 @@ func updateDisplay() {
|
||||
} else {
|
||||
_, _ = lvObjSetState("Main", "USB_DISCONNECTED")
|
||||
}
|
||||
_ = os.WriteFile("/userdata/usb_state", []byte(usbState), 0644)
|
||||
|
||||
if lastVideoState.Ready {
|
||||
_, _ = lvObjSetState("Main", "HDMI_CONNECTED")
|
||||
_ = os.WriteFile("/userdata/hdmi_state", []byte("connected"), 0644)
|
||||
} else {
|
||||
_, _ = lvObjSetState("Main", "HDMI_DISCONNECTED")
|
||||
_ = os.WriteFile("/userdata/hdmi_state", []byte("disconnected"), 0644)
|
||||
}
|
||||
|
||||
if networkState.IsUp() {
|
||||
@@ -120,7 +124,6 @@ func updateDisplay() {
|
||||
} else {
|
||||
_, _ = lvObjSetState("Network", "NO_NETWORK")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -37,6 +37,7 @@ type NetworkConfig struct {
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4RequestAddress null.String `json:"ipv4_request_address,omitempty"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
|
||||
@@ -95,6 +95,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
},
|
||||
RequestAddress: s.config.IPv4RequestAddress.String,
|
||||
})
|
||||
|
||||
s.dhcpClient = dhcpClient
|
||||
|
||||
@@ -3,10 +3,13 @@ package udhcpc
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
@@ -24,6 +27,7 @@ type DHCPClient struct {
|
||||
InterfaceName string
|
||||
leaseFile string
|
||||
pidFile string
|
||||
requestAddress string
|
||||
lease *Lease
|
||||
logger *zerolog.Logger
|
||||
process *os.Process
|
||||
@@ -35,6 +39,7 @@ type DHCPClientOptions struct {
|
||||
PidFile string
|
||||
Logger *zerolog.Logger
|
||||
OnLeaseChange func(lease *Lease)
|
||||
RequestAddress string
|
||||
}
|
||||
|
||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
||||
@@ -51,6 +56,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
||||
leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName),
|
||||
pidFile: options.PidFile,
|
||||
onLeaseChange: options.OnLeaseChange,
|
||||
requestAddress: options.RequestAddress,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +86,8 @@ func (c *DHCPClient) watchLink() {
|
||||
|
||||
for update := range ch {
|
||||
if update.Link.Attrs().Name == c.InterfaceName {
|
||||
if update.IfInfomsg.Flags&unix.IFF_RUNNING != 0 {
|
||||
fmt.Printf("[watchLink]link is up, starting udhcpc")
|
||||
if update.Flags&unix.IFF_RUNNING != 0 {
|
||||
c.logger.Info().Msg("link is up, starting udhcpc")
|
||||
go c.runUDHCPC()
|
||||
} else {
|
||||
c.logger.Info().Msg("link is down")
|
||||
@@ -90,10 +96,45 @@ func (c *DHCPClient) watchLink() {
|
||||
}
|
||||
}
|
||||
|
||||
type udhcpcOutput struct {
|
||||
mu *sync.Mutex
|
||||
logger *zerolog.Event
|
||||
}
|
||||
|
||||
func (w *udhcpcOutput) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.logger.Msg(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *DHCPClient) runUDHCPC() {
|
||||
cmd := exec.Command("udhcpc", "-i", c.InterfaceName, "-t", "1")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if c.requestAddress != "" {
|
||||
ip := net.ParseIP(c.requestAddress)
|
||||
if ip != nil && ip.To4() != nil {
|
||||
cmd.Args = append(cmd.Args, "-r", c.requestAddress)
|
||||
}
|
||||
}
|
||||
|
||||
udhcpcOutputLock := sync.Mutex{}
|
||||
udhcpcStdout := &udhcpcOutput{
|
||||
mu: &udhcpcOutputLock,
|
||||
logger: c.logger.Debug().Str("pipe", "stdout"),
|
||||
}
|
||||
udhcpcStderr := &udhcpcOutput{
|
||||
mu: &udhcpcOutputLock,
|
||||
logger: c.logger.Debug().Str("pipe", "stderr"),
|
||||
}
|
||||
|
||||
cmd.Stdout = udhcpcStdout
|
||||
cmd.Stderr = udhcpcStderr
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pdeathsig: syscall.SIGKILL,
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to run udhcpc")
|
||||
}
|
||||
@@ -113,6 +154,7 @@ func (c *DHCPClient) Run() error {
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go c.runUDHCPC()
|
||||
go c.watchLink()
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -34,6 +34,7 @@ const (
|
||||
FileStateFileWrite // update file content without checking
|
||||
FileStateMounted
|
||||
FileStateMountedConfigFS
|
||||
FileStateMountedFunctionFS
|
||||
FileStateSymlink
|
||||
FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order
|
||||
FileStateSymlinkNotInOrderConfigFS
|
||||
@@ -52,6 +53,7 @@ var FileStateString = map[FileState]string{
|
||||
FileStateSymlink: "SYMLINK",
|
||||
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
|
||||
FileStateTouch: "TOUCH",
|
||||
FileStateMountedFunctionFS: "FUNCTIONFS_MOUNTED",
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -78,6 +80,7 @@ const (
|
||||
FileChangeResolvedActionRemoveDirectory
|
||||
FileChangeResolvedActionTouch
|
||||
FileChangeResolvedActionMountConfigFS
|
||||
FileChangeResolvedActionMountFunctionFS
|
||||
)
|
||||
|
||||
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
||||
@@ -96,6 +99,7 @@ var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
||||
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
|
||||
FileChangeResolvedActionTouch: "TOUCH",
|
||||
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
|
||||
FileChangeResolvedActionMountFunctionFS: "FUNCTIONFS_MOUNT",
|
||||
}
|
||||
|
||||
type ChangeSet struct {
|
||||
@@ -147,6 +151,8 @@ func (f *RequestedFileChange) String() string {
|
||||
s = fmt.Sprintf("write: %s with content [%s]", f.Path, f.ExpectedContent)
|
||||
case FileStateMountedConfigFS:
|
||||
s = fmt.Sprintf("configfs: %s", f.Path)
|
||||
case FileStateMountedFunctionFS:
|
||||
s = fmt.Sprintf("functionfs: %s", f.Path)
|
||||
case FileStateTouch:
|
||||
s = fmt.Sprintf("touch: %s", f.Path)
|
||||
case FileStateUnknown:
|
||||
@@ -298,6 +304,8 @@ func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction {
|
||||
return FileChangeResolvedActionWriteFile
|
||||
case FileStateTouch:
|
||||
return FileChangeResolvedActionTouch
|
||||
case FileStateMountedFunctionFS:
|
||||
return FileChangeResolvedActionMountFunctionFS
|
||||
}
|
||||
|
||||
// get the actual state of the file
|
||||
@@ -424,6 +432,8 @@ func (c *ChangeSet) applyChange(change *FileChange) error {
|
||||
return os.Chtimes(change.Path, time.Now(), time.Now())
|
||||
case FileChangeResolvedActionMountConfigFS:
|
||||
return mountConfigFS(change.Path)
|
||||
case FileChangeResolvedActionMountFunctionFS:
|
||||
return mountFunctionFS(change.Path)
|
||||
case FileChangeResolvedActionDoNothing:
|
||||
return nil
|
||||
default:
|
||||
|
||||
@@ -2,6 +2,7 @@ package usbgadget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sourcegraph/tf-dag/dag"
|
||||
@@ -127,6 +128,11 @@ func (c *ChangeSetResolver) applyChanges() error {
|
||||
l.Str("action", actionStr).Str("change", change.String()).Msg("applying change")
|
||||
|
||||
err := c.changeset.applyChange(change)
|
||||
if err != nil {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
err = c.changeset.applyChange(change)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if change.IgnoreErrors {
|
||||
c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package usbgadget
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type gadgetConfigItem struct {
|
||||
@@ -51,6 +54,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
||||
"configuration": "Config 1: HID",
|
||||
},
|
||||
},
|
||||
// mtp
|
||||
"mtp": mtpConfig,
|
||||
// keyboard HID
|
||||
"keyboard": keyboardConfig,
|
||||
// mouse HID
|
||||
@@ -91,6 +96,8 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
|
||||
return u.enabledDevices.MassStorage
|
||||
case "mass_storage_lun0":
|
||||
return u.enabledDevices.MassStorage
|
||||
case "mtp":
|
||||
return u.enabledDevices.Mtp
|
||||
case "audio":
|
||||
return u.enabledDevices.Audio
|
||||
default:
|
||||
@@ -184,6 +191,45 @@ func mountConfigFS(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountFunctionFS(path string) error {
|
||||
err := os.MkdirAll("/dev/ffs-mtp", 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create mtp dev dir: %w", err)
|
||||
}
|
||||
mounted := false
|
||||
if f, err := os.Open("/proc/mounts"); err == nil {
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
if strings.Contains(scanner.Text(), functionFSPath) {
|
||||
mounted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
if !mounted {
|
||||
err := exec.Command("mount", "-t", "functionfs", "mtp", path).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount functionfs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
umtprdRunning := false
|
||||
if out, err := exec.Command("pgrep", "-x", "umtprd").Output(); err == nil && len(out) > 0 {
|
||||
umtprdRunning = true
|
||||
}
|
||||
|
||||
if !umtprdRunning {
|
||||
cmd := exec.Command("umtprd")
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to exec binary: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) Init() error {
|
||||
u.configLock.Lock()
|
||||
defer u.configLock.Unlock()
|
||||
|
||||
@@ -131,6 +131,16 @@ func (tx *UsbGadgetTransaction) MountConfigFS() {
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) MountFunctionFS() {
|
||||
tx.addFileChange("mtp", RequestedFileChange{
|
||||
Path: functionFSPath,
|
||||
Key: "mtp",
|
||||
ExpectedState: FileStateMountedFunctionFS,
|
||||
Description: "mount functionfs",
|
||||
DependsOn: []string{"reorder-symlinks"},
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) CreateConfigPath() {
|
||||
tx.mkdirAll(
|
||||
"gadget",
|
||||
@@ -164,7 +174,12 @@ func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
||||
deps = tx.writeGadgetItemConfig(item, deps)
|
||||
}
|
||||
|
||||
tx.WriteUDC()
|
||||
if tx.isGadgetConfigItemEnabled("mtp") {
|
||||
tx.MountFunctionFS()
|
||||
tx.WriteUDC(true)
|
||||
} else {
|
||||
tx.WriteUDC(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) getDisableKeys() []string {
|
||||
@@ -315,9 +330,19 @@ func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target stri
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) WriteUDC() {
|
||||
func (tx *UsbGadgetTransaction) WriteUDC(mtpServer bool) {
|
||||
// bound the gadget to a UDC (USB Device Controller)
|
||||
path := path.Join(tx.kvmGadgetPath, "UDC")
|
||||
if mtpServer {
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Key: "udc",
|
||||
Path: path,
|
||||
ExpectedState: FileStateFileContentMatch,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
DependsOn: []string{"mtp"},
|
||||
Description: "write UDC",
|
||||
})
|
||||
} else {
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Key: "udc",
|
||||
Path: path,
|
||||
@@ -327,6 +352,7 @@ func (tx *UsbGadgetTransaction) WriteUDC() {
|
||||
Description: "write UDC",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
|
||||
// remove the gadget from the UDC
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package usbgadget
|
||||
|
||||
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
||||
const udcPath = "/sys/kernel/config/usb_gadget/kvm/UDC"
|
||||
|
||||
@@ -23,3 +23,10 @@ var massStorageLun0Config = gadgetConfigItem{
|
||||
"inquiry_string": "KVM Virtual Media",
|
||||
},
|
||||
}
|
||||
|
||||
var mtpConfig = gadgetConfigItem{
|
||||
order: 3003,
|
||||
device: "ffs.mtp",
|
||||
path: []string{"functions", "ffs.mtp"},
|
||||
configPath: []string{"ffs.mtp"},
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (u *UsbGadget) IsUDCBound() (bool, error) {
|
||||
|
||||
// BindUDC binds the gadget to the UDC.
|
||||
func (u *UsbGadget) BindUDC() error {
|
||||
err := os.WriteFile(path.Join(dwc3Path, "bind"), []byte(u.udc), 0644)
|
||||
err := os.WriteFile(path.Join(udcPath, "bind"), []byte(u.udc), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error binding UDC: %w", err)
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (u *UsbGadget) BindUDC() error {
|
||||
|
||||
// UnbindUDC unbinds the gadget from the UDC.
|
||||
func (u *UsbGadget) UnbindUDC() error {
|
||||
err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(u.udc), 0644)
|
||||
err := os.WriteFile(path.Join(udcPath, " "), []byte(u.udc), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unbinding UDC: %w", err)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type Devices struct {
|
||||
RelativeMouse bool `json:"relative_mouse"`
|
||||
Keyboard bool `json:"keyboard"`
|
||||
MassStorage bool `json:"mass_storage"`
|
||||
Mtp bool `json:"mtp"`
|
||||
Audio bool `json:"audio"`
|
||||
}
|
||||
|
||||
@@ -88,6 +89,9 @@ type UsbGadget struct {
|
||||
const configFSPath = "/sys/kernel/config"
|
||||
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
||||
|
||||
const functionFSPath = "/dev/ffs-mtp"
|
||||
const umtprdPath = "/usr/sbin/umtprd"
|
||||
|
||||
var defaultLogger = logging.GetSubsystemLogger("usbgadget")
|
||||
|
||||
// NewUsbGadget creates a new UsbGadget.
|
||||
|
||||
11
io.go
11
io.go
@@ -58,7 +58,8 @@ func setGPIOValue(pin int, status bool) error {
|
||||
}
|
||||
|
||||
func setLedMode(ledConfigPath string, mode string) error {
|
||||
if mode == "network-link" {
|
||||
switch mode {
|
||||
case "network-link":
|
||||
err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED trigger: %v", err)
|
||||
@@ -71,7 +72,7 @@ func setLedMode(ledConfigPath string, mode string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED link: %v", err)
|
||||
}
|
||||
} else if mode == "network-tx" {
|
||||
case "network-tx":
|
||||
err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED trigger: %v", err)
|
||||
@@ -84,7 +85,7 @@ func setLedMode(ledConfigPath string, mode string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED tx: %v", err)
|
||||
}
|
||||
} else if mode == "network-rx" {
|
||||
case "network-rx":
|
||||
err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED trigger: %v", err)
|
||||
@@ -97,12 +98,12 @@ func setLedMode(ledConfigPath string, mode string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED rx: %v", err)
|
||||
}
|
||||
} else if mode == "kernel-activity" {
|
||||
case "kernel-activity":
|
||||
err := os.WriteFile(ledConfigPath+"/trigger", []byte("activity"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LED trigger: %v", err)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
return fmt.Errorf("invalid LED mode: %s", mode)
|
||||
}
|
||||
return nil
|
||||
|
||||
21
jsonrpc.go
21
jsonrpc.go
@@ -793,6 +793,13 @@ func updateUsbRelatedConfig() error {
|
||||
}
|
||||
|
||||
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
|
||||
mediaState, _ := rpcGetVirtualMediaState()
|
||||
if mediaState != nil && mediaState.Filename != "" {
|
||||
err := rpcUnmountImage()
|
||||
if err != nil {
|
||||
jsonRpcLogger.Error().Err(err).Msg("failed to unmount image")
|
||||
}
|
||||
}
|
||||
config.UsbDevices = &usbDevices
|
||||
gadget.SetGadgetDevices(config.UsbDevices)
|
||||
return updateUsbRelatedConfig()
|
||||
@@ -978,6 +985,13 @@ func rpcSetAudioMode(mode string) error {
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
if config.AudioMode != "disabled" {
|
||||
StartNtpAudioServer(handleAudioClient)
|
||||
} else {
|
||||
StopNtpAudioServer()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1064,6 +1078,8 @@ var rpcHandlers = map[string]RPCHandler{
|
||||
"getStorageSpace": {Func: rpcGetStorageSpace},
|
||||
"getSDStorageSpace": {Func: rpcGetSDStorageSpace},
|
||||
"resetSDStorage": {Func: rpcResetSDStorage},
|
||||
"mountSDStorage": {Func: rpcMountSDStorage},
|
||||
"unmountSDStorage": {Func: rpcUnmountSDStorage},
|
||||
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
||||
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
||||
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
||||
@@ -1113,4 +1129,9 @@ var rpcHandlers = map[string]RPCHandler{
|
||||
"setUpdateSource": {Func: rpcSetUpdateSource, Params: []string{"source"}},
|
||||
"getAudioMode": {Func: rpcGetAudioMode},
|
||||
"setAudioMode": {Func: rpcSetAudioMode, Params: []string{"mode"}},
|
||||
"startFrpc": {Func: rpcStartFrpc, Params: []string{"frpcToml"}},
|
||||
"stopFrpc": {Func: rpcStopFrpc},
|
||||
"getFrpcStatus": {Func: rpcGetFrpcStatus},
|
||||
"getFrpcToml": {Func: rpcGetFrpcToml},
|
||||
"getFrpcLog": {Func: rpcGetFrpcLog},
|
||||
}
|
||||
|
||||
4
main.go
4
main.go
@@ -14,6 +14,7 @@ import (
|
||||
var appCtx context.Context
|
||||
|
||||
func Main() {
|
||||
SyncConfigSD(true)
|
||||
LoadConfig()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
@@ -105,7 +106,6 @@ func Main() {
|
||||
logger.Warn().Err(err).Msg("failed to extract and run vpn bin")
|
||||
//TODO: prepare an error message screen buffer to show on kvm screen
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// initialize usb gadget
|
||||
@@ -129,6 +129,8 @@ func Main() {
|
||||
// Initialize VPN
|
||||
initVPN()
|
||||
|
||||
initSystemInfo()
|
||||
|
||||
//Auto update
|
||||
//go func() {
|
||||
// time.Sleep(15 * time.Minute)
|
||||
|
||||
8
ota.go
8
ota.go
@@ -59,7 +59,7 @@ var UpdateMetadataUrls = []string{
|
||||
"https://api.github.com/repos/luckfox-eng29/kvm/releases/latest",
|
||||
}
|
||||
|
||||
var builtAppVersion = "0.0.1+dev"
|
||||
var builtAppVersion = "0.0.2+dev"
|
||||
|
||||
var updateSource = "github"
|
||||
|
||||
@@ -136,11 +136,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
||||
appSha256 = release.Assets[0].Digest
|
||||
}
|
||||
|
||||
// add sha256 prefix
|
||||
if strings.HasPrefix(appSha256, "sha256:") {
|
||||
// delete "sha256:"
|
||||
appSha256 = appSha256[7:]
|
||||
}
|
||||
appSha256 = strings.TrimPrefix(appSha256, "sha256:")
|
||||
|
||||
remoteMetadata := &RemoteMetadata{
|
||||
AppUrl: updateUrl,
|
||||
|
||||
50
ui/package-lock.json
generated
50
ui/package-lock.json
generated
@@ -649,18 +649,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
|
||||
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/core": "^0.15.2",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
|
||||
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||
@@ -2136,9 +2148,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2574,9 +2586,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -4234,6 +4246,18 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -6764,9 +6788,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
BIN
ui/src/assets/tailscale.png
Normal file
BIN
ui/src/assets/tailscale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 637 B |
BIN
ui/src/assets/zerotier.png
Normal file
BIN
ui/src/assets/zerotier.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -11,6 +11,7 @@ import {
|
||||
useMountMediaStore,
|
||||
useSettingsStore,
|
||||
useUiStore,
|
||||
useAudioModeStore,
|
||||
} from "@/hooks/stores";
|
||||
import Container from "@components/Container";
|
||||
import { cx } from "@/cva.config";
|
||||
@@ -41,9 +42,9 @@ export default function Actionbar({
|
||||
const developerMode = useSettingsStore(state => state.developerMode);
|
||||
|
||||
// Audio related
|
||||
const [audioMode, setAudioMode] = useState("disabled");
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const audioMode = useAudioModeStore(state => state.audioMode);
|
||||
const setAudioMode = useAudioModeStore(state => state.setAudioMode);
|
||||
|
||||
// This is the only way to get a reliable state change for the popover
|
||||
// at time of writing this there is no mount, or unmount event for the popover
|
||||
@@ -126,11 +127,11 @@ export default function Actionbar({
|
||||
return (
|
||||
<>
|
||||
<LuHardDrive className={className} />
|
||||
<div
|
||||
{/*<div
|
||||
className={cx(className, "h-2 w-2 rounded-full bg-blue-700", {
|
||||
hidden: !remoteVirtualMediaState,
|
||||
})}
|
||||
/>
|
||||
/>*/}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
190
ui/src/components/LogDialog.tsx
Normal file
190
ui/src/components/LogDialog.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
type Variant = "danger" | "success" | "warning" | "info";
|
||||
|
||||
interface LogDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
variant?: Variant;
|
||||
cancelText?: string | null;
|
||||
}
|
||||
|
||||
const variantConfig = {
|
||||
danger: {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClass: "text-red-599",
|
||||
iconBgClass: "bg-red-99",
|
||||
buttonTheme: "danger",
|
||||
},
|
||||
success: {
|
||||
icon: CheckCircleIcon,
|
||||
iconClass: "text-green-599",
|
||||
iconBgClass: "bg-green-99",
|
||||
buttonTheme: "primary",
|
||||
},
|
||||
warning: {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClass: "text-yellow-599",
|
||||
iconBgClass: "bg-yellow-99",
|
||||
buttonTheme: "lightDanger",
|
||||
},
|
||||
info: {
|
||||
icon: InformationCircleIcon,
|
||||
iconClass: "text-blue-599",
|
||||
iconBgClass: "bg-blue-99",
|
||||
buttonTheme: "blank",
|
||||
},
|
||||
} as Record<
|
||||
Variant,
|
||||
{
|
||||
icon: React.ElementType;
|
||||
iconClass: string;
|
||||
iconBgClass: string;
|
||||
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
||||
}
|
||||
>;
|
||||
|
||||
const COLOR_MAP: Record<string, string | undefined> = {
|
||||
'30': '#000', '31': '#d32f2f', '32': '#388e3c', '33': '#f57c00',
|
||||
'34': '#1976d2', '35': '#7b1fa2', '36': '#0097a7', '37': '#424242',
|
||||
'90': '#757575', '91': '#f44336', '92': '#4caf50', '93': '#ff9800',
|
||||
'94': '#2196f3', '95': '#9c27b0', '96': '#00bcd4', '97': '#fafafa',
|
||||
};
|
||||
|
||||
interface AnsiProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Ansi({ children, className }: AnsiProps) {
|
||||
let curColor: string | undefined;
|
||||
let curBold = false;
|
||||
|
||||
const lines: { text: string; style: (React.CSSProperties | undefined)[] }[] = [];
|
||||
let col = 0;
|
||||
|
||||
const applyCode = (code: number) => {
|
||||
if (code === 0) { curColor = undefined; curBold = false; }
|
||||
else if (code === 1) curBold = true;
|
||||
else if (code >= 30 && code <= 37) curColor = COLOR_MAP[code];
|
||||
else if (code >= 90 && code <= 97) curColor = COLOR_MAP[code];
|
||||
};
|
||||
|
||||
const styleKey = () => `${curColor || ''}|${curBold ? 1 : 0}`;
|
||||
const stylePool: Record<string, React.CSSProperties> = {};
|
||||
const getStyle = (): React.CSSProperties | undefined => {
|
||||
const key = styleKey();
|
||||
if (!key) return undefined;
|
||||
if (!stylePool[key]) {
|
||||
stylePool[key] = {
|
||||
...(curColor ? { color: curColor } : {}),
|
||||
...(curBold ? { fontWeight: 'bold' } : {}),
|
||||
};
|
||||
}
|
||||
return stylePool[key];
|
||||
};
|
||||
|
||||
const tokens = children.split(/(\x1b\[[0-9;]*m|\r\n?|\n)/g);
|
||||
let currentLine = { text: '', style: [] as (React.CSSProperties | undefined)[] };
|
||||
|
||||
for (const chunk of tokens) {
|
||||
if (chunk.startsWith('\x1b[') && chunk.endsWith('m')) {
|
||||
const codes = chunk.slice(2, -1).split(';').map(Number);
|
||||
codes.forEach(applyCode);
|
||||
} else if (chunk === '\r\n' || chunk === '\n') {
|
||||
if (currentLine.text) lines.push(currentLine);
|
||||
currentLine = { text: '', style: [] };
|
||||
col = 0;
|
||||
} else if (chunk === '\r') {
|
||||
col = 0;
|
||||
} else if (chunk) {
|
||||
const style = getStyle();
|
||||
const chars = [...chunk]; // 正确识别 Unicode 码点
|
||||
for (const ch of chars) {
|
||||
if (col < currentLine.text.length) {
|
||||
currentLine.text =
|
||||
currentLine.text.slice(0, col) +
|
||||
ch +
|
||||
currentLine.text.slice(col + 1);
|
||||
currentLine.style[col] = style;
|
||||
} else {
|
||||
currentLine.text += ch;
|
||||
currentLine.style[col] = style;
|
||||
}
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentLine.text) lines.push(currentLine);
|
||||
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{lines.map((ln, idx) => (
|
||||
<div key={idx}>
|
||||
{[...ln.text].map((ch, i) => (
|
||||
<span key={i} style={ln.style[i]}>
|
||||
{ch}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogDialog({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
variant = "info",
|
||||
cancelText = "Cancel",
|
||||
}: LogDialogProps) {
|
||||
const { icon: Icon, iconClass, iconBgClass } = variantConfig[variant];
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className="mx-auto max-w-xl px-3 transition-all duration-300 ease-in-out">
|
||||
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-5 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
|
||||
<div className="space-y-3">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className={cx(
|
||||
"mx-auto flex size-11 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
||||
iconBgClass,
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className={cx("size-5", iconClass)} />
|
||||
</div>
|
||||
<div className="mt-2 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
<Ansi>{description}</Ansi>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-x-1">
|
||||
{cancelText && (
|
||||
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
196
ui/src/components/UsbEpModeSetting.tsx
Normal file
196
ui/src/components/UsbEpModeSetting.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useCallback , useEffect, useState } from "react";
|
||||
|
||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { SettingsItem } from "../routes/devices.$id.settings";
|
||||
|
||||
import Checkbox from "./Checkbox";
|
||||
import { Button } from "./Button";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
import { SettingsSectionHeader } from "./SettingsSectionHeader";
|
||||
import Fieldset from "./Fieldset";
|
||||
import { useUsbEpModeStore, useAudioModeStore } from "../hooks/stores";
|
||||
|
||||
export interface UsbDeviceConfig {
|
||||
keyboard: boolean;
|
||||
absolute_mouse: boolean;
|
||||
relative_mouse: boolean;
|
||||
mass_storage: boolean;
|
||||
mtp: boolean;
|
||||
audio: boolean;
|
||||
}
|
||||
|
||||
const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
||||
keyboard: true,
|
||||
absolute_mouse: true,
|
||||
relative_mouse: true,
|
||||
mass_storage: true,
|
||||
mtp: false,
|
||||
audio: true,
|
||||
};
|
||||
|
||||
const usbEpOptions = [
|
||||
{ value: "uac", label: "USB Audio Card"},
|
||||
{ value: "mtp", label: "Media Transfer Protocol"},
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
]
|
||||
|
||||
const audioModeOptions = [
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
{ value: "usb", label: "USB"},
|
||||
//{ value: "hdmi", label: "HDMI"},
|
||||
]
|
||||
|
||||
|
||||
export function UsbEpModeSetting() {
|
||||
const usbEpMode = useUsbEpModeStore(state => state.usbEpMode)
|
||||
const setUsbEpMode = useUsbEpModeStore(state => state.setUsbEpMode)
|
||||
|
||||
const audioMode = useAudioModeStore(state => state.audioMode);
|
||||
const setAudioMode = useAudioModeStore(state => state.setAudioMode);
|
||||
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [usbDeviceConfig, setUsbDeviceConfig] =
|
||||
useState<UsbDeviceConfig>(defaultUsbDeviceConfig);
|
||||
|
||||
const syncUsbDeviceConfig = useCallback(() => {
|
||||
send("getUsbDevices", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.error);
|
||||
notifications.error(
|
||||
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||
setUsbDeviceConfig(usbConfigState);
|
||||
if (usbConfigState.mtp && !usbConfigState.audio) {
|
||||
setUsbEpMode("mtp");
|
||||
} else if (usbConfigState.audio && !usbConfigState.mtp) {
|
||||
setUsbEpMode("uac");
|
||||
} else {
|
||||
setUsbEpMode("disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUsbConfigChange = useCallback(
|
||||
(devices: UsbDeviceConfig) => {
|
||||
setLoading(true);
|
||||
send("setUsbDevices", { devices }, async resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need some time to ensure the USB devices are updated
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncUsbDeviceConfig();
|
||||
notifications.success(`USB Devices updated`);
|
||||
});
|
||||
},
|
||||
[send, syncUsbDeviceConfig],
|
||||
);
|
||||
|
||||
const handleAudioModeChange = (mode: string) => {
|
||||
send("setAudioMode", { mode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set Audio Mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success(`Audio Mode set to ${mode}.It takes effect after refreshing the page`);
|
||||
setAudioMode(mode);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleUsbEpModeChange = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newMode = e.target.value;
|
||||
setUsbEpMode(newMode);
|
||||
|
||||
if (newMode === "uac") {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: true,
|
||||
mtp: false,
|
||||
})
|
||||
setUsbEpMode("uac");
|
||||
} else if (newMode === "mtp") {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: false,
|
||||
mtp: true,
|
||||
})
|
||||
handleAudioModeChange("disabled");
|
||||
setUsbEpMode("mtp");
|
||||
} else {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: false,
|
||||
mtp: false,
|
||||
})
|
||||
handleAudioModeChange("disabled");
|
||||
setUsbEpMode("disabled");
|
||||
}
|
||||
},
|
||||
[handleUsbConfigChange, usbDeviceConfig],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
syncUsbDeviceConfig();
|
||||
|
||||
send("getAudioMode", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setAudioMode(String(resp.result));
|
||||
});
|
||||
|
||||
}, [syncUsbDeviceConfig]);
|
||||
|
||||
return (
|
||||
<Fieldset disabled={loading} className="space-y-4">
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="USB Other Function"
|
||||
description="Select the active USB function (MTP or UAC)"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={usbEpMode}
|
||||
fullWidth
|
||||
onChange={handleUsbEpModeChange}
|
||||
options={usbEpOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{usbEpMode === "uac" && (
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="Audio Mode"
|
||||
badge="Experimental"
|
||||
description="Set the working mode of the audio"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={audioMode}
|
||||
options={audioModeOptions}
|
||||
onChange={e => handleAudioModeChange(e.target.value)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
)}
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LuPlay } from "react-icons/lu";
|
||||
import { LuPlay, LuView } from "react-icons/lu";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
interface OverlayContentProps {
|
||||
readonly children: React.ReactNode;
|
||||
@@ -215,6 +218,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||
const isNoSignal = hdmiState === "no_signal";
|
||||
const isOtherError = hdmiState === "no_lock" || hdmiState === "out_of_range";
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const onSendUsbWakeupSignal = useCallback(() => {
|
||||
send("sendUsbWakeupSignal", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to send USB wakeup signal: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
@@ -245,8 +260,20 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||
If using an adapter, ensure it's compatible and functioning
|
||||
correctly
|
||||
</li>
|
||||
<li>
|
||||
Ensure source device is not in sleep mode and outputting a signal
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
theme="light"
|
||||
text="Try Wakeup"
|
||||
TrailingIcon={LuView}
|
||||
size="SM"
|
||||
onClick={onSendUsbWakeupSignal}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
to={"https://wiki.luckfox.com/intro"}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import StatusCard from "@components/StatusCards";
|
||||
|
||||
import TailscaleIcon from "@/assets/tailscale.png";
|
||||
import ZeroTierIcon from "@/assets/zerotier.png";
|
||||
|
||||
const VpnConnectionStatusMap = {
|
||||
connected: "Connected",
|
||||
connecting: "Connecting",
|
||||
@@ -44,6 +47,24 @@ export default function VpnConnectionStatusCard({
|
||||
const props = StatusCardProps[state];
|
||||
if (!props) return;
|
||||
|
||||
const Icon = () => {
|
||||
if (title === "ZeroTier") {
|
||||
return (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-gray-300 dark:bg-gray-800">
|
||||
<img src={ZeroTierIcon} alt="zerotier" className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (title === "TailScale") {
|
||||
return (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-gray-800 dark:bg-gray-800">
|
||||
<img src={TailscaleIcon} alt="tailscale" className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
title={title || "Vpn Network"}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useMemo, forwardRef, useEffect, useCallback } from "react";
|
||||
import { useMemo, forwardRef, useEffect, useCallback, useState } from "react";
|
||||
import {
|
||||
LuArrowUpFromLine,
|
||||
LuCheckCheck,
|
||||
LuLink,
|
||||
LuPlus,
|
||||
LuRadioReceiver,
|
||||
LuFileBadge,
|
||||
LuFlagOff,
|
||||
} from "react-icons/lu";
|
||||
import { useClose } from "@headlessui/react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -14,11 +16,14 @@ import { useLocation } from "react-router-dom";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { formatters } from "@/utils";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore, useUsbEpModeStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import notifications from "@/notifications";
|
||||
import { SelectMenuBasic } from "../SelectMenuBasic";
|
||||
import { SettingsItem } from "../../routes/devices.$id.settings";
|
||||
import { UsbDeviceConfig } from "@components/UsbEpModeSetting";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
|
||||
@@ -26,6 +31,29 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||
useMountMediaStore();
|
||||
|
||||
const usbEpMode = useUsbEpModeStore(state => state.usbEpMode)
|
||||
const setUsbEpMode = useUsbEpModeStore(state => state.setUsbEpMode)
|
||||
|
||||
const [usbStorageMode, setUsbStorageMode] = useState("ums");
|
||||
const usbStorageModeOptions = [
|
||||
{ value: "ums", label: "USB Mass Storage"},
|
||||
{ value: "mtp", label: "MTP"},
|
||||
]
|
||||
|
||||
const getUsbEpMode = useCallback(() => {
|
||||
send("getUsbDevices", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.error);
|
||||
notifications.error(
|
||||
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||
setUsbEpMode(usbConfigState.mtp ? "mtp" : "uac");
|
||||
}
|
||||
});
|
||||
}, [send])
|
||||
|
||||
const bytesSentPerSecond = useMemo(() => {
|
||||
if (diskDataChannelStats.size < 2) return null;
|
||||
|
||||
@@ -68,6 +96,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleUsbStorageModeChange = (value: string) => {
|
||||
setUsbStorageMode(value);
|
||||
}
|
||||
|
||||
const renderGridCardContent = () => {
|
||||
if (!remoteVirtualMediaState) {
|
||||
return (
|
||||
@@ -187,7 +219,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
syncRemoteVirtualMediaState();
|
||||
}, [syncRemoteVirtualMediaState, location.pathname]);
|
||||
getUsbEpMode();
|
||||
}, [syncRemoteVirtualMediaState, location.pathname, getUsbEpMode]);
|
||||
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
@@ -202,6 +235,21 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
description="Mount an image to boot from or install an operating system."
|
||||
/>
|
||||
|
||||
<SettingsItem
|
||||
title="USB Storage Mode"
|
||||
description=""
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={usbStorageMode}
|
||||
fullWidth
|
||||
onChange={(e) => handleUsbStorageModeChange(e.target.value)}
|
||||
options={usbStorageModeOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
|
||||
{remoteVirtualMediaState?.source === "WebRTC" ? (
|
||||
<Card>
|
||||
<div className="flex items-center gap-x-1.5 px-2.5 py-2 text-sm">
|
||||
@@ -213,6 +261,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{usbStorageMode === "ums" && (
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
@@ -283,11 +332,48 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usbStorageMode === "mtp" && usbEpMode !== "mtp" && (
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div className="block select-none">
|
||||
<div className="group">
|
||||
<Card>
|
||||
<div className="w-full px-4 py-8">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
<div className="space-y-1">
|
||||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuFlagOff className="h-4 w-4 shrink-0 text-blue-700 dark:text-white" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
The MTP function has not been activated.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!remoteVirtualMediaState && (
|
||||
{!remoteVirtualMediaState && usbStorageMode === "ums" && (
|
||||
<div
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
@@ -315,6 +401,36 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usbStorageMode === "mtp" && usbEpMode === "mtp" && (
|
||||
<div
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Manager"
|
||||
onClick={() => {
|
||||
setModalView("mode");
|
||||
navigateTo("/mtp");
|
||||
}}
|
||||
LeadingIcon={LuFileBadge}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</GridCard>
|
||||
);
|
||||
|
||||
@@ -434,7 +434,7 @@ export interface MountMediaState {
|
||||
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
||||
|
||||
modalView: "mode" | "browser" | "url" | "device" | "sd" | "upload" | "upload_sd" | "error" | null;
|
||||
modalView: "mode" | "browser" | "url" | "device" | "sd" | "upload" | "upload_sd" | "error" | "mtp_device" | "mtp_sd" | null;
|
||||
setModalView: (view: MountMediaState["modalView"]) => void;
|
||||
|
||||
isMountMediaDialogOpen: boolean;
|
||||
@@ -567,6 +567,26 @@ export const useHidStore = create<HidState>((set, get) => ({
|
||||
setUsbState: state => set({ usbState: state }),
|
||||
}));
|
||||
|
||||
|
||||
export interface UsbEpModeStore {
|
||||
usbEpMode: string;
|
||||
setUsbEpMode: (mode: string) => void;
|
||||
}
|
||||
|
||||
export const useUsbEpModeStore = create<UsbEpModeStore>(set => ({
|
||||
usbEpMode: "disabled",
|
||||
setUsbEpMode: (mode: string) => set({ usbEpMode: mode }),
|
||||
}));
|
||||
|
||||
export interface AudioModeStore {
|
||||
audioMode: string;
|
||||
setAudioMode: (mode: string) => void;
|
||||
}
|
||||
export const useAudioModeStore = create<AudioModeStore>(set => ({
|
||||
audioMode: "disabled",
|
||||
setAudioMode: (mode: string) => set({ audioMode: mode }),
|
||||
}));
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
user: null,
|
||||
setUser: user => set({ user }),
|
||||
|
||||
@@ -31,6 +31,7 @@ import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||
import { DEVICE_API } from "./ui.config";
|
||||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||
import MountRoute from "./routes/devices.$id.mount";
|
||||
import MtpRoute from "./routes/devices.$id.mtp";
|
||||
import * as SettingsRoute from "./routes/devices.$id.settings";
|
||||
import SettingsMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsKeyboardRoute from "./routes/devices.$id.settings.keyboard";
|
||||
@@ -109,6 +110,10 @@ export async function checkAuth() {
|
||||
path: "mount",
|
||||
element: <MountRoute />,
|
||||
},
|
||||
{
|
||||
path: "mtp",
|
||||
element: <MtpRoute />,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <SettingsRoute.default />,
|
||||
|
||||
@@ -109,6 +109,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
function handleStorageMount(fileName: string, mode: RemoteVirtualMediaState["mode"]) {
|
||||
console.log(`Mounting ${fileName} as ${mode}`);
|
||||
|
||||
if (!fileName.endsWith(".iso") && !fileName.endsWith(".img")) {
|
||||
triggerError("Only ISO and IMG files are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
setMountInProgress(true);
|
||||
send("mountWithStorage", { filename: fileName, mode }, async resp => {
|
||||
if ("error" in resp) triggerError(resp.error.message);
|
||||
@@ -136,6 +141,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
function handleSDStorageMount(fileName: string, mode: RemoteVirtualMediaState["mode"]) {
|
||||
console.log(`Mounting ${fileName} as ${mode}`);
|
||||
|
||||
if (!fileName.endsWith(".iso") && !fileName.endsWith(".img")) {
|
||||
triggerError("Only ISO and IMG files are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
setMountInProgress(true);
|
||||
send("mountWithSDStorage", { filename: fileName, mode }, async resp => {
|
||||
if ("error" in resp) triggerError(resp.error.message);
|
||||
@@ -407,7 +417,7 @@ function ModeSelectionView({
|
||||
<div
|
||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||
onClick={() =>
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device" | "sd")
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -1069,6 +1079,7 @@ function SDFileView({
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const filesPerPage = 5;
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
@@ -1196,17 +1207,37 @@ function SDFileView({
|
||||
setCurrentPage(prev => Math.min(prev + 1, totalPages));
|
||||
};
|
||||
|
||||
function handleResetSDStorage() {
|
||||
async function handleResetSDStorage() {
|
||||
setLoading(true);
|
||||
send("resetSDStorage", {}, res => {
|
||||
console.log("Reset SD storage response:", res);
|
||||
if ("error" in res) {
|
||||
notifications.error(`Failed to reset SD card`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncStorage();
|
||||
}
|
||||
|
||||
async function handleUnmountSDStorage() {
|
||||
setLoading(true);
|
||||
send("unmountSDStorage", {}, res => {
|
||||
console.log("Unmount SD response:", res);
|
||||
if ("error" in res) {
|
||||
notifications.error(`Failed to unmount SD card`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncStorage();
|
||||
}
|
||||
|
||||
|
||||
if (sdMountStatus && sdMountStatus !== "ok") {
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
@@ -1223,6 +1254,7 @@ function SDFileView({
|
||||
: "SD card mount failed"}
|
||||
<Button
|
||||
size="XS"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
LeadingIcon={LuRefreshCw}
|
||||
onClick={handleResetSDStorage}
|
||||
@@ -1268,6 +1300,7 @@ function SDFileView({
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
disabled={loading}
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
onClick={() => onNewImageClick()}
|
||||
@@ -1312,14 +1345,14 @@ function SDFileView({
|
||||
theme="light"
|
||||
text="Previous"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
disabled={currentPage === 1 || loading}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1344,7 +1377,7 @@ function SDFileView({
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={selected === null || mountInProgress}
|
||||
disabled={selected === null || mountInProgress || loading}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
loading={mountInProgress}
|
||||
@@ -1412,6 +1445,7 @@ function SDFileView({
|
||||
>
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
@@ -1419,6 +1453,24 @@ function SDFileView({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.25s",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Unmount SD Card"
|
||||
onClick={() => handleUnmountSDStorage()}
|
||||
className="text-red-500 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1626
ui/src/routes/devices.$id.mtp.tsx
Normal file
1626
ui/src/routes/devices.$id.mtp.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,8 @@ import { CloudState } from "./adopt";
|
||||
import { useVpnStore } from "@/hooks/stores";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
|
||||
import { LogDialog } from "../components/LogDialog";
|
||||
|
||||
export interface TailScaleResponse {
|
||||
state: string;
|
||||
loginUrl: string;
|
||||
@@ -35,6 +37,11 @@ export interface ZeroTierResponse {
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export interface FrpcResponse {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface TLSState {
|
||||
mode: "self-signed" | "custom" | "disabled";
|
||||
certificate?: string;
|
||||
@@ -84,6 +91,11 @@ export default function SettingsAccessIndexRoute() {
|
||||
const [tempNetworkID, setTempNetworkID] = useState("");
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
|
||||
const [frpcToml, setFrpcToml] = useState<string>("");
|
||||
const [frpcLog, setFrpcLog] = useState<string>("");
|
||||
const [showFrpcLogModal, setShowFrpcLogModal] = useState(false);
|
||||
const [frpcStatus, setFrpcRunningStatus] = useState<FrpcResponse>({ running: false });
|
||||
|
||||
const getTLSState = useCallback(() => {
|
||||
send("getTLSState", {}, resp => {
|
||||
if ("error" in resp) return console.error(resp.error);
|
||||
@@ -262,6 +274,77 @@ export default function SettingsAccessIndexRoute() {
|
||||
});
|
||||
},[send, zeroTierNetworkID]);
|
||||
|
||||
const handleStartFrpc = useCallback(() => {
|
||||
send("startFrpc", { frpcToml }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to start frpc: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcRunningStatus({ running: false });
|
||||
return;
|
||||
}
|
||||
notifications.success("frpc started");
|
||||
setFrpcRunningStatus({ running: true });
|
||||
});
|
||||
}, [send, frpcToml]);
|
||||
|
||||
const handleStopFrpc = useCallback(() => {
|
||||
send("stopFrpc", { frpcToml }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to stop frpc: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("frpc stopped");
|
||||
setFrpcRunningStatus({ running: false });
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleGetFrpcLog = useCallback(() => {
|
||||
send("getFrpcLog", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc log: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcLog("");
|
||||
return;
|
||||
}
|
||||
setFrpcLog(resp.result as string);
|
||||
setShowFrpcLogModal(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getFrpcToml = useCallback(() => {
|
||||
send("getFrpcToml", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc toml: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcToml("");
|
||||
return;
|
||||
}
|
||||
setFrpcToml(resp.result as string);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getFrpcStatus = useCallback(() => {
|
||||
send("getFrpcStatus", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc status: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setFrpcRunningStatus(resp.result as FrpcResponse);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
useEffect(() => {
|
||||
getFrpcStatus();
|
||||
getFrpcToml();
|
||||
}, [getFrpcStatus, getFrpcToml]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
@@ -399,16 +482,6 @@ export default function SettingsAccessIndexRoute() {
|
||||
badge="Experimental"
|
||||
description="Connect to TailScale VPN network"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{ ((tailScaleConnectionState === "disconnected") || (tailScaleConnectionState === "closed")) && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable TailScale"
|
||||
onClick={handleTailScaleLogin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title=""
|
||||
@@ -425,6 +498,21 @@ export default function SettingsAccessIndexRoute() {
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title=""
|
||||
description=""
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{ ((tailScaleConnectionState === "disconnected") || (tailScaleConnectionState === "closed")) && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable"
|
||||
onClick={handleTailScaleLogin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -558,7 +646,58 @@ export default function SettingsAccessIndexRoute() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Frp"
|
||||
description="Connect to Frp Server"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Edit frpc.toml"
|
||||
placeholder="Enter frpc settings"
|
||||
value={frpcToml || ""}
|
||||
rows={3}
|
||||
onChange={e => setFrpcToml(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{frpcStatus.running ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Stop frpc"
|
||||
onClick={handleStopFrpc}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Log"
|
||||
onClick={handleGetFrpcLog}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Start frpc"
|
||||
onClick={handleStartFrpc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<LogDialog
|
||||
open={showFrpcLogModal}
|
||||
onClose={() => {
|
||||
setShowFrpcLogModal(false);
|
||||
}}
|
||||
title="Frpc Log"
|
||||
description={frpcLog}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { SettingsItem } from "@routes/devices.$id.settings";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { InputField } from "@/components/InputField";
|
||||
import { Button, LinkButton } from "@/components/Button";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { UsbEpModeSetting } from "@components/UsbEpModeSetting";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
@@ -333,13 +331,9 @@ export default function SettingsHardwareRoute() {
|
||||
|
||||
</div>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbDeviceSetting />
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbInfoSetting />
|
||||
</FeatureFlag>
|
||||
<UsbEpModeSetting />
|
||||
{/*<UsbDeviceSetting /> */}
|
||||
{/*<UsbInfoSetting /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,9 @@ const streamQualityOptions = [
|
||||
{ value: "0.1", label: "Low" },
|
||||
];
|
||||
|
||||
const audioModeOptions = [
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
{ value: "usb", label: "USB"},
|
||||
//{ value: "hdmi", label: "HDMI"},
|
||||
]
|
||||
|
||||
export default function SettingsVideoRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const [streamQuality, setStreamQuality] = useState("1");
|
||||
const [audioMode, setAudioMode] = useState("disabled");
|
||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
const [edid, setEdid] = useState<string | null>(null);
|
||||
|
||||
@@ -62,11 +55,6 @@ export default function SettingsVideoRoute() {
|
||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||
|
||||
useEffect(() => {
|
||||
send("getAudioMode", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setAudioMode(String(resp.result));
|
||||
});
|
||||
|
||||
send("getStreamQualityFactor", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setStreamQuality(String(resp.result));
|
||||
@@ -96,20 +84,6 @@ export default function SettingsVideoRoute() {
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleAudioModeChange = (mode: string) => {
|
||||
send("setAudioMode", { mode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set Audio Mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success(`Audio Mode set to ${audioModeOptions.find(x => x.value === mode )?.label}.It takes effect after refreshing the page`);
|
||||
setAudioMode(mode);
|
||||
});
|
||||
};
|
||||
|
||||
const handleStreamQualityChange = (factor: string) => {
|
||||
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
|
||||
if ("error" in resp) {
|
||||
@@ -149,20 +123,6 @@ export default function SettingsVideoRoute() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Audio Mode"
|
||||
badge="Experimental"
|
||||
description="Set the working mode of the audio"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={audioMode}
|
||||
options={audioModeOptions}
|
||||
onChange={e => handleAudioModeChange(e.target.value)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Stream Quality"
|
||||
description="Adjust the quality of the video stream"
|
||||
|
||||
@@ -233,7 +233,8 @@ export default function KvmIdRoute() {
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const { sendMessage, getWebSocket } = useWebSocket(
|
||||
`${wsProtocol}//${window.location.host}/webrtc/signaling/client?id=${params.id}`,
|
||||
//`${wsProtocol}//${window.location.host}/webrtc/signaling/client?id=${params.id}`,
|
||||
`${wsProtocol}//${window.location.host}/webrtc/signaling/client`,
|
||||
{
|
||||
heartbeat: true,
|
||||
retryOnError: true,
|
||||
@@ -362,9 +363,18 @@ export default function KvmIdRoute() {
|
||||
setLoadingMessage("Creating peer connection...");
|
||||
pc = new RTCPeerConnection({
|
||||
// We only use STUN or TURN servers if we're in the cloud
|
||||
...(isInCloud && iceConfig?.iceServers
|
||||
//...(isInCloud && iceConfig?.iceServers
|
||||
// ? { iceServers: [iceConfig?.iceServers] }
|
||||
// : {}),
|
||||
...(iceConfig?.iceServers
|
||||
? { iceServers: [iceConfig?.iceServers] }
|
||||
: {}),
|
||||
: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: ['stun:stun.l.google.com:19302']
|
||||
}
|
||||
]
|
||||
}),
|
||||
});
|
||||
|
||||
setPeerConnectionState(pc.connectionState);
|
||||
|
||||
39
usb.go
39
usb.go
@@ -12,6 +12,17 @@ var gadget *usbgadget.UsbGadget
|
||||
// initUsbGadget initializes the USB gadget.
|
||||
// call it only after the config is loaded.
|
||||
func initUsbGadget() {
|
||||
resp, _ := rpcGetSDMountStatus()
|
||||
if resp.Status == SDMountOK {
|
||||
if err := writeUmtprdConfFile(true); err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to write umtprd conf file")
|
||||
}
|
||||
} else {
|
||||
if err := writeUmtprdConfFile(false); err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to write umtprd conf file")
|
||||
}
|
||||
}
|
||||
|
||||
gadget = usbgadget.NewUsbGadget(
|
||||
"kvm",
|
||||
config.UsbDevices,
|
||||
@@ -38,6 +49,34 @@ func initUsbGadget() {
|
||||
}
|
||||
}
|
||||
|
||||
func initSystemInfo() {
|
||||
go func() {
|
||||
for {
|
||||
if !networkState.HasIPAssigned() {
|
||||
vpnLogger.Warn().Msg("waiting for network get IPv4 address, will retry in 3 seconds")
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
err := writeSystemInfoImg()
|
||||
if err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to create system_info.img")
|
||||
}
|
||||
|
||||
mediaState, _ := rpcGetVirtualMediaState()
|
||||
if mediaState != nil && mediaState.Filename == "system_info.img" {
|
||||
usbLogger.Error().Err(err).Msg("system_info.img is busy")
|
||||
} else if mediaState == nil || mediaState.Filename == "" {
|
||||
err = rpcMountWithStorage("system_info.img", Disk)
|
||||
if err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to mount system_info.img")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||
return gadget.KeyboardReport(modifier, keys)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func mountImage(imagePath string) error {
|
||||
|
||||
var nbdDevice *NBDDevice
|
||||
|
||||
const imagesFolder = "/userdata/picokvm/images"
|
||||
const imagesFolder = "/userdata/picokvm/share"
|
||||
const SDImagesFolder = "/mnt/sdcard"
|
||||
|
||||
func initImagesFolder() error {
|
||||
@@ -402,7 +402,7 @@ func rpcMountWithSDStorage(filename string, mode VirtualMediaMode) error {
|
||||
fullPath := filepath.Join(SDImagesFolder, filename)
|
||||
fileInfo, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[rpcMountWithStorage]failed to get file info: %w", err)
|
||||
return fmt.Errorf("[rpcMountWithSDStorage]failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
if err := setMassStorageMode(mode == CDROM); err != nil {
|
||||
@@ -694,6 +694,25 @@ func handleUploadHttp(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Upload completed"})
|
||||
}
|
||||
|
||||
func handleDownloadHttp(c *gin.Context) {
|
||||
filename := c.Query("file")
|
||||
if filename == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedFilename, err := sanitizeFilename(filename)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(imagesFolder, sanitizedFilename)
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", sanitizedFilename))
|
||||
c.FileAttachment(fullPath, sanitizedFilename)
|
||||
}
|
||||
|
||||
// SD Card
|
||||
func rpcGetSDStorageSpace() (*StorageSpace, error) {
|
||||
var stat syscall.Statfs_t
|
||||
@@ -715,13 +734,50 @@ func rpcGetSDStorageSpace() (*StorageSpace, error) {
|
||||
func rpcResetSDStorage() error {
|
||||
err := os.WriteFile("/sys/bus/platform/drivers/dwmmc_rockchip/unbind", []byte("ffaa0000.mmc"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to file: %v", err)
|
||||
logger.Error().Err(err).Msg("failed to unbind mmc")
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
err = os.WriteFile("/sys/bus/platform/drivers/dwmmc_rockchip/bind", []byte("ffaa0000.mmc"), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to file: %v", err)
|
||||
logger.Error().Err(err).Msg("failed to bind mmc")
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
err = updateMtpWithSDStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcMountSDStorage() error {
|
||||
err := os.WriteFile("/sys/bus/platform/drivers/dwmmc_rockchip/bind", []byte("ffaa0000.mmc"), 0644)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to bind mmc")
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
err = updateMtpWithSDStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcUnmountSDStorage() error {
|
||||
err := os.WriteFile("/sys/bus/platform/drivers/dwmmc_rockchip/unbind", []byte("ffaa0000.mmc"), 0644)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to unbind mmc")
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
err = updateMtpWithSDStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -861,3 +917,106 @@ func rpcStartSDStorageFileUpload(filename string, size int64) (*SDStorageFileUpl
|
||||
DataChannel: uploadId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleSDDownloadHttp(c *gin.Context) {
|
||||
filename := c.Query("file")
|
||||
if filename == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedFilename, err := sanitizeFilename(filename)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(SDImagesFolder, sanitizedFilename)
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", sanitizedFilename))
|
||||
c.FileAttachment(fullPath, sanitizedFilename)
|
||||
}
|
||||
|
||||
const umtprdConfPath = "/etc/umtprd/umtprd.conf"
|
||||
|
||||
var umtprdWriteLock sync.Mutex
|
||||
|
||||
func writeUmtprdConfFile(withSD bool) error {
|
||||
umtprdWriteLock.Lock()
|
||||
defer umtprdWriteLock.Unlock()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(umtprdConfPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create umtprd.conf dir: %w", err)
|
||||
}
|
||||
|
||||
conf := `loop_on_disconnect 1
|
||||
|
||||
storage "/userdata/picokvm/share" "share folder" "rw"
|
||||
`
|
||||
if withSD {
|
||||
conf += `storage "/mnt/sdcard" "sdcard folder" "rw"
|
||||
`
|
||||
}
|
||||
|
||||
conf += fmt.Sprintf(`
|
||||
manufacturer "%s"
|
||||
product "%s"
|
||||
serial "%s"
|
||||
|
||||
usb_vendor_id %s
|
||||
usb_product_id %s
|
||||
|
||||
usb_functionfs_mode 0x1
|
||||
|
||||
usb_dev_path "/dev/ffs-mtp/ep0"
|
||||
usb_epin_path "/dev/ffs-mtp/ep1"
|
||||
usb_epout_path "/dev/ffs-mtp/ep2"
|
||||
usb_epint_path "/dev/ffs-mtp/ep3"
|
||||
usb_max_packet_size 0x200
|
||||
`,
|
||||
config.UsbConfig.Manufacturer,
|
||||
config.UsbConfig.Product,
|
||||
config.UsbConfig.SerialNumber,
|
||||
config.UsbConfig.VendorId,
|
||||
config.UsbConfig.ProductId,
|
||||
)
|
||||
|
||||
return os.WriteFile(umtprdConfPath, []byte(conf), 0644)
|
||||
}
|
||||
|
||||
func updateMtpWithSDStatus() error {
|
||||
resp, _ := rpcGetSDMountStatus()
|
||||
if resp.Status == SDMountOK {
|
||||
if err := writeUmtprdConfFile(true); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to write umtprd conf file")
|
||||
}
|
||||
} else {
|
||||
if err := writeUmtprdConfFile(false); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to write umtprd conf file")
|
||||
}
|
||||
}
|
||||
|
||||
if config.UsbDevices.Mtp {
|
||||
if err := gadget.UnbindUDC(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to unbind UDC")
|
||||
}
|
||||
|
||||
if out, err := exec.Command("pgrep", "-x", "umtprd").Output(); err == nil && len(out) > 0 {
|
||||
cmd := exec.Command("killall", "umtprd")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to killall umtprd: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("umtprd")
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to exec binary: %w", err)
|
||||
}
|
||||
|
||||
if err := rpcSetUsbDevices(*config.UsbDevices); err != nil {
|
||||
return fmt.Errorf("failed to set usb devices: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
168
vpn.go
168
vpn.go
@@ -1,8 +1,12 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
@@ -30,8 +34,6 @@ func rpcLoginTailScale(xEdge bool) (TailScaleSettings, error) {
|
||||
IP: "",
|
||||
}
|
||||
|
||||
//fmt.Printf("[rpcLoginTailScale] xEdge: %v\n", xEdge)
|
||||
|
||||
_, err := CallVpnCtrlAction("login_tailscale", map[string]interface{}{"xEdge": xEdge})
|
||||
if err != nil {
|
||||
return settings, err
|
||||
@@ -63,13 +65,19 @@ func rpcLoginTailScale(xEdge bool) (TailScaleSettings, error) {
|
||||
case "logined":
|
||||
config.TailScaleAutoStart = true
|
||||
config.TailScaleXEdge = settings.XEdge
|
||||
SaveConfig()
|
||||
return settings, nil
|
||||
err := SaveConfig()
|
||||
if err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("failed to save config")
|
||||
}
|
||||
return settings, err
|
||||
case "connected":
|
||||
config.TailScaleAutoStart = true
|
||||
config.TailScaleXEdge = settings.XEdge
|
||||
SaveConfig()
|
||||
return settings, nil
|
||||
err = SaveConfig()
|
||||
if err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("failed to save config")
|
||||
}
|
||||
return settings, err
|
||||
case "connecting":
|
||||
if i >= 10 {
|
||||
settings.State = "disconnected"
|
||||
@@ -77,7 +85,10 @@ func rpcLoginTailScale(xEdge bool) (TailScaleSettings, error) {
|
||||
settings.State = "connecting"
|
||||
}
|
||||
case "cancel":
|
||||
go rpcLogoutTailScale()
|
||||
err := rpcLogoutTailScale()
|
||||
if err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("failed to logout tailscale")
|
||||
}
|
||||
settings.State = "disconnected"
|
||||
return settings, nil
|
||||
default:
|
||||
@@ -135,7 +146,6 @@ type ZeroTierSettings struct {
|
||||
|
||||
func rpcLoginZeroTier(networkID string) (ZeroTierSettings, error) {
|
||||
LoadConfig()
|
||||
//fmt.Printf("[rpcLoginZeroTier] networkID: %s\n", networkID)
|
||||
settings := ZeroTierSettings{
|
||||
State: "connecting",
|
||||
NetworkID: networkID,
|
||||
@@ -161,13 +171,14 @@ func rpcLoginZeroTier(networkID string) (ZeroTierSettings, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if settings.State == "closed" {
|
||||
switch settings.State {
|
||||
case "closed":
|
||||
config.ZeroTierAutoStart = false
|
||||
config.ZeroTierNetworkID = ""
|
||||
if err := SaveConfig(); err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("failed to save config")
|
||||
}
|
||||
} else if settings.State == "connected" || settings.State == "logined" {
|
||||
case "connected", "logined":
|
||||
config.ZeroTierAutoStart = true
|
||||
config.ZeroTierNetworkID = settings.NetworkID
|
||||
if err := SaveConfig(); err != nil {
|
||||
@@ -243,27 +254,146 @@ func HandleVpnDisplayUpdateMessage(event CtrlResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
if vpnUpdateDisplayState.TailScaleState == "connected" {
|
||||
switch vpnUpdateDisplayState.TailScaleState {
|
||||
case "connected":
|
||||
updateLabelIfChanged("Network_TailScale_Label", "Connected")
|
||||
} else if vpnUpdateDisplayState.TailScaleState == "logined" {
|
||||
case "logined":
|
||||
updateLabelIfChanged("Network_TailScale_Label", "Logined")
|
||||
} else {
|
||||
default:
|
||||
updateLabelIfChanged("Network_TailScale_Label", "Disconnected")
|
||||
}
|
||||
|
||||
if vpnUpdateDisplayState.ZeroTierState == "connected" {
|
||||
switch vpnUpdateDisplayState.ZeroTierState {
|
||||
case "connected":
|
||||
updateLabelIfChanged("Network_ZeroTier_Label", "Connected")
|
||||
} else if vpnUpdateDisplayState.ZeroTierState == "logined" {
|
||||
case "logined":
|
||||
updateLabelIfChanged("Network_ZeroTier_Label", "Logined")
|
||||
} else {
|
||||
default:
|
||||
updateLabelIfChanged("Network_ZeroTier_Label", "Disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
type FrpcStatus struct {
|
||||
Running bool `json:"running"`
|
||||
}
|
||||
|
||||
var (
|
||||
frpcTomlPath = "/userdata/frpc/frpc.toml"
|
||||
frpcLogPath = "/tmp/frpc.log"
|
||||
)
|
||||
|
||||
func frpcRunning() bool {
|
||||
cmd := exec.Command("pgrep", "-x", "frpc")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
func rpcGetFrpcLog() (string, error) {
|
||||
f, err := os.Open(frpcLogPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("frpc log file not exist")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
const want = 30
|
||||
lines := make([]string, 0, want+10)
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
lines = append(lines, sc.Text())
|
||||
if len(lines) > want {
|
||||
lines = lines[1:]
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
for _, l := range lines {
|
||||
buf = append(buf, l...)
|
||||
buf = append(buf, '\n')
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func rpcGetFrpcToml() (string, error) {
|
||||
return config.FrpcToml, nil
|
||||
}
|
||||
|
||||
func rpcStartFrpc(frpcToml string) error {
|
||||
if frpcRunning() {
|
||||
_ = exec.Command("pkill", "-x", "frpc").Run()
|
||||
}
|
||||
|
||||
if frpcToml != "" {
|
||||
_ = os.MkdirAll(filepath.Dir(frpcTomlPath), 0700)
|
||||
if err := os.WriteFile(frpcTomlPath, []byte(frpcToml), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("frpc", "-c", frpcTomlPath)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
logFile, err := os.OpenFile(frpcLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logFile.Close()
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start frpc failed: %w", err)
|
||||
} else {
|
||||
config.FrpcAutoStart = true
|
||||
config.FrpcToml = frpcToml
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("frpcToml is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcStopFrpc() error {
|
||||
if frpcRunning() {
|
||||
err := exec.Command("pkill", "-x", "frpc").Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop frpc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
config.FrpcAutoStart = false
|
||||
err := SaveConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcGetFrpcStatus() (FrpcStatus, error) {
|
||||
return FrpcStatus{Running: frpcRunning()}, nil
|
||||
}
|
||||
|
||||
func initVPN() {
|
||||
waitVpnCtrlClientConnected()
|
||||
go func() {
|
||||
for {
|
||||
if !networkState.IsOnline() {
|
||||
vpnLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if config.TailScaleAutoStart {
|
||||
if _, err := rpcLoginTailScale(config.TailScaleXEdge); err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("Failed to auto start TailScale")
|
||||
@@ -275,6 +405,12 @@ func initVPN() {
|
||||
vpnLogger.Error().Err(err).Msg("Failed to auto start ZeroTier")
|
||||
}
|
||||
}
|
||||
|
||||
if config.FrpcAutoStart && config.FrpcToml != "" {
|
||||
if err := rpcStartFrpc(config.FrpcToml); err != nil {
|
||||
vpnLogger.Error().Err(err).Msg("Failed to auto start frpc")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
||||
17
web.go
17
web.go
@@ -158,6 +158,8 @@ func setupRouter() *gin.Engine {
|
||||
protected.PUT("/auth/password-local", handleUpdatePassword)
|
||||
protected.DELETE("/auth/local-password", handleDeletePassword)
|
||||
protected.POST("/storage/upload", handleUploadHttp)
|
||||
protected.GET("/storage/download", handleDownloadHttp)
|
||||
protected.GET("/storage/sd-download", handleSDDownloadHttp)
|
||||
}
|
||||
|
||||
// Catch-all route for SPA
|
||||
@@ -214,12 +216,17 @@ var (
|
||||
func handleLocalWebRTCSignal(c *gin.Context) {
|
||||
// get the source from the request
|
||||
source := c.ClientIP()
|
||||
connectionID := uuid.New().String()
|
||||
|
||||
connectionID := c.Query("id")
|
||||
if connectionID == "" {
|
||||
connectionID = uuid.New().String()
|
||||
}
|
||||
|
||||
scopedLogger := websocketLogger.With().
|
||||
Str("component", "websocket").
|
||||
Str("source", source).
|
||||
Str("sourceType", "local").
|
||||
Str("connectionID", connectionID).
|
||||
Logger()
|
||||
|
||||
scopedLogger.Info().Msg("new websocket connection established")
|
||||
@@ -246,7 +253,10 @@ func handleLocalWebRTCSignal(c *gin.Context) {
|
||||
// Now use conn for websocket operations
|
||||
defer wsCon.Close(websocket.StatusNormalClosure, "")
|
||||
|
||||
err = wsjson.Write(context.Background(), wsCon, gin.H{"type": "device-metadata", "data": gin.H{"deviceVersion": builtAppVersion}})
|
||||
err = wsjson.Write(context.Background(), wsCon, gin.H{
|
||||
"type": "device-metadata",
|
||||
"data": gin.H{"deviceVersion": builtAppVersion, "connectionID": connectionID},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -271,8 +281,7 @@ func handleWebRTCSignalWsMessages(
|
||||
}()
|
||||
|
||||
// connection type
|
||||
var sourceType string
|
||||
sourceType = "local"
|
||||
var sourceType = "local"
|
||||
|
||||
l := scopedLogger.With().
|
||||
Str("source", source).
|
||||
|
||||
27
webrtc.go
27
webrtc.go
@@ -68,23 +68,38 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||
}
|
||||
|
||||
func newSession(config SessionConfig) (*Session, error) {
|
||||
func newSession(sessionConfig SessionConfig) (*Session, error) {
|
||||
webrtcSettingEngine := webrtc.SettingEngine{
|
||||
LoggerFactory: logging.GetPionDefaultLoggerFactory(),
|
||||
}
|
||||
iceServer := webrtc.ICEServer{}
|
||||
webrtcSettingEngine.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4,
|
||||
webrtc.NetworkTypeUDP6,
|
||||
})
|
||||
//iceServer := webrtc.ICEServer{}
|
||||
|
||||
var scopedLogger *zerolog.Logger
|
||||
if config.Logger != nil {
|
||||
l := config.Logger.With().Str("component", "webrtc").Logger()
|
||||
if sessionConfig.Logger != nil {
|
||||
l := sessionConfig.Logger.With().Str("component", "webrtc").Logger()
|
||||
scopedLogger = &l
|
||||
} else {
|
||||
scopedLogger = webrtcLogger
|
||||
}
|
||||
|
||||
iceServers := []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
}
|
||||
if config.STUN != "" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{config.STUN},
|
||||
})
|
||||
}
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
|
||||
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{iceServer},
|
||||
ICEServers: iceServers,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -162,7 +177,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||
peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
scopedLogger.Info().Interface("candidate", candidate).Msg("WebRTC peerConnection has a new ICE candidate")
|
||||
if candidate != nil {
|
||||
err := wsjson.Write(context.Background(), config.ws, gin.H{"type": "new-ice-candidate", "data": candidate.ToJSON()})
|
||||
err := wsjson.Write(context.Background(), sessionConfig.ws, gin.H{"type": "new-ice-candidate", "data": candidate.ToJSON()})
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("failed to write new-ice-candidate to WebRTC signaling channel")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user