Merge pull request 'Audio & Networking Hotfixes' (#41) from dev into main
Reviewed-on: https://codeberg.org/RhemaWave-Technologies/RhemaWave/pulls/41
This commit is contained in:
@@ -1,88 +0,0 @@
|
|||||||
# RhemaWave
|
|
||||||
|
|
||||||
A Go-based fullscreen SDL2/OpenGL application for Linux kiosk systems. Displays proclamations with audio playback and animated visual patterns.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Development uses Nix for a reproducible build environment.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix develop # enter dev shell
|
|
||||||
go build -o bin/rhemawave rhemawave.go # build
|
|
||||||
./bin/rhemawave # run
|
|
||||||
./dev_scripts/reset.sh # reset database (if needed)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Nix with flakes enabled
|
|
||||||
- A display server (X11/Wayland) and audio device for running the application
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **Page-based UI** — SDL2/OpenGL 3.3 fullscreen rendering with a page stack for navigation
|
|
||||||
- **Proclamation player** — scrolling text display with synchronized audio playback
|
|
||||||
- **Startup validation** — WiFi, TOS acceptance, email, license key checks
|
|
||||||
- **Singleton managers** — ShaderManager, FontManager, Logger (initialized once via `sync.Once`)
|
|
||||||
|
|
||||||
### Key Directories
|
|
||||||
|
|
||||||
```
|
|
||||||
bin/ # build output (gitignored)
|
|
||||||
share/rhemawave/ # assets (fonts, shaders, proclamations, TOS)
|
|
||||||
internal/ # Go source packages
|
|
||||||
packaging/ # deployment configs (kiosk, updater, polkit)
|
|
||||||
dev_scripts/ # developer utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
### Runtime Data
|
|
||||||
|
|
||||||
Stored in `~/.local/share/rhemawave/`:
|
|
||||||
|
|
||||||
- `data.db` — SQLite database (config, proclamations, logs)
|
|
||||||
- `log.txt` — application log
|
|
||||||
- `license_key` — license key file
|
|
||||||
|
|
||||||
### Development Rules
|
|
||||||
|
|
||||||
These rules must be followed during the development of RhemaWave to retain a
|
|
||||||
maintainable application.
|
|
||||||
|
|
||||||
1. The Application package is soley responsible for managing the database handler, and the
|
|
||||||
configuration cache. Internal packages may update the configuration within the database, and
|
|
||||||
then signal the application to refresh the cache via a callback function `refreshCache()`.
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
Deployed to kiosk hardware via NixOS.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
### Deployment Pipeline
|
|
||||||
|
|
||||||
A manual walkthough of deploying Radiant Wave updates.
|
|
||||||
For reference later when writing automated actions.
|
|
||||||
|
|
||||||
1. Create a new branch for issue being worked on
|
|
||||||
2. Perform work
|
|
||||||
3. Merge to dev when done
|
|
||||||
4. Build dev with `nix build` to verify no errors
|
|
||||||
5. Checkout KioskConfig dev
|
|
||||||
6. Update rhemawave-dev with `nix flake update rhemawave-dev`
|
|
||||||
7. Build rhemawavekiosk-dev virtual machine
|
|
||||||
8. Test virtual machine for problems
|
|
||||||
9. Build dev-iso
|
|
||||||
10. Install dev-iso to a machine and check for problems
|
|
||||||
11. Checkout `main`
|
|
||||||
12. Merge `dev`
|
|
||||||
13. `nix flake update rhemawave`
|
|
||||||
14. Build virtual machine
|
|
||||||
15. Build iso image
|
|
||||||
16. Build commercial virtual machine
|
|
||||||
17. Build commercial iso image
|
|
||||||
18. Build Raspberry Pi img
|
|
||||||
19. Approve all virtual machines and iso images
|
|
||||||
20. Approve all machines update on their own
|
|
||||||
21. Rename all iso image to "radiantwavekiosk-iso-{system}-{MM-DD-YYYY}-{radiantwave-git-hash}.iso"
|
|
||||||
22. Upload iso images to file server
|
|
||||||
23. Move old iso images to `archives/` on the fileserver
|
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#+TITLE: Radiantwave & KioskConfig Development & Release Pipeline
|
||||||
|
#+AUTHOR: Neil C.
|
||||||
|
#+DATE: 2026-06-23
|
||||||
|
#+OPTIONS: toc:nil num:nil
|
||||||
|
#+TAGS: development nixos radiantwave kiosk automation
|
||||||
|
|
||||||
|
* Radiant Wave
|
||||||
|
|
||||||
|
* Full Pipeline Workflow (Detailed)
|
||||||
|
|
||||||
|
** Development Phase
|
||||||
|
1. Create a new branch for the issue
|
||||||
|
2. Implement the changes for the issue
|
||||||
|
|
||||||
|
** Integration to =dev=
|
||||||
|
3. Open a pull request to =dev=
|
||||||
|
4. CI: Run =nix build= to verify no errors
|
||||||
|
5. CI: Run linting and unit tests
|
||||||
|
6. CI: Auto-update =rhemawave-dev= flake input
|
||||||
|
7. CI: Build all =dev= VMs in parallel (Kiosk, Commercial, RPi)
|
||||||
|
8. Merge the pull request to =dev= after CI passes
|
||||||
|
|
||||||
|
** Testing Phase
|
||||||
|
9. Run all VMs (Kiosk, Commercial, RPi) and test all functionality of the application on each VM
|
||||||
|
10. Install =dev-iso= to a test machine and verify installation and application functionality
|
||||||
|
|
||||||
|
** Release to =main=
|
||||||
|
11. Open a pull request from =dev= to =main=
|
||||||
|
12. CI: Build all =main= VMs in parallel (Kiosk, Commercial, RPi)
|
||||||
|
13. CI: Build all ISO images in parallel
|
||||||
|
14. CI: Auto-rename ISO images to format =radiantwave-{system}-{YYYY-MM-DD}-{git-hash}.iso=
|
||||||
|
15. CI: Upload ISO images to file server
|
||||||
|
16. CI: Move old ISO images to =archives/= on file server
|
||||||
|
17. Merge the pull request to =main= after CI passes
|
||||||
|
18. Tag the release (optional, for traceability)
|
||||||
|
|
||||||
|
* Automation Opportunities
|
||||||
|
|
||||||
|
This section describes which parts of the workflow can be automated via CI/CD, and how.
|
||||||
|
|
||||||
|
** Group 1: Development Validation (Steps 3-8)
|
||||||
|
- =What to automate=: PR triggering, =nix build=, linting, unit tests, flake updates, dev VM builds
|
||||||
|
- =How=: GitHub Actions workflow triggered on PR to =dev=
|
||||||
|
- =Why=: Catch integration errors before manual testing; ensure =dev= is always buildable
|
||||||
|
|
||||||
|
** Group 2: Testing (Steps 9-10)
|
||||||
|
- =What to automate=: VM functionality testing, ISO installation verification
|
||||||
|
- =How=: Scripted test suites, automated ISO installation in VMs
|
||||||
|
- =Why=: Reduce manual toil; increase test coverage and consistency
|
||||||
|
|
||||||
|
** Group 3: Release Pipeline (Steps 11-16)
|
||||||
|
- =What to automate=: Main VM/ISO builds, ISO renaming, file server uploads, archiving
|
||||||
|
- =How=: GitHub Actions workflow triggered on PR to =main=
|
||||||
|
- =Why=: Ensure consistent releases; eliminate human error in naming/uploading
|
||||||
|
|
||||||
|
* Preflight Checklist
|
||||||
|
|
||||||
|
- [ ] Create a new branch for the issue
|
||||||
|
- [ ] Implement the changes for the issue
|
||||||
|
- [ ] Open a pull request to =dev=
|
||||||
|
- [ ] Merge the pull request to =dev= after CI passes
|
||||||
|
- [ ] Run all VMs (Kiosk, Commercial, RPi) and test all functionality of the application on each VM
|
||||||
|
- [ ] Install =dev-iso= to a test machine and verify installation and application functionality
|
||||||
|
- [ ] Open a pull request from =dev= to =main=
|
||||||
|
- [ ] Merge the pull request to =main= after CI passes
|
||||||
|
- [ ] Tag the release (optional, for traceability)
|
||||||
|
|
||||||
|
* Automation Task List
|
||||||
|
|
||||||
|
** Group 1: Development Validation
|
||||||
|
- [ ] Set up GitHub Actions workflow for PRs to =dev=
|
||||||
|
- [ ] Configure =nix build= step
|
||||||
|
- [ ] Add linting and unit test steps
|
||||||
|
- [ ] Add flake auto-update step
|
||||||
|
- [ ] Configure parallel VM builds (Kiosk, Commercial, RPi)
|
||||||
|
- [ ] Verify workflow blocks PR merge on failure
|
||||||
|
|
||||||
|
** Group 2: Testing
|
||||||
|
- [ ] Create automated test scripts for VM functionality
|
||||||
|
- [ ] Set up automated ISO installation testing
|
||||||
|
- [ ] Integrate with CI for optional automated testing
|
||||||
|
|
||||||
|
** Group 3: Release Pipeline
|
||||||
|
- [ ] Set up GitHub Actions workflow for PRs to =main=
|
||||||
|
- [ ] Configure parallel VM builds
|
||||||
|
- [ ] Configure parallel ISO builds
|
||||||
|
- [ ] Add ISO auto-renaming step (using =git rev-parse HEAD= and =date +%Y-%m-%d=)
|
||||||
|
- [ ] Add file server upload step (=rsync= or similar)
|
||||||
|
- [ ] Add archive old ISOs step
|
||||||
|
- [ ] Verify workflow blocks PR merge on failure
|
||||||
@@ -41,11 +41,6 @@
|
|||||||
pkg-config
|
pkg-config
|
||||||
];
|
];
|
||||||
|
|
||||||
audioFile = pkgs.fetchurl {
|
|
||||||
url = "https://files.rhemawavetech.com/audio.wav";
|
|
||||||
sha256 = "sha256-5Dikxx9RKOoPfY1ukIgMoVQ7tzpMirp+XPj6OphfEW0=";
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.default = pkgs.buildGoModule {
|
packages.default = pkgs.buildGoModule {
|
||||||
@@ -75,10 +70,6 @@
|
|||||||
|
|
||||||
# Copy everything from your local share/rhemawave/* into the store
|
# Copy everything from your local share/rhemawave/* into the store
|
||||||
cp -r share/rhemawave/* $out/share/rhemawave/
|
cp -r share/rhemawave/* $out/share/rhemawave/
|
||||||
|
|
||||||
# Add the fetched audio file specifically
|
|
||||||
mkdir -p $out/share/rhemawave/audio
|
|
||||||
cp ${audioFile} $out/share/rhemawave/audio/audio.wav
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ func Run() {
|
|||||||
os.Remove(testFile)
|
os.Remove(testFile)
|
||||||
|
|
||||||
logger.AddFile(filepath.Join(dataDir, "log.txt"))
|
logger.AddFile(filepath.Join(dataDir, "log.txt"))
|
||||||
|
logger.SetLevel = logger.LevelDebug
|
||||||
|
|
||||||
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
|
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// Package db provides a database connection and functions for interacting with the database.
|
||||||
|
// The DB object should be held and maintained by the application, not by this package.
|
||||||
|
// The application should be responsible for connecting to, migrating, and initial population
|
||||||
|
// of the database. This package should only provide the schema definitions and helper functions.
|
||||||
|
// Schema definitions should clone API response structures as closely as possible.
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
connectMaxAttempts = 5
|
||||||
|
connectRetryDelay = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// Connect opens the SQLite database at dbpath with WAL mode and a hardened
|
||||||
|
// lock-recovery strategy suitable for kiosk use where the process may have
|
||||||
|
// crashed and left stale lock artefacts behind.
|
||||||
|
func Connect(dbpath string) (*gorm.DB, error) {
|
||||||
|
|
||||||
|
// ── Stale lock recovery ───────────────────────────────────────────────
|
||||||
|
// SQLite WAL mode leaves a .db-wal and .db-shm alongside the main file.
|
||||||
|
// If the previous process crashed, the OS releases file locks but the
|
||||||
|
// artefacts remain. SQLite recovers them automatically on the next open,
|
||||||
|
// UNLESS an OS-level advisory lock is still held (rare but possible on
|
||||||
|
// Linux with certain crash patterns). Removing them before opening is
|
||||||
|
// safe: SQLite treats absent WAL/SHM files as a clean slate and will
|
||||||
|
// recreate them. We only do this if the main DB file already exists
|
||||||
|
// (i.e. this is not a first-run) to avoid masking legitimate errors.
|
||||||
|
if _, err := os.Stat(dbpath); err == nil {
|
||||||
|
for _, ext := range []string{"-wal", "-shm"} {
|
||||||
|
artefact := dbpath + ext
|
||||||
|
if _, err := os.Stat(artefact); err == nil {
|
||||||
|
if removeErr := os.Remove(artefact); removeErr != nil {
|
||||||
|
// Non-fatal: log and continue — SQLite may still recover.
|
||||||
|
fmt.Printf("[DB] warning: could not remove stale %s: %v\n", ext, removeErr)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DB] removed stale lock artefact: %s\n", artefact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connection with retry ─────────────────────────────────────────────
|
||||||
|
// Even after removing WAL/SHM, the main DB file itself may still be
|
||||||
|
// briefly locked if another process is shutting down. Retry with
|
||||||
|
// backoff before giving up.
|
||||||
|
var (
|
||||||
|
db *gorm.DB
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for attempt := 1; attempt <= connectMaxAttempts; attempt++ {
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbpath), &gorm.Config{})
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if attempt == connectMaxAttempts {
|
||||||
|
return nil, fmt.Errorf("database still locked after %d attempts: %w", connectMaxAttempts, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[DB] open attempt %d/%d failed (%v), retrying in %s\n",
|
||||||
|
attempt, connectMaxAttempts, err, connectRetryDelay)
|
||||||
|
time.Sleep(connectRetryDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connection pool ───────────────────────────────────────────────────
|
||||||
|
// SQLite does not support concurrent writers. Setting MaxOpenConns(1)
|
||||||
|
// forces gorm to serialise all queries through a single connection,
|
||||||
|
// preventing "database is locked" errors from competing goroutines
|
||||||
|
// within this process. This is the single most important setting for
|
||||||
|
// SQLite in a multi-goroutine application.
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get underlying sql.DB: %w", err)
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
// ── Pragmas ───────────────────────────────────────────────────────────
|
||||||
|
pragmas := []string{
|
||||||
|
"PRAGMA journal_mode=WAL",
|
||||||
|
"PRAGMA synchronous=FULL",
|
||||||
|
"PRAGMA foreign_keys=ON",
|
||||||
|
// 5 s gives the serialised connection time to finish a long write
|
||||||
|
// before returning SQLITE_BUSY to the application layer.
|
||||||
|
"PRAGMA busy_timeout=5000",
|
||||||
|
}
|
||||||
|
for _, pragma := range pragmas {
|
||||||
|
if _, err := sqlDB.Exec(pragma); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set %q: %w", pragma, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the database schema.
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&AppConfig{},
|
||||||
|
&Log{},
|
||||||
|
&Proclamation{},
|
||||||
|
&ProclamationCategory{},
|
||||||
|
&SolfeggioFrequency{},
|
||||||
|
&AudioFile{},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Package db provides a database connection and functions for interacting with the database.
|
|
||||||
// The DB object should be held and maintained by the application, not by this package.
|
|
||||||
// The application should be responsible for connecting to, migrating, and initial population
|
|
||||||
// of the database. This package should only provide the schema definitions and helper functions.
|
|
||||||
// Schema definitions should clone API response structures as closely as possible.
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Connect to the database and return the gorm.DB object.
|
|
||||||
func Connect(dbpath string) (*gorm.DB, error) {
|
|
||||||
db, err := gorm.Open(sqlite.Open(dbpath), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set pragmas
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pragmas := []string{
|
|
||||||
"PRAGMA journal_mode=WAL",
|
|
||||||
"PRAGMA synchronous=FULL",
|
|
||||||
"PRAGMA foreign_keys=ON",
|
|
||||||
"PRAGMA busy_timeout=5000",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pragma := range pragmas {
|
|
||||||
if _, err := sqlDB.Exec(pragma); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate the database schema
|
|
||||||
func Migrate(db *gorm.DB) error {
|
|
||||||
// Pass all models to be migrated to this function call.
|
|
||||||
if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}, &ProclamationCategory{}, &SolfeggioFrequency{}, &AudioFile{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -20,12 +20,46 @@ type DownloadStatus struct {
|
|||||||
Err error // Error, if any
|
Err error // Error, if any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureDir checks if a dire
|
||||||
|
func ensureDir(path string) error {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
if !info.IsDir() {
|
||||||
|
return fmt.Errorf("path exists but is not a directory: %s", path)
|
||||||
|
}
|
||||||
|
fmt.Printf("Directory already exists: %s\n", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("error checking path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0760 = rwxrw---- (owner: rwx, group: rw-, others: ---)
|
||||||
|
if err := os.MkdirAll(path, 0760); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Directory created: %s\n", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DownloadAudioFiles downloads multiple audio files and sends progress updates
|
// DownloadAudioFiles downloads multiple audio files and sends progress updates
|
||||||
// to the provided channel. The channel is closed when all downloads are complete.
|
// to the provided channel. The channel is closed when all downloads are complete.
|
||||||
func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string, destDir string) {
|
func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string, destDir string) {
|
||||||
defer close(progressChan)
|
defer close(progressChan)
|
||||||
|
|
||||||
urlBase := "https://files.rhemawavetech.com/audio/"
|
logger.Info("Starting downloads")
|
||||||
|
|
||||||
|
if err := ensureDir(destDir); err != nil {
|
||||||
|
logger.Error("failed to ensureDir %s %v", destDir, err)
|
||||||
|
progressChan <- DownloadStatus{
|
||||||
|
Msg: fmt.Sprintf("Failed to ensure directory exists: %v", err),
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urlBase := "https://files.radiantwavetech.com/audio/"
|
||||||
|
|
||||||
// First, calculate the total size of files that need to be downloaded
|
// First, calculate the total size of files that need to be downloaded
|
||||||
totalSize := int64(0)
|
totalSize := int64(0)
|
||||||
@@ -33,11 +67,13 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
totalDownloaded := int64(0)
|
totalDownloaded := int64(0)
|
||||||
|
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
|
logger.Info("Downloading %s", filename)
|
||||||
destPath := filepath.Join(destDir, filename)
|
destPath := filepath.Join(destDir, filename)
|
||||||
// Check if the file already exists
|
// Check if the file already exists
|
||||||
url := urlBase + filename
|
url := urlBase + filename
|
||||||
resp, err := http.Head(url)
|
resp, err := http.Head(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get size for %s: %v", filename, err)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to get size for %s: %v", filename, err),
|
Msg: fmt.Sprintf("Failed to get size for %s: %v", filename, err),
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -45,6 +81,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
logger.Error("Failed to get size for %s: status %s", filename, resp.Status)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to get size for %s: status %s", filename, resp.Status),
|
Msg: fmt.Sprintf("Failed to get size for %s: status %s", filename, resp.Status),
|
||||||
Err: fmt.Errorf("bad status: %s", resp.Status),
|
Err: fmt.Errorf("bad status: %s", resp.Status),
|
||||||
@@ -54,9 +91,9 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
fileStat, err := os.Stat(destPath)
|
fileStat, err := os.Stat(destPath)
|
||||||
// If the file is incomplete, or does not exist
|
// If the file is incomplete, or does not exist
|
||||||
if os.IsNotExist(err) || fileStat.Size() != resp.ContentLength {
|
if os.IsNotExist(err) || fileStat.Size() != resp.ContentLength {
|
||||||
// Oh, and log a warning if the file is not complete
|
// log a warning if the file is not complete
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
logger.Warning("%s incomplete/corrupt. Adding to download queue.", filename)
|
logger.Warning("%s corrupt. Adding to download queue.", filename)
|
||||||
} else {
|
} else {
|
||||||
logger.Info("%s not found. Adding to download queue.", filename)
|
logger.Info("%s not found. Adding to download queue.", filename)
|
||||||
}
|
}
|
||||||
@@ -87,9 +124,12 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
url := urlBase + filename
|
url := urlBase + filename
|
||||||
destPath := filepath.Join(destDir, filename)
|
destPath := filepath.Join(destDir, filename)
|
||||||
|
|
||||||
|
// Ensure the destination directory exists
|
||||||
|
|
||||||
// Create the destination file
|
// Create the destination file
|
||||||
out, err := os.Create(destPath)
|
out, err := os.Create(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error("Failed to create file %s: %v", destPath, err)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to create file %s: %v", destPath, err),
|
Msg: fmt.Sprintf("Failed to create file %s: %v", destPath, err),
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -102,6 +142,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
out.Close()
|
out.Close()
|
||||||
os.Remove(destPath)
|
os.Remove(destPath)
|
||||||
|
logger.Error("Failed to download %s: %v", filename, err)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to download %s: %v", filename, err),
|
Msg: fmt.Sprintf("Failed to download %s: %v", filename, err),
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -113,6 +154,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
out.Close()
|
out.Close()
|
||||||
os.Remove(destPath)
|
os.Remove(destPath)
|
||||||
|
logger.Error("Failed to download %s: status %s", filename, resp.Status)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to download %s: status %s", filename, resp.Status),
|
Msg: fmt.Sprintf("Failed to download %s: status %s", filename, resp.Status),
|
||||||
Err: fmt.Errorf("bad status: %s", resp.Status),
|
Err: fmt.Errorf("bad status: %s", resp.Status),
|
||||||
@@ -127,6 +169,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
out.Close()
|
out.Close()
|
||||||
os.Remove(destPath)
|
os.Remove(destPath)
|
||||||
|
logger.Error("Failed to read %s: %v", filename, err)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to read %s: %v", filename, err),
|
Msg: fmt.Sprintf("Failed to read %s: %v", filename, err),
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -141,6 +184,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
if _, err := out.Write(buf[:n]); err != nil {
|
if _, err := out.Write(buf[:n]); err != nil {
|
||||||
out.Close()
|
out.Close()
|
||||||
os.Remove(destPath)
|
os.Remove(destPath)
|
||||||
|
logger.Error("Failed to write %s: %v", filename, err)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
Msg: fmt.Sprintf("Failed to write %s: %v", filename, err),
|
Msg: fmt.Sprintf("Failed to write %s: %v", filename, err),
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -148,6 +192,8 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("Finished downloading %s", filename)
|
||||||
|
|
||||||
// Update progress
|
// Update progress
|
||||||
totalDownloaded += int64(n)
|
totalDownloaded += int64(n)
|
||||||
progressChan <- DownloadStatus{
|
progressChan <- DownloadStatus{
|
||||||
@@ -158,13 +204,6 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.Close()
|
out.Close()
|
||||||
|
|
||||||
// Send completion for this file
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Max: totalSize,
|
|
||||||
Cur: totalDownloaded,
|
|
||||||
Msg: "Finished Downloading",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send final status
|
// Send final status
|
||||||
@@ -173,4 +212,5 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
|
|||||||
Cur: totalDownloaded,
|
Cur: totalDownloaded,
|
||||||
Msg: "All audio downloads completed!",
|
Msg: "All audio downloads completed!",
|
||||||
}
|
}
|
||||||
|
logger.Info("All audio downloads complete!")
|
||||||
}
|
}
|
||||||
|
|||||||
+192
-24
@@ -5,14 +5,15 @@ package network
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"rhemawavetech.com/rhemawave/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- D-Bus NetworkManager Constants ---
|
// --- D-Bus NetworkManager Constants ---
|
||||||
@@ -28,6 +29,22 @@ const (
|
|||||||
nmDeviceTypeWifi uint32 = 2
|
nmDeviceTypeWifi uint32 = 2
|
||||||
nmDeviceStateActivated uint32 = 100
|
nmDeviceStateActivated uint32 = 100
|
||||||
APFlagsPrivacy uint32 = 0x00000001 // Equivalent to NM_802_11_AP_FLAGS_PRIVACY
|
APFlagsPrivacy uint32 = 0x00000001 // Equivalent to NM_802_11_AP_FLAGS_PRIVACY
|
||||||
|
|
||||||
|
// Device states (from NM spec)
|
||||||
|
// nmDeviceStateUnknown uint32 = 0
|
||||||
|
nmDeviceStateUnmanaged uint32 = 10
|
||||||
|
nmDeviceStateUnavailable uint32 = 20
|
||||||
|
// nmDeviceStateDisconnected uint32 = 30
|
||||||
|
nmDeviceStateFailed uint32 = 120
|
||||||
|
|
||||||
|
// AP Security Flags (from NM_802_11_AP_SEC_*)
|
||||||
|
APFlagsWpaPsk uint32 = 0x00000100 // WPA-Personal
|
||||||
|
APFlagsWpaEap uint32 = 0x00000200 // WPA-Enterprise
|
||||||
|
APFlagsRsnPsk uint32 = 0x00000100 // WPA2-Personal (same flag as WPA-PSK)
|
||||||
|
APFlagsRsnSae uint32 = 0x00000400 // WPA3-Personal (SAE)
|
||||||
|
APFlagsRsnEap uint32 = 0x00000200 // WPA2-Enterprise (same flag as WPA-EAP)
|
||||||
|
APFlagsRsnOwe uint32 = 0x00000800 // OWE (Opportunistic Wireless Encryption)
|
||||||
|
APFlagsRsnEapSuiteB uint32 = 0x00001000 // WPA3-Enterprise
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrScanAborted is returned when a scan is cancelled via the stop channel.
|
// ErrScanAborted is returned when a scan is cancelled via the stop channel.
|
||||||
@@ -75,6 +92,7 @@ type Manager struct {
|
|||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
nmObject dbus.BusObject
|
nmObject dbus.BusObject
|
||||||
Devices []Device
|
Devices []Device
|
||||||
|
mu sync.RWMutex // protects Devices
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessPoint holds essential information about a discovered Wi-Fi network.
|
// AccessPoint holds essential information about a discovered Wi-Fi network.
|
||||||
@@ -89,31 +107,44 @@ type AccessPoint struct {
|
|||||||
|
|
||||||
// New creates a new network Manager and connects to the system D-Bus.
|
// New creates a new network Manager and connects to the system D-Bus.
|
||||||
func New() (*Manager, error) {
|
func New() (*Manager, error) {
|
||||||
|
logger.Info("[NET-001] Initializing NetworkManager client")
|
||||||
|
logger.Debug("[NET-002] Connecting to system D-Bus")
|
||||||
conn, err := dbus.ConnectSystemBus()
|
conn, err := dbus.ConnectSystemBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error("failed to connect to system bus: %v", err)
|
||||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
}
|
}
|
||||||
nmObject := conn.Object(nmService, nmPath)
|
nmObject := conn.Object(nmService, nmPath)
|
||||||
|
logger.Debug("network manager created")
|
||||||
return &Manager{conn: conn, nmObject: nmObject}, nil
|
return &Manager{conn: conn, nmObject: nmObject}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conn returns the underlying dbus.Conn for advanced queries.
|
// Conn returns the underlying dbus.Conn for advanced queries.
|
||||||
func (m *Manager) Conn() *dbus.Conn {
|
func (m *Manager) Conn() *dbus.Conn {
|
||||||
|
logger.Debug("[NET-010] Returning underlying D-Bus connection")
|
||||||
|
logger.Debug("Conn() returning m.conn")
|
||||||
return m.conn
|
return m.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close disconnects from the D-Bus.
|
// Close disconnects from the D-Bus.
|
||||||
func (m *Manager) Close() {
|
func (m *Manager) Close() {
|
||||||
|
logger.Info("[NET-020] Closing NetworkManager client")
|
||||||
|
logger.Debug("Disconnecting from the D-Bus")
|
||||||
if m.conn != nil {
|
if m.conn != nil {
|
||||||
m.conn.Close()
|
m.conn.Close()
|
||||||
|
logger.Debug("Disconnected")
|
||||||
}
|
}
|
||||||
|
logger.Debug("Nothing to disconnect!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindDevices scans the system for all available network devices (Ethernet and Wi-Fi).
|
// FindDevices scans the system for all available network devices (Ethernet and Wi-Fi).
|
||||||
func (m *Manager) FindDevices() error {
|
func (m *Manager) FindDevices() error {
|
||||||
|
logger.Info("[NET-030] Starting network device discovery")
|
||||||
|
logger.Debug("Searching for network devices")
|
||||||
var devicePaths []dbus.ObjectPath
|
var devicePaths []dbus.ObjectPath
|
||||||
call := m.nmObject.Call(nmInterface+".GetDevices", 0)
|
call := m.nmObject.Call(nmInterface+".GetDevices", 0)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
|
logger.Error("GetDevices D-Bus call failed: %v", call.Err)
|
||||||
return fmt.Errorf("GetDevices D-Bus call failed: %w", call.Err)
|
return fmt.Errorf("GetDevices D-Bus call failed: %w", call.Err)
|
||||||
}
|
}
|
||||||
if err := call.Store(&devicePaths); err != nil {
|
if err := call.Store(&devicePaths); err != nil {
|
||||||
@@ -125,10 +156,14 @@ func (m *Manager) FindDevices() error {
|
|||||||
deviceObj := m.conn.Object(nmService, path)
|
deviceObj := m.conn.Object(nmService, path)
|
||||||
typeVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".DeviceType")
|
typeVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".DeviceType")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("network: Skipping device %s, could not get DeviceType: %v", path, err)
|
logger.Warning("[NET-031] Skipping device %s because DeviceType could not be read: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deviceType, ok := typeVariant.Value().(uint32)
|
||||||
|
if !ok {
|
||||||
|
logger.Warning("[NET-032] Skipping device %s because DeviceType was invalid", path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
deviceType, _ := typeVariant.Value().(uint32)
|
|
||||||
|
|
||||||
var newDevice Device
|
var newDevice Device
|
||||||
switch deviceType {
|
switch deviceType {
|
||||||
@@ -141,10 +176,29 @@ func (m *Manager) FindDevices() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newDevice.Path = path
|
newDevice.Path = path
|
||||||
ifaceVariant, _ := deviceObj.GetProperty(nmDeviceInterface + ".Interface")
|
|
||||||
newDevice.InterfaceName, _ = ifaceVariant.Value().(string)
|
// Safely get Interface property
|
||||||
stateVariant, _ := deviceObj.GetProperty(nmDeviceInterface + ".State")
|
ifaceVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".Interface")
|
||||||
newDevice.State, _ = stateVariant.Value().(uint32)
|
if err != nil {
|
||||||
|
logger.Warning("[NET-033] Skipping device %s because Interface property could not be read: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if newDevice.InterfaceName, ok = ifaceVariant.Value().(string); !ok {
|
||||||
|
logger.Warning("[NET-034] Skipping device %s because Interface property was invalid", path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely get State property
|
||||||
|
stateVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".State")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("[NET-035] Skipping device %s because State property could not be read: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if newDevice.State, ok = stateVariant.Value().(uint32); !ok {
|
||||||
|
logger.Warning("[NET-036] Skipping device %s because State property was invalid", path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
m.Devices = append(m.Devices, newDevice)
|
m.Devices = append(m.Devices, newDevice)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -152,9 +206,35 @@ func (m *Manager) FindDevices() error {
|
|||||||
|
|
||||||
// CheckInternetConnection performs a check for an active internet connection.
|
// CheckInternetConnection performs a check for an active internet connection.
|
||||||
func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) {
|
func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) {
|
||||||
|
logger.Debug("[NET-040] Checking internet connectivity")
|
||||||
|
|
||||||
|
// Previously called FindDevices() here, which rebuilt m.Devices every 2s,
|
||||||
|
// causing [NET-030] log spam and invalidating any pointers held by callers.
|
||||||
|
// Device enumeration is done once in Init(); we only need fresh State values.
|
||||||
|
// If this is a fresh Manager with no devices yet (e.g. called from
|
||||||
|
// checkState which creates a new Manager each tick), do a full device
|
||||||
|
// discovery. If devices are already known (e.g. the WiFiSetupPage's
|
||||||
|
// long-lived Manager), only refresh State to avoid the [NET-030] spam
|
||||||
|
// and avoid rebuilding the slice which would invalidate held pointers.
|
||||||
|
if len(m.Devices) == 0 {
|
||||||
if err := m.FindDevices(); err != nil {
|
if err := m.FindDevices(); err != nil {
|
||||||
return &ConnectionStatus{StatusError, "Failed to scan devices"}, err
|
return &ConnectionStatus{StatusError, "Failed to scan devices"}, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
m.mu.Lock()
|
||||||
|
for i := range m.Devices {
|
||||||
|
deviceObj := m.conn.Object(nmService, m.Devices[i].Path)
|
||||||
|
stateVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".State")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("[NET-041] Could not refresh state for device %s: %v", m.Devices[i].Path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if state, ok := stateVariant.Value().(uint32); ok {
|
||||||
|
m.Devices[i].State = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Check Ethernet first (Priority)
|
// Check Ethernet first (Priority)
|
||||||
for _, device := range m.Devices {
|
for _, device := range m.Devices {
|
||||||
@@ -182,13 +262,22 @@ func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) {
|
|||||||
|
|
||||||
// StartScan performs a Wi-Fi scan asynchronously. The optional stopChan can be used to abort.
|
// StartScan performs a Wi-Fi scan asynchronously. The optional stopChan can be used to abort.
|
||||||
func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- error, stopChan <-chan struct{}) {
|
func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- error, stopChan <-chan struct{}) {
|
||||||
|
|
||||||
|
logger.Info("[NET-050] Starting Wi‑Fi scan")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
|
||||||
var wifiDevice *Device
|
var wifiDevice *Device
|
||||||
|
|
||||||
for i := range m.Devices {
|
for i := range m.Devices {
|
||||||
if m.Devices[i].Type == TypeWifi {
|
if m.Devices[i].Type == TypeWifi {
|
||||||
wifiDevice = &m.Devices[i]
|
wifiDevice = &m.Devices[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
if wifiDevice == nil {
|
if wifiDevice == nil {
|
||||||
errorChan <- fmt.Errorf("no Wi-Fi device available for scanning")
|
errorChan <- fmt.Errorf("no Wi-Fi device available for scanning")
|
||||||
return
|
return
|
||||||
@@ -196,27 +285,35 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e
|
|||||||
|
|
||||||
// Ensure Wi-Fi is enabled
|
// Ensure Wi-Fi is enabled
|
||||||
nmObj := m.conn.Object(nmService, nmPath)
|
nmObj := m.conn.Object(nmService, nmPath)
|
||||||
|
|
||||||
prop, err := nmObj.GetProperty(nmInterface + ".WirelessEnabled")
|
prop, err := nmObj.GetProperty(nmInterface + ".WirelessEnabled")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChan <- fmt.Errorf("could not read WirelessEnabled property: %w", err)
|
errorChan <- fmt.Errorf("could not read WirelessEnabled property: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !prop.Value().(bool) {
|
if !prop.Value().(bool) {
|
||||||
|
|
||||||
call := nmObj.Call("org.freedesktop.DBus.Properties.Set", 0,
|
call := nmObj.Call("org.freedesktop.DBus.Properties.Set", 0,
|
||||||
nmInterface,
|
nmInterface,
|
||||||
"WirelessEnabled",
|
"WirelessEnabled",
|
||||||
dbus.MakeVariant(true),
|
dbus.MakeVariant(true),
|
||||||
)
|
)
|
||||||
|
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
errorChan <- fmt.Errorf("could not enable Wi-Fi: %w", call.Err)
|
errorChan <- fmt.Errorf("could not enable Wi-Fi: %w", call.Err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(2 * time.Second) // Give NetworkManager time to bring up the device
|
time.Sleep(2 * time.Second) // Give NetworkManager time to bring up the device
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request a scan
|
// Request a scan
|
||||||
wifiDeviceWireless := m.conn.Object(nmService, wifiDevice.Path)
|
wifiDeviceWireless := m.conn.Object(nmService, wifiDevice.Path)
|
||||||
|
|
||||||
call := wifiDeviceWireless.Call(nmWirelessDeviceInterface+".RequestScan", 0, make(map[string]dbus.Variant))
|
call := wifiDeviceWireless.Call(nmWirelessDeviceInterface+".RequestScan", 0, make(map[string]dbus.Variant))
|
||||||
|
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
errorChan <- fmt.Errorf("scan request failed: %w", call.Err)
|
errorChan <- fmt.Errorf("scan request failed: %w", call.Err)
|
||||||
return
|
return
|
||||||
@@ -230,11 +327,14 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e
|
|||||||
}
|
}
|
||||||
|
|
||||||
var apPaths []dbus.ObjectPath
|
var apPaths []dbus.ObjectPath
|
||||||
|
|
||||||
call = wifiDeviceWireless.Call(nmWirelessDeviceInterface+".GetAllAccessPoints", 0)
|
call = wifiDeviceWireless.Call(nmWirelessDeviceInterface+".GetAllAccessPoints", 0)
|
||||||
|
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
errorChan <- fmt.Errorf("could not retrieve network list: %w", call.Err)
|
errorChan <- fmt.Errorf("could not retrieve network list: %w", call.Err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := call.Store(&apPaths); err != nil {
|
if err := call.Store(&apPaths); err != nil {
|
||||||
errorChan <- fmt.Errorf("could not parse network list: %w", err)
|
errorChan <- fmt.Errorf("could not parse network list: %w", err)
|
||||||
return
|
return
|
||||||
@@ -245,22 +345,24 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e
|
|||||||
errorChan <- err
|
errorChan <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resultsChan <- processedAPs
|
resultsChan <- processedAPs
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanAccessPoints filters and sorts the raw list of D-Bus AP paths.
|
// scanAccessPoints filters and sorts the raw list of D-Bus AP paths.
|
||||||
func (m *Manager) scanAccessPoints(apPaths []dbus.ObjectPath) ([]AccessPoint, error) {
|
func (m *Manager) scanAccessPoints(apPaths []dbus.ObjectPath) ([]AccessPoint, error) {
|
||||||
|
logger.Debug("[NET-060] Processing %d access points", len(apPaths))
|
||||||
tempNetworkMap := make(map[string]AccessPoint)
|
tempNetworkMap := make(map[string]AccessPoint)
|
||||||
for _, apPath := range apPaths {
|
for _, apPath := range apPaths {
|
||||||
apObj := m.conn.Object(nmService, apPath)
|
apObj := m.conn.Object(nmService, apPath)
|
||||||
props, err := m.getAllProperties(apObj, nmAPInterface)
|
props, err := m.getAllProperties(apObj, nmAPInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("network: Failed to get properties for AP %s: %v", apPath, err)
|
logger.Warning("[NET-061] Failed to read access point properties for %s: %v", apPath, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ssidBytes, _ := props["Ssid"].Value().([]byte)
|
ssidBytes, ok := props["Ssid"].Value().([]byte)
|
||||||
if len(ssidBytes) == 0 {
|
if !ok || len(ssidBytes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,12 +374,30 @@ func (m *Manager) scanAccessPoints(apPaths []dbus.ObjectPath) ([]AccessPoint, er
|
|||||||
|
|
||||||
isProtected := (flags&APFlagsPrivacy != 0) || (wpaFlags != 0) || (rsnFlags != 0)
|
isProtected := (flags&APFlagsPrivacy != 0) || (wpaFlags != 0) || (rsnFlags != 0)
|
||||||
|
|
||||||
// Determine the key management type
|
// Determine key management type
|
||||||
var keyMgmt string
|
var keyMgmt string
|
||||||
if wpaFlags != 0 {
|
if !isProtected {
|
||||||
|
keyMgmt = "none"
|
||||||
|
} else if wpaFlags != 0 {
|
||||||
|
// WPA (not WPA2/WPA3)
|
||||||
|
if wpaFlags&APFlagsWpaPsk != 0 {
|
||||||
keyMgmt = "wpa-psk"
|
keyMgmt = "wpa-psk"
|
||||||
|
} else if wpaFlags&APFlagsWpaEap != 0 {
|
||||||
|
keyMgmt = "wpa-eap"
|
||||||
|
} else {
|
||||||
|
keyMgmt = "wpa-psk" // Fallback
|
||||||
|
}
|
||||||
} else if rsnFlags != 0 {
|
} else if rsnFlags != 0 {
|
||||||
keyMgmt = "wpa-psk"
|
// WPA2 or WPA3 (RSN)
|
||||||
|
if rsnFlags&APFlagsRsnSae != 0 {
|
||||||
|
keyMgmt = "sae" // WPA3-Personal
|
||||||
|
} else if rsnFlags&APFlagsRsnEapSuiteB != 0 {
|
||||||
|
keyMgmt = "eap-suite-b-192" // WPA3-Enterprise
|
||||||
|
} else if rsnFlags&APFlagsRsnEap != 0 {
|
||||||
|
keyMgmt = "wpa-eap" // WPA2-Enterprise
|
||||||
|
} else {
|
||||||
|
keyMgmt = "wpa-psk" // WPA2-Personal (default for RSN)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
keyMgmt = "none"
|
keyMgmt = "none"
|
||||||
}
|
}
|
||||||
@@ -309,9 +429,45 @@ func (m *Manager) scanAccessPoints(apPaths []dbus.ObjectPath) ([]AccessPoint, er
|
|||||||
|
|
||||||
// Connect attempts to connect a Wi-Fi device to a given access point.
|
// Connect attempts to connect a Wi-Fi device to a given access point.
|
||||||
func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string) error {
|
func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string) error {
|
||||||
|
logger.Info("[NET-070] Connection requested for SSID %q using adapter %q", ap.SSID, wifiDevice.InterfaceName)
|
||||||
if wifiDevice.Type != TypeWifi {
|
if wifiDevice.Type != TypeWifi {
|
||||||
return fmt.Errorf("cannot connect: provided device is not a Wi-Fi device")
|
return fmt.Errorf("cannot connect: provided device is not a Wi-Fi device")
|
||||||
}
|
}
|
||||||
|
m.mu.RLock()
|
||||||
|
currentState := wifiDevice.State
|
||||||
|
for _, d := range m.Devices {
|
||||||
|
if d.Path == wifiDevice.Path {
|
||||||
|
currentState = d.State
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if currentState == nmDeviceStateFailed ||
|
||||||
|
currentState == nmDeviceStateUnmanaged ||
|
||||||
|
currentState == nmDeviceStateUnavailable {
|
||||||
|
return fmt.Errorf("cannot connect: device is in an error state (state=%d)", currentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any existing NM connection profile for this SSID to avoid
|
||||||
|
// accumulating duplicates across connection attempts.
|
||||||
|
settingsObj := m.conn.Object(nmService, "/org/freedesktop/NetworkManager/Settings")
|
||||||
|
var existingPaths []dbus.ObjectPath
|
||||||
|
if call := settingsObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0); call.Err == nil {
|
||||||
|
_ = call.Store(&existingPaths)
|
||||||
|
for _, cp := range existingPaths {
|
||||||
|
cObj := m.conn.Object(nmService, cp)
|
||||||
|
var csettings map[string]map[string]dbus.Variant
|
||||||
|
if c2 := cObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0); c2.Err == nil {
|
||||||
|
if err := c2.Store(&csettings); err == nil {
|
||||||
|
if id, ok := csettings["connection"]["id"].Value().(string); ok && id == ap.SSID {
|
||||||
|
logger.Info("[NET-073] Deleting existing profile for %q before reconnect", ap.SSID)
|
||||||
|
_ = cObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
settings := map[string]map[string]dbus.Variant{
|
settings := map[string]map[string]dbus.Variant{
|
||||||
"connection": {
|
"connection": {
|
||||||
@@ -349,34 +505,45 @@ func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string)
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// THIS IS THE LOG YOU NEED TO SEE IN SSH
|
// THIS IS THE LOG YOU NEED TO SEE IN SSH
|
||||||
log.Printf("NETWORK MANAGER FAIL: %v", err)
|
logger.Error("[NET-071] NetworkManager rejected the connection request: %v", err)
|
||||||
return fmt.Errorf("AddAndActivateConnection failed: %w", err)
|
return fmt.Errorf("AddAndActivateConnection failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Connection created: %s, Active path: %s", connectionPath, activeConnectionPath)
|
logger.Info("[NET-072] Connection created successfully. Connection=%s Active=%s", connectionPath, activeConnectionPath)
|
||||||
|
|
||||||
// Wait for the connection to be fully established
|
// Removed WaitForInternet() call here. It blocked the caller's thread
|
||||||
if _, err := m.WaitForInternet(30*time.Second, 5*time.Second); err != nil {
|
// for up to 35s and returned an error even on successful NM initiation
|
||||||
return fmt.Errorf("failed to establish internet connection: %w", err)
|
// if internet wasn't stable in time. The page's checkConnectionStatusLoop
|
||||||
}
|
// and handleConnectionStatusUpdate handle post-connection state tracking.
|
||||||
|
logger.Info("[NET-072] Connection initiated. Connection=%s Active=%s", connectionPath, activeConnectionPath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pingTest with a short timeout to prevent the UI loop from hanging
|
// pingTest with a short timeout to prevent the UI loop from hanging
|
||||||
func pingTest() bool {
|
func pingTest() bool {
|
||||||
// Use a dialer with a timeout instead of LookupHost which can hang
|
logger.Debug("[NET-080] Beginning internet reachability test")
|
||||||
timeout := 2 * time.Second
|
targets := []string{
|
||||||
conn, err := net.DialTimeout("tcp", "8.8.8.8:53", timeout)
|
"1.1.1.1:53", // Cloudflare DNS
|
||||||
if err != nil {
|
"8.8.8.8:53", // Google DNS
|
||||||
return false
|
"9.9.9.9:53", // Quad9 DNS
|
||||||
|
"149.112.112.112:53", // Quad9 Alternate
|
||||||
}
|
}
|
||||||
|
timeout := 2 * time.Second
|
||||||
|
|
||||||
|
for _, target := range targets {
|
||||||
|
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||||
|
if err == nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAllProperties is a helper to fetch all properties for a D-Bus object interface.
|
// getAllProperties is a helper to fetch all properties for a D-Bus object interface.
|
||||||
func (m *Manager) getAllProperties(obj dbus.BusObject, iface string) (map[string]dbus.Variant, error) {
|
func (m *Manager) getAllProperties(obj dbus.BusObject, iface string) (map[string]dbus.Variant, error) {
|
||||||
|
logger.Debug("[NET-090] Fetching all D‑Bus properties for interface %s", iface)
|
||||||
var props map[string]dbus.Variant
|
var props map[string]dbus.Variant
|
||||||
call := obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface)
|
call := obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
@@ -392,6 +559,7 @@ func (m *Manager) getAllProperties(obj dbus.BusObject, iface string) (map[string
|
|||||||
// or until maxWait elapses. When it first sees "up", it requires that state
|
// or until maxWait elapses. When it first sees "up", it requires that state
|
||||||
// to be stable for at least 'stableFor' before returning success to avoid flapping.
|
// to be stable for at least 'stableFor' before returning success to avoid flapping.
|
||||||
func (m *Manager) WaitForInternet(maxWait, stableFor time.Duration) (*ConnectionStatus, error) {
|
func (m *Manager) WaitForInternet(maxWait, stableFor time.Duration) (*ConnectionStatus, error) {
|
||||||
|
logger.Info("[NET-100] Waiting for internet connectivity (maxWait=%s stableFor=%s)", maxWait, stableFor)
|
||||||
deadline := time.Now().Add(maxWait)
|
deadline := time.Now().Add(maxWait)
|
||||||
var lastErr error
|
var lastErr error
|
||||||
var lastStatus *ConnectionStatus
|
var lastStatus *ConnectionStatus
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GitVersion = "1.0.0-beta"
|
var GitVersion = "cd79a6f"
|
||||||
|
|
||||||
type Home struct {
|
type Home struct {
|
||||||
Base
|
Base
|
||||||
|
|||||||
@@ -287,6 +287,12 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
var selectedAudio db.AudioFile
|
var selectedAudio db.AudioFile
|
||||||
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
||||||
logger.Error("error querying database for selected audio: %v", err)
|
logger.Error("error querying database for selected audio: %v", err)
|
||||||
|
selectedAudio = db.AudioFile{
|
||||||
|
Name: "OriginalAudio",
|
||||||
|
Path: filepath.Join(p.Context.CfgPtr.AssetsDir, "audio", "OriginalAudio.wav"),
|
||||||
|
Selected: true,
|
||||||
|
Order: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Playing %s", selectedAudio.Path)
|
logger.Info("Playing %s", selectedAudio.Path)
|
||||||
|
|||||||
@@ -91,17 +91,29 @@ func (p *WiFiSetupPage) Init(ctx *PageCtx) error {
|
|||||||
|
|
||||||
// Find Wi-Fi device
|
// Find Wi-Fi device
|
||||||
if err := p.networkManager.FindDevices(); err != nil {
|
if err := p.networkManager.FindDevices(); err != nil {
|
||||||
|
// Clean up before returning error
|
||||||
|
p.networkManager.Close()
|
||||||
|
p.networkManager = nil
|
||||||
|
return fmt.Errorf("failed to find network devices: %w. Try rebooting the computer. If this is your 3rd reboot, call for help", err)
|
||||||
}
|
}
|
||||||
for _, dev := range p.networkManager.Devices {
|
|
||||||
if dev.Type == network.TypeWifi {
|
// FindDevices() rebuilds m.Devices each call, so store a VALUE copy,
|
||||||
d := dev
|
// not a pointer into the slice — it would be invalidated on next call.
|
||||||
p.wifiDevice = &d
|
for i := range p.networkManager.Devices {
|
||||||
|
if p.networkManager.Devices[i].Type == network.TypeWifi {
|
||||||
|
device := p.networkManager.Devices[i]
|
||||||
|
p.wifiDevice = &device
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if p.wifiDevice == nil {
|
||||||
|
p.networkManager.Close()
|
||||||
|
p.networkManager = nil
|
||||||
|
return fmt.Errorf("no Wi-Fi adapter found. Try rebooting the computer. If this is your 3rd reboot, call for help")
|
||||||
|
}
|
||||||
|
|
||||||
// Create UI elements
|
// Create UI elements
|
||||||
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
white := colors.White
|
||||||
p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, white)
|
p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, white)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create title item: %w", err)
|
return fmt.Errorf("failed to create title item: %w", err)
|
||||||
@@ -120,7 +132,6 @@ func (p *WiFiSetupPage) Init(ctx *PageCtx) error {
|
|||||||
|
|
||||||
// Start scanning and status monitoring
|
// Start scanning and status monitoring
|
||||||
p.startScan()
|
p.startScan()
|
||||||
go p.checkConnectionStatusLoop()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -146,6 +157,11 @@ func (p *WiFiSetupPage) checkConnectionStatusLoop() {
|
|||||||
|
|
||||||
// startScan triggers a new asynchronous network scan.
|
// startScan triggers a new asynchronous network scan.
|
||||||
func (p *WiFiSetupPage) startScan() {
|
func (p *WiFiSetupPage) startScan() {
|
||||||
|
if p.stopScan != nil {
|
||||||
|
close(p.stopScan)
|
||||||
|
}
|
||||||
|
p.stopScan = make(chan struct{})
|
||||||
|
|
||||||
p.isScanning = true
|
p.isScanning = true
|
||||||
p.selectedIndex = 0
|
p.selectedIndex = 0
|
||||||
p.scrollOffset = 0
|
p.scrollOffset = 0
|
||||||
@@ -153,6 +169,7 @@ func (p *WiFiSetupPage) startScan() {
|
|||||||
|
|
||||||
if p.wifiDevice != nil {
|
if p.wifiDevice != nil {
|
||||||
go p.networkManager.StartScan(p.resultsChan, p.errorChan, p.stopScan)
|
go p.networkManager.StartScan(p.resultsChan, p.errorChan, p.stopScan)
|
||||||
|
go p.checkConnectionStatusLoop()
|
||||||
} else {
|
} else {
|
||||||
p.isScanning = false
|
p.isScanning = false
|
||||||
}
|
}
|
||||||
@@ -161,18 +178,12 @@ func (p *WiFiSetupPage) startScan() {
|
|||||||
// HandleEvent processes user input.
|
// HandleEvent processes user input.
|
||||||
func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
|
func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
|
||||||
switch e := (*event).(type) {
|
switch e := (*event).(type) {
|
||||||
|
|
||||||
case *sdl.TextInputEvent:
|
case *sdl.TextInputEvent:
|
||||||
if p.isEnteringPassword {
|
if p.isEnteringPassword {
|
||||||
// If we get a TextInputEvent, the system supports high-level input.
|
p.passwordInput += e.GetText() // Always append text
|
||||||
// Mark it supported so we don't double-type from KeyboardEvents.
|
p.textInputSupported = true // Mark as supported
|
||||||
if !p.textInputSupported {
|
|
||||||
p.textInputSupported = true
|
|
||||||
} else {
|
|
||||||
p.passwordInput += e.GetText()
|
|
||||||
p.updatePasswordTexture()
|
p.updatePasswordTexture()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case *sdl.KeyboardEvent:
|
case *sdl.KeyboardEvent:
|
||||||
if e.Type != sdl.KEYDOWN {
|
if e.Type != sdl.KEYDOWN {
|
||||||
@@ -180,13 +191,11 @@ func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.isEnteringPassword {
|
if p.isEnteringPassword {
|
||||||
// Pass the Modifiers (Shift/Caps) to the handler
|
|
||||||
p.handlePasswordInput(e.Keysym.Sym, e.Keysym.Mod)
|
p.handlePasswordInput(e.Keysym.Sym, e.Keysym.Mod)
|
||||||
} else {
|
} else {
|
||||||
p.handleNetworkSelection(e.Keysym.Sym)
|
p.handleNetworkSelection(e.Keysym.Sym)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +332,7 @@ func (p *WiFiSetupPage) handleNetworkSelection(sym sdl.Keycode) {
|
|||||||
case sdl.K_RETURN:
|
case sdl.K_RETURN:
|
||||||
if len(p.accessPoints) > 0 && p.selectedIndex < len(p.accessPoints) {
|
if len(p.accessPoints) > 0 && p.selectedIndex < len(p.accessPoints) {
|
||||||
p.selectedAP = &p.accessPoints[p.selectedIndex].AP
|
p.selectedAP = &p.accessPoints[p.selectedIndex].AP
|
||||||
p.pendingSSID = p.selectedAP.SSID
|
p.pendingSSID = p.selectedAP.SSID // Set for ALL networks (protected or not)
|
||||||
p.connectionError = ""
|
p.connectionError = ""
|
||||||
if p.selectedAP.IsProtected {
|
if p.selectedAP.IsProtected {
|
||||||
p.isEnteringPassword = true
|
p.isEnteringPassword = true
|
||||||
@@ -338,7 +347,7 @@ func (p *WiFiSetupPage) handleNetworkSelection(sym sdl.Keycode) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
p.isConnecting = false
|
p.isConnecting = false
|
||||||
p.pendingSSID = ""
|
p.pendingSSID = ""
|
||||||
p.connectionError = "Failed to initiate connection"
|
p.connectionError = fmt.Sprintf("Failed to connect: %s", err.Error()) // Include actual error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,18 +393,17 @@ func (p *WiFiSetupPage) Update(dt float32) error {
|
|||||||
|
|
||||||
// handleConnectionStatusUpdate CHECKS0 if connection succeeded or failed
|
// handleConnectionStatusUpdate CHECKS0 if connection succeeded or failed
|
||||||
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
|
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
|
||||||
if !p.isConnecting {
|
if !p.isConnecting || p.pendingSSID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// milestone 1: We are associated with an AP (even if internet is still pending)
|
// Only consider it successful if we're connected to the EXACT network we tried to join
|
||||||
isConnected := status.StatusCode == network.StatusWifiConnectedInternetUp ||
|
isConnectedToPending := status.StatusCode == network.StatusWifiConnectedInternetUp ||
|
||||||
status.StatusCode == network.StatusWifiConnectedNoInternet
|
status.StatusCode == network.StatusWifiConnectedNoInternet
|
||||||
|
|
||||||
if isConnected {
|
if isConnectedToPending {
|
||||||
activeSSID := p.getActiveSSID()
|
activeSSID := p.getActiveSSID()
|
||||||
// If we see our SSID, OR if we're connected to anything while trying, call it a win.
|
if activeSSID == p.pendingSSID { // ONLY accept if it's the network we tried
|
||||||
if activeSSID == p.pendingSSID || activeSSID != "" {
|
|
||||||
logger.Info("UI synced: Connected to %s", activeSSID)
|
logger.Info("UI synced: Connected to %s", activeSSID)
|
||||||
p.isConnecting = false
|
p.isConnecting = false
|
||||||
p.pendingSSID = ""
|
p.pendingSSID = ""
|
||||||
@@ -405,14 +413,12 @@ func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// milestone 2: Handle the "Not Connected" state during the handshake
|
// Handle failure: If we've been trying for too long, show error
|
||||||
if status.StatusCode == network.StatusWifiAvailableNotConnected {
|
if status.StatusCode == network.StatusWifiAvailableNotConnected {
|
||||||
// INCREASE THIS: 5 seconds is way too aggressive.
|
|
||||||
// Give it at least 20 seconds before assuming the password was wrong.
|
|
||||||
if time.Since(p.connectionAttemptTime) > 20*time.Second {
|
if time.Since(p.connectionAttemptTime) > 20*time.Second {
|
||||||
if time.Since(p.connectionAttemptTime) > 15*time.Second {
|
|
||||||
p.isConnecting = false
|
p.isConnecting = false
|
||||||
p.connectionError = "Connection failed - Check password or signal"
|
p.connectionError = "Connection failed - Check password or signal"
|
||||||
|
if p.selectedAP != nil && p.selectedAP.IsProtected {
|
||||||
p.isEnteringPassword = true
|
p.isEnteringPassword = true
|
||||||
p.enterPasswordMode()
|
p.enterPasswordMode()
|
||||||
p.updatePasswordErrorTexture()
|
p.updatePasswordErrorTexture()
|
||||||
@@ -553,6 +559,13 @@ func (p *WiFiSetupPage) updateConnectionStatusItem() {
|
|||||||
|
|
||||||
// buildNetworkUIList creates the renderable textures for the list of discovered networks.
|
// buildNetworkUIList creates the renderable textures for the list of discovered networks.
|
||||||
func (p *WiFiSetupPage) buildNetworkUIList(aps []network.AccessPoint) {
|
func (p *WiFiSetupPage) buildNetworkUIList(aps []network.AccessPoint) {
|
||||||
|
// Delete old textures to prevent memory leaks
|
||||||
|
for _, apItem := range p.accessPoints {
|
||||||
|
if apItem.Item.ID != 0 {
|
||||||
|
gl.DeleteTextures(1, &apItem.Item.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the currently active Wi-Fi SSID
|
// Get the currently active Wi-Fi SSID
|
||||||
activeSSID := p.getActiveSSID()
|
activeSSID := p.getActiveSSID()
|
||||||
p.activeSSID = activeSSID
|
p.activeSSID = activeSSID
|
||||||
|
|||||||
Reference in New Issue
Block a user