mirror of https://github.com/jetkvm/kvm.git
1028 lines
29 KiB
Go
1028 lines
29 KiB
Go
package kvm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"go.bug.st/serial"
|
|
|
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
|
)
|
|
|
|
type JSONRPCRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params map[string]interface{} `json:"params,omitempty"`
|
|
ID interface{} `json:"id,omitempty"`
|
|
}
|
|
|
|
type JSONRPCResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Result interface{} `json:"result,omitempty"`
|
|
Error interface{} `json:"error,omitempty"`
|
|
ID interface{} `json:"id"`
|
|
}
|
|
|
|
type JSONRPCEvent struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params,omitempty"`
|
|
}
|
|
|
|
type BacklightSettings struct {
|
|
MaxBrightness int `json:"max_brightness"`
|
|
DimAfter int `json:"dim_after"`
|
|
OffAfter int `json:"off_after"`
|
|
}
|
|
|
|
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
|
responseBytes, err := json.Marshal(response)
|
|
if err != nil {
|
|
jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC response")
|
|
return
|
|
}
|
|
err = session.RPCChannel.SendText(string(responseBytes))
|
|
if err != nil {
|
|
jsonRpcLogger.Warn().Err(err).Msg("Error sending JSONRPC response")
|
|
return
|
|
}
|
|
}
|
|
|
|
func writeJSONRPCEvent(event string, params interface{}, session *Session) {
|
|
request := JSONRPCEvent{
|
|
JSONRPC: "2.0",
|
|
Method: event,
|
|
Params: params,
|
|
}
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC event")
|
|
return
|
|
}
|
|
if session == nil || session.RPCChannel == nil {
|
|
jsonRpcLogger.Info().Msg("RPC channel not available")
|
|
return
|
|
}
|
|
|
|
requestString := string(requestBytes)
|
|
scopedLogger := jsonRpcLogger.With().
|
|
Str("data", requestString).
|
|
Logger()
|
|
|
|
scopedLogger.Info().Msg("sending JSONRPC event")
|
|
|
|
err = session.RPCChannel.SendText(requestString)
|
|
if err != nil {
|
|
scopedLogger.Warn().Err(err).Msg("error sending JSONRPC event")
|
|
return
|
|
}
|
|
}
|
|
|
|
func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
|
var request JSONRPCRequest
|
|
err := json.Unmarshal(message.Data, &request)
|
|
if err != nil {
|
|
jsonRpcLogger.Warn().
|
|
Str("data", string(message.Data)).
|
|
Err(err).
|
|
Msg("Error unmarshalling JSONRPC request")
|
|
|
|
errorResponse := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Error: map[string]interface{}{
|
|
"code": -32700,
|
|
"message": "Parse error",
|
|
},
|
|
ID: 0,
|
|
}
|
|
writeJSONRPCResponse(errorResponse, session)
|
|
return
|
|
}
|
|
|
|
scopedLogger := jsonRpcLogger.With().
|
|
Str("method", request.Method).
|
|
Interface("params", request.Params).
|
|
Interface("id", request.ID).Logger()
|
|
|
|
scopedLogger.Trace().Msg("Received RPC request")
|
|
|
|
handler, ok := rpcHandlers[request.Method]
|
|
if !ok {
|
|
errorResponse := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Error: map[string]interface{}{
|
|
"code": -32601,
|
|
"message": "Method not found",
|
|
},
|
|
ID: request.ID,
|
|
}
|
|
writeJSONRPCResponse(errorResponse, session)
|
|
return
|
|
}
|
|
|
|
scopedLogger.Trace().Msg("Calling RPC handler")
|
|
result, err := callRPCHandler(handler, request.Params)
|
|
if err != nil {
|
|
scopedLogger.Error().Err(err).Msg("Error calling RPC handler")
|
|
errorResponse := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Error: map[string]interface{}{
|
|
"code": -32603,
|
|
"message": "Internal error",
|
|
"data": err.Error(),
|
|
},
|
|
ID: request.ID,
|
|
}
|
|
writeJSONRPCResponse(errorResponse, session)
|
|
return
|
|
}
|
|
|
|
scopedLogger.Trace().Interface("result", result).Msg("RPC handler returned")
|
|
|
|
response := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Result: result,
|
|
ID: request.ID,
|
|
}
|
|
writeJSONRPCResponse(response, session)
|
|
}
|
|
|
|
func rpcPing() (string, error) {
|
|
return "pong", nil
|
|
}
|
|
|
|
func rpcGetDeviceID() (string, error) {
|
|
return GetDeviceID(), nil
|
|
}
|
|
|
|
func rpcReboot(force bool) error {
|
|
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
|
|
|
|
args := []string{}
|
|
if force {
|
|
args = append(args, "-f")
|
|
}
|
|
|
|
cmd := exec.Command("reboot", args...)
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("failed to reboot")
|
|
return fmt.Errorf("failed to reboot: %w", err)
|
|
}
|
|
|
|
// If the reboot command is successful, exit the program after 5 seconds
|
|
go func() {
|
|
time.Sleep(5 * time.Second)
|
|
os.Exit(0)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
var streamFactor = 1.0
|
|
|
|
func rpcGetStreamQualityFactor() (float64, error) {
|
|
return streamFactor, nil
|
|
}
|
|
|
|
func rpcSetStreamQualityFactor(factor float64) error {
|
|
logger.Info().Float64("factor", factor).Msg("Setting stream quality factor")
|
|
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
streamFactor = factor
|
|
return nil
|
|
}
|
|
|
|
func rpcGetAutoUpdateState() (bool, error) {
|
|
return config.AutoUpdateEnabled, nil
|
|
}
|
|
|
|
func rpcSetAutoUpdateState(enabled bool) (bool, error) {
|
|
config.AutoUpdateEnabled = enabled
|
|
if err := SaveConfig(); err != nil {
|
|
return config.AutoUpdateEnabled, fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
return enabled, nil
|
|
}
|
|
|
|
func rpcGetEDID() (string, error) {
|
|
resp, err := CallCtrlAction("get_edid", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
edid, ok := resp.Result["edid"]
|
|
if ok {
|
|
return edid.(string), nil
|
|
}
|
|
return "", errors.New("EDID not found in response")
|
|
}
|
|
|
|
func rpcSetEDID(edid string) error {
|
|
if edid == "" {
|
|
logger.Info().Msg("Restoring EDID to default")
|
|
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
|
} else {
|
|
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
|
}
|
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save EDID to config, allowing it to be restored on reboot.
|
|
config.EdidString = edid
|
|
_ = SaveConfig()
|
|
return nil
|
|
}
|
|
|
|
func rpcGetDevChannelState() (bool, error) {
|
|
return config.IncludePreRelease, nil
|
|
}
|
|
|
|
func rpcSetDevChannelState(enabled bool) error {
|
|
config.IncludePreRelease = enabled
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
|
includePreRelease := config.IncludePreRelease
|
|
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error checking for updates: %w", err)
|
|
}
|
|
|
|
return updateStatus, nil
|
|
}
|
|
|
|
func rpcTryUpdate() error {
|
|
includePreRelease := config.IncludePreRelease
|
|
go func() {
|
|
err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to try update")
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func rpcSetBacklightSettings(params BacklightSettings) error {
|
|
blConfig := params
|
|
|
|
// NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with.
|
|
if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 {
|
|
return fmt.Errorf("maxBrightness must be between 0 and 255")
|
|
}
|
|
|
|
if blConfig.DimAfter < 0 {
|
|
return fmt.Errorf("dimAfter must be a positive integer")
|
|
}
|
|
|
|
if blConfig.OffAfter < 0 {
|
|
return fmt.Errorf("offAfter must be a positive integer")
|
|
}
|
|
|
|
config.DisplayMaxBrightness = blConfig.MaxBrightness
|
|
config.DisplayDimAfterSec = blConfig.DimAfter
|
|
config.DisplayOffAfterSec = blConfig.OffAfter
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
logger.Info().Int("max_brightness", config.DisplayMaxBrightness).Int("dim_after", config.DisplayDimAfterSec).Int("off_after", config.DisplayOffAfterSec).Msg("rpc: display: settings applied")
|
|
|
|
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
|
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
|
startBacklightTickers()
|
|
|
|
// Wake the display after the settings are altered, this ensures the tickers
|
|
// are reset to the new settings, and will bring the display up to maxBrightness.
|
|
// Calling with force set to true, to ignore the current state of the display, and force
|
|
// it to reset the tickers.
|
|
wakeDisplay(true)
|
|
return nil
|
|
}
|
|
|
|
func rpcGetBacklightSettings() (*BacklightSettings, error) {
|
|
return &BacklightSettings{
|
|
MaxBrightness: config.DisplayMaxBrightness,
|
|
DimAfter: int(config.DisplayDimAfterSec),
|
|
OffAfter: int(config.DisplayOffAfterSec),
|
|
}, nil
|
|
}
|
|
|
|
const (
|
|
devModeFile = "/userdata/jetkvm/devmode.enable"
|
|
sshKeyDir = "/userdata/dropbear/.ssh"
|
|
sshKeyFile = "/userdata/dropbear/.ssh/authorized_keys"
|
|
)
|
|
|
|
type DevModeState struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type SSHKeyState struct {
|
|
SSHKey string `json:"sshKey"`
|
|
}
|
|
|
|
func rpcGetDevModeState() (DevModeState, error) {
|
|
devModeEnabled := false
|
|
if _, err := os.Stat(devModeFile); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return DevModeState{}, fmt.Errorf("error checking dev mode file: %w", err)
|
|
}
|
|
} else {
|
|
devModeEnabled = true
|
|
}
|
|
|
|
return DevModeState{
|
|
Enabled: devModeEnabled,
|
|
}, nil
|
|
}
|
|
|
|
func rpcSetDevModeState(enabled bool) error {
|
|
if enabled {
|
|
if _, err := os.Stat(devModeFile); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(devModeFile), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory for devmode file: %w", err)
|
|
}
|
|
if err := os.WriteFile(devModeFile, []byte{}, 0644); err != nil {
|
|
return fmt.Errorf("failed to create devmode file: %w", err)
|
|
}
|
|
} else {
|
|
logger.Debug().Msg("dev mode already enabled")
|
|
return nil
|
|
}
|
|
} else {
|
|
if _, err := os.Stat(devModeFile); err == nil {
|
|
if err := os.Remove(devModeFile); err != nil {
|
|
return fmt.Errorf("failed to remove devmode file: %w", err)
|
|
}
|
|
} else if os.IsNotExist(err) {
|
|
logger.Debug().Msg("dev mode already disabled")
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("error checking dev mode file: %w", err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("dropbear.sh")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH")
|
|
return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcGetSSHKeyState() (string, error) {
|
|
keyData, err := os.ReadFile(sshKeyFile)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return "", fmt.Errorf("error reading SSH key file: %w", err)
|
|
}
|
|
}
|
|
return string(keyData), nil
|
|
}
|
|
|
|
func rpcSetSSHKeyState(sshKey string) error {
|
|
if sshKey != "" {
|
|
// Create directory if it doesn't exist
|
|
if err := os.MkdirAll(sshKeyDir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create SSH key directory: %w", err)
|
|
}
|
|
|
|
// Write SSH key to file
|
|
if err := os.WriteFile(sshKeyFile, []byte(sshKey), 0600); err != nil {
|
|
return fmt.Errorf("failed to write SSH key: %w", err)
|
|
}
|
|
} else {
|
|
// Remove SSH key file if empty string is provided
|
|
if err := os.Remove(sshKeyFile); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to remove SSH key file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcGetTLSState() TLSState {
|
|
return getTLSState()
|
|
}
|
|
|
|
func rpcSetTLSState(state TLSState) error {
|
|
err := setTLSState(state)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set TLS state: %w", err)
|
|
}
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
|
handlerValue := reflect.ValueOf(handler.Func)
|
|
handlerType := handlerValue.Type()
|
|
|
|
if handlerType.Kind() != reflect.Func {
|
|
return nil, errors.New("handler is not a function")
|
|
}
|
|
|
|
numParams := handlerType.NumIn()
|
|
args := make([]reflect.Value, numParams)
|
|
// Get the parameter names from the RPCHandler
|
|
paramNames := handler.Params
|
|
|
|
if len(paramNames) != numParams {
|
|
return nil, errors.New("mismatch between handler parameters and defined parameter names")
|
|
}
|
|
|
|
for i := 0; i < numParams; i++ {
|
|
paramType := handlerType.In(i)
|
|
paramName := paramNames[i]
|
|
paramValue, ok := params[paramName]
|
|
if !ok {
|
|
return nil, errors.New("missing parameter: " + paramName)
|
|
}
|
|
|
|
convertedValue := reflect.ValueOf(paramValue)
|
|
if !convertedValue.Type().ConvertibleTo(paramType) {
|
|
if paramType.Kind() == reflect.Slice && (convertedValue.Kind() == reflect.Slice || convertedValue.Kind() == reflect.Array) {
|
|
newSlice := reflect.MakeSlice(paramType, convertedValue.Len(), convertedValue.Len())
|
|
for j := 0; j < convertedValue.Len(); j++ {
|
|
elemValue := convertedValue.Index(j)
|
|
if elemValue.Kind() == reflect.Interface {
|
|
elemValue = elemValue.Elem()
|
|
}
|
|
if !elemValue.Type().ConvertibleTo(paramType.Elem()) {
|
|
// Handle float64 to uint8 conversion
|
|
if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 {
|
|
intValue := int(elemValue.Float())
|
|
if intValue < 0 || intValue > 255 {
|
|
return nil, fmt.Errorf("value out of range for uint8: %v", intValue)
|
|
}
|
|
newSlice.Index(j).SetUint(uint64(intValue))
|
|
} else {
|
|
fromType := elemValue.Type()
|
|
toType := paramType.Elem()
|
|
return nil, fmt.Errorf("invalid element type in slice for parameter %s: from %v to %v", paramName, fromType, toType)
|
|
}
|
|
} else {
|
|
newSlice.Index(j).Set(elemValue.Convert(paramType.Elem()))
|
|
}
|
|
}
|
|
args[i] = newSlice
|
|
} else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map {
|
|
jsonData, err := json.Marshal(convertedValue.Interface())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal map to JSON: %v", err)
|
|
}
|
|
|
|
newStruct := reflect.New(paramType).Interface()
|
|
if err := json.Unmarshal(jsonData, newStruct); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v", err)
|
|
}
|
|
args[i] = reflect.ValueOf(newStruct).Elem()
|
|
} else {
|
|
return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind())
|
|
}
|
|
} else {
|
|
args[i] = convertedValue.Convert(paramType)
|
|
}
|
|
}
|
|
|
|
results := handlerValue.Call(args)
|
|
|
|
if len(results) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(results) == 1 {
|
|
if results[0].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
|
if !results[0].IsNil() {
|
|
return nil, results[0].Interface().(error)
|
|
}
|
|
return nil, nil
|
|
}
|
|
return results[0].Interface(), nil
|
|
}
|
|
|
|
if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
|
if !results[1].IsNil() {
|
|
return nil, results[1].Interface().(error)
|
|
}
|
|
return results[0].Interface(), nil
|
|
}
|
|
|
|
return nil, errors.New("unexpected return values from handler")
|
|
}
|
|
|
|
type RPCHandler struct {
|
|
Func interface{}
|
|
Params []string
|
|
}
|
|
|
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
|
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
|
var cdrom bool
|
|
if mode == "cdrom" {
|
|
cdrom = true
|
|
} else if mode != "file" {
|
|
logger.Info().Str("mode", mode).Msg("Invalid mode provided")
|
|
return "", fmt.Errorf("invalid mode: %s", mode)
|
|
}
|
|
|
|
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
|
|
|
err := setMassStorageMode(cdrom)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to set mass storage mode: %w", err)
|
|
}
|
|
|
|
logger.Info().Str("mode", mode).Msg("Mass storage mode set")
|
|
|
|
// Get the updated mode after setting
|
|
return rpcGetMassStorageMode()
|
|
}
|
|
|
|
func rpcGetMassStorageMode() (string, error) {
|
|
cdrom, err := getMassStorageMode()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get mass storage mode: %w", err)
|
|
}
|
|
|
|
mode := "file"
|
|
if cdrom {
|
|
mode = "cdrom"
|
|
}
|
|
return mode, nil
|
|
}
|
|
|
|
func rpcIsUpdatePending() (bool, error) {
|
|
return IsUpdatePending(), nil
|
|
}
|
|
|
|
func rpcGetUsbEmulationState() (bool, error) {
|
|
return gadget.IsUDCBound()
|
|
}
|
|
|
|
func rpcSetUsbEmulationState(enabled bool) error {
|
|
if enabled {
|
|
return gadget.BindUDC()
|
|
} else {
|
|
return gadget.UnbindUDC()
|
|
}
|
|
}
|
|
|
|
func rpcGetUsbConfig() (usbgadget.Config, error) {
|
|
LoadConfig()
|
|
return *config.UsbConfig, nil
|
|
}
|
|
|
|
func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
|
|
LoadConfig()
|
|
config.UsbConfig = &usbConfig
|
|
gadget.SetGadgetConfig(config.UsbConfig)
|
|
return updateUsbRelatedConfig()
|
|
}
|
|
|
|
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
|
|
if config.WakeOnLanDevices == nil {
|
|
return []WakeOnLanDevice{}, nil
|
|
}
|
|
return config.WakeOnLanDevices, nil
|
|
}
|
|
|
|
type SetWakeOnLanDevicesParams struct {
|
|
Devices []WakeOnLanDevice `json:"devices"`
|
|
}
|
|
|
|
func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
|
|
config.WakeOnLanDevices = params.Devices
|
|
return SaveConfig()
|
|
}
|
|
|
|
func rpcResetConfig() error {
|
|
config = defaultConfig
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to reset config: %w", err)
|
|
}
|
|
|
|
logger.Info().Msg("Configuration reset to default")
|
|
return nil
|
|
}
|
|
|
|
type DCPowerState struct {
|
|
IsOn bool `json:"isOn"`
|
|
Voltage float64 `json:"voltage"`
|
|
Current float64 `json:"current"`
|
|
Power float64 `json:"power"`
|
|
}
|
|
|
|
func rpcGetDCPowerState() (DCPowerState, error) {
|
|
return dcState, nil
|
|
}
|
|
|
|
func rpcSetDCPowerState(enabled bool) error {
|
|
logger.Info().Bool("enabled", enabled).Msg("Setting DC power state")
|
|
err := setDCPowerState(enabled)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set DC power state: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcGetActiveExtension() (string, error) {
|
|
return config.ActiveExtension, nil
|
|
}
|
|
|
|
func rpcSetActiveExtension(extensionId string) error {
|
|
if config.ActiveExtension == extensionId {
|
|
return nil
|
|
}
|
|
if config.ActiveExtension == "atx-power" {
|
|
_ = unmountATXControl()
|
|
} else if config.ActiveExtension == "dc-power" {
|
|
_ = unmountDCControl()
|
|
}
|
|
config.ActiveExtension = extensionId
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
if extensionId == "atx-power" {
|
|
_ = mountATXControl()
|
|
} else if extensionId == "dc-power" {
|
|
_ = mountDCControl()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcSetATXPowerAction(action string) error {
|
|
logger.Debug().Str("action", action).Msg("Executing ATX power action")
|
|
switch action {
|
|
case "power-short":
|
|
logger.Debug().Msg("Simulating short power button press")
|
|
return pressATXPowerButton(200 * time.Millisecond)
|
|
case "power-long":
|
|
logger.Debug().Msg("Simulating long power button press")
|
|
return pressATXPowerButton(5 * time.Second)
|
|
case "reset":
|
|
logger.Debug().Msg("Simulating reset button press")
|
|
return pressATXResetButton(200 * time.Millisecond)
|
|
default:
|
|
return fmt.Errorf("invalid action: %s", action)
|
|
}
|
|
}
|
|
|
|
type ATXState struct {
|
|
Power bool `json:"power"`
|
|
HDD bool `json:"hdd"`
|
|
}
|
|
|
|
func rpcGetATXState() (ATXState, error) {
|
|
state := ATXState{
|
|
Power: ledPWRState,
|
|
HDD: ledHDDState,
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
type SerialSettings struct {
|
|
BaudRate string `json:"baudRate"`
|
|
DataBits string `json:"dataBits"`
|
|
StopBits string `json:"stopBits"`
|
|
Parity string `json:"parity"`
|
|
}
|
|
|
|
func rpcGetSerialSettings() (SerialSettings, error) {
|
|
settings := SerialSettings{
|
|
BaudRate: strconv.Itoa(serialPortMode.BaudRate),
|
|
DataBits: strconv.Itoa(serialPortMode.DataBits),
|
|
StopBits: "1",
|
|
Parity: "none",
|
|
}
|
|
|
|
switch serialPortMode.StopBits {
|
|
case serial.OneStopBit:
|
|
settings.StopBits = "1"
|
|
case serial.OnePointFiveStopBits:
|
|
settings.StopBits = "1.5"
|
|
case serial.TwoStopBits:
|
|
settings.StopBits = "2"
|
|
}
|
|
|
|
switch serialPortMode.Parity {
|
|
case serial.NoParity:
|
|
settings.Parity = "none"
|
|
case serial.OddParity:
|
|
settings.Parity = "odd"
|
|
case serial.EvenParity:
|
|
settings.Parity = "even"
|
|
case serial.MarkParity:
|
|
settings.Parity = "mark"
|
|
case serial.SpaceParity:
|
|
settings.Parity = "space"
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
var serialPortMode = defaultMode
|
|
|
|
func rpcSetSerialSettings(settings SerialSettings) error {
|
|
baudRate, err := strconv.Atoi(settings.BaudRate)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid baud rate: %v", err)
|
|
}
|
|
dataBits, err := strconv.Atoi(settings.DataBits)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid data bits: %v", err)
|
|
}
|
|
|
|
var stopBits serial.StopBits
|
|
switch settings.StopBits {
|
|
case "1":
|
|
stopBits = serial.OneStopBit
|
|
case "1.5":
|
|
stopBits = serial.OnePointFiveStopBits
|
|
case "2":
|
|
stopBits = serial.TwoStopBits
|
|
default:
|
|
return fmt.Errorf("invalid stop bits: %s", settings.StopBits)
|
|
}
|
|
|
|
var parity serial.Parity
|
|
switch settings.Parity {
|
|
case "none":
|
|
parity = serial.NoParity
|
|
case "odd":
|
|
parity = serial.OddParity
|
|
case "even":
|
|
parity = serial.EvenParity
|
|
case "mark":
|
|
parity = serial.MarkParity
|
|
case "space":
|
|
parity = serial.SpaceParity
|
|
default:
|
|
return fmt.Errorf("invalid parity: %s", settings.Parity)
|
|
}
|
|
serialPortMode = &serial.Mode{
|
|
BaudRate: baudRate,
|
|
DataBits: dataBits,
|
|
StopBits: stopBits,
|
|
Parity: parity,
|
|
}
|
|
|
|
_ = port.SetMode(serialPortMode)
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcGetUsbDevices() (usbgadget.Devices, error) {
|
|
return *config.UsbDevices, nil
|
|
}
|
|
|
|
func updateUsbRelatedConfig() error {
|
|
if err := gadget.UpdateGadgetConfig(); err != nil {
|
|
return fmt.Errorf("failed to write gadget config: %w", err)
|
|
}
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
|
|
config.UsbDevices = &usbDevices
|
|
gadget.SetGadgetDevices(config.UsbDevices)
|
|
return updateUsbRelatedConfig()
|
|
}
|
|
|
|
func rpcSetUsbDeviceState(device string, enabled bool) error {
|
|
switch device {
|
|
case "absoluteMouse":
|
|
config.UsbDevices.AbsoluteMouse = enabled
|
|
case "relativeMouse":
|
|
config.UsbDevices.RelativeMouse = enabled
|
|
case "keyboard":
|
|
config.UsbDevices.Keyboard = enabled
|
|
case "massStorage":
|
|
config.UsbDevices.MassStorage = enabled
|
|
default:
|
|
return fmt.Errorf("invalid device: %s", device)
|
|
}
|
|
gadget.SetGadgetDevices(config.UsbDevices)
|
|
return updateUsbRelatedConfig()
|
|
}
|
|
|
|
func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
|
currentCloudURL := config.CloudURL
|
|
config.CloudURL = apiUrl
|
|
config.CloudAppURL = appUrl
|
|
|
|
if currentCloudURL != apiUrl {
|
|
disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl))
|
|
}
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var currentScrollSensitivity string = "default"
|
|
|
|
func rpcGetScrollSensitivity() (string, error) {
|
|
return currentScrollSensitivity, nil
|
|
}
|
|
|
|
func rpcSetScrollSensitivity(sensitivity string) error {
|
|
currentScrollSensitivity = sensitivity
|
|
return nil
|
|
}
|
|
|
|
func getKeyboardMacros() (interface{}, error) {
|
|
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
|
copy(macros, config.KeyboardMacros)
|
|
|
|
return macros, nil
|
|
}
|
|
|
|
type KeyboardMacrosParams struct {
|
|
Macros []interface{} `json:"macros"`
|
|
}
|
|
|
|
func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
|
if params.Macros == nil {
|
|
return nil, fmt.Errorf("missing or invalid macros parameter")
|
|
}
|
|
|
|
newMacros := make([]KeyboardMacro, 0, len(params.Macros))
|
|
|
|
for i, item := range params.Macros {
|
|
macroMap, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid macro at index %d", i)
|
|
}
|
|
|
|
id, _ := macroMap["id"].(string)
|
|
if id == "" {
|
|
id = fmt.Sprintf("macro-%d", time.Now().UnixNano())
|
|
}
|
|
|
|
name, _ := macroMap["name"].(string)
|
|
|
|
sortOrder := i + 1
|
|
if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok {
|
|
sortOrder = int(sortOrderFloat)
|
|
}
|
|
|
|
steps := []KeyboardMacroStep{}
|
|
if stepsArray, ok := macroMap["steps"].([]interface{}); ok {
|
|
for _, stepItem := range stepsArray {
|
|
stepMap, ok := stepItem.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
step := KeyboardMacroStep{}
|
|
|
|
if keysArray, ok := stepMap["keys"].([]interface{}); ok {
|
|
for _, k := range keysArray {
|
|
if keyStr, ok := k.(string); ok {
|
|
step.Keys = append(step.Keys, keyStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
if modsArray, ok := stepMap["modifiers"].([]interface{}); ok {
|
|
for _, m := range modsArray {
|
|
if modStr, ok := m.(string); ok {
|
|
step.Modifiers = append(step.Modifiers, modStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
if delay, ok := stepMap["delay"].(float64); ok {
|
|
step.Delay = int(delay)
|
|
}
|
|
|
|
steps = append(steps, step)
|
|
}
|
|
}
|
|
|
|
macro := KeyboardMacro{
|
|
ID: id,
|
|
Name: name,
|
|
Steps: steps,
|
|
SortOrder: sortOrder,
|
|
}
|
|
|
|
if err := macro.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid macro at index %d: %w", i, err)
|
|
}
|
|
|
|
newMacros = append(newMacros, macro)
|
|
}
|
|
|
|
config.KeyboardMacros = newMacros
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
var rpcHandlers = map[string]RPCHandler{
|
|
"ping": {Func: rpcPing},
|
|
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
|
"getDeviceID": {Func: rpcGetDeviceID},
|
|
"deregisterDevice": {Func: rpcDeregisterDevice},
|
|
"getCloudState": {Func: rpcGetCloudState},
|
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
|
"getVideoState": {Func: rpcGetVideoState},
|
|
"getUSBState": {Func: rpcGetUSBState},
|
|
"unmountImage": {Func: rpcUnmountImage},
|
|
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
|
|
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
|
|
"getJigglerState": {Func: rpcGetJigglerState},
|
|
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
|
|
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
|
|
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
|
|
"getAutoUpdateState": {Func: rpcGetAutoUpdateState},
|
|
"setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
|
|
"getEDID": {Func: rpcGetEDID},
|
|
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
|
|
"getDevChannelState": {Func: rpcGetDevChannelState},
|
|
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
|
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
|
"tryUpdate": {Func: rpcTryUpdate},
|
|
"getDevModeState": {Func: rpcGetDevModeState},
|
|
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
|
|
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
|
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
|
|
"getTLSState": {Func: rpcGetTLSState},
|
|
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
|
|
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
|
|
"getMassStorageMode": {Func: rpcGetMassStorageMode},
|
|
"isUpdatePending": {Func: rpcIsUpdatePending},
|
|
"getUsbEmulationState": {Func: rpcGetUsbEmulationState},
|
|
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
|
|
"getUsbConfig": {Func: rpcGetUsbConfig},
|
|
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
|
|
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
|
|
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
|
|
"getStorageSpace": {Func: rpcGetStorageSpace},
|
|
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
|
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
|
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
|
"listStorageFiles": {Func: rpcListStorageFiles},
|
|
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
|
|
"startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}},
|
|
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
|
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
|
"resetConfig": {Func: rpcResetConfig},
|
|
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
|
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
|
"getActiveExtension": {Func: rpcGetActiveExtension},
|
|
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
|
"getATXState": {Func: rpcGetATXState},
|
|
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
|
"getSerialSettings": {Func: rpcGetSerialSettings},
|
|
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
|
"getUsbDevices": {Func: rpcGetUsbDevices},
|
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
|
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
|
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
|
"getKeyboardMacros": {Func: getKeyboardMacros},
|
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
|
}
|