Compare commits

...

9 Commits

7 changed files with 107 additions and 45 deletions

View File

@ -22,7 +22,7 @@ var (
audioLogger zerolog.Logger audioLogger zerolog.Logger
currentAudioTrack *webrtc.TrackLocalStaticSample currentAudioTrack *webrtc.TrackLocalStaticSample
inputTrackHandling atomic.Bool inputTrackHandling atomic.Bool
useUSBForAudioOutput bool useUSBForAudioOutput atomic.Bool
audioOutputEnabled atomic.Bool audioOutputEnabled atomic.Bool
audioInputEnabled atomic.Bool audioInputEnabled atomic.Bool
) )
@ -32,7 +32,7 @@ func initAudio() {
// Load audio output source from config // Load audio output source from config
ensureConfigLoaded() ensureConfigLoaded()
useUSBForAudioOutput = config.AudioOutputSource == "usb" useUSBForAudioOutput.Store(config.AudioOutputSource == "usb")
// Enable both by default // Enable both by default
audioOutputEnabled.Store(true) audioOutputEnabled.Store(true)
@ -57,7 +57,7 @@ func startAudio() error {
// Start output audio if not running and enabled // Start output audio if not running and enabled
if outputSource == nil && audioOutputEnabled.Load() { if outputSource == nil && audioOutputEnabled.Load() {
alsaDevice := "hw:0,0" // HDMI alsaDevice := "hw:0,0" // HDMI
if useUSBForAudioOutput { if useUSBForAudioOutput.Load() {
alsaDevice = "hw:1,0" // USB alsaDevice = "hw:1,0" // USB
} }
@ -167,16 +167,17 @@ func SetAudioOutputSource(useUSB bool) error {
audioMutex.Lock() audioMutex.Lock()
defer audioMutex.Unlock() defer audioMutex.Unlock()
if useUSBForAudioOutput == useUSB { if useUSBForAudioOutput.Load() == useUSB {
return nil return nil
} }
audioLogger.Info(). audioLogger.Info().
Bool("old_usb", useUSBForAudioOutput). Bool("old_usb", useUSBForAudioOutput.Load()).
Bool("new_usb", useUSB). Bool("new_usb", useUSB).
Msg("Switching audio output source") Msg("Switching audio output source")
useUSBForAudioOutput = useUSB oldValue := useUSBForAudioOutput.Load()
useUSBForAudioOutput.Store(useUSB)
ensureConfigLoaded() ensureConfigLoaded()
if useUSB { if useUSB {
@ -186,6 +187,7 @@ func SetAudioOutputSource(useUSB bool) error {
} }
if err := SaveConfig(); err != nil { if err := SaveConfig(); err != nil {
audioLogger.Error().Err(err).Msg("Failed to save config") audioLogger.Error().Err(err).Msg("Failed to save config")
useUSBForAudioOutput.Store(oldValue)
return err return err
} }

View File

@ -305,11 +305,11 @@ func wakeDisplay(force bool, reason string) {
displayLogger.Warn().Err(err).Msg("failed to wake display") displayLogger.Warn().Err(err).Msg("failed to wake display")
} }
if config.DisplayDimAfterSec != 0 { if config.DisplayDimAfterSec != 0 && dimTicker != nil {
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second) dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
} }
if config.DisplayOffAfterSec != 0 { if config.DisplayOffAfterSec != 0 && offTicker != nil {
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second) offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
} }
backlightState = 0 backlightState = 0

View File

@ -20,7 +20,8 @@ type OutputRelay struct {
cancel context.CancelFunc cancel context.CancelFunc
logger zerolog.Logger logger zerolog.Logger
running atomic.Bool running atomic.Bool
sample media.Sample // Reusable sample for zero-allocation hot path sample media.Sample
stopped chan struct{}
// Stats (Uint32: overflows after 2.7 years @ 50fps, faster atomics on 32-bit ARM) // Stats (Uint32: overflows after 2.7 years @ 50fps, faster atomics on 32-bit ARM)
framesRelayed atomic.Uint32 framesRelayed atomic.Uint32
@ -38,8 +39,9 @@ func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSampl
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
logger: logger, logger: logger,
stopped: make(chan struct{}),
sample: media.Sample{ sample: media.Sample{
Duration: 20 * time.Millisecond, // Constant for all Opus frames Duration: 20 * time.Millisecond,
}, },
} }
} }
@ -55,13 +57,15 @@ func (r *OutputRelay) Start() error {
return nil return nil
} }
// Stop stops the relay // Stop stops the relay and waits for goroutine to exit
func (r *OutputRelay) Stop() { func (r *OutputRelay) Stop() {
if !r.running.Swap(false) { if !r.running.Swap(false) {
return return
} }
r.cancel() r.cancel()
<-r.stopped
r.logger.Debug(). r.logger.Debug().
Uint32("frames_relayed", r.framesRelayed.Load()). Uint32("frames_relayed", r.framesRelayed.Load()).
Uint32("frames_dropped", r.framesDropped.Load()). Uint32("frames_dropped", r.framesDropped.Load()).
@ -70,6 +74,8 @@ func (r *OutputRelay) Stop() {
// relayLoop continuously reads from audio source and writes to WebRTC // relayLoop continuously reads from audio source and writes to WebRTC
func (r *OutputRelay) relayLoop() { func (r *OutputRelay) relayLoop() {
defer close(r.stopped)
const reconnectDelay = 1 * time.Second const reconnectDelay = 1 * time.Second
for r.running.Load() { for r.running.Load() {

View File

@ -899,7 +899,7 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error {
if config.UsbDevices != nil && !config.UsbDevices.Audio && config.AudioOutputSource == "usb" { if config.UsbDevices != nil && !config.UsbDevices.Audio && config.AudioOutputSource == "usb" {
audioMutex.Lock() audioMutex.Lock()
config.AudioOutputSource = "hdmi" config.AudioOutputSource = "hdmi"
useUSBForAudioOutput = false useUSBForAudioOutput.Store(false)
audioSourceChanged = true audioSourceChanged = true
audioMutex.Unlock() audioMutex.Unlock()
} }
@ -908,7 +908,7 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error {
if config.UsbDevices != nil && config.UsbDevices.Audio && !wasAudioEnabled { if config.UsbDevices != nil && config.UsbDevices.Audio && !wasAudioEnabled {
audioMutex.Lock() audioMutex.Lock()
config.AudioOutputSource = "usb" config.AudioOutputSource = "usb"
useUSBForAudioOutput = true useUSBForAudioOutput.Store(true)
audioSourceChanged = true audioSourceChanged = true
audioMutex.Unlock() audioMutex.Unlock()
} }
@ -970,8 +970,10 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
} }
func rpcGetAudioOutputSource() (string, error) { func rpcGetAudioOutputSource() (string, error) {
ensureConfigLoaded() if useUSBForAudioOutput.Load() {
return config.AudioOutputSource, nil return "usb", nil
}
return "hdmi", nil
} }
func rpcSetAudioOutputSource(source string) error { func rpcSetAudioOutputSource(source string) error {

View File

@ -111,6 +111,7 @@ type Client struct {
var ( var (
defaultTimerDuration = 1 * time.Second defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second defaultLinkUpTimeout = 30 * time.Second
defaultDHCPTimeout = 5 * time.Second // DHCP request timeout (not link up timeout)
maxRenewalAttemptDuration = 2 * time.Hour maxRenewalAttemptDuration = 2 * time.Hour
) )
@ -125,11 +126,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
} }
if cfg.Timeout == 0 { if cfg.Timeout == 0 {
cfg.Timeout = defaultLinkUpTimeout cfg.Timeout = defaultDHCPTimeout
} }
if cfg.Retries == 0 { if cfg.Retries == 0 {
cfg.Retries = 3 cfg.Retries = 4
} }
return &Client{ return &Client{
@ -153,9 +154,15 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
}, nil }, nil
} }
func resetTimer(t *time.Timer, l *zerolog.Logger) { func resetTimer(t *time.Timer, attempt int, l *zerolog.Logger) {
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later") // Exponential backoff: 1s, 2s, 4s, 8s, max 8s
t.Reset(defaultTimerDuration) backoffAttempt := attempt
if backoffAttempt > 3 {
backoffAttempt = 3
}
delay := time.Duration(1<<backoffAttempt) * time.Second
l.Debug().Dur("delay", delay).Int("attempt", attempt).Msg("will retry later")
t.Reset(delay)
} }
func getRenewalTime(lease *Lease) time.Duration { func getRenewalTime(lease *Lease) time.Duration {
@ -168,12 +175,14 @@ func getRenewalTime(lease *Lease) time.Duration {
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) { func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
l := c.l.With().Str("interface", ifname).Int("family", family).Logger() l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
attempt := 0
for range t.C { for range t.C {
l.Info().Msg("requesting lease") l.Info().Int("attempt", attempt).Msg("requesting lease")
if _, err := c.ensureInterfaceUp(ifname); err != nil { if _, err := c.ensureInterfaceUp(ifname); err != nil {
l.Error().Err(err).Msg("failed to ensure interface up") l.Error().Err(err).Int("attempt", attempt).Msg("failed to ensure interface up")
resetTimer(t, c.l) resetTimer(t, attempt, c.l)
attempt++
continue continue
} }
@ -188,11 +197,14 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
lease, err = c.requestLease6(ifname) lease, err = c.requestLease6(ifname)
} }
if err != nil { if err != nil {
l.Error().Err(err).Msg("failed to request lease") l.Error().Err(err).Int("attempt", attempt).Msg("failed to request lease")
resetTimer(t, c.l) resetTimer(t, attempt, c.l)
attempt++
continue continue
} }
// Successfully obtained lease, reset attempt counter
attempt = 0
c.handleLeaseChange(lease) c.handleLeaseChange(lease)
nextRenewal := getRenewalTime(lease) nextRenewal := getRenewalTime(lease)

View File

@ -26,6 +26,31 @@ show_help() {
echo " $0 -r 192.168.0.17 -u admin" echo " $0 -r 192.168.0.17 -u admin"
} }
# Function to check if device is pingable
check_ping() {
local host=$1
msg_info "▶ Checking if device is reachable at ${host}..."
if ! ping -c 3 -W 5 "${host}" > /dev/null 2>&1; then
msg_err "Error: Cannot reach device at ${host}"
msg_err "Please verify the IP address and network connectivity"
exit 1
fi
msg_info "✓ Device is reachable"
}
# Function to check if SSH is accessible
check_ssh() {
local user=$1
local host=$2
msg_info "▶ Checking SSH connectivity to ${user}@${host}..."
if ! ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${host}" "echo 'SSH connection successful'" > /dev/null 2>&1; then
msg_err "Error: Cannot establish SSH connection to ${user}@${host}"
msg_err "Please verify SSH access and credentials"
exit 1
fi
msg_info "✓ SSH connection successful"
}
# Default values # Default values
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
REMOTE_USER="root" REMOTE_USER="root"
@ -113,6 +138,10 @@ if [ -z "$REMOTE_HOST" ]; then
exit 1 exit 1
fi fi
# Check device connectivity before proceeding
check_ping "${REMOTE_HOST}"
check_ssh "${REMOTE_USER}" "${REMOTE_HOST}"
# check if the current CPU architecture is x86_64 # check if the current CPU architecture is x86_64
if [ "$(uname -m)" != "x86_64" ]; then if [ "$(uname -m)" != "x86_64" ]; then
msg_warn "Warning: This script is only supported on x86_64 architecture" msg_warn "Warning: This script is only supported on x86_64 architecture"
@ -131,7 +160,7 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then
SKIP_UI_BUILD=false SKIP_UI_BUILD=false
fi fi
if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
msg_info "▶ Building frontend" msg_info "▶ Building frontend"
make frontend SKIP_UI_BUILD=0 make frontend SKIP_UI_BUILD=0
SKIP_UI_BUILD_RELEASE=1 SKIP_UI_BUILD_RELEASE=1
@ -144,13 +173,13 @@ fi
if [ "$RUN_GO_TESTS" = true ]; then if [ "$RUN_GO_TESTS" = true ]; then
msg_info "▶ Building go tests" msg_info "▶ Building go tests"
make build_dev_test make build_dev_test
msg_info "▶ Copying device-tests.tar.gz to remote host" msg_info "▶ Copying device-tests.tar.gz to remote host"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
msg_info "▶ Running go tests" msg_info "▶ Running go tests"
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
set -e set -e
TMP_DIR=$(mktemp -d) TMP_DIR=$(mktemp -d)
cd ${TMP_DIR} cd ${TMP_DIR}
@ -191,35 +220,35 @@ then
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Copy the binary to the remote host as if we were the OTA updater. # Copy the binary to the remote host as if we were the OTA updater.
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
# Reboot the device, the new app will be deployed by the startup process. # Reboot the device, the new app will be deployed by the startup process.
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
else else
msg_info "▶ Building development binary" msg_info "▶ Building development binary"
do_make build_dev \ do_make build_dev \
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Kill any existing instances of the application # Kill any existing instances of the application
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
# Copy the binary to the remote host # Copy the binary to the remote host
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
if [ "$RESET_USB_HID_DEVICE" = true ]; then if [ "$RESET_USB_HID_DEVICE" = true ]; then
msg_info "▶ Resetting USB HID device" msg_info "▶ Resetting USB HID device"
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
# Remove the old USB gadget configuration # Remove the old USB gadget configuration
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
fi fi
# Deploy and run the application on the remote host # Deploy and run the application on the remote host
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
set -e set -e
# Set the library path to include the directory where librockit.so is located # Set the library path to include the directory where librockit.so is located
@ -229,6 +258,17 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
killall jetkvm_app || true killall jetkvm_app || true
killall jetkvm_app_debug || true killall jetkvm_app_debug || true
# Wait until both binaries are killed, max 10 seconds
i=1
while [ \$i -le 10 ]; do
echo "Waiting for jetkvm_app and jetkvm_app_debug to be killed, \$i/10 ..."
if ! pgrep -f "jetkvm_app" > /dev/null && ! pgrep -f "jetkvm_app_debug" > /dev/null; then
break
fi
sleep 1
i=\$((i + 1))
done
# Navigate to the directory where the binary will be stored # Navigate to the directory where the binary will be stored
cd "${REMOTE_PATH}" cd "${REMOTE_PATH}"

View File

@ -46,10 +46,10 @@ export default function SettingsHardwareRoute() {
} }
setBacklightSettings(settings); setBacklightSettings(settings);
handleBacklightSettingsSave(); handleBacklightSettingsSave(settings);
}; };
const handleBacklightSettingsSave = () => { const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => { send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
@ -81,7 +81,7 @@ export default function SettingsHardwareRoute() {
const duration = enabled ? 90 : -1; const duration = enabled ? 90 : -1;
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => { send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data ||m.unknown_error() })); notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data || m.unknown_error() }));
setPowerSavingEnabled(!enabled); // Attempt to revert on error setPowerSavingEnabled(!enabled); // Attempt to revert on error
return; return;
} }