obniz-cli
Version:
[日本語はこちら](./README-ja.md)
391 lines (364 loc) • 11.5 kB
text/typescript
import chalk from "chalk";
import wifi from "node-wifi";
import ora, { Ora } from "ora";
import { NetworkInterfaceInfo, networkInterfaces } from "os";
import config from "../config";
export default class WiFi {
public stdout: any;
public onerror: any;
constructor(obj: { stdout: any; onerror: any }) {
this.stdout = obj.stdout;
this.onerror = obj.onerror;
wifi.init({
iface: null,
});
}
public async setNetwork(configs: any, duplicate: boolean = true) {
let spinner;
const successIds = [];
while (true) {
try {
spinner = ora(`Wi-Fi Scanning...`).start();
let networks;
while (true) {
networks = await this.scanObnizWiFi(30 * 1000);
if (networks.length === 0) {
continue;
}
break;
}
for (let i = 0; i < networks.length; i++) {
const network = networks[i];
if (!duplicate && successIds[network.ssid]) {
continue;
}
if (i !== 0) {
spinner = ora(``).start();
}
try {
// Connect access point
spinner.text = `Found ${chalk.green(network.ssid)}. Connecting...`;
try {
await wifi.connect({ ssid: network.ssid });
} catch (e) {
throw new Error(`Connection to ${chalk.green(network.ssid)} failed`);
}
const destIP = guessObnizIP();
console.log(destIP);
spinner.text = `Connected ${chalk.green(network.ssid)}. IP=${destIP} Configuring...`;
if (destIP === "1.2.3.4" || destIP === "192.168.0.1") {
const succeed = await this.setForUnder350Devices(destIP, spinner, configs);
if (!succeed) {
continue;
}
spinner.succeed(`Configuration sent ${chalk.green(network.ssid)}`);
} else {
const succeed = await this.setForEqualOrOver350Devices(destIP, spinner, configs);
if (!succeed) {
continue;
}
spinner.succeed(`Configuration Saved and Device Rebooted ${chalk.green(network.ssid)}`);
}
successIds[network.ssid] = true;
} catch (e) {
spinner.fail(`${chalk.green(network.ssid)} Configuration failed reson=${e.toString()}`);
}
}
} catch (e) {
spinner?.fail(`${e.toString()}`);
}
}
}
private async setForUnder350Devices(targetIP: string, spinner: Ora, configs: any) {
// Configure network via wifi
const networks = configs.networks;
if (!networks) {
throw new Error(`please provide "networks". see more detail at example json file`);
}
if (!Array.isArray(networks)) {
throw new Error(`"networks" must be an array`);
}
if (networks.length !== 1) {
throw new Error(`"networks" must have single object in array.`);
}
const network = networks[0];
const type = network.type;
const setting = network.settings;
let getCount = 0;
let responseBody = "";
while (true) {
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const httpResponse: any = await new Promise((resolve, reject) => {
const timeout = 3000;
setTimeout(() => {
reject(new Error(`Timed out ${timeout}`));
}, timeout);
fetch(`http://${targetIP}/`)
.then((result) => {
resolve(result);
})
.catch((e) => {
reject(e);
});
});
if (httpResponse.ok) {
responseBody = await httpResponse.text();
break;
}
} catch (e) {
// ignore fetching error
}
++getCount;
spinner.text = `${chalk.green(network.ssid)} Connecting HTTP Server... ${getCount}`;
if (getCount >= 4) {
throw new Error(`${chalk.green(network.ssid)} HTTP Communication Failed ${getCount} times. abort`);
}
}
if (this.isSettingMode(responseBody)) {
await this.sendReset(spinner, network.ssid);
spinner.succeed(`Network Reset done. ${chalk.green(network.ssid)}`);
return false;
}
// Send HTTP Request
let url = "http://192.168.0.1/";
if (type === "wifi") {
url = "http://192.168.0.1/";
} else if (type === "cellular") {
url = "http://192.168.0.1/lte";
} else if (type === "wifimesh") {
url = "http://192.168.0.1/mesh";
}
const options = this.createSettingData(type, setting);
const res = await fetch(url, options);
if (res.status !== 200) {
throw new Error(`Configuration failed ${chalk.green(network.ssid)}`);
}
return true;
}
/**
* setting configration for OS3.5.0 or older
* @param spinner Ora object
* @param configs json user set
* @returns
*/
private async setForEqualOrOver350Devices(targetIP: string, spinner: Ora, configs: any) {
// Configure network via wifi
let getCount = 0;
const responseBody = "";
// Login Loop
spinner.text = `Logining...`;
while (true) {
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const res: any = await new Promise((resolve, reject) => {
const timeout = 3000;
setTimeout(() => {
reject(new Error(`Timed out ${timeout}`));
}, timeout);
fetch(`http://${targetIP}/api/login`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({ passkey: "obniz" }),
})
.then((result) => {
resolve(result);
})
.catch((e) => {
reject(e);
});
});
if (res.status === 404) {
// already logged in
break;
}
if (res.status !== 200) {
throw new Error(`Can't Logged In`);
}
break;
} catch (e) {
// throw new Error(`Login Error`)
}
++getCount;
spinner.text = `Accessing and Logining... ${getCount}`;
if (getCount >= 4) {
throw new Error(`Login Failed ${getCount} times. abort`);
}
}
// after login
spinner.text = `Sending JSON`;
const setRes = await fetch(`http://${targetIP}/api/setting`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(configs),
});
if (setRes.status !== 201) {
throw new Error(`Configration Failed with status ${setRes.status}`);
}
const options: any = {
method: "POST",
};
spinner.text = `Rebooting`;
const params = new URLSearchParams();
params.append("mode", "reboot");
options.body = params;
// this will never done. because device will reboot
fetch(`http://${targetIP}/setting`, options).then();
return true;
}
private isSettingMode(responseBody: string) {
return responseBody.indexOf(`Normal Launch`) >= 0;
}
private async sendReset(spinner: any, identifier: string) {
// reset_all
const options = this.createResetData();
const res = await fetch("http://192.168.0.1/", options);
if (res.status !== 200) {
throw new Error(`Resetting Network failed ${chalk.green(identifier)}`);
}
}
/**
* リセットリクエストを送りWi-Fi設定モードに遷移させる
* @returns
*/
private createResetData() {
const options: any = {
method: "POST",
};
const urlSetting = {
mode: "reset_all",
};
const params = new URLSearchParams();
(Object.keys(urlSetting) as Array<keyof typeof urlSetting>).forEach((key) => params.append(key, urlSetting[key]));
options.body = params;
return options;
}
/**
* ネットワーク設定を送る
* @param type
* @param setting
* @returns
*/
private createSettingData(type: string, setting: any) {
const options: any = {
method: "POST",
};
if (type === "wifi") {
const urlSetting: any = {
ssid: "other",
input_ssid: setting.ssid,
pw: setting.password,
si: "",
nm: "",
gw: "",
ds: "",
proxy_ip: "",
proxy_port: "",
};
if (setting.dhcp === false) {
urlSetting.si = setting.static_ip;
urlSetting.nm = setting.subnetmask;
urlSetting.gw = setting.default_gateway;
urlSetting.ds = setting.dns;
}
if (setting.proxy) {
urlSetting.proxy_ip = setting.proxy_address;
urlSetting.proxy_port = setting.proxy_port;
}
const params = new URLSearchParams();
Object.keys(urlSetting).forEach((key) => params.append(key, urlSetting[key]));
options.body = params;
} else if (type === "wifimesh") {
const urlSetting: any = {
ssid: "other",
input_ssid: setting.ssid,
pw: setting.password,
meshid: "",
si: "",
nm: "",
gw: "",
ds: "",
proxy_ip: "",
proxy_port: "",
};
if (setting.dhcp === false) {
urlSetting.si = setting.static_ip;
urlSetting.nm = setting.subnetmask;
urlSetting.gw = setting.default_gateway;
urlSetting.ds = setting.dns;
}
if (setting.proxy) {
urlSetting.proxy_ip = setting.proxy_address;
urlSetting.proxy_port = setting.proxy_port;
}
if (setting.meshid) {
urlSetting.meshid = setting.meshid;
}
const params = new URLSearchParams();
Object.keys(urlSetting).forEach((key) => params.append(key, urlSetting[key]));
options.body = params;
} else if (type === "cellular") {
const urlSetting = setting;
const params = new URLSearchParams();
Object.keys(urlSetting).forEach((key) => params.append(key, urlSetting[key]));
options.body = params;
}
return options;
}
private scanObnizWiFi(timeout: number): Promise<any> {
return new Promise(async (resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout. Cannot find any connectable obniz.`));
}, timeout);
wifi.scan((error: Error | null, networks: any[]) => {
if (error) {
clearTimeout(timer);
reject(error);
return;
}
const obnizwifis = [];
for (const network of networks) {
const re = /^obniz-[0-9]{8}/;
if (re.test(network.ssid)) {
obnizwifis.push(network);
}
}
clearTimeout(timer);
resolve(obnizwifis);
});
});
}
}
function currentLocalIP() {
const nets = networkInterfaces();
for (const name of Object.keys(nets)) {
for (const net of nets[name]!) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === "IPv4" && !net.internal) {
if (net.address.startsWith("192.168") || net.address.startsWith("1.2.3.")) {
return net.address;
}
}
}
}
// obnizOS 3.5.0 standard
return "192.168.254.2";
}
function guessObnizIP() {
const ip = currentLocalIP();
if (ip === "1.2.3.5") {
return "1.2.3.4";
} else if (ip === "192.168.0.2") {
return "192.168.0.1";
} else {
return "192.168.254.1";
}
}