Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion bin/prod/expand-usb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,23 @@ if [ -b /dev/sda2 ]; then
fi

info "Creating partition on unused space"
printf "n\n\n\n\nw\n" | fdisk /dev/sda
# fdisk's own in-place re-read of the table can fail with "Device or
# resource busy" even though the write itself succeeded (seen live on a
# fresh USB drive) - the kernel picks up the new partition via udev
# shortly after regardless, so don't treat that as fatal; wait for the
# device node to actually appear instead of trusting fdisk's exit code.
printf "n\n\n\n\nw\n" | fdisk /dev/sda || true

info "Waiting for /dev/sda2 to appear"
for i in $(seq 1 10); do
[ -b /dev/sda2 ] && break
sleep 1
done

if [ ! -b /dev/sda2 ]; then
info "Partition 2 did not appear after creation"
exit 1
fi

info "Creating ext4 filesystem"
mkfs.ext4 -F -E nodiscard /dev/sda2
Expand Down
13 changes: 13 additions & 0 deletions bin/prod/wifi-connect
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ INTERFACE="${WIFI_INTERFACE:-wlan0}"
SYS_NET="${SYS_NET:-/sys/class/net}"
IWD_DIR="${IWD_DIR:-/var/lib/iwd}"
LOG_FILE="${LOG_FILE:-/var/log/reflash.log}"
AP_PROFILE="Recore"

SSID="$1"
PASS="$2"
Expand All @@ -14,6 +15,16 @@ info() {
echo "$1"
}

# A failed connection here (bad password, AP out of range, DHCP timeout)
# would otherwise leave the board in station mode with no working
# connection and no hotspot - unreachable until physical intervention (#90).
restore_hotspot() {
info "Connection failed - restoring hotspot ($AP_PROFILE) so the board stays reachable."
iwctl ap "$INTERFACE" stop 2>/dev/null
iwctl device "$INTERFACE" set-property Mode ap
iwctl ap "$INTERFACE" start-profile "$AP_PROFILE" 2>/dev/null
}

if [ -z "$SSID" ] || [ -z "$PASS" ]; then
echo "Usage: $0 <SSID> <Password>"
exit 1
Expand Down Expand Up @@ -66,6 +77,7 @@ for i in {1..3}; do
sleep 2
if [ $i -eq 3 ]; then
info "Connection command failed after 3 attempts."
restore_hotspot
exit 1
fi
done
Expand All @@ -85,5 +97,6 @@ while [ $COUNT -lt $MAX_RETRIES ]; do
done

info "Timed out waiting for IP address."
restore_hotspot
exit 1

55 changes: 52 additions & 3 deletions client/src/components/TheWifiSetup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<div v-else class="pa4 text-center mt4 color-error border-error-all">
<p>鈿狅笍 No WiFi dongle detected. Please plug in a USB WiFi adapter.</p>
</div>

<div v-if="statusMessage" class="pa2 mt4 text-center">
<p>{{ statusMessage }}</p>
<w-button v-if="networkSwitching" @click="continueToBoard">Continue at recore.local</w-button>
</div>
</div>
</w-dialog>
</template>
Expand Down Expand Up @@ -72,6 +77,9 @@ export default {
availableAPs: [],
selected: null,
progressVisible: false,
connectPollDeadline: 0,
networkSwitching: false,
statusMessage: "",
}),
computed: mapGetters(["options"]),
methods: {
Expand Down Expand Up @@ -100,11 +108,13 @@ export default {
},
async startWifiScan() {
this.progressVisible = true;
this.statusMessage = "Scanning for networks...";
try {
await axios.post('/api/wifi_start_scan');
setTimeout(this.pollScanResults, 1000);
} catch (err) {
this.progressVisible = false;
this.statusMessage = "Could not start scan.";
}
},
async pollScanResults() {
Expand All @@ -118,7 +128,7 @@ export default {
for (const ap in this.availableAPs) {
this.availableAPs[ap].label = this.availableAPs[ap].SSID + " " + this.availableAPs[ap].signal;
}
this.progressVisible = false;
this.statusMessage = `Found ${this.availableAPs.length} network(s).`;
}
} catch (err) {
setTimeout(this.pollScanResults, 1000);
Expand All @@ -130,6 +140,9 @@ export default {
return;
}
this.progressVisible = true;
this.networkSwitching = false;
this.connectPollDeadline = Date.now() + 15000;
this.statusMessage = `Connecting to ${this.selected.SSID}...`;
try {
await axios.post('/api/wifi_start_connect',{
SSID: this.selected.SSID,
Expand All @@ -139,33 +152,67 @@ export default {
} catch (err) {
const msg = err.response?.data || "Could not start connection";
this.$waveui.notify(msg, "error", 4000);
this.statusMessage = msg;
this.progressVisible = false;
}
},
async pollConnectResults() {
// Once the board switches to station mode it tears down its own
// hotspot AP - if that's the network this page loaded from, its
// origin is gone for good from here on, regardless of whether the
// station-mode connection ultimately succeeds (#90). Polling that
// dead origin can never observe an outcome, so only do it for a
// short window to catch fast, local failures (e.g. validation
// errors) that happen before the switch - after that, stop and
// point the user at reconnecting manually instead of spinning
// forever against an address that will never respond again.
//
// A deadline (not an attempt counter) bounds this, because a
// request against a dead origin doesn't necessarily fail quickly -
// confirmed live (#95) that a request can hang completely silently
// (no error, ever) once the interface disappears. The explicit
// timeout below turns that into a normal rejection so the loop
// keeps moving instead of getting stuck forever on one request.
if (Date.now() > this.connectPollDeadline) {
this.progressVisible = false;
this.networkSwitching = true;
this.statusMessage = `The board is switching to ${this.selected.SSID}. Reconnect this device to that network, then continue below.`;
return;
}
try {
const res = await axios.get('/api/wifi_poll_connect');
const res = await axios.get('/api/wifi_poll_connect', { timeout: 3000 });
if (res.status === 204) {
setTimeout(this.pollConnectResults, 1000);
} else {
console.log(res.data)
if(res.data.isConnecting == true){
this.statusMessage = `Still connecting to ${this.selected.SSID}...`;
setTimeout(this.pollConnectResults, 1000);
}
else {
if (res.data.error) {
this.$waveui.notify(res.data.error, "error", 0);
this.statusMessage = res.data.error;
}
else{
this.$waveui.notify("Connected to "+this.selected.SSID, "info", 0);
this.$waveui.notify("Connected to "+this.selected.SSID, "info", 0);
this.statusMessage = `Connected to ${this.selected.SSID}.`;
}
this.progressVisible = false;
}
}
} catch (err) {
// A hung/timed-out request here looks the same as a normal
// network blip - keep showing the same "still connecting"
// status rather than alarming the user prematurely. The
// deadline check above is what actually decides when to give up.
this.statusMessage = `Still connecting to ${this.selected.SSID}...`;
setTimeout(this.pollConnectResults, 1000);
}
},
continueToBoard() {
window.location.href = "http://recore.local/";
},
async getWifiStatus() {
try {
const response = await axios.get('/api/get_wifi_status');
Expand All @@ -186,6 +233,8 @@ export default {
immediate: true,
handler(is_open) {
if (is_open) {
this.networkSwitching = false;
this.statusMessage = "";
this.getWifiStatus()
this.dialog.show = true;
}
Expand Down
12 changes: 12 additions & 0 deletions client/src/main.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { createApp } from 'vue'
import WaveUI from 'wave-ui'
import axios from 'axios'
import App from './App.vue'
import store from './store'

import 'wave-ui/dist/wave-ui.css'

// Several components poll the board on a timer with no per-call timeout
// (WiFi status, transfer progress, scan results, ...). A request with no
// timeout that never resolves - confirmed live: the board's WiFi
// interface disappearing mid-request during an AP/station switch (#90,
// #95) causes exactly this - sits holding one of the browser's ~6
// same-origin connection slots forever. Enough of those piling up at
// once starves every other request, including ones with their own
// explicit timeout, since they can't get a socket to run on at all. A
// global default bounds every request that doesn't set its own.
axios.defaults.timeout = 10000

const app = createApp(App)
app.use(store)

Expand Down
5 changes: 5 additions & 0 deletions client/vue.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
module.exports = {
transpileDependencies: true,
// The board's rootfs is a small, tightly-sized initrd ramdisk (368M
// total) - sourcemaps add 3MB+ per build for a debugging aid that's
// not needed on-device, and repeated deploys without them were
// enough to fill the disk outright during testing.
productionSourceMap: false,
devServer: {
proxy: {
'^/api': {
Expand Down
42 changes: 42 additions & 0 deletions test/bats/wifi.bats
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,45 @@ EOF
assert_called_with "station wlan0 connect HomeNet"
[[ "$output" == *"Connected with IP: 192.168.1.50/24"* ]]
}

# --- connect failure -> hotspot fallback (#90) ------------------------------

@test "wifi-connect: restores hotspot if the connect command keeps failing" {
with_adapter
cat > "$SHIMDIR/iwctl" <<'EOF'
#!/usr/bin/env bash
echo "iwctl $*" >> "$CALLS"
if [ "$1 $2 $3" = "device wlan0 show" ]; then echo "Mode station"; fi
if [ "$1 $2 $3" = "station wlan0 connect" ]; then exit 1; fi
exit 0
EOF
chmod +x "$SHIMDIR/iwctl"
run "$PROD_BIN/wifi-connect" HomeNet hunter2
[ "$status" -eq 1 ]
[[ "$output" == *"Connection command failed after 3 attempts."* ]]
assert_called_with "device wlan0 set-property Mode ap"
assert_called_with "ap wlan0 start-profile Recore"
}

@test "wifi-connect: restores hotspot if DHCP never leases" {
with_adapter
cat > "$SHIMDIR/iwctl" <<'EOF'
#!/usr/bin/env bash
echo "iwctl $*" >> "$CALLS"
if [ "$1 $2 $3" = "device wlan0 show" ]; then echo "Mode station"; fi
exit 0
EOF
chmod +x "$SHIMDIR/iwctl"
cat > "$SHIMDIR/ip" <<'EOF'
#!/usr/bin/env bash
echo "ip $*" >> "$CALLS"
# No "inet " line in the output - no lease ever arrives.
exit 0
EOF
chmod +x "$SHIMDIR/ip"
run "$PROD_BIN/wifi-connect" HomeNet hunter2
[ "$status" -eq 1 ]
[[ "$output" == *"Timed out waiting for IP address."* ]]
assert_called_with "device wlan0 set-property Mode ap"
assert_called_with "ap wlan0 start-profile Recore"
}
Loading