refactor(audio): rename audio-server flag to audio-output-server for clarity

docs: update development documentation with new make targets
refactor: simplify audio quality presets implementation
style: remove redundant comments and align error handling
chore: add lint-ui-fix target to Makefile
This commit is contained in:
Alex P 2025-08-23 12:18:33 +00:00
parent 5e28a6c429
commit 2082b1a671
8 changed files with 184 additions and 103 deletions

View File

@ -231,22 +231,102 @@ systemctl restart jetkvm
cd ui && npm run lint cd ui && npm run lint
``` ```
### Local Code Quality Tools ### Essential Makefile Targets
The project includes several Makefile targets for local code quality checks that mirror the GitHub Actions workflows: The project includes several essential Makefile targets for development environment setup, building, and code quality:
#### Development Environment Setup
```bash ```bash
# Run Go linting (mirrors .github/workflows/lint.yml) # Set up complete development environment (recommended first step)
make lint make dev_env
# This runs setup_toolchain + build_audio_deps + installs Go tools
# - Clones rv1106-system toolchain to $HOME/.jetkvm/rv1106-system
# - Builds ALSA and Opus static libraries for ARM
# - Installs goimports and other Go development tools
# Run Go linting with auto-fix # Set up only the cross-compiler toolchain
make lint-fix make setup_toolchain
# Run UI linting (mirrors .github/workflows/ui-lint.yml) # Build only the audio dependencies (requires setup_toolchain)
make ui-lint make build_audio_deps
``` ```
**Note:** The `lint` and `lint-fix` targets require audio dependencies. Run `make dev_env` first if you haven't already. #### Building
```bash
# Build development version with debug symbols
make build_dev
# Builds jetkvm_app with version like 0.4.7-dev20241222
# Requires: make dev_env (for toolchain and audio dependencies)
# Build release version (production)
make build_release
# Builds optimized release version
# Requires: make dev_env and frontend build
# Build test binaries for device testing
make build_dev_test
# Creates device-tests.tar.gz with all test binaries
```
#### Code Quality and Linting
```bash
# Run both Go and UI linting
make lint
# Run both Go and UI linting with auto-fix
make lint-fix
# Run only Go linting
make lint-go
# Run only Go linting with auto-fix
make lint-go-fix
# Run only UI linting
make lint-ui
# Run only UI linting with auto-fix
make lint-ui-fix
```
**Note:** The Go linting targets (`lint-go`, `lint-go-fix`, and the combined `lint`/`lint-fix` targets) require audio dependencies. Run `make dev_env` first if you haven't already.
### Development Deployment Script
The `dev_deploy.sh` script is the primary tool for deploying your development changes to a JetKVM device:
```bash
# Basic deployment (builds and deploys everything)
./dev_deploy.sh -r 192.168.1.100
# Skip UI build for faster backend-only deployment
./dev_deploy.sh -r 192.168.1.100 --skip-ui-build
# Run Go tests on the device after deployment
./dev_deploy.sh -r 192.168.1.100 --run-go-tests
# Deploy with release build and install
./dev_deploy.sh -r 192.168.1.100 -i
# View all available options
./dev_deploy.sh --help
```
**Key features:**
- Automatically builds the Go backend with proper cross-compilation
- Optionally builds the React frontend (unless `--skip-ui-build`)
- Deploys binaries to the device via SSH/SCP
- Restarts the JetKVM service
- Can run tests on the device
- Supports custom SSH user and various deployment options
**Requirements:**
- SSH access to your JetKVM device
- `make dev_env` must be run first (for toolchain and audio dependencies)
- Device IP address or hostname
### API Testing ### API Testing

View File

@ -1,5 +1,5 @@
# --- JetKVM Audio/Toolchain Dev Environment Setup --- # --- JetKVM Audio/Toolchain Dev Environment Setup ---
.PHONY: setup_toolchain build_audio_deps dev_env lint lint-fix ui-lint .PHONY: setup_toolchain build_audio_deps dev_env lint lint-go lint-ui lint-fix lint-go-fix lint-ui-fix ui-lint
# Clone the rv1106-system toolchain to $HOME/.jetkvm/rv1106-system # Clone the rv1106-system toolchain to $HOME/.jetkvm/rv1106-system
setup_toolchain: setup_toolchain:
@ -9,8 +9,10 @@ setup_toolchain:
build_audio_deps: setup_toolchain build_audio_deps: setup_toolchain
bash tools/build_audio_deps.sh $(ALSA_VERSION) $(OPUS_VERSION) bash tools/build_audio_deps.sh $(ALSA_VERSION) $(OPUS_VERSION)
# Prepare everything needed for local development (toolchain + audio deps) # Prepare everything needed for local development (toolchain + audio deps + Go tools)
dev_env: build_audio_deps dev_env: build_audio_deps
@echo "Installing Go development tools..."
go install golang.org/x/tools/cmd/goimports@latest
@echo "Development environment ready." @echo "Development environment ready."
JETKVM_HOME ?= $(HOME)/.jetkvm JETKVM_HOME ?= $(HOME)/.jetkvm
TOOLCHAIN_DIR ?= $(JETKVM_HOME)/rv1106-system TOOLCHAIN_DIR ?= $(JETKVM_HOME)/rv1106-system
@ -127,8 +129,12 @@ release:
rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app
rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256
# Run both Go and UI linting
lint: lint-go lint-ui
@echo "All linting completed successfully!"
# Run golangci-lint locally with the same configuration as CI # Run golangci-lint locally with the same configuration as CI
lint: build_audio_deps lint-go: build_audio_deps
@echo "Running golangci-lint..." @echo "Running golangci-lint..."
@mkdir -p static && touch static/.gitkeep @mkdir -p static && touch static/.gitkeep
CGO_ENABLED=1 \ CGO_ENABLED=1 \
@ -136,8 +142,12 @@ lint: build_audio_deps
CGO_LDFLAGS="-L$(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/src/.libs -lasound -L$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/.libs -lopus -lm -ldl -static" \ CGO_LDFLAGS="-L$(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/src/.libs -lasound -L$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/.libs -lopus -lm -ldl -static" \
golangci-lint run --verbose golangci-lint run --verbose
# Run both Go and UI linting with auto-fix
lint-fix: lint-go-fix lint-ui-fix
@echo "All linting with auto-fix completed successfully!"
# Run golangci-lint with auto-fix # Run golangci-lint with auto-fix
lint-fix: build_audio_deps lint-go-fix: build_audio_deps
@echo "Running golangci-lint with auto-fix..." @echo "Running golangci-lint with auto-fix..."
@mkdir -p static && touch static/.gitkeep @mkdir -p static && touch static/.gitkeep
CGO_ENABLED=1 \ CGO_ENABLED=1 \
@ -146,7 +156,16 @@ lint-fix: build_audio_deps
golangci-lint run --fix --verbose golangci-lint run --fix --verbose
# Run UI linting locally (mirrors GitHub workflow ui-lint.yml) # Run UI linting locally (mirrors GitHub workflow ui-lint.yml)
ui-lint: lint-ui:
@echo "Running UI lint..." @echo "Running UI lint..."
@cd ui && npm ci @cd ui && npm ci
@cd ui && npm run lint @cd ui && npm run lint
# Run UI linting with auto-fix
lint-ui-fix:
@echo "Running UI lint with auto-fix..."
@cd ui && npm ci
@cd ui && npm run lint:fix
# Legacy alias for UI linting (for backward compatibility)
ui-lint: lint-ui

View File

@ -11,7 +11,7 @@ import (
func main() { func main() {
versionPtr := flag.Bool("version", false, "print version and exit") versionPtr := flag.Bool("version", false, "print version and exit")
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit") versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
audioServerPtr := flag.Bool("audio-server", false, "Run as audio server subprocess") audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess") audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
flag.Parse() flag.Parse()

View File

@ -15,7 +15,7 @@ var (
// isAudioServerProcess detects if we're running as the audio server subprocess // isAudioServerProcess detects if we're running as the audio server subprocess
func isAudioServerProcess() bool { func isAudioServerProcess() bool {
for _, arg := range os.Args { for _, arg := range os.Args {
if strings.Contains(arg, "--audio-server") { if strings.Contains(arg, "--audio-output-server") {
return true return true
} }
} }

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"sync/atomic" "sync/atomic"
"time" "time"
// Explicit import for CGO audio stream glue
) )
var ( var (
@ -33,7 +32,7 @@ type AudioConfig struct {
} }
// AudioMetrics tracks audio performance metrics // AudioMetrics tracks audio performance metrics
// Note: 64-bit fields must be first for proper alignment on 32-bit ARM
type AudioMetrics struct { type AudioMetrics struct {
FramesReceived int64 FramesReceived int64
FramesDropped int64 FramesDropped int64
@ -61,72 +60,67 @@ var (
metrics AudioMetrics metrics AudioMetrics
) )
// GetAudioQualityPresets returns predefined quality configurations // qualityPresets defines the base quality configurations
func GetAudioQualityPresets() map[AudioQuality]AudioConfig { var qualityPresets = map[AudioQuality]struct {
return map[AudioQuality]AudioConfig{ outputBitrate, inputBitrate int
sampleRate, channels int
frameSize time.Duration
}{
AudioQualityLow: { AudioQualityLow: {
Quality: AudioQualityLow, outputBitrate: 32, inputBitrate: 16,
Bitrate: 32, sampleRate: 22050, channels: 1,
SampleRate: 22050, frameSize: 40 * time.Millisecond,
Channels: 1,
FrameSize: 40 * time.Millisecond,
}, },
AudioQualityMedium: { AudioQualityMedium: {
Quality: AudioQualityMedium, outputBitrate: 64, inputBitrate: 32,
Bitrate: 64, sampleRate: 44100, channels: 2,
SampleRate: 44100, frameSize: 20 * time.Millisecond,
Channels: 2,
FrameSize: 20 * time.Millisecond,
}, },
AudioQualityHigh: { AudioQualityHigh: {
Quality: AudioQualityHigh, outputBitrate: 128, inputBitrate: 64,
Bitrate: 128, sampleRate: 48000, channels: 2,
SampleRate: 48000, frameSize: 20 * time.Millisecond,
Channels: 2,
FrameSize: 20 * time.Millisecond,
}, },
AudioQualityUltra: { AudioQualityUltra: {
Quality: AudioQualityUltra, outputBitrate: 192, inputBitrate: 96,
Bitrate: 192, sampleRate: 48000, channels: 2,
SampleRate: 48000, frameSize: 10 * time.Millisecond,
Channels: 2,
FrameSize: 10 * time.Millisecond,
}, },
}
// GetAudioQualityPresets returns predefined quality configurations for audio output
func GetAudioQualityPresets() map[AudioQuality]AudioConfig {
result := make(map[AudioQuality]AudioConfig)
for quality, preset := range qualityPresets {
result[quality] = AudioConfig{
Quality: quality,
Bitrate: preset.outputBitrate,
SampleRate: preset.sampleRate,
Channels: preset.channels,
FrameSize: preset.frameSize,
} }
}
return result
} }
// GetMicrophoneQualityPresets returns predefined quality configurations for microphone input // GetMicrophoneQualityPresets returns predefined quality configurations for microphone input
func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig { func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
return map[AudioQuality]AudioConfig{ result := make(map[AudioQuality]AudioConfig)
AudioQualityLow: { for quality, preset := range qualityPresets {
Quality: AudioQualityLow, result[quality] = AudioConfig{
Bitrate: 16, Quality: quality,
SampleRate: 16000, Bitrate: preset.inputBitrate,
Channels: 1, SampleRate: func() int {
FrameSize: 40 * time.Millisecond, if quality == AudioQualityLow {
}, return 16000
AudioQualityMedium: {
Quality: AudioQualityMedium,
Bitrate: 32,
SampleRate: 22050,
Channels: 1,
FrameSize: 20 * time.Millisecond,
},
AudioQualityHigh: {
Quality: AudioQualityHigh,
Bitrate: 64,
SampleRate: 44100,
Channels: 1,
FrameSize: 20 * time.Millisecond,
},
AudioQualityUltra: {
Quality: AudioQualityUltra,
Bitrate: 96,
SampleRate: 48000,
Channels: 1,
FrameSize: 10 * time.Millisecond,
},
} }
return preset.sampleRate
}(),
Channels: 1, // Microphone is always mono
FrameSize: preset.frameSize,
}
}
return result
} }
// SetAudioQuality updates the current audio quality configuration // SetAudioQuality updates the current audio quality configuration

View File

@ -410,9 +410,7 @@ func cgoAudioClose() {
C.jetkvm_audio_close() C.jetkvm_audio_close()
} }
// Optimized read and encode with pre-allocated error objects and reduced checks
func cgoAudioReadEncode(buf []byte) (int, error) { func cgoAudioReadEncode(buf []byte) (int, error) {
// Fast path: check minimum buffer size (reduced from 1500 to 1276 for 10ms frames)
if len(buf) < 1276 { if len(buf) < 1276 {
return 0, errBufferTooSmall return 0, errBufferTooSmall
} }
@ -427,11 +425,11 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
return int(n), nil return int(n), nil
} }
// Go wrappers for audio playback (microphone input) // Audio playback functions
func cgoAudioPlaybackInit() error { func cgoAudioPlaybackInit() error {
ret := C.jetkvm_audio_playback_init() ret := C.jetkvm_audio_playback_init()
if ret != 0 { if ret != 0 {
return errors.New("failed to init ALSA playback/Opus decoder") return errAudioPlaybackInit
} }
return nil return nil
} }
@ -440,44 +438,36 @@ func cgoAudioPlaybackClose() {
C.jetkvm_audio_playback_close() C.jetkvm_audio_playback_close()
} }
// Decodes Opus frame and writes to playback device
func cgoAudioDecodeWrite(buf []byte) (int, error) { func cgoAudioDecodeWrite(buf []byte) (int, error) {
if len(buf) == 0 { if len(buf) == 0 {
return 0, errors.New("empty buffer") return 0, errEmptyBuffer
} }
// Additional safety check to prevent segfault
if buf == nil { if buf == nil {
return 0, errors.New("nil buffer") return 0, errNilBuffer
}
if len(buf) > 4096 {
return 0, errBufferTooLarge
} }
// Validate buffer size to prevent potential overruns
if len(buf) > 4096 { // Maximum reasonable Opus frame size
return 0, errors.New("buffer too large")
}
// Ensure buffer is not deallocated by keeping a reference
bufPtr := unsafe.Pointer(&buf[0]) bufPtr := unsafe.Pointer(&buf[0])
if bufPtr == nil { if bufPtr == nil {
return 0, errors.New("invalid buffer pointer") return 0, errInvalidBufferPtr
} }
// Add recovery mechanism for C function crashes
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
// Log the panic but don't crash the entire program _ = r
// This should not happen with proper validation, but provides safety
_ = r // Explicitly ignore the panic value
} }
}() }()
n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf))) n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf)))
if n < 0 { if n < 0 {
return 0, errors.New("audio decode/write error") return 0, errAudioDecodeWrite
} }
return int(n), nil return int(n), nil
} }
// Wrapper functions for non-blocking audio manager // CGO function aliases
var ( var (
CGOAudioInit = cgoAudioInit CGOAudioInit = cgoAudioInit
CGOAudioClose = cgoAudioClose CGOAudioClose = cgoAudioClose

View File

@ -9,9 +9,8 @@ import (
) )
// AudioInputMetrics holds metrics for microphone input // AudioInputMetrics holds metrics for microphone input
// Note: int64 fields must be 64-bit aligned for atomic operations on ARM
type AudioInputMetrics struct { type AudioInputMetrics struct {
FramesSent int64 // Must be first for alignment FramesSent int64
FramesDropped int64 FramesDropped int64
BytesProcessed int64 BytesProcessed int64
ConnectionDrops int64 ConnectionDrops int64
@ -21,7 +20,6 @@ type AudioInputMetrics struct {
// AudioInputManager manages microphone input stream using IPC mode only // AudioInputManager manages microphone input stream using IPC mode only
type AudioInputManager struct { type AudioInputManager struct {
// metrics MUST be first for ARM32 alignment (contains int64 fields)
metrics AudioInputMetrics metrics AudioInputMetrics
ipcManager *AudioInputIPCManager ipcManager *AudioInputIPCManager

View File

@ -248,7 +248,7 @@ func (s *AudioServerSupervisor) startProcess() error {
defer s.mutex.Unlock() defer s.mutex.Unlock()
// Create new command // Create new command
s.cmd = exec.CommandContext(s.ctx, execPath, "--audio-server") s.cmd = exec.CommandContext(s.ctx, execPath, "--audio-output-server")
s.cmd.Stdout = os.Stdout s.cmd.Stdout = os.Stdout
s.cmd.Stderr = os.Stderr s.cmd.Stderr = os.Stderr
@ -261,7 +261,7 @@ func (s *AudioServerSupervisor) startProcess() error {
s.logger.Info().Int("pid", s.processPID).Msg("audio server process started") s.logger.Info().Int("pid", s.processPID).Msg("audio server process started")
// Add process to monitoring // Add process to monitoring
s.processMonitor.AddProcess(s.processPID, "audio-server") s.processMonitor.AddProcess(s.processPID, "audio-output-server")
if s.onProcessStart != nil { if s.onProcessStart != nil {
s.onProcessStart(s.processPID) s.onProcessStart(s.processPID)