From ffc26ac4e75eb087e0e32a6992d6f00d01ba79b9 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Tue, 11 Nov 2025 15:53:40 +0000 Subject: [PATCH] wip: move cgo to separate process --- cmd/main.go | 20 +- go.mod | 3 + go.sum | 6 + internal/native/cgo/test.patch | 210 ++++++++ internal/native/grpc_server.go | 387 ++++++++++++++ internal/native/interface.go | 37 ++ internal/native/ipc.go | 293 ++++++++++ internal/native/native.go | 6 +- internal/native/proto/README.md | 33 ++ internal/native/proto/native.pb.go | 596 +++++++++++++++++++++ internal/native/proto/native.proto | 262 +++++++++ internal/native/proto/native_grpc.pb.go | 492 +++++++++++++++++ internal/native/proxy.go | 682 ++++++++++++++++++++++++ internal/supervisor/consts.go | 9 + native.go | 13 +- native_process.go | 528 ++++++++++++++++++ scripts/generate_proto.sh | 44 ++ scripts/release.sh | 134 +++++ 18 files changed, 3748 insertions(+), 7 deletions(-) create mode 100644 internal/native/cgo/test.patch create mode 100644 internal/native/grpc_server.go create mode 100644 internal/native/interface.go create mode 100644 internal/native/ipc.go create mode 100644 internal/native/proto/README.md create mode 100644 internal/native/proto/native.pb.go create mode 100644 internal/native/proto/native.proto create mode 100644 internal/native/proto/native_grpc.pb.go create mode 100644 internal/native/proxy.go create mode 100644 internal/supervisor/consts.go create mode 100644 native_process.go create mode 100755 scripts/generate_proto.sh create mode 100644 scripts/release.sh diff --git a/cmd/main.go b/cmd/main.go index 9a1e1899..c9d48362 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,19 +17,35 @@ import ( const ( envChildID = "JETKVM_CHILD_ID" + envSubcomponent = "JETKVM_SUBCOMPONENT" 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(envSubcomponent) + if subcomponentOverride != "" { + subcomponent = subcomponentOverride + } + switch subcomponent { + case "native": + gspt.SetProcTitle(os.Args[0] + " [native]") + kvm.RunNativeProcess() + default: + gspt.SetProcTitle(os.Args[0] + " [app]") + kvm.Main() + } } 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 { diff --git a/go.mod b/go.mod index 404215b0..bcfbf2cc 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,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..45ce4685 100644 --- a/go.sum +++ b/go.sum @@ -226,6 +226,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/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/grpc_server.go b/internal/native/grpc_server.go new file mode 100644 index 00000000..b5fcce4a --- /dev/null +++ b/internal/native/grpc_server.go @@ -0,0 +1,387 @@ +package native + +import ( + "context" + "fmt" + "net" + "os" + "path/filepath" + "sync" + "time" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + 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 + eventChs []chan *pb.Event + eventM sync.Mutex +} + +// NewGRPCServer creates a new gRPC server for the native service +func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer { + s := &grpcServer{ + native: n, + logger: logger, + eventChs: make([]chan *pb.Event, 0), + } + + // Store original callbacks and wrap them to also broadcast events + originalVideoStateChange := n.onVideoStateChange + originalIndevEvent := n.onIndevEvent + originalRpcEvent := n.onRpcEvent + originalVideoFrameReceived := n.onVideoFrameReceived + + // 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", + 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", + IndevEvent: event, + }) + } + + n.onRpcEvent = func(event string) { + if originalRpcEvent != nil { + originalRpcEvent(event) + } + s.broadcastEvent(&pb.Event{ + Type: "rpc_event", + RpcEvent: event, + }) + } + + n.onVideoFrameReceived = func(frame []byte, duration time.Duration) { + if originalVideoFrameReceived != nil { + originalVideoFrameReceived(frame, duration) + } + s.broadcastEvent(&pb.Event{ + Type: "video_frame", + VideoFrame: &pb.VideoFrame{ + Frame: frame, + DurationNs: duration.Nanoseconds(), + }, + }) + } + + return s +} + +func (s *grpcServer) broadcastEvent(event *pb.Event) { + s.eventM.Lock() + defer s.eventM.Unlock() + + for _, ch := range s.eventChs { + select { + case ch <- event: + default: + // Channel full, skip + } + } +} + +// 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) { + 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) { + 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 +} + +// StreamEvents streams events from the native process +func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamEventsServer) error { + eventCh := make(chan *pb.Event, 100) + + // Register this channel for events + s.eventM.Lock() + s.eventChs = append(s.eventChs, eventCh) + s.eventM.Unlock() + + // Unregister on exit + defer func() { + s.eventM.Lock() + defer s.eventM.Unlock() + for i, ch := range s.eventChs { + if ch == eventCh { + s.eventChs = append(s.eventChs[:i], s.eventChs[i+1:]...) + break + } + } + close(eventCh) + }() + + // Stream events + for { + select { + case event := <-eventCh: + if err := stream.Send(event); err != nil { + return err + } + case <-stream.Context().Done(): + return stream.Context().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) { + // Remove socket if it exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + return nil, nil, fmt.Errorf("failed to remove existing socket: %w", err) + } + } + + // Ensure directory exists + if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil { + return nil, nil, fmt.Errorf("failed to create socket directory: %w", err) + } + + 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/interface.go b/internal/native/interface.go new file mode 100644 index 00000000..be4cb960 --- /dev/null +++ b/internal/native/interface.go @@ -0,0 +1,37 @@ +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/ipc.go b/internal/native/ipc.go new file mode 100644 index 00000000..4157c96b --- /dev/null +++ b/internal/native/ipc.go @@ -0,0 +1,293 @@ +package native + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "sync" + "time" + + "github.com/rs/zerolog" +) + +// Request represents a JSON-RPC request +type Request struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +// Response represents a JSON-RPC response +type Response struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id,omitempty"` + Result interface{} `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` +} + +// RPCError represents a JSON-RPC error +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// Event represents a JSON-RPC notification/event +type Event struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// ProcessConfig is the configuration for the native process +type ProcessConfig struct { + Disable bool `json:"disable"` + SystemVersion string `json:"system_version"` // Serialized as string + AppVersion string `json:"app_version"` // Serialized as string + DisplayRotation uint16 `json:"display_rotation"` + DefaultQualityFactor float64 `json:"default_quality_factor"` +} + +// IPCClient handles communication with the native process +type IPCClient struct { + stdin io.WriteCloser + stdout *bufio.Scanner + logger *zerolog.Logger + + requestID int64 + requestIDM sync.Mutex + + pendingRequests map[interface{}]chan *Response + pendingM sync.Mutex + + eventHandlers map[string][]func(data interface{}) + eventM sync.RWMutex + + readyCh chan struct{} + ready bool + + closed bool + closeM sync.Mutex +} + +type processCmd interface { + Start() error + Wait() error + GetProcess() interface { + Kill() error + Signal(sig interface{}) error + } + StdinPipe() (io.WriteCloser, error) + StdoutPipe() (io.ReadCloser, error) + StderrPipe() (io.ReadCloser, error) +} + +func NewIPCClient(cmd processCmd, logger *zerolog.Logger) (*IPCClient, error) { + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("failed to get stdin pipe: %w", err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + stdin.Close() + return nil, fmt.Errorf("failed to get stdout pipe: %w", err) + } + + client := &IPCClient{ + stdin: stdin, + stdout: bufio.NewScanner(stdout), + logger: logger, + pendingRequests: make(map[interface{}]chan *Response), + eventHandlers: make(map[string][]func(data interface{})), + readyCh: make(chan struct{}), + } + + // Start reading responses + go client.readLoop() + + return client, nil +} + +func (c *IPCClient) readLoop() { + for c.stdout.Scan() { + line := c.stdout.Bytes() + if len(line) == 0 { + continue + } + + // Try to parse as response + var resp Response + if err := json.Unmarshal(line, &resp); err == nil { + // Check if it's a ready signal (result is "ready" and no ID) + if resp.Result == "ready" && resp.ID == nil && !c.ready { + c.ready = true + close(c.readyCh) + continue + } + if resp.Result != nil || resp.Error != nil { + c.handleResponse(&resp) + continue + } + } + + // Try to parse as event + var event Event + if err := json.Unmarshal(line, &event); err == nil && event.Method == "event" { + c.handleEvent(&event) + continue + } + + c.logger.Warn().Bytes("line", line).Msg("unexpected message from native process") + } + + c.closeM.Lock() + if !c.closed { + c.closed = true + c.closeM.Unlock() + c.logger.Warn().Msg("native process stdout closed") + // Cancel all pending requests + c.pendingM.Lock() + for id, ch := range c.pendingRequests { + close(ch) + delete(c.pendingRequests, id) + } + c.pendingM.Unlock() + } else { + c.closeM.Unlock() + } +} + +func (c *IPCClient) handleResponse(resp *Response) { + c.pendingM.Lock() + ch, ok := c.pendingRequests[resp.ID] + if ok { + delete(c.pendingRequests, resp.ID) + } + c.pendingM.Unlock() + + if ok { + select { + case ch <- resp: + default: + } + } +} + +func (c *IPCClient) handleEvent(event *Event) { + if event.Method != "event" || event.Params == nil { + return + } + + eventType, ok := event.Params["type"].(string) + if !ok { + return + } + + data := event.Params["data"] + + c.eventM.RLock() + handlers := c.eventHandlers[eventType] + c.eventM.RUnlock() + + for _, handler := range handlers { + handler(data) + } +} + +func (c *IPCClient) Call(method string, params interface{}) (*Response, error) { + c.closeM.Lock() + if c.closed { + c.closeM.Unlock() + return nil, fmt.Errorf("client is closed") + } + c.closeM.Unlock() + + c.requestIDM.Lock() + c.requestID++ + id := c.requestID + c.requestIDM.Unlock() + + req := Request{ + JSONRPC: "2.0", + ID: id, + Method: method, + } + + if params != nil { + paramsBytes, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("failed to marshal params: %w", err) + } + req.Params = paramsBytes + } + + ch := make(chan *Response, 1) + c.pendingM.Lock() + c.pendingRequests[id] = ch + c.pendingM.Unlock() + + reqBytes, err := json.Marshal(req) + if err != nil { + c.pendingM.Lock() + delete(c.pendingRequests, id) + c.pendingM.Unlock() + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + if _, err := c.stdin.Write(append(reqBytes, '\n')); err != nil { + c.pendingM.Lock() + delete(c.pendingRequests, id) + c.pendingM.Unlock() + return nil, fmt.Errorf("failed to write request: %w", err) + } + + select { + case resp := <-ch: + if resp.Error != nil { + return nil, fmt.Errorf("RPC error: %s (code: %d)", resp.Error.Message, resp.Error.Code) + } + return resp, nil + case <-time.After(30 * time.Second): + c.pendingM.Lock() + delete(c.pendingRequests, id) + c.pendingM.Unlock() + return nil, fmt.Errorf("request timeout") + } +} + +func (c *IPCClient) OnEvent(eventType string, handler func(data interface{})) { + c.eventM.Lock() + defer c.eventM.Unlock() + c.eventHandlers[eventType] = append(c.eventHandlers[eventType], handler) +} + +func (c *IPCClient) WaitReady() error { + select { + case <-c.readyCh: + return nil + case <-time.After(10 * time.Second): + return fmt.Errorf("timeout waiting for ready signal") + } +} + +func (c *IPCClient) Close() error { + c.closeM.Lock() + defer c.closeM.Unlock() + if c.closed { + return nil + } + c.closed = true + + c.pendingM.Lock() + for id, ch := range c.pendingRequests { + close(ch) + delete(c.pendingRequests, id) + } + c.pendingM.Unlock() + + return c.stdin.Close() +} + diff --git a/internal/native/native.go b/internal/native/native.go index cb8761cf..e772db52 100644 --- a/internal/native/native.go +++ b/internal/native/native.go @@ -94,11 +94,11 @@ func NewNative(opts NativeOptions) *Native { } } -func (n *Native) Start() { +func (n *Native) Start() error { if n.disable { nativeLogger.Warn().Msg("native is disabled, skipping initialization") setCgoDisabled(true) - return + return nil } // set up singleton @@ -117,9 +117,11 @@ func (n *Native) Start() { 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..0cb58fff --- /dev/null +++ b/internal/native/proto/native.pb.go @@ -0,0 +1,596 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// This file should be regenerated from native.proto using: +// protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/native/proto/native.proto +// +// For now, this is a minimal implementation to allow compilation. +// TODO: Regenerate from proto file when protoc is available. + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} +} + +func (x *Empty) String() string { + return "" +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + return nil // Stub +} + +type VideoState struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + 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"` +} + +func (x *VideoState) Reset() { + *x = VideoState{} +} + +func (x *VideoState) String() string { + return "" +} + +func (*VideoState) ProtoMessage() {} + +func (x *VideoState) ProtoReflect() protoreflect.Message { + return nil // Stub +} + +// Request/Response types - minimal implementations +type VideoSetSleepModeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *VideoSetSleepModeRequest) Reset() { *x = VideoSetSleepModeRequest{} } +func (x *VideoSetSleepModeRequest) String() string { return "" } +func (*VideoSetSleepModeRequest) ProtoMessage() {} +func (x *VideoSetSleepModeRequest) ProtoReflect() protoreflect.Message { return nil } + +type VideoGetSleepModeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *VideoGetSleepModeResponse) Reset() { *x = VideoGetSleepModeResponse{} } +func (x *VideoGetSleepModeResponse) String() string { return "" } +func (*VideoGetSleepModeResponse) ProtoMessage() {} +func (x *VideoGetSleepModeResponse) ProtoReflect() protoreflect.Message { return nil } + +type VideoSleepModeSupportedResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` +} + +func (x *VideoSleepModeSupportedResponse) Reset() { *x = VideoSleepModeSupportedResponse{} } +func (x *VideoSleepModeSupportedResponse) String() string { return "" } +func (*VideoSleepModeSupportedResponse) ProtoMessage() {} +func (x *VideoSleepModeSupportedResponse) ProtoReflect() protoreflect.Message { return nil } + +type VideoSetQualityFactorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Factor float64 `protobuf:"fixed64,1,opt,name=factor,proto3" json:"factor,omitempty"` +} + +func (x *VideoSetQualityFactorRequest) Reset() { *x = VideoSetQualityFactorRequest{} } +func (x *VideoSetQualityFactorRequest) String() string { return "" } +func (*VideoSetQualityFactorRequest) ProtoMessage() {} +func (x *VideoSetQualityFactorRequest) ProtoReflect() protoreflect.Message { return nil } + +type VideoGetQualityFactorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Factor float64 `protobuf:"fixed64,1,opt,name=factor,proto3" json:"factor,omitempty"` +} + +func (x *VideoGetQualityFactorResponse) Reset() { *x = VideoGetQualityFactorResponse{} } +func (x *VideoGetQualityFactorResponse) String() string { return "" } +func (*VideoGetQualityFactorResponse) ProtoMessage() {} +func (x *VideoGetQualityFactorResponse) ProtoReflect() protoreflect.Message { return nil } + +type VideoSetEDIDRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Edid string `protobuf:"bytes,1,opt,name=edid,proto3" json:"edid,omitempty"` +} + +func (x *VideoSetEDIDRequest) Reset() { *x = VideoSetEDIDRequest{} } +func (x *VideoSetEDIDRequest) String() string { return "" } +func (*VideoSetEDIDRequest) ProtoMessage() {} +func (x *VideoSetEDIDRequest) ProtoReflect() protoreflect.Message { return nil } + +type VideoGetEDIDResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Edid string `protobuf:"bytes,1,opt,name=edid,proto3" json:"edid,omitempty"` +} + +func (x *VideoGetEDIDResponse) Reset() { *x = VideoGetEDIDResponse{} } +func (x *VideoGetEDIDResponse) String() string { return "" } +func (*VideoGetEDIDResponse) ProtoMessage() {} +func (x *VideoGetEDIDResponse) ProtoReflect() protoreflect.Message { return nil } + +type VideoLogStatusResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *VideoLogStatusResponse) Reset() { *x = VideoLogStatusResponse{} } +func (x *VideoLogStatusResponse) String() string { return "" } +func (*VideoLogStatusResponse) ProtoMessage() {} +func (x *VideoLogStatusResponse) ProtoReflect() protoreflect.Message { return nil } + +type GetLVGLVersionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *GetLVGLVersionResponse) Reset() { *x = GetLVGLVersionResponse{} } +func (x *GetLVGLVersionResponse) String() string { return "" } +func (*GetLVGLVersionResponse) ProtoMessage() {} +func (x *GetLVGLVersionResponse) ProtoReflect() protoreflect.Message { return nil } + +// UI message types +type UIObjHideRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` +} + +func (x *UIObjHideRequest) Reset() { *x = UIObjHideRequest{} } +func (x *UIObjHideRequest) String() string { return "" } +func (*UIObjHideRequest) ProtoMessage() {} +func (x *UIObjHideRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjHideResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjHideResponse) Reset() { *x = UIObjHideResponse{} } +func (x *UIObjHideResponse) String() string { return "" } +func (*UIObjHideResponse) ProtoMessage() {} +func (x *UIObjHideResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjShowRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + ObjName string `protobuf:"bytes,1,opt,name=obj_name,json=objName,proto3" json:"obj_name,omitempty"` +} + +func (x *UIObjShowRequest) Reset() { *x = UIObjShowRequest{} } +func (x *UIObjShowRequest) String() string { return "" } +func (*UIObjShowRequest) ProtoMessage() {} +func (x *UIObjShowRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjShowResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjShowResponse) Reset() { *x = UIObjShowResponse{} } +func (x *UIObjShowResponse) String() string { return "" } +func (*UIObjShowResponse) ProtoMessage() {} +func (x *UIObjShowResponse) ProtoReflect() protoreflect.Message { return nil } + +type UISetVarRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *UISetVarRequest) Reset() { *x = UISetVarRequest{} } +func (x *UISetVarRequest) String() string { return "" } +func (*UISetVarRequest) ProtoMessage() {} +func (x *UISetVarRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIGetVarRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *UIGetVarRequest) Reset() { *x = UIGetVarRequest{} } +func (x *UIGetVarRequest) String() string { return "" } +func (*UIGetVarRequest) ProtoMessage() {} +func (x *UIGetVarRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIGetVarResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *UIGetVarResponse) Reset() { *x = UIGetVarResponse{} } +func (x *UIGetVarResponse) String() string { return "" } +func (*UIGetVarResponse) ProtoMessage() {} +func (x *UIGetVarResponse) ProtoReflect() protoreflect.Message { return nil } + +// Additional UI types - abbreviated for brevity, follow same pattern +type UIObjAddStateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjAddStateRequest) Reset() { *x = UIObjAddStateRequest{} } +func (x *UIObjAddStateRequest) String() string { return "" } +func (*UIObjAddStateRequest) ProtoMessage() {} +func (x *UIObjAddStateRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjAddStateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjAddStateResponse) Reset() { *x = UIObjAddStateResponse{} } +func (x *UIObjAddStateResponse) String() string { return "" } +func (*UIObjAddStateResponse) ProtoMessage() {} +func (x *UIObjAddStateResponse) ProtoReflect() protoreflect.Message { return nil } + +// Event types +type Event struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // oneof data - using separate fields for now + VideoState *VideoState `protobuf:"bytes,2,opt,name=video_state,json=videoState,proto3" json:"video_state,omitempty"` + IndevEvent string `protobuf:"bytes,3,opt,name=indev_event,json=indevEvent,proto3" json:"indev_event,omitempty"` + RpcEvent string `protobuf:"bytes,4,opt,name=rpc_event,json=rpcEvent,proto3" json:"rpc_event,omitempty"` + VideoFrame *VideoFrame `protobuf:"bytes,5,opt,name=video_frame,json=videoFrame,proto3" json:"video_frame,omitempty"` +} + +func (x *Event) Reset() { *x = Event{} } +func (x *Event) String() string { return "" } +func (*Event) ProtoMessage() {} +func (x *Event) ProtoReflect() protoreflect.Message { return nil } + +type VideoFrame struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *VideoFrame) Reset() { *x = VideoFrame{} } +func (x *VideoFrame) String() string { return "" } +func (*VideoFrame) ProtoMessage() {} +func (x *VideoFrame) ProtoReflect() protoreflect.Message { return nil } + +// Additional request/response types - following same pattern +// (Abbreviated - all follow the same structure) +type UIObjClearStateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjClearStateRequest) Reset() { *x = UIObjClearStateRequest{} } +func (x *UIObjClearStateRequest) String() string { return "" } +func (*UIObjClearStateRequest) ProtoMessage() {} +func (x *UIObjClearStateRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjClearStateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjClearStateResponse) Reset() { *x = UIObjClearStateResponse{} } +func (x *UIObjClearStateResponse) String() string { return "" } +func (*UIObjClearStateResponse) ProtoMessage() {} +func (x *UIObjClearStateResponse) ProtoReflect() protoreflect.Message { return nil } + +// Remaining types follow same pattern - creating minimal stubs +// TODO: Regenerate from proto for complete implementation + +type UIObjAddFlagRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjAddFlagRequest) Reset() { *x = UIObjAddFlagRequest{} } +func (x *UIObjAddFlagRequest) String() string { return "" } +func (*UIObjAddFlagRequest) ProtoMessage() {} +func (x *UIObjAddFlagRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjAddFlagResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjAddFlagResponse) Reset() { *x = UIObjAddFlagResponse{} } +func (x *UIObjAddFlagResponse) String() string { return "" } +func (*UIObjAddFlagResponse) ProtoMessage() {} +func (x *UIObjAddFlagResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjClearFlagRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjClearFlagRequest) Reset() { *x = UIObjClearFlagRequest{} } +func (x *UIObjClearFlagRequest) String() string { return "" } +func (*UIObjClearFlagRequest) ProtoMessage() {} +func (x *UIObjClearFlagRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjClearFlagResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjClearFlagResponse) Reset() { *x = UIObjClearFlagResponse{} } +func (x *UIObjClearFlagResponse) String() string { return "" } +func (*UIObjClearFlagResponse) ProtoMessage() {} +func (x *UIObjClearFlagResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetOpacityRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjSetOpacityRequest) Reset() { *x = UIObjSetOpacityRequest{} } +func (x *UIObjSetOpacityRequest) String() string { return "" } +func (*UIObjSetOpacityRequest) ProtoMessage() {} +func (x *UIObjSetOpacityRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetOpacityResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjSetOpacityResponse) Reset() { *x = UIObjSetOpacityResponse{} } +func (x *UIObjSetOpacityResponse) String() string { return "" } +func (*UIObjSetOpacityResponse) ProtoMessage() {} +func (x *UIObjSetOpacityResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjFadeInRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjFadeInRequest) Reset() { *x = UIObjFadeInRequest{} } +func (x *UIObjFadeInRequest) String() string { return "" } +func (*UIObjFadeInRequest) ProtoMessage() {} +func (x *UIObjFadeInRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjFadeInResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjFadeInResponse) Reset() { *x = UIObjFadeInResponse{} } +func (x *UIObjFadeInResponse) String() string { return "" } +func (*UIObjFadeInResponse) ProtoMessage() {} +func (x *UIObjFadeInResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjFadeOutRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjFadeOutRequest) Reset() { *x = UIObjFadeOutRequest{} } +func (x *UIObjFadeOutRequest) String() string { return "" } +func (*UIObjFadeOutRequest) ProtoMessage() {} +func (x *UIObjFadeOutRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjFadeOutResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjFadeOutResponse) Reset() { *x = UIObjFadeOutResponse{} } +func (x *UIObjFadeOutResponse) String() string { return "" } +func (*UIObjFadeOutResponse) ProtoMessage() {} +func (x *UIObjFadeOutResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetLabelTextRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjSetLabelTextRequest) Reset() { *x = UIObjSetLabelTextRequest{} } +func (x *UIObjSetLabelTextRequest) String() string { return "" } +func (*UIObjSetLabelTextRequest) ProtoMessage() {} +func (x *UIObjSetLabelTextRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetLabelTextResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjSetLabelTextResponse) Reset() { *x = UIObjSetLabelTextResponse{} } +func (x *UIObjSetLabelTextResponse) String() string { return "" } +func (*UIObjSetLabelTextResponse) ProtoMessage() {} +func (x *UIObjSetLabelTextResponse) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetImageSrcRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UIObjSetImageSrcRequest) Reset() { *x = UIObjSetImageSrcRequest{} } +func (x *UIObjSetImageSrcRequest) String() string { return "" } +func (*UIObjSetImageSrcRequest) ProtoMessage() {} +func (x *UIObjSetImageSrcRequest) ProtoReflect() protoreflect.Message { return nil } + +type UIObjSetImageSrcResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *UIObjSetImageSrcResponse) Reset() { *x = UIObjSetImageSrcResponse{} } +func (x *UIObjSetImageSrcResponse) String() string { return "" } +func (*UIObjSetImageSrcResponse) ProtoMessage() {} +func (x *UIObjSetImageSrcResponse) ProtoReflect() protoreflect.Message { return nil } + +type DisplaySetRotationRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Rotation uint32 `protobuf:"varint,1,opt,name=rotation,proto3" json:"rotation,omitempty"` +} + +func (x *DisplaySetRotationRequest) Reset() { *x = DisplaySetRotationRequest{} } +func (x *DisplaySetRotationRequest) String() string { return "" } +func (*DisplaySetRotationRequest) ProtoMessage() {} +func (x *DisplaySetRotationRequest) ProtoReflect() protoreflect.Message { return nil } + +type DisplaySetRotationResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *DisplaySetRotationResponse) Reset() { *x = DisplaySetRotationResponse{} } +func (x *DisplaySetRotationResponse) String() string { return "" } +func (*DisplaySetRotationResponse) ProtoMessage() {} +func (x *DisplaySetRotationResponse) ProtoReflect() protoreflect.Message { return nil } + +type UpdateLabelIfChangedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UpdateLabelIfChangedRequest) Reset() { *x = UpdateLabelIfChangedRequest{} } +func (x *UpdateLabelIfChangedRequest) String() string { return "" } +func (*UpdateLabelIfChangedRequest) ProtoMessage() {} +func (x *UpdateLabelIfChangedRequest) ProtoReflect() protoreflect.Message { return nil } + +type UpdateLabelAndChangeVisibilityRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *UpdateLabelAndChangeVisibilityRequest) Reset() { *x = UpdateLabelAndChangeVisibilityRequest{} } +func (x *UpdateLabelAndChangeVisibilityRequest) String() string { return "" } +func (*UpdateLabelAndChangeVisibilityRequest) ProtoMessage() {} +func (x *UpdateLabelAndChangeVisibilityRequest) ProtoReflect() protoreflect.Message { return nil } + +type SwitchToScreenIfRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + 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"` +} + +func (x *SwitchToScreenIfRequest) Reset() { *x = SwitchToScreenIfRequest{} } +func (x *SwitchToScreenIfRequest) String() string { return "" } +func (*SwitchToScreenIfRequest) ProtoMessage() {} +func (x *SwitchToScreenIfRequest) ProtoReflect() protoreflect.Message { return nil } + +type SwitchToScreenIfDifferentRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + ScreenName string `protobuf:"bytes,1,opt,name=screen_name,json=screenName,proto3" json:"screen_name,omitempty"` +} + +func (x *SwitchToScreenIfDifferentRequest) Reset() { *x = SwitchToScreenIfDifferentRequest{} } +func (x *SwitchToScreenIfDifferentRequest) String() string { return "" } +func (*SwitchToScreenIfDifferentRequest) ProtoMessage() {} +func (x *SwitchToScreenIfDifferentRequest) ProtoReflect() protoreflect.Message { return nil } diff --git a/internal/native/proto/native.proto b/internal/native/proto/native.proto new file mode 100644 index 00000000..c320d81d --- /dev/null +++ b/internal/native/proto/native.proto @@ -0,0 +1,262 @@ +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 { + // Init + rpc Init(InitRequest) returns (InitResponse); + + // 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 InitRequest { + string system_version = 1; + string app_version = 2; + uint32 display_rotation = 3; + double default_quality_factor = 4; +} + +message InitResponse { + bool success = 1; + string error = 2; +} + +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..59d2a239 --- /dev/null +++ b/internal/native/proto/native_grpc.pb.go @@ -0,0 +1,492 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// This is a placeholder file. Run: protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative 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. +const _ = grpc.SupportPackageIsVersion7 + +// NativeServiceClient is the client API for NativeService service. +type NativeServiceClient interface { + // 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) VideoSetSleepMode(ctx context.Context, in *VideoSetSleepModeRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoSetSleepMode", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoGetSleepMode(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetSleepModeResponse, error) { + out := new(VideoGetSleepModeResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoGetSleepMode", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoSleepModeSupported(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoSleepModeSupportedResponse, error) { + out := new(VideoSleepModeSupportedResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoSleepModeSupported", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoSetQualityFactor(ctx context.Context, in *VideoSetQualityFactorRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoSetQualityFactor", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoGetQualityFactor(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetQualityFactorResponse, error) { + out := new(VideoGetQualityFactorResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoGetQualityFactor", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoSetEDID(ctx context.Context, in *VideoSetEDIDRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoSetEDID", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoGetEDID(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoGetEDIDResponse, error) { + out := new(VideoGetEDIDResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoGetEDID", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoLogStatus(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VideoLogStatusResponse, error) { + out := new(VideoLogStatusResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoLogStatus", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoStop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoStop", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) VideoStart(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/VideoStart", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) GetLVGLVersion(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetLVGLVersionResponse, error) { + out := new(GetLVGLVersionResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/GetLVGLVersion", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjHide(ctx context.Context, in *UIObjHideRequest, opts ...grpc.CallOption) (*UIObjHideResponse, error) { + out := new(UIObjHideResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjHide", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjShow(ctx context.Context, in *UIObjShowRequest, opts ...grpc.CallOption) (*UIObjShowResponse, error) { + out := new(UIObjShowResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjShow", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UISetVar(ctx context.Context, in *UISetVarRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/UISetVar", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIGetVar(ctx context.Context, in *UIGetVarRequest, opts ...grpc.CallOption) (*UIGetVarResponse, error) { + out := new(UIGetVarResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIGetVar", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjAddState(ctx context.Context, in *UIObjAddStateRequest, opts ...grpc.CallOption) (*UIObjAddStateResponse, error) { + out := new(UIObjAddStateResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjAddState", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjClearState(ctx context.Context, in *UIObjClearStateRequest, opts ...grpc.CallOption) (*UIObjClearStateResponse, error) { + out := new(UIObjClearStateResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjClearState", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjAddFlag(ctx context.Context, in *UIObjAddFlagRequest, opts ...grpc.CallOption) (*UIObjAddFlagResponse, error) { + out := new(UIObjAddFlagResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjAddFlag", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjClearFlag(ctx context.Context, in *UIObjClearFlagRequest, opts ...grpc.CallOption) (*UIObjClearFlagResponse, error) { + out := new(UIObjClearFlagResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjClearFlag", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjSetOpacity(ctx context.Context, in *UIObjSetOpacityRequest, opts ...grpc.CallOption) (*UIObjSetOpacityResponse, error) { + out := new(UIObjSetOpacityResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjSetOpacity", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjFadeIn(ctx context.Context, in *UIObjFadeInRequest, opts ...grpc.CallOption) (*UIObjFadeInResponse, error) { + out := new(UIObjFadeInResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjFadeIn", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjFadeOut(ctx context.Context, in *UIObjFadeOutRequest, opts ...grpc.CallOption) (*UIObjFadeOutResponse, error) { + out := new(UIObjFadeOutResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjFadeOut", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjSetLabelText(ctx context.Context, in *UIObjSetLabelTextRequest, opts ...grpc.CallOption) (*UIObjSetLabelTextResponse, error) { + out := new(UIObjSetLabelTextResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjSetLabelText", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UIObjSetImageSrc(ctx context.Context, in *UIObjSetImageSrcRequest, opts ...grpc.CallOption) (*UIObjSetImageSrcResponse, error) { + out := new(UIObjSetImageSrcResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/UIObjSetImageSrc", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) DisplaySetRotation(ctx context.Context, in *DisplaySetRotationRequest, opts ...grpc.CallOption) (*DisplaySetRotationResponse, error) { + out := new(DisplaySetRotationResponse) + err := c.cc.Invoke(ctx, "/native.NativeService/DisplaySetRotation", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UpdateLabelIfChanged(ctx context.Context, in *UpdateLabelIfChangedRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/UpdateLabelIfChanged", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) UpdateLabelAndChangeVisibility(ctx context.Context, in *UpdateLabelAndChangeVisibilityRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/UpdateLabelAndChangeVisibility", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) SwitchToScreenIf(ctx context.Context, in *SwitchToScreenIfRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/SwitchToScreenIf", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) SwitchToScreenIfDifferent(ctx context.Context, in *SwitchToScreenIfDifferentRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/SwitchToScreenIfDifferent", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) DoNotUseThisIsForCrashTestingOnly(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/native.NativeService/DoNotUseThisIsForCrashTestingOnly", in, out, opts...) + return out, err +} + +func (c *nativeServiceClient) StreamEvents(ctx context.Context, in *Empty, opts ...grpc.CallOption) (NativeService_StreamEventsClient, error) { + stream, err := c.cc.NewStream(ctx, &NativeService_ServiceDesc.Streams[0], "/native.NativeService/StreamEvents", opts...) + if err != nil { + return nil, err + } + x := &nativeServiceStreamEventsClient{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. +type NativeServiceServer interface { + // 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) 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) +} + +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: "VideoSetSleepMode", + Handler: nil, // Will be set by RegisterNativeServiceServer + }, + // Additional methods will be registered here + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamEvents", + Handler: nil, // Will be set by RegisterNativeServiceServer + ServerStreams: true, + }, + }, + Metadata: "native.proto", +} diff --git a/internal/native/proxy.go b/internal/native/proxy.go new file mode 100644 index 00000000..0523e905 --- /dev/null +++ b/internal/native/proxy.go @@ -0,0 +1,682 @@ +package native + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "sync" + "syscall" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/jetkvm/kvm/internal/supervisor" + "github.com/rs/zerolog" +) + +// cmdWrapper wraps exec.Cmd to implement processCmd interface +type cmdWrapper struct { + *exec.Cmd +} + +func (c *cmdWrapper) GetProcess() interface { + Kill() error + Signal(sig interface{}) error +} { + return &processWrapper{Process: c.Cmd.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 { + client *IPCClient + cmd *exec.Cmd + wrapped *cmdWrapper + logger *zerolog.Logger + ready chan struct{} + restartM sync.Mutex + stopped bool + opts NativeProxyOptions + binaryPath string + configJSON []byte + processWait chan error +} + +// NativeProxyOptions are options for creating a NativeProxy +type NativeProxyOptions struct { + Disable bool + SystemVersion *semver.Version + AppVersion *semver.Version + DisplayRotation uint16 + DefaultQualityFactor float64 + OnVideoStateChange func(state VideoState) + OnVideoFrameReceived func(frame []byte, duration time.Duration) + OnIndevEvent func(event string) + OnRpcEvent func(event string) + Logger *zerolog.Logger +} + +// NewNativeProxy creates a new NativeProxy that spawns a separate process +func NewNativeProxy(opts NativeProxyOptions) (*NativeProxy, error) { + if opts.Logger == nil { + opts.Logger = nativeLogger + } + + // 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) + } + binaryPath := exePath + + config := ProcessConfig{ + Disable: opts.Disable, + SystemVersion: "", + AppVersion: "", + DisplayRotation: opts.DisplayRotation, + DefaultQualityFactor: opts.DefaultQualityFactor, + } + if opts.SystemVersion != nil { + config.SystemVersion = opts.SystemVersion.String() + } + if opts.AppVersion != nil { + config.AppVersion = opts.AppVersion.String() + } + + configJSON, err := json.Marshal(config) + if err != nil { + return nil, fmt.Errorf("failed to marshal config: %w", err) + } + + cmd := exec.Command(binaryPath, string(configJSON)) + cmd.Stderr = os.Stderr // Forward stderr to parent + // Set environment variable to indicate native process mode + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=native", supervisor.EnvSubcomponent)) + + // Wrap cmd to implement processCmd interface + wrappedCmd := &cmdWrapper{Cmd: cmd} + + client, err := NewIPCClient(wrappedCmd, opts.Logger) + if err != nil { + return nil, fmt.Errorf("failed to create IPC client: %w", err) + } + + proxy := &NativeProxy{ + client: client, + cmd: cmd, + wrapped: wrappedCmd, + logger: opts.Logger, + ready: make(chan struct{}), + opts: opts, + binaryPath: binaryPath, + configJSON: configJSON, + processWait: make(chan error, 1), + } + + // Set up event handlers + proxy.setupEventHandlers(client) + + return proxy, nil +} + +// Start starts the native process +func (p *NativeProxy) Start() error { + p.restartM.Lock() + defer p.restartM.Unlock() + + if p.stopped { + return fmt.Errorf("proxy is stopped") + } + + if err := p.cmd.Start(); err != nil { + return fmt.Errorf("failed to start native process: %w", err) + } + + // 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 err + } + + // Start monitoring process for crashes + go p.monitorProcess() + + close(p.ready) + return nil +} + +// monitorProcess monitors the native process and restarts it if it crashes +func (p *NativeProxy) monitorProcess() { + for { + p.restartM.Lock() + cmd := p.cmd + stopped := p.stopped + p.restartM.Unlock() + + if stopped { + return + } + + if cmd == nil { + return + } + + err := cmd.Wait() + select { + case p.processWait <- err: + default: + } + + p.restartM.Lock() + if p.stopped { + p.restartM.Unlock() + return + } + p.restartM.Unlock() + + 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.restartM.Lock() + defer p.restartM.Unlock() + + if p.stopped { + return fmt.Errorf("proxy is stopped") + } + + // Create new command + cmd := exec.Command(p.binaryPath, string(p.configJSON)) + cmd.Stderr = os.Stderr + // Set environment variable to indicate native process mode + cmd.Env = append(os.Environ(), "JETKVM_NATIVE_PROCESS=1") + + // Wrap cmd to implement processCmd interface + wrappedCmd := &cmdWrapper{Cmd: cmd} + + // Close old client + if p.client != nil { + _ = p.client.Close() + } + + // Create new client + client, err := NewIPCClient(wrappedCmd, p.logger) + if err != nil { + return fmt.Errorf("failed to create IPC client: %w", err) + } + + // Set up event handlers again + p.setupEventHandlers(client) + + // Start the process + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start native process: %w", err) + } + + // Wait for ready + if err := client.WaitReady(); err != nil { + if cmd.Process != nil { + _ = cmd.Process.Kill() + _ = cmd.Wait() + } + return fmt.Errorf("timeout waiting for ready: %w", err) + } + + p.cmd = cmd + p.wrapped = wrappedCmd + p.client = client + + p.logger.Info().Msg("native process restarted successfully") + return nil +} + +func (p *NativeProxy) setupEventHandlers(client *IPCClient) { + if p.opts.OnVideoStateChange != nil { + client.OnEvent("video_state_change", func(data interface{}) { + dataBytes, err := json.Marshal(data) + if err != nil { + p.logger.Warn().Err(err).Msg("failed to marshal video state event") + return + } + var state VideoState + if err := json.Unmarshal(dataBytes, &state); err != nil { + p.logger.Warn().Err(err).Msg("failed to unmarshal video state event") + return + } + p.opts.OnVideoStateChange(state) + }) + } + + if p.opts.OnIndevEvent != nil { + client.OnEvent("indev_event", func(data interface{}) { + if event, ok := data.(string); ok { + p.opts.OnIndevEvent(event) + } + }) + } + + if p.opts.OnRpcEvent != nil { + client.OnEvent("rpc_event", func(data interface{}) { + if event, ok := data.(string); ok { + p.opts.OnRpcEvent(event) + } + }) + } + + if p.opts.OnVideoFrameReceived != nil { + client.OnEvent("video_frame", func(data interface{}) { + dataMap, ok := data.(map[string]interface{}) + if !ok { + p.logger.Warn().Msg("invalid video frame event data") + return + } + + frameData, ok := dataMap["frame"].([]interface{}) + if !ok { + p.logger.Warn().Msg("invalid frame data in event") + return + } + + frame := make([]byte, len(frameData)) + for i, v := range frameData { + if b, ok := v.(float64); ok { + frame[i] = byte(b) + } + } + + durationNs, ok := dataMap["duration"].(float64) + if !ok { + p.logger.Warn().Msg("invalid duration in event") + return + } + + p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs)) + }) + } +} + +// Stop stops the native process +func (p *NativeProxy) Stop() error { + p.restartM.Lock() + defer p.restartM.Unlock() + + p.stopped = true + + if err := p.client.Close(); err != nil { + p.logger.Warn().Err(err).Msg("failed to close IPC client") + } + + 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 +} + +// Implement all Native methods by forwarding to IPC + +func (p *NativeProxy) VideoSetSleepMode(enabled bool) error { + _, err := p.client.Call("VideoSetSleepMode", map[string]interface{}{ + "enabled": enabled, + }) + return err +} + +func (p *NativeProxy) VideoGetSleepMode() (bool, error) { + resp, err := p.client.Call("VideoGetSleepMode", nil) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) VideoSleepModeSupported() bool { + resp, err := p.client.Call("VideoSleepModeSupported", nil) + if err != nil { + return false + } + result, ok := resp.Result.(bool) + if !ok { + return false + } + return result +} + +func (p *NativeProxy) VideoSetQualityFactor(factor float64) error { + _, err := p.client.Call("VideoSetQualityFactor", map[string]interface{}{ + "factor": factor, + }) + return err +} + +func (p *NativeProxy) VideoGetQualityFactor() (float64, error) { + resp, err := p.client.Call("VideoGetQualityFactor", nil) + if err != nil { + return 0, err + } + result, ok := resp.Result.(float64) + if !ok { + return 0, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) VideoSetEDID(edid string) error { + _, err := p.client.Call("VideoSetEDID", map[string]interface{}{ + "edid": edid, + }) + return err +} + +func (p *NativeProxy) VideoGetEDID() (string, error) { + resp, err := p.client.Call("VideoGetEDID", nil) + if err != nil { + return "", err + } + result, ok := resp.Result.(string) + if !ok { + return "", fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) VideoLogStatus() (string, error) { + resp, err := p.client.Call("VideoLogStatus", nil) + if err != nil { + return "", err + } + result, ok := resp.Result.(string) + if !ok { + return "", fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) VideoStop() error { + _, err := p.client.Call("VideoStop", nil) + return err +} + +func (p *NativeProxy) VideoStart() error { + _, err := p.client.Call("VideoStart", nil) + return err +} + +func (p *NativeProxy) GetLVGLVersion() (string, error) { + resp, err := p.client.Call("GetLVGLVersion", nil) + if err != nil { + return "", err + } + result, ok := resp.Result.(string) + if !ok { + return "", fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjHide(objName string) (bool, error) { + resp, err := p.client.Call("UIObjHide", map[string]interface{}{ + "obj_name": objName, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjShow(objName string) (bool, error) { + resp, err := p.client.Call("UIObjShow", map[string]interface{}{ + "obj_name": objName, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UISetVar(name string, value string) { + _, _ = p.client.Call("UISetVar", map[string]interface{}{ + "name": name, + "value": value, + }) +} + +func (p *NativeProxy) UIGetVar(name string) string { + resp, err := p.client.Call("UIGetVar", map[string]interface{}{ + "name": name, + }) + if err != nil { + return "" + } + result, ok := resp.Result.(string) + if !ok { + return "" + } + return result +} + +func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) { + resp, err := p.client.Call("UIObjAddState", map[string]interface{}{ + "obj_name": objName, + "state": state, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) { + resp, err := p.client.Call("UIObjClearState", map[string]interface{}{ + "obj_name": objName, + "state": state, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) { + resp, err := p.client.Call("UIObjAddFlag", map[string]interface{}{ + "obj_name": objName, + "flag": flag, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) { + resp, err := p.client.Call("UIObjClearFlag", map[string]interface{}{ + "obj_name": objName, + "flag": flag, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) { + resp, err := p.client.Call("UIObjSetOpacity", map[string]interface{}{ + "obj_name": objName, + "opacity": opacity, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) { + resp, err := p.client.Call("UIObjFadeIn", map[string]interface{}{ + "obj_name": objName, + "duration": duration, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) { + resp, err := p.client.Call("UIObjFadeOut", map[string]interface{}{ + "obj_name": objName, + "duration": duration, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) { + resp, err := p.client.Call("UIObjSetLabelText", map[string]interface{}{ + "obj_name": objName, + "text": text, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) { + resp, err := p.client.Call("UIObjSetImageSrc", map[string]interface{}{ + "obj_name": objName, + "image": image, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) { + resp, err := p.client.Call("DisplaySetRotation", map[string]interface{}{ + "rotation": rotation, + }) + if err != nil { + return false, err + } + result, ok := resp.Result.(bool) + if !ok { + return false, fmt.Errorf("invalid response type") + } + return result, nil +} + +func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) { + _, _ = p.client.Call("UpdateLabelIfChanged", map[string]interface{}{ + "obj_name": objName, + "new_text": newText, + }) +} + +func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) { + _, _ = p.client.Call("UpdateLabelAndChangeVisibility", map[string]interface{}{ + "obj_name": objName, + "new_text": newText, + }) +} + +func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) { + _, _ = p.client.Call("SwitchToScreenIf", map[string]interface{}{ + "screen_name": screenName, + "should_switch": shouldSwitch, + }) +} + +func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) { + _, _ = p.client.Call("SwitchToScreenIfDifferent", map[string]interface{}{ + "screen_name": screenName, + }) +} + +func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() { + _, _ = p.client.Call("DoNotUseThisIsForCrashTestingOnly", nil) +} diff --git a/internal/supervisor/consts.go b/internal/supervisor/consts.go new file mode 100644 index 00000000..5f288489 --- /dev/null +++ b/internal/supervisor/consts.go @@ -0,0 +1,9 @@ +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 +) diff --git a/native.go b/native.go index 81a0e50d..518c3fa9 100644 --- a/native.go +++ b/native.go @@ -11,17 +11,19 @@ import ( ) var ( - nativeInstance *native.Native + nativeInstance native.NativeInterface nativeCmdLock = sync.Mutex{} ) func initNative(systemVersion *semver.Version, appVersion *semver.Version) { - nativeInstance = native.NewNative(native.NativeOptions{ + var err error + nativeInstance, err = native.NewNativeProxy(native.NativeProxyOptions{ Disable: failsafeModeActive, SystemVersion: systemVersion, AppVersion: appVersion, DisplayRotation: config.GetDisplayRotation(), DefaultQualityFactor: config.VideoQualityFactor, + Logger: nativeLogger, OnVideoStateChange: func(state native.VideoState) { lastVideoState = state triggerVideoStateUpdate() @@ -63,8 +65,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { } }, }) + if err != nil { + nativeLogger.Fatal().Err(err).Msg("failed to create native proxy") + } - nativeInstance.Start() + 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") diff --git a/native_process.go b/native_process.go new file mode 100644 index 00000000..c239afdf --- /dev/null +++ b/native_process.go @@ -0,0 +1,528 @@ +package kvm + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/jetkvm/kvm/internal/native" + "github.com/rs/zerolog" +) + +// RunNativeProcess runs the native process mode +func RunNativeProcess() { + // Initialize logger + logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + + // Parse command line arguments (config is passed as first arg) + if len(os.Args) < 2 { + logger.Fatal().Msg("usage: native process requires config_json argument") + } + + var config native.ProcessConfig + if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil { + logger.Fatal().Err(err).Msg("failed to parse config") + } + + // Parse version strings + var systemVersion *semver.Version + if config.SystemVersion != "" { + v, err := semver.NewVersion(config.SystemVersion) + if err != nil { + logger.Warn().Err(err).Str("version", config.SystemVersion).Msg("failed to parse system version") + } else { + systemVersion = v + } + } + + var appVersion *semver.Version + if config.AppVersion != "" { + v, err := semver.NewVersion(config.AppVersion) + if err != nil { + logger.Warn().Err(err).Str("version", config.AppVersion).Msg("failed to parse app version") + } else { + appVersion = v + } + } + + // Create native instance + nativeInstance := native.NewNative(native.NativeOptions{ + Disable: config.Disable, + SystemVersion: systemVersion, + AppVersion: appVersion, + DisplayRotation: config.DisplayRotation, + DefaultQualityFactor: config.DefaultQualityFactor, + OnVideoStateChange: func(state native.VideoState) { + sendEvent("video_state_change", state) + }, + OnIndevEvent: func(event string) { + sendEvent("indev_event", event) + }, + OnRpcEvent: func(event string) { + sendEvent("rpc_event", event) + }, + OnVideoFrameReceived: func(frame []byte, duration time.Duration) { + sendEvent("video_frame", map[string]interface{}{ + "frame": frame, + "duration": duration.Nanoseconds(), + }) + }, + }) + + // Start native instance + if err := nativeInstance.Start(); err != nil { + logger.Fatal().Err(err).Msg("failed to start native instance") + } + + // Create gRPC server + grpcServer := native.NewGRPCServer(nativeInstance, &logger) + + // Determine socket path + socketPath := os.Getenv("JETKVM_NATIVE_SOCKET") + if socketPath == "" { + // Default to a socket in /tmp + socketPath = filepath.Join("/tmp", fmt.Sprintf("jetkvm-native-%d.sock", os.Getpid())) + } + + // Start gRPC server + server, lis, err := native.StartGRPCServer(grpcServer, socketPath, &logger) + if err != nil { + logger.Fatal().Err(err).Msg("failed to start gRPC server") + } + + // Signal that we're ready by writing socket path to stdout (for parent to read) + fmt.Fprintf(os.Stdout, "%s\n", socketPath) + os.Stdout.Close() + + // Set up signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) + + // Wait for signal + <-sigChan + logger.Info().Msg("received termination signal") + + // Graceful shutdown + server.GracefulStop() + lis.Close() + + logger.Info().Msg("native process exiting") +} + +// All JSON-RPC handlers have been removed - now using gRPC + case "VideoSetSleepMode": + var params struct { + Enabled bool `json:"enabled"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + err = n.VideoSetSleepMode(params.Enabled) + + case "VideoGetSleepMode": + var result bool + result, err = n.VideoGetSleepMode() + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "VideoSleepModeSupported": + result = n.VideoSleepModeSupported() + sendResponse(encoder, req.ID, "result", result) + return + + case "VideoSetQualityFactor": + var params struct { + Factor float64 `json:"factor"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + err = n.VideoSetQualityFactor(params.Factor) + + case "VideoGetQualityFactor": + var result float64 + result, err = n.VideoGetQualityFactor() + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "VideoSetEDID": + var params struct { + EDID string `json:"edid"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + err = n.VideoSetEDID(params.EDID) + + case "VideoGetEDID": + var result string + result, err = n.VideoGetEDID() + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "VideoLogStatus": + var result string + result, err = n.VideoLogStatus() + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "VideoStop": + err = n.VideoStop() + + case "VideoStart": + err = n.VideoStart() + + case "GetLVGLVersion": + var result string + result, err = n.GetLVGLVersion() + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjHide": + var params struct { + ObjName string `json:"obj_name"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjHide(params.ObjName) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjShow": + var params struct { + ObjName string `json:"obj_name"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjShow(params.ObjName) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UISetVar": + var params struct { + Name string `json:"name"` + Value string `json:"value"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + n.UISetVar(params.Name, params.Value) + sendResponse(encoder, req.ID, "result", nil) + return + + case "UIGetVar": + var params struct { + Name string `json:"name"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + result = n.UIGetVar(params.Name) + sendResponse(encoder, req.ID, "result", result) + return + + case "UIObjAddState": + var params struct { + ObjName string `json:"obj_name"` + State string `json:"state"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjAddState(params.ObjName, params.State) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjClearState": + var params struct { + ObjName string `json:"obj_name"` + State string `json:"state"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjClearState(params.ObjName, params.State) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjAddFlag": + var params struct { + ObjName string `json:"obj_name"` + Flag string `json:"flag"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjAddFlag(params.ObjName, params.Flag) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjClearFlag": + var params struct { + ObjName string `json:"obj_name"` + Flag string `json:"flag"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjClearFlag(params.ObjName, params.Flag) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjSetOpacity": + var params struct { + ObjName string `json:"obj_name"` + Opacity int `json:"opacity"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjSetOpacity(params.ObjName, params.Opacity) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjFadeIn": + var params struct { + ObjName string `json:"obj_name"` + Duration uint32 `json:"duration"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjFadeIn(params.ObjName, params.Duration) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjFadeOut": + var params struct { + ObjName string `json:"obj_name"` + Duration uint32 `json:"duration"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjFadeOut(params.ObjName, params.Duration) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjSetLabelText": + var params struct { + ObjName string `json:"obj_name"` + Text string `json:"text"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjSetLabelText(params.ObjName, params.Text) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UIObjSetImageSrc": + var params struct { + ObjName string `json:"obj_name"` + Image string `json:"image"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.UIObjSetImageSrc(params.ObjName, params.Image) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "DisplaySetRotation": + var params struct { + Rotation uint16 `json:"rotation"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + var result bool + result, err = n.DisplaySetRotation(params.Rotation) + if err == nil { + sendResponse(encoder, req.ID, "result", result) + return + } + + case "UpdateLabelIfChanged": + var params struct { + ObjName string `json:"obj_name"` + NewText string `json:"new_text"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + n.UpdateLabelIfChanged(params.ObjName, params.NewText) + sendResponse(encoder, req.ID, "result", nil) + return + + case "UpdateLabelAndChangeVisibility": + var params struct { + ObjName string `json:"obj_name"` + NewText string `json:"new_text"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + n.UpdateLabelAndChangeVisibility(params.ObjName, params.NewText) + sendResponse(encoder, req.ID, "result", nil) + return + + case "SwitchToScreenIf": + var params struct { + ScreenName string `json:"screen_name"` + ShouldSwitch []string `json:"should_switch"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + n.SwitchToScreenIf(params.ScreenName, params.ShouldSwitch) + sendResponse(encoder, req.ID, "result", nil) + return + + case "SwitchToScreenIfDifferent": + var params struct { + ScreenName string `json:"screen_name"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + sendError(encoder, req.ID, -32602, "Invalid params", nil) + return + } + n.SwitchToScreenIfDifferent(params.ScreenName) + sendResponse(encoder, req.ID, "result", nil) + return + + case "DoNotUseThisIsForCrashTestingOnly": + n.DoNotUseThisIsForCrashTestingOnly() + sendResponse(encoder, req.ID, "result", nil) + return + + default: + sendError(encoder, req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method), nil) + return + } + + if err != nil { + sendError(encoder, req.ID, -32000, err.Error(), nil) + return + } + + sendResponse(encoder, req.ID, "result", result) +} + +func sendResponse(encoder *json.Encoder, id interface{}, key string, result interface{}) { + response := map[string]interface{}{ + "jsonrpc": "2.0", + key: result, + } + if id != nil { + response["id"] = id + } + if err := encoder.Encode(response); err != nil { + fmt.Fprintf(os.Stderr, "failed to send response: %v\n", err) + } +} + +func sendError(encoder *json.Encoder, id interface{}, code int, message string, data interface{}) { + response := map[string]interface{}{ + "jsonrpc": "2.0", + "error": map[string]interface{}{ + "code": code, + "message": message, + }, + } + if id != nil { + response["id"] = id + } + if data != nil { + response["error"].(map[string]interface{})["data"] = data + } + if err := encoder.Encode(response); err != nil { + fmt.Fprintf(os.Stderr, "failed to send error: %v\n", err) + } +} + +func sendEvent(eventType string, data interface{}) { + event := map[string]interface{}{ + "jsonrpc": "2.0", + "method": "event", + "params": map[string]interface{}{ + "type": eventType, + "data": data, + }, + } + encoder := json.NewEncoder(os.Stdout) + if err := encoder.Encode(event); err != nil { + fmt.Fprintf(os.Stderr, "failed to send event: %v\n", err) + } +} + 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