diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0b9432b..2e93ed6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -231,22 +231,102 @@ systemctl restart jetkvm 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 -# Run Go linting (mirrors .github/workflows/lint.yml) -make lint +# Set up complete development environment (recommended first step) +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 -make lint-fix +# Set up only the cross-compiler toolchain +make setup_toolchain -# Run UI linting (mirrors .github/workflows/ui-lint.yml) -make ui-lint +# Build only the audio dependencies (requires setup_toolchain) +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 diff --git a/Makefile b/Makefile index f59cd11..7d0d27e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # --- 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 setup_toolchain: @@ -9,8 +9,10 @@ setup_toolchain: build_audio_deps: setup_toolchain 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 + @echo "Installing Go development tools..." + go install golang.org/x/tools/cmd/goimports@latest @echo "Development environment ready." JETKVM_HOME ?= $(HOME)/.jetkvm 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.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 -lint: build_audio_deps +lint-go: build_audio_deps @echo "Running golangci-lint..." @mkdir -p static && touch static/.gitkeep 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" \ 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 -lint-fix: build_audio_deps +lint-go-fix: build_audio_deps @echo "Running golangci-lint with auto-fix..." @mkdir -p static && touch static/.gitkeep CGO_ENABLED=1 \ @@ -146,7 +156,16 @@ lint-fix: build_audio_deps golangci-lint run --fix --verbose # Run UI linting locally (mirrors GitHub workflow ui-lint.yml) -ui-lint: +lint-ui: @echo "Running UI lint..." @cd ui && npm ci @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 diff --git a/cmd/main.go b/cmd/main.go index 0cdb2b3..35ae413 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,7 +11,7 @@ import ( func main() { versionPtr := flag.Bool("version", false, "print version 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") flag.Parse() diff --git a/internal/audio/api.go b/internal/audio/api.go index b465fe8..d3a73f9 100644 --- a/internal/audio/api.go +++ b/internal/audio/api.go @@ -15,7 +15,7 @@ var ( // isAudioServerProcess detects if we're running as the audio server subprocess func isAudioServerProcess() bool { for _, arg := range os.Args { - if strings.Contains(arg, "--audio-server") { + if strings.Contains(arg, "--audio-output-server") { return true } } diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 0a7b468..93460f0 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -4,7 +4,6 @@ import ( "errors" "sync/atomic" "time" - // Explicit import for CGO audio stream glue ) var ( @@ -33,7 +32,7 @@ type AudioConfig struct { } // AudioMetrics tracks audio performance metrics -// Note: 64-bit fields must be first for proper alignment on 32-bit ARM + type AudioMetrics struct { FramesReceived int64 FramesDropped int64 @@ -61,72 +60,67 @@ var ( metrics AudioMetrics ) -// GetAudioQualityPresets returns predefined quality configurations +// qualityPresets defines the base quality configurations +var qualityPresets = map[AudioQuality]struct { + outputBitrate, inputBitrate int + sampleRate, channels int + frameSize time.Duration +}{ + AudioQualityLow: { + outputBitrate: 32, inputBitrate: 16, + sampleRate: 22050, channels: 1, + frameSize: 40 * time.Millisecond, + }, + AudioQualityMedium: { + outputBitrate: 64, inputBitrate: 32, + sampleRate: 44100, channels: 2, + frameSize: 20 * time.Millisecond, + }, + AudioQualityHigh: { + outputBitrate: 128, inputBitrate: 64, + sampleRate: 48000, channels: 2, + frameSize: 20 * time.Millisecond, + }, + AudioQualityUltra: { + outputBitrate: 192, inputBitrate: 96, + sampleRate: 48000, channels: 2, + frameSize: 10 * time.Millisecond, + }, +} + +// GetAudioQualityPresets returns predefined quality configurations for audio output func GetAudioQualityPresets() map[AudioQuality]AudioConfig { - return map[AudioQuality]AudioConfig{ - AudioQualityLow: { - Quality: AudioQualityLow, - Bitrate: 32, - SampleRate: 22050, - Channels: 1, - FrameSize: 40 * time.Millisecond, - }, - AudioQualityMedium: { - Quality: AudioQualityMedium, - Bitrate: 64, - SampleRate: 44100, - Channels: 2, - FrameSize: 20 * time.Millisecond, - }, - AudioQualityHigh: { - Quality: AudioQualityHigh, - Bitrate: 128, - SampleRate: 48000, - Channels: 2, - FrameSize: 20 * time.Millisecond, - }, - AudioQualityUltra: { - Quality: AudioQualityUltra, - Bitrate: 192, - SampleRate: 48000, - Channels: 2, - FrameSize: 10 * time.Millisecond, - }, + 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 func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig { - return map[AudioQuality]AudioConfig{ - AudioQualityLow: { - Quality: AudioQualityLow, - Bitrate: 16, - SampleRate: 16000, - Channels: 1, - FrameSize: 40 * time.Millisecond, - }, - 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, - }, + result := make(map[AudioQuality]AudioConfig) + for quality, preset := range qualityPresets { + result[quality] = AudioConfig{ + Quality: quality, + Bitrate: preset.inputBitrate, + SampleRate: func() int { + if quality == AudioQualityLow { + return 16000 + } + return preset.sampleRate + }(), + Channels: 1, // Microphone is always mono + FrameSize: preset.frameSize, + } } + return result } // SetAudioQuality updates the current audio quality configuration diff --git a/internal/audio/cgo_audio.go b/internal/audio/cgo_audio.go index c77739a..3d8f2a6 100644 --- a/internal/audio/cgo_audio.go +++ b/internal/audio/cgo_audio.go @@ -410,9 +410,7 @@ func cgoAudioClose() { C.jetkvm_audio_close() } -// Optimized read and encode with pre-allocated error objects and reduced checks func cgoAudioReadEncode(buf []byte) (int, error) { - // Fast path: check minimum buffer size (reduced from 1500 to 1276 for 10ms frames) if len(buf) < 1276 { return 0, errBufferTooSmall } @@ -427,11 +425,11 @@ func cgoAudioReadEncode(buf []byte) (int, error) { return int(n), nil } -// Go wrappers for audio playback (microphone input) +// Audio playback functions func cgoAudioPlaybackInit() error { ret := C.jetkvm_audio_playback_init() if ret != 0 { - return errors.New("failed to init ALSA playback/Opus decoder") + return errAudioPlaybackInit } return nil } @@ -440,44 +438,36 @@ func cgoAudioPlaybackClose() { C.jetkvm_audio_playback_close() } -// Decodes Opus frame and writes to playback device func cgoAudioDecodeWrite(buf []byte) (int, error) { if len(buf) == 0 { - return 0, errors.New("empty buffer") + return 0, errEmptyBuffer } - // Additional safety check to prevent segfault 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]) if bufPtr == nil { - return 0, errors.New("invalid buffer pointer") + return 0, errInvalidBufferPtr } - // Add recovery mechanism for C function crashes defer func() { if r := recover(); r != nil { - // Log the panic but don't crash the entire program - // This should not happen with proper validation, but provides safety - _ = r // Explicitly ignore the panic value + _ = r } }() n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf))) if n < 0 { - return 0, errors.New("audio decode/write error") + return 0, errAudioDecodeWrite } return int(n), nil } -// Wrapper functions for non-blocking audio manager +// CGO function aliases var ( CGOAudioInit = cgoAudioInit CGOAudioClose = cgoAudioClose diff --git a/internal/audio/input.go b/internal/audio/input.go index 300eb61..d99227d 100644 --- a/internal/audio/input.go +++ b/internal/audio/input.go @@ -9,9 +9,8 @@ import ( ) // AudioInputMetrics holds metrics for microphone input -// Note: int64 fields must be 64-bit aligned for atomic operations on ARM type AudioInputMetrics struct { - FramesSent int64 // Must be first for alignment + FramesSent int64 FramesDropped int64 BytesProcessed int64 ConnectionDrops int64 @@ -21,7 +20,6 @@ type AudioInputMetrics struct { // AudioInputManager manages microphone input stream using IPC mode only type AudioInputManager struct { - // metrics MUST be first for ARM32 alignment (contains int64 fields) metrics AudioInputMetrics ipcManager *AudioInputIPCManager diff --git a/internal/audio/supervisor.go b/internal/audio/supervisor.go index 3c4f478..8b4907f 100644 --- a/internal/audio/supervisor.go +++ b/internal/audio/supervisor.go @@ -248,7 +248,7 @@ func (s *AudioServerSupervisor) startProcess() error { defer s.mutex.Unlock() // 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.Stderr = os.Stderr @@ -261,7 +261,7 @@ func (s *AudioServerSupervisor) startProcess() error { s.logger.Info().Int("pid", s.processPID).Msg("audio server process started") // Add process to monitoring - s.processMonitor.AddProcess(s.processPID, "audio-server") + s.processMonitor.AddProcess(s.processPID, "audio-output-server") if s.onProcessStart != nil { s.onProcessStart(s.processPID)