diff --git a/.devcontainer/install-deps.sh b/.devcontainer/install-deps.sh index a412c21e..043c5c44 100755 --- a/.devcontainer/install-deps.sh +++ b/.devcontainer/install-deps.sh @@ -5,7 +5,7 @@ function sudo() { if [ "$UID" -eq 0 ]; then "$@" else - ${SUDO_PATH} -E "$@" + ${SUDO_PATH} "$@" fi } @@ -17,7 +17,8 @@ sudo apt-get install -y --no-install-recommends \ iputils-ping \ build-essential \ device-tree-compiler \ - gperf \ + gperf g++-multilib gcc-multilib \ + gdb-multiarch \ libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \ bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \ wget zstd \ @@ -31,35 +32,7 @@ pushd "${BUILDKIT_TMPDIR}" > /dev/null wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSION}/buildkit.tar.zst && \ sudo mkdir -p /opt/jetkvm-native-buildkit && \ - sudo tar --use-compress-program="zstd -d --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ + sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ rm buildkit.tar.zst popd - -# Install audio dependencies (ALSA and Opus) for JetKVM -echo "Installing JetKVM audio dependencies..." -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")" -AUDIO_DEPS_SCRIPT="${PROJECT_ROOT}/install_audio_deps.sh" - -if [ -f "${AUDIO_DEPS_SCRIPT}" ]; then - echo "Running audio dependencies installation..." - # Pre-create audio libs directory with proper permissions - sudo mkdir -p /opt/jetkvm-audio-libs - sudo chmod 777 /opt/jetkvm-audio-libs - # Run installation script (now it can write without sudo) - bash "${AUDIO_DEPS_SCRIPT}" - echo "Audio dependencies installation completed." - if [ -d "/opt/jetkvm-audio-libs" ]; then - echo "Audio libraries installed in /opt/jetkvm-audio-libs" - # Set recursive permissions for all subdirectories and files - sudo chmod -R 777 /opt/jetkvm-audio-libs - echo "Permissions set to allow all users access to audio libraries" - else - echo "Error: /opt/jetkvm-audio-libs directory not found after installation." - exit 1 - fi -else - echo "Warning: Audio dependencies script not found at ${AUDIO_DEPS_SCRIPT}" - echo "Skipping audio dependencies installation." -fi rm -rf "${BUILDKIT_TMPDIR}" diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000..91241db0 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc", + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-arm", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..710996cc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "GDB Debug - Native (binary)", + "type": "cppdbg", + "request": "launch", + "program": "internal/native/cgo/build/jknative-bin", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}", + "environment": [], + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb-multiarch", + "miDebuggerServerAddress": "${config:TARGET_IP}:${config:DEBUG_PORT}", + "targetArchitecture": "arm", + "preLaunchTask": "deploy", + "setupCommands": [ + { + "description": "Pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "externalConsole": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 41aeee58..a610159c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,19 @@ ] }, "git.ignoreLimitWarning": true, - "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo", - "cmake.ignoreCMakeListsMissing": true + "cmake.sourceDirectory": "${workspaceFolder}/internal/native/cgo", + "cmake.ignoreCMakeListsMissing": true, + "C_Cpp.inlayHints.autoDeclarationTypes.enabled": true, + "C_Cpp.inlayHints.parameterNames.enabled": true, + "C_Cpp.inlayHints.referenceOperator.enabled": true, + "TARGET_IP": "192.168.0.199", + "DEBUG_PORT": "2345", + "json.schemas": [ + { + "fileMatch": [ + "/internal/ota/testdata/ota/*.json" + ], + "url": "./internal/ota/testdata/ota.schema.json" + } + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..e1f9accc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "deploy", + "isBackground": true, + "type": "shell", + "command": "bash", + "args": [ + "dev_deploy.sh", + "-r", + "${config:TARGET_IP}", + "--gdb-port", + "${config:DEBUG_PORT}", + "--native-binary", + "--disable-docker" + ], + "problemMatcher": { + "base": "$gcc", + "background": { + "activeOnStart": true, + "beginsPattern": "${config:BINARY}", + "endsPattern": "Listening on port [0-9]{4}" + } + } + }, + ] +} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0ab65c4f..9d3a4b62 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -208,6 +208,12 @@ rm /userdata/kvm_config.json systemctl restart jetkvm ``` +### Debug native code with gdbserver + +Change the `TARGET_IP` in `.vscode/settings.json` to your JetKVM device IP, then set breakpoints in your native code and start the `Debug Native` configuration in VSCode. + +The code and GDB server will be deployed automatically. + --- ## Testing Your Changes diff --git a/Makefile b/Makefile index 8c2b18ef..0390bd06 100644 --- a/Makefile +++ b/Makefile @@ -1,51 +1,9 @@ -# Build ALSA and Opus static libs for ARM in /opt/jetkvm-audio-libs -build_audio_deps: - bash .devcontainer/install_audio_deps.sh $(ALSA_VERSION) $(OPUS_VERSION) - -# Prepare everything needed for local development (toolchain + audio deps + Go tools) -dev_env: build_audio_deps - $(CLEAN_GO_CACHE) - @echo "Installing Go development tools..." - go install golang.org/x/tools/cmd/goimports@latest - @echo "Development environment ready." -JETKVM_HOME ?= $(HOME)/.jetkvm -BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit -BUILDKIT_FLAVOR ?= arm-rockchip830-linux-uclibcgnueabihf -AUDIO_LIBS_DIR ?= /opt/jetkvm-audio-libs - -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.4.9-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.4.8 - - -# Audio library versions -ALSA_VERSION ?= 1.2.14 -OPUS_VERSION ?= 1.5.2 - -# Set PKG_CONFIG_PATH globally for all targets that use CGO with audio libraries -export PKG_CONFIG_PATH := $(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/utils:$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION) - -# Common command to clean Go cache with verbose output for all Go builds -CLEAN_GO_CACHE := @echo "Cleaning Go cache..."; go clean -cache -v - -# Optimization flags for ARM Cortex-A7 with NEON SIMD -OPTIM_CFLAGS := -O3 -mfpu=neon -mtune=cortex-a7 -mfloat-abi=hard -ftree-vectorize -ffast-math -funroll-loops -mvectorize-with-neon-quad -marm -D__ARM_NEON - -# Cross-compilation environment for ARM - exported globally -export GOOS := linux -export GOARCH := arm -export GOARM := 7 -export CC := $(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc -export CGO_ENABLED := 1 -export CGO_CFLAGS := $(OPTIM_CFLAGS) -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/include -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/include -export CGO_LDFLAGS := -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib -lrockit -lrockchip_mpp -lrga -lpthread -lm -ldl - -# Audio-specific flags (only used for audio C binaries, NOT for main Go app) -AUDIO_CFLAGS := $(CGO_CFLAGS) -I$(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/include -I$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/include -I$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/celt -AUDIO_LDFLAGS := $(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/src/.libs/libasound.a $(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/.libs/libopus.a -lm -ldl -lpthread +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.5.0-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.4.9 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm @@ -56,6 +14,8 @@ SKIP_NATIVE_IF_EXISTS ?= 0 SKIP_UI_BUILD ?= 0 ENABLE_SYNC_TRACE ?= 0 +CMAKE_BUILD_TYPE ?= Release + GO_BUILD_ARGS := -tags netgo,timetzdata,nomsgpack ifeq ($(ENABLE_SYNC_TRACE), 1) GO_BUILD_ARGS := $(GO_BUILD_ARGS),synctrace @@ -94,29 +54,26 @@ build_native: echo "Building native..."; \ CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \ LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \ + CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \ ./scripts/build_cgo.sh; \ fi -build_dev: build_native build_audio_deps - $(CLEAN_GO_CACHE) +build_dev: build_native @echo "Building..." - go build \ + $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ $(GO_RELEASE_BUILD_ARGS) \ -o $(BIN_DIR)/jetkvm_app -v cmd/main.go build_test2json: - $(CLEAN_GO_CACHE) $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json build_gotestsum: - $(CLEAN_GO_CACHE) @echo "Building gotestsum..." $(GO_CMD) install gotest.tools/gotestsum@latest cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum -build_dev_test: build_audio_deps build_test2json build_gotestsum - $(CLEAN_GO_CACHE) +build_dev_test: build_test2json build_gotestsum # collect all directories that contain tests @echo "Building tests for devices ..." @rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests @@ -126,7 +83,7 @@ build_dev_test: build_audio_deps build_test2json build_gotestsum test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \ test_pkg_full_name=$(KVM_PKG_NAME)/$$(echo $$test | sed 's/^.\///g'); \ test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \ - go test -v \ + $(GO_CMD) test -v \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ $(GO_BUILD_ARGS) \ -c -o $(BIN_DIR)/tests/$$test_filename $$test; \ @@ -163,10 +120,9 @@ dev_release: frontend build_dev rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app.sha256 -build_release: frontend build_native build_audio_deps - $(CLEAN_GO_CACHE) +build_release: frontend build_native @echo "Building release..." - go build \ + $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ $(GO_RELEASE_BUILD_ARGS) \ -o bin/jetkvm_app cmd/main.go @@ -180,37 +136,4 @@ release: @echo "Uploading release..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 - -# Run both Go and UI linting -lint: lint-go lint-ui - @echo "All linting completed successfully!" - -# Run golangci-lint locally with the same configuration as CI -lint-go: build_audio_deps - @echo "Running golangci-lint..." - @mkdir -p static && touch static/.gitkeep - golangci-lint run --verbose - -# Run both Go and UI linting with auto-fix -lint-fix: lint-go-fix lint-ui-fix - @echo "All linting with auto-fix completed successfully!" - -# Run golangci-lint with auto-fix -lint-go-fix: build_audio_deps - @echo "Running golangci-lint with auto-fix..." - @mkdir -p static && touch static/.gitkeep - golangci-lint run --fix --verbose - -# Run UI linting locally (mirrors GitHub workflow ui-lint.yml) -lint-ui: - @echo "Running UI lint..." - @cd ui && npm ci && npm run lint - -# Run UI linting with auto-fix -lint-ui-fix: - @echo "Running UI lint with auto-fix..." - @cd ui && npm ci && npm run lint:fix - -# Legacy alias for UI linting (for backward compatibility) -ui-lint: lint-ui + rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 9a1e1899..fcf2cdfe 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,23 +13,39 @@ import ( "github.com/erikdubbelboer/gspt" "github.com/jetkvm/kvm" + "github.com/jetkvm/kvm/internal/native" + "github.com/jetkvm/kvm/internal/supervisor" ) -const ( - envChildID = "JETKVM_CHILD_ID" - errorDumpDir = "/userdata/jetkvm/crashdump" - errorDumpLastFile = "last-crash.log" - errorDumpTemplate = "jetkvm-%s.log" +var ( + subcomponent string ) func program() { - gspt.SetProcTitle(os.Args[0] + " [app]") - kvm.Main() + subcomponentOverride := os.Getenv(supervisor.EnvSubcomponent) + if subcomponentOverride != "" { + subcomponent = subcomponentOverride + } + switch subcomponent { + case "native": + native.RunNativeProcess(os.Args[0]) + default: + kvm.Main() + } +} + +func setProcTitle(status string) { + if status != "" { + status = " " + status + } + title := fmt.Sprintf("jetkvm: [supervisor]%s", status) + gspt.SetProcTitle(title) } func main() { versionPtr := flag.Bool("version", false, "print version and exit") versionJSONPtr := flag.Bool("version-json", false, "print version as json and exit") + flag.StringVar(&subcomponent, "subcomponent", "", "subcomponent to run") flag.Parse() if *versionPtr || *versionJSONPtr { @@ -42,7 +58,7 @@ func main() { return } - childID := os.Getenv(envChildID) + childID := os.Getenv(supervisor.EnvChildID) switch childID { case "": doSupervise() @@ -55,6 +71,8 @@ func main() { } func supervise() error { + setProcTitle("") + // check binary path binPath, err := os.Executable() if err != nil { @@ -74,11 +92,11 @@ func supervise() error { // run the child binary cmd := exec.Command(binPath) - lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile) + lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile) cmd.Env = append(os.Environ(), []string{ - fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()), - fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath), + fmt.Sprintf("%s=%s", supervisor.EnvChildID, kvm.GetBuiltAppVersion()), + fmt.Sprintf("%s=%s", supervisor.ErrorDumpLastFile, lastFilePath), }...) cmd.Args = os.Args @@ -99,6 +117,8 @@ func supervise() error { return fmt.Errorf("failed to start command: %w", startErr) } + setProcTitle(fmt.Sprintf("started (pid=%d)", cmd.Process.Pid)) + go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM) @@ -107,8 +127,6 @@ func supervise() error { _ = cmd.Process.Signal(sig) }() - gspt.SetProcTitle(os.Args[0] + " [sup]") - cmdErr := cmd.Wait() if cmdErr == nil { return nil @@ -186,11 +204,11 @@ func renameFile(f *os.File, newName string) error { func ensureErrorDumpDir() error { // TODO: check if the directory is writable - f, err := os.Stat(errorDumpDir) + f, err := os.Stat(supervisor.ErrorDumpDir) if err == nil && f.IsDir() { return nil } - if err := os.MkdirAll(errorDumpDir, 0755); err != nil { + if err := os.MkdirAll(supervisor.ErrorDumpDir, 0755); err != nil { return fmt.Errorf("failed to create error dump directory: %w", err) } return nil @@ -200,7 +218,7 @@ func createErrorDump(logFile *os.File) { fmt.Println() fileName := fmt.Sprintf( - errorDumpTemplate, + supervisor.ErrorDumpTemplate, time.Now().Format("20060102-150405"), ) @@ -210,7 +228,7 @@ func createErrorDump(logFile *os.File) { return } - filePath := filepath.Join(errorDumpDir, fileName) + filePath := filepath.Join(supervisor.ErrorDumpDir, fileName) if err := renameFile(logFile, filePath); err != nil { fmt.Printf("failed to rename file: %v\n", err) return @@ -218,7 +236,7 @@ func createErrorDump(logFile *os.File) { fmt.Printf("error dump copied: %s\n", filePath) - lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile) + lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile) if err := ensureSymlink(filePath, lastFilePath); err != nil { fmt.Printf("failed to create symlink: %v\n", err) diff --git a/config.go b/config.go index b573e398..450c5029 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "sync" "github.com/jetkvm/kvm/internal/confparser" @@ -16,6 +17,10 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" ) +const ( + DefaultAPIURL = "https://api.jetkvm.com" +) + type WakeOnLanDevice struct { Name string `json:"name"` MacAddress string `json:"macAddress"` @@ -81,6 +86,7 @@ func (m *KeyboardMacro) Validate() error { type Config struct { CloudURL string `json:"cloud_url"` + UpdateAPIURL string `json:"update_api_url"` CloudAppURL string `json:"cloud_app_url"` CloudToken string `json:"cloud_token"` GoogleIdentity string `json:"google_identity"` @@ -118,8 +124,18 @@ type Config struct { AudioBufferPeriods int `json:"audio_buffer_periods"` // 2-24 AudioSampleRate int `json:"audio_sample_rate"` // Hz (32000, 44100, 48000) AudioPacketLossPerc int `json:"audio_packet_loss_perc"` // 0-100 + NativeMaxRestart uint `json:"native_max_restart_attempts"` } +// GetUpdateAPIURL returns the update API URL +func (c *Config) GetUpdateAPIURL() string { + if c.UpdateAPIURL == "" { + return DefaultAPIURL + } + return strings.TrimSuffix(c.UpdateAPIURL, "/") + "/releases" +} + +// GetDisplayRotation returns the display rotation func (c *Config) GetDisplayRotation() uint16 { rotationInt, err := strconv.ParseUint(c.DisplayRotation, 10, 16) if err != nil { @@ -129,6 +145,7 @@ func (c *Config) GetDisplayRotation() uint16 { return uint16(rotationInt) } +// SetDisplayRotation sets the display rotation func (c *Config) SetDisplayRotation(rotation string) error { _, err := strconv.ParseUint(rotation, 10, 16) if err != nil { @@ -168,7 +185,8 @@ var ( func getDefaultConfig() Config { return Config{ - CloudURL: "https://api.jetkvm.com", + CloudURL: DefaultAPIURL, + UpdateAPIURL: DefaultAPIURL, CloudAppURL: "https://app.jetkvm.com", AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", diff --git a/display.go b/display.go index 68723b59..bf802e05 100644 --- a/display.go +++ b/display.go @@ -232,6 +232,14 @@ func updateStaticContents() { // nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID()) } +// configureDisplayOnNativeRestart is called when the native process restarts +// it ensures the display is configured correctly after the restart +func configureDisplayOnNativeRestart() { + displayLogger.Info().Msg("native restarted, configuring display") + updateStaticContents() + requestDisplayUpdate(true, "native_restart") +} + // setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter // the backlight brightness of the JetKVM hardware's display. func setDisplayBrightness(brightness int, reason string) error { diff --git a/failsafe.go b/failsafe.go index 3c6b3d3a..d7de1c81 100644 --- a/failsafe.go +++ b/failsafe.go @@ -5,6 +5,8 @@ import ( "os" "strings" "sync" + + "github.com/jetkvm/kvm/internal/supervisor" ) const ( @@ -77,11 +79,17 @@ func checkFailsafeReason() { _ = os.Remove(lastCrashPath) // TODO: read the goroutine stack trace and check which goroutine is panicking - if strings.Contains(failsafeCrashLog, "runtime.cgocall") { - failsafeModeActive = true + failsafeModeActive = true + if strings.Contains(failsafeCrashLog, supervisor.FailsafeReasonVideoMaxRestartAttemptsReached) { failsafeModeReason = "video" return } + if strings.Contains(failsafeCrashLog, "runtime.cgocall") { + failsafeModeReason = "video" + return + } else { + failsafeModeReason = "unknown" + } }) } diff --git a/go.mod b/go.mod index 404215b0..aac58799 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/creack/goselect v0.1.2 // indirect @@ -87,6 +88,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/ugorji/go/codec v1.3.0 // indirect @@ -97,6 +99,9 @@ require ( golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1cb90138..8d33b159 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= @@ -173,6 +175,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU= github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -226,6 +230,12 @@ golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hw.go b/hw.go index 7797adc1..f6ec57f5 100644 --- a/hw.go +++ b/hw.go @@ -8,6 +8,8 @@ import ( "strings" "sync" "time" + + "github.com/jetkvm/kvm/internal/ota" ) func extractSerialNumber() (string, error) { @@ -29,22 +31,16 @@ func extractSerialNumber() (string, error) { return matches[1], nil } -func readOtpEntropy() ([]byte, error) { //nolint:unused - content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem") - if err != nil { - return nil, err - } - return content[0x17:0x1C], nil -} - -func hwReboot(force bool, postRebootAction *PostRebootAction, delay time.Duration) error { - logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay) +func hwReboot(force bool, postRebootAction *ota.PostRebootAction, delay time.Duration) error { + logger.Info().Dur("delayMs", delay).Msg("reboot requested") writeJSONRPCEvent("willReboot", postRebootAction, currentSession) time.Sleep(1 * time.Second) // Wait for the JSONRPCEvent to be sent nativeInstance.SwitchToScreenIfDifferent("rebooting_screen") - time.Sleep(delay - (1 * time.Second)) // wait requested extra settle time + if delay > 1*time.Second { + time.Sleep(delay - 1*time.Second) // wait requested extra settle time + } args := []string{} if force { diff --git a/internal/native/README.md b/internal/native/README.md new file mode 100644 index 00000000..97a2cf79 --- /dev/null +++ b/internal/native/README.md @@ -0,0 +1,20 @@ +# jetkvm-native + +This component (`internal/native/`) acts as a bridge between Golang and native (C/C++) code. +It manages spawning and communicating with a native process via sockets (gRPC and Unix stream). + +For performance-critical operations such as video frame, **a dedicated Unix socket should be used** to avoid the overhead of gRPC and ensure low-latency communication. + +## Debugging + +To enable debug mode, create a file called `.native-debug-mode` in the `/userdata/jetkvm` directory. + +```bash +touch /userdata/jetkvm/.native-debug-mode +``` + +This will cause the native process to listen for SIGHUP signal and crash the process. + +```bash +pgrep native | xargs kill -SIGHUP +``` \ No newline at end of file diff --git a/internal/native/cgo/CMakeLists.txt b/internal/native/cgo/CMakeLists.txt index 739dabe5..c4c01ded 100644 --- a/internal/native/cgo/CMakeLists.txt +++ b/internal/native/cgo/CMakeLists.txt @@ -42,6 +42,8 @@ FetchContent_MakeAvailable(lvgl) # Get source files, excluding CMake generated files file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.c" "ui/*.c") list(FILTER sources EXCLUDE REGEX "CMakeFiles.*CompilerId.*\\.c$") +# Exclude main.c from library sources (it's used for the binary target) +list(FILTER sources EXCLUDE REGEX "main\\.c$") add_library(jknative STATIC ${sources} ${CMAKE_CURRENT_SOURCE_DIR}/ctrl.h) @@ -68,4 +70,14 @@ target_link_libraries(jknative PRIVATE # libgpiod ) -install(TARGETS jknative DESTINATION lib) \ No newline at end of file +# Binary target using main.c as entry point +add_executable(jknative-bin ${CMAKE_CURRENT_SOURCE_DIR}/main.c) + +# Link the binary to the library (if needed in the future) +target_link_libraries(jknative-bin PRIVATE + jknative + pthread +) + +install(TARGETS jknative DESTINATION lib) +install(TARGETS jknative-bin DESTINATION bin) \ No newline at end of file diff --git a/internal/native/cgo/main.c b/internal/native/cgo/main.c new file mode 100644 index 00000000..a3e0e22f --- /dev/null +++ b/internal/native/cgo/main.c @@ -0,0 +1,227 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ctrl.h" +#include "main.h" + +#define SOCKET_PATH "/tmp/video.sock" +#define BUFFER_SIZE 4096 + +// Global state +static int client_fd = -1; +static pthread_mutex_t client_fd_mutex = PTHREAD_MUTEX_INITIALIZER; + +void jetkvm_c_log_handler(int level, const char *filename, const char *funcname, int line, const char *message) { + // printf("[%s] %s:%d %s: %s\n", filename ? filename : "unknown", funcname ? funcname : "unknown", line, message ? message : ""); + fprintf(stderr, "[%s] %s:%d %s: %s\n", filename ? filename : "unknown", funcname ? funcname : "unknown", line, message ? message : ""); +} + +// Video handler that pipes frames to the Unix socket +// This will be called by the video subsystem via video_send_frame -> jetkvm_set_video_handler's handler +void jetkvm_video_handler(const uint8_t *frame, ssize_t len) { + // pthread_mutex_lock(&client_fd_mutex); + // if (client_fd >= 0 && frame != NULL && len > 0) { + // ssize_t bytes_written = 0; + // while (bytes_written < len) { + // ssize_t n = write(client_fd, frame + bytes_written, len - bytes_written); + // if (n < 0) { + // if (errno == EPIPE || errno == ECONNRESET) { + // // Client disconnected + // close(client_fd); + // client_fd = -1; + // break; + // } + // perror("write"); + // break; + // } + // bytes_written += n; + // } + // } + // pthread_mutex_unlock(&client_fd_mutex); +} + +void jetkvm_video_state_handler(jetkvm_video_state_t *state) { + fprintf(stderr, "Video state: {\n" + "\"ready\": %d,\n" + "\"error\": \"%s\",\n" + "\"width\": %d,\n" + "\"height\": %d,\n" + "\"frame_per_second\": %f\n" + "}\n", state->ready, state->error, state->width, state->height, state->frame_per_second); +} + +void jetkvm_indev_handler(int code) { + fprintf(stderr, "Video indev: %d\n", code); +} + +void jetkvm_rpc_handler(const char *method, const char *params) { + fprintf(stderr, "Video rpc: %s %s\n", method, params); +} + +// Note: jetkvm_set_video_handler, jetkvm_set_indev_handler, jetkvm_set_rpc_handler, +// jetkvm_call_rpc_handler, and jetkvm_set_video_state_handler are implemented in +// the library (ctrl.c) and will be used from there when linking. + +int main(int argc, char *argv[]) { + const char *socket_path = SOCKET_PATH; + + // Allow custom socket path via command line argument + if (argc > 1) { + socket_path = argv[1]; + } + + // Remove existing socket file if it exists + unlink(socket_path); + + // Set handlers + jetkvm_set_log_handler(&jetkvm_c_log_handler); + jetkvm_set_video_handler(&jetkvm_video_handler); + jetkvm_set_video_state_handler(&jetkvm_video_state_handler); + jetkvm_set_indev_handler(&jetkvm_indev_handler); + jetkvm_set_rpc_handler(&jetkvm_rpc_handler); + + // Initialize video first (before accepting connections) + fprintf(stderr, "Initializing video...\n"); + if (jetkvm_video_init(1.0) != 0) { + fprintf(stderr, "Failed to initialize video\n"); + return 1; + } + + // Start video streaming - frames will be sent via video_send_frame + // which calls the video handler we set up + jetkvm_video_start(); + fprintf(stderr, "Video streaming started.\n"); + + // Create Unix domain socket + int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + perror("socket"); + jetkvm_video_stop(); + jetkvm_video_shutdown(); + return 1; + } + + // Make socket non-blocking + int flags = fcntl(server_fd, F_GETFL, 0); + if (flags < 0 || fcntl(server_fd, F_SETFL, flags | O_NONBLOCK) < 0) { + perror("fcntl"); + close(server_fd); + jetkvm_video_stop(); + jetkvm_video_shutdown(); + return 1; + } + + // Bind socket to path + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(server_fd); + jetkvm_video_stop(); + jetkvm_video_shutdown(); + return 1; + } + + // Listen for connections + if (listen(server_fd, 1) < 0) { + perror("listen"); + close(server_fd); + jetkvm_video_stop(); + jetkvm_video_shutdown(); + return 1; + } + + fprintf(stderr, "Listening on Unix socket: %s (non-blocking)\n", socket_path); + fprintf(stderr, "Video frames will be sent to connected clients...\n"); + + // Main loop: check for new connections and handle client disconnections + fd_set read_fds; + struct timeval timeout; + + while (1) { + FD_ZERO(&read_fds); + FD_SET(server_fd, &read_fds); + + pthread_mutex_lock(&client_fd_mutex); + int current_client_fd = client_fd; + if (current_client_fd >= 0) { + FD_SET(current_client_fd, &read_fds); + } + int max_fd = (current_client_fd > server_fd) ? current_client_fd : server_fd; + pthread_mutex_unlock(&client_fd_mutex); + + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + int result = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); + if (result < 0) { + if (errno == EINTR) { + continue; + } + perror("select"); + break; + } + + // Check for new connection + if (FD_ISSET(server_fd, &read_fds)) { + int accepted_fd = accept(server_fd, NULL, NULL); + if (accepted_fd >= 0) { + fprintf(stderr, "Client connected\n"); + pthread_mutex_lock(&client_fd_mutex); + if (client_fd >= 0) { + // Close previous client if any + close(client_fd); + } + client_fd = accepted_fd; + pthread_mutex_unlock(&client_fd_mutex); + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + perror("accept"); + } + } + + // Check if client disconnected + pthread_mutex_lock(&client_fd_mutex); + current_client_fd = client_fd; + pthread_mutex_unlock(&client_fd_mutex); + + if (current_client_fd >= 0 && FD_ISSET(current_client_fd, &read_fds)) { + // Client sent data or closed connection + char buffer[1]; + if (read(current_client_fd, buffer, 1) <= 0) { + fprintf(stderr, "Client disconnected\n"); + pthread_mutex_lock(&client_fd_mutex); + close(client_fd); + client_fd = -1; + pthread_mutex_unlock(&client_fd_mutex); + } + } + } + + // Stop video streaming + jetkvm_video_stop(); + jetkvm_video_shutdown(); + + // Cleanup + pthread_mutex_lock(&client_fd_mutex); + if (client_fd >= 0) { + close(client_fd); + client_fd = -1; + } + pthread_mutex_unlock(&client_fd_mutex); + + close(server_fd); + unlink(socket_path); + + return 0; +} + diff --git a/internal/native/cgo/main.h b/internal/native/cgo/main.h new file mode 100644 index 00000000..d9cc6c49 --- /dev/null +++ b/internal/native/cgo/main.h @@ -0,0 +1,26 @@ +#ifndef JETKVM_NATIVE_MAIN_H +#define JETKVM_NATIVE_MAIN_H + +#include +#include +#include +#include +#include +#include +#include +#include "ctrl.h" + +void jetkvm_c_log_handler(int level, const char *filename, const char *funcname, int line, const char *message); +void jetkvm_video_handler(const uint8_t *frame, ssize_t len); +void jetkvm_video_state_handler(jetkvm_video_state_t *state); +void jetkvm_indev_handler(int code); +void jetkvm_rpc_handler(const char *method, const char *params); + + +// typedef void (jetkvm_video_state_handler_t)(jetkvm_video_state_t *state); +// typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, int line, const char *message); +// typedef void (jetkvm_rpc_handler_t)(const char *method, const char *params); +// typedef void (jetkvm_video_handler_t)(const uint8_t *frame, ssize_t len); +// typedef void (jetkvm_indev_handler_t)(int code); + +#endif \ No newline at end of file diff --git a/internal/native/cgo/test.patch b/internal/native/cgo/test.patch new file mode 100644 index 00000000..5f32a357 --- /dev/null +++ b/internal/native/cgo/test.patch @@ -0,0 +1,210 @@ +diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c +index 2a4a034..760621a 100644 +--- a/internal/native/cgo/video.c ++++ b/internal/native/cgo/video.c +@@ -354,6 +354,10 @@ bool detected_signal = false, streaming_flag = false, streaming_stopped = true; + pthread_t *streaming_thread = NULL; + pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER; + ++// Diagnostic tracking for validation ++static uint64_t last_close_time = 0; ++static int consecutive_failures = 0; ++ + bool get_streaming_flag() + { + log_info("getting streaming flag"); +@@ -395,6 +399,12 @@ void *run_video_stream(void *arg) + continue; + } + ++ // Log attempt to open with timing info ++ RK_U64 time_since_close = last_close_time > 0 ? (get_us() - last_close_time) : 0; ++ log_info("[DIAG] Attempting to open %s (time_since_last_close=%llu us)", ++ VIDEO_DEV, time_since_close); ++ ++ RK_U64 open_start_time = get_us(); + int video_dev_fd = open(VIDEO_DEV, O_RDWR); + if (video_dev_fd < 0) + { +@@ -402,7 +412,9 @@ void *run_video_stream(void *arg) + usleep(1000000); + continue; + } +- log_info("opened video capture device %s", VIDEO_DEV); ++ RK_U64 open_end_time = get_us(); ++ log_info("[DIAG] opened video capture device %s in %llu us", ++ VIDEO_DEV, open_end_time - open_start_time); + + uint32_t width = detected_width; + uint32_t height = detected_height; +@@ -414,14 +426,45 @@ void *run_video_stream(void *arg) + fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUYV; + fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; + ++ // Probe device state before attempting format set ++ struct v4l2_format query_fmt; ++ memset(&query_fmt, 0, sizeof(query_fmt)); ++ query_fmt.type = type; ++ int query_ret = ioctl(video_dev_fd, VIDIOC_G_FMT, &query_fmt); ++ log_info("[DIAG] VIDIOC_G_FMT probe: ret=%d, errno=%d (%s)", ++ query_ret, query_ret < 0 ? errno : 0, ++ query_ret < 0 ? strerror(errno) : "OK"); ++ ++ RK_U64 set_fmt_start_time = get_us(); ++ log_info("[DIAG] Attempting VIDIOC_S_FMT: %ux%u, time_since_open=%llu us", ++ width, height, set_fmt_start_time - open_end_time); ++ + if (ioctl(video_dev_fd, VIDIOC_S_FMT, &fmt) < 0) + { +- log_error("Set format fail: %s", strerror(errno)); ++ RK_U64 failure_time = get_us(); ++ int saved_errno = errno; ++ consecutive_failures++; ++ ++ log_error("[DIAG] Set format fail: errno=%d (%s)", saved_errno, strerror(saved_errno)); ++ log_error("[DIAG] Failure context: consecutive_failures=%d, time_since_open=%llu us, " ++ "time_since_last_close=%llu us, resolution=%ux%u, streaming_flag=%d", ++ consecutive_failures, ++ failure_time - open_end_time, ++ last_close_time > 0 ? (open_start_time - last_close_time) : 0, ++ width, height, ++ streaming_flag); ++ + usleep(100000); // Sleep for 100 milliseconds + close(video_dev_fd); ++ last_close_time = get_us(); ++ log_info("[DIAG] Closed device after format failure at %llu us", last_close_time); + continue; + } + ++ // Success - reset failure counter ++ log_info("[DIAG] VIDIOC_S_FMT succeeded (previous consecutive failures: %d)", consecutive_failures); ++ consecutive_failures = 0; ++ + struct v4l2_buffer buf; + + struct v4l2_requestbuffers req; +@@ -601,9 +644,46 @@ void *run_video_stream(void *arg) + } + cleanup: + log_info("cleaning up video capture device %s", VIDEO_DEV); +- if (ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type) < 0) ++ ++ RK_U64 streamoff_start = get_us(); ++ log_info("[DIAG] Attempting VIDIOC_STREAMOFF"); ++ ++ int streamoff_ret = ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type); ++ RK_U64 streamoff_end = get_us(); ++ ++ if (streamoff_ret < 0) ++ { ++ log_error("[DIAG] VIDIOC_STREAMOFF failed: errno=%d (%s), duration=%llu us", ++ errno, strerror(errno), streamoff_end - streamoff_start); ++ } ++ else ++ { ++ log_info("[DIAG] VIDIOC_STREAMOFF succeeded in %llu us", ++ streamoff_end - streamoff_start); ++ } ++ ++ // VALIDATION TEST: Explicitly free V4L2 buffer queue ++ struct v4l2_requestbuffers req_free; ++ memset(&req_free, 0, sizeof(req_free)); ++ req_free.count = 0; // Tell driver to free all buffers ++ req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; ++ req_free.memory = V4L2_MEMORY_DMABUF; ++ ++ RK_U64 reqbufs_start = get_us(); ++ log_info("[DIAG] VALIDATION: Calling VIDIOC_REQBUFS(count=0) to free buffer queue"); ++ ++ int reqbufs_ret = ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free); ++ RK_U64 reqbufs_end = get_us(); ++ ++ if (reqbufs_ret < 0) ++ { ++ log_error("[DIAG] VALIDATION: REQBUFS(0) FAILED - errno=%d (%s), duration=%llu us", ++ errno, strerror(errno), reqbufs_end - reqbufs_start); ++ } ++ else + { +- log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno)); ++ log_info("[DIAG] VALIDATION: REQBUFS(0) SUCCEEDED - freed buffers in %llu us", ++ reqbufs_end - reqbufs_start); + } + + venc_stop(); +@@ -617,9 +697,13 @@ void *run_video_stream(void *arg) + } + + log_info("closing video capture device %s", VIDEO_DEV); ++ RK_U64 close_start = get_us(); + close(video_dev_fd); ++ last_close_time = get_us(); ++ log_info("[DIAG] Device closed, took %llu us, timestamp=%llu", ++ last_close_time - close_start, last_close_time); + } +- ++ + log_info("video stream thread exiting"); + + streaming_stopped = true; +@@ -648,7 +732,7 @@ void video_shutdown() + RK_MPI_MB_DestroyPool(memPool); + } + log_info("Destroyed memory pool"); +- ++ + pthread_mutex_destroy(&streaming_mutex); + log_info("Destroyed streaming mutex"); + } +@@ -665,14 +749,14 @@ void video_start_streaming() + log_warn("video streaming already started"); + return; + } +- ++ + pthread_t *new_thread = malloc(sizeof(pthread_t)); + if (new_thread == NULL) + { + log_error("Failed to allocate memory for streaming thread"); + return; + } +- ++ + set_streaming_flag(true); + int result = pthread_create(new_thread, NULL, run_video_stream, NULL); + if (result != 0) +@@ -682,7 +766,7 @@ void video_start_streaming() + free(new_thread); + return; + } +- ++ + // Only set streaming_thread after successful creation + streaming_thread = new_thread; + } +@@ -693,7 +777,7 @@ void video_stop_streaming() + log_info("video streaming already stopped"); + return; + } +- ++ + log_info("stopping video streaming"); + set_streaming_flag(false); + +@@ -711,7 +795,7 @@ void video_stop_streaming() + free(streaming_thread); + streaming_thread = NULL; + +- log_info("video streaming stopped"); ++ log_info("video streaming stopped"); + } + + void video_restart_streaming() +@@ -818,4 +902,4 @@ void video_set_quality_factor(float factor) + + float video_get_quality_factor() { + return quality_factor; +-} +\ No newline at end of file ++} diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go index be1a5a36..b33eb534 100644 --- a/internal/native/cgo_linux.go +++ b/internal/native/cgo_linux.go @@ -50,17 +50,9 @@ static inline void jetkvm_cgo_setup_rpc_handler() { import "C" var ( - cgoLock sync.Mutex - cgoDisabled bool + cgoLock sync.Mutex ) -func setCgoDisabled(disabled bool) { - cgoLock.Lock() - defer cgoLock.Unlock() - - cgoDisabled = disabled -} - //export jetkvm_go_video_state_handler func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) { videoState := VideoState{ @@ -104,10 +96,6 @@ func jetkvm_go_rpc_handler(method *C.cchar_t, params *C.cchar_t) { var eventCodeToNameMap = map[int]string{} func uiEventCodeToName(code int) string { - if cgoDisabled { - return "" - } - name, ok := eventCodeToNameMap[code] if !ok { cCode := C.int(code) @@ -120,10 +108,6 @@ func uiEventCodeToName(code int) string { } func setUpNativeHandlers() { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -135,10 +119,6 @@ func setUpNativeHandlers() { } func uiInit(rotation uint16) { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -148,10 +128,6 @@ func uiInit(rotation uint16) { } func uiTick() { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -159,10 +135,6 @@ func uiTick() { } func videoInit(factor float64) error { - if cgoDisabled { - return nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -176,10 +148,6 @@ func videoInit(factor float64) error { } func videoShutdown() { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -187,10 +155,6 @@ func videoShutdown() { } func videoStart() { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -198,10 +162,6 @@ func videoStart() { } func videoStop() { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -209,10 +169,6 @@ func videoStop() { } func videoLogStatus() string { - if cgoDisabled { - return "" - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -223,10 +179,6 @@ func videoLogStatus() string { } func uiSetVar(name string, value string) { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -240,10 +192,6 @@ func uiSetVar(name string, value string) { } func uiGetVar(name string) string { - if cgoDisabled { - return "" - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -254,10 +202,6 @@ func uiGetVar(name string) string { } func uiSwitchToScreen(screen string) { - if cgoDisabled { - return - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -267,10 +211,6 @@ func uiSwitchToScreen(screen string) { } func uiGetCurrentScreen() string { - if cgoDisabled { - return "" - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -279,10 +219,6 @@ func uiGetCurrentScreen() string { } func uiObjAddState(objName string, state string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -295,10 +231,6 @@ func uiObjAddState(objName string, state string) (bool, error) { } func uiObjClearState(objName string, state string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -311,10 +243,6 @@ func uiObjClearState(objName string, state string) (bool, error) { } func uiGetLVGLVersion() string { - if cgoDisabled { - return "" - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -323,10 +251,6 @@ func uiGetLVGLVersion() string { // TODO: use Enum instead of string but it's not a hot path and performance is not a concern now func uiObjAddFlag(objName string, flag string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -339,10 +263,6 @@ func uiObjAddFlag(objName string, flag string) (bool, error) { } func uiObjClearFlag(objName string, flag string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -363,10 +283,6 @@ func uiObjShow(objName string) (bool, error) { } func uiObjSetOpacity(objName string, opacity int) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -378,10 +294,6 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) { } func uiObjFadeIn(objName string, duration uint32) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -394,10 +306,6 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) { } func uiObjFadeOut(objName string, duration uint32) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -410,10 +318,6 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) { } func uiLabelSetText(objName string, text string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -431,10 +335,6 @@ func uiLabelSetText(objName string, text string) (bool, error) { } func uiImgSetSrc(objName string, src string) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -450,10 +350,6 @@ func uiImgSetSrc(objName string, src string) (bool, error) { } func uiDispSetRotation(rotation uint16) (bool, error) { - if cgoDisabled { - return false, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -466,10 +362,6 @@ func uiDispSetRotation(rotation uint16) (bool, error) { } func videoGetStreamQualityFactor() (float64, error) { - if cgoDisabled { - return 0, nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -478,10 +370,6 @@ func videoGetStreamQualityFactor() (float64, error) { } func videoSetStreamQualityFactor(factor float64) error { - if cgoDisabled { - return nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -490,10 +378,6 @@ func videoSetStreamQualityFactor(factor float64) error { } func videoGetEDID() (string, error) { - if cgoDisabled { - return "", nil - } - cgoLock.Lock() defer cgoLock.Unlock() @@ -502,10 +386,6 @@ func videoGetEDID() (string, error) { } func videoSetEDID(edid string) error { - if cgoDisabled { - return nil - } - cgoLock.Lock() defer cgoLock.Unlock() diff --git a/internal/native/empty.go b/internal/native/empty.go new file mode 100644 index 00000000..846cf9ae --- /dev/null +++ b/internal/native/empty.go @@ -0,0 +1,111 @@ +package native + +type EmptyNativeInterface struct { +} + +func (e *EmptyNativeInterface) Start() error { return nil } + +func (e *EmptyNativeInterface) VideoSetSleepMode(enabled bool) error { return nil } + +func (e *EmptyNativeInterface) VideoGetSleepMode() (bool, error) { return false, nil } + +func (e *EmptyNativeInterface) VideoSleepModeSupported() bool { + return false +} + +func (e *EmptyNativeInterface) VideoSetQualityFactor(factor float64) error { + return nil +} + +func (e *EmptyNativeInterface) VideoGetQualityFactor() (float64, error) { + return 0, nil +} + +func (e *EmptyNativeInterface) VideoSetEDID(edid string) error { + return nil +} + +func (e *EmptyNativeInterface) VideoGetEDID() (string, error) { + return "", nil +} + +func (e *EmptyNativeInterface) VideoLogStatus() (string, error) { + return "", nil +} + +func (e *EmptyNativeInterface) VideoStop() error { + return nil +} + +func (e *EmptyNativeInterface) VideoStart() error { + return nil +} + +func (e *EmptyNativeInterface) GetLVGLVersion() (string, error) { + return "", nil +} + +func (e *EmptyNativeInterface) UIObjHide(objName string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjShow(objName string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UISetVar(name string, value string) { +} + +func (e *EmptyNativeInterface) UIGetVar(name string) string { + return "" +} + +func (e *EmptyNativeInterface) UIObjAddState(objName string, state string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjClearState(objName string, state string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjAddFlag(objName string, flag string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjClearFlag(objName string, flag string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjSetOpacity(objName string, opacity int) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjFadeIn(objName string, duration uint32) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjFadeOut(objName string, duration uint32) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjSetLabelText(objName string, text string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UIObjSetImageSrc(objName string, image string) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) DisplaySetRotation(rotation uint16) (bool, error) { + return false, nil +} + +func (e *EmptyNativeInterface) UpdateLabelIfChanged(objName string, newText string) {} + +func (e *EmptyNativeInterface) UpdateLabelAndChangeVisibility(objName string, newText string) {} + +func (e *EmptyNativeInterface) SwitchToScreenIf(screenName string, shouldSwitch []string) {} + +func (e *EmptyNativeInterface) SwitchToScreenIfDifferent(screenName string) {} + +func (e *EmptyNativeInterface) DoNotUseThisIsForCrashTestingOnly() {} diff --git a/internal/native/grpc_client.go b/internal/native/grpc_client.go new file mode 100644 index 00000000..85a3201b --- /dev/null +++ b/internal/native/grpc_client.go @@ -0,0 +1,272 @@ +package native + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + "time" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + + pb "github.com/jetkvm/kvm/internal/native/proto" +) + +// GRPCClient wraps the gRPC client for the native service +type GRPCClient struct { + ctx context.Context + cancel context.CancelFunc + + conn *grpc.ClientConn + client pb.NativeServiceClient + logger *zerolog.Logger + + eventStream pb.NativeService_StreamEventsClient + eventM sync.RWMutex + eventCh chan *pb.Event + eventDone chan struct{} + + onVideoStateChange func(state VideoState) + onIndevEvent func(event string) + onRpcEvent func(event string) + + closed bool + closeM sync.Mutex +} + +type grpcClientOptions struct { + SocketPath string + Logger *zerolog.Logger + OnVideoStateChange func(state VideoState) + OnIndevEvent func(event string) + OnRpcEvent func(event string) +} + +// NewGRPCClient creates a new gRPC client connected to the native service +func NewGRPCClient(opts grpcClientOptions) (*GRPCClient, error) { + // Connect to the Unix domain socket + conn, err := grpc.NewClient( + fmt.Sprintf("unix-abstract:%v", opts.SocketPath), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to gRPC server: %w", err) + } + + client := pb.NewNativeServiceClient(conn) + + ctx, cancel := context.WithCancel(context.Background()) + + grpcClient := &GRPCClient{ + ctx: ctx, + cancel: cancel, + conn: conn, + client: client, + logger: opts.Logger, + eventCh: make(chan *pb.Event, 100), + eventDone: make(chan struct{}), + onVideoStateChange: opts.OnVideoStateChange, + onIndevEvent: opts.OnIndevEvent, + onRpcEvent: opts.OnRpcEvent, + } + + // Start event stream + go grpcClient.startEventStream() + + // Start event handler to process events from the channel + go func() { + for { + select { + case event := <-grpcClient.eventCh: + grpcClient.handleEvent(event) + case <-grpcClient.eventDone: + return + } + } + }() + + return grpcClient, nil +} + +func (c *GRPCClient) handleEventStream(stream pb.NativeService_StreamEventsClient) { + c.eventM.Lock() + c.eventStream = stream + defer func() { + c.eventStream = nil + c.eventM.Unlock() + }() + + for { + logger := c.logger.With().Interface("stream", stream).Logger() + if stream == nil { + logger.Error().Msg("event stream is nil") + break + } + + event, err := stream.Recv() + + if err != nil { + if errors.Is(err, io.EOF) { + logger.Debug().Msg("event stream closed") + } else { + logger.Warn().Err(err).Msg("event stream error") + } + break + } + + // enrich the logger with the event type and data, if debug mode is enabled + if c.logger.GetLevel() <= zerolog.DebugLevel { + logger = logger.With(). + Str("type", event.Type). + Interface("data", event.Data). + Logger() + } + logger.Trace().Msg("received event") + + select { + case c.eventCh <- event: + default: + logger.Warn().Msg("event channel full, dropping event") + } + } +} + +func (c *GRPCClient) startEventStream() { + for { + // check if the client is closed + c.closeM.Lock() + if c.closed { + c.closeM.Unlock() + return + } + c.closeM.Unlock() + + // check if the context is done + select { + case <-c.ctx.Done(): + c.logger.Info().Msg("event stream context done, closing") + return + default: + } + + stream, err := c.client.StreamEvents(c.ctx, &pb.Empty{}) + if err != nil { + c.logger.Warn().Err(err).Msg("failed to start event stream, retrying ...") + time.Sleep(5 * time.Second) + continue + } + + c.handleEventStream(stream) + + // Wait before retrying + time.Sleep(1 * time.Second) + } +} + +func (c *GRPCClient) checkIsReady(ctx context.Context) error { + c.logger.Trace().Msg("connection is idle, connecting ...") + + resp, err := c.client.IsReady(ctx, &pb.IsReadyRequest{}) + if err != nil { + if errors.Is(err, status.Error(codes.Unavailable, "")) { + return fmt.Errorf("timeout waiting for ready: %w", err) + } + return fmt.Errorf("failed to check if ready: %w", err) + } + if resp.Ready { + return nil + } + return nil +} + +// WaitReady waits for the gRPC connection to be ready +func (c *GRPCClient) WaitReady() error { + ctx, cancel := context.WithTimeout(c.ctx, 60*time.Second) + defer cancel() + + prevState := connectivity.Idle + for { + state := c.conn.GetState() + c.logger. + With(). + Str("state", state.String()). + Int("prev_state", int(prevState)). + Logger() + + prevState = state + if state == connectivity.Idle || state == connectivity.Ready { + if err := c.checkIsReady(ctx); err != nil { + time.Sleep(1 * time.Second) + continue + } + } + + c.logger.Info().Msg("waiting for connection to be ready") + + if state == connectivity.Ready { + return nil + } + if state == connectivity.Shutdown { + return fmt.Errorf("connection failed: %v", state) + } + + if !c.conn.WaitForStateChange(ctx, state) { + return ctx.Err() + } + } +} + +func (c *GRPCClient) handleEvent(event *pb.Event) { + switch event.Type { + case "video_state_change": + state := event.GetVideoState() + if state == nil { + c.logger.Warn().Msg("video state event is nil") + return + } + c.onVideoStateChange(VideoState{ + Ready: state.Ready, + Error: state.Error, + Width: int(state.Width), + Height: int(state.Height), + FramePerSecond: state.FramePerSecond, + }) + case "indev_event": + c.onIndevEvent(event.GetIndevEvent()) + case "rpc_event": + c.onRpcEvent(event.GetRpcEvent()) + default: + c.logger.Warn().Str("type", event.Type).Msg("unknown event type") + } +} + +// Close closes the gRPC client +func (c *GRPCClient) Close() error { + c.closeM.Lock() + defer c.closeM.Unlock() + if c.closed { + return nil + } + c.closed = true + + // cancel all ongoing operations + c.cancel() + + close(c.eventDone) + + c.eventM.Lock() + if c.eventStream != nil { + if err := c.eventStream.CloseSend(); err != nil { + c.logger.Warn().Err(err).Msg("failed to close event stream") + } + } + c.eventM.Unlock() + + return c.conn.Close() +} diff --git a/internal/native/grpc_clientmethods.go b/internal/native/grpc_clientmethods.go new file mode 100644 index 00000000..c9a83adc --- /dev/null +++ b/internal/native/grpc_clientmethods.go @@ -0,0 +1,212 @@ +package native + +import ( + "context" + + pb "github.com/jetkvm/kvm/internal/native/proto" +) + +// Below are generated methods, do not edit manually + +// Video methods +func (c *GRPCClient) VideoSetSleepMode(enabled bool) error { + _, err := c.client.VideoSetSleepMode(context.Background(), &pb.VideoSetSleepModeRequest{Enabled: enabled}) + return err +} + +func (c *GRPCClient) VideoGetSleepMode() (bool, error) { + resp, err := c.client.VideoGetSleepMode(context.Background(), &pb.Empty{}) + if err != nil { + return false, err + } + return resp.Enabled, nil +} + +func (c *GRPCClient) VideoSleepModeSupported() bool { + resp, err := c.client.VideoSleepModeSupported(context.Background(), &pb.Empty{}) + if err != nil { + return false + } + return resp.Supported +} + +func (c *GRPCClient) VideoSetQualityFactor(factor float64) error { + _, err := c.client.VideoSetQualityFactor(context.Background(), &pb.VideoSetQualityFactorRequest{Factor: factor}) + return err +} + +func (c *GRPCClient) VideoGetQualityFactor() (float64, error) { + resp, err := c.client.VideoGetQualityFactor(context.Background(), &pb.Empty{}) + if err != nil { + return 0, err + } + return resp.Factor, nil +} + +func (c *GRPCClient) VideoSetEDID(edid string) error { + _, err := c.client.VideoSetEDID(context.Background(), &pb.VideoSetEDIDRequest{Edid: edid}) + return err +} + +func (c *GRPCClient) VideoGetEDID() (string, error) { + resp, err := c.client.VideoGetEDID(context.Background(), &pb.Empty{}) + if err != nil { + return "", err + } + return resp.Edid, nil +} + +func (c *GRPCClient) VideoLogStatus() (string, error) { + resp, err := c.client.VideoLogStatus(context.Background(), &pb.Empty{}) + if err != nil { + return "", err + } + return resp.Status, nil +} + +func (c *GRPCClient) VideoStop() error { + _, err := c.client.VideoStop(context.Background(), &pb.Empty{}) + return err +} + +func (c *GRPCClient) VideoStart() error { + _, err := c.client.VideoStart(context.Background(), &pb.Empty{}) + return err +} + +// UI methods +func (c *GRPCClient) GetLVGLVersion() (string, error) { + resp, err := c.client.GetLVGLVersion(context.Background(), &pb.Empty{}) + if err != nil { + return "", err + } + return resp.Version, nil +} + +func (c *GRPCClient) UIObjHide(objName string) (bool, error) { + resp, err := c.client.UIObjHide(context.Background(), &pb.UIObjHideRequest{ObjName: objName}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjShow(objName string) (bool, error) { + resp, err := c.client.UIObjShow(context.Background(), &pb.UIObjShowRequest{ObjName: objName}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UISetVar(name string, value string) { + _, _ = c.client.UISetVar(context.Background(), &pb.UISetVarRequest{Name: name, Value: value}) +} + +func (c *GRPCClient) UIGetVar(name string) string { + resp, err := c.client.UIGetVar(context.Background(), &pb.UIGetVarRequest{Name: name}) + if err != nil { + return "" + } + return resp.Value +} + +func (c *GRPCClient) UIObjAddState(objName string, state string) (bool, error) { + resp, err := c.client.UIObjAddState(context.Background(), &pb.UIObjAddStateRequest{ObjName: objName, State: state}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjClearState(objName string, state string) (bool, error) { + resp, err := c.client.UIObjClearState(context.Background(), &pb.UIObjClearStateRequest{ObjName: objName, State: state}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjAddFlag(objName string, flag string) (bool, error) { + resp, err := c.client.UIObjAddFlag(context.Background(), &pb.UIObjAddFlagRequest{ObjName: objName, Flag: flag}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjClearFlag(objName string, flag string) (bool, error) { + resp, err := c.client.UIObjClearFlag(context.Background(), &pb.UIObjClearFlagRequest{ObjName: objName, Flag: flag}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjSetOpacity(objName string, opacity int) (bool, error) { + resp, err := c.client.UIObjSetOpacity(context.Background(), &pb.UIObjSetOpacityRequest{ObjName: objName, Opacity: int32(opacity)}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjFadeIn(objName string, duration uint32) (bool, error) { + resp, err := c.client.UIObjFadeIn(context.Background(), &pb.UIObjFadeInRequest{ObjName: objName, Duration: duration}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjFadeOut(objName string, duration uint32) (bool, error) { + resp, err := c.client.UIObjFadeOut(context.Background(), &pb.UIObjFadeOutRequest{ObjName: objName, Duration: duration}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjSetLabelText(objName string, text string) (bool, error) { + resp, err := c.client.UIObjSetLabelText(context.Background(), &pb.UIObjSetLabelTextRequest{ObjName: objName, Text: text}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UIObjSetImageSrc(objName string, image string) (bool, error) { + resp, err := c.client.UIObjSetImageSrc(context.Background(), &pb.UIObjSetImageSrcRequest{ObjName: objName, Image: image}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) DisplaySetRotation(rotation uint16) (bool, error) { + resp, err := c.client.DisplaySetRotation(context.Background(), &pb.DisplaySetRotationRequest{Rotation: uint32(rotation)}) + if err != nil { + return false, err + } + return resp.Success, nil +} + +func (c *GRPCClient) UpdateLabelIfChanged(objName string, newText string) { + _, _ = c.client.UpdateLabelIfChanged(context.Background(), &pb.UpdateLabelIfChangedRequest{ObjName: objName, NewText: newText}) +} + +func (c *GRPCClient) UpdateLabelAndChangeVisibility(objName string, newText string) { + _, _ = c.client.UpdateLabelAndChangeVisibility(context.Background(), &pb.UpdateLabelAndChangeVisibilityRequest{ObjName: objName, NewText: newText}) +} + +func (c *GRPCClient) SwitchToScreenIf(screenName string, shouldSwitch []string) { + _, _ = c.client.SwitchToScreenIf(context.Background(), &pb.SwitchToScreenIfRequest{ScreenName: screenName, ShouldSwitch: shouldSwitch}) +} + +func (c *GRPCClient) SwitchToScreenIfDifferent(screenName string) { + _, _ = c.client.SwitchToScreenIfDifferent(context.Background(), &pb.SwitchToScreenIfDifferentRequest{ScreenName: screenName}) +} + +func (c *GRPCClient) DoNotUseThisIsForCrashTestingOnly() { + _, _ = c.client.DoNotUseThisIsForCrashTestingOnly(context.Background(), &pb.Empty{}) +} diff --git a/internal/native/grpc_server.go b/internal/native/grpc_server.go new file mode 100644 index 00000000..9b54fb5b --- /dev/null +++ b/internal/native/grpc_server.go @@ -0,0 +1,164 @@ +package native + +import ( + "context" + "fmt" + "net" + "sync" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + + pb "github.com/jetkvm/kvm/internal/native/proto" +) + +// grpcServer wraps the Native instance and implements the gRPC service +type grpcServer struct { + pb.UnimplementedNativeServiceServer + native *Native + logger *zerolog.Logger + eventStreamChan chan *pb.Event + eventStreamMu sync.Mutex + eventStreamCtx context.Context + eventStreamCancel context.CancelFunc +} + +// NewGRPCServer creates a new gRPC server for the native service +func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer { + s := &grpcServer{ + native: n, + logger: logger, + eventStreamChan: make(chan *pb.Event, 100), + } + + // Store original callbacks and wrap them to also broadcast events + originalVideoStateChange := n.onVideoStateChange + originalIndevEvent := n.onIndevEvent + originalRpcEvent := n.onRpcEvent + + // Wrap callbacks to both call original and broadcast events + n.onVideoStateChange = func(state VideoState) { + if originalVideoStateChange != nil { + originalVideoStateChange(state) + } + event := &pb.Event{ + Type: "video_state_change", + Data: &pb.Event_VideoState{ + VideoState: &pb.VideoState{ + Ready: state.Ready, + Error: state.Error, + Width: int32(state.Width), + Height: int32(state.Height), + FramePerSecond: state.FramePerSecond, + }, + }, + } + s.broadcastEvent(event) + } + + n.onIndevEvent = func(event string) { + if originalIndevEvent != nil { + originalIndevEvent(event) + } + s.broadcastEvent(&pb.Event{ + Type: "indev_event", + Data: &pb.Event_IndevEvent{ + IndevEvent: event, + }, + }) + } + + n.onRpcEvent = func(event string) { + if originalRpcEvent != nil { + originalRpcEvent(event) + } + s.broadcastEvent(&pb.Event{ + Type: "rpc_event", + Data: &pb.Event_RpcEvent{ + RpcEvent: event, + }, + }) + } + + return s +} + +func (s *grpcServer) broadcastEvent(event *pb.Event) { + s.eventStreamChan <- event +} + +func (s *grpcServer) IsReady(ctx context.Context, req *pb.IsReadyRequest) (*pb.IsReadyResponse, error) { + return &pb.IsReadyResponse{Ready: true, VideoReady: true}, nil +} + +// StreamEvents streams events from the native process +func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamEventsServer) error { + setProcTitle("connected") + defer setProcTitle("waiting") + + // Cancel previous stream if exists + s.eventStreamMu.Lock() + if s.eventStreamCancel != nil { + s.logger.Debug().Msg("cancelling previous StreamEvents call") + s.eventStreamCancel() + } + + // Create a cancellable context for this stream + ctx, cancel := context.WithCancel(stream.Context()) + s.eventStreamCtx = ctx + s.eventStreamCancel = cancel + s.eventStreamMu.Unlock() + + // Clean up when this stream ends + defer func() { + s.eventStreamMu.Lock() + defer s.eventStreamMu.Unlock() + if s.eventStreamCtx == ctx { + s.eventStreamCancel = nil + s.eventStreamCtx = nil + } + cancel() + }() + + // Stream events + for { + select { + case event := <-s.eventStreamChan: + // Check if this stream is still the active one + s.eventStreamMu.Lock() + isActive := s.eventStreamCtx == ctx + s.eventStreamMu.Unlock() + + if !isActive { + s.logger.Debug().Msg("stream replaced by new call, exiting") + return context.Canceled + } + + if err := stream.Send(event); err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// StartGRPCServer starts the gRPC server on a Unix domain socket +func StartGRPCServer(server *grpcServer, socketPath string, logger *zerolog.Logger) (*grpc.Server, net.Listener, error) { + lis, err := net.Listen("unix", socketPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to listen on socket: %w", err) + } + + s := grpc.NewServer() + pb.RegisterNativeServiceServer(s, server) + + go func() { + if err := s.Serve(lis); err != nil { + logger.Error().Err(err).Msg("gRPC server error") + } + }() + + logger.Info().Str("socket", socketPath).Msg("gRPC server started") + return s, lis, nil +} diff --git a/internal/native/grpc_servermethods.go b/internal/native/grpc_servermethods.go new file mode 100644 index 00000000..cc16dfd1 --- /dev/null +++ b/internal/native/grpc_servermethods.go @@ -0,0 +1,230 @@ +package native + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/jetkvm/kvm/internal/native/proto" +) + +// Below are generated methods, do not edit manually + +// Video methods +func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) { + if err := s.native.VideoSetSleepMode(req.Enabled); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.Empty{}, nil +} + +func (s *grpcServer) VideoGetSleepMode(ctx context.Context, req *pb.Empty) (*pb.VideoGetSleepModeResponse, error) { + enabled, err := s.native.VideoGetSleepMode() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.VideoGetSleepModeResponse{Enabled: enabled}, nil +} + +func (s *grpcServer) VideoSleepModeSupported(ctx context.Context, req *pb.Empty) (*pb.VideoSleepModeSupportedResponse, error) { + return &pb.VideoSleepModeSupportedResponse{Supported: s.native.VideoSleepModeSupported()}, nil +} + +func (s *grpcServer) VideoSetQualityFactor(ctx context.Context, req *pb.VideoSetQualityFactorRequest) (*pb.Empty, error) { + if err := s.native.VideoSetQualityFactor(req.Factor); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.Empty{}, nil +} + +func (s *grpcServer) VideoGetQualityFactor(ctx context.Context, req *pb.Empty) (*pb.VideoGetQualityFactorResponse, error) { + factor, err := s.native.VideoGetQualityFactor() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.VideoGetQualityFactorResponse{Factor: factor}, nil +} + +func (s *grpcServer) VideoSetEDID(ctx context.Context, req *pb.VideoSetEDIDRequest) (*pb.Empty, error) { + if err := s.native.VideoSetEDID(req.Edid); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.Empty{}, nil +} + +func (s *grpcServer) VideoGetEDID(ctx context.Context, req *pb.Empty) (*pb.VideoGetEDIDResponse, error) { + edid, err := s.native.VideoGetEDID() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.VideoGetEDIDResponse{Edid: edid}, nil +} + +func (s *grpcServer) VideoLogStatus(ctx context.Context, req *pb.Empty) (*pb.VideoLogStatusResponse, error) { + logStatus, err := s.native.VideoLogStatus() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.VideoLogStatusResponse{Status: logStatus}, nil +} + +func (s *grpcServer) VideoStop(ctx context.Context, req *pb.Empty) (*pb.Empty, error) { + procPrefix = "jetkvm: [native]" + setProcTitle(lastProcTitle) + + if err := s.native.VideoStop(); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.Empty{}, nil +} + +func (s *grpcServer) VideoStart(ctx context.Context, req *pb.Empty) (*pb.Empty, error) { + procPrefix = "jetkvm: [native+video]" + setProcTitle(lastProcTitle) + + if err := s.native.VideoStart(); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.Empty{}, nil +} + +// UI methods +func (s *grpcServer) GetLVGLVersion(ctx context.Context, req *pb.Empty) (*pb.GetLVGLVersionResponse, error) { + version, err := s.native.GetLVGLVersion() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.GetLVGLVersionResponse{Version: version}, nil +} + +func (s *grpcServer) UIObjHide(ctx context.Context, req *pb.UIObjHideRequest) (*pb.UIObjHideResponse, error) { + success, err := s.native.UIObjHide(req.ObjName) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjHideResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjShow(ctx context.Context, req *pb.UIObjShowRequest) (*pb.UIObjShowResponse, error) { + success, err := s.native.UIObjShow(req.ObjName) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjShowResponse{Success: success}, nil +} + +func (s *grpcServer) UISetVar(ctx context.Context, req *pb.UISetVarRequest) (*pb.Empty, error) { + s.native.UISetVar(req.Name, req.Value) + return &pb.Empty{}, nil +} + +func (s *grpcServer) UIGetVar(ctx context.Context, req *pb.UIGetVarRequest) (*pb.UIGetVarResponse, error) { + value := s.native.UIGetVar(req.Name) + return &pb.UIGetVarResponse{Value: value}, nil +} + +func (s *grpcServer) UIObjAddState(ctx context.Context, req *pb.UIObjAddStateRequest) (*pb.UIObjAddStateResponse, error) { + success, err := s.native.UIObjAddState(req.ObjName, req.State) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjAddStateResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjClearState(ctx context.Context, req *pb.UIObjClearStateRequest) (*pb.UIObjClearStateResponse, error) { + success, err := s.native.UIObjClearState(req.ObjName, req.State) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjClearStateResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjAddFlag(ctx context.Context, req *pb.UIObjAddFlagRequest) (*pb.UIObjAddFlagResponse, error) { + success, err := s.native.UIObjAddFlag(req.ObjName, req.Flag) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjAddFlagResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjClearFlag(ctx context.Context, req *pb.UIObjClearFlagRequest) (*pb.UIObjClearFlagResponse, error) { + success, err := s.native.UIObjClearFlag(req.ObjName, req.Flag) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjClearFlagResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjSetOpacity(ctx context.Context, req *pb.UIObjSetOpacityRequest) (*pb.UIObjSetOpacityResponse, error) { + success, err := s.native.UIObjSetOpacity(req.ObjName, int(req.Opacity)) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjSetOpacityResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjFadeIn(ctx context.Context, req *pb.UIObjFadeInRequest) (*pb.UIObjFadeInResponse, error) { + success, err := s.native.UIObjFadeIn(req.ObjName, req.Duration) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjFadeInResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjFadeOut(ctx context.Context, req *pb.UIObjFadeOutRequest) (*pb.UIObjFadeOutResponse, error) { + success, err := s.native.UIObjFadeOut(req.ObjName, req.Duration) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjFadeOutResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjSetLabelText(ctx context.Context, req *pb.UIObjSetLabelTextRequest) (*pb.UIObjSetLabelTextResponse, error) { + success, err := s.native.UIObjSetLabelText(req.ObjName, req.Text) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjSetLabelTextResponse{Success: success}, nil +} + +func (s *grpcServer) UIObjSetImageSrc(ctx context.Context, req *pb.UIObjSetImageSrcRequest) (*pb.UIObjSetImageSrcResponse, error) { + success, err := s.native.UIObjSetImageSrc(req.ObjName, req.Image) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.UIObjSetImageSrcResponse{Success: success}, nil +} + +func (s *grpcServer) DisplaySetRotation(ctx context.Context, req *pb.DisplaySetRotationRequest) (*pb.DisplaySetRotationResponse, error) { + success, err := s.native.DisplaySetRotation(uint16(req.Rotation)) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &pb.DisplaySetRotationResponse{Success: success}, nil +} + +func (s *grpcServer) UpdateLabelIfChanged(ctx context.Context, req *pb.UpdateLabelIfChangedRequest) (*pb.Empty, error) { + s.native.UpdateLabelIfChanged(req.ObjName, req.NewText) + return &pb.Empty{}, nil +} + +func (s *grpcServer) UpdateLabelAndChangeVisibility(ctx context.Context, req *pb.UpdateLabelAndChangeVisibilityRequest) (*pb.Empty, error) { + s.native.UpdateLabelAndChangeVisibility(req.ObjName, req.NewText) + return &pb.Empty{}, nil +} + +func (s *grpcServer) SwitchToScreenIf(ctx context.Context, req *pb.SwitchToScreenIfRequest) (*pb.Empty, error) { + s.native.SwitchToScreenIf(req.ScreenName, req.ShouldSwitch) + return &pb.Empty{}, nil +} + +func (s *grpcServer) SwitchToScreenIfDifferent(ctx context.Context, req *pb.SwitchToScreenIfDifferentRequest) (*pb.Empty, error) { + s.native.SwitchToScreenIfDifferent(req.ScreenName) + return &pb.Empty{}, nil +} + +func (s *grpcServer) DoNotUseThisIsForCrashTestingOnly(ctx context.Context, req *pb.Empty) (*pb.Empty, error) { + s.native.DoNotUseThisIsForCrashTestingOnly() + return &pb.Empty{}, nil +} diff --git a/internal/native/interface.go b/internal/native/interface.go new file mode 100644 index 00000000..9974399d --- /dev/null +++ b/internal/native/interface.go @@ -0,0 +1,36 @@ +package native + +// NativeInterface defines the interface that both Native and NativeProxy implement +type NativeInterface interface { + Start() error + VideoSetSleepMode(enabled bool) error + VideoGetSleepMode() (bool, error) + VideoSleepModeSupported() bool + VideoSetQualityFactor(factor float64) error + VideoGetQualityFactor() (float64, error) + VideoSetEDID(edid string) error + VideoGetEDID() (string, error) + VideoLogStatus() (string, error) + VideoStop() error + VideoStart() error + GetLVGLVersion() (string, error) + UIObjHide(objName string) (bool, error) + UIObjShow(objName string) (bool, error) + UISetVar(name string, value string) + UIGetVar(name string) string + UIObjAddState(objName string, state string) (bool, error) + UIObjClearState(objName string, state string) (bool, error) + UIObjAddFlag(objName string, flag string) (bool, error) + UIObjClearFlag(objName string, flag string) (bool, error) + UIObjSetOpacity(objName string, opacity int) (bool, error) + UIObjFadeIn(objName string, duration uint32) (bool, error) + UIObjFadeOut(objName string, duration uint32) (bool, error) + UIObjSetLabelText(objName string, text string) (bool, error) + UIObjSetImageSrc(objName string, image string) (bool, error) + DisplaySetRotation(rotation uint16) (bool, error) + UpdateLabelIfChanged(objName string, newText string) + UpdateLabelAndChangeVisibility(objName string, newText string) + SwitchToScreenIf(screenName string, shouldSwitch []string) + SwitchToScreenIfDifferent(screenName string) + DoNotUseThisIsForCrashTestingOnly() +} diff --git a/internal/native/native.go b/internal/native/native.go index 0d21083b..61c4b0ac 100644 --- a/internal/native/native.go +++ b/internal/native/native.go @@ -1,6 +1,7 @@ package native import ( + "os" "sync" "time" @@ -9,7 +10,6 @@ import ( ) type Native struct { - disable bool ready chan struct{} l *zerolog.Logger lD *zerolog.Logger @@ -28,18 +28,23 @@ type Native struct { } type NativeOptions struct { - Disable bool SystemVersion *semver.Version AppVersion *semver.Version DisplayRotation uint16 DefaultQualityFactor float64 + MaxRestartAttempts uint OnVideoStateChange func(state VideoState) OnVideoFrameReceived func(frame []byte, duration time.Duration) OnIndevEvent func(event string) OnRpcEvent func(event string) + OnNativeRestart func() } func NewNative(opts NativeOptions) *Native { + pid := os.Getpid() + nativeSubLogger := nativeLogger.With().Int("pid", pid).Str("scope", "native").Logger() + displaySubLogger := displayLogger.With().Int("pid", pid).Str("scope", "native").Logger() + onVideoStateChange := opts.OnVideoStateChange if onVideoStateChange == nil { onVideoStateChange = func(state VideoState) { @@ -50,7 +55,7 @@ func NewNative(opts NativeOptions) *Native { onVideoFrameReceived := opts.OnVideoFrameReceived if onVideoFrameReceived == nil { onVideoFrameReceived = func(frame []byte, duration time.Duration) { - nativeLogger.Info().Interface("frame", frame).Dur("duration", duration).Msg("video frame received") + nativeLogger.Trace().Interface("frame", frame).Dur("duration", duration).Msg("video frame received") } } @@ -76,10 +81,9 @@ func NewNative(opts NativeOptions) *Native { } return &Native{ - disable: opts.Disable, ready: make(chan struct{}), - l: nativeLogger, - lD: displayLogger, + l: &nativeSubLogger, + lD: &displaySubLogger, systemVersion: opts.SystemVersion, appVersion: opts.AppVersion, displayRotation: opts.DisplayRotation, @@ -94,21 +98,11 @@ func NewNative(opts NativeOptions) *Native { } } -func (n *Native) Start(initialEDID string) { - if n.disable { - nativeLogger.Warn().Msg("native is disabled, skipping initialization") - setCgoDisabled(true) - return - } - +func (n *Native) Start() error { // set up singleton setInstance(n) setUpNativeHandlers() - if err := videoSetEDID(initialEDID); err != nil { - n.l.Warn().Err(err).Msg("failed to set EDID before video init") - } - // start the native video go n.handleLogChan() go n.handleVideoStateChan() @@ -121,9 +115,11 @@ func (n *Native) Start(initialEDID string) { if err := videoInit(n.defaultQualityFactor); err != nil { n.l.Error().Err(err).Msg("failed to initialize video") + return err } close(n.ready) + return nil } // DoNotUseThisIsForCrashTestingOnly diff --git a/internal/native/proto/README.md b/internal/native/proto/README.md new file mode 100644 index 00000000..aa619f6d --- /dev/null +++ b/internal/native/proto/README.md @@ -0,0 +1,33 @@ +# Proto Files + +This directory contains the Protocol Buffer definitions for the native service. + +## Generating Code + +To generate the Go code from the proto files, run: + +```bash +./scripts/generate_proto.sh +``` + +Or manually: + +```bash +protoc \ + --go_out=. \ + --go_opt=paths=source_relative \ + --go-grpc_out=. \ + --go-grpc_opt=paths=source_relative \ + internal/native/proto/native.proto +``` + +## Prerequisites + +- `protoc` - Protocol Buffer compiler +- `protoc-gen-go` - Go plugin for protoc (install with: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`) +- `protoc-gen-go-grpc` - gRPC Go plugin for protoc (install with: `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest`) + +## Note + +The current `native.pb.go` and `native_grpc.pb.go` files are placeholder/stub files. They should be regenerated from `native.proto` using the commands above. + diff --git a/internal/native/proto/native.pb.go b/internal/native/proto/native.pb.go new file mode 100644 index 00000000..7eb5fdcc --- /dev/null +++ b/internal/native/proto/native.pb.go @@ -0,0 +1,2775 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.21.12 +// source: internal/native/proto/native.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Messages +type Empty struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Empty) Reset() { + *x = Empty{} + mi := &file_internal_native_proto_native_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{0} +} + +type IsReadyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IsReadyRequest) Reset() { + *x = IsReadyRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IsReadyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsReadyRequest) ProtoMessage() {} + +func (x *IsReadyRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsReadyRequest.ProtoReflect.Descriptor instead. +func (*IsReadyRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{1} +} + +type IsReadyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + VideoReady bool `protobuf:"varint,3,opt,name=video_ready,json=videoReady,proto3" json:"video_ready,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IsReadyResponse) Reset() { + *x = IsReadyResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IsReadyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsReadyResponse) ProtoMessage() {} + +func (x *IsReadyResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsReadyResponse.ProtoReflect.Descriptor instead. +func (*IsReadyResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{2} +} + +func (x *IsReadyResponse) GetReady() bool { + if x != nil { + return x.Ready + } + return false +} + +func (x *IsReadyResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *IsReadyResponse) GetVideoReady() bool { + if x != nil { + return x.VideoReady + } + return false +} + +type VideoState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + Width int32 `protobuf:"varint,3,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"` + FramePerSecond float64 `protobuf:"fixed64,5,opt,name=frame_per_second,json=framePerSecond,proto3" json:"frame_per_second,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoState) Reset() { + *x = VideoState{} + mi := &file_internal_native_proto_native_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoState) ProtoMessage() {} + +func (x *VideoState) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoState.ProtoReflect.Descriptor instead. +func (*VideoState) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{3} +} + +func (x *VideoState) GetReady() bool { + if x != nil { + return x.Ready + } + return false +} + +func (x *VideoState) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *VideoState) GetWidth() int32 { + if x != nil { + return x.Width + } + return 0 +} + +func (x *VideoState) GetHeight() int32 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *VideoState) GetFramePerSecond() float64 { + if x != nil { + return x.FramePerSecond + } + return 0 +} + +type VideoSetSleepModeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoSetSleepModeRequest) Reset() { + *x = VideoSetSleepModeRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoSetSleepModeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoSetSleepModeRequest) ProtoMessage() {} + +func (x *VideoSetSleepModeRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoSetSleepModeRequest.ProtoReflect.Descriptor instead. +func (*VideoSetSleepModeRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{4} +} + +func (x *VideoSetSleepModeRequest) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type VideoGetSleepModeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoGetSleepModeResponse) Reset() { + *x = VideoGetSleepModeResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoGetSleepModeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoGetSleepModeResponse) ProtoMessage() {} + +func (x *VideoGetSleepModeResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoGetSleepModeResponse.ProtoReflect.Descriptor instead. +func (*VideoGetSleepModeResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{5} +} + +func (x *VideoGetSleepModeResponse) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type VideoSleepModeSupportedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoSleepModeSupportedResponse) Reset() { + *x = VideoSleepModeSupportedResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoSleepModeSupportedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoSleepModeSupportedResponse) ProtoMessage() {} + +func (x *VideoSleepModeSupportedResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoSleepModeSupportedResponse.ProtoReflect.Descriptor instead. +func (*VideoSleepModeSupportedResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{6} +} + +func (x *VideoSleepModeSupportedResponse) GetSupported() bool { + if x != nil { + return x.Supported + } + return false +} + +type VideoSetQualityFactorRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Factor float64 `protobuf:"fixed64,1,opt,name=factor,proto3" json:"factor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoSetQualityFactorRequest) Reset() { + *x = VideoSetQualityFactorRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoSetQualityFactorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoSetQualityFactorRequest) ProtoMessage() {} + +func (x *VideoSetQualityFactorRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoSetQualityFactorRequest.ProtoReflect.Descriptor instead. +func (*VideoSetQualityFactorRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{7} +} + +func (x *VideoSetQualityFactorRequest) GetFactor() float64 { + if x != nil { + return x.Factor + } + return 0 +} + +type VideoGetQualityFactorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Factor float64 `protobuf:"fixed64,1,opt,name=factor,proto3" json:"factor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoGetQualityFactorResponse) Reset() { + *x = VideoGetQualityFactorResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoGetQualityFactorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoGetQualityFactorResponse) ProtoMessage() {} + +func (x *VideoGetQualityFactorResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoGetQualityFactorResponse.ProtoReflect.Descriptor instead. +func (*VideoGetQualityFactorResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{8} +} + +func (x *VideoGetQualityFactorResponse) GetFactor() float64 { + if x != nil { + return x.Factor + } + return 0 +} + +type VideoSetEDIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Edid string `protobuf:"bytes,1,opt,name=edid,proto3" json:"edid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoSetEDIDRequest) Reset() { + *x = VideoSetEDIDRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoSetEDIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoSetEDIDRequest) ProtoMessage() {} + +func (x *VideoSetEDIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoSetEDIDRequest.ProtoReflect.Descriptor instead. +func (*VideoSetEDIDRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{9} +} + +func (x *VideoSetEDIDRequest) GetEdid() string { + if x != nil { + return x.Edid + } + return "" +} + +type VideoGetEDIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Edid string `protobuf:"bytes,1,opt,name=edid,proto3" json:"edid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoGetEDIDResponse) Reset() { + *x = VideoGetEDIDResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoGetEDIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoGetEDIDResponse) ProtoMessage() {} + +func (x *VideoGetEDIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoGetEDIDResponse.ProtoReflect.Descriptor instead. +func (*VideoGetEDIDResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{10} +} + +func (x *VideoGetEDIDResponse) GetEdid() string { + if x != nil { + return x.Edid + } + return "" +} + +type VideoLogStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoLogStatusResponse) Reset() { + *x = VideoLogStatusResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoLogStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoLogStatusResponse) ProtoMessage() {} + +func (x *VideoLogStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoLogStatusResponse.ProtoReflect.Descriptor instead. +func (*VideoLogStatusResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{11} +} + +func (x *VideoLogStatusResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type GetLVGLVersionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLVGLVersionResponse) Reset() { + *x = GetLVGLVersionResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLVGLVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLVGLVersionResponse) ProtoMessage() {} + +func (x *GetLVGLVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLVGLVersionResponse.ProtoReflect.Descriptor instead. +func (*GetLVGLVersionResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{12} +} + +func (x *GetLVGLVersionResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +type UIObjHideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjHideRequest) Reset() { + *x = UIObjHideRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjHideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjHideRequest) ProtoMessage() {} + +func (x *UIObjHideRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjHideRequest.ProtoReflect.Descriptor instead. +func (*UIObjHideRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{13} +} + +func (x *UIObjHideRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +type UIObjHideResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjHideResponse) Reset() { + *x = UIObjHideResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjHideResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjHideResponse) ProtoMessage() {} + +func (x *UIObjHideResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjHideResponse.ProtoReflect.Descriptor instead. +func (*UIObjHideResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{14} +} + +func (x *UIObjHideResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjShowRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjShowRequest) Reset() { + *x = UIObjShowRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjShowRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjShowRequest) ProtoMessage() {} + +func (x *UIObjShowRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjShowRequest.ProtoReflect.Descriptor instead. +func (*UIObjShowRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{15} +} + +func (x *UIObjShowRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +type UIObjShowResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjShowResponse) Reset() { + *x = UIObjShowResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjShowResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjShowResponse) ProtoMessage() {} + +func (x *UIObjShowResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjShowResponse.ProtoReflect.Descriptor instead. +func (*UIObjShowResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{16} +} + +func (x *UIObjShowResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UISetVarRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UISetVarRequest) Reset() { + *x = UISetVarRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UISetVarRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UISetVarRequest) ProtoMessage() {} + +func (x *UISetVarRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UISetVarRequest.ProtoReflect.Descriptor instead. +func (*UISetVarRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{17} +} + +func (x *UISetVarRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UISetVarRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type UIGetVarRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIGetVarRequest) Reset() { + *x = UIGetVarRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIGetVarRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIGetVarRequest) ProtoMessage() {} + +func (x *UIGetVarRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIGetVarRequest.ProtoReflect.Descriptor instead. +func (*UIGetVarRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{18} +} + +func (x *UIGetVarRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UIGetVarResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIGetVarResponse) Reset() { + *x = UIGetVarResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIGetVarResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIGetVarResponse) ProtoMessage() {} + +func (x *UIGetVarResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIGetVarResponse.ProtoReflect.Descriptor instead. +func (*UIGetVarResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{19} +} + +func (x *UIGetVarResponse) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type UIObjAddStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjAddStateRequest) Reset() { + *x = UIObjAddStateRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjAddStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjAddStateRequest) ProtoMessage() {} + +func (x *UIObjAddStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjAddStateRequest.ProtoReflect.Descriptor instead. +func (*UIObjAddStateRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{20} +} + +func (x *UIObjAddStateRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjAddStateRequest) GetState() string { + if x != nil { + return x.State + } + return "" +} + +type UIObjAddStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjAddStateResponse) Reset() { + *x = UIObjAddStateResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjAddStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjAddStateResponse) ProtoMessage() {} + +func (x *UIObjAddStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjAddStateResponse.ProtoReflect.Descriptor instead. +func (*UIObjAddStateResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{21} +} + +func (x *UIObjAddStateResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjClearStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjClearStateRequest) Reset() { + *x = UIObjClearStateRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjClearStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjClearStateRequest) ProtoMessage() {} + +func (x *UIObjClearStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjClearStateRequest.ProtoReflect.Descriptor instead. +func (*UIObjClearStateRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{22} +} + +func (x *UIObjClearStateRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjClearStateRequest) GetState() string { + if x != nil { + return x.State + } + return "" +} + +type UIObjClearStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjClearStateResponse) Reset() { + *x = UIObjClearStateResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjClearStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjClearStateResponse) ProtoMessage() {} + +func (x *UIObjClearStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjClearStateResponse.ProtoReflect.Descriptor instead. +func (*UIObjClearStateResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{23} +} + +func (x *UIObjClearStateResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjAddFlagRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Flag string `protobuf:"bytes,2,opt,name=flag,proto3" json:"flag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjAddFlagRequest) Reset() { + *x = UIObjAddFlagRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjAddFlagRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjAddFlagRequest) ProtoMessage() {} + +func (x *UIObjAddFlagRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjAddFlagRequest.ProtoReflect.Descriptor instead. +func (*UIObjAddFlagRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{24} +} + +func (x *UIObjAddFlagRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjAddFlagRequest) GetFlag() string { + if x != nil { + return x.Flag + } + return "" +} + +type UIObjAddFlagResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjAddFlagResponse) Reset() { + *x = UIObjAddFlagResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjAddFlagResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjAddFlagResponse) ProtoMessage() {} + +func (x *UIObjAddFlagResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjAddFlagResponse.ProtoReflect.Descriptor instead. +func (*UIObjAddFlagResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{25} +} + +func (x *UIObjAddFlagResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjClearFlagRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Flag string `protobuf:"bytes,2,opt,name=flag,proto3" json:"flag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjClearFlagRequest) Reset() { + *x = UIObjClearFlagRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjClearFlagRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjClearFlagRequest) ProtoMessage() {} + +func (x *UIObjClearFlagRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjClearFlagRequest.ProtoReflect.Descriptor instead. +func (*UIObjClearFlagRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{26} +} + +func (x *UIObjClearFlagRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjClearFlagRequest) GetFlag() string { + if x != nil { + return x.Flag + } + return "" +} + +type UIObjClearFlagResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjClearFlagResponse) Reset() { + *x = UIObjClearFlagResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjClearFlagResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjClearFlagResponse) ProtoMessage() {} + +func (x *UIObjClearFlagResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjClearFlagResponse.ProtoReflect.Descriptor instead. +func (*UIObjClearFlagResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{27} +} + +func (x *UIObjClearFlagResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjSetOpacityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Opacity int32 `protobuf:"varint,2,opt,name=opacity,proto3" json:"opacity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetOpacityRequest) Reset() { + *x = UIObjSetOpacityRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetOpacityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetOpacityRequest) ProtoMessage() {} + +func (x *UIObjSetOpacityRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetOpacityRequest.ProtoReflect.Descriptor instead. +func (*UIObjSetOpacityRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{28} +} + +func (x *UIObjSetOpacityRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjSetOpacityRequest) GetOpacity() int32 { + if x != nil { + return x.Opacity + } + return 0 +} + +type UIObjSetOpacityResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetOpacityResponse) Reset() { + *x = UIObjSetOpacityResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetOpacityResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetOpacityResponse) ProtoMessage() {} + +func (x *UIObjSetOpacityResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetOpacityResponse.ProtoReflect.Descriptor instead. +func (*UIObjSetOpacityResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{29} +} + +func (x *UIObjSetOpacityResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjFadeInRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Duration uint32 `protobuf:"varint,2,opt,name=duration,proto3" json:"duration,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjFadeInRequest) Reset() { + *x = UIObjFadeInRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjFadeInRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjFadeInRequest) ProtoMessage() {} + +func (x *UIObjFadeInRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjFadeInRequest.ProtoReflect.Descriptor instead. +func (*UIObjFadeInRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{30} +} + +func (x *UIObjFadeInRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjFadeInRequest) GetDuration() uint32 { + if x != nil { + return x.Duration + } + return 0 +} + +type UIObjFadeInResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjFadeInResponse) Reset() { + *x = UIObjFadeInResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjFadeInResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjFadeInResponse) ProtoMessage() {} + +func (x *UIObjFadeInResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjFadeInResponse.ProtoReflect.Descriptor instead. +func (*UIObjFadeInResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{31} +} + +func (x *UIObjFadeInResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjFadeOutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Duration uint32 `protobuf:"varint,2,opt,name=duration,proto3" json:"duration,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjFadeOutRequest) Reset() { + *x = UIObjFadeOutRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjFadeOutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjFadeOutRequest) ProtoMessage() {} + +func (x *UIObjFadeOutRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjFadeOutRequest.ProtoReflect.Descriptor instead. +func (*UIObjFadeOutRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{32} +} + +func (x *UIObjFadeOutRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjFadeOutRequest) GetDuration() uint32 { + if x != nil { + return x.Duration + } + return 0 +} + +type UIObjFadeOutResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjFadeOutResponse) Reset() { + *x = UIObjFadeOutResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjFadeOutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjFadeOutResponse) ProtoMessage() {} + +func (x *UIObjFadeOutResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjFadeOutResponse.ProtoReflect.Descriptor instead. +func (*UIObjFadeOutResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{33} +} + +func (x *UIObjFadeOutResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjSetLabelTextRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetLabelTextRequest) Reset() { + *x = UIObjSetLabelTextRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetLabelTextRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetLabelTextRequest) ProtoMessage() {} + +func (x *UIObjSetLabelTextRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetLabelTextRequest.ProtoReflect.Descriptor instead. +func (*UIObjSetLabelTextRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{34} +} + +func (x *UIObjSetLabelTextRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjSetLabelTextRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type UIObjSetLabelTextResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetLabelTextResponse) Reset() { + *x = UIObjSetLabelTextResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetLabelTextResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetLabelTextResponse) ProtoMessage() {} + +func (x *UIObjSetLabelTextResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetLabelTextResponse.ProtoReflect.Descriptor instead. +func (*UIObjSetLabelTextResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{35} +} + +func (x *UIObjSetLabelTextResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UIObjSetImageSrcRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetImageSrcRequest) Reset() { + *x = UIObjSetImageSrcRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetImageSrcRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetImageSrcRequest) ProtoMessage() {} + +func (x *UIObjSetImageSrcRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetImageSrcRequest.ProtoReflect.Descriptor instead. +func (*UIObjSetImageSrcRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{36} +} + +func (x *UIObjSetImageSrcRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UIObjSetImageSrcRequest) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +type UIObjSetImageSrcResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UIObjSetImageSrcResponse) Reset() { + *x = UIObjSetImageSrcResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UIObjSetImageSrcResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UIObjSetImageSrcResponse) ProtoMessage() {} + +func (x *UIObjSetImageSrcResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UIObjSetImageSrcResponse.ProtoReflect.Descriptor instead. +func (*UIObjSetImageSrcResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{37} +} + +func (x *UIObjSetImageSrcResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type DisplaySetRotationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rotation uint32 `protobuf:"varint,1,opt,name=rotation,proto3" json:"rotation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplaySetRotationRequest) Reset() { + *x = DisplaySetRotationRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplaySetRotationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplaySetRotationRequest) ProtoMessage() {} + +func (x *DisplaySetRotationRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplaySetRotationRequest.ProtoReflect.Descriptor instead. +func (*DisplaySetRotationRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{38} +} + +func (x *DisplaySetRotationRequest) GetRotation() uint32 { + if x != nil { + return x.Rotation + } + return 0 +} + +type DisplaySetRotationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplaySetRotationResponse) Reset() { + *x = DisplaySetRotationResponse{} + mi := &file_internal_native_proto_native_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplaySetRotationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplaySetRotationResponse) ProtoMessage() {} + +func (x *DisplaySetRotationResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplaySetRotationResponse.ProtoReflect.Descriptor instead. +func (*DisplaySetRotationResponse) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{39} +} + +func (x *DisplaySetRotationResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type UpdateLabelIfChangedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + NewText string `protobuf:"bytes,2,opt,name=new_text,json=newText,proto3" json:"new_text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateLabelIfChangedRequest) Reset() { + *x = UpdateLabelIfChangedRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateLabelIfChangedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateLabelIfChangedRequest) ProtoMessage() {} + +func (x *UpdateLabelIfChangedRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateLabelIfChangedRequest.ProtoReflect.Descriptor instead. +func (*UpdateLabelIfChangedRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{40} +} + +func (x *UpdateLabelIfChangedRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UpdateLabelIfChangedRequest) GetNewText() string { + if x != nil { + return x.NewText + } + return "" +} + +type UpdateLabelAndChangeVisibilityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` + NewText string `protobuf:"bytes,2,opt,name=new_text,json=newText,proto3" json:"new_text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateLabelAndChangeVisibilityRequest) Reset() { + *x = UpdateLabelAndChangeVisibilityRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateLabelAndChangeVisibilityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateLabelAndChangeVisibilityRequest) ProtoMessage() {} + +func (x *UpdateLabelAndChangeVisibilityRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateLabelAndChangeVisibilityRequest.ProtoReflect.Descriptor instead. +func (*UpdateLabelAndChangeVisibilityRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{41} +} + +func (x *UpdateLabelAndChangeVisibilityRequest) GetObjName() string { + if x != nil { + return x.ObjName + } + return "" +} + +func (x *UpdateLabelAndChangeVisibilityRequest) GetNewText() string { + if x != nil { + return x.NewText + } + return "" +} + +type SwitchToScreenIfRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScreenName string `protobuf:"bytes,1,opt,name=screen_name,json=screenName,proto3" json:"screen_name,omitempty"` + ShouldSwitch []string `protobuf:"bytes,2,rep,name=should_switch,json=shouldSwitch,proto3" json:"should_switch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchToScreenIfRequest) Reset() { + *x = SwitchToScreenIfRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchToScreenIfRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchToScreenIfRequest) ProtoMessage() {} + +func (x *SwitchToScreenIfRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SwitchToScreenIfRequest.ProtoReflect.Descriptor instead. +func (*SwitchToScreenIfRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{42} +} + +func (x *SwitchToScreenIfRequest) GetScreenName() string { + if x != nil { + return x.ScreenName + } + return "" +} + +func (x *SwitchToScreenIfRequest) GetShouldSwitch() []string { + if x != nil { + return x.ShouldSwitch + } + return nil +} + +type SwitchToScreenIfDifferentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScreenName string `protobuf:"bytes,1,opt,name=screen_name,json=screenName,proto3" json:"screen_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchToScreenIfDifferentRequest) Reset() { + *x = SwitchToScreenIfDifferentRequest{} + mi := &file_internal_native_proto_native_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchToScreenIfDifferentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchToScreenIfDifferentRequest) ProtoMessage() {} + +func (x *SwitchToScreenIfDifferentRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SwitchToScreenIfDifferentRequest.ProtoReflect.Descriptor instead. +func (*SwitchToScreenIfDifferentRequest) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{43} +} + +func (x *SwitchToScreenIfDifferentRequest) GetScreenName() string { + if x != nil { + return x.ScreenName + } + return "" +} + +type Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Types that are valid to be assigned to Data: + // + // *Event_VideoState + // *Event_IndevEvent + // *Event_RpcEvent + // *Event_VideoFrame + Data isEvent_Data `protobuf_oneof:"data"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Event) Reset() { + *x = Event{} + mi := &file_internal_native_proto_native_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Event) ProtoMessage() {} + +func (x *Event) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Event.ProtoReflect.Descriptor instead. +func (*Event) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{44} +} + +func (x *Event) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Event) GetData() isEvent_Data { + if x != nil { + return x.Data + } + return nil +} + +func (x *Event) GetVideoState() *VideoState { + if x != nil { + if x, ok := x.Data.(*Event_VideoState); ok { + return x.VideoState + } + } + return nil +} + +func (x *Event) GetIndevEvent() string { + if x != nil { + if x, ok := x.Data.(*Event_IndevEvent); ok { + return x.IndevEvent + } + } + return "" +} + +func (x *Event) GetRpcEvent() string { + if x != nil { + if x, ok := x.Data.(*Event_RpcEvent); ok { + return x.RpcEvent + } + } + return "" +} + +func (x *Event) GetVideoFrame() *VideoFrame { + if x != nil { + if x, ok := x.Data.(*Event_VideoFrame); ok { + return x.VideoFrame + } + } + return nil +} + +type isEvent_Data interface { + isEvent_Data() +} + +type Event_VideoState struct { + VideoState *VideoState `protobuf:"bytes,2,opt,name=video_state,json=videoState,proto3,oneof"` +} + +type Event_IndevEvent struct { + IndevEvent string `protobuf:"bytes,3,opt,name=indev_event,json=indevEvent,proto3,oneof"` +} + +type Event_RpcEvent struct { + RpcEvent string `protobuf:"bytes,4,opt,name=rpc_event,json=rpcEvent,proto3,oneof"` +} + +type Event_VideoFrame struct { + VideoFrame *VideoFrame `protobuf:"bytes,5,opt,name=video_frame,json=videoFrame,proto3,oneof"` +} + +func (*Event_VideoState) isEvent_Data() {} + +func (*Event_IndevEvent) isEvent_Data() {} + +func (*Event_RpcEvent) isEvent_Data() {} + +func (*Event_VideoFrame) isEvent_Data() {} + +type VideoFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + Frame []byte `protobuf:"bytes,1,opt,name=frame,proto3" json:"frame,omitempty"` + DurationNs int64 `protobuf:"varint,2,opt,name=duration_ns,json=durationNs,proto3" json:"duration_ns,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoFrame) Reset() { + *x = VideoFrame{} + mi := &file_internal_native_proto_native_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoFrame) ProtoMessage() {} + +func (x *VideoFrame) ProtoReflect() protoreflect.Message { + mi := &file_internal_native_proto_native_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoFrame.ProtoReflect.Descriptor instead. +func (*VideoFrame) Descriptor() ([]byte, []int) { + return file_internal_native_proto_native_proto_rawDescGZIP(), []int{45} +} + +func (x *VideoFrame) GetFrame() []byte { + if x != nil { + return x.Frame + } + return nil +} + +func (x *VideoFrame) GetDurationNs() int64 { + if x != nil { + return x.DurationNs + } + return 0 +} + +var File_internal_native_proto_native_proto protoreflect.FileDescriptor + +var file_internal_native_proto_native_proto_rawDesc = string([]byte{ + 0x0a, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x22, 0x07, 0x0a, 0x05, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x10, 0x0a, 0x0e, 0x49, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5e, 0x0a, 0x0f, 0x49, 0x73, 0x52, 0x65, 0x61, + 0x64, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, + 0x61, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x5f, + 0x72, 0x65, 0x61, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x76, 0x69, 0x64, + 0x65, 0x6f, 0x52, 0x65, 0x61, 0x64, 0x79, 0x22, 0x90, 0x01, 0x0a, 0x0a, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, + 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, + 0x12, 0x28, 0x0a, 0x10, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0e, 0x66, 0x72, 0x61, 0x6d, + 0x65, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x22, 0x34, 0x0a, 0x18, 0x56, 0x69, + 0x64, 0x65, 0x6f, 0x53, 0x65, 0x74, 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x22, 0x35, 0x0a, 0x19, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, 0x53, 0x6c, 0x65, 0x65, + 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x3f, 0x0a, 0x1f, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x22, 0x36, 0x0a, 0x1c, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x53, 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x22, 0x37, 0x0a, 0x1d, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, + 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x06, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x29, 0x0a, 0x13, 0x56, 0x69, 0x64, + 0x65, 0x6f, 0x53, 0x65, 0x74, 0x45, 0x44, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x65, 0x64, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x65, 0x64, 0x69, 0x64, 0x22, 0x2a, 0x0a, 0x14, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, + 0x45, 0x44, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x65, 0x64, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x64, 0x69, 0x64, + 0x22, 0x30, 0x0a, 0x16, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x22, 0x32, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4c, 0x56, 0x47, 0x4c, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2d, 0x0a, 0x10, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x48, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, + 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, + 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x2d, 0x0a, 0x11, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x48, 0x69, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x22, 0x2d, 0x0a, 0x10, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x68, 0x6f, + 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, + 0x61, 0x6d, 0x65, 0x22, 0x2d, 0x0a, 0x11, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x68, 0x6f, 0x77, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x22, 0x3b, 0x0a, 0x0f, 0x55, 0x49, 0x53, 0x65, 0x74, 0x56, 0x61, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x25, 0x0a, 0x0f, 0x55, 0x49, 0x47, 0x65, 0x74, 0x56, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x28, 0x0a, 0x10, 0x55, 0x49, 0x47, 0x65, 0x74, 0x56, + 0x61, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x22, 0x47, 0x0a, 0x14, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, 0x64, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x15, 0x55, 0x49, 0x4f, + 0x62, 0x6a, 0x41, 0x64, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x49, 0x0a, 0x16, + 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x33, 0x0a, 0x17, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x43, 0x6c, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x44, 0x0a, 0x13, + 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, 0x64, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, + 0x61, 0x67, 0x22, 0x30, 0x0a, 0x14, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, 0x64, 0x46, 0x6c, + 0x61, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x22, 0x46, 0x0a, 0x15, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, + 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x22, 0x32, 0x0a, 0x16, + 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x22, 0x4d, 0x0a, 0x16, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4f, 0x70, 0x61, 0x63, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, + 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, + 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x6f, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x22, + 0x33, 0x0a, 0x17, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4f, 0x70, 0x61, 0x63, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x22, 0x4b, 0x0a, 0x12, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, + 0x65, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, + 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, + 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x2f, 0x0a, 0x13, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x49, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x22, 0x4c, 0x0a, 0x13, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x4f, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x30, 0x0a, 0x14, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x4f, 0x75, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x22, 0x49, 0x0a, 0x18, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, + 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x35, 0x0a, + 0x19, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x65, + 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x22, 0x4a, 0x0a, 0x17, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, + 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x72, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x22, 0x34, 0x0a, 0x18, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, + 0x65, 0x53, 0x72, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, + 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x37, 0x0a, 0x19, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x36, 0x0a, 0x1a, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x53, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x49, 0x66, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x54, 0x65, 0x78, 0x74, 0x22, 0x5d, 0x0a, 0x25, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x41, 0x6e, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x62, 0x6a, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x54, 0x65, 0x78, 0x74, 0x22, 0x5f, 0x0a, 0x17, 0x53, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x54, 0x6f, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x49, 0x66, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x63, 0x72, + 0x65, 0x65, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x68, 0x6f, 0x75, 0x6c, + 0x64, 0x5f, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, + 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x22, 0x43, 0x0a, 0x20, + 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x54, 0x6f, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x49, 0x66, + 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0xd3, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x35, 0x0a, 0x0b, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, + 0x64, 0x65, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x76, 0x69, 0x64, 0x65, + 0x6f, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x76, 0x5f, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, + 0x6e, 0x64, 0x65, 0x76, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x09, 0x72, 0x70, 0x63, + 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, + 0x72, 0x70, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x0b, 0x76, 0x69, 0x64, 0x65, + 0x6f, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x46, 0x72, 0x61, 0x6d, + 0x65, 0x48, 0x00, 0x52, 0x0a, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x42, + 0x06, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x43, 0x0a, 0x0a, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x46, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x64, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x73, 0x32, 0xfd, 0x11, 0x0a, + 0x0d, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, + 0x0a, 0x07, 0x49, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, 0x16, 0x2e, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x2e, 0x49, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x17, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x49, 0x73, 0x52, 0x65, 0x61, + 0x64, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x11, 0x56, 0x69, + 0x64, 0x65, 0x6f, 0x53, 0x65, 0x74, 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x12, + 0x20, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x65, + 0x74, 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x45, 0x0a, 0x11, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, 0x53, 0x6c, 0x65, 0x65, + 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, + 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x17, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x65, 0x64, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x27, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x53, 0x6c, 0x65, 0x65, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x15, 0x56, 0x69, + 0x64, 0x65, 0x6f, 0x53, 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x24, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, + 0x65, 0x6f, 0x53, 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x15, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x47, 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x25, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, + 0x65, 0x74, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x53, 0x65, 0x74, 0x45, 0x44, 0x49, 0x44, 0x12, 0x1b, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x65, 0x74, 0x45, 0x44, 0x49, 0x44, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x0c, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x47, 0x65, 0x74, 0x45, + 0x44, 0x49, 0x44, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x47, 0x65, 0x74, 0x45, 0x44, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3f, 0x0a, 0x0e, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x1e, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f, + 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x29, 0x0a, 0x09, 0x56, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x0d, + 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x0a, + 0x56, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, + 0x56, 0x47, 0x4c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0d, 0x2e, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1e, 0x2e, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x56, 0x47, 0x4c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x55, 0x49, 0x4f, + 0x62, 0x6a, 0x48, 0x69, 0x64, 0x65, 0x12, 0x18, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, + 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x48, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x19, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x48, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x55, + 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x68, 0x6f, 0x77, 0x12, 0x18, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x68, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, + 0x6a, 0x53, 0x68, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, + 0x08, 0x55, 0x49, 0x53, 0x65, 0x74, 0x56, 0x61, 0x72, 0x12, 0x17, 0x2e, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x2e, 0x55, 0x49, 0x53, 0x65, 0x74, 0x56, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x3d, 0x0a, 0x08, 0x55, 0x49, 0x47, 0x65, 0x74, 0x56, 0x61, 0x72, 0x12, 0x17, 0x2e, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x47, 0x65, 0x74, 0x56, 0x61, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, + 0x55, 0x49, 0x47, 0x65, 0x74, 0x56, 0x61, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4c, 0x0a, 0x0d, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, 0x64, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1c, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x41, 0x64, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, + 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, + 0x0a, 0x0f, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1e, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x43, 0x6c, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x43, 0x6c, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, 0x64, 0x46, 0x6c, + 0x61, 0x67, 0x12, 0x1b, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, + 0x6a, 0x41, 0x64, 0x64, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x41, 0x64, + 0x64, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, + 0x0e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x46, 0x6c, 0x61, 0x67, 0x12, + 0x1d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, + 0x65, 0x61, 0x72, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, + 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x46, 0x6c, 0x61, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, + 0x0a, 0x0f, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4f, 0x70, 0x61, 0x63, 0x69, 0x74, + 0x79, 0x12, 0x1e, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x53, 0x65, 0x74, 0x4f, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x53, 0x65, 0x74, 0x4f, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x49, + 0x6e, 0x12, 0x1a, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, + 0x46, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, + 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x55, 0x49, + 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x4f, 0x75, 0x74, 0x12, 0x1b, 0x2e, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x4f, 0x75, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x46, 0x61, 0x64, 0x65, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, + 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x12, 0x20, 0x2e, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x54, 0x65, 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6e, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x55, 0x0a, 0x10, 0x55, 0x49, 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, + 0x53, 0x72, 0x63, 0x12, 0x1f, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, 0x4f, + 0x62, 0x6a, 0x53, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x72, 0x63, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x49, + 0x4f, 0x62, 0x6a, 0x53, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x72, 0x63, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x6e, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x74, + 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x53, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x49, 0x66, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x23, 0x2e, 0x6e, 0x61, + 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x49, 0x66, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x5e, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x41, 0x6e, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x79, 0x12, 0x2d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x41, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x56, + 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x42, 0x0a, 0x10, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x54, 0x6f, 0x53, 0x63, 0x72, 0x65, 0x65, + 0x6e, 0x49, 0x66, 0x12, 0x1f, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x53, 0x77, 0x69, + 0x74, 0x63, 0x68, 0x54, 0x6f, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x49, 0x66, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x54, 0x0a, 0x19, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x54, 0x6f, 0x53, + 0x63, 0x72, 0x65, 0x65, 0x6e, 0x49, 0x66, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x74, + 0x12, 0x28, 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, + 0x54, 0x6f, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x49, 0x66, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x41, 0x0a, 0x21, 0x44, 0x6f, 0x4e, + 0x6f, 0x74, 0x55, 0x73, 0x65, 0x54, 0x68, 0x69, 0x73, 0x49, 0x73, 0x46, 0x6f, 0x72, 0x43, 0x72, + 0x61, 0x73, 0x68, 0x54, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x0d, + 0x2e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2e, 0x0a, 0x0c, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x0d, 0x2e, 0x6e, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x6e, 0x61, + 0x74, 0x69, 0x76, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x65, 0x74, 0x6b, 0x76, + 0x6d, 0x2f, 0x6b, 0x76, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6e, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +}) + +var ( + file_internal_native_proto_native_proto_rawDescOnce sync.Once + file_internal_native_proto_native_proto_rawDescData []byte +) + +func file_internal_native_proto_native_proto_rawDescGZIP() []byte { + file_internal_native_proto_native_proto_rawDescOnce.Do(func() { + file_internal_native_proto_native_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_native_proto_native_proto_rawDesc), len(file_internal_native_proto_native_proto_rawDesc))) + }) + return file_internal_native_proto_native_proto_rawDescData +} + +var file_internal_native_proto_native_proto_msgTypes = make([]protoimpl.MessageInfo, 46) +var file_internal_native_proto_native_proto_goTypes = []any{ + (*Empty)(nil), // 0: native.Empty + (*IsReadyRequest)(nil), // 1: native.IsReadyRequest + (*IsReadyResponse)(nil), // 2: native.IsReadyResponse + (*VideoState)(nil), // 3: native.VideoState + (*VideoSetSleepModeRequest)(nil), // 4: native.VideoSetSleepModeRequest + (*VideoGetSleepModeResponse)(nil), // 5: native.VideoGetSleepModeResponse + (*VideoSleepModeSupportedResponse)(nil), // 6: native.VideoSleepModeSupportedResponse + (*VideoSetQualityFactorRequest)(nil), // 7: native.VideoSetQualityFactorRequest + (*VideoGetQualityFactorResponse)(nil), // 8: native.VideoGetQualityFactorResponse + (*VideoSetEDIDRequest)(nil), // 9: native.VideoSetEDIDRequest + (*VideoGetEDIDResponse)(nil), // 10: native.VideoGetEDIDResponse + (*VideoLogStatusResponse)(nil), // 11: native.VideoLogStatusResponse + (*GetLVGLVersionResponse)(nil), // 12: native.GetLVGLVersionResponse + (*UIObjHideRequest)(nil), // 13: native.UIObjHideRequest + (*UIObjHideResponse)(nil), // 14: native.UIObjHideResponse + (*UIObjShowRequest)(nil), // 15: native.UIObjShowRequest + (*UIObjShowResponse)(nil), // 16: native.UIObjShowResponse + (*UISetVarRequest)(nil), // 17: native.UISetVarRequest + (*UIGetVarRequest)(nil), // 18: native.UIGetVarRequest + (*UIGetVarResponse)(nil), // 19: native.UIGetVarResponse + (*UIObjAddStateRequest)(nil), // 20: native.UIObjAddStateRequest + (*UIObjAddStateResponse)(nil), // 21: native.UIObjAddStateResponse + (*UIObjClearStateRequest)(nil), // 22: native.UIObjClearStateRequest + (*UIObjClearStateResponse)(nil), // 23: native.UIObjClearStateResponse + (*UIObjAddFlagRequest)(nil), // 24: native.UIObjAddFlagRequest + (*UIObjAddFlagResponse)(nil), // 25: native.UIObjAddFlagResponse + (*UIObjClearFlagRequest)(nil), // 26: native.UIObjClearFlagRequest + (*UIObjClearFlagResponse)(nil), // 27: native.UIObjClearFlagResponse + (*UIObjSetOpacityRequest)(nil), // 28: native.UIObjSetOpacityRequest + (*UIObjSetOpacityResponse)(nil), // 29: native.UIObjSetOpacityResponse + (*UIObjFadeInRequest)(nil), // 30: native.UIObjFadeInRequest + (*UIObjFadeInResponse)(nil), // 31: native.UIObjFadeInResponse + (*UIObjFadeOutRequest)(nil), // 32: native.UIObjFadeOutRequest + (*UIObjFadeOutResponse)(nil), // 33: native.UIObjFadeOutResponse + (*UIObjSetLabelTextRequest)(nil), // 34: native.UIObjSetLabelTextRequest + (*UIObjSetLabelTextResponse)(nil), // 35: native.UIObjSetLabelTextResponse + (*UIObjSetImageSrcRequest)(nil), // 36: native.UIObjSetImageSrcRequest + (*UIObjSetImageSrcResponse)(nil), // 37: native.UIObjSetImageSrcResponse + (*DisplaySetRotationRequest)(nil), // 38: native.DisplaySetRotationRequest + (*DisplaySetRotationResponse)(nil), // 39: native.DisplaySetRotationResponse + (*UpdateLabelIfChangedRequest)(nil), // 40: native.UpdateLabelIfChangedRequest + (*UpdateLabelAndChangeVisibilityRequest)(nil), // 41: native.UpdateLabelAndChangeVisibilityRequest + (*SwitchToScreenIfRequest)(nil), // 42: native.SwitchToScreenIfRequest + (*SwitchToScreenIfDifferentRequest)(nil), // 43: native.SwitchToScreenIfDifferentRequest + (*Event)(nil), // 44: native.Event + (*VideoFrame)(nil), // 45: native.VideoFrame +} +var file_internal_native_proto_native_proto_depIdxs = []int32{ + 3, // 0: native.Event.video_state:type_name -> native.VideoState + 45, // 1: native.Event.video_frame:type_name -> native.VideoFrame + 1, // 2: native.NativeService.IsReady:input_type -> native.IsReadyRequest + 4, // 3: native.NativeService.VideoSetSleepMode:input_type -> native.VideoSetSleepModeRequest + 0, // 4: native.NativeService.VideoGetSleepMode:input_type -> native.Empty + 0, // 5: native.NativeService.VideoSleepModeSupported:input_type -> native.Empty + 7, // 6: native.NativeService.VideoSetQualityFactor:input_type -> native.VideoSetQualityFactorRequest + 0, // 7: native.NativeService.VideoGetQualityFactor:input_type -> native.Empty + 9, // 8: native.NativeService.VideoSetEDID:input_type -> native.VideoSetEDIDRequest + 0, // 9: native.NativeService.VideoGetEDID:input_type -> native.Empty + 0, // 10: native.NativeService.VideoLogStatus:input_type -> native.Empty + 0, // 11: native.NativeService.VideoStop:input_type -> native.Empty + 0, // 12: native.NativeService.VideoStart:input_type -> native.Empty + 0, // 13: native.NativeService.GetLVGLVersion:input_type -> native.Empty + 13, // 14: native.NativeService.UIObjHide:input_type -> native.UIObjHideRequest + 15, // 15: native.NativeService.UIObjShow:input_type -> native.UIObjShowRequest + 17, // 16: native.NativeService.UISetVar:input_type -> native.UISetVarRequest + 18, // 17: native.NativeService.UIGetVar:input_type -> native.UIGetVarRequest + 20, // 18: native.NativeService.UIObjAddState:input_type -> native.UIObjAddStateRequest + 22, // 19: native.NativeService.UIObjClearState:input_type -> native.UIObjClearStateRequest + 24, // 20: native.NativeService.UIObjAddFlag:input_type -> native.UIObjAddFlagRequest + 26, // 21: native.NativeService.UIObjClearFlag:input_type -> native.UIObjClearFlagRequest + 28, // 22: native.NativeService.UIObjSetOpacity:input_type -> native.UIObjSetOpacityRequest + 30, // 23: native.NativeService.UIObjFadeIn:input_type -> native.UIObjFadeInRequest + 32, // 24: native.NativeService.UIObjFadeOut:input_type -> native.UIObjFadeOutRequest + 34, // 25: native.NativeService.UIObjSetLabelText:input_type -> native.UIObjSetLabelTextRequest + 36, // 26: native.NativeService.UIObjSetImageSrc:input_type -> native.UIObjSetImageSrcRequest + 38, // 27: native.NativeService.DisplaySetRotation:input_type -> native.DisplaySetRotationRequest + 40, // 28: native.NativeService.UpdateLabelIfChanged:input_type -> native.UpdateLabelIfChangedRequest + 41, // 29: native.NativeService.UpdateLabelAndChangeVisibility:input_type -> native.UpdateLabelAndChangeVisibilityRequest + 42, // 30: native.NativeService.SwitchToScreenIf:input_type -> native.SwitchToScreenIfRequest + 43, // 31: native.NativeService.SwitchToScreenIfDifferent:input_type -> native.SwitchToScreenIfDifferentRequest + 0, // 32: native.NativeService.DoNotUseThisIsForCrashTestingOnly:input_type -> native.Empty + 0, // 33: native.NativeService.StreamEvents:input_type -> native.Empty + 2, // 34: native.NativeService.IsReady:output_type -> native.IsReadyResponse + 0, // 35: native.NativeService.VideoSetSleepMode:output_type -> native.Empty + 5, // 36: native.NativeService.VideoGetSleepMode:output_type -> native.VideoGetSleepModeResponse + 6, // 37: native.NativeService.VideoSleepModeSupported:output_type -> native.VideoSleepModeSupportedResponse + 0, // 38: native.NativeService.VideoSetQualityFactor:output_type -> native.Empty + 8, // 39: native.NativeService.VideoGetQualityFactor:output_type -> native.VideoGetQualityFactorResponse + 0, // 40: native.NativeService.VideoSetEDID:output_type -> native.Empty + 10, // 41: native.NativeService.VideoGetEDID:output_type -> native.VideoGetEDIDResponse + 11, // 42: native.NativeService.VideoLogStatus:output_type -> native.VideoLogStatusResponse + 0, // 43: native.NativeService.VideoStop:output_type -> native.Empty + 0, // 44: native.NativeService.VideoStart:output_type -> native.Empty + 12, // 45: native.NativeService.GetLVGLVersion:output_type -> native.GetLVGLVersionResponse + 14, // 46: native.NativeService.UIObjHide:output_type -> native.UIObjHideResponse + 16, // 47: native.NativeService.UIObjShow:output_type -> native.UIObjShowResponse + 0, // 48: native.NativeService.UISetVar:output_type -> native.Empty + 19, // 49: native.NativeService.UIGetVar:output_type -> native.UIGetVarResponse + 21, // 50: native.NativeService.UIObjAddState:output_type -> native.UIObjAddStateResponse + 23, // 51: native.NativeService.UIObjClearState:output_type -> native.UIObjClearStateResponse + 25, // 52: native.NativeService.UIObjAddFlag:output_type -> native.UIObjAddFlagResponse + 27, // 53: native.NativeService.UIObjClearFlag:output_type -> native.UIObjClearFlagResponse + 29, // 54: native.NativeService.UIObjSetOpacity:output_type -> native.UIObjSetOpacityResponse + 31, // 55: native.NativeService.UIObjFadeIn:output_type -> native.UIObjFadeInResponse + 33, // 56: native.NativeService.UIObjFadeOut:output_type -> native.UIObjFadeOutResponse + 35, // 57: native.NativeService.UIObjSetLabelText:output_type -> native.UIObjSetLabelTextResponse + 37, // 58: native.NativeService.UIObjSetImageSrc:output_type -> native.UIObjSetImageSrcResponse + 39, // 59: native.NativeService.DisplaySetRotation:output_type -> native.DisplaySetRotationResponse + 0, // 60: native.NativeService.UpdateLabelIfChanged:output_type -> native.Empty + 0, // 61: native.NativeService.UpdateLabelAndChangeVisibility:output_type -> native.Empty + 0, // 62: native.NativeService.SwitchToScreenIf:output_type -> native.Empty + 0, // 63: native.NativeService.SwitchToScreenIfDifferent:output_type -> native.Empty + 0, // 64: native.NativeService.DoNotUseThisIsForCrashTestingOnly:output_type -> native.Empty + 44, // 65: native.NativeService.StreamEvents:output_type -> native.Event + 34, // [34:66] is the sub-list for method output_type + 2, // [2:34] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_internal_native_proto_native_proto_init() } +func file_internal_native_proto_native_proto_init() { + if File_internal_native_proto_native_proto != nil { + return + } + file_internal_native_proto_native_proto_msgTypes[44].OneofWrappers = []any{ + (*Event_VideoState)(nil), + (*Event_IndevEvent)(nil), + (*Event_RpcEvent)(nil), + (*Event_VideoFrame)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_native_proto_native_proto_rawDesc), len(file_internal_native_proto_native_proto_rawDesc)), + NumEnums: 0, + NumMessages: 46, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_internal_native_proto_native_proto_goTypes, + DependencyIndexes: file_internal_native_proto_native_proto_depIdxs, + MessageInfos: file_internal_native_proto_native_proto_msgTypes, + }.Build() + File_internal_native_proto_native_proto = out.File + file_internal_native_proto_native_proto_goTypes = nil + file_internal_native_proto_native_proto_depIdxs = nil +} diff --git a/internal/native/proto/native.proto b/internal/native/proto/native.proto new file mode 100644 index 00000000..538d3303 --- /dev/null +++ b/internal/native/proto/native.proto @@ -0,0 +1,258 @@ +syntax = "proto3"; + +package native; + +option go_package = "github.com/jetkvm/kvm/internal/native/proto"; + +// NativeService provides methods to interact with the native layer +service NativeService { + // Ready check + rpc IsReady(IsReadyRequest) returns (IsReadyResponse); + + // Video methods + rpc VideoSetSleepMode(VideoSetSleepModeRequest) returns (Empty); + rpc VideoGetSleepMode(Empty) returns (VideoGetSleepModeResponse); + rpc VideoSleepModeSupported(Empty) returns (VideoSleepModeSupportedResponse); + rpc VideoSetQualityFactor(VideoSetQualityFactorRequest) returns (Empty); + rpc VideoGetQualityFactor(Empty) returns (VideoGetQualityFactorResponse); + rpc VideoSetEDID(VideoSetEDIDRequest) returns (Empty); + rpc VideoGetEDID(Empty) returns (VideoGetEDIDResponse); + rpc VideoLogStatus(Empty) returns (VideoLogStatusResponse); + rpc VideoStop(Empty) returns (Empty); + rpc VideoStart(Empty) returns (Empty); + + // UI methods + rpc GetLVGLVersion(Empty) returns (GetLVGLVersionResponse); + rpc UIObjHide(UIObjHideRequest) returns (UIObjHideResponse); + rpc UIObjShow(UIObjShowRequest) returns (UIObjShowResponse); + rpc UISetVar(UISetVarRequest) returns (Empty); + rpc UIGetVar(UIGetVarRequest) returns (UIGetVarResponse); + rpc UIObjAddState(UIObjAddStateRequest) returns (UIObjAddStateResponse); + rpc UIObjClearState(UIObjClearStateRequest) returns (UIObjClearStateResponse); + rpc UIObjAddFlag(UIObjAddFlagRequest) returns (UIObjAddFlagResponse); + rpc UIObjClearFlag(UIObjClearFlagRequest) returns (UIObjClearFlagResponse); + rpc UIObjSetOpacity(UIObjSetOpacityRequest) returns (UIObjSetOpacityResponse); + rpc UIObjFadeIn(UIObjFadeInRequest) returns (UIObjFadeInResponse); + rpc UIObjFadeOut(UIObjFadeOutRequest) returns (UIObjFadeOutResponse); + rpc UIObjSetLabelText(UIObjSetLabelTextRequest) returns (UIObjSetLabelTextResponse); + rpc UIObjSetImageSrc(UIObjSetImageSrcRequest) returns (UIObjSetImageSrcResponse); + rpc DisplaySetRotation(DisplaySetRotationRequest) returns (DisplaySetRotationResponse); + rpc UpdateLabelIfChanged(UpdateLabelIfChangedRequest) returns (Empty); + rpc UpdateLabelAndChangeVisibility(UpdateLabelAndChangeVisibilityRequest) returns (Empty); + rpc SwitchToScreenIf(SwitchToScreenIfRequest) returns (Empty); + rpc SwitchToScreenIfDifferent(SwitchToScreenIfDifferentRequest) returns (Empty); + + // Testing + rpc DoNotUseThisIsForCrashTestingOnly(Empty) returns (Empty); + + // Events stream + rpc StreamEvents(Empty) returns (stream Event); +} + +// Messages +message Empty {} + +message IsReadyRequest {} + +message IsReadyResponse { + bool ready = 1; + string error = 2; + bool video_ready = 3; +} + +message VideoState { + bool ready = 1; + string error = 2; + int32 width = 3; + int32 height = 4; + double frame_per_second = 5; +} + +message VideoSetSleepModeRequest { + bool enabled = 1; +} + +message VideoGetSleepModeResponse { + bool enabled = 1; +} + +message VideoSleepModeSupportedResponse { + bool supported = 1; +} + +message VideoSetQualityFactorRequest { + double factor = 1; +} + +message VideoGetQualityFactorResponse { + double factor = 1; +} + +message VideoSetEDIDRequest { + string edid = 1; +} + +message VideoGetEDIDResponse { + string edid = 1; +} + +message VideoLogStatusResponse { + string status = 1; +} + +message GetLVGLVersionResponse { + string version = 1; +} + +message UIObjHideRequest { + string obj_name = 1; +} + +message UIObjHideResponse { + bool success = 1; +} + +message UIObjShowRequest { + string obj_name = 1; +} + +message UIObjShowResponse { + bool success = 1; +} + +message UISetVarRequest { + string name = 1; + string value = 2; +} + +message UIGetVarRequest { + string name = 1; +} + +message UIGetVarResponse { + string value = 1; +} + +message UIObjAddStateRequest { + string obj_name = 1; + string state = 2; +} + +message UIObjAddStateResponse { + bool success = 1; +} + +message UIObjClearStateRequest { + string obj_name = 1; + string state = 2; +} + +message UIObjClearStateResponse { + bool success = 1; +} + +message UIObjAddFlagRequest { + string obj_name = 1; + string flag = 2; +} + +message UIObjAddFlagResponse { + bool success = 1; +} + +message UIObjClearFlagRequest { + string obj_name = 1; + string flag = 2; +} + +message UIObjClearFlagResponse { + bool success = 1; +} + +message UIObjSetOpacityRequest { + string obj_name = 1; + int32 opacity = 2; +} + +message UIObjSetOpacityResponse { + bool success = 1; +} + +message UIObjFadeInRequest { + string obj_name = 1; + uint32 duration = 2; +} + +message UIObjFadeInResponse { + bool success = 1; +} + +message UIObjFadeOutRequest { + string obj_name = 1; + uint32 duration = 2; +} + +message UIObjFadeOutResponse { + bool success = 1; +} + +message UIObjSetLabelTextRequest { + string obj_name = 1; + string text = 2; +} + +message UIObjSetLabelTextResponse { + bool success = 1; +} + +message UIObjSetImageSrcRequest { + string obj_name = 1; + string image = 2; +} + +message UIObjSetImageSrcResponse { + bool success = 1; +} + +message DisplaySetRotationRequest { + uint32 rotation = 1; +} + +message DisplaySetRotationResponse { + bool success = 1; +} + +message UpdateLabelIfChangedRequest { + string obj_name = 1; + string new_text = 2; +} + +message UpdateLabelAndChangeVisibilityRequest { + string obj_name = 1; + string new_text = 2; +} + +message SwitchToScreenIfRequest { + string screen_name = 1; + repeated string should_switch = 2; +} + +message SwitchToScreenIfDifferentRequest { + string screen_name = 1; +} + +message Event { + string type = 1; + oneof data { + VideoState video_state = 2; + string indev_event = 3; + string rpc_event = 4; + VideoFrame video_frame = 5; + } +} + +message VideoFrame { + bytes frame = 1; + int64 duration_ns = 2; +} + diff --git a/internal/native/proto/native_grpc.pb.go b/internal/native/proto/native_grpc.pb.go new file mode 100644 index 00000000..abdd38bb --- /dev/null +++ b/internal/native/proto/native_grpc.pb.go @@ -0,0 +1,1326 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.21.12 +// source: internal/native/proto/native.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + NativeService_IsReady_FullMethodName = "/native.NativeService/IsReady" + NativeService_VideoSetSleepMode_FullMethodName = "/native.NativeService/VideoSetSleepMode" + NativeService_VideoGetSleepMode_FullMethodName = "/native.NativeService/VideoGetSleepMode" + NativeService_VideoSleepModeSupported_FullMethodName = "/native.NativeService/VideoSleepModeSupported" + NativeService_VideoSetQualityFactor_FullMethodName = "/native.NativeService/VideoSetQualityFactor" + NativeService_VideoGetQualityFactor_FullMethodName = "/native.NativeService/VideoGetQualityFactor" + NativeService_VideoSetEDID_FullMethodName = "/native.NativeService/VideoSetEDID" + NativeService_VideoGetEDID_FullMethodName = "/native.NativeService/VideoGetEDID" + NativeService_VideoLogStatus_FullMethodName = "/native.NativeService/VideoLogStatus" + NativeService_VideoStop_FullMethodName = "/native.NativeService/VideoStop" + NativeService_VideoStart_FullMethodName = "/native.NativeService/VideoStart" + NativeService_GetLVGLVersion_FullMethodName = "/native.NativeService/GetLVGLVersion" + NativeService_UIObjHide_FullMethodName = "/native.NativeService/UIObjHide" + NativeService_UIObjShow_FullMethodName = "/native.NativeService/UIObjShow" + NativeService_UISetVar_FullMethodName = "/native.NativeService/UISetVar" + NativeService_UIGetVar_FullMethodName = "/native.NativeService/UIGetVar" + NativeService_UIObjAddState_FullMethodName = "/native.NativeService/UIObjAddState" + NativeService_UIObjClearState_FullMethodName = "/native.NativeService/UIObjClearState" + NativeService_UIObjAddFlag_FullMethodName = "/native.NativeService/UIObjAddFlag" + NativeService_UIObjClearFlag_FullMethodName = "/native.NativeService/UIObjClearFlag" + NativeService_UIObjSetOpacity_FullMethodName = "/native.NativeService/UIObjSetOpacity" + NativeService_UIObjFadeIn_FullMethodName = "/native.NativeService/UIObjFadeIn" + NativeService_UIObjFadeOut_FullMethodName = "/native.NativeService/UIObjFadeOut" + NativeService_UIObjSetLabelText_FullMethodName = "/native.NativeService/UIObjSetLabelText" + NativeService_UIObjSetImageSrc_FullMethodName = "/native.NativeService/UIObjSetImageSrc" + NativeService_DisplaySetRotation_FullMethodName = "/native.NativeService/DisplaySetRotation" + NativeService_UpdateLabelIfChanged_FullMethodName = "/native.NativeService/UpdateLabelIfChanged" + NativeService_UpdateLabelAndChangeVisibility_FullMethodName = "/native.NativeService/UpdateLabelAndChangeVisibility" + NativeService_SwitchToScreenIf_FullMethodName = "/native.NativeService/SwitchToScreenIf" + NativeService_SwitchToScreenIfDifferent_FullMethodName = "/native.NativeService/SwitchToScreenIfDifferent" + NativeService_DoNotUseThisIsForCrashTestingOnly_FullMethodName = "/native.NativeService/DoNotUseThisIsForCrashTestingOnly" + NativeService_StreamEvents_FullMethodName = "/native.NativeService/StreamEvents" +) + +// NativeServiceClient is the client API for NativeService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type NativeServiceClient interface { + // Ready check + IsReady(ctx context.Context, in *IsReadyRequest, opts ...grpc.CallOption) (*IsReadyResponse, error) + // Video methods + VideoSetSleepMode(ctx context.Context, in *VideoSetSleepModeRequest, opts ...grpc.CallOption) (*Empty, error) + VideoGetSleepMode(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetSleepModeResponse, error) + VideoSleepModeSupported(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoSleepModeSupportedResponse, error) + VideoSetQualityFactor(ctx context.Context, in *VideoSetQualityFactorRequest, opts ...grpc.CallOption) (*Empty, error) + VideoGetQualityFactor(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetQualityFactorResponse, error) + VideoSetEDID(ctx context.Context, in *VideoSetEDIDRequest, opts ...grpc.CallOption) (*Empty, error) + VideoGetEDID(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetEDIDResponse, error) + VideoLogStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoLogStatusResponse, error) + VideoStop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) + VideoStart(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) + // UI methods + GetLVGLVersion(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetLVGLVersionResponse, error) + UIObjHide(ctx context.Context, in *UIObjHideRequest, opts ...grpc.CallOption) (*UIObjHideResponse, error) + UIObjShow(ctx context.Context, in *UIObjShowRequest, opts ...grpc.CallOption) (*UIObjShowResponse, error) + UISetVar(ctx context.Context, in *UISetVarRequest, opts ...grpc.CallOption) (*Empty, error) + UIGetVar(ctx context.Context, in *UIGetVarRequest, opts ...grpc.CallOption) (*UIGetVarResponse, error) + UIObjAddState(ctx context.Context, in *UIObjAddStateRequest, opts ...grpc.CallOption) (*UIObjAddStateResponse, error) + UIObjClearState(ctx context.Context, in *UIObjClearStateRequest, opts ...grpc.CallOption) (*UIObjClearStateResponse, error) + UIObjAddFlag(ctx context.Context, in *UIObjAddFlagRequest, opts ...grpc.CallOption) (*UIObjAddFlagResponse, error) + UIObjClearFlag(ctx context.Context, in *UIObjClearFlagRequest, opts ...grpc.CallOption) (*UIObjClearFlagResponse, error) + UIObjSetOpacity(ctx context.Context, in *UIObjSetOpacityRequest, opts ...grpc.CallOption) (*UIObjSetOpacityResponse, error) + UIObjFadeIn(ctx context.Context, in *UIObjFadeInRequest, opts ...grpc.CallOption) (*UIObjFadeInResponse, error) + UIObjFadeOut(ctx context.Context, in *UIObjFadeOutRequest, opts ...grpc.CallOption) (*UIObjFadeOutResponse, error) + UIObjSetLabelText(ctx context.Context, in *UIObjSetLabelTextRequest, opts ...grpc.CallOption) (*UIObjSetLabelTextResponse, error) + UIObjSetImageSrc(ctx context.Context, in *UIObjSetImageSrcRequest, opts ...grpc.CallOption) (*UIObjSetImageSrcResponse, error) + DisplaySetRotation(ctx context.Context, in *DisplaySetRotationRequest, opts ...grpc.CallOption) (*DisplaySetRotationResponse, error) + UpdateLabelIfChanged(ctx context.Context, in *UpdateLabelIfChangedRequest, opts ...grpc.CallOption) (*Empty, error) + UpdateLabelAndChangeVisibility(ctx context.Context, in *UpdateLabelAndChangeVisibilityRequest, opts ...grpc.CallOption) (*Empty, error) + SwitchToScreenIf(ctx context.Context, in *SwitchToScreenIfRequest, opts ...grpc.CallOption) (*Empty, error) + SwitchToScreenIfDifferent(ctx context.Context, in *SwitchToScreenIfDifferentRequest, opts ...grpc.CallOption) (*Empty, error) + // Testing + DoNotUseThisIsForCrashTestingOnly(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) + // Events stream + StreamEvents(ctx context.Context, in *Empty, opts ...grpc.CallOption) (NativeService_StreamEventsClient, error) +} + +type nativeServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNativeServiceClient(cc grpc.ClientConnInterface) NativeServiceClient { + return &nativeServiceClient{cc} +} + +func (c *nativeServiceClient) IsReady(ctx context.Context, in *IsReadyRequest, opts ...grpc.CallOption) (*IsReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IsReadyResponse) + err := c.cc.Invoke(ctx, NativeService_IsReady_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoSetSleepMode(ctx context.Context, in *VideoSetSleepModeRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_VideoSetSleepMode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoGetSleepMode(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetSleepModeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VideoGetSleepModeResponse) + err := c.cc.Invoke(ctx, NativeService_VideoGetSleepMode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoSleepModeSupported(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoSleepModeSupportedResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VideoSleepModeSupportedResponse) + err := c.cc.Invoke(ctx, NativeService_VideoSleepModeSupported_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoSetQualityFactor(ctx context.Context, in *VideoSetQualityFactorRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_VideoSetQualityFactor_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoGetQualityFactor(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetQualityFactorResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VideoGetQualityFactorResponse) + err := c.cc.Invoke(ctx, NativeService_VideoGetQualityFactor_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoSetEDID(ctx context.Context, in *VideoSetEDIDRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_VideoSetEDID_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoGetEDID(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetEDIDResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VideoGetEDIDResponse) + err := c.cc.Invoke(ctx, NativeService_VideoGetEDID_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoLogStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoLogStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VideoLogStatusResponse) + err := c.cc.Invoke(ctx, NativeService_VideoLogStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoStop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_VideoStop_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) VideoStart(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_VideoStart_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) GetLVGLVersion(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetLVGLVersionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetLVGLVersionResponse) + err := c.cc.Invoke(ctx, NativeService_GetLVGLVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjHide(ctx context.Context, in *UIObjHideRequest, opts ...grpc.CallOption) (*UIObjHideResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjHideResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjHide_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjShow(ctx context.Context, in *UIObjShowRequest, opts ...grpc.CallOption) (*UIObjShowResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjShowResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjShow_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UISetVar(ctx context.Context, in *UISetVarRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_UISetVar_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIGetVar(ctx context.Context, in *UIGetVarRequest, opts ...grpc.CallOption) (*UIGetVarResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIGetVarResponse) + err := c.cc.Invoke(ctx, NativeService_UIGetVar_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjAddState(ctx context.Context, in *UIObjAddStateRequest, opts ...grpc.CallOption) (*UIObjAddStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjAddStateResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjAddState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjClearState(ctx context.Context, in *UIObjClearStateRequest, opts ...grpc.CallOption) (*UIObjClearStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjClearStateResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjClearState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjAddFlag(ctx context.Context, in *UIObjAddFlagRequest, opts ...grpc.CallOption) (*UIObjAddFlagResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjAddFlagResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjAddFlag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjClearFlag(ctx context.Context, in *UIObjClearFlagRequest, opts ...grpc.CallOption) (*UIObjClearFlagResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjClearFlagResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjClearFlag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjSetOpacity(ctx context.Context, in *UIObjSetOpacityRequest, opts ...grpc.CallOption) (*UIObjSetOpacityResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjSetOpacityResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjSetOpacity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjFadeIn(ctx context.Context, in *UIObjFadeInRequest, opts ...grpc.CallOption) (*UIObjFadeInResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjFadeInResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjFadeIn_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjFadeOut(ctx context.Context, in *UIObjFadeOutRequest, opts ...grpc.CallOption) (*UIObjFadeOutResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjFadeOutResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjFadeOut_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjSetLabelText(ctx context.Context, in *UIObjSetLabelTextRequest, opts ...grpc.CallOption) (*UIObjSetLabelTextResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjSetLabelTextResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjSetLabelText_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UIObjSetImageSrc(ctx context.Context, in *UIObjSetImageSrcRequest, opts ...grpc.CallOption) (*UIObjSetImageSrcResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UIObjSetImageSrcResponse) + err := c.cc.Invoke(ctx, NativeService_UIObjSetImageSrc_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) DisplaySetRotation(ctx context.Context, in *DisplaySetRotationRequest, opts ...grpc.CallOption) (*DisplaySetRotationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DisplaySetRotationResponse) + err := c.cc.Invoke(ctx, NativeService_DisplaySetRotation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UpdateLabelIfChanged(ctx context.Context, in *UpdateLabelIfChangedRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_UpdateLabelIfChanged_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) UpdateLabelAndChangeVisibility(ctx context.Context, in *UpdateLabelAndChangeVisibilityRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_UpdateLabelAndChangeVisibility_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) SwitchToScreenIf(ctx context.Context, in *SwitchToScreenIfRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_SwitchToScreenIf_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) SwitchToScreenIfDifferent(ctx context.Context, in *SwitchToScreenIfDifferentRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_SwitchToScreenIfDifferent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) DoNotUseThisIsForCrashTestingOnly(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, NativeService_DoNotUseThisIsForCrashTestingOnly_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nativeServiceClient) StreamEvents(ctx context.Context, in *Empty, opts ...grpc.CallOption) (NativeService_StreamEventsClient, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &NativeService_ServiceDesc.Streams[0], NativeService_StreamEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &nativeServiceStreamEventsClient{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type NativeService_StreamEventsClient interface { + Recv() (*Event, error) + grpc.ClientStream +} + +type nativeServiceStreamEventsClient struct { + grpc.ClientStream +} + +func (x *nativeServiceStreamEventsClient) Recv() (*Event, error) { + m := new(Event) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// NativeServiceServer is the server API for NativeService service. +// All implementations must embed UnimplementedNativeServiceServer +// for forward compatibility +type NativeServiceServer interface { + // Ready check + IsReady(context.Context, *IsReadyRequest) (*IsReadyResponse, error) + // Video methods + VideoSetSleepMode(context.Context, *VideoSetSleepModeRequest) (*Empty, error) + VideoGetSleepMode(context.Context, *Empty) (*VideoGetSleepModeResponse, error) + VideoSleepModeSupported(context.Context, *Empty) (*VideoSleepModeSupportedResponse, error) + VideoSetQualityFactor(context.Context, *VideoSetQualityFactorRequest) (*Empty, error) + VideoGetQualityFactor(context.Context, *Empty) (*VideoGetQualityFactorResponse, error) + VideoSetEDID(context.Context, *VideoSetEDIDRequest) (*Empty, error) + VideoGetEDID(context.Context, *Empty) (*VideoGetEDIDResponse, error) + VideoLogStatus(context.Context, *Empty) (*VideoLogStatusResponse, error) + VideoStop(context.Context, *Empty) (*Empty, error) + VideoStart(context.Context, *Empty) (*Empty, error) + // UI methods + GetLVGLVersion(context.Context, *Empty) (*GetLVGLVersionResponse, error) + UIObjHide(context.Context, *UIObjHideRequest) (*UIObjHideResponse, error) + UIObjShow(context.Context, *UIObjShowRequest) (*UIObjShowResponse, error) + UISetVar(context.Context, *UISetVarRequest) (*Empty, error) + UIGetVar(context.Context, *UIGetVarRequest) (*UIGetVarResponse, error) + UIObjAddState(context.Context, *UIObjAddStateRequest) (*UIObjAddStateResponse, error) + UIObjClearState(context.Context, *UIObjClearStateRequest) (*UIObjClearStateResponse, error) + UIObjAddFlag(context.Context, *UIObjAddFlagRequest) (*UIObjAddFlagResponse, error) + UIObjClearFlag(context.Context, *UIObjClearFlagRequest) (*UIObjClearFlagResponse, error) + UIObjSetOpacity(context.Context, *UIObjSetOpacityRequest) (*UIObjSetOpacityResponse, error) + UIObjFadeIn(context.Context, *UIObjFadeInRequest) (*UIObjFadeInResponse, error) + UIObjFadeOut(context.Context, *UIObjFadeOutRequest) (*UIObjFadeOutResponse, error) + UIObjSetLabelText(context.Context, *UIObjSetLabelTextRequest) (*UIObjSetLabelTextResponse, error) + UIObjSetImageSrc(context.Context, *UIObjSetImageSrcRequest) (*UIObjSetImageSrcResponse, error) + DisplaySetRotation(context.Context, *DisplaySetRotationRequest) (*DisplaySetRotationResponse, error) + UpdateLabelIfChanged(context.Context, *UpdateLabelIfChangedRequest) (*Empty, error) + UpdateLabelAndChangeVisibility(context.Context, *UpdateLabelAndChangeVisibilityRequest) (*Empty, error) + SwitchToScreenIf(context.Context, *SwitchToScreenIfRequest) (*Empty, error) + SwitchToScreenIfDifferent(context.Context, *SwitchToScreenIfDifferentRequest) (*Empty, error) + // Testing + DoNotUseThisIsForCrashTestingOnly(context.Context, *Empty) (*Empty, error) + // Events stream + StreamEvents(*Empty, NativeService_StreamEventsServer) error + mustEmbedUnimplementedNativeServiceServer() +} + +// UnimplementedNativeServiceServer must be embedded to have forward compatible implementations. +type UnimplementedNativeServiceServer struct { +} + +func (UnimplementedNativeServiceServer) IsReady(context.Context, *IsReadyRequest) (*IsReadyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsReady not implemented") +} +func (UnimplementedNativeServiceServer) VideoSetSleepMode(context.Context, *VideoSetSleepModeRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoSetSleepMode not implemented") +} +func (UnimplementedNativeServiceServer) VideoGetSleepMode(context.Context, *Empty) (*VideoGetSleepModeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoGetSleepMode not implemented") +} +func (UnimplementedNativeServiceServer) VideoSleepModeSupported(context.Context, *Empty) (*VideoSleepModeSupportedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoSleepModeSupported not implemented") +} +func (UnimplementedNativeServiceServer) VideoSetQualityFactor(context.Context, *VideoSetQualityFactorRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoSetQualityFactor not implemented") +} +func (UnimplementedNativeServiceServer) VideoGetQualityFactor(context.Context, *Empty) (*VideoGetQualityFactorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoGetQualityFactor not implemented") +} +func (UnimplementedNativeServiceServer) VideoSetEDID(context.Context, *VideoSetEDIDRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoSetEDID not implemented") +} +func (UnimplementedNativeServiceServer) VideoGetEDID(context.Context, *Empty) (*VideoGetEDIDResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoGetEDID not implemented") +} +func (UnimplementedNativeServiceServer) VideoLogStatus(context.Context, *Empty) (*VideoLogStatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoLogStatus not implemented") +} +func (UnimplementedNativeServiceServer) VideoStop(context.Context, *Empty) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoStop not implemented") +} +func (UnimplementedNativeServiceServer) VideoStart(context.Context, *Empty) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method VideoStart not implemented") +} +func (UnimplementedNativeServiceServer) GetLVGLVersion(context.Context, *Empty) (*GetLVGLVersionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLVGLVersion not implemented") +} +func (UnimplementedNativeServiceServer) UIObjHide(context.Context, *UIObjHideRequest) (*UIObjHideResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjHide not implemented") +} +func (UnimplementedNativeServiceServer) UIObjShow(context.Context, *UIObjShowRequest) (*UIObjShowResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjShow not implemented") +} +func (UnimplementedNativeServiceServer) UISetVar(context.Context, *UISetVarRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UISetVar not implemented") +} +func (UnimplementedNativeServiceServer) UIGetVar(context.Context, *UIGetVarRequest) (*UIGetVarResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIGetVar not implemented") +} +func (UnimplementedNativeServiceServer) UIObjAddState(context.Context, *UIObjAddStateRequest) (*UIObjAddStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjAddState not implemented") +} +func (UnimplementedNativeServiceServer) UIObjClearState(context.Context, *UIObjClearStateRequest) (*UIObjClearStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjClearState not implemented") +} +func (UnimplementedNativeServiceServer) UIObjAddFlag(context.Context, *UIObjAddFlagRequest) (*UIObjAddFlagResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjAddFlag not implemented") +} +func (UnimplementedNativeServiceServer) UIObjClearFlag(context.Context, *UIObjClearFlagRequest) (*UIObjClearFlagResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjClearFlag not implemented") +} +func (UnimplementedNativeServiceServer) UIObjSetOpacity(context.Context, *UIObjSetOpacityRequest) (*UIObjSetOpacityResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjSetOpacity not implemented") +} +func (UnimplementedNativeServiceServer) UIObjFadeIn(context.Context, *UIObjFadeInRequest) (*UIObjFadeInResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjFadeIn not implemented") +} +func (UnimplementedNativeServiceServer) UIObjFadeOut(context.Context, *UIObjFadeOutRequest) (*UIObjFadeOutResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjFadeOut not implemented") +} +func (UnimplementedNativeServiceServer) UIObjSetLabelText(context.Context, *UIObjSetLabelTextRequest) (*UIObjSetLabelTextResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjSetLabelText not implemented") +} +func (UnimplementedNativeServiceServer) UIObjSetImageSrc(context.Context, *UIObjSetImageSrcRequest) (*UIObjSetImageSrcResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UIObjSetImageSrc not implemented") +} +func (UnimplementedNativeServiceServer) DisplaySetRotation(context.Context, *DisplaySetRotationRequest) (*DisplaySetRotationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisplaySetRotation not implemented") +} +func (UnimplementedNativeServiceServer) UpdateLabelIfChanged(context.Context, *UpdateLabelIfChangedRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateLabelIfChanged not implemented") +} +func (UnimplementedNativeServiceServer) UpdateLabelAndChangeVisibility(context.Context, *UpdateLabelAndChangeVisibilityRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateLabelAndChangeVisibility not implemented") +} +func (UnimplementedNativeServiceServer) SwitchToScreenIf(context.Context, *SwitchToScreenIfRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SwitchToScreenIf not implemented") +} +func (UnimplementedNativeServiceServer) SwitchToScreenIfDifferent(context.Context, *SwitchToScreenIfDifferentRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SwitchToScreenIfDifferent not implemented") +} +func (UnimplementedNativeServiceServer) DoNotUseThisIsForCrashTestingOnly(context.Context, *Empty) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoNotUseThisIsForCrashTestingOnly not implemented") +} +func (UnimplementedNativeServiceServer) StreamEvents(*Empty, NativeService_StreamEventsServer) error { + return status.Errorf(codes.Unimplemented, "method StreamEvents not implemented") +} +func (UnimplementedNativeServiceServer) mustEmbedUnimplementedNativeServiceServer() {} + +// UnsafeNativeServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NativeServiceServer will +// result in compilation errors. +type UnsafeNativeServiceServer interface { + mustEmbedUnimplementedNativeServiceServer() +} + +func RegisterNativeServiceServer(s grpc.ServiceRegistrar, srv NativeServiceServer) { + s.RegisterService(&NativeService_ServiceDesc, srv) +} + +func _NativeService_IsReady_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IsReadyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).IsReady(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_IsReady_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).IsReady(ctx, req.(*IsReadyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoSetSleepMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VideoSetSleepModeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoSetSleepMode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoSetSleepMode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoSetSleepMode(ctx, req.(*VideoSetSleepModeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoGetSleepMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoGetSleepMode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoGetSleepMode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoGetSleepMode(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoSleepModeSupported_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoSleepModeSupported(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoSleepModeSupported_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoSleepModeSupported(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoSetQualityFactor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VideoSetQualityFactorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoSetQualityFactor(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoSetQualityFactor_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoSetQualityFactor(ctx, req.(*VideoSetQualityFactorRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoGetQualityFactor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoGetQualityFactor(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoGetQualityFactor_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoGetQualityFactor(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoSetEDID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VideoSetEDIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoSetEDID(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoSetEDID_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoSetEDID(ctx, req.(*VideoSetEDIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoGetEDID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoGetEDID(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoGetEDID_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoGetEDID(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoLogStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoLogStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoLogStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoLogStatus(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoStop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoStop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoStop_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoStop(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_VideoStart_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).VideoStart(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_VideoStart_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).VideoStart(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_GetLVGLVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).GetLVGLVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_GetLVGLVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).GetLVGLVersion(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjHide_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjHideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjHide(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjHide_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjHide(ctx, req.(*UIObjHideRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjShow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjShowRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjShow(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjShow_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjShow(ctx, req.(*UIObjShowRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UISetVar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UISetVarRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UISetVar(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UISetVar_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UISetVar(ctx, req.(*UISetVarRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIGetVar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIGetVarRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIGetVar(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIGetVar_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIGetVar(ctx, req.(*UIGetVarRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjAddState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjAddStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjAddState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjAddState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjAddState(ctx, req.(*UIObjAddStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjClearState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjClearStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjClearState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjClearState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjClearState(ctx, req.(*UIObjClearStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjAddFlag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjAddFlagRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjAddFlag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjAddFlag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjAddFlag(ctx, req.(*UIObjAddFlagRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjClearFlag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjClearFlagRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjClearFlag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjClearFlag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjClearFlag(ctx, req.(*UIObjClearFlagRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjSetOpacity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjSetOpacityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjSetOpacity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjSetOpacity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjSetOpacity(ctx, req.(*UIObjSetOpacityRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjFadeIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjFadeInRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjFadeIn(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjFadeIn_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjFadeIn(ctx, req.(*UIObjFadeInRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjFadeOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjFadeOutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjFadeOut(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjFadeOut_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjFadeOut(ctx, req.(*UIObjFadeOutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjSetLabelText_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjSetLabelTextRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjSetLabelText(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjSetLabelText_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjSetLabelText(ctx, req.(*UIObjSetLabelTextRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UIObjSetImageSrc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UIObjSetImageSrcRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UIObjSetImageSrc(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UIObjSetImageSrc_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UIObjSetImageSrc(ctx, req.(*UIObjSetImageSrcRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_DisplaySetRotation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisplaySetRotationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).DisplaySetRotation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_DisplaySetRotation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).DisplaySetRotation(ctx, req.(*DisplaySetRotationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UpdateLabelIfChanged_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateLabelIfChangedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UpdateLabelIfChanged(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UpdateLabelIfChanged_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UpdateLabelIfChanged(ctx, req.(*UpdateLabelIfChangedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_UpdateLabelAndChangeVisibility_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateLabelAndChangeVisibilityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).UpdateLabelAndChangeVisibility(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_UpdateLabelAndChangeVisibility_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).UpdateLabelAndChangeVisibility(ctx, req.(*UpdateLabelAndChangeVisibilityRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_SwitchToScreenIf_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchToScreenIfRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).SwitchToScreenIf(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_SwitchToScreenIf_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).SwitchToScreenIf(ctx, req.(*SwitchToScreenIfRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_SwitchToScreenIfDifferent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchToScreenIfDifferentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).SwitchToScreenIfDifferent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_SwitchToScreenIfDifferent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).SwitchToScreenIfDifferent(ctx, req.(*SwitchToScreenIfDifferentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_DoNotUseThisIsForCrashTestingOnly_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NativeServiceServer).DoNotUseThisIsForCrashTestingOnly(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NativeService_DoNotUseThisIsForCrashTestingOnly_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NativeServiceServer).DoNotUseThisIsForCrashTestingOnly(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NativeService_StreamEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(NativeServiceServer).StreamEvents(m, &nativeServiceStreamEventsServer{ServerStream: stream}) +} + +type NativeService_StreamEventsServer interface { + Send(*Event) error + grpc.ServerStream +} + +type nativeServiceStreamEventsServer struct { + grpc.ServerStream +} + +func (x *nativeServiceStreamEventsServer) Send(m *Event) error { + return x.ServerStream.SendMsg(m) +} + +// NativeService_ServiceDesc is the grpc.ServiceDesc for NativeService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NativeService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "native.NativeService", + HandlerType: (*NativeServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "IsReady", + Handler: _NativeService_IsReady_Handler, + }, + { + MethodName: "VideoSetSleepMode", + Handler: _NativeService_VideoSetSleepMode_Handler, + }, + { + MethodName: "VideoGetSleepMode", + Handler: _NativeService_VideoGetSleepMode_Handler, + }, + { + MethodName: "VideoSleepModeSupported", + Handler: _NativeService_VideoSleepModeSupported_Handler, + }, + { + MethodName: "VideoSetQualityFactor", + Handler: _NativeService_VideoSetQualityFactor_Handler, + }, + { + MethodName: "VideoGetQualityFactor", + Handler: _NativeService_VideoGetQualityFactor_Handler, + }, + { + MethodName: "VideoSetEDID", + Handler: _NativeService_VideoSetEDID_Handler, + }, + { + MethodName: "VideoGetEDID", + Handler: _NativeService_VideoGetEDID_Handler, + }, + { + MethodName: "VideoLogStatus", + Handler: _NativeService_VideoLogStatus_Handler, + }, + { + MethodName: "VideoStop", + Handler: _NativeService_VideoStop_Handler, + }, + { + MethodName: "VideoStart", + Handler: _NativeService_VideoStart_Handler, + }, + { + MethodName: "GetLVGLVersion", + Handler: _NativeService_GetLVGLVersion_Handler, + }, + { + MethodName: "UIObjHide", + Handler: _NativeService_UIObjHide_Handler, + }, + { + MethodName: "UIObjShow", + Handler: _NativeService_UIObjShow_Handler, + }, + { + MethodName: "UISetVar", + Handler: _NativeService_UISetVar_Handler, + }, + { + MethodName: "UIGetVar", + Handler: _NativeService_UIGetVar_Handler, + }, + { + MethodName: "UIObjAddState", + Handler: _NativeService_UIObjAddState_Handler, + }, + { + MethodName: "UIObjClearState", + Handler: _NativeService_UIObjClearState_Handler, + }, + { + MethodName: "UIObjAddFlag", + Handler: _NativeService_UIObjAddFlag_Handler, + }, + { + MethodName: "UIObjClearFlag", + Handler: _NativeService_UIObjClearFlag_Handler, + }, + { + MethodName: "UIObjSetOpacity", + Handler: _NativeService_UIObjSetOpacity_Handler, + }, + { + MethodName: "UIObjFadeIn", + Handler: _NativeService_UIObjFadeIn_Handler, + }, + { + MethodName: "UIObjFadeOut", + Handler: _NativeService_UIObjFadeOut_Handler, + }, + { + MethodName: "UIObjSetLabelText", + Handler: _NativeService_UIObjSetLabelText_Handler, + }, + { + MethodName: "UIObjSetImageSrc", + Handler: _NativeService_UIObjSetImageSrc_Handler, + }, + { + MethodName: "DisplaySetRotation", + Handler: _NativeService_DisplaySetRotation_Handler, + }, + { + MethodName: "UpdateLabelIfChanged", + Handler: _NativeService_UpdateLabelIfChanged_Handler, + }, + { + MethodName: "UpdateLabelAndChangeVisibility", + Handler: _NativeService_UpdateLabelAndChangeVisibility_Handler, + }, + { + MethodName: "SwitchToScreenIf", + Handler: _NativeService_SwitchToScreenIf_Handler, + }, + { + MethodName: "SwitchToScreenIfDifferent", + Handler: _NativeService_SwitchToScreenIfDifferent_Handler, + }, + { + MethodName: "DoNotUseThisIsForCrashTestingOnly", + Handler: _NativeService_DoNotUseThisIsForCrashTestingOnly_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamEvents", + Handler: _NativeService_StreamEvents_Handler, + ServerStreams: true, + }, + }, + Metadata: "internal/native/proto/native.proto", +} diff --git a/internal/native/proxy.go b/internal/native/proxy.go new file mode 100644 index 00000000..01f0bb11 --- /dev/null +++ b/internal/native/proxy.go @@ -0,0 +1,688 @@ +package native + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/jetkvm/kvm/internal/supervisor" + "github.com/jetkvm/kvm/internal/utils" + "github.com/rs/zerolog" +) + +const ( + maxFrameSize = 1920 * 1080 / 2 + defaultMaxRestartAttempts uint = 5 +) + +type nativeProxyOptions struct { + Disable bool `env:"JETKVM_NATIVE_DISABLE"` + SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"` + AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"` + DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"` + DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"` + CtrlUnixSocket string `env:"JETKVM_NATIVE_CTRL_UNIX_SOCKET"` + VideoStreamUnixSocket string `env:"JETKVM_NATIVE_VIDEO_STREAM_UNIX_SOCKET"` + BinaryPath string `env:"JETKVM_NATIVE_BINARY_PATH"` + LoggerLevel zerolog.Level `env:"JETKVM_NATIVE_LOGGER_LEVEL"` + HandshakeMessage string `env:"JETKVM_NATIVE_HANDSHAKE_MESSAGE"` + MaxRestartAttempts uint + + OnVideoFrameReceived func(frame []byte, duration time.Duration) + OnIndevEvent func(event string) + OnRpcEvent func(event string) + OnVideoStateChange func(state VideoState) + OnNativeRestart func() +} + +func randomId(binaryLength int) string { + s := make([]byte, binaryLength) + _, err := rand.Read(s) + if err != nil { + nativeLogger.Error().Err(err).Msg("failed to generate random ID") + return strings.Repeat("0", binaryLength*2) // return all zeros if error + } + return hex.EncodeToString(s) +} + +func (n *NativeOptions) toProxyOptions() *nativeProxyOptions { + // random 16 bytes hex string + handshakeMessage := randomId(16) + maxRestartAttempts := defaultMaxRestartAttempts + if n.MaxRestartAttempts > 0 { + maxRestartAttempts = n.MaxRestartAttempts + } + return &nativeProxyOptions{ + SystemVersion: n.SystemVersion, + AppVersion: n.AppVersion, + DisplayRotation: n.DisplayRotation, + DefaultQualityFactor: n.DefaultQualityFactor, + OnVideoFrameReceived: n.OnVideoFrameReceived, + OnIndevEvent: n.OnIndevEvent, + OnRpcEvent: n.OnRpcEvent, + OnVideoStateChange: n.OnVideoStateChange, + OnNativeRestart: n.OnNativeRestart, + HandshakeMessage: handshakeMessage, + MaxRestartAttempts: maxRestartAttempts, + } +} + +func (p *nativeProxyOptions) toNativeOptions() *NativeOptions { + return &NativeOptions{ + SystemVersion: p.SystemVersion, + AppVersion: p.AppVersion, + DisplayRotation: p.DisplayRotation, + DefaultQualityFactor: p.DefaultQualityFactor, + } +} + +// cmdWrapper wraps exec.Cmd to implement processCmd interface +type cmdWrapper struct { + *exec.Cmd + stdoutHandler *nativeProxyStdoutHandler +} + +func (c *cmdWrapper) GetProcess() interface { + Kill() error + Signal(sig interface{}) error +} { + return &processWrapper{Process: c.Process} +} + +type processWrapper struct { + *os.Process +} + +func (p *processWrapper) Signal(sig interface{}) error { + if sig == nil { + // Check if process is alive by sending signal 0 + return p.Process.Signal(os.Signal(syscall.Signal(0))) + } + if s, ok := sig.(os.Signal); ok { + return p.Process.Signal(s) + } + return fmt.Errorf("invalid signal type") +} + +// NativeProxy is a proxy that communicates with a separate native process +type NativeProxy struct { + nativeUnixSocket string + videoStreamUnixSocket string + videoStreamListener net.Listener + binaryPath string + + startMu sync.Mutex // mutex for the start process (context and isStopped) + ctx context.Context + cancel context.CancelFunc + + client *GRPCClient + clientMu sync.RWMutex // mutex for the client + + cmd *cmdWrapper + cmdMu sync.Mutex // mutex for the cmd + + logger *zerolog.Logger + options *nativeProxyOptions + restarts uint + stopped bool +} + +// NewNativeProxy creates a new NativeProxy that spawns a separate process +func NewNativeProxy(opts NativeOptions) (*NativeProxy, error) { + proxyOptions := opts.toProxyOptions() + proxyOptions.VideoStreamUnixSocket = fmt.Sprintf("@jetkvm/native/video-stream/%s", randomId(4)) + + // Get the current executable path to spawn itself + exePath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %w", err) + } + + proxy := &NativeProxy{ + nativeUnixSocket: proxyOptions.CtrlUnixSocket, + videoStreamUnixSocket: proxyOptions.VideoStreamUnixSocket, + binaryPath: exePath, + logger: nativeLogger, + options: proxyOptions, + restarts: 0, + } + + return proxy, nil +} + +func (p *NativeProxy) startVideoStreamListener() error { + if p.videoStreamListener != nil { + return nil + } + + logger := p.logger.With().Str("socketPath", p.videoStreamUnixSocket).Logger() + listener, err := net.Listen("unixpacket", p.videoStreamUnixSocket) + if err != nil { + logger.Warn().Err(err).Msg("failed to start video stream listener") + return fmt.Errorf("failed to start video stream listener: %w", err) + } + logger.Info().Msg("video stream listener started") + p.videoStreamListener = listener + + go func() { + for { + conn, err := listener.Accept() + if err != nil { + logger.Warn().Err(err).Msg("failed to accept socket") + continue + } + + logger.Info().Msg("video stream socket accepted") + go p.handleVideoFrame(conn) + } + }() + + return nil +} + +type nativeProxyStdoutHandler struct { + mu *sync.Mutex + handshakeCh chan bool + handshakeMessage string + handshakeDone bool +} + +func (w *nativeProxyStdoutHandler) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + if !w.handshakeDone && strings.Contains(string(p), w.handshakeMessage) { + w.handshakeDone = true + w.handshakeCh <- true + return len(p), nil + } + + os.Stdout.Write(p) + + return len(p), nil +} + +func (p *NativeProxy) toProcessCommand() (*cmdWrapper, error) { + // generate a new random ID for the gRPC socket on each restart + // sometimes the socket is not closed properly when the process exits + // this is a workaround to avoid the issue + p.nativeUnixSocket = fmt.Sprintf("jetkvm/native/grpc/%s", randomId(4)) + p.options.CtrlUnixSocket = p.nativeUnixSocket + + envArgs, err := utils.MarshalEnv(p.options) + if err != nil { + return nil, fmt.Errorf("failed to marshal environment variables: %w", err) + } + + cmd := &cmdWrapper{ + Cmd: exec.Command( + p.binaryPath, + "-subcomponent=native", + ), + stdoutHandler: &nativeProxyStdoutHandler{ + mu: &sync.Mutex{}, + handshakeCh: make(chan bool), + handshakeMessage: p.options.HandshakeMessage, + }, + } + cmd.Stdout = cmd.stdoutHandler + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGTERM, + } + // Set environment variable to indicate native process mode + cmd.Env = append( + os.Environ(), + envArgs..., + ) + + return cmd, nil +} + +func (p *NativeProxy) handleVideoFrame(conn net.Conn) { + defer conn.Close() + + inboundPacket := make([]byte, maxFrameSize) + lastFrame := time.Now() + + for { + n, err := conn.Read(inboundPacket) + if err != nil { + p.logger.Warn().Err(err).Msg("failed to read video frame from socket") + break + } + now := time.Now() + sinceLastFrame := now.Sub(lastFrame) + lastFrame = now + p.options.OnVideoFrameReceived(inboundPacket[:n], sinceLastFrame) + } +} + +// it should be only called by start() method, as it isn't thread-safe +func (p *NativeProxy) setUpGRPCClient() error { + // wait until handshake completed + select { + case <-p.cmd.stdoutHandler.handshakeCh: + p.logger.Info().Msg("handshake completed") + case <-time.After(10 * time.Second): + return fmt.Errorf("handshake not completed within 10 seconds") + } + + logger := p.logger.With().Str("socketPath", "@"+p.nativeUnixSocket).Logger() + client, err := NewGRPCClient(grpcClientOptions{ + SocketPath: p.nativeUnixSocket, + Logger: &logger, + OnIndevEvent: p.options.OnIndevEvent, + OnRpcEvent: p.options.OnRpcEvent, + OnVideoStateChange: p.options.OnVideoStateChange, + }) + + logger.Info().Msg("created gRPC client") + if err != nil { + return fmt.Errorf("failed to create gRPC client: %w", err) + } + p.client = client + + // Wait for ready signal from the native process + if err := p.client.WaitReady(); err != nil { + // Clean up if ready failed + if p.cmd.Process != nil { + _ = p.cmd.Process.Kill() + _ = p.cmd.Wait() + } + return fmt.Errorf("failed to wait for ready: %w", err) + } + + // Call on native restart callback if it exists and restarts are greater than 0 + if p.options.OnNativeRestart != nil && p.restarts > 0 { + go p.options.OnNativeRestart() + } + + return nil +} + +func (p *NativeProxy) doStart() error { + p.cmdMu.Lock() + defer p.cmdMu.Unlock() + + // lock OS thread to prevent the process from being moved to a different thread + // see also https://go.dev/issue/27505 + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cmd, err := p.toProcessCommand() + if err != nil { + return fmt.Errorf("failed to create process: %w", err) + } + + p.cmd = cmd + + if err := p.cmd.Start(); err != nil { + return fmt.Errorf("failed to start native process: %w", err) + } + + // here we'll replace the logger with a new one that includes the process ID + // there's no need to lock the mutex here as the side effect is acceptable + newLogger := p.logger.With().Int("pid", p.cmd.Process.Pid).Logger() + p.logger = &newLogger + + p.logger.Info().Msg("native process started") + + if err := p.setUpGRPCClient(); err != nil { + return fmt.Errorf("failed to set up gRPC client: %w", err) + } + + return nil +} + +// Start starts the native process +func (p *NativeProxy) Start() error { + p.startMu.Lock() + defer p.startMu.Unlock() + + p.ctx, p.cancel = context.WithCancel(context.Background()) + + if p.stopped { + return fmt.Errorf("proxy is stopped") + } + + if err := p.startVideoStreamListener(); err != nil { + return fmt.Errorf("failed to start video stream listener: %w", err) + } + + if err := p.doStart(); err != nil { + return fmt.Errorf("failed to start native process: %w", err) + } + + go p.monitorProcess() + + return nil +} + +// monitorProcess monitors the native process and restarts it if it crashes +func (p *NativeProxy) monitorProcess() { + for { + if p.stopped { + return + } + + select { + case <-p.ctx.Done(): + p.logger.Trace().Msg("context done, stopping monitor process [before wait]") + return + default: + } + + p.cmdMu.Lock() + err := fmt.Errorf("native process not started") + if p.cmd != nil { + err = p.cmd.Wait() + } + p.cmdMu.Unlock() + + if p.stopped { + return + } + + select { + case <-p.ctx.Done(): + p.logger.Trace().Msg("context done, stopping monitor process [after wait]") + return + default: + } + + p.logger.Warn().Err(err).Msg("native process exited, restarting ...") + + // Wait a bit before restarting + time.Sleep(1 * time.Second) + + // Restart the process + if err := p.restartProcess(); err != nil { + p.logger.Error().Err(err).Msg("failed to restart native process") + // Wait longer before retrying + time.Sleep(5 * time.Second) + continue + } + } +} + +// restartProcess restarts the native process +func (p *NativeProxy) restartProcess() error { + p.restarts++ + logger := p.logger.With().Uint("attempt", p.restarts).Uint("maxAttempts", p.options.MaxRestartAttempts).Logger() + + if p.restarts >= p.options.MaxRestartAttempts { + logger.Fatal().Msgf("max restart attempts reached, exiting: %s", supervisor.FailsafeReasonVideoMaxRestartAttemptsReached) + return fmt.Errorf("max restart attempts reached") + } + + if p.stopped { + return fmt.Errorf("proxy is stopped") + } + + // Close old client + p.clientMu.Lock() + if p.client != nil { + if err := p.client.Close(); err != nil { + logger.Warn().Err(err).Msg("failed to close gRPC client") + } + p.client = nil // set to nil to avoid closing it again + } + p.clientMu.Unlock() + logger.Info().Msg("gRPC client closed") + + logger.Info().Msg("attempting to restart native process") + if err := p.doStart(); err != nil { + logger.Error().Err(err).Msg("failed to start native process") + return fmt.Errorf("failed to start native process: %w", err) + } + + logger.Info().Msg("native process restarted successfully") + return nil +} + +// Stop stops the native process +func (p *NativeProxy) Stop() error { + p.startMu.Lock() + defer p.startMu.Unlock() + + p.stopped = true + + if p.cmd.Process != nil { + if err := p.cmd.Process.Kill(); err != nil { + return fmt.Errorf("failed to kill native process: %w", err) + } + _ = p.cmd.Wait() + } + + return nil +} + +func zeroValue[V string | bool | float64]() V { + var v V + return v +} + +func nativeProxyClientExec[K comparable, V string | bool | float64](p *NativeProxy, fn func(*GRPCClient) (V, error)) (V, error) { + p.clientMu.RLock() + defer p.clientMu.RUnlock() + + if p.client == nil { + return zeroValue[V](), fmt.Errorf("gRPC client not initialized") + } + + return fn(p.client) +} + +func nativeProxyClientExecWithoutArgument(p *NativeProxy, fn func(*GRPCClient) error) error { + p.clientMu.RLock() + defer p.clientMu.RUnlock() + + if p.client == nil { + return fmt.Errorf("gRPC client not initialized") + } + + return fn(p.client) +} + +// Implement all Native methods by forwarding to gRPC client +func (p *NativeProxy) VideoSetSleepMode(enabled bool) error { + return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + return client.VideoSetSleepMode(enabled) + }) +} + +func (p *NativeProxy) VideoGetSleepMode() (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.VideoGetSleepMode() + }) +} + +func (p *NativeProxy) VideoSleepModeSupported() bool { + result, _ := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.VideoSleepModeSupported(), nil + }) + return result +} + +func (p *NativeProxy) VideoSetQualityFactor(factor float64) error { + return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + return client.VideoSetQualityFactor(factor) + }) +} + +func (p *NativeProxy) VideoGetQualityFactor() (float64, error) { + return nativeProxyClientExec[float64](p, func(client *GRPCClient) (float64, error) { + return client.VideoGetQualityFactor() + }) +} + +func (p *NativeProxy) VideoSetEDID(edid string) error { + return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + return client.VideoSetEDID(edid) + }) +} + +func (p *NativeProxy) VideoGetEDID() (string, error) { + return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { + return client.VideoGetEDID() + }) +} + +func (p *NativeProxy) VideoLogStatus() (string, error) { + return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { + return client.VideoLogStatus() + }) +} + +func (p *NativeProxy) VideoStop() error { + return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + return client.VideoStop() + }) +} + +func (p *NativeProxy) VideoStart() error { + return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + return client.VideoStart() + }) +} + +func (p *NativeProxy) GetLVGLVersion() (string, error) { + return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { + return client.GetLVGLVersion() + }) +} + +func (p *NativeProxy) UIObjHide(objName string) (bool, error) { + result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjHide(objName) + }) + return result, err +} + +func (p *NativeProxy) UIObjShow(objName string) (bool, error) { + result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjShow(objName) + }) + return result, err +} + +func (p *NativeProxy) UISetVar(name string, value string) { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.UISetVar(name, value) + return nil + }) +} + +func (p *NativeProxy) UIGetVar(name string) string { + result, _ := nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { + return client.UIGetVar(name), nil + }) + return result +} + +func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjAddState(objName, state) + }) +} + +func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjClearState(objName, state) + }) +} + +func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjAddFlag(objName, flag) + }) +} + +func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjClearFlag(objName, flag) + }) +} + +func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjFadeIn(objName, duration) + }) +} + +func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjFadeOut(objName, duration) + }) +} + +func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjSetLabelText(objName, text) + }) +} + +func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjSetImageSrc(objName, image) + }) +} + +func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.UIObjSetOpacity(objName, opacity) + }) +} + +func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) { + return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { + return client.DisplaySetRotation(rotation) + }) +} + +func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.UpdateLabelIfChanged(objName, newText) + return nil + }) +} + +func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.UpdateLabelAndChangeVisibility(objName, newText) + return nil + }) +} + +func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.SwitchToScreenIf(screenName, shouldSwitch) + return nil + }) +} + +func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.SwitchToScreenIfDifferent(screenName) + return nil + }) +} + +func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() { + _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { + client.DoNotUseThisIsForCrashTestingOnly() + return nil + }) +} diff --git a/internal/native/server.go b/internal/native/server.go new file mode 100644 index 00000000..ae983159 --- /dev/null +++ b/internal/native/server.go @@ -0,0 +1,137 @@ +package native + +import ( + "context" + "fmt" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/caarlos0/env/v11" + "github.com/erikdubbelboer/gspt" + "github.com/rs/zerolog" +) + +// Native Process +// stdout - exchange messages with the parent process +// stderr - logging and error messages + +var ( + procPrefix string = "jetkvm: [native]" + lastProcTitle string +) + +const ( + DebugModeFile = "/userdata/jetkvm/.native-debug-mode" +) + +func setProcTitle(status string) { + lastProcTitle = status + if status != "" { + status = " " + status + } + title := fmt.Sprintf("%s%s", procPrefix, status) + gspt.SetProcTitle(title) +} + +func monitorCrashSignal(ctx context.Context, logger *zerolog.Logger, nativeInstance NativeInterface) { + logger.Info().Msg("DEBUG mode: will crash the process on SIGHUP signal") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGHUP) + + for { + select { + case sig := <-sigChan: + logger.Info().Str("signal", sig.String()).Msg("received termination signal") + nativeInstance.DoNotUseThisIsForCrashTestingOnly() + case <-ctx.Done(): + logger.Info().Msg("context done, stopping monitor process") + return + } + } +} + +// RunNativeProcess runs the native process mode +func RunNativeProcess(binaryName string) { + appCtx, appCtxCancel := context.WithCancel(context.Background()) + defer appCtxCancel() + + logger := nativeLogger.With().Int("pid", os.Getpid()).Logger() + setProcTitle("starting") + + // Parse native options + var proxyOptions nativeProxyOptions + if err := env.Parse(&proxyOptions); err != nil { + logger.Fatal().Err(err).Msg("failed to parse native proxy options") + } + + // Connect to video stream socket + conn, err := net.Dial("unixpacket", proxyOptions.VideoStreamUnixSocket) + if err != nil { + logger.Fatal().Err(err).Msg("failed to connect to video stream socket") + } + logger.Info().Str("videoStreamSocketPath", proxyOptions.VideoStreamUnixSocket).Msg("connected to video stream socket") + + nativeOptions := proxyOptions.toNativeOptions() + nativeOptions.OnVideoFrameReceived = func(frame []byte, duration time.Duration) { + _, err := conn.Write(frame) + if err != nil { + logger.Fatal().Err(err).Msg("failed to write frame to video stream socket") + } + } + + // Create native instance + nativeInstance := NewNative(*nativeOptions) + gspt.SetProcTitle("jetkvm: [native] initializing") + + // Start native instance + if err := nativeInstance.Start(); err != nil { + logger.Fatal().Err(err).Msg("failed to start native instance") + } + + grpcLogger := logger.With().Str("socketPath", fmt.Sprintf("@%v", proxyOptions.CtrlUnixSocket)).Logger() + setProcTitle("starting gRPC server") + // Create gRPC server + grpcServer := NewGRPCServer(nativeInstance, &grpcLogger) + + logger.Info().Msg("starting gRPC server") + // Start gRPC server + server, lis, err := StartGRPCServer(grpcServer, fmt.Sprintf("@%v", proxyOptions.CtrlUnixSocket), &logger) + if err != nil { + logger.Fatal().Err(err).Msg("failed to start gRPC server") + } + setProcTitle("ready") + + if _, err := os.Stat(DebugModeFile); err == nil { + logger.Info().Msg("DEBUG mode: enabled") + go monitorCrashSignal(appCtx, &logger, nativeInstance) + } + + // Signal that we're ready by writing handshake message to stdout (for parent to read) + // Stdout.Write is used to avoid buffering the message + _, err = os.Stdout.Write([]byte(proxyOptions.HandshakeMessage + "\n")) + if err != nil { + logger.Fatal().Err(err).Msg("failed to write handshake message to stdout") + } + + // Set up signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) + + // Wait for signal + sig := <-sigChan + logger.Info(). + Str("signal", sig.String()). + Msg("received termination signal") + + // Graceful shutdown might stuck forever, + // we will use Stop() instead to force quit the gRPC server, + // we can implement a graceful shutdown with a timeout in the future if needed + server.Stop() + lis.Close() + + logger.Info().Msg("native process exiting") +} diff --git a/internal/network/types/config.go b/internal/network/types/config.go index 364f8609..33afbcc7 100644 --- a/internal/network/types/config.go +++ b/internal/network/types/config.go @@ -30,7 +30,7 @@ type MDNSListenOptions struct { // NetworkConfig represents the complete network configuration for an interface type NetworkConfig struct { - DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"jetdhcpc"` + DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"udhcpc"` Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"` diff --git a/internal/ota/app.go b/internal/ota/app.go new file mode 100644 index 00000000..55caa8e8 --- /dev/null +++ b/internal/ota/app.go @@ -0,0 +1,45 @@ +package ota + +import ( + "context" + "time" +) + +const ( + appUpdatePath = "/userdata/jetkvm/jetkvm_app.update" +) + +// DO NOT call it directly, it's not thread safe +// Mutex is currently held by the caller, e.g. doUpdate +func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) error { + l := s.l.With().Str("path", appUpdatePath).Logger() + + if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil { + return s.componentUpdateError("Error downloading app update", err, &l) + } + + downloadFinished := time.Now() + appUpdate.downloadFinishedAt = downloadFinished + appUpdate.downloadProgress = 1 + s.triggerComponentUpdateState("app", appUpdate) + + if err := s.verifyFile( + appUpdatePath, + appUpdate.hash, + &appUpdate.verificationProgress, + ); err != nil { + return s.componentUpdateError("Error verifying app update hash", err, &l) + } + verifyFinished := time.Now() + appUpdate.verifiedAt = verifyFinished + appUpdate.verificationProgress = 1 + appUpdate.updatedAt = verifyFinished + appUpdate.updateProgress = 1 + s.triggerComponentUpdateState("app", appUpdate) + + l.Info().Msg("App update downloaded") + + s.rebootNeeded = true + + return nil +} diff --git a/internal/ota/errors.go b/internal/ota/errors.go new file mode 100644 index 00000000..a1d0b4c5 --- /dev/null +++ b/internal/ota/errors.go @@ -0,0 +1,24 @@ +package ota + +import ( + "errors" + "fmt" + + "github.com/rs/zerolog" +) + +var ( + // ErrVersionNotFound is returned when the specified version is not found + ErrVersionNotFound = errors.New("specified version not found") +) + +func (s *State) componentUpdateError(prefix string, err error, l *zerolog.Logger) error { + if l == nil { + l = s.l + } + l.Error().Err(err).Msg(prefix) + s.error = fmt.Sprintf("%s: %v", prefix, err) + s.updating = false + s.triggerStateUpdate() + return err +} diff --git a/internal/ota/ota.go b/internal/ota/ota.go new file mode 100644 index 00000000..52cbb6e2 --- /dev/null +++ b/internal/ota/ota.go @@ -0,0 +1,429 @@ +package ota + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptrace" + "net/url" + "time" + + "github.com/rs/zerolog" +) + +// HttpClient is the interface for the HTTP client +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// UpdateReleaseAPIEndpoint updates the release API endpoint +func (s *State) UpdateReleaseAPIEndpoint(endpoint string) { + s.releaseAPIEndpoint = endpoint +} + +// GetReleaseAPIEndpoint returns the release API endpoint +func (s *State) GetReleaseAPIEndpoint() string { + return s.releaseAPIEndpoint +} + +// getUpdateURL returns the update URL for the given parameters +func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) { + updateURL, err := url.Parse(s.releaseAPIEndpoint) + if err != nil { + return "", fmt.Errorf("error parsing update metadata URL: %w", err), false + } + + isCustomVersion := false + + query := updateURL.Query() + query.Set("deviceId", params.DeviceID) + query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease)) + + // set the custom versions if they are specified + for component, constraint := range params.Components { + if constraint == "" { + continue + } + + query.Set(component+"Version", constraint) + isCustomVersion = true + } + + updateURL.RawQuery = query.Encode() + + return updateURL.String(), nil, isCustomVersion +} + +// newHTTPRequestWithTrace creates a new HTTP request with a trace logger +// TODO: use OTEL instead of doing this manually +func (s *State) newHTTPRequestWithTrace(ctx context.Context, method, url string, body io.Reader, logger func() *zerolog.Event) (*http.Request, error) { + localCtx := ctx + if s.l.GetLevel() <= zerolog.TraceLevel { + if logger == nil { + logger = func() *zerolog.Event { return s.l.Trace() } + } + + l := func() *zerolog.Event { return logger().Str("url", url).Str("method", method) } + localCtx = httptrace.WithClientTrace(localCtx, &httptrace.ClientTrace{ + GetConn: func(hostPort string) { l().Str("hostPort", hostPort).Msg("[conn] starting to create conn") }, + GotConn: func(info httptrace.GotConnInfo) { l().Interface("info", info).Msg("[conn] connection established") }, + PutIdleConn: func(err error) { l().Err(err).Msg("[conn] connection returned to idle pool") }, + GotFirstResponseByte: func() { l().Msg("[resp] first response byte received") }, + Got100Continue: func() { l().Msg("[resp] 100 continue received") }, + DNSStart: func(info httptrace.DNSStartInfo) { l().Interface("info", info).Msg("[dns] starting to look up dns") }, + DNSDone: func(info httptrace.DNSDoneInfo) { l().Interface("info", info).Msg("[dns] done looking up dns") }, + ConnectStart: func(network, addr string) { + l().Str("network", network).Str("addr", addr).Msg("[tcp] starting tcp connection") + }, + ConnectDone: func(network, addr string, err error) { + l().Str("network", network).Str("addr", addr).Err(err).Msg("[tcp] tcp connection created") + }, + TLSHandshakeStart: func() { l().Msg("[tls] handshake started") }, + TLSHandshakeDone: func(state tls.ConnectionState, err error) { + l(). + Str("tlsVersion", tls.VersionName(state.Version)). + Str("cipherSuite", tls.CipherSuiteName(state.CipherSuite)). + Str("negotiatedProtocol", state.NegotiatedProtocol). + Str("serverName", state.ServerName). + Err(err).Msg("[tls] handshake done") + }, + }) + } + + return http.NewRequestWithContext(localCtx, method, url, body) +} + +func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*UpdateMetadata, error) { + metadata := &UpdateMetadata{} + + logger := s.l.With().Logger() + if params.RequestID != "" { + logger = logger.With().Str("requestID", params.RequestID).Logger() + } + t := time.Now() + traceLogger := func() *zerolog.Event { + return logger.Trace().Dur("duration", time.Since(t)) + } + + url, err, isCustomVersion := s.getUpdateURL(params) + traceLogger().Err(err). + Msg("fetchUpdateMetadata: getUpdateURL") + if err != nil { + return nil, fmt.Errorf("error getting update URL: %w", err) + } + + traceLogger(). + Str("url", url). + Msg("fetching update metadata") + + req, err := s.newHTTPRequestWithTrace(ctx, "GET", url, nil, traceLogger) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + client := s.client() + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + traceLogger(). + Int("status", resp.StatusCode). + Msg("fetchUpdateMetadata: response") + + if isCustomVersion && resp.StatusCode == http.StatusNotFound { + return nil, ErrVersionNotFound + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + err = json.NewDecoder(resp.Body).Decode(metadata) + if err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + traceLogger(). + Msg("fetchUpdateMetadata: completed") + + return metadata, nil +} + +func (s *State) triggerStateUpdate() { + s.onStateUpdate(s.ToRPCState()) +} + +func (s *State) triggerComponentUpdateState(component string, update *componentUpdateStatus) { + s.componentUpdateStatuses[component] = *update + s.triggerStateUpdate() +} + +// TryUpdate tries to update the given components +// if the update is already in progress, it returns an error +func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error { + locked := s.mu.TryLock() + if !locked { + return fmt.Errorf("update already in progress") + } + + return s.doUpdate(ctx, params) +} + +// before calling doUpdate, the caller must have locked the mutex +// otherwise a runtime error will occur +func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { + defer s.mu.Unlock() + + scopedLogger := s.l.With(). + Interface("params", params). + Logger() + + scopedLogger.Info().Msg("checking for updates") + if s.updating { + return fmt.Errorf("update already in progress") + } + + s.updating = true + s.triggerStateUpdate() + + if len(params.Components) == 0 { + params.Components = defaultComponents + } + + _, shouldUpdateApp := params.Components["app"] + _, shouldUpdateSystem := params.Components["system"] + + if !shouldUpdateApp && !shouldUpdateSystem { + return s.componentUpdateError( + "Update aborted: no components were specified to update. Requested components: ", + fmt.Errorf("%v", params.Components), + &scopedLogger, + ) + } + + appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, params) + if err != nil { + return s.componentUpdateError("Error checking for updates", err, &scopedLogger) + } + + s.metadataFetchedAt = time.Now() + s.triggerStateUpdate() + + if shouldUpdateApp && appUpdate.available { + appUpdate.pending = true + s.updating = true + s.triggerComponentUpdateState("app", appUpdate) + } + + if shouldUpdateSystem && systemUpdate.available { + systemUpdate.pending = true + s.updating = true + s.triggerComponentUpdateState("system", systemUpdate) + } + + scopedLogger.Trace().Bool("pending", appUpdate.pending).Msg("Checking for app update") + + if appUpdate.pending { + scopedLogger.Info(). + Str("url", appUpdate.url). + Str("hash", appUpdate.hash). + Msg("App update available") + + if err := s.updateApp(ctx, appUpdate); err != nil { + return s.componentUpdateError("Error updating app", err, &scopedLogger) + } + } else { + scopedLogger.Info().Msg("App is up to date") + } + + scopedLogger.Trace().Bool("pending", systemUpdate.pending).Msg("Checking for system update") + + if systemUpdate.pending { + if err := s.updateSystem(ctx, systemUpdate); err != nil { + return s.componentUpdateError("Error updating system", err, &scopedLogger) + } + } else { + scopedLogger.Info().Msg("System is up to date") + } + + if s.rebootNeeded { + if appUpdate.customVersionUpdate || systemUpdate.customVersionUpdate { + scopedLogger.Info().Msg("disabling auto-update due to custom version update") + // If they are explicitly updating a custom version, we assume they want to disable auto-update + if _, err := s.setAutoUpdate(false); err != nil { + scopedLogger.Warn().Err(err).Msg("Failed to disable auto-update") + } + } + + scopedLogger.Info().Msg("System Rebooting due to OTA update") + + redirectUrl := "/settings/general/update" + + if params.ResetConfig { + scopedLogger.Info().Msg("Resetting config") + if err := s.resetConfig(); err != nil { + return s.componentUpdateError("Error resetting config", err, &scopedLogger) + } + redirectUrl = "/welcome" + } + + postRebootAction := &PostRebootAction{ + HealthCheck: "/device/status", + RedirectTo: redirectUrl, + } + + // REBOOT_REDIRECT_DELAY_MS is 7 seconds in the UI, + // it means that healthCheckUrl will be called after 7 seconds that we send willReboot JSONRPC event + // so we need to reboot it within 7 seconds to avoid it being called before the device is rebooted + if err := s.reboot(true, postRebootAction, 5*time.Second); err != nil { + return s.componentUpdateError("Error requesting reboot", err, &scopedLogger) + } + } + + // We don't need set the updating flag to false here. Either it will; + // - set to false by the componentUpdateError function + // - device will reboot + return nil +} + +// UpdateParams represents the parameters for the update +type UpdateParams struct { + DeviceID string `json:"deviceID"` + Components map[string]string `json:"components"` + IncludePreRelease bool `json:"includePreRelease"` + ResetConfig bool `json:"resetConfig"` + // RequestID is a unique identifier for the update request + // When it's set, detailed trace logs will be enabled (if the log level is Trace) + RequestID string +} + +// getUpdateStatus gets the update status for the given components +// and updates the componentUpdateStatuses map +func (s *State) getUpdateStatus( + ctx context.Context, + params UpdateParams, +) ( + appUpdate *componentUpdateStatus, + systemUpdate *componentUpdateStatus, + err error, +) { + appUpdate = &componentUpdateStatus{} + systemUpdate = &componentUpdateStatus{} + + if currentAppUpdate, ok := s.componentUpdateStatuses["app"]; ok { + appUpdate = ¤tAppUpdate + } + + if currentSystemUpdate, ok := s.componentUpdateStatuses["system"]; ok { + systemUpdate = ¤tSystemUpdate + } + + err = s.checkUpdateStatus(ctx, params, appUpdate, systemUpdate) + if err != nil { + return nil, nil, err + } + + s.componentUpdateStatuses["app"] = *appUpdate + s.componentUpdateStatuses["system"] = *systemUpdate + + return appUpdate, systemUpdate, nil +} + +// checkUpdateStatus checks the update status for the given components +func (s *State) checkUpdateStatus( + ctx context.Context, + params UpdateParams, + appUpdateStatus *componentUpdateStatus, + systemUpdateStatus *componentUpdateStatus, +) error { + // get the local versions + systemVersionLocal, appVersionLocal, err := s.getLocalVersion() + if err != nil { + return fmt.Errorf("error getting local version: %w", err) + } + appUpdateStatus.localVersion = appVersionLocal.String() + systemUpdateStatus.localVersion = systemVersionLocal.String() + + logger := s.l.With().Logger() + if params.RequestID != "" { + logger = logger.With().Str("requestID", params.RequestID).Logger() + } + t := time.Now() + + logger.Trace(). + Str("appVersionLocal", appVersionLocal.String()). + Str("systemVersionLocal", systemVersionLocal.String()). + Dur("duration", time.Since(t)). + Msg("checkUpdateStatus: getLocalVersion") + + // fetch the remote metadata + remoteMetadata, err := s.fetchUpdateMetadata(ctx, params) + if err != nil { + if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound { + err = ErrVersionNotFound + } else { + err = fmt.Errorf("error checking for updates: %w", err) + } + return err + } + + logger.Trace(). + Interface("remoteMetadata", remoteMetadata). + Dur("duration", time.Since(t)). + Msg("checkUpdateStatus: fetchUpdateMetadata") + + // parse the remote metadata to the componentUpdateStatuses + if err := remoteMetadataToComponentStatus( + remoteMetadata, + "app", + appUpdateStatus, + params, + ); err != nil { + return fmt.Errorf("error parsing remote app version: %w", err) + } + + if err := remoteMetadataToComponentStatus( + remoteMetadata, + "system", + systemUpdateStatus, + params, + ); err != nil { + return fmt.Errorf("error parsing remote system version: %w", err) + } + + if s.l.GetLevel() <= zerolog.TraceLevel { + appUpdateStatus.getZerologLogger(&logger).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [app]") + systemUpdateStatus.getZerologLogger(&logger).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [system]") + } + + logger.Trace(). + Dur("duration", time.Since(t)). + Msg("checkUpdateStatus: completed") + + return nil +} + +// GetUpdateStatus returns the current update status (for backwards compatibility) +func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) { + // if no components are specified, use the default components + // we should remove this once app router feature is released + if len(params.Components) == 0 { + params.Components = defaultComponents + } + + appUpdateStatus := componentUpdateStatus{} + systemUpdateStatus := componentUpdateStatus{} + err := s.checkUpdateStatus(ctx, params, &appUpdateStatus, &systemUpdateStatus) + if err != nil { + return nil, fmt.Errorf("error getting update status: %w", err) + } + + return toUpdateStatus(&appUpdateStatus, &systemUpdateStatus, ""), nil +} diff --git a/internal/ota/ota_test.go b/internal/ota/ota_test.go new file mode 100644 index 00000000..2c8ce661 --- /dev/null +++ b/internal/ota/ota_test.go @@ -0,0 +1,261 @@ +package ota + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/gwatts/rootcerts" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +//go:embed testdata/ota +var testDataFS embed.FS + +const pseudoDeviceID = "golang-test" +const releaseAPIEndpoint = "https://api.jetkvm.com/releases" + +type testData struct { + Name string `json:"name"` + WithoutCerts bool `json:"withoutCerts"` + RemoteMetadata []struct { + Code int `json:"code"` + Params map[string]string `json:"params"` + Data UpdateMetadata `json:"data"` + } `json:"remoteMetadata"` + LocalMetadata struct { + SystemVersion string `json:"systemVersion"` + AppVersion string `json:"appVersion"` + } `json:"localMetadata"` + UpdateParams UpdateParams `json:"updateParams"` + Expected struct { + System bool `json:"system"` + App bool `json:"app"` + Error string `json:"error,omitempty"` + } `json:"expected"` +} + +func (d *testData) ToFixtures(t *testing.T) map[string]mockData { + fixtures := make(map[string]mockData) + for _, resp := range d.RemoteMetadata { + url, err := url.Parse(releaseAPIEndpoint) + if err != nil { + t.Fatalf("failed to parse release API endpoint: %v", err) + } + query := url.Query() + query.Set("deviceId", pseudoDeviceID) + for key, value := range resp.Params { + query.Set(key, value) + } + url.RawQuery = query.Encode() + fixtures[url.String()] = mockData{ + Metadata: &resp.Data, + StatusCode: resp.Code, + } + } + return fixtures +} + +func (d *testData) ToUpdateParams() UpdateParams { + d.UpdateParams.DeviceID = pseudoDeviceID + return d.UpdateParams +} + +func loadTestData(t *testing.T, filename string) *testData { + f, err := testDataFS.ReadFile(filepath.Join("testdata", "ota", filename)) + if err != nil { + t.Fatalf("failed to read test data file %s: %v", filename, err) + } + + var testData testData + if err := json.Unmarshal(f, &testData); err != nil { + t.Fatalf("failed to unmarshal test data file %s: %v", filename, err) + } + + return &testData +} + +type mockData struct { + Metadata *UpdateMetadata + StatusCode int +} + +type mockHTTPClient struct { + DoFunc func(req *http.Request) (*http.Response, error) + Fixtures map[string]mockData +} + +func compareURLs(a *url.URL, b *url.URL) bool { + if a.String() == b.String() { + return true + } + if a.Host != b.Host || a.Scheme != b.Scheme || a.Path != b.Path { + return false + } + + // do a quick check to see if the query parameters are the same + queryA := a.Query() + queryB := b.Query() + if len(queryA) != len(queryB) { + return false + } + for key := range queryA { + if queryA.Get(key) != queryB.Get(key) { + return false + } + } + for key := range queryB { + if queryA.Get(key) != queryB.Get(key) { + return false + } + } + return true +} + +func (m *mockHTTPClient) getFixture(expectedURL *url.URL) *mockData { + for u, fixture := range m.Fixtures { + fixtureURL, err := url.Parse(u) + if err != nil { + continue + } + if compareURLs(fixtureURL, expectedURL) { + return &fixture + } + } + return nil +} + +func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { + fixture := m.getFixture(req.URL) + if fixture == nil { + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewBufferString("")), + }, fmt.Errorf("no fixture found for URL: %s", req.URL.String()) + } + + resp := &http.Response{ + StatusCode: fixture.StatusCode, + } + + jsonData, err := json.Marshal(fixture.Metadata) + if err != nil { + return nil, fmt.Errorf("error marshalling metadata: %w", err) + } + + resp.Body = io.NopCloser(bytes.NewBufferString(string(jsonData))) + return resp, nil +} + +func newMockHTTPClient(fixtures map[string]mockData) *mockHTTPClient { + return &mockHTTPClient{ + Fixtures: fixtures, + } +} + +func newOtaState(d *testData, t *testing.T) *State { + pseudoGetLocalVersion := func() (systemVersion *semver.Version, appVersion *semver.Version, err error) { + appVersion = semver.MustParse(d.LocalMetadata.AppVersion) + systemVersion = semver.MustParse(d.LocalMetadata.SystemVersion) + return systemVersion, appVersion, nil + } + + traceLevel := zerolog.InfoLevel + + if os.Getenv("TEST_LOG_TRACE") == "1" { + traceLevel = zerolog.TraceLevel + } + logger := zerolog.New(os.Stdout).Level(traceLevel) + otaState := NewState(Options{ + SkipConfirmSystem: true, + Logger: &logger, + ReleaseAPIEndpoint: releaseAPIEndpoint, + GetHTTPClient: func() HttpClient { + if d.RemoteMetadata != nil { + return newMockHTTPClient(d.ToFixtures(t)) + } + transport := http.DefaultTransport.(*http.Transport).Clone() + if !d.WithoutCerts { + transport.TLSClientConfig = &tls.Config{RootCAs: rootcerts.ServerCertPool()} + } else { + transport.TLSClientConfig = &tls.Config{RootCAs: x509.NewCertPool()} + } + client := &http.Client{ + Transport: transport, + } + return client + }, + GetLocalVersion: pseudoGetLocalVersion, + HwReboot: func(force bool, postRebootAction *PostRebootAction, delay time.Duration) error { return nil }, + ResetConfig: func() error { return nil }, + OnStateUpdate: func(state *RPCState) {}, + OnProgressUpdate: func(progress float32) {}, + }) + return otaState +} + +func testUsingJson(t *testing.T, filename string) { + td := loadTestData(t, filename) + otaState := newOtaState(td, t) + info, err := otaState.GetUpdateStatus(context.Background(), td.ToUpdateParams()) + if err != nil { + if td.Expected.Error != "" { + assert.ErrorContains(t, err, td.Expected.Error) + } else { + t.Fatalf("failed to get update status: %v", err) + } + } + + if td.Expected.System { + assert.True(t, info.SystemUpdateAvailable, fmt.Sprintf("system update should available, but reason: %s", info.SystemUpdateAvailableReason)) + } else { + assert.False(t, info.SystemUpdateAvailable, fmt.Sprintf("system update should not be available, but reason: %s", info.SystemUpdateAvailableReason)) + } + + if td.Expected.App { + assert.True(t, info.AppUpdateAvailable, fmt.Sprintf("app update should available, but reason: %s", info.AppUpdateAvailableReason)) + } else { + assert.False(t, info.AppUpdateAvailable, fmt.Sprintf("app update should not be available, but reason: %s", info.AppUpdateAvailableReason)) + } +} + +func TestCheckUpdateComponentsSystemOnlyUpgrade(t *testing.T) { + testUsingJson(t, "system_only_upgrade.json") +} + +func TestCheckUpdateComponentsSystemOnlyDowngrade(t *testing.T) { + testUsingJson(t, "system_only_downgrade.json") +} + +func TestCheckUpdateComponentsAppOnlyUpgrade(t *testing.T) { + testUsingJson(t, "app_only_upgrade.json") +} + +func TestCheckUpdateComponentsAppOnlyDowngrade(t *testing.T) { + testUsingJson(t, "app_only_downgrade.json") +} + +func TestCheckUpdateComponentsSystemBothUpgrade(t *testing.T) { + testUsingJson(t, "both_upgrade.json") +} + +func TestCheckUpdateComponentsSystemBothDowngrade(t *testing.T) { + testUsingJson(t, "both_downgrade.json") +} + +func TestCheckUpdateComponentsNoComponents(t *testing.T) { + testUsingJson(t, "no_components.json") +} diff --git a/internal/ota/rpc.go b/internal/ota/rpc.go new file mode 100644 index 00000000..30f132ea --- /dev/null +++ b/internal/ota/rpc.go @@ -0,0 +1,167 @@ +package ota + +import ( + "fmt" + "reflect" + "strings" + "time" + + "github.com/Masterminds/semver/v3" +) + +// to make the field names consistent with the RPCState struct +var componentFieldMap = map[string]string{ + "app": "App", + "system": "System", +} + +// RPCState represents the current OTA state for the RPC API +type RPCState struct { + Updating bool `json:"updating"` + Error string `json:"error,omitempty"` + MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"` + AppUpdatePending bool `json:"appUpdatePending"` + SystemUpdatePending bool `json:"systemUpdatePending"` + AppDownloadProgress *float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar + AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"` + SystemDownloadProgress *float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar + SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"` + AppVerificationProgress *float32 `json:"appVerificationProgress,omitempty"` + AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"` + SystemVerificationProgress *float32 `json:"systemVerificationProgress,omitempty"` + SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"` + AppUpdateProgress *float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar + AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"` + SystemUpdateProgress *float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement + SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"` +} + +func setTimeIfNotZero(rpcVal reflect.Value, i int, status time.Time) { + if !status.IsZero() { + rpcVal.Field(i).Set(reflect.ValueOf(&status)) + } +} + +func setFloat32IfNotZero(rpcVal reflect.Value, i int, status float32) { + if status != 0 { + rpcVal.Field(i).Set(reflect.ValueOf(&status)) + } +} + +// applyComponentStatusToRPCState uses reflection to map componentUpdateStatus fields to RPCState +func applyComponentStatusToRPCState(component string, status componentUpdateStatus, rpcState *RPCState) { + prefix := componentFieldMap[component] + if prefix == "" { + return + } + + rpcVal := reflect.ValueOf(rpcState).Elem() + + // it's really inefficient, but hey we do not need to use this often + // componentUpdateStatus is for internal use only, and all fields are unexported + for i := 0; i < rpcVal.NumField(); i++ { + rpcFieldName, hasPrefix := strings.CutPrefix(rpcVal.Type().Field(i).Name, prefix) + if !hasPrefix { + continue + } + + switch rpcFieldName { + case "DownloadProgress": + setFloat32IfNotZero(rpcVal, i, status.downloadProgress) + case "DownloadFinishedAt": + setTimeIfNotZero(rpcVal, i, status.downloadFinishedAt) + case "VerificationProgress": + setFloat32IfNotZero(rpcVal, i, status.verificationProgress) + case "VerifiedAt": + setTimeIfNotZero(rpcVal, i, status.verifiedAt) + case "UpdateProgress": + setFloat32IfNotZero(rpcVal, i, status.updateProgress) + case "UpdatedAt": + setTimeIfNotZero(rpcVal, i, status.updatedAt) + case "UpdatePending": + rpcVal.Field(i).SetBool(status.pending) + default: + continue + } + } +} + +// ToRPCState converts the State to the RPCState +func (s *State) ToRPCState() *RPCState { + r := &RPCState{ + Updating: s.updating, + Error: s.error, + MetadataFetchedAt: &s.metadataFetchedAt, + } + + for component, status := range s.componentUpdateStatuses { + applyComponentStatusToRPCState(component, status, r) + } + + return r +} + +func remoteMetadataToComponentStatus( + remoteMetadata *UpdateMetadata, + component string, + componentStatus *componentUpdateStatus, + params UpdateParams, +) error { + prefix := componentFieldMap[component] + if prefix == "" { + return fmt.Errorf("unknown component: %s", component) + } + + remoteMetadataVal := reflect.ValueOf(remoteMetadata).Elem() + for i := 0; i < remoteMetadataVal.NumField(); i++ { + fieldName, hasPrefix := strings.CutPrefix(remoteMetadataVal.Type().Field(i).Name, prefix) + if !hasPrefix { + continue + } + + switch fieldName { + case "URL": + componentStatus.url = remoteMetadataVal.Field(i).String() + case "Hash": + componentStatus.hash = remoteMetadataVal.Field(i).String() + case "Version": + componentStatus.version = remoteMetadataVal.Field(i).String() + default: + // fmt.Printf("unknown field %s", fieldName) + continue + } + } + + localVersion, err := semver.NewVersion(componentStatus.localVersion) + if err != nil { + return fmt.Errorf("error parsing local version: %w", err) + } + + remoteVersion, err := semver.NewVersion(componentStatus.version) + if err != nil { + return fmt.Errorf("error parsing remote version: %w", err) + } + componentStatus.available = remoteVersion.GreaterThan(localVersion) + componentStatus.availableReason = fmt.Sprintf("remote version %s is greater than local version %s", remoteVersion.String(), localVersion.String()) + + // Handle pre-release updates + if remoteVersion.Prerelease() != "" && params.IncludePreRelease && componentStatus.available { + componentStatus.availableReason += " (pre-release)" + } + + // If a custom version is specified, use it to determine if the update is available + constraint, componentExists := params.Components[component] + // we don't need to check again if it's already available + if componentExists && constraint != "" { + componentStatus.available = componentStatus.version != componentStatus.localVersion + if componentStatus.available { + componentStatus.availableReason = fmt.Sprintf("custom version %s is not equal to local version %s", constraint, componentStatus.localVersion) + componentStatus.customVersionUpdate = true + } + } else if !componentExists { + componentStatus.available = false + componentStatus.availableReason = "component not specified in update parameters" + } + + return nil +} diff --git a/internal/ota/state.go b/internal/ota/state.go new file mode 100644 index 00000000..2bb7055e --- /dev/null +++ b/internal/ota/state.go @@ -0,0 +1,215 @@ +package ota + +import ( + "sync" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/rs/zerolog" +) + +var ( + availableComponents = []string{"app", "system"} + defaultComponents = map[string]string{ + "app": "", + "system": "", + } +) + +// UpdateMetadata represents the metadata of an update +type UpdateMetadata struct { + AppVersion string `json:"appVersion"` + AppURL string `json:"appUrl"` + AppHash string `json:"appHash"` + SystemVersion string `json:"systemVersion"` + SystemURL string `json:"systemUrl"` + SystemHash string `json:"systemHash"` +} + +// LocalMetadata represents the local metadata of the system +type LocalMetadata struct { + AppVersion string `json:"appVersion"` + SystemVersion string `json:"systemVersion"` +} + +// UpdateStatus represents the current update status +type UpdateStatus struct { + Local *LocalMetadata `json:"local"` + Remote *UpdateMetadata `json:"remote"` + SystemUpdateAvailable bool `json:"systemUpdateAvailable"` + AppUpdateAvailable bool `json:"appUpdateAvailable"` + WillDisableAutoUpdate bool `json:"willDisableAutoUpdate"` + + // only available for debugging and won't be exported + SystemUpdateAvailableReason string `json:"-"` + AppUpdateAvailableReason string `json:"-"` + + // for backwards compatibility + Error string `json:"error,omitempty"` +} + +// PostRebootAction represents the action to be taken after a reboot +// It is used to redirect the user to a specific page after a reboot +type PostRebootAction struct { + HealthCheck string `json:"healthCheck"` // The health check URL to call after the reboot + RedirectTo string `json:"redirectTo"` // The URL to redirect to after the reboot +} + +// componentUpdateStatus represents the status of a component update +type componentUpdateStatus struct { + pending bool + available bool + availableReason string // why the component is available or not available + customVersionUpdate bool + version string + localVersion string + url string + hash string + downloadProgress float32 + downloadFinishedAt time.Time + verificationProgress float32 + verifiedAt time.Time + updateProgress float32 + updatedAt time.Time + dependsOn []string +} + +func (c *componentUpdateStatus) getZerologLogger(l *zerolog.Logger) *zerolog.Logger { + logger := l.With(). + Bool("pending", c.pending). + Bool("available", c.available). + Str("availableReason", c.availableReason). + Str("version", c.version). + Str("localVersion", c.localVersion). + Str("url", c.url). + Str("hash", c.hash). + Float32("downloadProgress", c.downloadProgress). + Time("downloadFinishedAt", c.downloadFinishedAt). + Float32("verificationProgress", c.verificationProgress). + Time("verifiedAt", c.verifiedAt). + Float32("updateProgress", c.updateProgress). + Time("updatedAt", c.updatedAt). + Strs("dependsOn", c.dependsOn). + Logger() + return &logger +} + +// HwRebootFunc is a function that reboots the hardware +type HwRebootFunc func(force bool, postRebootAction *PostRebootAction, delay time.Duration) error + +// ResetConfigFunc is a function that resets the config +type ResetConfigFunc func() error + +// SetAutoUpdateFunc is a function that sets the auto-update state +type SetAutoUpdateFunc func(enabled bool) (bool, error) + +// GetHTTPClientFunc is a function that returns the HTTP client +type GetHTTPClientFunc func() HttpClient + +// OnStateUpdateFunc is a function that updates the state of the OTA +type OnStateUpdateFunc func(state *RPCState) + +// OnProgressUpdateFunc is a function that updates the progress of the OTA +type OnProgressUpdateFunc func(progress float32) + +// GetLocalVersionFunc is a function that returns the local version of the system and app +type GetLocalVersionFunc func() (systemVersion *semver.Version, appVersion *semver.Version, err error) + +// State represents the current OTA state for the UI +type State struct { + releaseAPIEndpoint string + l *zerolog.Logger + mu sync.Mutex + updating bool + error string + metadataFetchedAt time.Time + rebootNeeded bool + componentUpdateStatuses map[string]componentUpdateStatus + client GetHTTPClientFunc + reboot HwRebootFunc + getLocalVersion GetLocalVersionFunc + onStateUpdate OnStateUpdateFunc + resetConfig ResetConfigFunc + setAutoUpdate SetAutoUpdateFunc +} + +func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, error string) *UpdateStatus { + return &UpdateStatus{ + Local: &LocalMetadata{ + AppVersion: appUpdate.localVersion, + SystemVersion: systemUpdate.localVersion, + }, + Remote: &UpdateMetadata{ + AppVersion: appUpdate.version, + AppURL: appUpdate.url, + AppHash: appUpdate.hash, + SystemVersion: systemUpdate.version, + SystemURL: systemUpdate.url, + SystemHash: systemUpdate.hash, + }, + SystemUpdateAvailable: systemUpdate.available, + SystemUpdateAvailableReason: systemUpdate.availableReason, + AppUpdateAvailable: appUpdate.available, + AppUpdateAvailableReason: appUpdate.availableReason, + WillDisableAutoUpdate: appUpdate.customVersionUpdate || systemUpdate.customVersionUpdate, + Error: error, + } +} + +// ToUpdateStatus converts the State to the UpdateStatus +func (s *State) ToUpdateStatus() *UpdateStatus { + appUpdate, ok := s.componentUpdateStatuses["app"] + if !ok { + return nil + } + + systemUpdate, ok := s.componentUpdateStatuses["system"] + if !ok { + return nil + } + + return toUpdateStatus(&appUpdate, &systemUpdate, s.error) +} + +// IsUpdatePending returns true if an update is pending +func (s *State) IsUpdatePending() bool { + return s.updating +} + +// Options represents the options for the OTA state +type Options struct { + Logger *zerolog.Logger + GetHTTPClient GetHTTPClientFunc + GetLocalVersion GetLocalVersionFunc + OnStateUpdate OnStateUpdateFunc + OnProgressUpdate OnProgressUpdateFunc + HwReboot HwRebootFunc + ReleaseAPIEndpoint string + ResetConfig ResetConfigFunc + SkipConfirmSystem bool + SetAutoUpdate SetAutoUpdateFunc +} + +// NewState creates a new OTA state +func NewState(opts Options) *State { + components := make(map[string]componentUpdateStatus) + for _, component := range availableComponents { + components[component] = componentUpdateStatus{} + } + + s := &State{ + l: opts.Logger, + client: opts.GetHTTPClient, + reboot: opts.HwReboot, + onStateUpdate: opts.OnStateUpdate, + getLocalVersion: opts.GetLocalVersion, + componentUpdateStatuses: components, + releaseAPIEndpoint: opts.ReleaseAPIEndpoint, + resetConfig: opts.ResetConfig, + setAutoUpdate: opts.SetAutoUpdate, + } + if !opts.SkipConfirmSystem { + go s.confirmCurrentSystem() + } + return s +} diff --git a/internal/ota/sys.go b/internal/ota/sys.go new file mode 100644 index 00000000..6a5002f6 --- /dev/null +++ b/internal/ota/sys.go @@ -0,0 +1,101 @@ +package ota + +import ( + "bytes" + "context" + "os/exec" + "time" +) + +const ( + systemUpdatePath = "/userdata/jetkvm/update_system.tar" +) + +// DO NOT call it directly, it's not thread safe +// Mutex is currently held by the caller, e.g. doUpdate +func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateStatus) error { + l := s.l.With().Str("path", systemUpdatePath).Logger() + + if err := s.downloadFile(ctx, systemUpdatePath, systemUpdate.url, "system"); err != nil { + return s.componentUpdateError("Error downloading system update", err, &l) + } + + downloadFinished := time.Now() + systemUpdate.downloadFinishedAt = downloadFinished + systemUpdate.downloadProgress = 1 + s.triggerComponentUpdateState("system", systemUpdate) + + if err := s.verifyFile( + systemUpdatePath, + systemUpdate.hash, + &systemUpdate.verificationProgress, + ); err != nil { + return s.componentUpdateError("Error verifying system update hash", err, &l) + } + verifyFinished := time.Now() + systemUpdate.verifiedAt = verifyFinished + systemUpdate.verificationProgress = 1 + systemUpdate.updatedAt = verifyFinished + systemUpdate.updateProgress = 1 + s.triggerComponentUpdateState("system", systemUpdate) + + l.Info().Msg("System update downloaded") + + l.Info().Msg("Starting rk_ota command") + + cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all") + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + if err := cmd.Start(); err != nil { + return s.componentUpdateError("Error starting rk_ota command", err, &l) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + ticker := time.NewTicker(1800 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if systemUpdate.updateProgress >= 0.99 { + return + } + systemUpdate.updateProgress += 0.01 + if systemUpdate.updateProgress > 0.99 { + systemUpdate.updateProgress = 0.99 + } + s.triggerComponentUpdateState("system", systemUpdate) + case <-ctx.Done(): + return + } + } + }() + + err := cmd.Wait() + cancel() + rkLogger := s.l.With(). + Str("output", b.String()). + Int("exitCode", cmd.ProcessState.ExitCode()).Logger() + if err != nil { + return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger) + } + rkLogger.Info().Msg("rk_ota success") + + s.rebootNeeded = true + systemUpdate.updateProgress = 1 + systemUpdate.updatedAt = verifyFinished + s.triggerComponentUpdateState("system", systemUpdate) + + return nil +} + +func (s *State) confirmCurrentSystem() { + output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() + if err != nil { + s.l.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup") + } + s.l.Trace().Str("output", string(output)).Msg("current partition in A/B setup set") +} diff --git a/internal/ota/testdata/ota.schema.json b/internal/ota/testdata/ota.schema.json new file mode 100644 index 00000000..15965850 --- /dev/null +++ b/internal/ota/testdata/ota.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OTA Test Data Schema", + "description": "Schema for OTA update test data", + "type": "object", + "required": ["name", "remoteMetadata", "localMetadata", "updateParams"], + "properties": { + "name": { + "type": "string", + "description": "Name of the test case" + }, + "withoutCerts": { + "type": "boolean", + "default": false, + "description": "Whether to run the test without Root CA certificates" + }, + "remoteMetadata": { + "type": "array", + "description": "Remote metadata responses", + "items": { + "type": "object", + "required": ["params", "code", "data"], + "properties": { + "params": { + "type": "object", + "description": "Query parameters used for the request", + "required": ["prerelease"], + "properties": { + "prerelease": { + "type": "string", + "description": "Whether to include pre-release versions" + }, + "appVersion": { + "type": "string", + "description": "Application version string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "systemVersion": { + "type": "string", + "description": "System version string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + } + }, + "additionalProperties": false + }, + "code": { + "type": "integer", + "description": "HTTP status code" + }, + "data": { + "type": "object", + "required": ["appVersion", "appUrl", "appHash", "systemVersion", "systemUrl", "systemHash"], + "properties": { + "appVersion": { + "type": "string", + "description": "Application version string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "appUrl": { + "type": "string", + "description": "URL to download the application", + "format": "uri" + }, + "appHash": { + "type": "string", + "description": "SHA-256 hash of the application", + "pattern": "^[a-f0-9]{64}$" + }, + "systemVersion": { + "type": "string", + "description": "System version string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "systemUrl": { + "type": "string", + "description": "URL to download the system", + "format": "uri" + }, + "systemHash": { + "type": "string", + "description": "SHA-256 hash of the system", + "pattern": "^[a-f0-9]{64}$" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "localMetadata": { + "type": "object", + "description": "Local metadata containing current installed versions", + "required": ["systemVersion", "appVersion"], + "properties": { + "systemVersion": { + "type": "string", + "description": "Currently installed system version", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "appVersion": { + "type": "string", + "description": "Currently installed application version", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + } + }, + "additionalProperties": false + }, + "updateParams": { + "type": "object", + "description": "Parameters for the update operation", + "required": ["includePreRelease"], + "properties": { + "includePreRelease": { + "type": "boolean", + "description": "Whether to include pre-release versions" + }, + "components": { + "type": "object", + "description": "Component update configuration", + "properties": { + "system": { + "type": "string", + "description": "System component update configuration (empty string to update)" + }, + "app": { + "type": "string", + "description": "App component update configuration (version string to update to)" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": false + }, + "expected": { + "type": "object", + "description": "Expected update results", + "required": [], + "properties": { + "system": { + "type": "boolean", + "description": "Whether system update is expected" + }, + "app": { + "type": "boolean", + "description": "Whether app update is expected" + }, + "error": { + "type": "string", + "description": "Error message if the test case is expected to fail" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} + diff --git a/internal/ota/testdata/ota/app_only_downgrade.json b/internal/ota/testdata/ota/app_only_downgrade.json new file mode 100644 index 00000000..e8e2f7d1 --- /dev/null +++ b/internal/ota/testdata/ota/app_only_downgrade.json @@ -0,0 +1,34 @@ +{ + "name": "Downgrade App Only", + "remoteMetadata": [ + { + "params": { + "prerelease": "false", + "appVersion": "0.4.6" + }, + "code": 200, + "data": { + "appVersion": "0.4.6", + "appUrl": "https://update.jetkvm.com/app/0.4.6/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.5", + "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.2", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "app": "0.4.6" + } + }, + "expected": { + "system": false, + "app": true + } +} \ No newline at end of file diff --git a/internal/ota/testdata/ota/app_only_upgrade.json b/internal/ota/testdata/ota/app_only_upgrade.json new file mode 100644 index 00000000..69aa7fb7 --- /dev/null +++ b/internal/ota/testdata/ota/app_only_upgrade.json @@ -0,0 +1,33 @@ +{ + "name": "Upgrade App Only", + "remoteMetadata": [ + { + "params": { + "prerelease": "false" + }, + "code": 200, + "data": { + "appVersion": "0.4.7", + "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.5", + "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.2", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "app": "" + } + }, + "expected": { + "system": false, + "app": true + } +} \ No newline at end of file diff --git a/internal/ota/testdata/ota/both_downgrade.json b/internal/ota/testdata/ota/both_downgrade.json new file mode 100644 index 00000000..3c57461c --- /dev/null +++ b/internal/ota/testdata/ota/both_downgrade.json @@ -0,0 +1,37 @@ +{ + "name": "Downgrade System & App", + "remoteMetadata": [ + { + "params": { + "prerelease": "false", + "systemVersion": "0.2.2", + "appVersion": "0.4.6" + }, + "code": 200, + "data": { + "appVersion": "0.4.6", + "appUrl": "https://update.jetkvm.com/app/0.4.6/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.2", + "systemUrl": "https://update.jetkvm.com/system/0.2.2/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.5", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "system": "0.2.2", + "app": "0.4.6" + } + }, + "expected": { + "system": true, + "app": true + } +} + diff --git a/internal/ota/testdata/ota/both_upgrade.json b/internal/ota/testdata/ota/both_upgrade.json new file mode 100644 index 00000000..c3d3daee --- /dev/null +++ b/internal/ota/testdata/ota/both_upgrade.json @@ -0,0 +1,34 @@ +{ + "name": "Upgrade System & App (components given)", + "remoteMetadata": [ + { + "params": { + "prerelease": "false" + }, + "code": 200, + "data": { + "appVersion": "0.4.7", + "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.5", + "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.2", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "system": "", + "app": "" + } + }, + "expected": { + "system": true, + "app": true + } +} \ No newline at end of file diff --git a/internal/ota/testdata/ota/no_components.json b/internal/ota/testdata/ota/no_components.json new file mode 100644 index 00000000..9fb8b253 --- /dev/null +++ b/internal/ota/testdata/ota/no_components.json @@ -0,0 +1,32 @@ +{ + "name": "Upgrade System & App (no components given)", + "remoteMetadata": [ + { + "params": { + "prerelease": "false" + }, + "code": 200, + "data": { + "appVersion": "0.4.7", + "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.5", + "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.2", + "appVersion": "0.4.2" + }, + "updateParams": { + "includePreRelease": false, + "components": {} + }, + "expected": { + "system": true, + "app": true + } +} + diff --git a/internal/ota/testdata/ota/system_only_downgrade.json b/internal/ota/testdata/ota/system_only_downgrade.json new file mode 100644 index 00000000..007f5279 --- /dev/null +++ b/internal/ota/testdata/ota/system_only_downgrade.json @@ -0,0 +1,34 @@ +{ + "name": "Downgrade System Only", + "remoteMetadata": [ + { + "params": { + "prerelease": "false", + "systemVersion": "0.2.2" + }, + "code": 200, + "data": { + "appVersion": "0.4.7", + "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.2", + "systemUrl": "https://update.jetkvm.com/system/0.2.2/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.5", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "system": "0.2.2" + } + }, + "expected": { + "system": true, + "app": false + } +} \ No newline at end of file diff --git a/internal/ota/testdata/ota/system_only_upgrade.json b/internal/ota/testdata/ota/system_only_upgrade.json new file mode 100644 index 00000000..b32c9434 --- /dev/null +++ b/internal/ota/testdata/ota/system_only_upgrade.json @@ -0,0 +1,33 @@ +{ + "name": "Upgrade System Only", + "remoteMetadata": [ + { + "params": { + "prerelease": "false" + }, + "code": 200, + "data": { + "appVersion": "0.4.7", + "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", + "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", + "systemVersion": "0.2.6", + "systemUrl": "https://update.jetkvm.com/system/0.2.6/system.tar", + "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" + } + } + ], + "localMetadata": { + "systemVersion": "0.2.5", + "appVersion": "0.4.5" + }, + "updateParams": { + "includePreRelease": false, + "components": { + "system": "" + } + }, + "expected": { + "system": true, + "app": false + } +} \ No newline at end of file diff --git a/internal/ota/testdata/ota/without_certs.json b/internal/ota/testdata/ota/without_certs.json new file mode 100644 index 00000000..d5150896 --- /dev/null +++ b/internal/ota/testdata/ota/without_certs.json @@ -0,0 +1,17 @@ +{ + "name": "Without Certs", + "localMetadata": { + "systemVersion": "0.2.5", + "appVersion": "0.4.7" + }, + "updateParams": { + "includePreRelease": false, + "components": {} + }, + "expected": { + "system": false, + "app": false, + "error": "certificate signed by unknown authority" + } +} + diff --git a/internal/ota/utils.go b/internal/ota/utils.go new file mode 100644 index 00000000..b03db342 --- /dev/null +++ b/internal/ota/utils.go @@ -0,0 +1,193 @@ +package ota + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "time" + + "github.com/rs/zerolog" +) + +func syncFilesystem() error { + // Flush filesystem buffers to ensure all data is written to disk + if err := exec.Command("sync").Run(); err != nil { + return fmt.Errorf("error flushing filesystem buffers: %w", err) + } + + // Clear the filesystem caches to force a read from disk + if err := os.WriteFile("/proc/sys/vm/drop_caches", []byte("1"), 0644); err != nil { + return fmt.Errorf("error clearing filesystem caches: %w", err) + } + + return nil +} + +func (s *State) downloadFile(ctx context.Context, path string, url string, component string) error { + logger := s.l.With(). + Str("path", path). + Str("url", url). + Str("downloadComponent", component). + Logger() + t := time.Now() + traceLogger := func() *zerolog.Event { + return logger.Trace().Dur("duration", time.Since(t)) + } + traceLogger().Msg("downloading file") + + componentUpdate, ok := s.componentUpdateStatuses[component] + if !ok { + return fmt.Errorf("component %s not found", component) + } + + downloadProgress := componentUpdate.downloadProgress + + if _, err := os.Stat(path); err == nil { + traceLogger().Msg("removing existing file") + if err := os.Remove(path); err != nil { + return fmt.Errorf("error removing existing file: %w", err) + } + } + + unverifiedPath := path + ".unverified" + if _, err := os.Stat(unverifiedPath); err == nil { + traceLogger().Msg("removing existing unverified file") + if err := os.Remove(unverifiedPath); err != nil { + return fmt.Errorf("error removing existing unverified file: %w", err) + } + } + + traceLogger().Msg("creating unverified file") + file, err := os.Create(unverifiedPath) + if err != nil { + return fmt.Errorf("error creating file: %w", err) + } + defer file.Close() + + traceLogger().Msg("creating request") + req, err := s.newHTTPRequestWithTrace(ctx, "GET", url, nil, traceLogger) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + client := s.client() + traceLogger().Msg("starting download") + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error downloading file: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + totalSize := resp.ContentLength + if totalSize <= 0 { + return fmt.Errorf("invalid content length") + } + + var written int64 + buf := make([]byte, 32*1024) + for { + nr, er := resp.Body.Read(buf) + if nr > 0 { + nw, ew := file.Write(buf[0:nr]) + if nw < nr { + return fmt.Errorf("short write: %d < %d", nw, nr) + } + written += int64(nw) + if ew != nil { + return fmt.Errorf("error writing to file: %w", ew) + } + progress := float32(written) / float32(totalSize) + if progress-downloadProgress >= 0.01 { + componentUpdate.downloadProgress = progress + s.triggerComponentUpdateState(component, &componentUpdate) + } + } + if er != nil { + if er == io.EOF { + break + } + return fmt.Errorf("error reading response body: %w", er) + } + } + + traceLogger().Msg("download finished") + file.Close() + + traceLogger().Msg("syncing filesystem") + if err := syncFilesystem(); err != nil { + return fmt.Errorf("error syncing filesystem: %w", err) + } + + return nil +} +func (s *State) verifyFile(path string, expectedHash string, verifyProgress *float32) error { + l := s.l.With().Str("path", path).Logger() + + unverifiedPath := path + ".unverified" + fileToHash, err := os.Open(unverifiedPath) + if err != nil { + return fmt.Errorf("error opening file for hashing: %w", err) + } + defer fileToHash.Close() + + hash := sha256.New() + fileInfo, err := fileToHash.Stat() + if err != nil { + return fmt.Errorf("error getting file info: %w", err) + } + totalSize := fileInfo.Size() + + buf := make([]byte, 32*1024) + verified := int64(0) + + for { + nr, er := fileToHash.Read(buf) + if nr > 0 { + nw, ew := hash.Write(buf[0:nr]) + if nw < nr { + return fmt.Errorf("short write: %d < %d", nw, nr) + } + verified += int64(nw) + if ew != nil { + return fmt.Errorf("error writing to hash: %w", ew) + } + progress := float32(verified) / float32(totalSize) + if progress-*verifyProgress >= 0.01 { + *verifyProgress = progress + s.triggerStateUpdate() + } + } + if er != nil { + if er == io.EOF { + break + } + return fmt.Errorf("error reading file: %w", er) + } + } + + hashSum := hash.Sum(nil) + l.Info().Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") + + if hex.EncodeToString(hashSum) != expectedHash { + return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) + } + + if err := os.Rename(unverifiedPath, path); err != nil { + return fmt.Errorf("error renaming file: %w", err) + } + + if err := os.Chmod(path, 0755); err != nil { + return fmt.Errorf("error making file executable: %w", err) + } + + return nil +} diff --git a/internal/supervisor/consts.go b/internal/supervisor/consts.go new file mode 100644 index 00000000..ab250553 --- /dev/null +++ b/internal/supervisor/consts.go @@ -0,0 +1,11 @@ +package supervisor + +const ( + EnvChildID = "JETKVM_CHILD_ID" // The child ID is the version of the app that is running + EnvSubcomponent = "JETKVM_SUBCOMPONENT" // The subcomponent is the component that is running + ErrorDumpDir = "/userdata/jetkvm/crashdump" // The error dump directory is the directory where the error dumps are stored + ErrorDumpLastFile = "last-crash.log" // The error dump last file is the last error dump file + ErrorDumpTemplate = "jetkvm-%s.log" // The error dump template is the template for the error dump file + + FailsafeReasonVideoMaxRestartAttemptsReached = "failsafe::video.max_restart_attempts_reached" +) diff --git a/internal/utils/env.go b/internal/utils/env.go new file mode 100644 index 00000000..4040b961 --- /dev/null +++ b/internal/utils/env.go @@ -0,0 +1,92 @@ +package utils + +import ( + "fmt" + "reflect" + "strconv" +) + +func MarshalEnv(instance interface{}) ([]string, error) { + v := reflect.ValueOf(instance) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, fmt.Errorf("instance is nil") + } + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil, fmt.Errorf("instance must be a struct or pointer to struct") + } + + t := v.Type() + var result []string + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + fieldValue := v.Field(i) + + // Get the env tag + envTag := field.Tag.Get("env") + if envTag == "" || envTag == "-" { + continue + } + + // Skip unexported fields + if !fieldValue.CanInterface() { + continue + } + + var valueStr string + + // Handle different types + switch fieldValue.Kind() { + case reflect.Bool: + valueStr = strconv.FormatBool(fieldValue.Bool()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + valueStr = strconv.FormatUint(fieldValue.Uint(), 10) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + valueStr = strconv.FormatInt(fieldValue.Int(), 10) + + case reflect.Float32, reflect.Float64: + valueStr = strconv.FormatFloat(fieldValue.Float(), 'f', -1, 64) + + case reflect.String: + valueStr = fieldValue.String() + + case reflect.Ptr: + if fieldValue.IsNil() { + continue // Skip nil pointers + } + elem := fieldValue.Elem() + // Handle *semver.Version and other pointer types + if elem.CanInterface() { + if stringer, ok := elem.Interface().(fmt.Stringer); ok { + valueStr = stringer.String() + } else { + valueStr = fmt.Sprintf("%v", elem.Interface()) + } + } else { + valueStr = fmt.Sprintf("%v", elem.Interface()) + } + + default: + // For other types, try to convert to string + if fieldValue.CanInterface() { + if stringer, ok := fieldValue.Interface().(fmt.Stringer); ok { + valueStr = stringer.String() + } else { + valueStr = fmt.Sprintf("%v", fieldValue.Interface()) + } + } else { + valueStr = fmt.Sprintf("%v", fieldValue.Interface()) + } + } + + result = append(result, fmt.Sprintf("%s=%s", envTag, valueStr)) + } + + return result, nil +} diff --git a/internal/utils/env_test.go b/internal/utils/env_test.go new file mode 100644 index 00000000..25313677 --- /dev/null +++ b/internal/utils/env_test.go @@ -0,0 +1,57 @@ +package utils + +import ( + "reflect" + "testing" + + "github.com/Masterminds/semver/v3" +) + +type nativeOptions struct { + Disable bool `env:"JETKVM_NATIVE_DISABLE"` + SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"` + AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"` + DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"` + DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"` +} + +func TestMarshalEnv(t *testing.T) { + tests := []struct { + name string + instance interface{} + want []string + wantErr bool + }{ + { + name: "basic struct", + instance: nativeOptions{ + Disable: false, + SystemVersion: semver.MustParse("1.1.0"), + AppVersion: semver.MustParse("1111.0.0"), + DisplayRotation: 1, + DefaultQualityFactor: 1.0, + }, + want: []string{ + "JETKVM_NATIVE_DISABLE=false", + "JETKVM_NATIVE_SYSTEM_VERSION=1.1.0", + "JETKVM_NATIVE_APP_VERSION=1111.0.0", + "JETKVM_NATIVE_DISPLAY_ROTATION=1", + "JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR=1", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MarshalEnv(tt.instance) + if (err != nil) != tt.wantErr { + t.Errorf("MarshalEnv() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/jsonrpc.go b/jsonrpc.go index 4bb65fff..465990c1 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -242,7 +242,8 @@ func rpcRefreshHdmiConnection() error { return err } if currentEDID == "" { - currentEDID = nativeInstance.GetDefaultEDID() + // Use the default EDID from the native package + currentEDID = native.DefaultEDID } return nativeInstance.VideoSetEDID(currentEDID) } @@ -251,55 +252,6 @@ func rpcGetVideoLogStatus() (string, error) { return nativeInstance.VideoLogStatus() } -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) - // to ensure backwards compatibility, - // if there's an error, we won't return an error, but we will set the error field - if err != nil { - if updateStatus == nil { - return nil, fmt.Errorf("error checking for updates: %w", err) - } - updateStatus.Error = err.Error() - } - - return updateStatus, nil -} - -func rpcGetLocalVersion() (*LocalMetadata, error) { - systemVersion, appVersion, err := GetLocalVersion() - if err != nil { - return nil, fmt.Errorf("error getting local version: %w", err) - } - return &LocalMetadata{ - AppVersion: appVersion.String(), - SystemVersion: systemVersion.String(), - }, 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 rpcSetDisplayRotation(params DisplayRotationSettings) error { currentRotation := config.DisplayRotation if currentRotation == params.Rotation { @@ -669,7 +621,10 @@ func rpcGetMassStorageMode() (string, error) { } func rpcIsUpdatePending() (bool, error) { - return IsUpdatePending(), nil + if otaState == nil { + return false, nil + } + return otaState.IsUpdatePending(), nil } func rpcGetUsbEmulationState() (bool, error) { @@ -1368,7 +1323,10 @@ var rpcHandlers = map[string]RPCHandler{ "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, "getLocalVersion": {Func: rpcGetLocalVersion}, "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel, Params: []string{"channel"}}, + "checkUpdateComponents": {Func: rpcCheckUpdateComponents, Params: []string{"params", "includePreRelease"}}, "tryUpdate": {Func: rpcTryUpdate}, + "tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"params", "includePreRelease", "resetConfig"}}, "getDevModeState": {Func: rpcGetDevModeState}, "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState}, diff --git a/main.go b/main.go index 081b039b..86d45b75 100644 --- a/main.go +++ b/main.go @@ -2,22 +2,37 @@ package kvm import ( "context" + "fmt" "net/http" "os" "os/signal" "syscall" "time" + "github.com/erikdubbelboer/gspt" "github.com/gwatts/rootcerts" + "github.com/jetkvm/kvm/internal/ota" ) var appCtx context.Context +var procPrefix string = "jetkvm: [app]" + +func setProcTitle(status string) { + if status != "" { + status = " " + status + } + title := fmt.Sprintf("%s%s", procPrefix, status) + gspt.SetProcTitle(title) +} func Main() { + setProcTitle("starting") + logger.Log().Msg("JetKVM Starting Up") checkFailsafeReason() if failsafeModeActive { + procPrefix = "jetkvm: [app+failsafe]" logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated") } @@ -38,10 +53,10 @@ func Main() { Msg("starting JetKVM") go runWatchdog() - go confirmCurrentSystem() - initDisplay() + setProcTitle("initNative") initNative(systemVersionLocal, appVersionLocal) + initDisplay() initAudio() defer stopAudio() @@ -56,7 +71,12 @@ func Main() { Int("ca_certs_loaded", len(rootcerts.Certs())). Msg("loaded Root CA certificates") + initOta() + + http.DefaultClient.Timeout = 1 * time.Minute + // Initialize network + setProcTitle("initNetwork") if err := initNetwork(); err != nil { logger.Error().Err(err).Msg("failed to initialize network") // TODO: reset config to default @@ -64,17 +84,21 @@ func Main() { } // Initialize time sync + setProcTitle("initTimeSync") initTimeSync() timeSync.Start() // Initialize mDNS + setProcTitle("initMdns") if err := initMdns(); err != nil { logger.Error().Err(err).Msg("failed to initialize mDNS") } + setProcTitle("initPrometheus") initPrometheus() // initialize usb gadget + setProcTitle("initUsbGadget") initUsbGadget() if err := setInitialVirtualMediaState(); err != nil { logger.Warn().Err(err).Msg("failed to set initial virtual media state") @@ -116,7 +140,10 @@ func Main() { } includePreRelease := config.IncludePreRelease - err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) + err = otaState.TryUpdate(context.Background(), ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + }) if err != nil { logger.Warn().Err(err).Msg("failed to auto update") } @@ -139,6 +166,9 @@ func Main() { initPublicIPState() initSerialPort() + + setProcTitle("ready") + sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs diff --git a/native.go b/native.go index 0eb01bf3..5f09ef83 100644 --- a/native.go +++ b/native.go @@ -11,17 +11,28 @@ import ( ) var ( - nativeInstance *native.Native + nativeInstance native.NativeInterface nativeCmdLock = sync.Mutex{} ) func initNative(systemVersion *semver.Version, appVersion *semver.Version) { - nativeInstance = native.NewNative(native.NativeOptions{ - Disable: failsafeModeActive, + if failsafeModeActive { + nativeInstance = &native.EmptyNativeInterface{} + nativeLogger.Warn().Msg("failsafe mode active, using empty native interface") + return + } + + nativeLogger.Info().Msg("initializing native proxy") + var err error + nativeInstance, err = native.NewNativeProxy(native.NativeOptions{ SystemVersion: systemVersion, AppVersion: appVersion, DisplayRotation: config.GetDisplayRotation(), DefaultQualityFactor: config.VideoQualityFactor, + MaxRestartAttempts: config.NativeMaxRestart, + OnNativeRestart: func() { + configureDisplayOnNativeRestart() + }, OnVideoStateChange: func(state native.VideoState) { lastVideoState = state triggerVideoStateUpdate() @@ -63,8 +74,18 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { } }, }) + if err != nil { + nativeLogger.Fatal().Err(err).Msg("failed to create native proxy") + } - nativeInstance.Start(config.EdidString) + if err := nativeInstance.Start(); err != nil { + nativeLogger.Fatal().Err(err).Msg("failed to start native proxy") + } + go func() { + if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil { + nativeLogger.Warn().Err(err).Msg("error setting EDID") + } + }() if os.Getenv("JETKVM_CRASH_TESTING") == "1" { nativeInstance.DoNotUseThisIsForCrashTestingOnly() diff --git a/network.go b/network.go index 3a2b24f9..eb14c70d 100644 --- a/network.go +++ b/network.go @@ -11,6 +11,7 @@ import ( "github.com/jetkvm/kvm/internal/confparser" "github.com/jetkvm/kvm/internal/mdns" "github.com/jetkvm/kvm/internal/network/types" + "github.com/jetkvm/kvm/internal/ota" "github.com/jetkvm/kvm/pkg/myip" "github.com/jetkvm/kvm/pkg/nmlite" "github.com/jetkvm/kvm/pkg/nmlite/link" @@ -225,7 +226,7 @@ func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error { return nm.SetHostname(hostname, domain) } -func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) { +func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *ota.PostRebootAction) { oldDhcpClient := oldConfig.DHCPClient.String l := networkLogger.With(). @@ -249,7 +250,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re l.Info().Msg("IPv4 mode changed with udhcpc, reboot required") if newIPv4Mode == "static" && oldIPv4Mode != "static" { - postRebootAction = &PostRebootAction{ + postRebootAction = &ota.PostRebootAction{ HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String), RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String), } @@ -266,7 +267,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re // Handle IP change for redirect (only if both are not nil and IP changed) if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil && newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String { - postRebootAction = &PostRebootAction{ + postRebootAction = &ota.PostRebootAction{ HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String), RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String), } @@ -283,6 +284,11 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re l.Info().Msg("IPv6 mode changed with udhcpc, reboot required") } + if newConfig.Hostname.String != oldConfig.Hostname.String { + rebootRequired = true + l.Info().Msg("Hostname changed, reboot required") + } + return rebootRequired, postRebootAction } diff --git a/ota.go b/ota.go index 5371e428..ef7f9c21 100644 --- a/ota.go +++ b/ota.go @@ -1,59 +1,65 @@ package kvm import ( - "bytes" "context" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "encoding/json" "fmt" - "io" "net/http" - "net/url" "os" - "os/exec" "strings" - "time" "github.com/Masterminds/semver/v3" - "github.com/gwatts/rootcerts" - "github.com/rs/zerolog" + "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/ota" ) -type UpdateMetadata struct { - AppVersion string `json:"appVersion"` - AppUrl string `json:"appUrl"` - AppHash string `json:"appHash"` - SystemVersion string `json:"systemVersion"` - SystemUrl string `json:"systemUrl"` - SystemHash string `json:"systemHash"` -} - -type LocalMetadata struct { - AppVersion string `json:"appVersion"` - SystemVersion string `json:"systemVersion"` -} - -// UpdateStatus represents the current update status -type UpdateStatus struct { - Local *LocalMetadata `json:"local"` - Remote *UpdateMetadata `json:"remote"` - SystemUpdateAvailable bool `json:"systemUpdateAvailable"` - AppUpdateAvailable bool `json:"appUpdateAvailable"` - - // for backwards compatibility - Error string `json:"error,omitempty"` -} - -const UpdateMetadataUrl = "https://api.jetkvm.com/releases" - var builtAppVersion = "0.1.0+dev" +var otaState *ota.State + +func initOta() { + otaState = ota.NewState(ota.Options{ + Logger: otaLogger, + ReleaseAPIEndpoint: config.GetUpdateAPIURL(), + GetHTTPClient: func() ota.HttpClient { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = config.NetworkConfig.GetTransportProxyFunc() + + client := &http.Client{ + Transport: transport, + } + return client + }, + GetLocalVersion: GetLocalVersion, + HwReboot: hwReboot, + ResetConfig: rpcResetConfig, + SetAutoUpdate: rpcSetAutoUpdateState, + OnStateUpdate: func(state *ota.RPCState) { + triggerOTAStateUpdate(state) + }, + OnProgressUpdate: func(progress float32) { + writeJSONRPCEvent("otaProgress", progress, currentSession) + }, + }) +} + +func triggerOTAStateUpdate(state *ota.RPCState) { + go func() { + if currentSession == nil || (otaState == nil && state == nil) { + return + } + if state == nil { + state = otaState.ToRPCState() + } + writeJSONRPCEvent("otaState", state, currentSession) + }() +} + +// GetBuiltAppVersion returns the built-in app version func GetBuiltAppVersion() string { return builtAppVersion } +// GetLocalVersion returns the local version of the system and app func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) { appVersion, err = semver.NewVersion(builtAppVersion) if err != nil { @@ -73,519 +79,107 @@ func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Versio return systemVersion, appVersion, nil } -func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateMetadata, error) { - metadata := &UpdateMetadata{} +func getUpdateStatus(includePreRelease bool) (*ota.UpdateStatus, error) { + updateStatus, err := otaState.GetUpdateStatus(context.Background(), ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + RequestID: uuid.New().String(), + }) - updateUrl, err := url.Parse(UpdateMetadataUrl) + // to ensure backwards compatibility, + // if there's an error, we won't return an error, but we will set the error field if err != nil { - return nil, fmt.Errorf("error parsing update metadata URL: %w", err) - } - - query := updateUrl.Query() - query.Set("deviceId", deviceId) - query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) - updateUrl.RawQuery = query.Encode() - - logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates") - - req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.Proxy = config.NetworkConfig.GetTransportProxyFunc() - - client := &http.Client{ - Transport: transport, - } - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - err = json.NewDecoder(resp.Body).Decode(metadata) - if err != nil { - return nil, fmt.Errorf("error decoding response: %w", err) - } - - return metadata, nil -} - -func downloadFile(ctx context.Context, path string, url string, downloadProgress *float32) error { - if _, err := os.Stat(path); err == nil { - if err := os.Remove(path); err != nil { - return fmt.Errorf("error removing existing file: %w", err) + if updateStatus == nil { + return nil, fmt.Errorf("error checking for updates: %w", err) } + updateStatus.Error = err.Error() } - unverifiedPath := path + ".unverified" - if _, err := os.Stat(unverifiedPath); err == nil { - if err := os.Remove(unverifiedPath); err != nil { - return fmt.Errorf("error removing existing unverified file: %w", err) - } + // otaState doesn't have the current auto-update state, so we need to get it from the config + if updateStatus.WillDisableAutoUpdate { + updateStatus.WillDisableAutoUpdate = config.AutoUpdateEnabled } - file, err := os.Create(unverifiedPath) - if err != nil { - return fmt.Errorf("error creating file: %w", err) - } - defer file.Close() - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return fmt.Errorf("error creating request: %w", err) - } - - client := http.Client{ - Timeout: 10 * time.Minute, - Transport: &http.Transport{ - Proxy: config.NetworkConfig.GetTransportProxyFunc(), - TLSHandshakeTimeout: 30 * time.Second, - TLSClientConfig: &tls.Config{ - RootCAs: rootcerts.ServerCertPool(), - }, - }, - } - - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("error downloading file: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - totalSize := resp.ContentLength - if totalSize <= 0 { - return fmt.Errorf("invalid content length") - } - - var written int64 - buf := make([]byte, 32*1024) - for { - nr, er := resp.Body.Read(buf) - if nr > 0 { - nw, ew := file.Write(buf[0:nr]) - if nw < nr { - return fmt.Errorf("short file write: %d < %d", nw, nr) - } - written += int64(nw) - if ew != nil { - return fmt.Errorf("error writing to file: %w", ew) - } - progress := float32(written) / float32(totalSize) - if progress-*downloadProgress >= 0.01 { - *downloadProgress = progress - triggerOTAStateUpdate() - } - } - if er != nil { - if er == io.EOF { - break - } - return fmt.Errorf("error reading response body: %w", er) - } - } - - file.Close() - - // Flush filesystem buffers to ensure all data is written to disk - err = exec.Command("sync").Run() - if err != nil { - return fmt.Errorf("error flushing filesystem buffers: %w", err) - } - - // Clear the filesystem caches to force a read from disk - err = os.WriteFile("/proc/sys/vm/drop_caches", []byte("1"), 0644) - if err != nil { - return fmt.Errorf("error clearing filesystem caches: %w", err) - } - - return nil -} - -func verifyFile(path string, expectedHash string, verifyProgress *float32, scopedLogger *zerolog.Logger) error { - if scopedLogger == nil { - scopedLogger = otaLogger - } - - unverifiedPath := path + ".unverified" - fileToHash, err := os.Open(unverifiedPath) - if err != nil { - return fmt.Errorf("error opening file for hashing: %w", err) - } - defer fileToHash.Close() - - hash := sha256.New() - fileInfo, err := fileToHash.Stat() - if err != nil { - return fmt.Errorf("error getting file info: %w", err) - } - totalSize := fileInfo.Size() - - buf := make([]byte, 32*1024) - verified := int64(0) - - for { - nr, er := fileToHash.Read(buf) - if nr > 0 { - nw, ew := hash.Write(buf[0:nr]) - if nw < nr { - return fmt.Errorf("short hash write: %d < %d", nw, nr) - } - verified += int64(nw) - if ew != nil { - return fmt.Errorf("error writing to hash: %w", ew) - } - progress := float32(verified) / float32(totalSize) - if progress-*verifyProgress >= 0.01 { - *verifyProgress = progress - triggerOTAStateUpdate() - } - } - if er != nil { - if er == io.EOF { - break - } - return fmt.Errorf("error reading file: %w", er) - } - } - - // close the file so we can rename below - if err := fileToHash.Close(); err != nil { - return fmt.Errorf("error closing file: %w", err) - } - - hashSum := hex.EncodeToString(hash.Sum(nil)) - scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of") - - if hashSum != expectedHash { - return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash) - } - - if err := os.Rename(unverifiedPath, path); err != nil { - return fmt.Errorf("error renaming file: %w", err) - } - - if err := os.Chmod(path, 0755); err != nil { - return fmt.Errorf("error making file executable: %w", err) - } - - return nil -} - -type OTAState struct { - Updating bool `json:"updating"` - Error string `json:"error,omitempty"` - MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"` - AppUpdatePending bool `json:"appUpdatePending"` - SystemUpdatePending bool `json:"systemUpdatePending"` - AppDownloadProgress float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar - AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"` - SystemDownloadProgress float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar - SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"` - AppVerificationProgress float32 `json:"appVerificationProgress,omitempty"` - AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"` - SystemVerificationProgress float32 `json:"systemVerificationProgress,omitempty"` - SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"` - AppUpdateProgress float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar - AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"` - SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement - SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"` -} - -var otaState = OTAState{} - -func triggerOTAStateUpdate() { - go func() { - if currentSession == nil { - logger.Info().Msg("No active RPC session, skipping update state update") - return - } - writeJSONRPCEvent("otaState", otaState, currentSession) - }() -} - -func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { - scopedLogger := otaLogger.With(). - Str("deviceId", deviceId). - Bool("includePreRelease", includePreRelease). - Logger() - - scopedLogger.Info().Msg("Trying to update...") - if otaState.Updating { - return fmt.Errorf("update already in progress") - } - - otaState = OTAState{ - Updating: true, - } - triggerOTAStateUpdate() - - defer func() { - otaState.Updating = false - triggerOTAStateUpdate() - }() - - updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease) - if err != nil { - otaState.Error = fmt.Sprintf("Error checking for updates: %v", err) - scopedLogger.Error().Err(err).Msg("Error checking for updates") - return fmt.Errorf("error checking for updates: %w", err) - } - - now := time.Now() - otaState.MetadataFetchedAt = &now - otaState.AppUpdatePending = updateStatus.AppUpdateAvailable - otaState.SystemUpdatePending = updateStatus.SystemUpdateAvailable - triggerOTAStateUpdate() - - local := updateStatus.Local - remote := updateStatus.Remote - appUpdateAvailable := updateStatus.AppUpdateAvailable - systemUpdateAvailable := updateStatus.SystemUpdateAvailable - - rebootNeeded := false - - if appUpdateAvailable { - scopedLogger.Info(). - Str("local", local.AppVersion). - Str("remote", remote.AppVersion). - Msg("App update available") - - err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) - if err != nil { - otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) - scopedLogger.Error().Err(err).Msg("Error downloading app update") - triggerOTAStateUpdate() - return fmt.Errorf("error downloading app update: %w", err) - } - - downloadFinished := time.Now() - otaState.AppDownloadFinishedAt = &downloadFinished - otaState.AppDownloadProgress = 1 - triggerOTAStateUpdate() - - err = verifyFile( - "/userdata/jetkvm/jetkvm_app.update", - remote.AppHash, - &otaState.AppVerificationProgress, - &scopedLogger, - ) - if err != nil { - otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) - scopedLogger.Error().Err(err).Msg("Error verifying app update hash") - triggerOTAStateUpdate() - return fmt.Errorf("error verifying app update: %w", err) - } - - verifyFinished := time.Now() - otaState.AppVerifiedAt = &verifyFinished - otaState.AppVerificationProgress = 1 - triggerOTAStateUpdate() - - otaState.AppUpdatedAt = &verifyFinished - otaState.AppUpdateProgress = 1 - triggerOTAStateUpdate() - - scopedLogger.Info().Msg("App update downloaded") - rebootNeeded = true - triggerOTAStateUpdate() - } else { - scopedLogger.Info().Msg("App is up to date") - } - - if systemUpdateAvailable { - scopedLogger.Info(). - Str("local", local.SystemVersion). - Str("remote", remote.SystemVersion). - Msg("System update available") - - err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) - if err != nil { - otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) - scopedLogger.Error().Err(err).Msg("Error downloading system update") - triggerOTAStateUpdate() - return fmt.Errorf("error downloading system update: %w", err) - } - - downloadFinished := time.Now() - otaState.SystemDownloadFinishedAt = &downloadFinished - otaState.SystemDownloadProgress = 1 - triggerOTAStateUpdate() - - err = verifyFile( - "/userdata/jetkvm/update_system.tar", - remote.SystemHash, - &otaState.SystemVerificationProgress, - &scopedLogger, - ) - if err != nil { - otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) - scopedLogger.Error().Err(err).Msg("Error verifying system update hash") - triggerOTAStateUpdate() - return fmt.Errorf("error verifying system update: %w", err) - } - - scopedLogger.Info().Msg("System update downloaded") - verifyFinished := time.Now() - otaState.SystemVerifiedAt = &verifyFinished - otaState.SystemVerificationProgress = 1 - triggerOTAStateUpdate() - - scopedLogger.Info().Msg("Starting rk_ota command") - cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all") - var b bytes.Buffer - cmd.Stdout = &b - cmd.Stderr = &b - err = cmd.Start() - if err != nil { - otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err) - scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") - triggerOTAStateUpdate() - return fmt.Errorf("error starting rk_ota command: %w", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - go func() { - ticker := time.NewTicker(1800 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - if otaState.SystemUpdateProgress >= 0.99 { - return - } - otaState.SystemUpdateProgress += 0.01 - if otaState.SystemUpdateProgress > 0.99 { - otaState.SystemUpdateProgress = 0.99 - } - triggerOTAStateUpdate() - case <-ctx.Done(): - return - } - } - }() - - err = cmd.Wait() - cancel() - output := b.String() - if err != nil { - otaState.Error = fmt.Sprintf("Error executing rk_ota command: %v\nOutput: %s", err, output) - scopedLogger.Error(). - Err(err). - Str("output", output). - Int("exitCode", cmd.ProcessState.ExitCode()). - Msg("Error executing rk_ota command") - triggerOTAStateUpdate() - return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) - } - - scopedLogger.Info().Str("output", output).Msg("rk_ota success") - otaState.SystemUpdateProgress = 1 - otaState.SystemUpdatedAt = &verifyFinished - rebootNeeded = true - triggerOTAStateUpdate() - } else { - scopedLogger.Info().Msg("System is up to date") - } - - if rebootNeeded { - scopedLogger.Info().Msg("System Rebooting due to OTA update") - - // Build redirect URL with conditional query parameters - redirectTo := "/settings/general/update" - queryParams := url.Values{} - if systemUpdateAvailable { - queryParams.Set("systemVersion", remote.SystemVersion) - } - if appUpdateAvailable { - queryParams.Set("appVersion", remote.AppVersion) - } - if len(queryParams) > 0 { - redirectTo += "?" + queryParams.Encode() - } - - postRebootAction := &PostRebootAction{ - HealthCheck: "/device/status", - RedirectTo: redirectTo, - } - - if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil { - return fmt.Errorf("error requesting reboot: %w", err) - } - } - - return nil -} - -func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) { - updateStatus := &UpdateStatus{} - - // Get local versions - systemVersionLocal, appVersionLocal, err := GetLocalVersion() - if err != nil { - return updateStatus, fmt.Errorf("error getting local version: %w", err) - } - updateStatus.Local = &LocalMetadata{ - AppVersion: appVersionLocal.String(), - SystemVersion: systemVersionLocal.String(), - } - - // Get remote metadata - remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease) - if err != nil { - return updateStatus, fmt.Errorf("error checking for updates: %w", err) - } - updateStatus.Remote = remoteMetadata - - // Get remote versions - systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion) - if err != nil { - return updateStatus, fmt.Errorf("error parsing remote system version: %w", err) - } - appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion) - if err != nil { - return updateStatus, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion) - } - - updateStatus.SystemUpdateAvailable = systemVersionRemote.GreaterThan(systemVersionLocal) - updateStatus.AppUpdateAvailable = appVersionRemote.GreaterThan(appVersionLocal) - - // Handle pre-release updates - isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != "" - isRemoteAppPreRelease := appVersionRemote.Prerelease() != "" - - if isRemoteSystemPreRelease && !includePreRelease { - updateStatus.SystemUpdateAvailable = false - } - if isRemoteAppPreRelease && !includePreRelease { - updateStatus.AppUpdateAvailable = false - } + otaLogger.Info().Interface("updateStatus", updateStatus).Msg("Update status") return updateStatus, nil } -func IsUpdatePending() bool { - return otaState.Updating +func rpcGetDevChannelState() (bool, error) { + return config.IncludePreRelease, nil } -// make sure our current a/b partition is set as default -func confirmCurrentSystem() { - output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() - if err != nil { - logger.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup") +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() (*ota.UpdateStatus, error) { + return getUpdateStatus(config.IncludePreRelease) +} + +func rpcGetUpdateStatusChannel(channel string) (*ota.UpdateStatus, error) { + switch channel { + case "stable": + return getUpdateStatus(false) + case "dev": + return getUpdateStatus(true) + default: + return nil, fmt.Errorf("invalid channel: %s", channel) } } + +func rpcGetLocalVersion() (*ota.LocalMetadata, error) { + systemVersion, appVersion, err := GetLocalVersion() + if err != nil { + return nil, fmt.Errorf("error getting local version: %w", err) + } + return &ota.LocalMetadata{ + AppVersion: appVersion.String(), + SystemVersion: systemVersion.String(), + }, nil +} + +type updateParams struct { + Components map[string]string `json:"components,omitempty"` +} + +func rpcTryUpdate() error { + return rpcTryUpdateComponents(updateParams{ + Components: make(map[string]string), + }, config.IncludePreRelease, false) +} + +// rpcCheckUpdateComponents checks the update status for the given components +func rpcCheckUpdateComponents(params updateParams, includePreRelease bool) (*ota.UpdateStatus, error) { + updateParams := ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + Components: params.Components, + } + info, err := otaState.GetUpdateStatus(context.Background(), updateParams) + if err != nil { + return nil, fmt.Errorf("failed to check update: %w", err) + } + return info, nil +} + +func rpcTryUpdateComponents(params updateParams, includePreRelease bool, resetConfig bool) error { + updateParams := ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + ResetConfig: resetConfig, + Components: params.Components, + } + + go func() { + err := otaState.TryUpdate(context.Background(), updateParams) + if err != nil { + otaLogger.Warn().Err(err).Msg("failed to try update") + } + }() + return nil +} diff --git a/scripts/build_cgo.sh b/scripts/build_cgo.sh index 87577e39..d67fa7cd 100755 --- a/scripts/build_cgo.sh +++ b/scripts/build_cgo.sh @@ -4,6 +4,8 @@ set -e SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") source ${SCRIPT_PATH}/build_utils.sh +CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} + CGO_PATH=$(realpath "${SCRIPT_PATH}/../internal/native/cgo") BUILD_DIR=${CGO_PATH}/build @@ -31,7 +33,7 @@ VERBOSE=1 cmake -B "${BUILD_DIR}" \ -DCONFIG_LV_BUILD_EXAMPLES=OFF \ -DCONFIG_LV_BUILD_DEMOS=OFF \ -DSKIP_GLIBC_NAMES=ON \ - -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ -DCMAKE_INSTALL_PREFIX="${TMP_DIR}" msg_info "▶ Copying built library and header files" diff --git a/scripts/configure_vscode.py b/scripts/configure_vscode.py new file mode 100755 index 00000000..cac54685 --- /dev/null +++ b/scripts/configure_vscode.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import json +import os + +DEFAULT_C_INTELLISENSE_SETTINGS = { + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + # "compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc", + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-arm", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} + +def configure_c_intellisense(): + settings_path = os.path.join('.vscode', 'c_cpp_properties.json') + settings = DEFAULT_C_INTELLISENSE_SETTINGS.copy() + + # open existing settings if they exist + if os.path.exists(settings_path): + with open(settings_path, 'r') as f: + settings = json.load(f) + + # update compiler path + settings['configurations'][0]['compilerPath'] = "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc" + settings['configurations'][0]['configurationProvider'] = "ms-vscode.cmake-tools" + + with open(settings_path, 'w') as f: + json.dump(settings, f, indent=4) + + print("C/C++ IntelliSense configuration updated.") + + +if __name__ == "__main__": + configure_c_intellisense() \ No newline at end of file diff --git a/scripts/dev_deploy.sh b/scripts/dev_deploy.sh index 6c8b204c..96e7cf60 100755 --- a/scripts/dev_deploy.sh +++ b/scripts/dev_deploy.sh @@ -12,12 +12,14 @@ show_help() { echo echo "Optional:" echo " -u, --user Remote username (default: root)" + echo " --gdb-port GDB debug port (default: 2345)" echo " --run-go-tests Run go tests" echo " --run-go-tests-only Run go tests and exit" echo " --skip-ui-build Skip frontend/UI build" echo " --skip-native-build Skip native build" echo " --disable-docker Disable docker build" echo " --enable-sync-trace Enable sync trace (do not use in release builds)" + echo " --native-binary Build and deploy the native binary (FOR DEBUGGING ONLY)" echo " -i, --install Build for release and install the app" echo " --help Display this help message" echo @@ -58,6 +60,8 @@ REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false SKIP_UI_BUILD_RELEASE=0 SKIP_NATIVE_BUILD=0 +GDB_DEBUG_PORT=2345 +BUILD_NATIVE_BINARY=false ENABLE_SYNC_TRACE=0 RESET_USB_HID_DEVICE=false LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" @@ -79,6 +83,10 @@ while [[ $# -gt 0 ]]; do REMOTE_USER="$2" shift 2 ;; + --gdb-port) + GDB_DEBUG_PORT="$2" + shift 2 + ;; --skip-ui-build) SKIP_UI_BUILD=true shift @@ -113,6 +121,10 @@ while [[ $# -gt 0 ]]; do RUN_GO_TESTS=true shift ;; + --native-binary) + BUILD_NATIVE_BINARY=true + shift + ;; -i|--install) INSTALL_APP=true shift @@ -141,6 +153,10 @@ fi # Check device connectivity before proceeding check_ping "${REMOTE_HOST}" check_ssh "${REMOTE_USER}" "${REMOTE_HOST}" +function sshdev() { + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "$@" + return $? +} # check if the current CPU architecture is x86_64 if [ "$(uname -m)" != "x86_64" ]; then @@ -152,6 +168,34 @@ if [ "$BUILD_IN_DOCKER" = true ]; then build_docker_image fi +if [ "$BUILD_NATIVE_BINARY" = true ]; then + msg_info "▶ Building native binary" + CMAKE_BUILD_TYPE=Debug make build_native + msg_info "▶ Checking if GDB is available on remote host" + if ! sshdev "command -v gdbserver > /dev/null 2>&1"; then + msg_warn "Error: gdbserver is not installed on the remote host" + tar -czf - -C /opt/jetkvm-native-buildkit/gdb/ . | sshdev "tar -xzf - -C /usr/bin" + msg_info "✓ gdbserver installed on remote host" + fi + msg_info "▶ Stopping any existing instances of jetkvm_native_debug on remote host" + sshdev "killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug gdbserver || true >> /dev/null 2>&1" + sshdev "cat > ${REMOTE_PATH}/jetkvm_native_debug" < internal/native/cgo/build/jknative-bin + sshdev -t ash << EOF +set -e + +# Set the library path to include the directory where librockit.so is located +export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH + +cd ${REMOTE_PATH} +killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug || true +sleep 5 +echo 'V' > /dev/watchdog +chmod +x jetkvm_native_debug +gdbserver localhost:${GDB_DEBUG_PORT} ./jetkvm_native_debug +EOF + exit 0 +fi + # Build the development version on the host # When using `make build_release`, the frontend will be built regardless of the `SKIP_UI_BUILD` flag # check if static/index.html exists @@ -176,10 +220,10 @@ if [ "$RUN_GO_TESTS" = true ]; then make build_dev_test msg_info "▶ Copying device-tests.tar.gz to remote host" - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz + sshdev "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz msg_info "▶ Running go tests" - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' + sshdev ash << 'EOF' set -e TMP_DIR=$(mktemp -d) cd ${TMP_DIR} @@ -222,10 +266,10 @@ then ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} # Copy the binary to the remote host as if we were the OTA updater. - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app + sshdev "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app # Reboot the device, the new app will be deployed by the startup process. - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot" + sshdev "reboot" else msg_info "▶ Building development binary" do_make build_dev \ @@ -234,21 +278,21 @@ else ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} # Kill any existing instances of the application - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + sshdev "killall jetkvm_app_debug || true" # Copy the binary to the remote host - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app + sshdev "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app if [ "$RESET_USB_HID_DEVICE" = true ]; then msg_info "▶ Resetting USB HID device" msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" # Remove the old USB gadget configuration - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" + sshdev "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" + sshdev "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" fi # Deploy and run the application on the remote host - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF + sshdev ash << EOF set -e # Set the library path to include the directory where librockit.so is located diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh new file mode 100755 index 00000000..150bef94 --- /dev/null +++ b/scripts/generate_proto.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Generate gRPC code from proto files + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$PROJECT_ROOT" + +# Check if protoc is installed +if ! command -v protoc &> /dev/null; then + echo "Error: protoc is not installed" + echo "Install it with:" + echo " apt-get install protobuf-compiler # Debian/Ubuntu" + echo " brew install protobuf # macOS" + exit 1 +fi + +# Check if protoc-gen-go is installed +if ! command -v protoc-gen-go &> /dev/null; then + echo "Error: protoc-gen-go is not installed" + echo "Install it with: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest" + exit 1 +fi + +# Check if protoc-gen-go-grpc is installed +if ! command -v protoc-gen-go-grpc &> /dev/null; then + echo "Error: protoc-gen-go-grpc is not installed" + echo "Install it with: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest" + exit 1 +fi + +# Generate code +echo "Generating gRPC code from proto files..." +protoc \ + --go_out=. \ + --go_opt=paths=source_relative \ + --go-grpc_out=. \ + --go-grpc_opt=paths=source_relative \ + internal/native/proto/native.proto + +echo "Done!" + diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100644 index 00000000..b6afef91 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,134 @@ +#!/bin/bash +set -eE +set -o pipefail + +SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") +source ${SCRIPT_PATH}/build_utils.sh + +# Function to display help message +show_help() { + echo "Usage: $0 [options] -v " + echo + echo "Required:" + echo " --app-version App version to release" + echo " --system-version System version to release" + echo + echo "Optional:" + echo " -u, --user Remote username (default: root)" + echo " --run-go-tests Run go tests" + echo " --run-go-tests-only Run go tests and exit" + echo " --skip-ui-build Skip frontend/UI build" + echo " --skip-native-build Skip native build" + echo " --disable-docker Disable docker build" + echo " -i, --install Build for release and install the app" + echo " --help Display this help message" + echo + echo "Example:" + echo " $0 --system-version 0.2.6" +} + + +BUILD_VERSION=$1 +R2_PATH="r2://jetkvm-update/system" +PACK_BIN_PATH="./tools/linux/Linux_Pack_Firmware" +UNPACK_BIN="${PACK_BIN_PATH}/mk-update_unpack.sh" + +# Create temporary directory for downloads +TEMP_DIR=$(mktemp -d) +msg_ok "Created temporary directory: $TEMP_DIR" + +# Cleanup function +cleanup() { + if [ -d "$TEMP_DIR" ]; then + msg_info "Cleaning up temporary directory: $TEMP_DIR" + rm -rf "$TEMP_DIR" + fi +} + +# Set trap to cleanup on exit +# trap cleanup EXIT + +mkdir -p ${TEMP_DIR}/extracted-update +${UNPACK_BIN} -i update.img -o ${TEMP_DIR}/extracted-update + +exit 0 +# Check if the version already exists +if rclone lsf $R2_PATH/$BUILD_VERSION/ | grep -q .; then + msg_err "Error: Version $BUILD_VERSION already exists in the remote storage." + exit 1 +fi + +# Check if the version exists in the github +RELEASE_URL="https://api.github.com/repos/jetkvm/rv1106-system/releases/tags/v$BUILD_VERSION" + +# Download the release JSON +RELEASE_JSON=$(curl -s $RELEASE_URL) + +# Check if the release has assets we need +if echo $RELEASE_JSON | jq -e '.assets | length == 0' > /dev/null; then + msg_err "Error: Version $BUILD_VERSION does not have assets we need." + exit 1 +fi + +function get_file_by_name() { + local file_name=$1 + local file_url=$(echo $RELEASE_JSON | jq -r ".assets[] | select(.name == \"$file_name\") | .browser_download_url") + if [ -z "$file_url" ]; then + msg_err "Error: File $file_name not found in the release." + exit 1 + fi + local digest=$(echo $RELEASE_JSON | jq -r ".assets[] | select(.name == \"$file_name\") | .digest") + local temp_file_path="$TEMP_DIR/$file_name" + + msg_info "Downloading $file_name: $file_url" + + # Download the file to temporary directory + curl -L -o "$temp_file_path" "$file_url" + + # Verify digest if available + if [ "$digest" != "null" ] && [ -n "$digest" ]; then + msg_info "Verifying digest for $file_name ..." + local calculated_digest=$(sha256sum "$temp_file_path" | cut -d' ' -f1) + # Strip "sha256:" prefix if present + local expected_digest=$(echo "$digest" | sed 's/^sha256://') + if [ "$calculated_digest" != "$expected_digest" ]; then + msg_err "🙅 Digest verification failed for $file_name" + msg_info "Expected: $expected_digest" + msg_info "Calculated: $calculated_digest" + exit 1 + fi + else + msg_warn "Warning: No digest available for $file_name, skipping verification" + fi + + msg_ok "✅ $file_name downloaded and verified." +} + +get_file_by_name "update_ota.tar" +get_file_by_name "update.img" + +strings -d bin/jetkvm_app | grep -x '0.4.8' + +# Ask for confirmation +msg_info "Do you want to continue with the release? (y/n)" +read -n 1 -s -r -p "Press y to continue, any other key to exit" +echo -ne "\n" +if [ "$REPLY" != "y" ]; then + msg_err "🙅 Release cancelled." + exit 1 +fi + +msg_info "Releasing $BUILD_VERSION..." + +sha256sum $TEMP_DIR/update_ota.tar | awk '{print $1}' > $TEMP_DIR/update_ota.tar.sha256 +sha256sum $TEMP_DIR/update.img | awk '{print $1}' > $TEMP_DIR/update.img.sha256 + +# Check if the version already exists +msg_info "Copying to $R2_PATH/$BUILD_VERSION/" + +rclone copyto --progress $TEMP_DIR/update_ota.tar $R2_PATH/$BUILD_VERSION/system.tar +rclone copyto --progress $TEMP_DIR/update_ota.tar.sha256 $R2_PATH/$BUILD_VERSION/system.tar.sha256 +rclone copyto --progress $TEMP_DIR/update.img $R2_PATH/$BUILD_VERSION/update.img +rclone copyto --progress $TEMP_DIR/update.img.sha256 $R2_PATH/$BUILD_VERSION/update.img.sha256 + +msg_ok "✅ $BUILD_VERSION released." \ No newline at end of file diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index 5de1efd2..1d94afc5 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -47,52 +47,8 @@ "access_tls_self_signed": "Selvsigneret", "access_tls_updated": "TLS-indstillingerne er blevet opdateret", "access_update_tls_settings": "Opdater TLS-indstillinger", - "action_bar_audio": "Lyd", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Forbindelsesstatistik", - "audio_input_failed_disable": "Kunne ikke deaktivere lydindgang: {error}", - "audio_input_failed_enable": "Kunne ikke aktivere lydindgang: {error}", - "audio_input_auto_enable_disabled": "Automatisk aktivering af mikrofon deaktiveret", - "audio_input_auto_enable_enabled": "Automatisk aktivering af mikrofon aktiveret", - "audio_output_disabled": "Lydudgang deaktiveret", - "audio_output_enabled": "Lydudgang aktiveret", - "audio_output_failed_disable": "Kunne ikke deaktivere lydudgang: {error}", - "audio_output_failed_enable": "Kunne ikke aktivere lydudgang: {error}", - "audio_popover_title": "Lyd", - "audio_popover_description": "Hurtige lydkontroller til højttalere og mikrofon", - "audio_speakers_title": "Højttalere", - "audio_speakers_description": "Lyd fra mål til højttalere", - "audio_microphone_title": "Mikrofon", - "audio_microphone_description": "Mikrofonindgang til mål", - "audio_https_only": "Kun HTTPS", - "audio_settings_description": "Konfigurer lydindgangs- og lydudgangsindstillinger for din JetKVM-enhed", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Aktiver eller deaktiver lyd fra fjerncomputeren", - "audio_settings_output_source_description": "Vælg lydoptagelsesenheden (HDMI eller USB)", - "audio_settings_output_source_failed": "Kunne ikke indstille lydudgangskilde: {error}", - "audio_settings_output_source_success": "Lydudgangskilde opdateret. Lyd starter om 30-60 sekunder.", - "audio_settings_output_source_title": "Lydudgangskilde", - "audio_settings_output_title": "Lydudgang", - "audio_settings_title": "Lyd", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", - "audio_settings_auto_enable_microphone_description": "Aktiver automatisk browsermikrofon ved tilslutning (ellers skal du aktivere det manuelt ved hver session)", - "audio_settings_bitrate_title": "Opus Bitrate", - "audio_settings_bitrate_description": "Lydkodningsbitrate (højere = bedre kvalitet, mere båndbredde)", - "audio_settings_complexity_title": "Opus Kompleksitet", - "audio_settings_complexity_description": "Encoder-kompleksitet (0-10, højere = bedre kvalitet, mere CPU)", - "audio_settings_dtx_title": "DTX (Diskontinuerlig Transmission)", - "audio_settings_dtx_description": "Spar båndbredde under stilhed", - "audio_settings_fec_title": "FEC (Fremadrettet Fejlkorrektion)", - "audio_settings_fec_description": "Forbedre lydkvaliteten på tabende forbindelser", - "audio_settings_buffer_title": "Bufferperioder", - "audio_settings_buffer_description": "ALSA bufferstørrelse (højere = mere stabil, mere latens)", - "audio_settings_sample_rate_title": "Samplingsrate", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Pakketabskompensation", - "audio_settings_packet_loss_description": "FEC overhead-procent (højere = bedre gendannelse, mere båndbredde)", - "audio_settings_config_updated": "Lydkonfiguration opdateret", - "audio_settings_apply_button": "Anvend indstillinger", - "audio_settings_applied": "Lydindstillinger anvendt", "action_bar_extension": "Udvidelse", "action_bar_fullscreen": "Fuldskærm", "action_bar_settings": "Indstillinger", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Lydudgangskilde opdateret. Lyd starter om 30-60 sekunder.", "audio_settings_output_source_title": "Lydudgangskilde", "audio_settings_output_title": "Lydudgang", + "audio_settings_packet_loss_description": "FEC overhead-procent (højere = bedre gendannelse, mere båndbredde)", + "audio_settings_packet_loss_title": "Pakketabskompensation", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Samplingsrate", "audio_settings_title": "Lyd", "audio_settings_usb_label": "USB", "audio_speakers_description": "Lyd fra mål til højttalere", @@ -881,8 +841,8 @@ "usb_device_description": "USB-enheder, der skal emuleres på målcomputeren", "usb_device_enable_absolute_mouse_description": "Aktivér absolut mus (markør)", "usb_device_enable_absolute_mouse_title": "Aktivér absolut mus (markør)", - "usb_device_enable_audio_description": "Aktiver tovejs lyd", - "usb_device_enable_audio_title": "Aktiver USB-lyd", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Aktivér tastatur", "usb_device_enable_keyboard_title": "Aktivér tastatur", "usb_device_enable_mass_storage_description": "Nogle gange skal det muligvis deaktiveres for at forhindre problemer med bestemte enheder.", @@ -892,7 +852,7 @@ "usb_device_failed_load": "Kunne ikke indlæse USB-enheder: {error}", "usb_device_failed_set": "Kunne ikke indstille USB-enheder: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, mus, masselager og lyd", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Kun tastatur", "usb_device_restore_default": "Gendan til standard", "usb_device_title": "USB-enhed", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 6db2d90d..665fce25 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -49,50 +49,6 @@ "access_update_tls_settings": "TLS-Einstellungen aktualisieren", "action_bar_audio": "Audio", "action_bar_connection_stats": "Verbindungsstatistiken", - "audio_input_failed_disable": "Fehler beim Deaktivieren des Audioeingangs: {error}", - "audio_input_failed_enable": "Fehler beim Aktivieren des Audioeingangs: {error}", - "audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert", - "audio_input_auto_enable_enabled": "Automatische Mikrofonaktivierung aktiviert", - "audio_output_disabled": "Audioausgang deaktiviert", - "audio_output_enabled": "Audioausgang aktiviert", - "audio_output_failed_disable": "Fehler beim Deaktivieren des Audioausgangs: {error}", - "audio_output_failed_enable": "Fehler beim Aktivieren des Audioausgangs: {error}", - "audio_popover_title": "Audio", - "audio_popover_description": "Schnelle Audiosteuerung für Lautsprecher und Mikrofon", - "audio_speakers_title": "Lautsprecher", - "audio_speakers_description": "Audio vom Ziel zu Lautsprechern", - "audio_microphone_title": "Mikrofon", - "audio_microphone_description": "Mikrofoneingang zum Ziel", - "audio_https_only": "Nur HTTPS", - "audio_settings_description": "Konfigurieren Sie Audio-Eingangs- und Ausgangseinstellungen für Ihr JetKVM-Gerät", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Audio vom entfernten Computer aktivieren oder deaktivieren", - "audio_settings_output_source_description": "Wählen Sie das Audioaufnahmegerät (HDMI oder USB)", - "audio_settings_output_source_failed": "Fehler beim Festlegen der Audioausgabequelle: {error}", - "audio_settings_output_source_success": "Audioausgabequelle aktualisiert. Audio startet in 30-60 Sekunden.", - "audio_settings_output_source_title": "Audioausgabequelle", - "audio_settings_output_title": "Audioausgang", - "audio_settings_title": "Audio", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Mikrofon automatisch aktivieren", - "audio_settings_auto_enable_microphone_description": "Browser-Mikrofon beim Verbinden automatisch aktivieren (andernfalls müssen Sie es in jeder Sitzung manuell aktivieren)", - "audio_settings_bitrate_title": "Opus Bitrate", - "audio_settings_bitrate_description": "Audio-Codierungsbitrate (höher = bessere Qualität, mehr Bandbreite)", - "audio_settings_complexity_title": "Opus Komplexität", - "audio_settings_complexity_description": "Encoder-Komplexität (0-10, höher = bessere Qualität, mehr CPU)", - "audio_settings_dtx_title": "DTX (Discontinuous Transmission)", - "audio_settings_dtx_description": "Bandbreite während Stille sparen", - "audio_settings_fec_title": "FEC (Forward Error Correction)", - "audio_settings_fec_description": "Audioqualität bei verlustbehafteten Verbindungen verbessern", - "audio_settings_buffer_title": "Pufferperioden", - "audio_settings_buffer_description": "ALSA-Puffergröße (höher = stabiler, mehr Latenz)", - "audio_settings_sample_rate_title": "Abtastrate", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Paketverlust-Kompensation", - "audio_settings_packet_loss_description": "FEC-Overhead-Prozentsatz (höher = bessere Wiederherstellung, mehr Bandbreite)", - "audio_settings_config_updated": "Audiokonfiguration aktualisiert", - "audio_settings_apply_button": "Einstellungen anwenden", - "audio_settings_applied": "Audioeinstellungen angewendet", "action_bar_extension": "Erweiterung", "action_bar_fullscreen": "Vollbild", "action_bar_settings": "Einstellungen", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Audioausgabequelle aktualisiert. Audio startet in 30-60 Sekunden.", "audio_settings_output_source_title": "Audioausgabequelle", "audio_settings_output_title": "Audioausgang", + "audio_settings_packet_loss_description": "FEC-Overhead-Prozentsatz (höher = bessere Wiederherstellung, mehr Bandbreite)", + "audio_settings_packet_loss_title": "Paketverlust-Kompensation", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Abtastrate", "audio_settings_title": "Audio", "audio_settings_usb_label": "USB", "audio_speakers_description": "Audio vom Ziel zu Lautsprechern", @@ -881,8 +841,8 @@ "usb_device_description": "USB-Geräte zum Emulieren auf dem Zielcomputer", "usb_device_enable_absolute_mouse_description": "Absolute Maus (Zeiger) aktivieren", "usb_device_enable_absolute_mouse_title": "Absolute Maus (Zeiger) aktivieren", - "usb_device_enable_audio_description": "Bidirektionales Audio aktivieren", - "usb_device_enable_audio_title": "USB-Audio aktivieren", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Tastatur aktivieren", "usb_device_enable_keyboard_title": "Tastatur aktivieren", "usb_device_enable_mass_storage_description": "Manchmal muss es möglicherweise deaktiviert werden, um Probleme mit bestimmten Geräten zu vermeiden", @@ -892,7 +852,7 @@ "usb_device_failed_load": "USB-Geräte konnten nicht geladen werden: {error}", "usb_device_failed_set": "Fehler beim Festlegen der USB-Geräte: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, Maus und Massenspeicher", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, Maus, Massenspeicher und Audio", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Nur Tastatur", "usb_device_restore_default": "Auf Standard zurücksetzen", "usb_device_title": "USB-Gerät", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 7f97432b..aa3f22c3 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -49,53 +49,6 @@ "access_update_tls_settings": "Update TLS Settings", "action_bar_audio": "Audio", "action_bar_connection_stats": "Connection Stats", - "audio_input_failed_disable": "Failed to disable audio input: {error}", - "audio_input_failed_enable": "Failed to enable audio input: {error}", - "audio_input_auto_enable_disabled": "Auto-enable microphone disabled", - "audio_input_auto_enable_enabled": "Auto-enable microphone enabled", - "audio_output_disabled": "Audio output disabled", - "audio_output_enabled": "Audio output enabled", - "audio_output_failed_disable": "Failed to disable audio output: {error}", - "audio_output_failed_enable": "Failed to enable audio output: {error}", - "audio_popover_title": "Audio", - "audio_popover_description": "Quick audio controls for speakers and microphone", - "audio_speakers_title": "Speakers", - "audio_speakers_description": "Audio from target to speakers", - "audio_microphone_title": "Microphone", - "audio_microphone_description": "Microphone input to target", - "audio_https_only": "HTTPS only", - "audio_settings_description": "Configure audio input and output settings for your JetKVM device", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Enable or disable audio from the remote computer", - "audio_settings_output_source_description": "Select the audio capture device (HDMI or USB)", - "audio_settings_output_source_failed": "Failed to set audio output source: {error}", - "audio_settings_output_source_success": "Audio output source updated. Audio will start in 30-60 seconds.", - "audio_settings_output_source_title": "Audio Output Source", - "audio_settings_output_title": "Audio Output", - "audio_settings_title": "Audio", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Auto-enable Microphone", - "audio_settings_auto_enable_microphone_description": "Automatically enable browser microphone when connecting (otherwise you must manually enable each session)", - "audio_settings_bitrate_title": "Opus Bitrate", - "audio_settings_bitrate_description": "Audio encoding bitrate (higher = better quality, more bandwidth)", - "audio_settings_complexity_title": "Opus Complexity", - "audio_settings_complexity_description": "Encoder complexity (0-10, higher = better quality, more CPU)", - "audio_settings_dtx_title": "DTX (Discontinuous Transmission)", - "audio_settings_dtx_description": "Save bandwidth during silence", - "audio_settings_fec_title": "FEC (Forward Error Correction)", - "audio_settings_fec_description": "Improve audio quality on lossy connections", - "audio_settings_buffer_title": "Buffer Periods", - "audio_settings_buffer_description": "ALSA buffer size (higher = more stable, more latency)", - "audio_settings_sample_rate_title": "Sample Rate", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Packet Loss Compensation", - "audio_settings_packet_loss_description": "FEC overhead percentage (higher = better recovery, more bandwidth)", - "audio_settings_config_updated": "Audio configuration updated", - "audio_settings_apply_button": "Apply Settings", - "audio_settings_applied": "Audio settings applied", - "audio_settings_default_suffix": " (default)", - "audio_settings_default_lan_suffix": " (default - LAN)", - "audio_settings_no_compensation_suffix": " (no compensation)", "action_bar_extension": "Extension", "action_bar_fullscreen": "Fullscreen", "action_bar_settings": "Settings", @@ -122,6 +75,7 @@ "advanced_error_update_ssh_key": "Failed to update SSH key: {error}", "advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}", "advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}", + "advanced_error_version_update": "Failed to initiate version update: {error}", "advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)", "advanced_loopback_only_title": "Loopback-Only Mode", "advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:", @@ -148,6 +102,19 @@ "advanced_update_ssh_key_button": "Update SSH Key", "advanced_usb_emulation_description": "Control the USB emulation state", "advanced_usb_emulation_title": "USB Emulation", + "advanced_version_update_app_label": "App Version", + "advanced_version_update_button": "Update to Version", + "advanced_version_update_description": "Install a specific version from GitHub releases", + "advanced_version_update_github_link": "JetKVM releases page", + "advanced_version_update_helper": "Find available versions on the", + "advanced_version_update_reset_config_description": "Reset configuration after the update", + "advanced_version_update_reset_config_label": "Reset configuration", + "advanced_version_update_system_label": "System Version", + "advanced_version_update_target_app": "App only", + "advanced_version_update_target_both": "Both App and System", + "advanced_version_update_target_label": "What to update", + "advanced_version_update_target_system": "System only", + "advanced_version_update_title": "Update to Specific Version", "already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.", "already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.", "already_adopted_return_to_dashboard": "Return to Dashboard", @@ -192,18 +159,25 @@ "audio_settings_complexity_description": "Encoder complexity (0-10, higher = better quality, more CPU)", "audio_settings_complexity_title": "Opus Complexity", "audio_settings_config_updated": "Audio configuration updated", + "audio_settings_default_lan_suffix": " (default - LAN)", + "audio_settings_default_suffix": " (default)", "audio_settings_description": "Configure audio input and output settings for your JetKVM device", "audio_settings_dtx_description": "Save bandwidth during silence", "audio_settings_dtx_title": "DTX (Discontinuous Transmission)", "audio_settings_fec_description": "Improve audio quality on lossy connections", "audio_settings_fec_title": "FEC (Forward Error Correction)", "audio_settings_hdmi_label": "HDMI", + "audio_settings_no_compensation_suffix": " (no compensation)", "audio_settings_output_description": "Enable or disable audio from the remote computer", "audio_settings_output_source_description": "Select the audio capture device (HDMI or USB)", "audio_settings_output_source_failed": "Failed to set audio output source: {error}", "audio_settings_output_source_success": "Audio output source updated. Audio will start in 30-60 seconds.", "audio_settings_output_source_title": "Audio Output Source", "audio_settings_output_title": "Audio Output", + "audio_settings_packet_loss_description": "FEC overhead percentage (higher = better recovery, more bandwidth)", + "audio_settings_packet_loss_title": "Packet Loss Compensation", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Sample Rate", "audio_settings_title": "Audio", "audio_settings_usb_label": "USB", "audio_speakers_description": "Audio from target to speakers", @@ -267,6 +241,7 @@ "connection_stats_remote_ip_address": "Remote IP Address", "connection_stats_remote_ip_address_copy_error": "Failed to copy remote IP address", "connection_stats_remote_ip_address_copy_success": "Remote IP address { ip } copied to clipboard", + "connection_stats_remote_ip_address_description": "The IP address of the remote device.", "connection_stats_round_trip_time": "Round-Trip Time", "connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.", "connection_stats_sidebar": "Connection Stats", @@ -332,6 +307,7 @@ "general_auto_update_description": "Automatically update the device to the latest version", "general_auto_update_error": "Failed to set auto-update: {error}", "general_auto_update_title": "Auto Update", + "general_check_for_stable_updates": "Downgrade", "general_check_for_updates": "Check for Updates", "general_page_description": "Configure device settings and update preferences", "general_reboot_description": "Do you want to proceed with rebooting the system?", @@ -352,9 +328,13 @@ "general_update_checking_title": "Checking for updates…", "general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!", "general_update_completed_title": "Update Completed Successfully", + "general_update_downgrade_available_description": "A downgrade is available to revert to a previous version.", + "general_update_downgrade_available_title": "Downgrade Available", + "general_update_downgrade_button": "Downgrade Now", "general_update_error_description": "An error occurred while updating your device. Please try again later.", "general_update_error_details": "Error details: {errorMessage}", "general_update_error_title": "Update Error", + "general_update_keep_current_button": "Keep Current Version", "general_update_later_button": "Do it later", "general_update_now_button": "Update Now", "general_update_rebooting": "Rebooting to complete the update…", @@ -370,6 +350,7 @@ "general_update_up_to_date_title": "System is up to date", "general_update_updating_description": "Please don't turn off your device. This process may take a few minutes.", "general_update_updating_title": "Updating your device", + "general_update_will_disable_auto_update_description": "You're about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.", "getting_remote_session_description": "Getting remote session description attempt {attempt}", "hardware_backlight_settings_error": "Failed to set backlight settings: {error}", "hardware_backlight_settings_get_error": "Failed to get backlight settings: {error}", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index e6afd3aa..eb405855 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -49,50 +49,6 @@ "access_update_tls_settings": "Actualizar la configuración de TLS", "action_bar_audio": "Audio", "action_bar_connection_stats": "Estadísticas de conexión", - "audio_input_failed_disable": "Error al desactivar la entrada de audio: {error}", - "audio_input_failed_enable": "Error al activar la entrada de audio: {error}", - "audio_input_auto_enable_disabled": "Habilitación automática de micrófono desactivada", - "audio_input_auto_enable_enabled": "Habilitación automática de micrófono activada", - "audio_output_disabled": "Salida de audio desactivada", - "audio_output_enabled": "Salida de audio activada", - "audio_output_failed_disable": "Error al desactivar la salida de audio: {error}", - "audio_output_failed_enable": "Error al activar la salida de audio: {error}", - "audio_popover_title": "Audio", - "audio_popover_description": "Controles de audio rápidos para altavoces y micrófono", - "audio_speakers_title": "Altavoces", - "audio_speakers_description": "Audio del objetivo a los altavoces", - "audio_microphone_title": "Micrófono", - "audio_microphone_description": "Entrada de micrófono al objetivo", - "audio_https_only": "Solo HTTPS", - "audio_settings_description": "Configure los ajustes de entrada y salida de audio para su dispositivo JetKVM", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Habilitar o deshabilitar el audio de la computadora remota", - "audio_settings_output_source_description": "Seleccione el dispositivo de captura de audio (HDMI o USB)", - "audio_settings_output_source_failed": "Error al configurar la fuente de salida de audio: {error}", - "audio_settings_output_source_success": "Fuente de salida de audio actualizada. El audio comenzará en 30-60 segundos.", - "audio_settings_output_source_title": "Fuente de salida de audio", - "audio_settings_output_title": "Salida de audio", - "audio_settings_title": "Audio", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Habilitar micrófono automáticamente", - "audio_settings_auto_enable_microphone_description": "Habilitar automáticamente el micrófono del navegador al conectar (de lo contrario, debe habilitarlo manualmente en cada sesión)", - "audio_settings_bitrate_title": "Bitrate Opus", - "audio_settings_bitrate_description": "Tasa de bits de codificación de audio (mayor = mejor calidad, más ancho de banda)", - "audio_settings_complexity_title": "Complejidad Opus", - "audio_settings_complexity_description": "Complejidad del codificador (0-10, mayor = mejor calidad, más CPU)", - "audio_settings_dtx_title": "DTX (Transmisión Discontinua)", - "audio_settings_dtx_description": "Ahorrar ancho de banda durante el silencio", - "audio_settings_fec_title": "FEC (Corrección de Errores)", - "audio_settings_fec_description": "Mejorar la calidad de audio en conexiones con pérdida", - "audio_settings_buffer_title": "Períodos de Buffer", - "audio_settings_buffer_description": "Tamaño del buffer ALSA (mayor = más estable, más latencia)", - "audio_settings_sample_rate_title": "Tasa de Muestreo", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Compensación de Pérdida de Paquetes", - "audio_settings_packet_loss_description": "Porcentaje de sobrecarga FEC (mayor = mejor recuperación, más ancho de banda)", - "audio_settings_config_updated": "Configuración de audio actualizada", - "audio_settings_apply_button": "Aplicar configuración", - "audio_settings_applied": "Configuración de audio aplicada", "action_bar_extension": "Extensión", "action_bar_fullscreen": "Pantalla completa", "action_bar_settings": "Ajustes", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Fuente de salida de audio actualizada. El audio comenzará en 30-60 segundos.", "audio_settings_output_source_title": "Fuente de salida de audio", "audio_settings_output_title": "Salida de audio", + "audio_settings_packet_loss_description": "Porcentaje de sobrecarga FEC (mayor = mejor recuperación, más ancho de banda)", + "audio_settings_packet_loss_title": "Compensación de Pérdida de Paquetes", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Tasa de Muestreo", "audio_settings_title": "Audio", "audio_settings_usb_label": "USB", "audio_speakers_description": "Audio del objetivo a los altavoces", @@ -881,8 +841,8 @@ "usb_device_description": "Dispositivos USB para emular en la computadora de destino", "usb_device_enable_absolute_mouse_description": "Habilitar el puntero absoluto del ratón", "usb_device_enable_absolute_mouse_title": "Habilitar el puntero absoluto del ratón", - "usb_device_enable_audio_description": "Habilitar audio bidireccional", - "usb_device_enable_audio_title": "Habilitar audio USB", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Habilitar el teclado", "usb_device_enable_keyboard_title": "Habilitar el teclado", "usb_device_enable_mass_storage_description": "A veces puede ser necesario desactivarlo para evitar problemas con ciertos dispositivos.", @@ -892,7 +852,7 @@ "usb_device_failed_load": "No se pudieron cargar los dispositivos USB: {error}", "usb_device_failed_set": "No se pudieron configurar los dispositivos USB: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Teclado, ratón y almacenamiento masivo", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Teclado, ratón, almacenamiento masivo y audio", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Sólo teclado", "usb_device_restore_default": "Restaurar a valores predeterminados", "usb_device_title": "Dispositivo USB", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index 11a3cd9b..43e6ae37 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -49,50 +49,6 @@ "access_update_tls_settings": "Mettre à jour les paramètres TLS", "action_bar_audio": "Audio", "action_bar_connection_stats": "Statistiques de connexion", - "audio_input_failed_disable": "Échec de la désactivation de l'entrée audio : {error}", - "audio_input_failed_enable": "Échec de l'activation de l'entrée audio : {error}", - "audio_input_auto_enable_disabled": "Activation automatique du microphone désactivée", - "audio_input_auto_enable_enabled": "Activation automatique du microphone activée", - "audio_output_disabled": "Sortie audio désactivée", - "audio_output_enabled": "Sortie audio activée", - "audio_output_failed_disable": "Échec de la désactivation de la sortie audio : {error}", - "audio_output_failed_enable": "Échec de l'activation de la sortie audio : {error}", - "audio_popover_title": "Audio", - "audio_popover_description": "Contrôles audio rapides pour haut-parleurs et microphone", - "audio_speakers_title": "Haut-parleurs", - "audio_speakers_description": "Audio de la cible vers les haut-parleurs", - "audio_microphone_title": "Microphone", - "audio_microphone_description": "Entrée microphone vers la cible", - "audio_https_only": "HTTPS uniquement", - "audio_settings_description": "Configurez les paramètres d'entrée et de sortie audio pour votre appareil JetKVM", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Activer ou désactiver l'audio de l'ordinateur distant", - "audio_settings_output_source_description": "Sélectionnez le périphérique de capture audio (HDMI ou USB)", - "audio_settings_output_source_failed": "Échec de la configuration de la source de sortie audio : {error}", - "audio_settings_output_source_success": "Source de sortie audio mise à jour. L'audio démarrera dans 30 à 60 secondes.", - "audio_settings_output_source_title": "Source de sortie audio", - "audio_settings_output_title": "Sortie audio", - "audio_settings_title": "Audio", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Activer automatiquement le microphone", - "audio_settings_auto_enable_microphone_description": "Activer automatiquement le microphone du navigateur lors de la connexion (sinon vous devez l'activer manuellement à chaque session)", - "audio_settings_bitrate_title": "Débit Opus", - "audio_settings_bitrate_description": "Débit d'encodage audio (plus élevé = meilleure qualité, plus de bande passante)", - "audio_settings_complexity_title": "Complexité Opus", - "audio_settings_complexity_description": "Complexité de l'encodeur (0-10, plus élevé = meilleure qualité, plus de CPU)", - "audio_settings_dtx_title": "DTX (Transmission Discontinue)", - "audio_settings_dtx_description": "Économiser la bande passante pendant le silence", - "audio_settings_fec_title": "FEC (Correction d'Erreur)", - "audio_settings_fec_description": "Améliorer la qualité audio sur les connexions avec perte", - "audio_settings_buffer_title": "Périodes de Tampon", - "audio_settings_buffer_description": "Taille du tampon ALSA (plus élevé = plus stable, plus de latence)", - "audio_settings_sample_rate_title": "Fréquence d'Échantillonnage", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Compensation de Perte de Paquets", - "audio_settings_packet_loss_description": "Pourcentage de surcharge FEC (plus élevé = meilleure récupération, plus de bande passante)", - "audio_settings_config_updated": "Configuration audio mise à jour", - "audio_settings_apply_button": "Appliquer les paramètres", - "audio_settings_applied": "Paramètres audio appliqués", "action_bar_extension": "Extension", "action_bar_fullscreen": "Plein écran", "action_bar_settings": "Paramètres", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Source de sortie audio mise à jour. L'audio démarrera dans 30 à 60 secondes.", "audio_settings_output_source_title": "Source de sortie audio", "audio_settings_output_title": "Sortie audio", + "audio_settings_packet_loss_description": "Pourcentage de surcharge FEC (plus élevé = meilleure récupération, plus de bande passante)", + "audio_settings_packet_loss_title": "Compensation de Perte de Paquets", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Fréquence d'Échantillonnage", "audio_settings_title": "Audio", "audio_settings_usb_label": "USB", "audio_speakers_description": "Audio de la cible vers les haut-parleurs", @@ -881,8 +841,8 @@ "usb_device_description": "Périphériques USB à émuler sur l'ordinateur cible", "usb_device_enable_absolute_mouse_description": "Activer la souris absolue (pointeur)", "usb_device_enable_absolute_mouse_title": "Activer la souris absolue (pointeur)", - "usb_device_enable_audio_description": "Activer l'audio bidirectionnel", - "usb_device_enable_audio_title": "Activer l'audio USB", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Activer le clavier", "usb_device_enable_keyboard_title": "Activer le clavier", "usb_device_enable_mass_storage_description": "Parfois, il peut être nécessaire de le désactiver pour éviter des problèmes avec certains appareils", @@ -892,7 +852,7 @@ "usb_device_failed_load": "Échec du chargement des périphériques USB : {error}", "usb_device_failed_set": "Échec de la configuration des périphériques USB : {error}", "usb_device_keyboard_mouse_and_mass_storage": "Clavier, souris et stockage de masse", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Clavier, souris, stockage de masse et audio", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Clavier uniquement", "usb_device_restore_default": "Restaurer les paramètres par défaut", "usb_device_title": "périphérique USB", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 86fe5b5b..cd7c79bd 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -49,50 +49,6 @@ "access_update_tls_settings": "Aggiorna le impostazioni TLS", "action_bar_audio": "Audio", "action_bar_connection_stats": "Statistiche di connessione", - "audio_input_failed_disable": "Impossibile disabilitare l'ingresso audio: {error}", - "audio_input_failed_enable": "Impossibile abilitare l'ingresso audio: {error}", - "audio_input_auto_enable_disabled": "Abilitazione automatica microfono disabilitata", - "audio_input_auto_enable_enabled": "Abilitazione automatica microfono abilitata", - "audio_output_disabled": "Uscita audio disabilitata", - "audio_output_enabled": "Uscita audio abilitata", - "audio_output_failed_disable": "Impossibile disabilitare l'uscita audio: {error}", - "audio_output_failed_enable": "Impossibile abilitare l'uscita audio: {error}", - "audio_popover_title": "Audio", - "audio_popover_description": "Controlli audio rapidi per altoparlanti e microfono", - "audio_speakers_title": "Altoparlanti", - "audio_speakers_description": "Audio dal target agli altoparlanti", - "audio_microphone_title": "Microfono", - "audio_microphone_description": "Ingresso microfono al target", - "audio_https_only": "Solo HTTPS", - "audio_settings_description": "Configura le impostazioni di ingresso e uscita audio per il tuo dispositivo JetKVM", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Abilita o disabilita l'audio dal computer remoto", - "audio_settings_output_source_description": "Seleziona il dispositivo di acquisizione audio (HDMI o USB)", - "audio_settings_output_source_failed": "Impossibile impostare la sorgente di uscita audio: {error}", - "audio_settings_output_source_success": "Sorgente di uscita audio aggiornata. L'audio inizierà tra 30-60 secondi.", - "audio_settings_output_source_title": "Sorgente di uscita audio", - "audio_settings_output_title": "Uscita audio", - "audio_settings_title": "Audio", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Abilita automaticamente il microfono", - "audio_settings_auto_enable_microphone_description": "Abilita automaticamente il microfono del browser durante la connessione (altrimenti devi abilitarlo manualmente ad ogni sessione)", - "audio_settings_bitrate_title": "Bitrate Opus", - "audio_settings_bitrate_description": "Bitrate di codifica audio (più alto = migliore qualità, più banda)", - "audio_settings_complexity_title": "Complessità Opus", - "audio_settings_complexity_description": "Complessità dell'encoder (0-10, più alto = migliore qualità, più CPU)", - "audio_settings_dtx_title": "DTX (Trasmissione Discontinua)", - "audio_settings_dtx_description": "Risparmia banda durante il silenzio", - "audio_settings_fec_title": "FEC (Correzione Errori)", - "audio_settings_fec_description": "Migliora la qualità audio su connessioni con perdita", - "audio_settings_buffer_title": "Periodi Buffer", - "audio_settings_buffer_description": "Dimensione buffer ALSA (più alto = più stabile, più latenza)", - "audio_settings_sample_rate_title": "Frequenza di Campionamento", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Compensazione Perdita Pacchetti", - "audio_settings_packet_loss_description": "Percentuale overhead FEC (più alto = migliore recupero, più banda)", - "audio_settings_config_updated": "Configurazione audio aggiornata", - "audio_settings_apply_button": "Applica impostazioni", - "audio_settings_applied": "Impostazioni audio applicate", "action_bar_extension": "Estensione", "action_bar_fullscreen": "A schermo intero", "action_bar_settings": "Impostazioni", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Sorgente di uscita audio aggiornata. L'audio inizierà tra 30-60 secondi.", "audio_settings_output_source_title": "Sorgente di uscita audio", "audio_settings_output_title": "Uscita audio", + "audio_settings_packet_loss_description": "Percentuale overhead FEC (più alto = migliore recupero, più banda)", + "audio_settings_packet_loss_title": "Compensazione Perdita Pacchetti", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Frequenza di Campionamento", "audio_settings_title": "Audio", "audio_settings_usb_label": "USB", "audio_speakers_description": "Audio dal target agli altoparlanti", @@ -881,8 +841,8 @@ "usb_device_description": "Dispositivi USB da emulare sul computer di destinazione", "usb_device_enable_absolute_mouse_description": "Abilita mouse assoluto (puntatore)", "usb_device_enable_absolute_mouse_title": "Abilita mouse assoluto (puntatore)", - "usb_device_enable_audio_description": "Abilita audio bidirezionale", - "usb_device_enable_audio_title": "Abilita audio USB", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Abilita tastiera", "usb_device_enable_keyboard_title": "Abilita tastiera", "usb_device_enable_mass_storage_description": "A volte potrebbe essere necessario disattivarlo per evitare problemi con determinati dispositivi", @@ -892,7 +852,7 @@ "usb_device_failed_load": "Impossibile caricare i dispositivi USB: {error}", "usb_device_failed_set": "Impossibile impostare i dispositivi USB: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastiera, mouse e memoria di massa", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastiera, mouse, archiviazione di massa e audio", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Solo tastiera", "usb_device_restore_default": "Ripristina impostazioni predefinite", "usb_device_title": "Dispositivo USB", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index 696081a0..0457389c 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -47,52 +47,8 @@ "access_tls_self_signed": "Selvsignert", "access_tls_updated": "TLS-innstillingene er oppdatert", "access_update_tls_settings": "Oppdater TLS-innstillinger", - "action_bar_audio": "Lyd", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Tilkoblingsstatistikk", - "audio_input_failed_disable": "Kunne ikke deaktivere lydinngang: {error}", - "audio_input_failed_enable": "Kunne ikke aktivere lydinngang: {error}", - "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon deaktivert", - "audio_input_auto_enable_enabled": "Automatisk aktivering av mikrofon aktivert", - "audio_output_disabled": "Lydutgang deaktivert", - "audio_output_enabled": "Lydutgang aktivert", - "audio_output_failed_disable": "Kunne ikke deaktivere lydutgang: {error}", - "audio_output_failed_enable": "Kunne ikke aktivere lydutgang: {error}", - "audio_popover_title": "Lyd", - "audio_popover_description": "Raske lydkontroller for høyttalere og mikrofon", - "audio_speakers_title": "Høyttalere", - "audio_speakers_description": "Lyd fra mål til høyttalere", - "audio_microphone_title": "Mikrofon", - "audio_microphone_description": "Mikrofoninngang til mål", - "audio_https_only": "Kun HTTPS", - "audio_settings_description": "Konfigurer lydinngangs- og lydutgangsinnstillinger for JetKVM-enheten din", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Aktiver eller deaktiver lyd fra den eksterne datamaskinen", - "audio_settings_output_source_description": "Velg lydopptaksenhet (HDMI eller USB)", - "audio_settings_output_source_failed": "Kunne ikke angi lydutgangskilde: {error}", - "audio_settings_output_source_success": "Lydutgangskilde oppdatert. Lyd starter om 30-60 sekunder.", - "audio_settings_output_source_title": "Lydutgangskilde", - "audio_settings_output_title": "Lydutgang", - "audio_settings_title": "Lyd", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", - "audio_settings_auto_enable_microphone_description": "Aktiver automatisk nettlesermikrofon ved tilkobling (ellers må du aktivere det manuelt hver økt)", - "audio_settings_bitrate_title": "Opus Bitrate", - "audio_settings_bitrate_description": "Lydkodingsbitrate (høyere = bedre kvalitet, mer båndbredde)", - "audio_settings_complexity_title": "Opus Kompleksitet", - "audio_settings_complexity_description": "Encoder-kompleksitet (0-10, høyere = bedre kvalitet, mer CPU)", - "audio_settings_dtx_title": "DTX (Diskontinuerlig Overføring)", - "audio_settings_dtx_description": "Spar båndbredde under stillhet", - "audio_settings_fec_title": "FEC (Fremadrettet Feilkorreksjon)", - "audio_settings_fec_description": "Forbedre lydkvaliteten på tapende tilkoblinger", - "audio_settings_buffer_title": "Bufferperioder", - "audio_settings_buffer_description": "ALSA bufferstørrelse (høyere = mer stabil, mer latens)", - "audio_settings_sample_rate_title": "Samplingsrate", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Pakketapskompensasjon", - "audio_settings_packet_loss_description": "FEC overhead-prosent (høyere = bedre gjenoppretting, mer båndbredde)", - "audio_settings_config_updated": "Lydkonfigurasjon oppdatert", - "audio_settings_apply_button": "Bruk innstillinger", - "audio_settings_applied": "Lydinnstillinger brukt", "action_bar_extension": "Forlengelse", "action_bar_fullscreen": "Fullskjerm", "action_bar_settings": "Innstillinger", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "Lydutgangskilde oppdatert. Lyd starter om 30-60 sekunder.", "audio_settings_output_source_title": "Lydutgangskilde", "audio_settings_output_title": "Lydutgang", + "audio_settings_packet_loss_description": "FEC overhead-prosent (høyere = bedre gjenoppretting, mer båndbredde)", + "audio_settings_packet_loss_title": "Pakketapskompensasjon", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Samplingsrate", "audio_settings_title": "Lyd", "audio_settings_usb_label": "USB", "audio_speakers_description": "Lyd fra mål til høyttalere", @@ -881,8 +841,8 @@ "usb_device_description": "USB-enheter som skal emuleres på måldatamaskinen", "usb_device_enable_absolute_mouse_description": "Aktiver absolutt mus (peker)", "usb_device_enable_absolute_mouse_title": "Aktiver absolutt mus (peker)", - "usb_device_enable_audio_description": "Aktiver toveis lyd", - "usb_device_enable_audio_title": "Aktiver USB-lyd", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Aktiver tastatur", "usb_device_enable_keyboard_title": "Aktiver tastatur", "usb_device_enable_mass_storage_description": "Noen ganger må det kanskje deaktiveres for å forhindre problemer med visse enheter.", @@ -892,7 +852,7 @@ "usb_device_failed_load": "Klarte ikke å laste inn USB-enheter: {error}", "usb_device_failed_set": "Kunne ikke angi USB-enheter: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, mus, masselagring og lyd", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Kun tastatur", "usb_device_restore_default": "Gjenopprett til standard", "usb_device_title": "USB-enhet", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 012f4592..f2af9932 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -47,51 +47,7 @@ "access_tls_self_signed": "Självsignerad", "access_tls_updated": "TLS-inställningarna har uppdaterats", "access_update_tls_settings": "Uppdatera TLS-inställningar", - "action_bar_audio": "Ljud", - "audio_input_failed_disable": "Det gick inte att inaktivera ljudingången: {error}", - "audio_input_failed_enable": "Det gick inte att aktivera ljudingången: {error}", - "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon inaktiverad", - "audio_input_auto_enable_enabled": "Automatisk aktivering av mikrofon aktiverad", - "audio_output_disabled": "Ljudutgång inaktiverad", - "audio_output_enabled": "Ljudutgång aktiverad", - "audio_output_failed_disable": "Det gick inte att inaktivera ljudutgången: {error}", - "audio_output_failed_enable": "Det gick inte att aktivera ljudutgången: {error}", - "audio_popover_title": "Ljud", - "audio_popover_description": "Snabba ljudkontroller för högtalare och mikrofon", - "audio_speakers_title": "Högtalare", - "audio_speakers_description": "Ljud från mål till högtalare", - "audio_microphone_title": "Mikrofon", - "audio_microphone_description": "Mikrofoningång till mål", - "audio_https_only": "Endast HTTPS", - "audio_settings_description": "Konfigurera ljudinmatnings- och ljudutgångsinställningar för din JetKVM-enhet", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "Aktivera eller inaktivera ljud från fjärrdatorn", - "audio_settings_output_source_description": "Välj ljudinspelningsenhet (HDMI eller USB)", - "audio_settings_output_source_failed": "Det gick inte att ställa in ljudutgångskälla: {error}", - "audio_settings_output_source_success": "Ljudutgångskälla uppdaterad. Ljud startar om 30-60 sekunder.", - "audio_settings_output_source_title": "Ljudutgångskälla", - "audio_settings_output_title": "Ljudutgång", - "audio_settings_title": "Ljud", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "Aktivera mikrofon automatiskt", - "audio_settings_auto_enable_microphone_description": "Aktivera automatiskt webbläsarmikrofon vid anslutning (annars måste du aktivera den manuellt varje session)", - "audio_settings_bitrate_title": "Opus Bitrate", - "audio_settings_bitrate_description": "Ljudkodningsbitrate (högre = bättre kvalitet, mer bandbredd)", - "audio_settings_complexity_title": "Opus Komplexitet", - "audio_settings_complexity_description": "Encoder-komplexitet (0-10, högre = bättre kvalitet, mer CPU)", - "audio_settings_dtx_title": "DTX (Diskontinuerlig Överföring)", - "audio_settings_dtx_description": "Spara bandbredd under tystnad", - "audio_settings_fec_title": "FEC (Framåtriktad Felkorrigering)", - "audio_settings_fec_description": "Förbättra ljudkvaliteten på förlustdrabbade anslutningar", - "audio_settings_buffer_title": "Bufferperioder", - "audio_settings_buffer_description": "ALSA bufferstorlek (högre = mer stabil, mer latens)", - "audio_settings_sample_rate_title": "Samplingsfrekvens", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "Paketförlustkompensation", - "audio_settings_packet_loss_description": "FEC overhead-procent (högre = bättre återställning, mer bandbredd)", - "audio_settings_config_updated": "Ljudkonfiguration uppdaterad", - "audio_settings_apply_button": "Tillämpa inställningar", - "audio_settings_applied": "Ljudinställningar tillämpade", + "action_bar_audio": "Audio", "action_bar_extension": "Förlängning", "action_bar_fullscreen": "Helskärm", "action_bar_settings": "Inställningar", @@ -200,6 +156,10 @@ "audio_settings_output_source_success": "Ljudutgångskälla uppdaterad. Ljud startar om 30-60 sekunder.", "audio_settings_output_source_title": "Ljudutgångskälla", "audio_settings_output_title": "Ljudutgång", + "audio_settings_packet_loss_description": "FEC overhead-procent (högre = bättre återställning, mer bandbredd)", + "audio_settings_packet_loss_title": "Paketförlustkompensation", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "Samplingsfrekvens", "audio_settings_title": "Ljud", "audio_settings_usb_label": "USB", "audio_speakers_description": "Ljud från mål till högtalare", @@ -880,8 +840,8 @@ "usb_device_description": "USB-enheter att emulera på måldatorn", "usb_device_enable_absolute_mouse_description": "Aktivera absolut mus (pekare)", "usb_device_enable_absolute_mouse_title": "Aktivera absolut mus (pekare)", - "usb_device_enable_audio_description": "Aktivera dubbelriktad ljud", - "usb_device_enable_audio_title": "Aktivera USB-ljud", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Aktivera tangentbord", "usb_device_enable_keyboard_title": "Aktivera tangentbord", "usb_device_enable_mass_storage_description": "Ibland kan det behöva inaktiveras för att förhindra problem med vissa enheter.", @@ -891,7 +851,7 @@ "usb_device_failed_load": "Misslyckades med att ladda USB-enheter: {error}", "usb_device_failed_set": "Misslyckades med att ställa in USB-enheter: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tangentbord, mus och masslagring", - "usb_device_keyboard_mouse_mass_storage_and_audio": "Tangentbord, mus, masslagring och ljud", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Endast tangentbord", "usb_device_restore_default": "Återställ till standard", "usb_device_title": "USB-enhet", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index a1123a94..c7c73572 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -47,52 +47,8 @@ "access_tls_self_signed": "自签名", "access_tls_updated": "TLS 设置更新成功", "access_update_tls_settings": "更新 TLS 设置", - "action_bar_audio": "音频", + "action_bar_audio": "Audio", "action_bar_connection_stats": "连接统计", - "audio_input_failed_disable": "禁用音频输入失败:{error}", - "audio_input_failed_enable": "启用音频输入失败:{error}", - "audio_input_auto_enable_disabled": "自动启用麦克风已禁用", - "audio_input_auto_enable_enabled": "自动启用麦克风已启用", - "audio_output_disabled": "音频输出已禁用", - "audio_output_enabled": "音频输出已启用", - "audio_output_failed_disable": "禁用音频输出失败:{error}", - "audio_output_failed_enable": "启用音频输出失败:{error}", - "audio_popover_title": "音频", - "audio_popover_description": "扬声器和麦克风的快速音频控制", - "audio_speakers_title": "扬声器", - "audio_speakers_description": "从目标设备到扬声器的音频", - "audio_microphone_title": "麦克风", - "audio_microphone_description": "麦克风输入到目标设备", - "audio_https_only": "仅限 HTTPS", - "audio_settings_description": "配置 JetKVM 设备的音频输入和输出设置", - "audio_settings_hdmi_label": "HDMI", - "audio_settings_output_description": "启用或禁用来自远程计算机的音频", - "audio_settings_output_source_description": "选择音频捕获设备(HDMI 或 USB)", - "audio_settings_output_source_failed": "设置音频输出源失败:{error}", - "audio_settings_output_source_success": "音频输出源已更新。音频将在30-60秒内启动。", - "audio_settings_output_source_title": "音频输出源", - "audio_settings_output_title": "音频输出", - "audio_settings_title": "音频", - "audio_settings_usb_label": "USB", - "audio_settings_auto_enable_microphone_title": "自动启用麦克风", - "audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)", - "audio_settings_bitrate_title": "Opus 比特率", - "audio_settings_bitrate_description": "音频编码比特率(越高 = 质量越好,带宽越大)", - "audio_settings_complexity_title": "Opus 复杂度", - "audio_settings_complexity_description": "编码器复杂度(0-10,越高 = 质量越好,CPU 使用越多)", - "audio_settings_dtx_title": "DTX(不连续传输)", - "audio_settings_dtx_description": "在静音时节省带宽", - "audio_settings_fec_title": "FEC(前向纠错)", - "audio_settings_fec_description": "改善有损连接上的音频质量", - "audio_settings_buffer_title": "缓冲周期", - "audio_settings_buffer_description": "ALSA 缓冲大小(越高 = 越稳定,延迟越高)", - "audio_settings_sample_rate_title": "采样率", - "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", - "audio_settings_packet_loss_title": "丢包补偿", - "audio_settings_packet_loss_description": "FEC 开销百分比(越高 = 恢复越好,带宽越大)", - "audio_settings_config_updated": "音频配置已更新", - "audio_settings_apply_button": "应用设置", - "audio_settings_applied": "音频设置已应用", "action_bar_extension": "扩展", "action_bar_fullscreen": "全屏", "action_bar_settings": "设置", @@ -201,6 +157,10 @@ "audio_settings_output_source_success": "音频输出源已更新。音频将在30-60秒内启动。", "audio_settings_output_source_title": "音频输出源", "audio_settings_output_title": "音频输出", + "audio_settings_packet_loss_description": "FEC 开销百分比(越高 = 恢复越好,带宽越大)", + "audio_settings_packet_loss_title": "丢包补偿", + "audio_settings_sample_rate_description": "Audio sampling frequency (automatically detected from source)", + "audio_settings_sample_rate_title": "采样率", "audio_settings_title": "音频", "audio_settings_usb_label": "USB", "audio_speakers_description": "从目标设备到扬声器的音频", @@ -881,8 +841,8 @@ "usb_device_description": "在目标计算机上仿真的 USB 设备", "usb_device_enable_absolute_mouse_description": "启用绝对鼠标(指针)", "usb_device_enable_absolute_mouse_title": "启用绝对鼠标(指针)", - "usb_device_enable_audio_description": "启用双向音频", - "usb_device_enable_audio_title": "启用 USB 音频", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "启用键盘", "usb_device_enable_keyboard_title": "启用键盘", "usb_device_enable_mass_storage_description": "有时可能需要禁用它以防止某些设备出现问题", @@ -892,7 +852,7 @@ "usb_device_failed_load": "无法加载 USB 设备: {error}", "usb_device_failed_set": "无法设置 USB 设备: {error}", "usb_device_keyboard_mouse_and_mass_storage": "键盘、鼠标和大容量存储器", - "usb_device_keyboard_mouse_mass_storage_and_audio": "键盘、鼠标、大容量存储和音频", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "仅限键盘", "usb_device_restore_default": "恢复默认设置", "usb_device_title": "USB 设备", diff --git a/ui/src/components/FailSafeModeOverlay.tsx b/ui/src/components/FailSafeModeOverlay.tsx index eadc5d9d..f60f4216 100644 --- a/ui/src/components/FailSafeModeOverlay.tsx +++ b/ui/src/components/FailSafeModeOverlay.tsx @@ -1,10 +1,9 @@ import { useState } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { motion, AnimatePresence } from "framer-motion"; -import { LuInfo } from "react-icons/lu"; import { Button } from "@/components/Button"; -import Card, { GridCard } from "@components/Card"; +import { GridCard } from "@components/Card"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useVersion } from "@/hooks/useVersion"; @@ -34,40 +33,12 @@ function OverlayContent({ children }: OverlayContentProps) { ); } -interface TooltipProps { - readonly children: React.ReactNode; - readonly text: string; - readonly show: boolean; -} - -function Tooltip({ children, text, show }: TooltipProps) { - if (!show) { - return <>{children}; - } - - - return ( -
- {children} -
- -
- - {text} -
-
-
-
- ); -} - export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) { const { send } = useJsonRpc(); const { navigateTo } = useDeviceUiNavigation(); const { appVersion } = useVersion(); const { systemVersion } = useDeviceStore(); const [isDownloadingLogs, setIsDownloadingLogs] = useState(false); - const [hasDownloadedLogs, setHasDownloadedLogs] = useState(false); const getReasonCopy = () => { switch (reason) { @@ -115,7 +86,6 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) { URL.revokeObjectURL(url); notifications.success("Crash logs downloaded successfully"); - setHasDownloadedLogs(true); // Open GitHub issue const issueBody = `## Issue Description @@ -146,7 +116,7 @@ Please attach the recovery logs file that was downloaded to your computer: }; const handleDowngrade = () => { - navigateTo(`/settings/general/update?app=${DOWNGRADE_VERSION}`); + navigateTo(`/settings/general/update?custom_app_version=${DOWNGRADE_VERSION}`); }; return ( @@ -182,25 +152,19 @@ Please attach the recovery logs file that was downloaded to your computer: />
- -
diff --git a/ui/src/components/NestedSettingsGroup.tsx b/ui/src/components/NestedSettingsGroup.tsx new file mode 100644 index 00000000..3ee57b0f --- /dev/null +++ b/ui/src/components/NestedSettingsGroup.tsx @@ -0,0 +1,22 @@ +import { cx } from "@/cva.config"; + +interface NestedSettingsGroupProps { + readonly children: React.ReactNode; + readonly className?: string; +} + +export function NestedSettingsGroup(props: NestedSettingsGroupProps) { + const { children, className } = props; + + return ( +
+ {children} +
+ ); +} + diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index e444179b..65c75ffb 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -553,7 +553,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu controlsList="nofullscreen" style={videoStyle} className={cx( - "max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000", + "max-h-full max-w-full sm:min-h-[384px] sm:min-w-[512px] bg-black/50 object-contain transition-all duration-1000", { "cursor-none": settings.isCursorHidden, "!opacity-0": diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx index 94c2f99d..feb99617 100644 --- a/ui/src/hooks/useVersion.tsx +++ b/ui/src/hooks/useVersion.tsx @@ -6,6 +6,19 @@ import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/ import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; +export interface VersionInfo { + appVersion: string; + systemVersion: string; +} + +export interface SystemVersionInfo { + local: VersionInfo; + remote?: VersionInfo; + systemUpdateAvailable: boolean; + appUpdateAvailable: boolean; + error?: string; +} + export function useVersion() { const { appVersion, diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index 766b8c4a..9b2d3cd3 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -11,6 +11,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SettingsItem } from "@components/SettingsItem"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; +import { NestedSettingsGroup } from "@components/NestedSettingsGroup"; import { TextAreaWithLabel } from "@components/TextArea"; import api from "@/api"; import notifications from "@/notifications"; @@ -237,39 +238,30 @@ export default function SettingsAccessIndexRoute() { {tlsMode === "custom" && ( -
-
- -
- handleTlsCertChange(e.target.value)} - /> -
- -
-
- handleTlsKeyChange(e.target.value)} - /> -
-
-
+ + + handleTlsCertChange(e.target.value)} + /> + handleTlsKeyChange(e.target.value)} + />
-
+ )} {selectedProvider === "custom" && ( -
+
-
+ )} )} diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index 4c3c9e94..dd39f9b6 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -1,21 +1,30 @@ import { useCallback, useEffect, useState } from "react"; import { useSettingsStore } from "@hooks/stores"; -import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; +import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; +import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; import { Button } from "@components/Button"; -import Checkbox from "@components/Checkbox"; +import Checkbox, { CheckboxWithLabel } from "@components/Checkbox"; import { ConfirmDialog } from "@components/ConfirmDialog"; import { GridCard } from "@components/Card"; import { SettingsItem } from "@components/SettingsItem"; import { SettingsPageHeader } from "@components/SettingsPageheader"; +import { NestedSettingsGroup } from "@components/NestedSettingsGroup"; import { TextAreaWithLabel } from "@components/TextArea"; +import { InputFieldWithLabel } from "@components/InputField"; +import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { isOnDevice } from "@/main"; import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; import { sleep } from "@/utils"; +import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc"; +import { SystemVersionInfo } from "@hooks/useVersion"; + +import { FeatureFlag } from "../components/FeatureFlag"; export default function SettingsAdvancedRoute() { const { send } = useJsonRpc(); + const { navigateTo } = useDeviceUiNavigation(); const [sshKey, setSSHKey] = useState(""); const { setDeveloperMode } = useSettingsStore(); @@ -23,7 +32,12 @@ export default function SettingsAdvancedRoute() { const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false); const [showLoopbackWarning, setShowLoopbackWarning] = useState(false); const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false); - + const [updateTarget, setUpdateTarget] = useState("app"); + const [appVersion, setAppVersion] = useState(""); + const [systemVersion, setSystemVersion] = useState(""); + const [resetConfig, setResetConfig] = useState(false); + const [versionChangeAcknowledged, setVersionChangeAcknowledged] = useState(false); + const [customVersionUpdateLoading, setCustomVersionUpdateLoading] = useState(false); const settings = useSettingsStore(); useEffect(() => { @@ -173,6 +187,61 @@ export default function SettingsAdvancedRoute() { setShowLoopbackWarning(false); }, [applyLoopbackOnlyMode, setShowLoopbackWarning]); + const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => { + notifications.error( + m.advanced_error_version_update({ + error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error()) + }), + { duration: 1000 * 15 } // 15 seconds + ); + setCustomVersionUpdateLoading(false); + }, []); + + const handleCustomVersionUpdate = useCallback(async () => { + const components: UpdateComponents = {}; + if (["app", "both"].includes(updateTarget) && appVersion) components.app = appVersion; + if (["system", "both"].includes(updateTarget) && systemVersion) components.system = systemVersion; + let versionInfo: SystemVersionInfo | undefined; + + try { + // we do not need to set it to false if check succeeds, + // because it will be redirected to the update page later + setCustomVersionUpdateLoading(true); + versionInfo = await checkUpdateComponents({ + components, + }, devChannel); + } catch (error: unknown) { + const jsonRpcError = error as JsonRpcError; + handleVersionUpdateError(jsonRpcError); + return; + } + + let hasUpdate = false; + + const pageParams = new URLSearchParams(); + if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) { + hasUpdate = true; + pageParams.set("custom_app_version", versionInfo.remote?.appVersion); + } + if (components.system && versionInfo?.remote?.systemVersion && versionInfo?.systemUpdateAvailable) { + hasUpdate = true; + pageParams.set("custom_system_version", versionInfo.remote?.systemVersion); + } + pageParams.set("reset_config", resetConfig.toString()); + + if (!hasUpdate) { + handleVersionUpdateError("No update available"); + return; + } + + // Navigate to update page + navigateTo(`/settings/general/update?${pageParams.toString()}`); + }, [ + updateTarget, appVersion, systemVersion, devChannel, + navigateTo, resetConfig, handleVersionUpdateError, + setCustomVersionUpdateLoading + ]); + return (
handleDevModeChange(e.target.checked)} /> - - {settings.developerMode && ( - -
- - - -
-
-

- {m.advanced_developer_mode_enabled_title()} -

-
-
    -
  • {m.advanced_developer_mode_warning_security()}
  • -
  • {m.advanced_developer_mode_warning_risks()}
  • -
+ {settings.developerMode ? ( + + +
+ + + +
+
+

+ {m.advanced_developer_mode_enabled_title()} +

+
+
    +
  • {m.advanced_developer_mode_warning_security()}
  • +
  • {m.advanced_developer_mode_warning_risks()}
  • +
+
+
+
+ {m.advanced_developer_mode_warning_advanced()}
-
- {m.advanced_developer_mode_warning_advanced()} +
+ + + {isOnDevice && ( +
+ + setSSHKey(e.target.value)} + placeholder={m.advanced_ssh_public_key_placeholder()} + /> +

+ {m.advanced_ssh_default_user()}root. +

+
+
-
-
- )} + )} + + +
+ + + setUpdateTarget(e.target.value)} + /> + + {(updateTarget === "app" || updateTarget === "both") && ( + setAppVersion(e.target.value)} + /> + )} + + {(updateTarget === "system" || updateTarget === "both") && ( + setSystemVersion(e.target.value)} + /> + )} + +

+ {m.advanced_version_update_helper()}{" "} + + {m.advanced_version_update_github_link()} + +

+ +
+ setResetConfig(e.target.checked)} + /> +
+ +
+ setVersionChangeAcknowledged(e.target.checked)} + /> +
+ +
+
+
+ ) : null} - {isOnDevice && settings.developerMode && ( -
- -
- setSSHKey(e.target.value)} - placeholder={m.advanced_ssh_public_key_placeholder()} - /> -

- {m.advanced_ssh_default_user()}root. -

-
-
-
-
- )} + {settings.debugMode && ( - <> + - + )}
diff --git a/ui/src/routes/devices.$id.settings.general._index.tsx b/ui/src/routes/devices.$id.settings.general._index.tsx index 86e92bcd..802e8737 100644 --- a/ui/src/routes/devices.$id.settings.general._index.tsx +++ b/ui/src/routes/devices.$id.settings.general._index.tsx @@ -17,7 +17,6 @@ export default function SettingsGeneralRoute() { const { send } = useJsonRpc(); const { navigateTo } = useDeviceUiNavigation(); const [autoUpdate, setAutoUpdate] = useState(true); - const currentVersions = useDeviceStore(state => { const { appVersion, systemVersion } = state; if (!appVersion || !systemVersion) return null; @@ -48,10 +47,10 @@ export default function SettingsGeneralRoute() { const localeOptions = useMemo(() => { return ["", ...locales] .map((code) => { - const [localizedName, nativeName] = map_locale_code_to_name(currentLocale, code); - // don't repeat the name if it's the same in both locales (or blank) - const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName; - return { value: code, label: label } + const [localizedName, nativeName] = map_locale_code_to_name(currentLocale, code); + // don't repeat the name if it's the same in both locales (or blank) + const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName; + return { value: code, label: label } }); }, [currentLocale]); @@ -85,6 +84,8 @@ export default function SettingsGeneralRoute() {
@@ -108,7 +109,7 @@ export default function SettingsGeneralRoute() { } /> -
+
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 42776414..3527f269 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -8,6 +8,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SettingsItem } from "@components/SettingsItem"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; +import { NestedSettingsGroup } from "@components/NestedSettingsGroup"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbInfoSetting } from "@components/UsbInfoSetting"; import notifications from "@/notifications"; @@ -156,7 +157,7 @@ export default function SettingsHardwareRoute() { />
{backlightSettings.max_brightness != 0 && ( - <> + - + )}

{m.hardware_display_wake_up_note()} diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index d4dd0bd7..8fdece8b 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -278,6 +278,14 @@ export default function SettingsNetworkRoute() { }); } + if (dirty.hostname) { + changes.push({ + label: m.network_hostname_title(), + from: initialSettingsRef.current?.hostname?.toString() ?? "", + to: data.hostname?.toString() ?? "", + }); + } + // If no critical fields are changed, save immediately if (changes.length === 0) return onSubmit(settings); diff --git a/ui/src/routes/devices.$id.settings.video.tsx b/ui/src/routes/devices.$id.settings.video.tsx index 5cd21c4d..231b48b1 100644 --- a/ui/src/routes/devices.$id.settings.video.tsx +++ b/ui/src/routes/devices.$id.settings.video.tsx @@ -7,6 +7,7 @@ import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { SettingsItem } from "@components/SettingsItem"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; +import { NestedSettingsGroup } from "@components/NestedSettingsGroup"; import Fieldset from "@components/Fieldset"; import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; @@ -184,7 +185,7 @@ export default function SettingsVideoRoute() { description={m.video_enhancement_description()} /> -

+
-
+
; } + if (isFailsafeMode && failsafeReason) { + return ; + } + const hasConnectionFailed = connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? ""); @@ -1000,7 +1011,7 @@ export default function KvmIdRoute() { } return null; - }, [location.pathname, rebootState?.isRebooting, rebootState?.postRebootAction, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]); + }, [location.pathname, rebootState?.isRebooting, rebootState?.postRebootAction, isFailsafeMode, failsafeReason, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]); return ( @@ -1048,9 +1059,7 @@ export default function KvmIdRoute() { className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4" >
- {isFailsafeMode && failsafeReason ? ( - - ) : !!ConnectionStatusElement && ConnectionStatusElement} + {!!ConnectionStatusElement && ConnectionStatusElement}
diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts index ae97be13..d67b5c2f 100644 --- a/ui/src/utils/jsonrpc.ts +++ b/ui/src/utils/jsonrpc.ts @@ -221,16 +221,21 @@ export interface SystemVersionInfo { remote?: VersionInfo; systemUpdateAvailable: boolean; appUpdateAvailable: boolean; + willDisableAutoUpdate?: boolean; error?: string; } +const UPDATE_STATUS_RPC_TIMEOUT_MS = 10000; +const UPDATE_STATUS_RPC_MAX_ATTEMPTS = 6; + export async function getUpdateStatus() { const response = await callJsonRpc({ method: "getUpdateStatus", // This function calls our api server to see if there are any updates available. // It can be called on page load right after a restart, so we need to give it time to // establish a connection to the api server. - maxAttempts: 6, + maxAttempts: UPDATE_STATUS_RPC_MAX_ATTEMPTS, + attemptTimeoutMs: UPDATE_STATUS_RPC_TIMEOUT_MS, }); if (response.error) throw response.error; @@ -242,3 +247,27 @@ export async function getLocalVersion() { if (response.error) throw response.error; return response.result; } + +export type UpdateComponent = "app" | "system"; +export type UpdateComponents = Partial>; + +export interface updateParams { + components?: UpdateComponents; +} + +export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) { + const response = await callJsonRpc({ + method: "checkUpdateComponents", + params: { + params, + includePreRelease, + }, + // maxAttempts is set to 1, + // because it currently retry for all errors, + // and we don't want to retry if the error is not a network error + maxAttempts: 1, + attemptTimeoutMs: UPDATE_STATUS_RPC_TIMEOUT_MS, + }); + if (response.error) throw response.error; + return response.result; +} \ No newline at end of file diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 3935c2df..28324b55 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -33,7 +33,7 @@ export default defineConfig(({ mode, command }) => { outdir: "./localization/paraglide", outputStructure: 'message-modules', cookieName: 'JETKVM_LOCALE', - strategy: ['cookie', 'preferredLanguage', 'baseLocale'], + strategy: ['cookie', 'baseLocale'], })) return { diff --git a/webrtc.go b/webrtc.go index 5c9f8a5f..ad94d3f8 100644 --- a/webrtc.go +++ b/webrtc.go @@ -289,7 +289,7 @@ func newSession(config SessionConfig) (*Session, error) { }) // Wait for channel to be open before sending initial state d.OnOpen(func() { - triggerOTAStateUpdate() + triggerOTAStateUpdate(otaState.ToRPCState()) triggerVideoStateUpdate() triggerUSBStateUpdate() notifyFailsafeMode(session)