Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions kvmapp/system/init.d/S95nanokvm
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ stop_services() {
# The server owns VI/VPSS. Let it release MMF before stopping kvm_system.
stop_process NanoKVM-Server
stop_process kvm_system
# Runtime viewer state belongs to tmpfs and is safe to discard on restart.
rm -f /run/nanokvm/oled_viewers
rm -rf /tmp/kvm_system /tmp/server
}

start_services() {
mkdir -p /run/nanokvm
cp -r /kvmapp/kvm_system /tmp/
if [ -f /kvmapp/kvm_new_app ]; then
touch /tmp/.nanokvm_migrating
Expand Down
2 changes: 2 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func initialize() {
vm.EnableHdmiCapture()
}
vm.SetHdmiViewerCount(0)
vm.StartOLEDViewerPublisher()

// run mouse jiggler
jiggler.GetJiggler().Run()
Expand Down Expand Up @@ -123,5 +124,6 @@ func run() {
}

func dispose() {
vm.StopOLEDViewerPublisher()
common.GetKvmVision().Close()
}
2 changes: 1 addition & 1 deletion server/proto/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ type GetMemoryLimitRsp struct {
}

type SetOledReq struct {
Sleep int `validate:"omitempty"`
Sleep *int `json:"sleep"`
}

type GetOLEDRsp struct {
Expand Down
8 changes: 8 additions & 0 deletions server/service/vm/hdmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ func SetHdmiViewerCount(count int) {
SetHdmiViewerCountForSource("legacy", count)
}

// OLEDViewerCount returns only long-lived streaming viewers. It intentionally
// excludes capture leases used by screenshots and other short-lived jobs.
func OLEDViewerCount() int {
hdmiMutex.Lock()
defer hdmiMutex.Unlock()
return hdmiDemand.ViewerCount()
}

// SetHdmiViewerCountForSource is retained for callers that cannot provide a
// source revision. Streamers should use UpdateHdmiViewerSnapshot instead.
func SetHdmiViewerCountForSource(source string, count int) {
Expand Down
96 changes: 59 additions & 37 deletions server/service/vm/oled.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package vm

import (
"NanoKVM-Server/proto"
"fmt"
"errors"
"os"
"path/filepath"
"strconv"
"strings"

Expand All @@ -12,61 +13,82 @@ import (
)

const (
OLEDExistFile = "/etc/kvm/oled_exist"
OLEDSleepFile = "/etc/kvm/oled_sleep"
OLEDExistFile = "/etc/kvm/oled_exist"
OLEDSleepFile = "/etc/kvm/oled_sleep"
defaultOLEDSleep = 60
)

func validOLEDSleep(sleep int) bool {
return sleep == 0 || (sleep >= 10 && sleep <= 3600)
}

func readOLEDSleep() (int, error) {
data, err := os.ReadFile(OLEDSleepFile)
if errors.Is(err, os.ErrNotExist) {
return defaultOLEDSleep, nil
}
if err != nil {
return 0, err
}
sleep, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil || !validOLEDSleep(sleep) {
return 0, errors.New("invalid OLED sleep setting")
}
return sleep, nil
}

func writeFileAtomic(path string, data []byte) error {
tmp, err := os.CreateTemp(filepath.Dir(path), ".oled-sleep-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err = tmp.Chmod(0o644); err == nil {
_, err = tmp.Write(data)
}
if err == nil {
err = tmp.Sync()
}
if closeErr := tmp.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return os.Rename(tmpPath, path)
}

func (s *Service) SetOLED(c *gin.Context) {
var req proto.SetOledReq
var rsp proto.Response

if err := proto.ParseFormRequest(c, &req); err != nil {
if err := proto.ParseFormRequest(c, &req); err != nil || req.Sleep == nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}

data := []byte(fmt.Sprintf("%d", req.Sleep))
err := os.WriteFile(OLEDSleepFile, data, 0o644)
if err != nil {
rsp.ErrRsp(c, -2, "failed to write data")
if !validOLEDSleep(*req.Sleep) {
rsp.ErrRsp(c, -1, "sleep must be 0 or between 10 and 3600 seconds")
return
}
if err := writeFileAtomic(OLEDSleepFile, []byte(strconv.Itoa(*req.Sleep)+"\n")); err != nil {
rsp.ErrRsp(c, -2, "failed to write OLED sleep setting")
return
}

rsp.OkRsp(c)
log.Debugf("set OLED sleep: %d", req.Sleep)
log.Debugf("set OLED sleep: %d", *req.Sleep)
}

func (s *Service) GetOLED(c *gin.Context) {
var rsp proto.Response

if _, err := os.Stat(OLEDExistFile); err != nil {
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: false,
Sleep: 0,
})
return
}

data, err := os.ReadFile(OLEDSleepFile)
if err != nil {
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: true,
Sleep: 0,
})
rsp.OkRspWithData(c, &proto.GetOLEDRsp{Exist: false})
return
}

content := strings.TrimSpace(string(data))
sleep, err := strconv.Atoi(content)
sleep, err := readOLEDSleep()
if err != nil {
log.Errorf("failed to parse OLED: %s", err)
rsp.ErrRsp(c, -1, "failed to parse OLED config")
log.Errorf("failed to parse OLED sleep setting: %s", err)
rsp.ErrRsp(c, -1, "failed to parse OLED sleep setting")
return
}

rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: true,
Sleep: sleep,
})
log.Debugf("get OLED config successful, sleep %d", sleep)
rsp.OkRspWithData(c, &proto.GetOLEDRsp{Exist: true, Sleep: sleep})
}
98 changes: 98 additions & 0 deletions server/service/vm/oled_viewers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package vm

import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"time"

log "github.com/sirupsen/logrus"
)

const oledViewersFile = "/run/nanokvm/oled_viewers"

var (
oledPublisherMu sync.Mutex
oledPublisherCancel context.CancelFunc
oledPublisherDone chan struct{}
)

func publishOLEDViewerCount(count int) error {
if count < 0 {
count = 0
}
if err := os.MkdirAll(filepath.Dir(oledViewersFile), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(oledViewersFile), ".oled_viewers-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err = tmp.Chmod(0o644); err == nil {
_, err = fmt.Fprintf(tmp, "%d\n", count)
}
if err == nil {
err = tmp.Sync()
}
if closeErr := tmp.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return os.Rename(tmpPath, oledViewersFile)
}

// StartOLEDViewerPublisher periodically publishes a heartbeat on tmpfs. The
// OLED process treats an absent or stale file as zero so either process can be
// upgraded or restarted independently.
func StartOLEDViewerPublisher() {
oledPublisherMu.Lock()
defer oledPublisherMu.Unlock()
if oledPublisherCancel != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
oledPublisherCancel = cancel
done := make(chan struct{})
oledPublisherDone = done
if err := publishOLEDViewerCount(OLEDViewerCount()); err != nil {
log.Warnf("failed to publish OLED viewers: %s", err)
}
go func(done chan struct{}) {
defer close(done)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := publishOLEDViewerCount(OLEDViewerCount()); err != nil {
log.Warnf("failed to publish OLED viewers: %s", err)
}
}
}
}(done)
}

func StopOLEDViewerPublisher() {
oledPublisherMu.Lock()
done := oledPublisherDone
if oledPublisherCancel != nil {
oledPublisherCancel()
oledPublisherCancel = nil
oledPublisherDone = nil
}
oledPublisherMu.Unlock()
if done != nil {
<-done
}
if err := os.Remove(oledViewersFile); err != nil && !os.IsNotExist(err) {
log.Warnf("failed to remove OLED viewer state: %s", err)
}
}
38 changes: 34 additions & 4 deletions support/sg2002/kvm_system/main/include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@
#define KEY_LONGLONG_PRESS 9000
#define WIFI_CONNECTION_DELAY 5000
#define OLED_SLEEP_DELAY_MIN 10
#define OLED_SLEEP_DELAY_DEFAULT 30
#define OLED_SLEEP_DELAY_DEFAULT 60
#define OLED_WIFI_SLEEP_DELAY 300
#define OLED_EVENT_WAKE_DELAY 15
#define OLED_EVENT_WAKE_MAX 60
#define OLED_CAROUSEL_DELAY 6
#define OLED_CAROUSEL_START_DELAY 900
#define OLED_PIXEL_SHIFT_DELAY 60
#define OLED_PIXEL_SHIFT_X 3
#define OLED_PIXEL_SHIFT_Y 3
#define KVM_WD_COUNT_MAX 10
#define RM_Watchdog_times 60

Expand Down Expand Up @@ -67,7 +75,12 @@ typedef struct {
int8_t sys_thread_running = 0;
int8_t wifi_config_process = -1; // 1:QR;2:Test;3:IP;
char wifi_ap_pass[9] = {0};
uint8_t oled_sleep_state = 0; // 0:wakeup; 1:sleep;
uint8_t oled_sleep_state = 0; // 0:wakeup; 1:sleep;
// Key/system threads only publish these intents. OLED I2C remains owned by
// the OLED thread.
volatile uint8_t oled_manual_off = 0;
volatile uint64_t oled_local_activity_ms = 0;
volatile uint64_t oled_wifi_activity_ms = 0;
int8_t reconvery_update = 0; // 0:Undetected; 1:Needs Update; 2:Update finish; -1:not need to update
uint8_t ping_allow = 1;
} kvm_sys_state_t;
Expand Down Expand Up @@ -95,9 +108,26 @@ typedef struct {
int8_t type = -1; // cat /kvmapp/kvm/type
int8_t now_fps = -1; // cat /kvmapp/kvm/now_fps
int16_t qlty = -1; // cat /kvmapp/kvm/qlty
uint16_t oled_sleep_param = 0;
uint16_t oled_sleep_param = OLED_SLEEP_DELAY_DEFAULT;
uint8_t oled_sleep_state = 0; // 0:wakeup; 1:sleep;
uint64_t oled_sleep_start = 0;
uint64_t oled_sleep_start = 0;
uint64_t event_wake_deadline_ms = 0;
uint64_t event_wake_started_ms = 0;
uint64_t event_wake_last_ms = 0;
uint64_t next_page_switch_ms = 0;
uint64_t next_pixel_shift_ms = 0;
uint64_t wifi_context_started_ms = 0;
uint64_t viewer_file_seen_ms = 0;
uint8_t main_page = 0;
uint8_t pixel_shift_x = 0;
uint8_t pixel_shift_y = 0;
int8_t pixel_shift_dx = 1;
int8_t pixel_shift_dy = 1;
uint8_t power_state = 0; // 0: off, 1: on
uint8_t wake_pending = 0;
uint8_t force_full_redraw = 1;
uint8_t viewers = 0;
uint64_t render_signature = 0;
uint64_t ue_patch_state = 0;
} kvm_oled_state_t;

Expand Down
Loading
Loading