mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
feat: add native macOS support (v0.4.5)
- CoreWLAN WiFi diagnostics (SSID, RSSI, channel, noise, PHY mode) - WiFi site survey for ESP32 node placement - macOS system info + USB serial driver detection (CP210x, CH340, FTDI) - Permissions checker (network, USB, WiFi scan, location) - Entitlements for network, USB, serial, Bonjour access - Overlay titlebar with native traffic lights - DMG installer with Applications shortcut - Universal binary (lipo ARM64+x64) in CI - Code signing + notarization workflow (when secrets configured) - macOS diagnostics page in UI (conditional, only on macOS) - Install script: scripts/install-macos.sh - Cargo cache in CI, separate runners per arch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3733e54aef
commit
dee2d53f32
14 changed files with 1126 additions and 84 deletions
241
.github/workflows/desktop-release.yml
vendored
241
.github/workflows/desktop-release.yml
vendored
|
|
@ -7,9 +7,9 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., 0.4.0)'
|
||||
description: 'Version to release (e.g., 0.4.5)'
|
||||
required: true
|
||||
default: '0.4.0'
|
||||
default: '0.4.5'
|
||||
attach_to_existing:
|
||||
description: 'Attach to existing release tag (leave empty to create new)'
|
||||
required: false
|
||||
|
|
@ -20,11 +20,17 @@ env:
|
|||
|
||||
jobs:
|
||||
build-macos:
|
||||
name: Build macOS
|
||||
runs-on: macos-latest
|
||||
name: Build macOS (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||
include:
|
||||
- target: aarch64-apple-darwin
|
||||
arch: arm64
|
||||
runner: macos-14
|
||||
- target: x86_64-apple-darwin
|
||||
arch: x64
|
||||
runner: macos-13
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -39,6 +45,19 @@ jobs:
|
|||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
rust-port/wifi-densepose-rs/target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
|
@ -48,7 +67,7 @@ jobs:
|
|||
run: npm run build
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --version "^2.0.0"
|
||||
run: cargo install tauri-cli --version "^2.0.0" --locked
|
||||
|
||||
- name: Build Tauri app
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop
|
||||
|
|
@ -57,25 +76,138 @@ jobs:
|
|||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
- name: Get architecture name
|
||||
id: arch
|
||||
- name: Import signing certificate
|
||||
if: env.APPLE_CERTIFICATE != ''
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then
|
||||
echo "arch=arm64" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "arch=x64" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
||||
security create-keychain -p "" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "" build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
|
||||
rm certificate.p12
|
||||
|
||||
- name: Package macOS app
|
||||
- name: Sign .app bundle
|
||||
if: env.APPLE_CERTIFICATE != ''
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
cd rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos
|
||||
zip -r "RuView-Desktop-${{ github.event.inputs.version || '0.4.0' }}-macos-${{ steps.arch.outputs.arch }}.zip" "RuView Desktop.app"
|
||||
APP_PATH="rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/RuView Desktop.app"
|
||||
codesign --deep --force --options runtime \
|
||||
--entitlements rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/entitlements.plist \
|
||||
--sign "$APPLE_SIGNING_IDENTITY" "$APP_PATH"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
- name: Notarize app
|
||||
if: env.APPLE_ID != ''
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
APP_PATH="rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/RuView Desktop.app"
|
||||
ZIP_PATH="/tmp/RuView-notarize.zip"
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
xcrun notarytool submit "$ZIP_PATH" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APPLE_ID_PASSWORD" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--wait
|
||||
xcrun stapler staple "$APP_PATH"
|
||||
|
||||
- name: Package macOS DMG
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version || '0.4.5' }}"
|
||||
ARCH="${{ matrix.arch }}"
|
||||
APP_PATH="rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos"
|
||||
DMG_NAME="RuView-Desktop-${VERSION}-macos-${ARCH}.dmg"
|
||||
|
||||
# Create DMG with Applications symlink
|
||||
mkdir -p /tmp/dmg-staging
|
||||
cp -r "${APP_PATH}/RuView Desktop.app" /tmp/dmg-staging/
|
||||
ln -s /Applications /tmp/dmg-staging/Applications
|
||||
|
||||
hdiutil create -volname "RuView Desktop" \
|
||||
-srcfolder /tmp/dmg-staging \
|
||||
-ov -format UDZO \
|
||||
"${APP_PATH}/${DMG_NAME}"
|
||||
|
||||
rm -rf /tmp/dmg-staging
|
||||
|
||||
- name: Package macOS ZIP
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version || '0.4.5' }}"
|
||||
ARCH="${{ matrix.arch }}"
|
||||
cd "rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos"
|
||||
zip -r "RuView-Desktop-${VERSION}-macos-${ARCH}.zip" "RuView Desktop.app"
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-macos-${{ steps.arch.outputs.arch }}
|
||||
path: rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/*.zip
|
||||
name: ruview-macos-${{ matrix.arch }}
|
||||
path: |
|
||||
rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/*.zip
|
||||
rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/*.dmg
|
||||
|
||||
build-macos-universal:
|
||||
name: Build macOS Universal
|
||||
needs: [build-macos]
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download ARM64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ruview-macos-arm64
|
||||
path: artifacts/arm64
|
||||
|
||||
- name: Download x64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ruview-macos-x64
|
||||
path: artifacts/x64
|
||||
|
||||
- name: Create universal DMG
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version || '0.4.5' }}"
|
||||
|
||||
# Extract both apps
|
||||
mkdir -p /tmp/arm64 /tmp/x64
|
||||
cd artifacts/arm64 && unzip -q "RuView-Desktop-${VERSION}-macos-arm64.zip" -d /tmp/arm64 && cd ../..
|
||||
cd artifacts/x64 && unzip -q "RuView-Desktop-${VERSION}-macos-x64.zip" -d /tmp/x64 && cd ../..
|
||||
|
||||
# Use lipo to create universal binary from the main executable
|
||||
ARM_BIN="/tmp/arm64/RuView Desktop.app/Contents/MacOS/RuView Desktop"
|
||||
X64_BIN="/tmp/x64/RuView Desktop.app/Contents/MacOS/RuView Desktop"
|
||||
|
||||
if [ -f "$ARM_BIN" ] && [ -f "$X64_BIN" ]; then
|
||||
mkdir -p /tmp/universal
|
||||
cp -r "/tmp/arm64/RuView Desktop.app" "/tmp/universal/"
|
||||
lipo -create "$ARM_BIN" "$X64_BIN" -output "/tmp/universal/RuView Desktop.app/Contents/MacOS/RuView Desktop"
|
||||
|
||||
# Create universal DMG
|
||||
mkdir -p /tmp/dmg-universal
|
||||
cp -r "/tmp/universal/RuView Desktop.app" /tmp/dmg-universal/
|
||||
ln -s /Applications /tmp/dmg-universal/Applications
|
||||
|
||||
hdiutil create -volname "RuView Desktop" \
|
||||
-srcfolder /tmp/dmg-universal \
|
||||
-ov -format UDZO \
|
||||
"artifacts/RuView-Desktop-${VERSION}-macos-universal.dmg"
|
||||
|
||||
rm -rf /tmp/dmg-universal /tmp/universal
|
||||
fi
|
||||
|
||||
- name: Upload universal artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-macos-universal
|
||||
path: artifacts/*.dmg
|
||||
|
||||
build-windows:
|
||||
name: Build Windows
|
||||
|
|
@ -92,6 +224,19 @@ jobs:
|
|||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
rust-port/wifi-densepose-rs/target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
|
@ -101,7 +246,7 @@ jobs:
|
|||
run: npm run build
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --version "^2.0.0"
|
||||
run: cargo install tauri-cli --version "^2.0.0" --locked
|
||||
|
||||
- name: Build Tauri app
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop
|
||||
|
|
@ -124,7 +269,7 @@ jobs:
|
|||
|
||||
create-release:
|
||||
name: Create Release
|
||||
needs: [build-macos, build-windows]
|
||||
needs: [build-macos, build-macos-universal, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -143,38 +288,60 @@ jobs:
|
|||
- name: Create or Update Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: RuView Desktop v${{ github.event.inputs.version || '0.4.0' }}
|
||||
tag_name: ${{ github.event.inputs.attach_to_existing || format('desktop-v{0}', github.event.inputs.version || '0.4.0') }}
|
||||
name: RuView Desktop v${{ github.event.inputs.version || '0.4.5' }}
|
||||
tag_name: ${{ github.event.inputs.attach_to_existing || format('desktop-v{0}', github.event.inputs.version || '0.4.5') }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: ${{ github.event.inputs.attach_to_existing == '' }}
|
||||
files: |
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.msi
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
body: |
|
||||
## RuView Desktop v${{ github.event.inputs.version || '0.4.0' }}
|
||||
## RuView Desktop v${{ github.event.inputs.version || '0.4.5' }}
|
||||
|
||||
WiFi-based human pose estimation desktop application.
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | Architecture | Download |
|
||||
|----------|--------------|----------|
|
||||
| macOS | Apple Silicon (M1/M2/M3) | `RuView-Desktop-*-macos-arm64.zip` |
|
||||
| macOS | Intel | `RuView-Desktop-*-macos-x64.zip` |
|
||||
| Windows | x64 | `RuView-Desktop-*.msi` or `RuView-Desktop-*.exe` |
|
||||
| Platform | Architecture | Format | Download |
|
||||
|----------|--------------|--------|----------|
|
||||
| macOS | Apple Silicon (M1/M2/M3/M4) | DMG | `RuView-Desktop-*-macos-arm64.dmg` |
|
||||
| macOS | Intel | DMG | `RuView-Desktop-*-macos-x64.dmg` |
|
||||
| macOS | Universal | DMG | `RuView-Desktop-*-macos-universal.dmg` |
|
||||
| macOS | Apple Silicon | ZIP | `RuView-Desktop-*-macos-arm64.zip` |
|
||||
| macOS | Intel | ZIP | `RuView-Desktop-*-macos-x64.zip` |
|
||||
| Windows | x64 | MSI | `RuView-Desktop-*.msi` |
|
||||
| Windows | x64 | EXE | `RuView-Desktop-*.exe` |
|
||||
|
||||
### Installation
|
||||
### macOS Installation
|
||||
|
||||
**macOS:**
|
||||
1. Download the appropriate `.zip` file for your Mac
|
||||
2. Extract the zip file
|
||||
3. Move `RuView Desktop.app` to your Applications folder
|
||||
4. Right-click and select "Open" (first time only, to bypass Gatekeeper)
|
||||
**DMG (recommended):**
|
||||
1. Download the `.dmg` for your Mac (Apple Silicon = M1+, Intel = older Macs, Universal = both)
|
||||
2. Open the DMG and drag `RuView Desktop.app` to Applications
|
||||
3. First launch: right-click > Open (bypasses Gatekeeper)
|
||||
|
||||
**Windows:**
|
||||
**Homebrew:**
|
||||
```bash
|
||||
brew install --cask ruview-desktop
|
||||
```
|
||||
|
||||
**ZIP:**
|
||||
1. Download and extract the `.zip`
|
||||
2. Move `RuView Desktop.app` to Applications
|
||||
|
||||
### macOS Features (v0.4.5)
|
||||
- Native WiFi diagnostics via CoreWLAN (SSID, RSSI, channel, noise)
|
||||
- WiFi site survey for ESP32 node placement
|
||||
- USB serial driver detection (CP210x, CH340, FTDI)
|
||||
- macOS permissions checker
|
||||
- Overlay titlebar with native traffic lights
|
||||
- DMG installer with Applications shortcut
|
||||
- Code signed and notarized (when secrets configured)
|
||||
- Universal binary (runs native on both Intel and Apple Silicon)
|
||||
|
||||
### Windows Installation
|
||||
1. Download the `.msi` installer
|
||||
2. Run the installer
|
||||
3. Launch RuView Desktop from the Start menu
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Allow outgoing network connections (mDNS discovery, OTA, HTTP API) -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<!-- Allow incoming network connections (UDP beacon listener) -->
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<!-- USB device access for ESP32 serial flashing/provisioning -->
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<!-- Serial port access for ESP32 communication -->
|
||||
<key>com.apple.security.device.serial</key>
|
||||
<true/>
|
||||
<!-- Read/write access to user-selected files (firmware binaries) -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<!-- Bonjour/mDNS service browsing -->
|
||||
<key>com.apple.security.network.bonjour</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
|
||||
/// macOS WiFi network info from CoreWLAN via system_profiler.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MacWifiInfo {
|
||||
pub ssid: Option<String>,
|
||||
pub bssid: Option<String>,
|
||||
pub channel: Option<u32>,
|
||||
pub rssi: Option<i32>,
|
||||
pub noise: Option<i32>,
|
||||
pub tx_rate: Option<f64>,
|
||||
pub security: Option<String>,
|
||||
pub phy_mode: Option<String>,
|
||||
}
|
||||
|
||||
/// macOS system info relevant for RuView diagnostics.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MacSystemInfo {
|
||||
pub os_version: String,
|
||||
pub arch: String,
|
||||
pub model: Option<String>,
|
||||
pub wifi_interface: Option<String>,
|
||||
pub wifi_power: bool,
|
||||
pub serial_drivers: Vec<String>,
|
||||
}
|
||||
|
||||
/// Permission check result for macOS-specific capabilities.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MacPermissions {
|
||||
pub network_access: bool,
|
||||
pub usb_access: bool,
|
||||
pub wifi_scan: bool,
|
||||
pub location_services: bool,
|
||||
}
|
||||
|
||||
/// Get current WiFi connection info using CoreWLAN via airport CLI.
|
||||
/// macOS-only: uses `/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I`
|
||||
#[tauri::command]
|
||||
pub async fn macos_wifi_info() -> Result<MacWifiInfo, String> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("macOS-only command".into());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let output = Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport")
|
||||
.arg("-I")
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to query WiFi: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err("airport command failed".into());
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let mut info = MacWifiInfo {
|
||||
ssid: None,
|
||||
bssid: None,
|
||||
channel: None,
|
||||
rssi: None,
|
||||
noise: None,
|
||||
tx_rate: None,
|
||||
security: None,
|
||||
phy_mode: None,
|
||||
};
|
||||
|
||||
for line in text.lines() {
|
||||
let parts: Vec<&str> = line.splitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let key = parts[0].trim();
|
||||
let val = parts[1].trim();
|
||||
|
||||
match key {
|
||||
"SSID" => info.ssid = Some(val.to_string()),
|
||||
"BSSID" => info.bssid = Some(val.to_string()),
|
||||
"channel" => info.channel = val.split(',').next().and_then(|v| v.trim().parse().ok()),
|
||||
"agrCtlRSSI" => info.rssi = val.parse().ok(),
|
||||
"agrCtlNoise" => info.noise = val.parse().ok(),
|
||||
"lastTxRate" => info.tx_rate = val.parse().ok(),
|
||||
"link auth" => info.security = Some(val.to_string()),
|
||||
"PHY Mode" | "phyMode" => info.phy_mode = Some(val.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan nearby WiFi networks (macOS only).
|
||||
/// Returns list of visible networks with RSSI for site survey.
|
||||
#[tauri::command]
|
||||
pub async fn macos_wifi_scan() -> Result<Vec<MacWifiScanResult>, String> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("macOS-only command".into());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let output = Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport")
|
||||
.arg("-s")
|
||||
.output()
|
||||
.map_err(|e| format!("WiFi scan failed: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err("WiFi scan command failed. Location Services may need to be enabled.".into());
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let mut results = Vec::new();
|
||||
|
||||
for line in text.lines().skip(1) {
|
||||
// airport -s format: SSID BSSID RSSI CHANNEL HT CC SECURITY
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse fixed-width columns (BSSID is at a fixed position)
|
||||
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
||||
if parts.len() < 7 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find BSSID (xx:xx:xx:xx:xx:xx pattern) to anchor parsing
|
||||
let bssid_idx = parts.iter().position(|p| p.matches(':').count() == 5);
|
||||
if let Some(idx) = bssid_idx {
|
||||
let ssid = if idx > 0 {
|
||||
parts[..idx].join(" ")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
results.push(MacWifiScanResult {
|
||||
ssid,
|
||||
bssid: parts[idx].to_string(),
|
||||
rssi: parts.get(idx + 1).and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||
channel: parts.get(idx + 2).and_then(|v| v.split(',').next().and_then(|c| c.parse().ok())).unwrap_or(0),
|
||||
security: parts.get(idx + 5..).map(|s| s.join(" ")).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results.sort_by(|a, b| b.rssi.cmp(&a.rssi));
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
/// WiFi scan result entry.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MacWifiScanResult {
|
||||
pub ssid: String,
|
||||
pub bssid: String,
|
||||
pub rssi: i32,
|
||||
pub channel: u32,
|
||||
pub security: String,
|
||||
}
|
||||
|
||||
/// Get macOS system info relevant to RuView operation.
|
||||
#[tauri::command]
|
||||
pub async fn macos_system_info() -> Result<MacSystemInfo, String> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("macOS-only command".into());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// OS version
|
||||
let os_version = Command::new("sw_vers")
|
||||
.arg("-productVersion")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_else(|_| "unknown".into());
|
||||
|
||||
// Architecture
|
||||
let arch = Command::new("uname")
|
||||
.arg("-m")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_else(|_| "unknown".into());
|
||||
|
||||
// Model identifier
|
||||
let model = Command::new("sysctl")
|
||||
.args(["-n", "hw.model"])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
|
||||
|
||||
// WiFi interface
|
||||
let wifi_interface = Command::new("networksetup")
|
||||
.args(["-listallhardwareports"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
let text = String::from_utf8_lossy(&o.stdout).to_string();
|
||||
let mut found_wifi = false;
|
||||
for line in text.lines() {
|
||||
if line.contains("Wi-Fi") {
|
||||
found_wifi = true;
|
||||
continue;
|
||||
}
|
||||
if found_wifi && line.starts_with("Device:") {
|
||||
return Some(line.replace("Device:", "").trim().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
// WiFi power status
|
||||
let wifi_power = wifi_interface.as_ref()
|
||||
.and_then(|iface| {
|
||||
Command::new("networksetup")
|
||||
.args(["-getairportpower", iface])
|
||||
.output()
|
||||
.ok()
|
||||
})
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).contains("On"))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Check for USB serial drivers (kext)
|
||||
let serial_drivers = detect_serial_drivers();
|
||||
|
||||
Ok(MacSystemInfo {
|
||||
os_version,
|
||||
arch,
|
||||
model,
|
||||
wifi_interface,
|
||||
wifi_power,
|
||||
serial_drivers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect installed USB serial drivers on macOS.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect_serial_drivers() -> Vec<String> {
|
||||
let drivers_to_check = [
|
||||
("CH34x", "com.wch.usbserial.CH34x"),
|
||||
("CP210x", "com.silabs.driver.CP210xVCPDriver"),
|
||||
("FTDI", "com.FTDI.driver.FTDIUSBSerialDriver"),
|
||||
("CH9102", "com.wch.usbserial.CH9102"),
|
||||
];
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
for (name, bundle_id) in &drivers_to_check {
|
||||
if let Ok(output) = Command::new("kextstat").output() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if text.contains(bundle_id) {
|
||||
found.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check /dev for any connected USB serial devices
|
||||
if let Ok(entries) = std::fs::read_dir("/dev") {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("cu.usb") || name.starts_with("cu.wch") {
|
||||
found.push(format!("/dev/{}", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
/// Check macOS permissions relevant to RuView.
|
||||
#[tauri::command]
|
||||
pub async fn macos_check_permissions() -> Result<MacPermissions, String> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("macOS-only command".into());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Network access: try binding a socket
|
||||
let network_access = std::net::UdpSocket::bind("0.0.0.0:0").is_ok();
|
||||
|
||||
// USB access: check /dev/cu.usb* exists
|
||||
let usb_access = std::fs::read_dir("/dev")
|
||||
.map(|entries| {
|
||||
entries.flatten().any(|e| {
|
||||
e.file_name().to_string_lossy().starts_with("cu.usb")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
// WiFi scan: try running airport
|
||||
let wifi_scan = Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport")
|
||||
.arg("-I")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
// Location services (needed for WiFi scanning)
|
||||
let location_services = Command::new("defaults")
|
||||
.args(["read", "/var/db/locationd/clients.plist"])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(true); // Assume enabled if can't check
|
||||
|
||||
Ok(MacPermissions {
|
||||
network_access,
|
||||
usb_access,
|
||||
wifi_scan,
|
||||
location_services,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_wifi_info_struct() {
|
||||
let info = MacWifiInfo {
|
||||
ssid: Some("TestNet".into()),
|
||||
bssid: Some("AA:BB:CC:DD:EE:FF".into()),
|
||||
channel: Some(6),
|
||||
rssi: Some(-45),
|
||||
noise: Some(-90),
|
||||
tx_rate: Some(866.0),
|
||||
security: Some("wpa2-psk".into()),
|
||||
phy_mode: Some("802.11ac".into()),
|
||||
};
|
||||
assert_eq!(info.ssid, Some("TestNet".into()));
|
||||
assert_eq!(info.channel, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_info_struct() {
|
||||
let info = MacSystemInfo {
|
||||
os_version: "14.0".into(),
|
||||
arch: "arm64".into(),
|
||||
model: Some("Mac14,2".into()),
|
||||
wifi_interface: Some("en0".into()),
|
||||
wifi_power: true,
|
||||
serial_drivers: vec!["CH34x".into()],
|
||||
};
|
||||
assert_eq!(info.arch, "arm64");
|
||||
assert!(info.wifi_power);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_result_struct() {
|
||||
let result = MacWifiScanResult {
|
||||
ssid: "MyNetwork".into(),
|
||||
bssid: "00:11:22:33:44:55".into(),
|
||||
rssi: -52,
|
||||
channel: 36,
|
||||
security: "WPA2".into(),
|
||||
};
|
||||
assert_eq!(result.rssi, -52);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod discovery;
|
||||
pub mod flash;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
pub mod ota;
|
||||
pub mod provision;
|
||||
pub mod server;
|
||||
|
|
|
|||
|
|
@ -3,50 +3,102 @@ pub mod domain;
|
|||
pub mod state;
|
||||
|
||||
use commands::{discovery, flash, ota, provision, server, settings, wasm};
|
||||
#[cfg(target_os = "macos")]
|
||||
use commands::macos;
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(state::AppState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Discovery
|
||||
discovery::discover_nodes,
|
||||
discovery::list_serial_ports,
|
||||
discovery::configure_esp32_wifi,
|
||||
// Flash
|
||||
flash::flash_firmware,
|
||||
flash::flash_progress,
|
||||
flash::verify_firmware,
|
||||
flash::check_espflash,
|
||||
flash::supported_chips,
|
||||
// OTA
|
||||
ota::ota_update,
|
||||
ota::batch_ota_update,
|
||||
ota::check_ota_endpoint,
|
||||
// WASM
|
||||
wasm::wasm_list,
|
||||
wasm::wasm_upload,
|
||||
wasm::wasm_control,
|
||||
wasm::wasm_info,
|
||||
wasm::wasm_stats,
|
||||
wasm::check_wasm_support,
|
||||
// Server
|
||||
server::start_server,
|
||||
server::stop_server,
|
||||
server::server_status,
|
||||
server::restart_server,
|
||||
server::server_logs,
|
||||
// Provision
|
||||
provision::provision_node,
|
||||
provision::read_nvs,
|
||||
provision::erase_nvs,
|
||||
provision::validate_config,
|
||||
provision::generate_mesh_configs,
|
||||
// Settings
|
||||
settings::get_settings,
|
||||
settings::save_settings,
|
||||
])
|
||||
.manage(state::AppState::default());
|
||||
|
||||
// Register all commands including macOS-specific ones
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.invoke_handler(tauri::generate_handler![
|
||||
// Discovery
|
||||
discovery::discover_nodes,
|
||||
discovery::list_serial_ports,
|
||||
discovery::configure_esp32_wifi,
|
||||
// Flash
|
||||
flash::flash_firmware,
|
||||
flash::flash_progress,
|
||||
flash::verify_firmware,
|
||||
flash::check_espflash,
|
||||
flash::supported_chips,
|
||||
// OTA
|
||||
ota::ota_update,
|
||||
ota::batch_ota_update,
|
||||
ota::check_ota_endpoint,
|
||||
// WASM
|
||||
wasm::wasm_list,
|
||||
wasm::wasm_upload,
|
||||
wasm::wasm_control,
|
||||
wasm::wasm_info,
|
||||
wasm::wasm_stats,
|
||||
wasm::check_wasm_support,
|
||||
// Server
|
||||
server::start_server,
|
||||
server::stop_server,
|
||||
server::server_status,
|
||||
server::restart_server,
|
||||
server::server_logs,
|
||||
// Provision
|
||||
provision::provision_node,
|
||||
provision::read_nvs,
|
||||
provision::erase_nvs,
|
||||
provision::validate_config,
|
||||
provision::generate_mesh_configs,
|
||||
// Settings
|
||||
settings::get_settings,
|
||||
settings::save_settings,
|
||||
// macOS
|
||||
macos::macos_wifi_info,
|
||||
macos::macos_wifi_scan,
|
||||
macos::macos_system_info,
|
||||
macos::macos_check_permissions,
|
||||
]);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let builder = builder.invoke_handler(tauri::generate_handler![
|
||||
// Discovery
|
||||
discovery::discover_nodes,
|
||||
discovery::list_serial_ports,
|
||||
discovery::configure_esp32_wifi,
|
||||
// Flash
|
||||
flash::flash_firmware,
|
||||
flash::flash_progress,
|
||||
flash::verify_firmware,
|
||||
flash::check_espflash,
|
||||
flash::supported_chips,
|
||||
// OTA
|
||||
ota::ota_update,
|
||||
ota::batch_ota_update,
|
||||
ota::check_ota_endpoint,
|
||||
// WASM
|
||||
wasm::wasm_list,
|
||||
wasm::wasm_upload,
|
||||
wasm::wasm_control,
|
||||
wasm::wasm_info,
|
||||
wasm::wasm_stats,
|
||||
wasm::check_wasm_support,
|
||||
// Server
|
||||
server::start_server,
|
||||
server::stop_server,
|
||||
server::server_status,
|
||||
server::restart_server,
|
||||
server::server_logs,
|
||||
// Provision
|
||||
provision::provision_node,
|
||||
provision::read_nvs,
|
||||
provision::erase_nvs,
|
||||
provision::validate_config,
|
||||
provision::generate_mesh_configs,
|
||||
// Settings
|
||||
settings::get_settings,
|
||||
settings::save_settings,
|
||||
]);
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "macos"),
|
||||
allow(unused_imports)
|
||||
)]
|
||||
|
||||
fn main() {
|
||||
wifi_densepose_desktop::run();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "RuView Desktop",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"identifier": "net.ruv.ruview",
|
||||
"build": {
|
||||
"frontendDist": "ui/dist",
|
||||
|
|
@ -17,19 +17,45 @@
|
|||
"height": 800,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600,
|
||||
"resizable": true
|
||||
"resizable": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"decorations": true,
|
||||
"transparent": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": ["dmg", "app", "updater"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "WiFi-based human pose estimation and vital sign monitoring",
|
||||
"longDescription": "RuView turns commodity WiFi signals into real-time human pose estimation, vital sign monitoring, and presence detection without cameras or wearables.",
|
||||
"copyright": "Copyright (c) 2024-2026 rUv",
|
||||
"macOS": {
|
||||
"entitlements": "entitlements.plist",
|
||||
"minimumSystemVersion": "11.0",
|
||||
"frameworks": [],
|
||||
"dmg": {
|
||||
"appPosition": { "x": 180, "y": 170 },
|
||||
"applicationFolderPosition": { "x": 480, "y": 170 },
|
||||
"windowSize": { "width": 660, "height": 400 }
|
||||
}
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { EdgeModules } from "./pages/EdgeModules";
|
|||
import { Sensing } from "./pages/Sensing";
|
||||
import { MeshView } from "./pages/MeshView";
|
||||
import { Settings } from "./pages/Settings";
|
||||
import { MacOSDiagnostics } from "./pages/MacOSDiagnostics";
|
||||
|
||||
const isMacOS = navigator.userAgent.includes("Mac");
|
||||
|
||||
type Page =
|
||||
| "dashboard"
|
||||
|
|
@ -19,7 +22,8 @@ type Page =
|
|||
| "wasm"
|
||||
| "sensing"
|
||||
| "mesh"
|
||||
| "settings";
|
||||
| "settings"
|
||||
| "macos";
|
||||
|
||||
interface NavItem {
|
||||
id: Page;
|
||||
|
|
@ -37,6 +41,7 @@ const NAV_ITEMS: NavItem[] = [
|
|||
{ id: "sensing", label: "Sensing", icon: "\u2248" },
|
||||
{ id: "mesh", label: "Mesh View", icon: "\u2B2F" },
|
||||
{ id: "settings", label: "Settings", icon: "\u2699" },
|
||||
...(isMacOS ? [{ id: "macos" as Page, label: "macOS", icon: "\uF8FF" }] : []),
|
||||
];
|
||||
|
||||
interface LiveStatus {
|
||||
|
|
@ -100,6 +105,7 @@ const App: React.FC = () => {
|
|||
case "sensing": return <Sensing />;
|
||||
case "mesh": return <MeshView />;
|
||||
case "settings": return <Settings />;
|
||||
case "macos": return <MacOSDiagnostics />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,31 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* macOS titlebar overlay: drag region + traffic light padding */
|
||||
@supports (-webkit-app-region: drag) {
|
||||
.macos-titlebar {
|
||||
-webkit-app-region: drag;
|
||||
height: 28px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
.macos-titlebar button,
|
||||
.macos-titlebar a,
|
||||
.macos-titlebar input {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add top padding on macOS for overlay titlebar */
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
html.macos-app #root > div {
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Typography Scale ===== */
|
||||
.heading-xl { font: 600 28px/1.2 var(--font-sans); color: var(--text-primary); letter-spacing: -0.02em; }
|
||||
.heading-lg { font: 600 20px/1.3 var(--font-sans); color: var(--text-primary); letter-spacing: -0.01em; }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import ReactDOM from "react-dom/client";
|
|||
import "./design-system.css";
|
||||
import App from "./App";
|
||||
|
||||
// Detect macOS for native titlebar integration
|
||||
if (navigator.userAgent.includes("Mac")) {
|
||||
document.documentElement.classList.add("macos-app");
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
interface WifiInfo {
|
||||
ssid: string | null;
|
||||
bssid: string | null;
|
||||
channel: number | null;
|
||||
rssi: number | null;
|
||||
noise: number | null;
|
||||
tx_rate: number | null;
|
||||
security: string | null;
|
||||
phy_mode: string | null;
|
||||
}
|
||||
|
||||
interface WifiScanResult {
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
rssi: number;
|
||||
channel: number;
|
||||
security: string;
|
||||
}
|
||||
|
||||
interface SystemInfo {
|
||||
os_version: string;
|
||||
arch: string;
|
||||
model: string | null;
|
||||
wifi_interface: string | null;
|
||||
wifi_power: boolean;
|
||||
serial_drivers: string[];
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
network_access: boolean;
|
||||
usb_access: boolean;
|
||||
wifi_scan: boolean;
|
||||
location_services: boolean;
|
||||
}
|
||||
|
||||
const isMacOS = navigator.userAgent.includes("Mac");
|
||||
|
||||
export const MacOSDiagnostics: React.FC = () => {
|
||||
const [wifiInfo, setWifiInfo] = useState<WifiInfo | null>(null);
|
||||
const [scanResults, setScanResults] = useState<WifiScanResult[]>([]);
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
||||
const [permissions, setPermissions] = useState<Permissions | null>(null);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMacOS) return;
|
||||
loadSystemInfo();
|
||||
loadWifiInfo();
|
||||
loadPermissions();
|
||||
}, []);
|
||||
|
||||
const loadSystemInfo = async () => {
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
const info = await invoke<SystemInfo>("macos_system_info");
|
||||
setSystemInfo(info);
|
||||
} catch (e) {
|
||||
console.warn("Failed to get system info:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWifiInfo = async () => {
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
const info = await invoke<WifiInfo>("macos_wifi_info");
|
||||
setWifiInfo(info);
|
||||
} catch (e) {
|
||||
console.warn("Failed to get WiFi info:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPermissions = async () => {
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
const perms = await invoke<Permissions>("macos_check_permissions");
|
||||
setPermissions(perms);
|
||||
} catch (e) {
|
||||
console.warn("Failed to check permissions:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const runWifiScan = async () => {
|
||||
setScanning(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
const results = await invoke<WifiScanResult[]>("macos_wifi_scan");
|
||||
setScanResults(results);
|
||||
} catch (e: unknown) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isMacOS) {
|
||||
return (
|
||||
<div style={{ padding: 32 }}>
|
||||
<h2 style={{ color: "var(--text-primary)", margin: 0 }}>macOS Diagnostics</h2>
|
||||
<p style={{ color: "var(--text-muted)", marginTop: 12 }}>
|
||||
This page shows macOS-specific WiFi and system diagnostics. Only available on macOS.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rssiColor = (rssi: number) => {
|
||||
if (rssi >= -50) return "var(--accent-green, #22c55e)";
|
||||
if (rssi >= -70) return "var(--accent, #7c3aed)";
|
||||
return "var(--accent-red, #ef4444)";
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 960 }}>
|
||||
<h2 style={{ color: "var(--text-primary)", margin: "0 0 24px 0", fontFamily: "var(--font-sans)" }}>
|
||||
macOS Diagnostics
|
||||
</h2>
|
||||
|
||||
{/* System Info */}
|
||||
{systemInfo && (
|
||||
<div className="card" style={{ marginBottom: 20, padding: 20 }}>
|
||||
<h3 style={{ color: "var(--text-primary)", margin: "0 0 12px", fontSize: 14 }}>System</h3>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px 24px", fontSize: 13 }}>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>macOS</span> <span style={{ color: "var(--text-primary)" }}>{systemInfo.os_version}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Arch</span> <span style={{ color: "var(--text-primary)" }}>{systemInfo.arch}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Model</span> <span style={{ color: "var(--text-primary)" }}>{systemInfo.model || "N/A"}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>WiFi</span> <span style={{ color: "var(--text-primary)" }}>{systemInfo.wifi_interface || "N/A"} ({systemInfo.wifi_power ? "On" : "Off"})</span></div>
|
||||
{systemInfo.serial_drivers.length > 0 && (
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<span style={{ color: "var(--text-muted)" }}>Serial Drivers</span>{" "}
|
||||
<span style={{ color: "var(--text-primary)", fontFamily: "var(--font-mono)" }}>
|
||||
{systemInfo.serial_drivers.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Permissions */}
|
||||
{permissions && (
|
||||
<div className="card" style={{ marginBottom: 20, padding: 20 }}>
|
||||
<h3 style={{ color: "var(--text-primary)", margin: "0 0 12px", fontSize: 14 }}>Permissions</h3>
|
||||
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
|
||||
{[
|
||||
{ label: "Network", ok: permissions.network_access },
|
||||
{ label: "USB Serial", ok: permissions.usb_access },
|
||||
{ label: "WiFi Scan", ok: permissions.wifi_scan },
|
||||
{ label: "Location", ok: permissions.location_services },
|
||||
].map((p) => (
|
||||
<span
|
||||
key={p.label}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "4px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontFamily: "var(--font-mono)",
|
||||
background: p.ok ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)",
|
||||
color: p.ok ? "#22c55e" : "#ef4444",
|
||||
border: `1px solid ${p.ok ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"}`,
|
||||
}}
|
||||
>
|
||||
{p.ok ? "\u2713" : "\u2717"} {p.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current WiFi */}
|
||||
{wifiInfo && (
|
||||
<div className="card" style={{ marginBottom: 20, padding: 20 }}>
|
||||
<h3 style={{ color: "var(--text-primary)", margin: "0 0 12px", fontSize: 14 }}>Current WiFi Connection</h3>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "8px 24px", fontSize: 13 }}>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>SSID</span> <span style={{ color: "var(--text-primary)", fontWeight: 600 }}>{wifiInfo.ssid || "N/A"}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Channel</span> <span style={{ color: "var(--text-primary)" }}>{wifiInfo.channel || "N/A"}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>RSSI</span> <span style={{ color: rssiColor(wifiInfo.rssi || -100), fontWeight: 600 }}>{wifiInfo.rssi || "N/A"} dBm</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>BSSID</span> <span style={{ color: "var(--text-primary)", fontFamily: "var(--font-mono)", fontSize: 11 }}>{wifiInfo.bssid || "N/A"}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Noise</span> <span style={{ color: "var(--text-primary)" }}>{wifiInfo.noise || "N/A"} dBm</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Tx Rate</span> <span style={{ color: "var(--text-primary)" }}>{wifiInfo.tx_rate || "N/A"} Mbps</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>Security</span> <span style={{ color: "var(--text-primary)" }}>{wifiInfo.security || "N/A"}</span></div>
|
||||
<div><span style={{ color: "var(--text-muted)" }}>PHY</span> <span style={{ color: "var(--text-primary)" }}>{wifiInfo.phy_mode || "N/A"}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* WiFi Scan */}
|
||||
<div className="card" style={{ padding: 20 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<h3 style={{ color: "var(--text-primary)", margin: 0, fontSize: 14 }}>WiFi Site Survey</h3>
|
||||
<button
|
||||
onClick={runWifiScan}
|
||||
disabled={scanning}
|
||||
style={{
|
||||
padding: "6px 14px",
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
background: "linear-gradient(135deg, var(--accent), #a855f7)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
cursor: scanning ? "wait" : "pointer",
|
||||
opacity: scanning ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{scanning ? "Scanning..." : "Scan Networks"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ color: "#ef4444", fontSize: 12, marginBottom: 12, padding: "8px 12px", background: "rgba(239,68,68,0.1)", borderRadius: 6 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scanResults.length > 0 && (
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
{["SSID", "BSSID", "Ch", "RSSI", "Security"].map((h) => (
|
||||
<th key={h} style={{ padding: "6px 8px", textAlign: "left", color: "var(--text-muted)", fontWeight: 500 }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scanResults.map((r, i) => (
|
||||
<tr key={i} style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<td style={{ padding: "6px 8px", color: "var(--text-primary)", fontWeight: 500 }}>{r.ssid || "(hidden)"}</td>
|
||||
<td style={{ padding: "6px 8px", color: "var(--text-muted)", fontFamily: "var(--font-mono)", fontSize: 10 }}>{r.bssid}</td>
|
||||
<td style={{ padding: "6px 8px", color: "var(--text-primary)" }}>{r.channel}</td>
|
||||
<td style={{ padding: "6px 8px", color: rssiColor(r.rssi), fontWeight: 600 }}>{r.rssi} dBm</td>
|
||||
<td style={{ padding: "6px 8px", color: "var(--text-muted)", fontSize: 10 }}>{r.security}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{scanResults.length === 0 && !scanning && (
|
||||
<p style={{ color: "var(--text-muted)", fontSize: 12, margin: 0 }}>
|
||||
Click "Scan Networks" to discover nearby WiFi access points. Useful for positioning ESP32 nodes.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -229,3 +229,42 @@ export interface AppSettings {
|
|||
discover_interval_ms: number;
|
||||
theme: "dark" | "light";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// macOS Diagnostics (only available on macOS)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface MacWifiInfo {
|
||||
ssid: string | null;
|
||||
bssid: string | null;
|
||||
channel: number | null;
|
||||
rssi: number | null;
|
||||
noise: number | null;
|
||||
tx_rate: number | null;
|
||||
security: string | null;
|
||||
phy_mode: string | null;
|
||||
}
|
||||
|
||||
export interface MacWifiScanResult {
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
rssi: number;
|
||||
channel: number;
|
||||
security: string;
|
||||
}
|
||||
|
||||
export interface MacSystemInfo {
|
||||
os_version: string;
|
||||
arch: string;
|
||||
model: string | null;
|
||||
wifi_interface: string | null;
|
||||
wifi_power: boolean;
|
||||
serial_drivers: string[];
|
||||
}
|
||||
|
||||
export interface MacPermissions {
|
||||
network_access: boolean;
|
||||
usb_access: boolean;
|
||||
wifi_scan: boolean;
|
||||
location_services: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// Application version - single source of truth
|
||||
export const APP_VERSION = "0.4.4";
|
||||
export const APP_VERSION = "0.4.5";
|
||||
|
|
|
|||
75
scripts/install-macos.sh
Executable file
75
scripts/install-macos.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
# RuView Desktop macOS installer
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/ruvnet/RuView/main/scripts/install-macos.sh | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${RUVIEW_VERSION:-0.4.5}"
|
||||
REPO="ruvnet/RuView"
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
arm64|aarch64) ARCH_LABEL="arm64" ;;
|
||||
x86_64) ARCH_LABEL="x64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
DMG_NAME="RuView-Desktop-${VERSION}-macos-${ARCH_LABEL}.dmg"
|
||||
ZIP_NAME="RuView-Desktop-${VERSION}-macos-${ARCH_LABEL}.zip"
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/desktop-v${VERSION}"
|
||||
|
||||
echo "RuView Desktop Installer"
|
||||
echo "========================"
|
||||
echo "Version: ${VERSION}"
|
||||
echo "Arch: ${ARCH_LABEL}"
|
||||
echo ""
|
||||
|
||||
# Try DMG first, fall back to ZIP
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
|
||||
echo "Downloading..."
|
||||
if curl -fsSL -o "${TMPDIR}/${DMG_NAME}" "${DOWNLOAD_URL}/${DMG_NAME}" 2>/dev/null; then
|
||||
echo "Installing from DMG..."
|
||||
MOUNT_POINT=$(hdiutil attach "${TMPDIR}/${DMG_NAME}" -nobrowse -noautoopen | tail -1 | awk '{print $NF}')
|
||||
|
||||
# Remove old version
|
||||
if [ -d "/Applications/RuView Desktop.app" ]; then
|
||||
echo "Removing previous installation..."
|
||||
rm -rf "/Applications/RuView Desktop.app"
|
||||
fi
|
||||
|
||||
cp -r "${MOUNT_POINT}/RuView Desktop.app" /Applications/
|
||||
hdiutil detach "$MOUNT_POINT" -quiet
|
||||
|
||||
elif curl -fsSL -o "${TMPDIR}/${ZIP_NAME}" "${DOWNLOAD_URL}/${ZIP_NAME}" 2>/dev/null; then
|
||||
echo "Installing from ZIP..."
|
||||
cd "$TMPDIR"
|
||||
unzip -q "$ZIP_NAME"
|
||||
|
||||
if [ -d "/Applications/RuView Desktop.app" ]; then
|
||||
echo "Removing previous installation..."
|
||||
rm -rf "/Applications/RuView Desktop.app"
|
||||
fi
|
||||
|
||||
cp -r "RuView Desktop.app" /Applications/
|
||||
|
||||
else
|
||||
echo "Failed to download RuView Desktop v${VERSION}"
|
||||
echo "Check https://github.com/${REPO}/releases for available versions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clear Gatekeeper quarantine attribute
|
||||
xattr -cr "/Applications/RuView Desktop.app" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "RuView Desktop v${VERSION} installed to /Applications"
|
||||
echo ""
|
||||
echo "To launch: open '/Applications/RuView Desktop.app'"
|
||||
echo ""
|
||||
echo "ESP32 serial drivers:"
|
||||
echo " CP210x: https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers"
|
||||
echo " CH340: https://github.com/WCHSoftGroup/ch34xser_macos"
|
||||
echo " FTDI: https://ftdichip.com/drivers/vcp-drivers/"
|
||||
Loading…
Add table
Add a link
Reference in a new issue