phone/src/services/ap-monitor.ts
2025-11-20 18:18:47 -08:00

239 lines
7.9 KiB
TypeScript

#!/usr/bin/env bun
import { $ } from "bun"
// Configuration
const CONFIG = {
checkInterval: 10_000, // 10 seconds
ap: {
ip: "192.168.4.1",
dhcpRange: "192.168.4.2,192.168.4.20",
channel: 7,
},
}
// Get AP SSID from hostname
const hostname = (await $`hostname`.text()).trim()
const AP_SSID = `${hostname}-setup`
const AP_CONNECTION_NAME = `${AP_SSID}-ap`
console.log("Starting WiFi AP Monitor...")
console.log(`AP SSID will be: ${AP_SSID}`)
console.log(`AP connection name: ${AP_CONNECTION_NAME}`)
console.log(`Checking connectivity every ${CONFIG.checkInterval / 1000} seconds\n`)
let apRunning = false
async function isConnectedToWiFi(): Promise<boolean> {
try {
// Check if wlan0 is connected to a WiFi network AS A CLIENT (not in AP mode)
// We need to check if there's an active connection that is NOT our AP
const activeConnections = await $`nmcli -t -f NAME,TYPE connection show --active`.quiet().text()
const lines = activeConnections.trim().split("\n")
for (const line of lines) {
const [name, type] = line.split(":")
// Check if there's a wifi connection that's NOT our AP
if (type === "802-11-wireless" && name !== AP_CONNECTION_NAME) {
return true
}
}
return false
} catch (error) {
console.error("[isConnectedToWiFi] ERROR: Failed to check WiFi status")
console.error(error)
return false
}
}
async function startAP() {
if (apRunning) {
console.log("[startAP] AP already running, skipping")
return
}
console.log("🔴 Not connected to WiFi - Starting WiFi AP...")
console.log(`[startAP] AP SSID: ${AP_SSID}`)
console.log(`[startAP] AP Connection: ${AP_CONNECTION_NAME}`)
try {
// Check if connection profile already exists
console.log("[startAP] Checking if connection profile exists...")
const existsResult = await $`sudo nmcli connection show ${AP_CONNECTION_NAME}`.nothrow().quiet()
if (existsResult.exitCode !== 0) {
console.log("[startAP] Connection profile does not exist, creating...")
// Create AP connection profile (open network, no password)
await $`sudo nmcli connection add type wifi ifname wlan0 con-name ${AP_CONNECTION_NAME} autoconnect no ssid ${AP_SSID} 802-11-wireless.mode ap 802-11-wireless.band bg 802-11-wireless.channel ${CONFIG.ap.channel} ipv4.method shared ipv4.addresses ${CONFIG.ap.ip}/24`
console.log("[startAP] ✓ Connection profile created (open network)")
} else {
console.log("[startAP] Connection profile already exists, reusing")
}
// Bring up the AP
console.log("[startAP] Bringing up AP connection...")
await $`sudo nmcli connection up ${AP_CONNECTION_NAME}`
console.log("[startAP] ✓ AP connection activated")
apRunning = true
console.log(`✓ AP started successfully!`)
console.log(` SSID: ${AP_SSID}`)
console.log(` Password: None (open network)`)
console.log(` Connect and visit: http://${CONFIG.ap.ip}\n`)
} catch (error) {
console.error("[startAP] ERROR: Failed to start AP")
console.error(error)
apRunning = false
}
}
async function stopAP() {
if (!apRunning) {
console.log("[stopAP] AP not running, skipping")
return
}
console.log("🟢 Connected to WiFi - Stopping AP...")
console.log(`[stopAP] Bringing down connection: ${AP_CONNECTION_NAME}`)
try {
// Bring down the AP connection
await $`sudo nmcli connection down ${AP_CONNECTION_NAME}`.nothrow()
console.log("[stopAP] ✓ AP connection deactivated")
apRunning = false
console.log("✓ AP stopped successfully\n")
} catch (error) {
console.error("[stopAP] ERROR: Failed to stop AP")
console.error(error)
// Set to false anyway to allow retry
apRunning = false
}
}
async function checkAndManageAP() {
const connected = await isConnectedToWiFi()
if (connected && apRunning) {
console.log("[checkAndManageAP] WiFi connected and AP running → stopping AP")
await stopAP()
} else if (!connected && !apRunning) {
console.log("[checkAndManageAP] WiFi disconnected and AP stopped → starting AP")
await startAP()
} else if (!connected && apRunning) {
// AP is running but no WiFi client connection
// Check if our saved WiFi network is available
console.log("[checkAndManageAP] AP running, checking if saved WiFi is available...")
const savedNetwork = await findAvailableSavedNetwork()
if (savedNetwork) {
console.log(
`[checkAndManageAP] Found available saved network: ${savedNetwork}, attempting connection...`,
)
// Try to connect first
const connected = await tryConnect(savedNetwork)
if (connected) {
console.log(`[checkAndManageAP] Successfully connected to ${savedNetwork}, stopping AP...`)
await stopAP()
} else {
console.log(`[checkAndManageAP] Failed to connect to ${savedNetwork}, keeping AP running`)
// Delete the failed connection profile
try {
await $`sudo nmcli connection delete ${savedNetwork}`.nothrow()
console.log(`[checkAndManageAP] Deleted failed connection profile for ${savedNetwork}`)
} catch (error) {
console.error(`[checkAndManageAP] Failed to delete connection profile:`, error)
}
}
}
}
}
async function findAvailableSavedNetwork(): Promise<string | null> {
try {
// Get all saved WiFi connections (exclude our AP)
const savedConnections = await $`nmcli -t -f NAME,TYPE connection show`.quiet().text()
const lines = savedConnections.trim().split("\n")
const savedWiFiNames: string[] = []
for (const line of lines) {
const [name, type] = line.split(":")
if (type === "802-11-wireless" && name !== AP_CONNECTION_NAME) {
savedWiFiNames.push(name!)
}
}
if (savedWiFiNames.length === 0) {
return null
}
// Get the actual SSIDs for logging
const savedSSIDs: string[] = []
for (const savedName of savedWiFiNames) {
const connInfo = await $`nmcli -t -f 802-11-wireless.ssid connection show ${savedName}`
.quiet()
.nothrow()
.text()
const ssid = connInfo.split(":")[1]?.trim()
if (ssid) {
savedSSIDs.push(ssid)
}
}
console.log(`[findAvailableSavedNetwork] Saved WiFi networks: ${savedSSIDs.join(", ")}`)
// Scan for available networks
const scanResult = await $`nmcli -t -f SSID device wifi list`.quiet().nothrow().text()
const availableSSIDs = scanResult.trim().split("\n")
// Check if any saved network is available
for (const savedName of savedWiFiNames) {
// NetworkManager connection names often match SSIDs, but let's also check the connection's SSID
const connInfo = await $`nmcli -t -f 802-11-wireless.ssid connection show ${savedName}`
.quiet()
.nothrow()
.text()
const ssid = connInfo.split(":")[1]?.trim()
if (ssid && availableSSIDs.includes(ssid)) {
console.log(`[findAvailableSavedNetwork] Found available network: ${ssid}`)
return savedName // Return the connection name
}
}
return null
} catch (error) {
console.error("[findAvailableSavedNetwork] ERROR checking for WiFi")
console.error(error)
return null
}
}
async function tryConnect(connectionName: string): Promise<boolean> {
try {
console.log(`[tryConnect] Attempting to connect to ${connectionName}...`)
await $`sudo nmcli connection up ${connectionName}`
console.log(`[tryConnect] Successfully connected to ${connectionName}`)
return true
} catch (error) {
console.error(`[tryConnect] Failed to connect to ${connectionName}:`, error)
return false
}
}
// Initial check
const connected = await isConnectedToWiFi()
console.log(
`[checkAndManageAP] WiFi: ${connected ? "connected" : "disconnected"}, AP: ${
apRunning ? "running" : "stopped"
}`,
)
await checkAndManageAP()
// Check periodically
setInterval(checkAndManageAP, CONFIG.checkInterval)
console.log("Monitor running...")