├── app ├── views │ ├── 404.ejs │ └── index.ejs ├── dependency_manager.js ├── api.js ├── iwlist.js ├── public │ ├── app.js │ └── app.css └── wifi_manager.js ├── assets ├── etc │ ├── dnsmasq │ │ ├── dnsmasq.station.template │ │ └── dnsmasq.ap.template │ ├── hostapd │ │ ├── hostapd.conf.station.template │ │ └── hostapd.conf.template │ ├── wpa_supplicant │ │ └── wpa_supplicant.conf.template │ └── dhcpcd │ │ ├── dhcpcd.station.template │ │ └── dhcpcd.ap.template ├── bin │ └── hostapd.rtl871xdrv └── init.d │ └── raspberry-wifi-conf ├── .bowerrc ├── .gitignore ├── src ├── main.spec.ts ├── main.ts ├── wifi-manager.ts └── check-prerequisites.ts ├── jest.config.js ├── tsconfig.json ├── config.json ├── bower.json ├── LICENSE ├── package.json ├── server.js └── README.md /app/views/404.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/etc/dnsmasq/dnsmasq.station.template: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/public/external" 3 | } -------------------------------------------------------------------------------- /assets/etc/hostapd/hostapd.conf.station.template: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | 3 | /node_modules 4 | /npm-debug.log 5 | /app/public/external -------------------------------------------------------------------------------- /src/main.spec.ts: -------------------------------------------------------------------------------- 1 | it('should work', () => { 2 | expect('work').toBe('work'); 3 | }); -------------------------------------------------------------------------------- /assets/bin/hostapd.rtl871xdrv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/hot-pot/master/assets/bin/hostapd.rtl871xdrv -------------------------------------------------------------------------------- /assets/etc/dnsmasq/dnsmasq.ap.template: -------------------------------------------------------------------------------- 1 | interface={{ wifi_interface }} # Use the require wireless interface - usually wlan0 2 | dhcp-range={{ subnet_range_start }},{{ subnet_range_end }},{{ netmask }},24h 3 | -------------------------------------------------------------------------------- /assets/etc/wpa_supplicant/wpa_supplicant.conf.template: -------------------------------------------------------------------------------- 1 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 2 | update_config=1 3 | country=DE 4 | 5 | network={ 6 | ssid="{{ wifi_ssid }}" 7 | psk="{{ wifi_passcode }}" 8 | key_mgmt=WPA-PSK 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | } -------------------------------------------------------------------------------- /assets/etc/hostapd/hostapd.conf.template: -------------------------------------------------------------------------------- 1 | interface={{ wifi_interface }} 2 | 3 | driver={{ wifi_driver_type }} 4 | 5 | ssid={{ ssid }} 6 | hw_mode=g 7 | channel=7 8 | wmm_enabled=0 9 | macaddr_acl=0 10 | auth_algs=1 11 | ignore_broadcast_ssid=0 12 | wpa=2 13 | wpa_passphrase={{ passphrase }} 14 | wpa_key_mgmt=WPA-PSK 15 | wpa_pairwise=TKIP 16 | rsn_pairwise=CCMP 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": ["types/*"] 6 | }, 7 | "target": "es5", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "declarationMap": true, 11 | "outDir": "./lib", 12 | "rootDir": "./src", 13 | "strict": true, 14 | "noImplicitAny": false, 15 | "typeRoots": ["./types", "./node_modules/@types/"], 16 | "esModuleInterop": true 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "*.test.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { testDeps } from "./check-prerequisites"; 2 | import { WifiManager } from "./wifi-manager"; 3 | 4 | (async () => { 5 | try { 6 | const wifiManager = new WifiManager(); 7 | 8 | await testDeps({ 9 | "binaries": ["dnsmasq", "hostapd", "iw"], 10 | "files": ["/etc/dnsmasq.conf"] 11 | }); 12 | await wifiManager.isWifiEnabled(); 13 | await wifiManager.enableApMode(); 14 | await start_http_server(); 15 | } catch (err) { 16 | console.error(err); 17 | } 18 | })(); 19 | 20 | async function start_http_server() { 21 | 22 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wifi_interface": "wlan0", 3 | "wifi_driver_type": "nl80211", 4 | 5 | "access_point": { 6 | "force_reconfigure": false, 7 | "wifi_interface": "wlan0", 8 | "ssid": "rpi-config-ap", 9 | "passphrase": "123456z*", 10 | "domain": "rpi.config", 11 | "ip_addr": "192.168.88.1", 12 | "netmask": "255.255.255.0", 13 | "subnet_ip": "192.168.88.0", 14 | "broadcast_address": "192.168.88.255", 15 | "subnet_range_start": "192.168.88.100", 16 | "subnet_range_end": "192.168.88.200" 17 | }, 18 | 19 | "server": { 20 | "port": 8888 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raspberry-wifi-conf", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "ignore": [ 7 | "**/.*", 8 | "node_modules", 9 | "bower_components", 10 | "test", 11 | "tests", 12 | "app/public/external" 13 | ], 14 | "dependencies": { 15 | "angularjs": "~1.3.13", 16 | "font-awesome": "~4.3.0" 17 | }, 18 | 19 | "chloe": [ 20 | "# Chloe is a simple `Go` binary which can prune un-needed files ", 21 | "# from your bower install. The following `.gitignore`-esq set of ", 22 | "# lines tell chloe which files to prune. ", 23 | "# Check it out: https://github.com/sabhiram/go-chloe ", 24 | 25 | "**/external/**/*.md", 26 | "**/external/**/*.json", 27 | "**/external/**/*.gzip", 28 | "**/external/**/.*ignore", 29 | 30 | "**/external/angularjs/*.css", 31 | "**/external/font-awesome/less", 32 | "**/external/font-awesome/scss" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/wifi-manager.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from 'child_process'; 2 | import shell from 'shelljs'; 3 | 4 | const config: any = {}; 5 | 6 | interface IWifiInfo { 7 | hw_addr: string; 8 | inet_addr: string; 9 | ap_addr: string; 10 | ap_ssid: string; 11 | unassociated: string; 12 | } 13 | 14 | 15 | 16 | export class WifiManager { 17 | async enableApMode() { 18 | 19 | } 20 | 21 | async isWifiEnabled() { 22 | 23 | } 24 | 25 | async getWifiInfo(): Promise { 26 | try { 27 | const blah = await asyncExec('ifconfig wlan0'); 28 | const blah2 = await asyncExec('iwconfig wlan0'); 29 | } catch (err) { 30 | console.error(err); 31 | 32 | throw err; 33 | } 34 | 35 | return { 36 | hw_addr: '', 37 | inet_addr: '', 38 | ap_addr: '', 39 | ap_ssid: '', 40 | unassociated: '' 41 | }; 42 | } 43 | } 44 | 45 | async function asyncExec(cmd: string) { 46 | return new Promise((resolve, reject) => { 47 | const child = execFile(cmd); 48 | child.addListener("error", reject); 49 | child.addListener("exit", resolve); 50 | }); 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shaba Abhiram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /assets/init.d/raspberry-wifi-conf: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # /etc/init.d/raspberry-wifi-conf 3 | 4 | ### BEGIN INIT INFO 5 | # Provides: raspberry-wifi-conf 6 | # Required-Start: $local_fs $syslog $network 7 | # Required-Stop: $local_fs $syslog 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: Script to ensure wifi connectivity 11 | # Description: A NodeJS application to ensure Wifi connectivity by setting the RPI as an AP if needed 12 | ### END INIT INFO 13 | 14 | # Carry out specific functions when asked to by the system 15 | case "$1" in 16 | start) 17 | echo "Starting raspberry-wifi-conf service" 18 | cd /home/pi/raspberry-wifi-conf 19 | sudo /usr/bin/node server.js & 20 | echo $! > node.pid 21 | ;; 22 | stop) 23 | echo "Stopping raspberry-wifi-conf service" 24 | PIDFile=/home/pi/raspberry-wifi-conf/node.pid 25 | if [ -f $PIDFile ]; then 26 | sudo kill -9 $(cat $PIDFile) 27 | sudo kill -9 $(($(cat $PIDFile) + 1)) 28 | sudo rm $PIDFile 29 | fi 30 | ;; 31 | *) 32 | echo "Usage: /etc/init.d/raspberry-wifi-conf {start|stop}" 33 | exit 1 34 | ;; 35 | esac 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kibibit/hot-pot", 3 | "version": "0.0.1", 4 | "description": "Node RPI Wifi Configuration Application", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start:dev": "ts-node src/main.ts", 9 | "provision": "apt-get update -y && apt-get install dnsmasq hostapd iw -y", 10 | "test": "jest", 11 | "test:watch": "jest --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/Kibibit/hot-pot.git" 16 | }, 17 | "keywords": [ 18 | "RaspberryPi", 19 | "Node", 20 | "Wifi" 21 | ], 22 | "author": "Neil Kalman", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/kibibit/hot-pot/issues" 26 | }, 27 | "homepage": "https://github.com/kibibit/hot-pot", 28 | "dependencies": { 29 | "fs-extra": "^9.0.1", 30 | "lodash": "^4.17.20", 31 | "shelljs": "^0.8.4" 32 | }, 33 | "devDependencies": { 34 | "@types/fs-extra": "^9.0.2", 35 | "@types/jest": "^26.0.15", 36 | "@types/lodash": "^4.14.162", 37 | "@types/node": "^14.14.2", 38 | "@types/shelljs": "^0.8.8", 39 | "jest": "^26.6.1", 40 | "ts-jest": "^26.4.2", 41 | "ts-node": "^9.0.0", 42 | "typescript": "^4.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/check-prerequisites.ts: -------------------------------------------------------------------------------- 1 | import { every } from 'lodash'; 2 | import shell from 'shelljs'; 3 | import { pathExistsSync } from 'fs-extra'; 4 | 5 | interface IDeps { 6 | binaries?: string[]; 7 | files?: string[]; 8 | } 9 | 10 | export async function testDeps(deps: IDeps) { 11 | deps.binaries = deps.binaries || []; 12 | deps.files = deps.files || []; 13 | 14 | const binaryChecks = deps.binaries.map((binary) => ({binary, doesExist: !!shell.which(binary)})); 15 | 16 | const areAllBinariesAvailable = every(binaryChecks, (binaryCheck) => binaryCheck.doesExist); 17 | 18 | if (!areAllBinariesAvailable) { 19 | const missingDeps = binaryChecks 20 | .filter((binaryCheck) => !binaryCheck.doesExist) 21 | .map((binaryCheck) => binaryCheck.binary); 22 | 23 | throw new Error(`Dependency error: The following dependencies are missing: ${ missingDeps.join(', ') }.\nDid you run 'sudo npm run-script provision'?`); 24 | } 25 | 26 | const filesChecks = deps.files.map((filePath) => ({filePath, doesExist: pathExistsSync(filePath)})); 27 | const areAllFilesAvailable = every(filesChecks, (fileCheck) => fileCheck.doesExist); 28 | 29 | if (!areAllFilesAvailable) { 30 | const missingFiles = filesChecks 31 | .filter((fileCheck) => !fileCheck.doesExist) 32 | .map((fileCheck) => fileCheck.filePath); 33 | 34 | throw new Error(`The following files are missing: ${ missingFiles.join(', ') }.`); 35 | } 36 | 37 | return true; 38 | } -------------------------------------------------------------------------------- /assets/etc/dhcpcd/dhcpcd.station.template: -------------------------------------------------------------------------------- 1 | # A sample configuration for dhcpcd. 2 | # See dhcpcd.conf(5) for details. 3 | 4 | # Allow users of this group to interact with dhcpcd via the control socket. 5 | #controlgroup wheel 6 | 7 | # Inform the DHCP server of our hostname for DDNS. 8 | hostname 9 | 10 | # Use the hardware address of the interface for the Client ID. 11 | clientid 12 | # or 13 | # Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361. 14 | # Some non-RFC compliant DHCP servers do not reply with this set. 15 | # In this case, comment out duid and enable clientid above. 16 | #duid 17 | 18 | # Persist interface configuration when dhcpcd exits. 19 | persistent 20 | 21 | # Rapid commit support. 22 | # Safe to enable by default because it requires the equivalent option set 23 | # on the server to actually work. 24 | option rapid_commit 25 | 26 | # A list of options to request from the DHCP server. 27 | option domain_name_servers, domain_name, domain_search, host_name 28 | option classless_static_routes 29 | # Most distributions have NTP support. 30 | option ntp_servers 31 | # Respect the network MTU. This is applied to DHCP routes. 32 | option interface_mtu 33 | 34 | # A ServerID is required by RFC2131. 35 | require dhcp_server_identifier 36 | 37 | # Generate Stable Private IPv6 Addresses instead of hardware based ones 38 | slaac private 39 | 40 | # Example static IP configuration: 41 | #interface eth0 42 | #static ip_address=192.168.0.10/24 43 | #static ip6_address=fd51:42f8:caae:d92e::ff/64 44 | #static routers=192.168.0.1 45 | #static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1 46 | 47 | # It is possible to fall back to a static IP if DHCP fails: 48 | # define static profile 49 | #profile static_eth0 50 | #static ip_address=192.168.1.23/24 51 | #static routers=192.168.1.1 52 | #static domain_name_servers=192.168.1.1 53 | 54 | # fallback to static profile on eth0 55 | #interface eth0 56 | #fallback static_eth0 57 | -------------------------------------------------------------------------------- /assets/etc/dhcpcd/dhcpcd.ap.template: -------------------------------------------------------------------------------- 1 | # A sample configuration for dhcpcd. 2 | # See dhcpcd.conf(5) for details. 3 | 4 | # Allow users of this group to interact with dhcpcd via the control socket. 5 | #controlgroup wheel 6 | 7 | # Inform the DHCP server of our hostname for DDNS. 8 | hostname 9 | 10 | # Use the hardware address of the interface for the Client ID. 11 | clientid 12 | # or 13 | # Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361. 14 | # Some non-RFC compliant DHCP servers do not reply with this set. 15 | # In this case, comment out duid and enable clientid above. 16 | #duid 17 | 18 | # Persist interface configuration when dhcpcd exits. 19 | persistent 20 | 21 | # Rapid commit support. 22 | # Safe to enable by default because it requires the equivalent option set 23 | # on the server to actually work. 24 | option rapid_commit 25 | 26 | # A list of options to request from the DHCP server. 27 | option domain_name_servers, domain_name, domain_search, host_name 28 | option classless_static_routes 29 | # Most distributions have NTP support. 30 | option ntp_servers 31 | # Respect the network MTU. This is applied to DHCP routes. 32 | option interface_mtu 33 | 34 | # A ServerID is required by RFC2131. 35 | require dhcp_server_identifier 36 | 37 | # Generate Stable Private IPv6 Addresses instead of hardware based ones 38 | slaac private 39 | 40 | # Example static IP configuration: 41 | #interface eth0 42 | #static ip_address=192.168.0.10/24 43 | #static ip6_address=fd51:42f8:caae:d92e::ff/64 44 | #static routers=192.168.0.1 45 | #static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1 46 | 47 | # It is possible to fall back to a static IP if DHCP fails: 48 | # define static profile 49 | #profile static_eth0 50 | #static ip_address=192.168.1.23/24 51 | #static routers=192.168.1.1 52 | #static domain_name_servers=192.168.1.1 53 | 54 | # fallback to static profile on eth0 55 | #interface eth0 56 | #fallback static_eth0 57 | 58 | 59 | interface {{ wifi_interface }} 60 | static ip_address={{ ip_addr }}/24 61 | nohook wpa_supplicant 62 | -------------------------------------------------------------------------------- /app/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rpi Wifi Config 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 26 | 27 |
28 |
29 | 30 |
34 |
35 |
{{ cell.ssid }}
36 |
{{ cell.signal_strength }}
37 |
38 | 39 |
40 |
41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/dependency_manager.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore")._, 2 | async = require("async"), 3 | fs = require("fs"), 4 | exec = require("child_process").exec, 5 | config = require("../config.json"); 6 | 7 | /*****************************************************************************\ 8 | Return a set of functions which we can use to manage our dependencies 9 | \*****************************************************************************/ 10 | module.exports = function() { 11 | 12 | // Check dependencies based on the input "deps" object. 13 | // deps will contain: {"binaries": [...], "files":[...]} 14 | _check_deps = function(deps, callback) { 15 | if (typeof(deps["binaries"]) == "undefined") { 16 | deps["binaries"] = []; 17 | } 18 | if (typeof(deps["files"]) == "undefined") { 19 | deps["files"] = []; 20 | } 21 | 22 | // Define functions to check our binary deps 23 | var check_exe_fns = _.map(deps["binaries"], function(bin_dep) { 24 | //console.log("Building || function for " + bin_dep); 25 | return function(callback) { 26 | exec("which " + bin_dep, function(error, stdout, stderr) { 27 | if (error) return callback(error); 28 | if (stdout == "") return callback("\"which " + bin_dep + "\" returned no valid binary"); 29 | return callback(null) 30 | }); 31 | }; 32 | }); 33 | 34 | // Define functions to check our file deps 35 | var check_file_fns = _.map(deps["files"], function(file) { 36 | //console.log("Building || function for " + file); 37 | return function(callback) { 38 | fs.exists(file, function(exists) { 39 | if (exists) return callback(null); 40 | return callback(file + " does not exist"); 41 | }); 42 | }; 43 | }); 44 | 45 | // Dispatch the parallel functions 46 | async.series([ 47 | function check_binaries(next_step) { 48 | async.parallel(check_exe_fns, next_step); 49 | }, 50 | function check_files(next_step) { 51 | async.parallel(check_file_fns, next_step); 52 | }, 53 | ], callback); 54 | }; 55 | 56 | return { 57 | check_deps: _check_deps 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /app/api.js: -------------------------------------------------------------------------------- 1 | var path = require("path"), 2 | util = require("util"), 3 | iwlist = require("./iwlist"), 4 | express = require("express"), 5 | bodyParser = require('body-parser'), 6 | config = require("../config.json"), 7 | http_test = config.http_test_only; 8 | 9 | // Helper function to log errors and send a generic status "SUCCESS" 10 | // message to the caller 11 | function log_error_send_success_with(success_obj, error, response) { 12 | if (error) { 13 | console.log("ERROR: " + error); 14 | response.send({ status: "ERROR", error: error }); 15 | } else { 16 | success_obj = success_obj || {}; 17 | success_obj["status"] = "SUCCESS"; 18 | response.send(success_obj); 19 | } 20 | response.end(); 21 | } 22 | 23 | /*****************************************************************************\ 24 | Returns a function which sets up the app and our various routes. 25 | \*****************************************************************************/ 26 | module.exports = function(wifi_manager, callback) { 27 | var app = express(); 28 | 29 | // Configure the app 30 | app.set("view engine", "ejs"); 31 | app.set("views", path.join(__dirname, "views")); 32 | app.set("trust proxy", true); 33 | 34 | // Setup static routes to public assets 35 | app.use(express.static(path.join(__dirname, "public"))); 36 | app.use(bodyParser.json()); 37 | 38 | // Setup HTTP routes for rendering views 39 | app.get("/", function(request, response) { 40 | response.render("index"); 41 | }); 42 | 43 | // Setup HTTP routes for various APIs we wish to implement 44 | // the responses to these are typically JSON 45 | app.get("/api/rescan_wifi", function(request, response) { 46 | console.log("Server got /rescan_wifi"); 47 | iwlist(function(error, result) { 48 | log_error_send_success_with(result[0], error, response); 49 | }); 50 | }); 51 | 52 | app.post("/api/enable_wifi", function(request, response) { 53 | var conn_info = { 54 | wifi_ssid: request.body.wifi_ssid, 55 | wifi_passcode: request.body.wifi_passcode, 56 | }; 57 | 58 | // TODO: If wifi did not come up correctly, it should fail 59 | // currently we ignore ifup failures. 60 | wifi_manager.enable_wifi_mode(conn_info, function(error) { 61 | if (error) { 62 | console.log("Enable Wifi ERROR: " + error); 63 | console.log("Attempt to re-enable AP mode"); 64 | wifi_manager.enable_ap_mode(config.access_point.ssid, function(error) { 65 | console.log("... AP mode reset"); 66 | }); 67 | response.redirect("/"); 68 | } 69 | // Success! - exit 70 | console.log("Wifi Enabled! - Exiting"); 71 | process.exit(0); 72 | }); 73 | }); 74 | 75 | // Listen on our server 76 | app.listen(config.server.port); 77 | } 78 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var async = require("async"), 2 | wifi_manager = require("./app/wifi_manager")(), 3 | dependency_manager = require("./app/dependency_manager")(), 4 | config = require("./config.json"); 5 | 6 | /*****************************************************************************\ 7 | 1. Check for dependencies 8 | 2. Check to see if we are connected to a wifi AP 9 | 3. If connected to a wifi, do nothing -> exit 10 | 4. Convert RPI to act as a AP (with a configurable SSID) 11 | 5. Host a lightweight HTTP server which allows for the user to connect and 12 | configure the RPIs wifi connection. The interfaces exposed are RESTy so 13 | other applications can similarly implement their own UIs around the 14 | data returned. 15 | 6. Once the RPI is successfully configured, reset it to act as a wifi 16 | device (not AP anymore), and setup its wifi network based on what the 17 | user picked. 18 | 7. At this stage, the RPI is named, and has a valid wifi connection which 19 | its bound to, reboot the pi and re-run this script on startup. 20 | \*****************************************************************************/ 21 | async.series([ 22 | 23 | // 1. Check if we have the required dependencies installed 24 | function test_deps(next_step) { 25 | dependency_manager.check_deps({ 26 | "binaries": ["dnsmasq", "hostapd", "iw"], 27 | "files": ["/etc/dnsmasq.conf"] 28 | }, function(error) { 29 | if (error) console.log(" * Dependency error, did you run `sudo npm run-script provision`?"); 30 | next_step(error); 31 | }); 32 | }, 33 | 34 | // 2. Check if wifi is enabled / connected 35 | function test_is_wifi_enabled(next_step) { 36 | wifi_manager.is_wifi_enabled(function(error, result_ip) { 37 | 38 | if (result_ip) { 39 | console.log("\nWifi is enabled."); 40 | var reconfigure = config.access_point.force_reconfigure || false; 41 | if (reconfigure) { 42 | console.log("\nForce reconfigure enabled - try to enable access point"); 43 | } else { 44 | process.exit(0); 45 | } 46 | } else { 47 | console.log("\nWifi is not enabled, Enabling AP for self-configure"); 48 | } 49 | next_step(error); 50 | }); 51 | }, 52 | 53 | // 3. Turn RPI into an access point 54 | function enable_rpi_ap(next_step) { 55 | wifi_manager.enable_ap_mode(config.access_point.ssid, function(error) { 56 | if(error) { 57 | console.log("... AP Enable ERROR: " + error); 58 | } else { 59 | console.log("... AP Enable Success!"); 60 | } 61 | next_step(error); 62 | }); 63 | }, 64 | 65 | // 4. Host HTTP server while functioning as AP, the "api.js" 66 | // file contains all the needed logic to get a basic express 67 | // server up. It uses a small angular application which allows 68 | // us to choose the wifi of our choosing. 69 | function start_http_server(next_step) { 70 | console.log("\nHTTP server running..."); 71 | require("./app/api.js")(wifi_manager, next_step); 72 | }, 73 | 74 | 75 | ], function(error) { 76 | if (error) { 77 | console.log("ERROR: " + error); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /app/iwlist.js: -------------------------------------------------------------------------------- 1 | var exec = require("child_process").exec; 2 | 3 | /*****************************************************************************\ 4 | Return a function which is responsible for using "iwlist scan" to figure 5 | out the list of visible SSIDs along with their RSSI (and other info) 6 | \*****************************************************************************/ 7 | module.exports = function(cmd_options, callback) { 8 | // Handle case where no options are passed in 9 | if (typeof(cmd_options) == "function" && typeof(callback) == "undefined") { 10 | callback = cmd_options; 11 | cmd_options = ""; 12 | } 13 | 14 | var fields_to_extract = { 15 | "ssid": /ESSID:\"(.*)\"/, 16 | "quality": /Quality=(\d+)\/100/, 17 | "signal_strength": /.*Signal level=(\d+)\/100/, 18 | "encrypted": /Encryption key:(on)/, 19 | "open": /Encryption key:(off)/, 20 | }; 21 | 22 | exec("iwlist scan", function(error, stdout, stderr) { 23 | // Handle errors from running "iwlist scan" 24 | if (error) { 25 | return callback(error, output) 26 | } 27 | 28 | /* The output structure looks like this: 29 | [ 30 | { 31 | interface: "wlan0", 32 | scan_results: [ 33 | { ssid: "WifiB", address: "...", "signal_strength": 57 }, 34 | { ssid: "WifiA", address: "...", "signal_strength": 35 } 35 | ] 36 | }, 37 | ... 38 | ] */ 39 | var output = [], 40 | interface_entry = null, 41 | current_cell = null; 42 | 43 | function append_previous_cell() { 44 | if (current_cell != null && interface_entry != null) { 45 | if (typeof(current_cell["ssid"]) != "undefined" && 46 | current_cell["ssid"] != "" ) { 47 | interface_entry["scan_results"].push(current_cell); 48 | } 49 | current_cell = null; 50 | } 51 | } 52 | 53 | function append_previous_interface() { 54 | append_previous_cell(); 55 | if (interface_entry != null) { 56 | output.push(interface_entry); 57 | interface_entry = null; 58 | } 59 | } 60 | 61 | // Parse the result, build return object 62 | lines = stdout.split("\n"); 63 | for (var idx in lines) { 64 | line = lines[idx].trim(); 65 | 66 | // Detect new interface 67 | var re_new_interface = line.match(/([^\s]+)\s+Scan completed :/); 68 | if (re_new_interface) { 69 | console.log("Found new interface: " + re_new_interface[1]); 70 | append_previous_interface(); 71 | interface_entry = { 72 | "interface": re_new_interface[1], 73 | "scan_results": [] 74 | }; 75 | continue; 76 | } 77 | 78 | // Detect new cell 79 | var re_new_cell = line.match(/Cell ([0-9]+) - Address: (.*)/); 80 | if (re_new_cell) { 81 | append_previous_cell(); 82 | current_cell = { 83 | "cell_id": parseInt(re_new_cell[1]), 84 | "address": re_new_cell[2], 85 | }; 86 | continue; 87 | } 88 | 89 | // Handle other fields we want to extract 90 | for (var key in fields_to_extract) { 91 | var match = line.match(fields_to_extract[key]); 92 | if (match) { 93 | current_cell[key] = match[1]; 94 | } 95 | } 96 | } 97 | 98 | // Add the last item we tracked 99 | append_previous_interface(); 100 | 101 | return callback(null, output); 102 | }); 103 | 104 | } 105 | -------------------------------------------------------------------------------- /app/public/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*** 4 | * Define the app and inject any modules we wish to 5 | * refer to. 6 | ***/ 7 | var app = angular.module("RpiWifiConfig", []); 8 | 9 | /******************************************************************************\ 10 | Function: 11 | AppController 12 | 13 | Dependencies: 14 | ... 15 | 16 | Description: 17 | Main application controller 18 | \******************************************************************************/ 19 | app.controller("AppController", ["PiManager", "$scope", "$location", "$timeout", 20 | 21 | function(PiManager, $scope, $location, $timeout) { 22 | // Scope variable declaration 23 | $scope.scan_results = []; 24 | $scope.selected_cell = null; 25 | $scope.scan_running = false; 26 | $scope.network_passcode = ""; 27 | $scope.show_passcode_entry_field = false; 28 | 29 | // Scope filter definitions 30 | $scope.orderScanResults = function(cell) { 31 | return parseInt(cell.signal_strength); 32 | } 33 | 34 | $scope.foo = function() { console.log("foo"); } 35 | $scope.bar = function() { console.log("bar"); } 36 | 37 | // Scope function definitions 38 | $scope.rescan = function() { 39 | $scope.scan_results = []; 40 | $scope.selected_cell = null; 41 | $scope.scan_running = true; 42 | PiManager.rescan_wifi().then(function(response) { 43 | console.log(response.data); 44 | if (response.data.status == "SUCCESS") { 45 | $scope.scan_results = response.data.scan_results; 46 | } 47 | $scope.scan_running = false; 48 | }); 49 | } 50 | 51 | $scope.change_selection = function(cell) { 52 | $scope.network_passcode = ""; 53 | $scope.selected_cell = cell; 54 | $scope.show_passcode_entry_field = (cell != null) ? true : false; 55 | } 56 | 57 | $scope.submit_selection = function() { 58 | if (!$scope.selected_cell) return; 59 | 60 | var wifi_info = { 61 | wifi_ssid: $scope.selected_cell["ssid"], 62 | wifi_passcode: $scope.network_passcode, 63 | }; 64 | 65 | PiManager.enable_wifi(wifi_info).then(function(response) { 66 | console.log(response.data); 67 | if (response.data.status == "SUCCESS") { 68 | console.log("AP Enabled - nothing left to do..."); 69 | } 70 | }); 71 | } 72 | 73 | // Defer load the scanned results from the rpi 74 | $scope.rescan(); 75 | }] 76 | ); 77 | 78 | /*****************************************************************************\ 79 | Service to hit the rpi wifi config server 80 | \*****************************************************************************/ 81 | app.service("PiManager", ["$http", 82 | 83 | function($http) { 84 | return { 85 | rescan_wifi: function() { 86 | return $http.get("/api/rescan_wifi"); 87 | }, 88 | enable_wifi: function(wifi_info) { 89 | return $http.post("/api/enable_wifi", wifi_info); 90 | } 91 | }; 92 | }] 93 | 94 | ); 95 | 96 | /*****************************************************************************\ 97 | Directive to show / hide / clear the password prompt 98 | \*****************************************************************************/ 99 | app.directive("rwcPasswordEntry", function($timeout) { 100 | return { 101 | restrict: "E", 102 | 103 | scope: { 104 | visible: "=", 105 | passcode: "=", 106 | reset: "&", 107 | submit: "&", 108 | }, 109 | 110 | replace: true, // Use provided template (as opposed to static 111 | // content that the modal scope might define in the 112 | // DOM) 113 | template: [ 114 | "
", 115 | "
", 116 | " ", 117 | "
Cancel
", 118 | "
Submit
", 119 | "
", 120 | "
" 121 | ].join("\n"), 122 | 123 | // Link function to bind modal to the app 124 | link: function(scope, element, attributes) { 125 | }, 126 | }; 127 | }); 128 | -------------------------------------------------------------------------------- /app/public/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; padding: 0; 3 | font-family: "Roboto", "PT Sans", sans-serif; 4 | height: 100%; min-height: 100%; 5 | overflow: hidden; 6 | position: relative; 7 | } 8 | 9 | .page-header { 10 | width: 100%; 11 | height: 50px; line-height: 50px; 12 | color: white; 13 | 14 | font-size: 20pt; 15 | text-align: center; 16 | 17 | -webkit-user-select: none; 18 | 19 | background: #000000; /*#039be5;*/ 20 | border-bottom: 2px solid #ffffff; 21 | } 22 | .page-header a { 23 | position: absolute; 24 | right: 0; margin: 8px 5px; 25 | text-decoration: none; 26 | 27 | color: white; 28 | width: 30px; height: 30px; 29 | border-radius: 100%; 30 | border: 2px solid white; 31 | 32 | -webkit-transition: all 0.3s ease; 33 | -moz-transition: all 0.3s ease; 34 | -ms-transition: all 0.3s ease; 35 | -o-transition: all 0.3s ease; 36 | transition: all 0.3s ease; 37 | } 38 | .page-header a:hover { 39 | color: yellow; 40 | background: #444444; 41 | } 42 | .page-header .active { 43 | color: yellow; 44 | background: #444444; 45 | } 46 | .page-header a i { 47 | font-size: 18pt; 48 | position: absolute; 49 | top: 3px; left: 5px; 50 | padding-bottom: 0; 51 | } 52 | 53 | .page-content { 54 | width: 100%; 55 | height: calc(100% - 50px); 56 | } 57 | 58 | .scan-results-container { 59 | margin: 0 auto; 60 | margin-bottom: 10px; 61 | 62 | width: 100%; max-width: 400px; 63 | height: calc(100% - 5px); 64 | border-bottom: 2px solid white; 65 | 66 | background: #ffffff; 67 | 68 | overflow-y: auto; 69 | overflow-x: hidden; 70 | } 71 | 72 | .scan-result { 73 | color: white; 74 | position: relative; 75 | height: 40px; line-height: 40px; 76 | 77 | -webkit-transition: all 0.3s ease; 78 | -moz-transition: all 0.3s ease; 79 | -ms-transition: all 0.3s ease; 80 | -o-transition: all 0.3s ease; 81 | transition: all 0.3s ease; 82 | cursor: pointer; 83 | 84 | border-bottom: 2px solid #666666; 85 | background: #222222; 86 | } 87 | .scan-result:hover { 88 | color: yellow; 89 | background: #444444; 90 | } 91 | .scan-result.selected { 92 | color: yellow; 93 | background: #888888; 94 | } 95 | .scan-result .ssid { 96 | position: absolute; 97 | top: 0; left: 10%; 98 | width: 80%; height: 100%; 99 | } 100 | .scan-result .secure { 101 | color: white; 102 | padding-left: 3px; 103 | } 104 | .scan-result .signal_stength { 105 | position: absolute; 106 | top: 0; left: 85%; 107 | width: 10%; height: 100%; 108 | } 109 | 110 | /* 111 | 112 | .password_input { 113 | margin: 0 auto; 114 | 115 | width: 80%; max-width: 300px; 116 | height: 45px; 117 | 118 | background: #f0f9fe; 119 | 120 | -webkit-transition: all 0.3s ease; 121 | -moz-transition: all 0.3s ease; 122 | -ms-transition: all 0.3s ease; 123 | -o-transition: all 0.3s ease; 124 | transition: all 0.3s ease; 125 | 126 | overflow: hidden; 127 | } 128 | 129 | .password_input.hidden { 130 | height: 0px; 131 | } 132 | 133 | .password_input input { 134 | width: 98%; 135 | height: 87%; 136 | line-height: 134%; 137 | 138 | font-size: 17pt; 139 | } 140 | 141 | .submit_btn { 142 | margin: 0 auto; 143 | 144 | width: 80%; max-width: 300px; 145 | height: 45px; line-height: 45px; 146 | 147 | background: #222222; 148 | color: white; 149 | cursor: pointer; 150 | 151 | font-size: 18pt; 152 | text-align: center; 153 | 154 | border-bottom-left-radius: 12px; 155 | border-bottom-right-radius: 12px; 156 | 157 | -webkit-transition: all 0.3s ease; 158 | -moz-transition: all 0.3s ease; 159 | -ms-transition: all 0.3s ease; 160 | -o-transition: all 0.3s ease; 161 | transition: all 0.3s ease; 162 | } 163 | 164 | .submit_btn:hover { 165 | background: #28e602; 166 | color: black; 167 | } 168 | */ 169 | 170 | 171 | .rwc-password-entry-container { 172 | position: absolute; 173 | 174 | top: 0; left: 0; 175 | width: 100%; height: 100%; 176 | 177 | background: rgba(0,0,0,0.85); 178 | 179 | -webkit-transition: all 0.3s ease; 180 | -moz-transition: all 0.3s ease; 181 | -ms-transition: all 0.3s ease; 182 | -o-transition: all 0.3s ease; 183 | transition: all 0.3s ease; 184 | } 185 | 186 | .rwc-password-entry-container.hide-me { 187 | top: 100%; 188 | opacity: 0; 189 | } 190 | 191 | .rwc-password-entry-container .box { 192 | position: absolute; 193 | top:0; 194 | bottom: 0; 195 | left: 0; 196 | right: 0; 197 | 198 | margin: auto; 199 | 200 | width: 320px; 201 | height: 150px; 202 | 203 | background: rgba(0,0,0,1.0); 204 | border: 2px solid #666666; 205 | } 206 | 207 | .rwc-password-entry-container .box input { 208 | width: 90%; 209 | height: 50px; line-height: 50px; 210 | 211 | font-size: 18pt; 212 | border: 0; 213 | 214 | margin-left: 5%; 215 | margin-top: 15px; 216 | } 217 | 218 | .rwc-password-entry-container .box .btn { 219 | width: 130px; 220 | height: 40px; line-height: 40px; 221 | text-align: center; 222 | 223 | border: 2px solid #000000; 224 | 225 | position: absolute; 226 | 227 | -webkit-transition: all 0.3s ease; 228 | -moz-transition: all 0.3s ease; 229 | -ms-transition: all 0.3s ease; 230 | -o-transition: all 0.3s ease; 231 | transition: all 0.3s ease; 232 | 233 | cursor: pointer; 234 | } 235 | .btn-cancel { 236 | background: #881111; 237 | top: 90px; left: 20px; 238 | } 239 | .btn-ok { 240 | top: 90px; left: 170px; 241 | background: #118811; 242 | } 243 | .btn-cancel:hover { 244 | background: #ff2222; 245 | } 246 | .btn-ok:hover { 247 | background: #22ff22; 248 | } 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | @kibibit/hot-pot 5 |

6 |

7 | 23 |

24 | A Node application which helps you onboard devices into a network without a screen 25 |

26 |
27 | 28 | Tested on Stretch and Raspberrt Pi 3 29 | 30 | Based on [this project](https://github.com/sabhiram/raspberry-wifi-conf). We basically wanted to improve upon this solution by prettifying it and making it work for other platforms 31 | 32 | ## RPI 4 Note: 33 | 34 | I realize that a bunch of folks will try this out using the shiny new RaspberryPi v4. I caution you that this is not something I have tried, I believe this was tested on a Pi3 to success. However, if you find that this works on a Pi4, please let me know and I will adjust the readme accordingly. If it does not work, it is probably a few PRs away from success :) 35 | 36 | ## Why? 37 | 38 | When unable to connect to a wifi network, this service will turn the RPI into a wireless AP. This allows us to connect to it via a phone or other device and configure our home wifi network (for example). 39 | 40 | Once configured, it prompts the PI to reboot with the appropriate wifi credentials. If this process fails, it immediately re-enables the PI as an AP which can be configurable again. 41 | 42 | This project broadly follows these [instructions](https://www.raspberrypi.org/documentation/configuration/wireless/access-point.md) in setting up a RaspberryPi as a wireless AP. 43 | 44 | ## Requirements 45 | 46 | The NodeJS modules required are pretty much just `underscore`, `async`, and `express`. 47 | 48 | The web application requires `angular` and `font-awesome` to render correctly. To make the deployment of this easy, one of the other requirements is `bower`. 49 | 50 | If you do not have `bower` installed already, you can install it globally by running: `sudo npm install bower -g`. 51 | 52 | ## Install 53 | 54 | ```sh 55 | $git clone https://github.com/sabhiram/raspberry-wifi-conf.git 56 | $cd raspberry-wifi-conf 57 | $npm update 58 | $bower install 59 | $sudo npm run-script provision 60 | $sudo npm start 61 | ``` 62 | 63 | 64 | ## Setup the app as a service 65 | 66 | There is a startup script included to make the server starting and stopping easier. Do remember that the application is assumed to be installed under `/home/pi/raspberry-wifi-conf`. Feel free to change this in the `assets/init.d/raspberry-wifi-conf` file. 67 | 68 | ```sh 69 | $sudo cp assets/init.d/raspberry-wifi-conf /etc/init.d/raspberry-wifi-conf 70 | $sudo chmod +x /etc/init.d/raspberry-wifi-conf 71 | $sudo update-rc.d raspberry-wifi-conf defaults 72 | ``` 73 | 74 | ### Gotchas 75 | 76 | #### `hostapd` 77 | 78 | The `hostapd` application does not like to behave itself on some wifi adapters (RTL8192CU et al). This link does a good job explaining the issue and the remedy: [Edimax Wifi Issues](http://willhaley.com/blog/raspberry-pi-hotspot-ew7811un-rtl8188cus/). The gist of what you need to do is as follows: 79 | 80 | ``` 81 | # run iw to detect if you have a rtl871xdrv or nl80211 driver 82 | $iw list 83 | ``` 84 | 85 | If the above says `nl80211 not found.` it means you are running the `rtl871xdrv` driver and probably need to update the `hostapd` binary as follows: 86 | ``` 87 | $cd raspberry-wifi-conf 88 | $sudo mv /usr/sbin/hostapd /usr/sbin/hostapd.OLD 89 | $sudo mv assets/bin/hostapd.rtl871xdrv /usr/sbin/hostapd 90 | $sudo chmod 755 /usr/sbin/hostapd 91 | ``` 92 | 93 | Note that the `wifi_driver_type` config variable is defaulted to the `nl80211` driver. However, if `iw list` fails on the app startup, it will automatically set the driver type of `rtl871xdrv`. Remember that even though you do not need to update the config / default value - you will need to use the updated `hostapd` binary bundled with this app. 94 | 95 | #### `dhcpcd` 96 | 97 | Latest versions of raspbian use dhcpcd to manage network interfaces, since we are running our own dhcp server, if you have dhcpcd installed - make sure you deny the wifi interface as described in the installation section. 98 | 99 | TODO: Handle this automatically. 100 | 101 | ## Usage 102 | 103 | This is approximately what occurs when we run this app: 104 | 105 | 1. Check to see if we are connected to a wifi AP 106 | 2. If connected to a wifi, do nothing -> exit 107 | 3. (if not wifi, then) Convert RPI to act as an AP (with a configurable SSID) 108 | 4. Host a lightweight HTTP server which allows for the user to connect and configure the RPIs wifi connection. The interfaces exposed are RESTy so other applications can similarly implement their own UIs around the data returned. 109 | 5. Once the RPI is successfully configured, reset it to act as a wifi device (not AP anymore), and setup it's wifi network based on what the user selected. 110 | 6. At this stage, the RPI is named, and has a valid wifi connection which it is now bound to. 111 | 112 | Typically, I have the following line in my `/etc/rc.local` file: 113 | ``` 114 | cd /home/pi/raspberry-wifi-conf 115 | sudo /usr/bin/node server.js 116 | ``` 117 | 118 | Note that this is run in a blocking fashion, in that this script will have to exit before we can proceed with others defined in `rc.local`. This way I can guarantee that other services which might rely on wifi will have said connection before being run. If this is not the case for you, and you just want this to run (if needed) in the background, then you can do: 119 | 120 | ``` 121 | cd /home/pi/raspberry-wifi-conf 122 | sudo /usr/bin/node server.js < /dev/null & 123 | ``` 124 | 125 | ## User Interface 126 | 127 | In my config file, I have set up the static ip for my PI when in AP mode to `192.168.44.1` and the AP's broadcast SSID to `rpi-config-ap`. These are images captured from my osx dev box. 128 | 129 | Step 1: Power on Pi which runs this app on startup (assume it is not configured for a wifi connection). Once it boots up, you will see `rpi-config-ap` among the wifi connections. The password is configured in config.json. 130 | 131 | 132 | 133 | Step 2: Join the above network, and navigate to the static IP and port we set in config.json (`http://192.168.44.1:88`), you will see: 134 | 135 | 136 | 137 | Step 3: Select your home (or whatever) network, punch in the wifi passcode if any, and click `Submit`. You are done! Your Pi is now on your home wifi!! 138 | 139 | ## Testing 140 | 141 | ## License 142 | 143 | MIT © 2019 Neil Kalman neilkalman@gmail.com 144 | 145 |
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
146 | -------------------------------------------------------------------------------- /app/wifi_manager.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore")._, 2 | async = require("async"), 3 | fs = require("fs"), 4 | exec = require("child_process").exec, 5 | config = require("../config.json"); 6 | 7 | // Better template format 8 | _.templateSettings = { 9 | interpolate: /\{\{(.+?)\}\}/g, 10 | evaluate : /\{\[([\s\S]+?)\]\}/g 11 | }; 12 | 13 | // Helper function to write a given template to a file based on a given 14 | // context 15 | function write_template_to_file(template_path, file_name, context, callback) { 16 | async.waterfall([ 17 | 18 | function read_template_file(next_step) { 19 | fs.readFile(template_path, {encoding: "utf8"}, next_step); 20 | }, 21 | 22 | function update_file(file_txt, next_step) { 23 | var template = _.template(file_txt); 24 | fs.writeFile(file_name, template(context), next_step); 25 | } 26 | 27 | ], callback); 28 | } 29 | 30 | /*****************************************************************************\ 31 | Return a set of functions which we can use to manage and check our wifi 32 | connection information 33 | \*****************************************************************************/ 34 | module.exports = function() { 35 | // Detect which wifi driver we should use, the rtl871xdrv or the nl80211 36 | exec("iw list", function(error, stdout, stderr) { 37 | if (stderr.match(/^nl80211 not found/)) { 38 | config.wifi_driver_type = "rtl871xdrv"; 39 | } 40 | }); 41 | 42 | // Hack: this just assumes that the outbound interface will be "wlan0" 43 | 44 | // Define some globals 45 | var ifconfig_fields = { 46 | "hw_addr": /HWaddr\s([^\s]+)/, 47 | "inet_addr": /inet\s*([^\s]+)/, 48 | }, iwconfig_fields = { 49 | "ap_addr": /Access Point:\s([^\s]+)/, 50 | "ap_ssid": /ESSID:\"([^\"]+)\"/, 51 | "unassociated": /(unassociated)\s+Nick/, 52 | }, last_wifi_info = null; 53 | 54 | // TODO: rpi-config-ap hardcoded, should derive from a constant 55 | 56 | // Get generic info on an interface 57 | var _get_wifi_info = function(callback) { 58 | var output = { 59 | hw_addr: "", 60 | inet_addr: "", 61 | ap_addr: "", 62 | ap_ssid: "", 63 | unassociated: "", 64 | }; 65 | 66 | // Inner function which runs a given command and sets a bunch 67 | // of fields 68 | function run_command_and_set_fields(cmd, fields, callback) { 69 | exec(cmd, function(error, stdout, stderr) { 70 | if (error) return callback(error); 71 | 72 | for (var key in fields) { 73 | re = stdout.match(fields[key]); 74 | if (re && re.length > 1) { 75 | output[key] = re[1]; 76 | } 77 | } 78 | 79 | callback(null); 80 | }); 81 | } 82 | 83 | // Run a bunch of commands and aggregate info 84 | async.series([ 85 | function run_ifconfig(next_step) { 86 | run_command_and_set_fields("ifconfig wlan0", ifconfig_fields, next_step); 87 | }, 88 | function run_iwconfig(next_step) { 89 | run_command_and_set_fields("iwconfig wlan0", iwconfig_fields, next_step); 90 | }, 91 | ], function(error) { 92 | last_wifi_info = output; 93 | return callback(error, output); 94 | }); 95 | }, 96 | 97 | _reboot_wireless_network = function(wlan_iface, callback) { 98 | async.series([ 99 | function down(next_step) { 100 | exec("sudo ifconfig " + wlan_iface + " down", function(error, stdout, stderr) { 101 | if (!error) console.log("ifconfig " + wlan_iface + " down successful..."); 102 | next_step(); 103 | }); 104 | }, 105 | function up(next_step) { 106 | exec("sudo ifconfig " + wlan_iface + " up", function(error, stdout, stderr) { 107 | if (!error) console.log("ifconfig " + wlan_iface + " up successful..."); 108 | next_step(); 109 | }); 110 | }, 111 | ], callback); 112 | }, 113 | 114 | // Wifi related functions 115 | _is_wifi_enabled_sync = function(info) { 116 | // If we are not an AP, and we have a valid 117 | // inet_addr - wifi is enabled! 118 | //console.log(_is_ap_enabled_sync(info)); 119 | if (null == _is_ap_enabled_sync(info) && 120 | "" != info["inet_addr"] && 121 | "Not-Associated" != info["ap_addr"] && 122 | "" != info["ap_addr"] ) { 123 | return info["inet_addr"]; 124 | } 125 | return null; 126 | }, 127 | 128 | _is_wifi_enabled = function(callback) { 129 | _get_wifi_info(function(error, info) { 130 | if (error) return callback(error, null); 131 | return callback(null, _is_wifi_enabled_sync(info)); 132 | }); 133 | }, 134 | 135 | // Access Point related functions 136 | _is_ap_enabled_sync = function(info) { 137 | 138 | var is_ap = info["ap_ssid"] == config.access_point.ssid; 139 | 140 | if(is_ap == true){ 141 | return info["ap_ssid"]; 142 | } 143 | else{ 144 | 145 | return null; 146 | } 147 | 148 | }, 149 | 150 | _is_ap_enabled = function(callback) { 151 | _get_wifi_info(function(error, info) { 152 | if (error) return callback(error, null); 153 | return callback(null, _is_ap_enabled_sync(info)); 154 | }); 155 | }, 156 | 157 | // Enables the accesspoint w/ bcast_ssid. This assumes that both 158 | // dnsmasq and hostapd are installed using: 159 | // $sudo npm run-script provision 160 | _enable_ap_mode = function(bcast_ssid, callback) { 161 | _is_ap_enabled(function(error, result_addr) { 162 | if (error) { 163 | console.log("ERROR: " + error); 164 | return callback(error); 165 | } 166 | 167 | if (result_addr && !config.access_point.force_reconfigure) { 168 | console.log("\nAccess point is enabled with ADDR: " + result_addr); 169 | return callback(null); 170 | } else if (config.access_point.force_reconfigure) { 171 | console.log("\nForce reconfigure enabled - reset AP"); 172 | } else { 173 | console.log("\nAP is not enabled yet... enabling..."); 174 | } 175 | 176 | var context = config.access_point; 177 | context["enable_ap"] = true; 178 | context["wifi_driver_type"] = config.wifi_driver_type; 179 | 180 | // Here we need to actually follow the steps to enable the ap 181 | async.series([ 182 | 183 | // Enable the access point ip and netmask + static 184 | // DHCP for the wlan0 interface 185 | function update_interfaces(next_step) { 186 | write_template_to_file( 187 | "./assets/etc/dhcpcd/dhcpcd.ap.template", 188 | "/etc/dhcpcd.conf", 189 | context, next_step); 190 | }, 191 | 192 | 193 | // Enable the interface in the dhcp server 194 | function update_dhcp_interface(next_step) { 195 | write_template_to_file( 196 | "./assets/etc/dnsmasq/dnsmasq.ap.template", 197 | "/etc/dnsmasq.conf", 198 | context, next_step); 199 | }, 200 | 201 | // Enable hostapd.conf file 202 | function update_hostapd_conf(next_step) { 203 | write_template_to_file( 204 | "./assets/etc/hostapd/hostapd.conf.template", 205 | "/etc/hostapd/hostapd.conf", 206 | context, next_step); 207 | }, 208 | 209 | function restart_dhcp_service(next_step) { 210 | exec("sudo systemctl restart dhcpcd", function(error, stdout, stderr) { 211 | if (!error) console.log("... dhcpcd server restarted!"); 212 | else console.log("... dhcpcd server failed! - " + stdout); 213 | next_step(); 214 | }); 215 | }, 216 | 217 | 218 | function reboot_network_interfaces(next_step) { 219 | _reboot_wireless_network(config.wifi_interface, next_step); 220 | }, 221 | 222 | function restart_hostapd_service(next_step) { 223 | exec("sudo systemctl restart hostapd", function(error, stdout, stderr) { 224 | //console.log(stdout); 225 | if (!error) console.log("... hostapd restarted!"); 226 | next_step(); 227 | }); 228 | }, 229 | 230 | function restart_dnsmasq_service(next_step) { 231 | exec("sudo systemctl restart dnsmasq", function(error, stdout, stderr) { 232 | if (!error) console.log("... dnsmasq server restarted!"); 233 | else console.log("... dnsmasq server failed! - " + stdout); 234 | next_step(); 235 | }); 236 | }, 237 | 238 | 239 | ], callback); 240 | }); 241 | }, 242 | 243 | // Disables AP mode and reverts to wifi connection 244 | _enable_wifi_mode = function(connection_info, callback) { 245 | 246 | _is_wifi_enabled(function(error, result_ip) { 247 | if (error) return callback(error); 248 | 249 | if (result_ip) { 250 | console.log("\nWifi connection is enabled with IP: " + result_ip); 251 | return callback(null); 252 | } 253 | 254 | async.series([ 255 | 256 | 257 | //Add new network 258 | function update_wpa_supplicant(next_step) { 259 | write_template_to_file( 260 | "./assets/etc/wpa_supplicant/wpa_supplicant.conf.template", 261 | "/etc/wpa_supplicant/wpa_supplicant.conf", 262 | connection_info, next_step); 263 | }, 264 | 265 | function update_interfaces(next_step) { 266 | write_template_to_file( 267 | "./assets/etc/dhcpcd/dhcpcd.station.template", 268 | "/etc/dhcpcd.conf", 269 | connection_info, next_step); 270 | }, 271 | 272 | // Enable the interface in the dhcp server 273 | function update_dhcp_interface(next_step) { 274 | write_template_to_file( 275 | "./assets/etc/dnsmasq/dnsmasq.station.template", 276 | "/etc/dnsmasq.conf", 277 | connection_info, next_step); 278 | }, 279 | 280 | // Enable hostapd.conf file 281 | function update_hostapd_conf(next_step) { 282 | write_template_to_file( 283 | "./assets/etc/hostapd/hostapd.conf.station.template", 284 | "/etc/hostapd/hostapd.conf", 285 | connection_info, next_step); 286 | }, 287 | 288 | function restart_dnsmasq_service(next_step) { 289 | exec("sudo systemctl stop dnsmasq", function(error, stdout, stderr) { 290 | if (!error) console.log("... dnsmasq server stopped!"); 291 | else console.log("... dnsmasq server failed! - " + stdout); 292 | next_step(); 293 | }); 294 | }, 295 | 296 | function restart_hostapd_service(next_step) { 297 | exec("sudo systemctl stop hostapd", function(error, stdout, stderr) { 298 | //console.log(stdout); 299 | if (!error) console.log("... hostapd stopped!"); 300 | next_step(); 301 | }); 302 | }, 303 | 304 | function restart_dhcp_service(next_step) { 305 | exec("sudo systemctl restart dhcpcd", function(error, stdout, stderr) { 306 | if (!error) console.log("... dhcpcd server restarted!"); 307 | else console.log("... dhcpcd server failed! - " + stdout); 308 | next_step(); 309 | }); 310 | }, 311 | 312 | function reboot_network_interfaces(next_step) { 313 | _reboot_wireless_network(config.wifi_interface, next_step); 314 | }, 315 | 316 | ], callback); 317 | }); 318 | 319 | }; 320 | 321 | return { 322 | get_wifi_info: _get_wifi_info, 323 | reboot_wireless_network: _reboot_wireless_network, 324 | 325 | is_wifi_enabled: _is_wifi_enabled, 326 | is_wifi_enabled_sync: _is_wifi_enabled_sync, 327 | 328 | is_ap_enabled: _is_ap_enabled, 329 | is_ap_enabled_sync: _is_ap_enabled_sync, 330 | 331 | enable_ap_mode: _enable_ap_mode, 332 | enable_wifi_mode: _enable_wifi_mode, 333 | }; 334 | } 335 | --------------------------------------------------------------------------------