239 lines
7.9 KiB
TypeScript
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...")
|