diff --git a/cloud.go b/cloud.go index 3f46a42..57d4366 100644 --- a/cloud.go +++ b/cloud.go @@ -139,19 +139,33 @@ var ( ) ) +type CloudConnectionState uint8 + +const ( + CloudConnectionStateNotConfigured CloudConnectionState = iota + CloudConnectionStateDisconnected + CloudConnectionStateConnecting + CloudConnectionStateConnected +) + var ( - cloudConnectionAlive bool - cloudConnectionAliveLock = &sync.Mutex{} + cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured + cloudConnectionStateLock = &sync.Mutex{} cloudDisconnectChan chan error cloudDisconnectLock = &sync.Mutex{} ) -func setCloudConnectionAlive(alive bool) { - cloudConnectionAliveLock.Lock() - defer cloudConnectionAliveLock.Unlock() +func setCloudConnectionState(state CloudConnectionState) { + cloudConnectionStateLock.Lock() + defer cloudConnectionStateLock.Unlock() - cloudConnectionAlive = alive + if cloudConnectionState == CloudConnectionStateDisconnected && + (config.CloudToken == "" || config.CloudURL == "") { + state = CloudConnectionStateNotConfigured + } + + cloudConnectionState = state go waitCtrlAndRequestDisplayUpdate() } @@ -297,6 +311,8 @@ func runWebsocketClient() error { wsURL.Scheme = "wss" } + setCloudConnectionState(CloudConnectionStateConnecting) + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) header.Set("X-App-Version", builtAppVersion) @@ -314,12 +330,12 @@ func runWebsocketClient() error { c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() - setCloudConnectionAlive(true) + setCloudConnectionState(CloudConnectionStateConnected) return true }, @@ -350,7 +366,7 @@ func runWebsocketClient() error { if err != nil { if errors.Is(err, context.Canceled) { cloudLogger.Info().Msg("websocket connection canceled") - setCloudConnectionAlive(false) + setCloudConnectionState(CloudConnectionStateDisconnected) return nil } @@ -540,6 +556,8 @@ func rpcDeregisterDevice() error { cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") disconnectCloud(fmt.Errorf("device deregistered")) + setCloudConnectionState(CloudConnectionStateNotConfigured) + return nil } diff --git a/dev_deploy.sh b/dev_deploy.sh index 02bbb24..ca627cd 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -91,7 +91,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug +PION_LOG_TRACE=jetkvm,cloud,websocket,native ./jetkvm_app_debug EOF echo "Deployment complete." diff --git a/display.go b/display.go index c64e751..dff4355 100644 --- a/display.go +++ b/display.go @@ -1,6 +1,7 @@ package kvm import ( + "context" "errors" "fmt" "os" @@ -53,6 +54,18 @@ func lvObjShow(objName string) (*CtrlResponse, error) { return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN") } +func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity}) +} + +func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration}) +} + func lvLabelSetText(objName string, text string) (*CtrlResponse, error) { return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text}) } @@ -69,13 +82,20 @@ func updateLabelIfChanged(objName string, newText string) { } func switchToScreenIfDifferent(screenName string) { - displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") if currentScreen != screenName { + displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") switchToScreen(screenName) } } +var ( + cloudBlinkCtx context.Context + cloudBlinkCancel context.CancelFunc + cloudBlinkTicker *time.Ticker +) + func updateDisplay() { + updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) if usbState == "configured" { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") @@ -99,16 +119,67 @@ func updateDisplay() { switchToScreenIfDifferent("ui_No_Network_Screen") } - if config.CloudToken == "" || config.CloudURL == "" { + if cloudConnectionState == CloudConnectionStateNotConfigured { lvObjHide("ui_Home_Header_Cloud_Status_Icon") } else { lvObjShow("ui_Home_Header_Cloud_Status_Icon") - // TODO: blink the icon if establishing connection - if cloudConnectionAlive { - _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") - } else { - _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png") + } + + switch cloudConnectionState { + case CloudConnectionStateDisconnected: + lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png") + stopCloudBlink() + case CloudConnectionStateConnecting: + lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + startCloudBlink() + case CloudConnectionStateConnected: + lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + stopCloudBlink() + } +} + +func startCloudBlink() { + if cloudBlinkTicker == nil { + cloudBlinkTicker = time.NewTicker(2 * time.Second) + } + + if cloudBlinkCtx != nil { + cloudBlinkCancel() + } + + cloudBlinkCtx, cloudBlinkCancel = context.WithCancel(appCtx) + cloudBlinkTicker.Reset(2 * time.Second) + + go func() { + defer cloudBlinkTicker.Stop() + for { + select { + case <-cloudBlinkTicker.C: + if cloudConnectionState != CloudConnectionStateConnecting { + return + } + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + case <-cloudBlinkCtx.Done(): + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjSetOpacity("ui_Home_Header_Cloud_Status_Icon", 255) + return + } } + }() +} + +func stopCloudBlink() { + if cloudBlinkTicker != nil { + cloudBlinkTicker.Stop() + } + + if cloudBlinkCtx != nil { + cloudBlinkCancel() } } @@ -128,7 +199,7 @@ func requestDisplayUpdate() { } go func() { wakeDisplay(false) - displayLogger.Info().Msg("display updating") + displayLogger.Debug().Msg("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() diff --git a/native.go b/native.go index b61598c..c339569 100644 --- a/native.go +++ b/native.go @@ -335,7 +335,10 @@ func ensureBinaryUpdated(destPath string) error { _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - nativeLogger.Info().Msg("writing jetkvm_native") + nativeLogger.Info(). + Interface("hash", srcHash). + Msg("writing jetkvm_native") + _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { diff --git a/resource/jetkvm_native b/resource/jetkvm_native index 5b169d8..084ce14 100644 Binary files a/resource/jetkvm_native and b/resource/jetkvm_native differ diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index b23902c..b540b94 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -b556ac0d6f38518f20dcf212ba65fe97981aa169ae418c68b2cbb155447affda +4b925c7aa73d2e35a227833e806658cb17e1d25900611f93ed70b11ac9f1716d diff --git a/web.go b/web.go index 58c7ed1..e74d551 100644 --- a/web.go +++ b/web.go @@ -203,7 +203,7 @@ func handleLocalWebRTCSignal(c *gin.Context) { wsOptions := &websocket.AcceptOptions{ InsecureSkipVerify: true, // Allow connections from any origin OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime() @@ -244,7 +244,7 @@ func handleWebRTCSignalWsMessages( runCtx, cancelRun := context.WithCancel(context.Background()) defer func() { if isCloudConnection { - setCloudConnectionAlive(false) + setCloudConnectionState(CloudConnectionStateDisconnected) } cancelRun() }()