mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
2614db6b89
* feat(ui): add log level selector in Troubleshooting Mode Add a UI dropdown in Advanced > Troubleshooting Mode that lets users set the system log verbosity (Error, Warning, Info, Debug, Trace). Changes take effect immediately without restart via new getDefaultLogLevel/setDefaultLogLevel JSON-RPC endpoints. Also downgrades the noisy wakeup_on_write permission denied warning from Warn to Debug level, and removes the INFO→WARN config migration so users can actually select INFO. Localized for all 14 languages. * chore(ui): disable no-floating-promises in oxlint The rule is not actionable for the current codebase; turn it off explicitly. * refactor(ui): drop void prefixes on JSON-RPC send in advanced settings no-floating-promises is disabled in oxlint; match the rest of the codebase. * fix(ui): localize log level dropdown and fix optimistic update - Replace hardcoded English dropdown labels with localized m.*() calls - Replace hardcoded error string with m.advanced_error_set_log_level() - Optimistically update dropdown on change and revert on RPC failure - Add 6 new i18n keys across all 14 locales * chore: add remote-agent to .gitignore and auto-sort i18n in pre-commit - Ignore the compiled e2e/remote-agent/remote-agent binary - Add lint-staged rule to run i18n:resort on message JSON changes * copy(ui): improve log level setting description Apply outcome-oriented copy: explain what the setting does for the user and when to change it, rather than restating the control's mechanics. Updated across all 14 locales. * fix(logging): scope loggers not rebuilt when config level matches base default UpdateLogLevel compared the new config level against the base default (ErrorLevel) instead of the previous config level. When switching from WARN back to ERROR, the comparison was equal so scope loggers kept their old WarnLevel filter — WRN messages continued appearing despite the user selecting Error. Compare against the previous defaultLogLevelFromConfig instead. * test(logging): add RPC probe for log level filtering Add a dedicated emitTestLog JSON-RPC method and a focused e2e spec that verifies live TRACE/DEBUG/INFO/WARN/ERROR filtering against last.log. * chore(ui): update .gitignore to exclude screenshot.png file
1354 lines
39 KiB
Go
1354 lines
39 KiB
Go
package kvm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
|
"github.com/jetkvm/kvm/internal/utils"
|
|
)
|
|
|
|
type JSONRPCRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params map[string]any `json:"params,omitempty"`
|
|
ID any `json:"id,omitempty"`
|
|
}
|
|
|
|
type JSONRPCResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Result any `json:"result,omitempty"`
|
|
Error any `json:"error,omitempty"`
|
|
ID any `json:"id"`
|
|
}
|
|
|
|
type JSONRPCEvent struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params any `json:"params,omitempty"`
|
|
}
|
|
|
|
type DisplayRotationSettings struct {
|
|
Rotation string `json:"rotation"`
|
|
}
|
|
|
|
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 any, 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.Trace().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]any{
|
|
"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")
|
|
t := time.Now()
|
|
|
|
handler, ok := rpcHandlers[request.Method]
|
|
if !ok {
|
|
errorResponse := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Error: map[string]any{
|
|
"code": -32601,
|
|
"message": "Method not found",
|
|
},
|
|
ID: request.ID,
|
|
}
|
|
writeJSONRPCResponse(errorResponse, session)
|
|
return
|
|
}
|
|
|
|
result, err := callRPCHandler(scopedLogger, handler, request.Params)
|
|
if err != nil {
|
|
scopedLogger.Error().Err(err).Msg("Error calling RPC handler")
|
|
errorResponse := JSONRPCResponse{
|
|
JSONRPC: "2.0",
|
|
Error: map[string]any{
|
|
"code": -32603,
|
|
"message": "Internal error",
|
|
"data": err.Error(),
|
|
},
|
|
ID: request.ID,
|
|
}
|
|
writeJSONRPCResponse(errorResponse, session)
|
|
return
|
|
}
|
|
|
|
scopedLogger.Trace().Dur("duration", time.Since(t)).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 via RPC")
|
|
return hwReboot(force, nil, 0)
|
|
}
|
|
|
|
func rpcGetStreamQualityFactor() (float64, error) {
|
|
return config.VideoQualityFactor, nil
|
|
}
|
|
|
|
func rpcSetStreamQualityFactor(factor float64) error {
|
|
logger.Info().Float64("factor", factor).Msg("Setting stream quality factor")
|
|
err := nativeInstance.VideoSetQualityFactor(factor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config.VideoQualityFactor = factor
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcGetVideoCodecPreference() (string, error) {
|
|
return config.VideoCodecPreference, nil
|
|
}
|
|
|
|
func rpcSetVideoCodecPreference(codec string) error {
|
|
if codec != "auto" && codec != "h265" && codec != "h264" {
|
|
return fmt.Errorf("invalid codec preference: %s (must be auto, h265, or h264)", codec)
|
|
}
|
|
logger.Info().Str("codec", codec).Msg("Setting video codec preference")
|
|
config.VideoCodecPreference = codec
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
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 := nativeInstance.VideoGetEDID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func rpcSetEDID(edid string) error {
|
|
if edid == "" {
|
|
logger.Info().Msg("Restoring EDID to default")
|
|
} else {
|
|
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
|
}
|
|
err := nativeInstance.VideoSetEDID(edid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save EDID to config, allowing it to be restored on reboot.
|
|
config.EdidString = edid
|
|
_ = SaveConfig()
|
|
return nil
|
|
}
|
|
|
|
func rpcGetVideoLogStatus() (string, error) {
|
|
return nativeInstance.VideoLogStatus()
|
|
}
|
|
|
|
func rpcSetDisplayRotation(params DisplayRotationSettings) error {
|
|
currentRotation := config.DisplayRotation
|
|
if currentRotation == params.Rotation {
|
|
return nil
|
|
}
|
|
|
|
err := config.SetDisplayRotation(params.Rotation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = nativeInstance.DisplaySetRotation(config.GetDisplayRotation())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func rpcGetDisplayRotation() (*DisplayRotationSettings, error) {
|
|
return &DisplayRotationSettings{
|
|
Rotation: config.DisplayRotation,
|
|
}, 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, "backlight_settings_changed")
|
|
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 == "" {
|
|
// 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
|
|
}
|
|
|
|
// Validate SSH key
|
|
if err := utils.ValidateSSHKey(sshKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type RPCHandler struct {
|
|
Func any
|
|
Params []string
|
|
OptionalParams []string
|
|
}
|
|
|
|
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
|
func callRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (result any, err error) {
|
|
// Use defer to recover from a panic
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Convert the panic to an error
|
|
if e, ok := r.(error); ok {
|
|
err = e
|
|
} else {
|
|
err = fmt.Errorf("panic occurred: %v", r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Call the handler
|
|
result, err = riskyCallRPCHandler(logger, handler, params)
|
|
return result, err // do not combine these two lines into one, as it breaks the above defer function's setting of err
|
|
}
|
|
|
|
func riskyCallRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (any, 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()
|
|
allParamNames := append(handler.Params, handler.OptionalParams...) //nolint:gocritic
|
|
|
|
if len(allParamNames) != numParams {
|
|
err := fmt.Errorf("mismatch between handler parameters (%d) and defined parameter names (%d)", numParams, len(allParamNames))
|
|
logger.Error().Strs("paramNames", allParamNames).Err(err).Msg("Cannot call RPC handler")
|
|
return nil, err
|
|
}
|
|
|
|
optionalSet := make(map[string]bool, len(handler.OptionalParams))
|
|
for _, name := range handler.OptionalParams {
|
|
optionalSet[name] = true
|
|
}
|
|
|
|
args := make([]reflect.Value, numParams)
|
|
|
|
for i := range numParams {
|
|
paramType := handlerType.In(i)
|
|
paramName := allParamNames[i]
|
|
paramValue, ok := params[paramName]
|
|
if !ok {
|
|
if optionalSet[paramName] {
|
|
args[i] = reflect.Zero(paramType)
|
|
continue
|
|
}
|
|
err := fmt.Errorf("missing parameter: %s", paramName)
|
|
logger.Error().Err(err).Msg("Cannot marshal arguments for RPC handler")
|
|
return nil, err
|
|
}
|
|
|
|
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 for parameter %s", intValue, paramName)
|
|
}
|
|
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 for parameter %s", err, paramName)
|
|
}
|
|
|
|
newStruct := reflect.New(paramType).Interface()
|
|
if err := json.Unmarshal(jsonData, newStruct); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v for parameter %s", err, paramName)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
logger.Trace().Msg("Calling RPC handler")
|
|
results := handlerValue.Call(args)
|
|
|
|
if len(results) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(results) == 1 {
|
|
if ok, err := asError(results[0]); ok {
|
|
return nil, err
|
|
}
|
|
return results[0].Interface(), nil
|
|
}
|
|
|
|
if len(results) == 2 {
|
|
if ok, err := asError(results[1]); ok {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return results[0].Interface(), nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("too many return values from handler: %d", len(results))
|
|
}
|
|
|
|
func asError(value reflect.Value) (bool, error) {
|
|
if value.Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
|
if value.IsNil() {
|
|
return true, nil
|
|
}
|
|
return true, value.Interface().(error)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
|
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
|
var cdrom bool
|
|
switch mode {
|
|
case "cdrom":
|
|
cdrom = true
|
|
case "file":
|
|
cdrom = false
|
|
default:
|
|
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 := getMassStorageCDROMEnabled()
|
|
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 otaState.IsUpdatePending(), nil
|
|
}
|
|
|
|
func rpcGetUsbEmulationState() (bool, error) {
|
|
return gadget.IsUDCBound()
|
|
}
|
|
|
|
func rpcSetUsbEmulationState(enabled bool) error {
|
|
setUSBEmulationDesired(enabled)
|
|
|
|
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()
|
|
}
|
|
|
|
// resetConfig resets the config file to defaults. Used internally by OTA updates and native events.
|
|
func resetConfig() error {
|
|
defaultConfig := getDefaultConfig()
|
|
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
|
|
}
|
|
|
|
// factoryResetPaths lists all user data paths that should be removed during a factory reset.
|
|
var factoryResetPaths = []string{
|
|
configPath,
|
|
configPath + ".bak",
|
|
imagesFolder,
|
|
tlsStorePath,
|
|
sshKeyDir,
|
|
serialSettingsPath,
|
|
SerialCommandHistoryPath,
|
|
failsafeDefaultLastCrashPath,
|
|
}
|
|
|
|
func rpcFactoryReset() error {
|
|
logger.Info().Msg("Factory reset initiated, removing all user data")
|
|
|
|
var errs []error
|
|
for _, path := range factoryResetPaths {
|
|
if err := os.RemoveAll(path); err != nil {
|
|
logger.Warn().Err(err).Str("path", path).Msg("failed to remove path during factory reset")
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
logger.Warn().Int("errors", len(errs)).Msg("factory reset completed with errors, rebooting anyway")
|
|
} else {
|
|
logger.Info().Msg("Factory reset complete, rebooting device")
|
|
}
|
|
|
|
// Reboot asynchronously to allow the RPC response to be sent first.
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
if err := hwReboot(true, nil, 0); err != nil {
|
|
logger.Error().Err(err).Msg("failed to reboot after factory reset")
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
type DCPowerState struct {
|
|
IsOn bool `json:"isOn"`
|
|
Voltage float64 `json:"voltage"`
|
|
Current float64 `json:"current"`
|
|
Power float64 `json:"power"`
|
|
RestoreState int `json:"restoreState"`
|
|
}
|
|
|
|
func rpcGetDCPowerState() (DCPowerState, error) {
|
|
return getDCState(), 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 rpcSetDCRestoreState(state int) error {
|
|
logger.Info().Int("state", state).Msg("Setting DC restore state")
|
|
err := setDCRestoreState(state)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set DC restore state: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcGetActiveExtension() (string, error) {
|
|
return config.ActiveExtension, nil
|
|
}
|
|
|
|
func rpcSetActiveExtension(extensionId string) error {
|
|
if config.ActiveExtension == extensionId {
|
|
return nil
|
|
}
|
|
switch config.ActiveExtension {
|
|
case "atx-power":
|
|
_ = unmountATXControl()
|
|
case "dc-power":
|
|
_ = unmountDCControl()
|
|
}
|
|
config.ActiveExtension = extensionId
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
switch extensionId {
|
|
case "atx-power":
|
|
_ = mountATXControl()
|
|
case "dc-power":
|
|
_ = mountDCControl()
|
|
}
|
|
|
|
// Re-publish MQTT HA Discovery for the new extension
|
|
if mqttManager != nil {
|
|
mqttManager.republishHADiscovery()
|
|
}
|
|
|
|
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.Load(),
|
|
HDD: ledHDDState.Load(),
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func rpcSendCustomCommand(command string) error {
|
|
logger.Debug().Str("Command", command).Msg("JSONRPC: Sending custom serial command")
|
|
err := sendCustomCommand(command)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send custom command in jsonrpc: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcGetSerialSettings() (SerialSettings, error) {
|
|
return getSerialSettings()
|
|
}
|
|
|
|
func rpcSetSerialSettings(settings SerialSettings) error {
|
|
return setSerialSettings(settings)
|
|
}
|
|
|
|
const SerialCommandHistoryPath = "/userdata/serialCommandHistory.json"
|
|
|
|
func rpcGetSerialCommandHistory() ([]string, error) {
|
|
items := []string{}
|
|
|
|
file, err := os.Open(SerialCommandHistoryPath)
|
|
if err != nil {
|
|
logger.Debug().Msg("SerialCommandHistory file doesn't exist, using default")
|
|
return items, nil
|
|
}
|
|
defer file.Close()
|
|
|
|
// load and merge the default config with the user config
|
|
var loadedItems []string
|
|
if err := json.NewDecoder(file).Decode(&loadedItems); err != nil {
|
|
logger.Warn().Err(err).Msg("SerialCommandHistory file JSON parsing failed")
|
|
return items, nil
|
|
}
|
|
|
|
return loadedItems, nil
|
|
}
|
|
|
|
func rpcSetSerialCommandHistory(commandHistory []string) error {
|
|
logger.Trace().Str("path", SerialCommandHistoryPath).Msg("Saving serial command history")
|
|
|
|
file, err := os.Create(SerialCommandHistoryPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SerialCommandHistory file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(commandHistory); err != nil {
|
|
return fmt.Errorf("failed to encode SerialCommandHistory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcDeleteSerialCommandHistory() error {
|
|
logger.Trace().Str("path", SerialCommandHistoryPath).Msg("Deleting serial command history")
|
|
empty := []string{}
|
|
|
|
file, err := os.Create(SerialCommandHistoryPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SerialCommandHistory file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(empty); err != nil {
|
|
return fmt.Errorf("failed to encode SerialCommandHistory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcSetTerminalPaused(terminalPaused bool) error {
|
|
setTerminalPaused(terminalPaused)
|
|
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)
|
|
}
|
|
// Reset recovery timer so auto-recovery doesn't interfere during
|
|
// the host's USB re-enumeration window after a deliberate config change.
|
|
setUSBRecoveryTimer(time.Now())
|
|
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
|
|
case "serialConsole":
|
|
config.UsbDevices.SerialConsole = 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 publicIPState != nil {
|
|
publicIPState.SetCloudflareEndpoint(apiUrl)
|
|
}
|
|
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcGetKeyboardLayout() (string, error) {
|
|
return config.KeyboardLayout, nil
|
|
}
|
|
|
|
func rpcSetKeyboardLayout(layout string) error {
|
|
config.KeyboardLayout = layout
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getKeyboardMacros() (any, error) {
|
|
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
|
copy(macros, config.KeyboardMacros)
|
|
|
|
return macros, nil
|
|
}
|
|
|
|
type KeyboardMacrosParams struct {
|
|
Macros []any `json:"macros"`
|
|
}
|
|
|
|
func setKeyboardMacros(params KeyboardMacrosParams) (any, 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]any)
|
|
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"].([]any); ok {
|
|
for _, stepItem := range stepsArray {
|
|
stepMap, ok := stepItem.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
step := KeyboardMacroStep{}
|
|
|
|
if keysArray, ok := stepMap["keys"].([]any); ok {
|
|
for _, k := range keysArray {
|
|
if keyStr, ok := k.(string); ok {
|
|
step.Keys = append(step.Keys, keyStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
if modsArray, ok := stepMap["modifiers"].([]any); 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
|
|
}
|
|
|
|
func rpcGetLocalLoopbackOnly() (bool, error) {
|
|
return config.LocalLoopbackOnly, nil
|
|
}
|
|
|
|
func rpcSetLocalLoopbackOnly(enabled bool) error {
|
|
// Check if the setting is actually changing
|
|
if config.LocalLoopbackOnly == enabled {
|
|
return nil
|
|
}
|
|
|
|
// Update the setting
|
|
config.LocalLoopbackOnly = enabled
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var validLogLevels = map[string]bool{
|
|
"TRACE": true,
|
|
"DEBUG": true,
|
|
"INFO": true,
|
|
"WARN": true,
|
|
"ERROR": true,
|
|
}
|
|
|
|
const testLogProbeMessage = "JSON-RPC test log probe"
|
|
|
|
func rpcGetDefaultLogLevel() (string, error) {
|
|
return config.DefaultLogLevel, nil
|
|
}
|
|
|
|
func rpcSetDefaultLogLevel(level string) error {
|
|
if !validLogLevels[level] {
|
|
return fmt.Errorf("invalid log level: %s", level)
|
|
}
|
|
|
|
if config.DefaultLogLevel == level {
|
|
return nil
|
|
}
|
|
|
|
config.DefaultLogLevel = level
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
logging.GetRootLogger().UpdateLogLevel(level)
|
|
|
|
return nil
|
|
}
|
|
|
|
func rpcEmitTestLog(level string) error {
|
|
if !validLogLevels[level] {
|
|
return fmt.Errorf("invalid log level: %s", level)
|
|
}
|
|
|
|
testLogger := logging.GetSubsystemLogger("testlog")
|
|
|
|
switch level {
|
|
case "TRACE":
|
|
testLogger.Trace().Msg(testLogProbeMessage)
|
|
case "DEBUG":
|
|
testLogger.Debug().Msg(testLogProbeMessage)
|
|
case "INFO":
|
|
testLogger.Info().Msg(testLogProbeMessage)
|
|
case "WARN":
|
|
testLogger.Warn().Msg(testLogProbeMessage)
|
|
case "ERROR":
|
|
testLogger.Error().Msg(testLogProbeMessage)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
keyboardMacroCancel context.CancelFunc
|
|
keyboardMacroLock sync.Mutex
|
|
)
|
|
|
|
// cancelKeyboardMacro cancels any ongoing keyboard macro execution
|
|
func cancelKeyboardMacro() {
|
|
keyboardMacroLock.Lock()
|
|
defer keyboardMacroLock.Unlock()
|
|
|
|
if keyboardMacroCancel != nil {
|
|
keyboardMacroCancel()
|
|
logger.Info().Msg("canceled keyboard macro")
|
|
keyboardMacroCancel = nil
|
|
}
|
|
}
|
|
|
|
func setKeyboardMacroCancel(cancel context.CancelFunc) {
|
|
keyboardMacroLock.Lock()
|
|
defer keyboardMacroLock.Unlock()
|
|
|
|
keyboardMacroCancel = cancel
|
|
}
|
|
|
|
func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error {
|
|
cancelKeyboardMacro()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
setKeyboardMacroCancel(cancel)
|
|
|
|
s := hidrpc.KeyboardMacroState{
|
|
State: true,
|
|
IsPaste: true,
|
|
}
|
|
|
|
if currentSession != nil {
|
|
currentSession.reportHidRPCKeyboardMacroState(s)
|
|
}
|
|
|
|
err := rpcDoExecuteKeyboardMacro(ctx, macro)
|
|
|
|
setKeyboardMacroCancel(nil)
|
|
|
|
s.State = false
|
|
if currentSession != nil {
|
|
currentSession.reportHidRPCKeyboardMacroState(s)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func rpcCancelKeyboardMacro() {
|
|
cancelKeyboardMacro()
|
|
}
|
|
|
|
var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize)
|
|
|
|
func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool {
|
|
return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys)
|
|
}
|
|
|
|
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error {
|
|
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro")
|
|
|
|
for i, step := range macro {
|
|
delay := time.Duration(step.Delay) * time.Millisecond
|
|
|
|
err := rpcKeyboardReport(step.Modifier, step.Keys)
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to execute keyboard macro")
|
|
return err
|
|
}
|
|
|
|
// notify the device that the keyboard state is being cleared
|
|
if isClearKeyStep(step) {
|
|
gadget.UpdateKeysDown(0, keyboardClearStateKeys)
|
|
}
|
|
|
|
// Use context-aware sleep that can be cancelled
|
|
select {
|
|
case <-time.After(delay):
|
|
// Sleep completed normally
|
|
case <-ctx.Done():
|
|
// make sure keyboard state is reset
|
|
err := rpcKeyboardReport(0, keyboardClearStateKeys)
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to reset keyboard state")
|
|
}
|
|
|
|
logger.Debug().Int("step", i).Msg("Keyboard macro cancelled during sleep")
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var rpcHandlers = map[string]RPCHandler{
|
|
"ping": {Func: rpcPing},
|
|
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
|
"getDeviceID": {Func: rpcGetDeviceID},
|
|
"deregisterDevice": {Func: rpcDeregisterDevice},
|
|
"getCloudState": {Func: rpcGetCloudState},
|
|
"getNetworkState": {Func: rpcGetNetworkState},
|
|
"getNetworkSettings": {Func: rpcGetNetworkSettings},
|
|
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
|
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
|
"getKeyDownState": {Func: rpcGetKeysDownState},
|
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
|
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
|
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY", "wheelX"}},
|
|
"getVideoState": {Func: rpcGetVideoState},
|
|
"getUSBState": {Func: rpcGetUSBState},
|
|
"unmountImage": {Func: rpcUnmountImage},
|
|
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
|
|
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
|
|
"getJigglerState": {Func: rpcGetJigglerState},
|
|
"setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}},
|
|
"getJigglerConfig": {Func: rpcGetJigglerConfig},
|
|
"getTimezones": {Func: rpcGetTimezones},
|
|
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}, OptionalParams: []string{"broadcastIP"}},
|
|
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
|
|
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
|
|
"getVideoCodecPreference": {Func: rpcGetVideoCodecPreference},
|
|
"setVideoCodecPreference": {Func: rpcSetVideoCodecPreference, Params: []string{"codec"}},
|
|
"getAutoUpdateState": {Func: rpcGetAutoUpdateState},
|
|
"setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
|
|
"getEDID": {Func: rpcGetEDID},
|
|
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
|
|
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
|
|
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
|
|
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
|
|
"getDevChannelState": {Func: rpcGetDevChannelState},
|
|
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
|
"getLocalVersion": {Func: rpcGetLocalVersion},
|
|
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
|
"checkUpdateComponents": {Func: rpcCheckUpdateComponents, Params: []string{"params", "includePreRelease"}},
|
|
"getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel},
|
|
"tryUpdate": {Func: rpcTryUpdate},
|
|
"tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"params", "includePreRelease", "resetConfig"}},
|
|
"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"}},
|
|
"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"}},
|
|
"factoryReset": {Func: rpcFactoryReset},
|
|
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
|
|
"getDisplayRotation": {Func: rpcGetDisplayRotation},
|
|
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
|
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
|
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
|
|
"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"}},
|
|
"sendCustomCommand": {Func: rpcSendCustomCommand, Params: []string{"command"}},
|
|
"getSerialCommandHistory": {Func: rpcGetSerialCommandHistory},
|
|
"setSerialCommandHistory": {Func: rpcSetSerialCommandHistory, Params: []string{"commandHistory"}},
|
|
"deleteSerialCommandHistory": {Func: rpcDeleteSerialCommandHistory},
|
|
"setTerminalPaused": {Func: rpcSetTerminalPaused, Params: []string{"terminalPaused"}},
|
|
"getUsbDevices": {Func: rpcGetUsbDevices},
|
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
|
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
|
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
|
"getKeyboardMacros": {Func: getKeyboardMacros},
|
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
|
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
|
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
|
"getDefaultLogLevel": {Func: rpcGetDefaultLogLevel},
|
|
"setDefaultLogLevel": {Func: rpcSetDefaultLogLevel, Params: []string{"level"}},
|
|
"emitTestLog": {Func: rpcEmitTestLog, Params: []string{"level"}},
|
|
"getPublicIPAddresses": {Func: rpcGetPublicIPAddresses, Params: []string{"refresh"}},
|
|
"checkPublicIPAddresses": {Func: rpcCheckPublicIPAddresses},
|
|
"getTailscaleStatus": {Func: rpcGetTailscaleStatus},
|
|
"getTailscaleControlURL": {Func: rpcGetTailscaleControlURL},
|
|
"setTailscaleControlURL": {Func: rpcSetTailscaleControlURL, Params: []string{"controlURL"}},
|
|
"getMqttSettings": {Func: rpcGetMqttSettings},
|
|
"setMqttSettings": {Func: rpcSetMqttSettings, Params: []string{"settings"}},
|
|
"getMqttStatus": {Func: rpcGetMqttStatus},
|
|
"testMqttConnection": {Func: rpcTestMqttConnection, Params: []string{"settings"}},
|
|
}
|