├── 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 |
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 |
--------------------------------------------------------------------------------