Merge pull request #67 from dacarley/1

2 - New "Network" page
This commit is contained in:
David A. Carley
2022-08-23 15:32:43 -07:00
committed by GitHub
56 changed files with 16792 additions and 876 deletions

3
.gitignore vendored
View File

@@ -20,9 +20,8 @@ __pycache__
*.zip *.zip
/rpi-share /rpi-share
/rpi-root /rpi-root
/package-lock.json
/src/bbserial/linux-rpi-raspberrypi-kernel* /src/bbserial/linux-rpi-raspberrypi-kernel*
/src/bbserial/raspberrypi-kernel* /src/bbserial/raspberrypi-kernel*
.vscode
*.elf *.elf
*.hex *.hex

View File

@@ -1,5 +1,6 @@
include package.json README.md include package.json README.md requirements.txt
graft scripts graft scripts
graft python-packages
graft src/py/bbctrl/http graft src/py/bbctrl/http
graft src/py/camotics graft src/py/camotics
include src/avr/bbctrl-avr-firmware.hex include src/avr/bbctrl-avr-firmware.hex

View File

@@ -27,8 +27,7 @@ BETA_VERSION := $(VERSION)-rc$(shell ./scripts/next-rc)
BETA_PKG_NAME := bbctrl-$(BETA_VERSION) BETA_PKG_NAME := bbctrl-$(BETA_VERSION)
SUBPROJECTS := avr boot pwr jig SUBPROJECTS := avr boot pwr jig
WATCH := src/pug src/pug/templates src/stylus src/js src/resources Makefile WATCH := src/pug src/pug/templates src/stylus src/js src/resources src/svelte-components src/static Makefile
WATCH += src/static
ifndef HOST ifndef HOST
HOST=onefinity HOST=onefinity
@@ -100,6 +99,9 @@ node_modules: package.json
$(TARGET_DIR)/%: src/resources/% $(TARGET_DIR)/%: src/resources/%
install -D $< $@ install -D $< $@
src/svelte-components/dist/%:
cd src/svelte-components && rm -rf dist && npm run build
$(TARGET_DIR)/index.html: build/templates.pug $(TARGET_DIR)/index.html: build/templates.pug
$(TARGET_DIR)/index.html: $(wildcard src/static/js/*) $(TARGET_DIR)/index.html: $(wildcard src/static/js/*)
$(TARGET_DIR)/index.html: $(wildcard src/static/css/*) $(TARGET_DIR)/index.html: $(wildcard src/static/css/*)
@@ -108,9 +110,16 @@ $(TARGET_DIR)/index.html: $(wildcard src/js/*)
$(TARGET_DIR)/index.html: $(wildcard src/stylus/*) $(TARGET_DIR)/index.html: $(wildcard src/stylus/*)
$(TARGET_DIR)/index.html: src/resources/config-template.json $(TARGET_DIR)/index.html: src/resources/config-template.json
$(TARGET_DIR)/index.html: $(wildcard src/resources/onefinity*defaults.json) $(TARGET_DIR)/index.html: $(wildcard src/resources/onefinity*defaults.json)
$(TARGET_DIR)/index.html: $(wildcard src/svelte-components/dist/*)
$(TARGET_DIR)/%.html: src/pug/%.pug node_modules FORCE:
@mkdir -p $(shell dirname $@)
$(TARGET_DIR)/%.html: src/pug/%.pug node_modules FORCE
cd src/svelte-components && rm -rf dist && npm run build
@mkdir -p $(TARGET_DIR)/svelte-components
cp src/svelte-components/dist/* $(TARGET_DIR)/svelte-components/
@mkdir -p $(TARGET_DIR)
$(PUG) -O pug-opts.js -P $< -o $(TARGET_DIR) || (rm -f $@; exit 1) $(PUG) -O pug-opts.js -P $< -o $(TARGET_DIR) || (rm -f $@; exit 1)
pylint: pylint:

5580
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{ {
"name": "bbctrl", "name": "bbctrl",
"version": "1.0.9", "version": "1.0.10b1",
"homepage": "https://onefinitycnc.com/", "homepage": "https://onefinitycnc.com/",
"repository": "https://github.com/OneFinityCNC/onefinity", "repository": "https://github.com/OneFinityCNC/onefinity",
"license": "GPL-3.0+", "license": "GPL-3.0+",
"dependencies": { "dependencies": {
"browserify": "", "browserify": "^17.0.0",
"jshint": "", "jshint": "^2.13.4",
"jstransformer-escape-html": "", "jstransformer-escape-html": "^1.1.0",
"jstransformer-stylus": "", "jstransformer-scss": "^2.0.0",
"jstransformer-stylus": "^1.5.0",
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"pug-cli": "" "pug-cli": "^1.0.0-alpha6"
} }
} }

Binary file not shown.

Binary file not shown.

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
watchdog==0.10.6

View File

@@ -18,20 +18,23 @@ function query_config() {
if [ -e $WLAN0_CFG ]; then if [ -e $WLAN0_CFG ]; then
SSID=$(grep wpa-ssid $WLAN0_CFG | SSID=$(grep wpa-ssid $WLAN0_CFG |
sed 's/^[[:space:]]*wpa-ssid "\([^"]*\)"/\1/') sed 's/^[[:space:]]*wpa-ssid "\([^"]*\)"/\1/')
echo "{\"ssid\": \"$SSID\", \"mode\": \"client\"}" # echo "{\"ssid\": \"$SSID\", \"mode\": \"client\"}"
echo "{\"ssid\": \"$SSID\"}"
else else
if [ -e $HOSTAPD_CFG -a -e /etc/default/hostapd ]; then # if [ -e $HOSTAPD_CFG -a -e /etc/default/hostapd ]; then
SSID=$(grep ^ssid= $HOSTAPD_CFG | sed 's/^ssid=\(.*\)$/\1/') # SSID=$(grep ^ssid= $HOSTAPD_CFG | sed 's/^ssid=\(.*\)$/\1/')
CHANNEL=$(grep ^channel= $HOSTAPD_CFG | # CHANNEL=$(grep ^channel= $HOSTAPD_CFG |
sed 's/^channel=\(.*\)$/\1/') # sed 's/^channel=\(.*\)$/\1/')
echo -n "{\"ssid\": \"$SSID\", " # echo -n "{\"ssid\": \"$SSID\", "
echo "\"channel\": $CHANNEL, \"mode\": \"ap\"}" # echo "\"channel\": $CHANNEL, \"mode\": \"ap\"}"
else # else
echo "{\"mode\": \"disabled\"}" # echo "{\"mode\": \"disabled\"}"
fi # fi
echo "{}"
fi fi
} }

View File

@@ -0,0 +1,3 @@
#!/bin/bash -ex
pip3 download -d python-packages -r requirements.txt

0
scripts/edit-boot-config Normal file → Executable file
View File

0
scripts/edit-config Normal file → Executable file
View File

View File

@@ -121,19 +121,21 @@ fi
# Install rc.local # Install rc.local
cp scripts/rc.local /etc/ cp scripts/rc.local /etc/
# Ensure that the watchdog python library is installed
pip3 list --format=columns | grep watchdog >/dev/null
if [ $? -ne 0 ]; then
pip3 install scripts/pathtools-0.1.2.tar.gz scripts/watchdog-v0.10.6.tar.gz
fi
# Install bbctrl # Install bbctrl
if $UPDATE_PY; then if $UPDATE_PY; then
service bbctrl stop
rm -rf /usr/local/lib/python*/dist-packages/bbctrl-* rm -rf /usr/local/lib/python*/dist-packages/bbctrl-*
# Ensure python dependencies are installed
pip3 install --no-index --find-links python-packages -r requirements.txt
./setup.py install --force ./setup.py install --force
service bbctrl restart
HTTP_DIR=$(find /usr/local/lib/ -type d -name "http") HTTP_DIR=$(find /usr/local/lib/ -type d -name "http")
chmod 777 $HTTP_DIR chmod 777 $HTTP_DIR
service bbctrl restart
fi fi
# Expand the file system if necessary # Expand the file system if necessary

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,3 @@
hostinfo.sh &
ratpoison & ratpoison &
xset -dpms xset -dpms

View File

@@ -5,26 +5,31 @@ import json
pkg = json.load(open('package.json', 'r')) pkg = json.load(open('package.json', 'r'))
setup( setup(
name = pkg['name'], name=pkg['name'],
version = pkg['version'], version=pkg['version'],
description = 'Buildbotics Machine Controller', description='Buildbotics Machine Controller',
long_description = open('README.md', 'rt').read(), long_description=open('README.md', 'rt').read(),
author = 'Joseph Coffland', author='Joseph Coffland',
author_email = 'joseph@buildbotics.org', author_email='joseph@buildbotics.org',
platforms = ['any'], platforms=['any'],
license = pkg['license'], license=pkg['license'],
url = pkg['homepage'], url=pkg['homepage'],
package_dir = {'': 'src/py'}, package_dir={'': 'src/py'},
packages = ['bbctrl', 'inevent', 'lcd', 'camotics'], packages=[
include_package_data = True, 'bbctrl',
entry_points = { 'inevent',
'lcd',
'camotics',
'iw_parse'
],
include_package_data=True,
entry_points={
'console_scripts': [ 'console_scripts': [
'bbctrl = bbctrl:run' 'bbctrl = bbctrl:run'
] ]
}, },
scripts = [ scripts=[
'scripts/update-bbctrl', 'scripts/update-bbctrl',
'scripts/upgrade-bbctrl', 'scripts/upgrade-bbctrl',
'scripts/sethostname', 'scripts/sethostname',
@@ -34,7 +39,14 @@ setup(
'scripts/edit-config', 'scripts/edit-config',
'scripts/edit-boot-config', 'scripts/edit-boot-config',
'scripts/browser', 'scripts/browser',
], ],
install_requires = 'tornado sockjs-tornado pyserial pyudev smbus2 watchdog'.split(), install_requires=[
zip_safe = False, 'tornado',
) 'sockjs-tornado',
'pyserial',
'pyudev',
'smbus2',
'watchdog'
],
zip_safe=False,
)

View File

@@ -50,6 +50,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdbool.h> #include <stdbool.h>
#include <util/delay.h>
// For emu // For emu
@@ -61,13 +62,18 @@ int main(int argc, char *argv[]) {
__argc = argc; __argc = argc;
__argv = argv; __argv = argv;
wdt_enable(WDTO_250MS);
// Init // Init
cli(); // disable interrupts cli(); // disable interrupts
emu_init(); // Init emulator emu_init(); // Init emulator
hw_init(); // hardware setup - must be first hw_init(); // hardware setup - must be first
_delay_ms(5000); //2 seconds to charge capacitor banks, 1 second for shunt test, 2 seconds to recharge banks
wdt_enable(WDTO_250MS);
outputs_init(); // output pins outputs_init(); // output pins
switch_init(); // switches switch_init(); // switches
estop_init(); // emergency stop handler estop_init(); // emergency stop handler

View File

@@ -49,18 +49,11 @@ module.exports = {
configRestored: false, configRestored: false,
confirmReset: false, confirmReset: false,
configReset: false, configReset: false,
latest: '',
autoCheckUpgrade: true, autoCheckUpgrade: true,
reset_variant: '' reset_variant: ''
} }
}, },
events: {
latest_version: function (version) {
this.latest = version
}
},
ready: function () { ready: function () {
this.autoCheckUpgrade = this.config.admin['auto-check-upgrade'] this.autoCheckUpgrade = this.config.admin['auto-check-upgrade']
}, },

View File

@@ -1,177 +1,14 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
var api = require('./api');
module.exports = { module.exports = {
template: '#admin-network-view-template', template: "#admin-network-view-template",
props: ['config', 'state'],
attached: function () {
data: function () { this.svelteComponent = SvelteComponents.create(
return { "AdminNetworkView",
hostnameSet: false, document.getElementById("svelte-root")
usernameSet: false, );
passwordSet: false,
redirectTimeout: 0,
hostname: '',
username: '',
current: '',
password: '',
password2: '',
wifi_mode: 'client',
wifi_ssid: '',
wifi_ch: undefined,
wifi_pass: '',
wifiConfirm: false,
rebooting: false
}
}, },
detached: function() {
ready: function () { this.svelteComponent.$destroy();
api.get('hostname').done(function (hostname) {
this.hostname = hostname;
}.bind(this));
api.get('remote/username').done(function (username) {
this.username = username;
}.bind(this));
api.get('wifi').done(function (config) {
this.wifi_mode = config.mode;
this.wifi_ssid = config.ssid;
this.wifi_ch = config.channel;
}.bind(this));
},
methods: {
redirect: function (hostname) {
if (0 < this.redirectTimeout) {
this.redirectTimeout -= 1;
setTimeout(function () {this.redirect(hostname)}.bind(this), 1000);
} else location.hostname = hostname;
},
set_hostname: function () {
api.put('hostname', {hostname: this.hostname}).done(function () {
this.redirectTimeout = 45;
this.hostnameSet = true;
api.put('reboot').always(function () {
if (String(location.hostname) == 'localhost') return;
var hostname = this.hostname;
if (String(location.hostname).endsWith('.local'))
hostname += '.local'
this.$dispatch('hostname-changed', hostname);
this.redirect(hostname);
}.bind(this));
}.bind(this)).fail(function (error) {
api.alert('Set hostname failed', error);
})
},
set_username: function () {
api.put('remote/username', {username: this.username}).done(function () {
this.usernameSet = true;
}.bind(this)).fail(function (error) {
api.alert('Set username failed', error);
})
},
set_password: function () {
if (this.password != this.password2) {
alert('Passwords to not match');
return;
}
if (this.password.length < 6) {
alert('Password too short');
return;
}
api.put('remote/password', {
current: this.current,
password: this.password
}).done(function () {
this.passwordSet = true;
}.bind(this)).fail(function (error) {
api.alert('Set password failed', error);
})
},
config_wifi: function () {
this.wifiConfirm = false;
if (!this.wifi_ssid.length) {
alert('SSID not set');
return;
}
if (32 < this.wifi_ssid.length) {
alert('SSID longer than 32 characters');
return;
}
if (this.wifi_pass.length && this.wifi_pass.length < 8) {
alert('WiFi password shorter than 8 characters');
return;
}
if (128 < this.wifi_pass.length) {
alert('WiFi password longer than 128 characters');
return;
}
this.rebooting = true;
var config = {
mode: this.wifi_mode,
channel: this.wifi_ch,
ssid: this.wifi_ssid,
pass: this.wifi_pass
}
api.put('wifi', config).fail(function (error) {
api.alert('Failed to configure WiFi', error);
this.rebooting = false;
}.bind(this))
}
} }
} };

View File

@@ -1,30 +1,3 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict' 'use strict'

View File

@@ -1,36 +1,11 @@
/******************************************************************************\ "use strict";
This file is part of the Buildbotics firmware. const api = require("./api");
const cookie = require("./cookie")("bbctrl-");
const Sock = require("./sock");
const omit = require("lodash.omit");
Copyright (c) 2015 - 2018, Buildbotics LLC SvelteComponents.initNetworkInfo();
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
const api = require('./api');
const cookie = require('./cookie')('bbctrl-');
const Sock = require('./sock');
const omit = require('lodash.omit');
function is_newer_version(current, latest) { function is_newer_version(current, latest) {
const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/; const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/;
@@ -47,7 +22,8 @@ function is_newer_version(current, latest) {
const patch = latestParts[3] - currentParts[3]; const patch = latestParts[3] - currentParts[3];
// If current is a pre-release, and latest is a release // If current is a pre-release, and latest is a release
const betaToRelease = latestParts[4].length === 0 && currentParts[4].length > 0; const betaToRelease =
latestParts[4].length === 0 && currentParts[4].length > 0;
switch (true) { switch (true) {
case major > 0: case major > 0:
@@ -61,15 +37,17 @@ function is_newer_version(current, latest) {
} }
} }
function is_object(o) { return o !== null && typeof o == 'object' } function is_object(o) {
function is_array(o) { return Array.isArray(o) } return o !== null && typeof o == "object";
}
function update_array(dst, src) { function is_array(o) {
while (dst.length) dst.pop() return Array.isArray(o);
for (var i = 0; i < src.length; i++)
Vue.set(dst, i, src[i]);
} }
function update_array(dst, src) {
while (dst.length) dst.pop();
for (var i = 0; i < src.length; i++) Vue.set(dst, i, src[i]);
}
function update_object(dst, src, remove) { function update_object(dst, src, remove) {
var props, index, key, value; var props, index, key, value;
@@ -79,8 +57,7 @@ function update_object(dst, src, remove) {
for (index in props) { for (index in props) {
key = props[index]; key = props[index];
if (!src.hasOwnProperty(key)) if (!src.hasOwnProperty(key)) Vue.delete(dst, key);
Vue.delete(dst, key);
} }
} }
@@ -91,111 +68,100 @@ function update_object(dst, src, remove) {
if (is_array(value) && dst.hasOwnProperty(key) && is_array(dst[key])) if (is_array(value) && dst.hasOwnProperty(key) && is_array(dst[key]))
update_array(dst[key], value); update_array(dst[key], value);
else if (is_object(value) && dst.hasOwnProperty(key) && is_object(dst[key])) else if (is_object(value) && dst.hasOwnProperty(key) && is_object(dst[key]))
update_object(dst[key], value, remove); update_object(dst[key], value, remove);
else Vue.set(dst, key, value); else Vue.set(dst, key, value);
} }
} }
module.exports = new Vue({ module.exports = new Vue({
el: 'body', el: "body",
data: function () { data: function () {
return { return {
status: 'connecting', status: "connecting",
currentView: 'loading', currentView: "loading",
index: -1, index: -1,
modified: false, modified: false,
template: require('../resources/config-template.json'), template: require("../resources/config-template.json"),
config: { config: {
settings: { units: 'METRIC' }, settings: { units: "METRIC" },
motors: [{}, {}, {}, {}], motors: [{}, {}, {}, {}],
version: '<loading>', version: "<loading>",
full_version: '<loading>' full_version: "<loading>",
}, },
state: { state: {
messages: [], messages: [],
probing_active: false, probing_active: false,
wait_for_probing_complete: false, wait_for_probing_complete: false,
show_probe_complete_modal: false, show_probe_complete_modal: false,
show_probe_failed_modal: false show_probe_failed_modal: false,
}, },
video_size: cookie.get('video-size', 'small'), video_size: cookie.get("video-size", "small"),
crosshair: cookie.get('crosshair', 'false') != 'false', crosshair: cookie.get("crosshair", "false") != "false",
errorTimeout: 30, errorTimeout: 30,
errorTimeoutStart: 0, errorTimeoutStart: 0,
errorShow: false, errorShow: false,
errorMessage: '', errorMessage: "",
confirmUpgrade: false, confirmUpgrade: false,
confirmUpload: false, confirmUpload: false,
firmwareUpgrading: false, firmwareUpgrading: false,
checkedUpgrade: false, checkedUpgrade: false,
firmwareName: '', firmwareName: "",
latestVersion: '', latestVersion: "",
ipAddress: '0.0.0.0',
wifiSSID: '',
confirmShutdown: false, confirmShutdown: false,
diskSpace: '' };
}
}, },
components: { components: {
'estop': { template: '#estop-template' }, estop: { template: "#estop-template" },
'loading-view': { template: '<h1>Loading...</h1>' }, "loading-view": { template: "<h1>Loading...</h1>" },
'control-view': require('./control-view'), "control-view": require("./control-view"),
'settings-view': require('./settings-view'), "settings-view": require("./settings-view"),
'motor-view': require('./motor-view'), "motor-view": require("./motor-view"),
'tool-view': require('./tool-view'), "tool-view": require("./tool-view"),
'io-view': require('./io-view'), "io-view": require("./io-view"),
'admin-general-view': require('./admin-general-view'), "admin-general-view": require("./admin-general-view"),
'admin-network-view': require('./admin-network-view'), "admin-network-view": require("./admin-network-view"),
'help-view': { template: '#help-view-template' }, "help-view": { template: "#help-view-template" },
'cheat-sheet-view': { "cheat-sheet-view": {
template: '#cheat-sheet-view-template', template: "#cheat-sheet-view-template",
data: function () { return { showUnimplemented: false } } data: function () {
} return { showUnimplemented: false };
},
},
}, },
events: { events: {
'config-changed': function () { "config-changed": function () {
this.modified = true; this.modified = true;
}, },
'hostname-changed': function (hostname) {
this.hostname = hostname
},
send: function (msg) { send: function (msg) {
if (this.status == 'connected') { if (this.status == "connected") {
console.debug('>', msg); console.debug(">", msg);
this.sock.send(msg); this.sock.send(msg);
} }
}, },
connected: function () { connected: function () {
this.update() this.update();
}, },
update: function () { update: function () {
this.update() this.update();
}, },
check: function () { check: async function () {
this.latestVersion = ''; try {
const response = await fetch("https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt", {
cache: "no-cache"
});
$.ajax({ this.latestVersion = (await response.text()).trim();
type: 'GET', } catch (err) {
url: 'https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt', this.latestVersion = "";
data: { hid: this.state.hid }, }
cache: false
}).done(function (data) {
this.latestVersion = data;
this.$broadcast('latest_version', data);
}.bind(this))
}, },
upgrade: function () { upgrade: function () {
@@ -221,7 +187,7 @@ module.exports = new Vue({
// Popup error dialog // Popup error dialog
this.errorShow = true; this.errorShow = true;
this.errorMessage = msg.msg; this.errorMessage = msg.msg;
} },
}, },
computed: { computed: {
@@ -236,17 +202,17 @@ module.exports = new Vue({
} }
return msgs; return msgs;
} },
}, },
ready: function () { ready: function () {
$(window).on('hashchange', this.parse_hash); $(window).on("hashchange", this.parse_hash);
this.connect(); this.connect();
}, },
methods: { methods: {
metric: function () { metric: function () {
return this.config.settings.units != 'IMPERIAL' return this.config.settings.units != "IMPERIAL";
}, },
block_error_dialog: function () { block_error_dialog: function () {
@@ -255,30 +221,30 @@ module.exports = new Vue({
}, },
toggle_video: function (e) { toggle_video: function (e) {
if (this.video_size == 'small') this.video_size = 'large'; if (this.video_size == "small") this.video_size = "large";
else if (this.video_size == 'large') this.video_size = 'small'; else if (this.video_size == "large") this.video_size = "small";
cookie.set('video-size', this.video_size); cookie.set("video-size", this.video_size);
}, },
toggle_crosshair: function (e) { toggle_crosshair: function (e) {
e.preventDefault(); e.preventDefault();
this.crosshair = !this.crosshair; this.crosshair = !this.crosshair;
cookie.set('crosshair', this.crosshair); cookie.set("crosshair", this.crosshair);
}, },
estop: function () { estop: function () {
if (this.state.xx == 'ESTOPPED') api.put('clear'); if (this.state.xx == "ESTOPPED") api.put("clear");
else api.put('estop'); else api.put("estop");
}, },
upgrade_confirmed: async function () { upgrade_confirmed: async function () {
this.confirmUpgrade = false; this.confirmUpgrade = false;
try { try {
await api.put('upgrade'); await api.put("upgrade");
this.firmwareUpgrading = true; this.firmwareUpgrading = true;
} catch (err) { } catch (err) {
api.alert('Error during upgrade.'); api.alert("Error during upgrade.");
console.error("Error during upgrade", err); console.error("Error during upgrade", err);
} }
}, },
@@ -287,22 +253,27 @@ module.exports = new Vue({
this.confirmUpload = false; this.confirmUpload = false;
const form = new FormData(); const form = new FormData();
form.append('firmware', this.firmware); form.append("firmware", this.firmware);
$.ajax({ $.ajax({
url: '/api/firmware/update', url: "/api/firmware/update",
type: 'PUT', type: "PUT",
data: form, data: form,
cache: false, cache: false,
contentType: false, contentType: false,
processData: false processData: false,
})
}).success(function () { .success(
this.firmwareUpgrading = true; function () {
}.bind(this)).error(function (err) { this.firmwareUpgrading = true;
api.alert('Firmware update failed'); }.bind(this)
console.error('Firmware update failed', err); )
}.bind(this)) .error(
function (err) {
api.alert("Firmware update failed");
console.error("Firmware update failed", err);
}.bind(this)
);
}, },
show_upgrade: function () { show_upgrade: function () {
@@ -310,97 +281,58 @@ module.exports = new Vue({
return is_newer_version(this.config.version, this.latestVersion); return is_newer_version(this.config.version, this.latestVersion);
}, },
update: function () { update: async function () {
api.get('config/load').done(function (config) { const config = await api.get("config/load");
update_object(this.config, config, true);
this.parse_hash();
if (!this.checkedUpgrade) { update_object(this.config, config, true);
this.checkedUpgrade = true; this.parse_hash();
var check = this.config.admin['auto-check-upgrade']; if (!this.checkedUpgrade) {
if (typeof check == 'undefined' || check) this.checkedUpgrade = true;
this.$emit('check');
}
this.check_ip_address(); var check = this.config.admin["auto-check-upgrade"];
this.check_ssid(); if (typeof check == "undefined" || check) this.$emit("check");
}.bind(this)) }
},
check_ip_address: function () {
$.ajax({
type: 'GET',
url: 'hostinfo.txt',
data: { hid: this.state.hid },
cache: false
}).done(function (data) {
console.debug('>', data);
this.ipAddress = 'IP:' + data;
this.$broadcast('ipAddress', data);
}.bind(this))
},
check_ssid: function () {
$.ajax({
type: 'GET',
url: 'ssidinfo.txt',
data: { hid: this.state.hid },
cache: false
}).done(function (data) {
console.debug('>', data);
this.wifiSSID = 'SSID:' + data;
this.$broadcast('wifiSSID', data);
}.bind(this))
},
get_ip_address: function () {
console.debug('get_ip>', this.ipAddress);
return this.ipAddress;
},
get_ssid: function () {
console.debug('get_ssid>', this.wifiSSID);
return this.wifiSSID;
}, },
shutdown: function () { shutdown: function () {
this.confirmShutdown = false; this.confirmShutdown = false;
api.put('shutdown'); api.put("shutdown");
}, },
reboot: function () { reboot: function () {
this.confirmShutdown = false; this.confirmShutdown = false;
api.put('reboot'); api.put("reboot");
}, },
connect: function () { connect: function () {
this.sock = new Sock(`//${location.host}/sockjs`); this.sock = new Sock(`//${location.host}/sockjs`);
this.sock.onmessage = (e) => { this.sock.onmessage = (e) => {
if (typeof e.data != 'object') { if (typeof e.data != "object") {
return; return;
} }
if ('log' in e.data) { if ("log" in e.data) {
if (e.data.log.msg === "Switch not found") { if (e.data.log.msg === "Switch not found") {
this.$broadcast('probing_failed'); this.$broadcast("probing_failed");
} else { } else {
this.$broadcast('log', e.data.log); this.$broadcast("log", e.data.log);
} }
delete e.data.log; delete e.data.log;
} }
// Check for session ID change on controller // Check for session ID change on controller
if ('sid' in e.data) { if ("sid" in e.data) {
if (typeof this.sid == 'undefined') { if (typeof this.sid == "undefined") {
this.sid = e.data.sid; this.sid = e.data.sid;
} else if (this.sid != e.data.sid) { } else if (this.sid != e.data.sid) {
if (typeof this.hostname !== 'undefined' && location.hostname !== 'localhost') { if (
typeof this.hostname !== "undefined" &&
location.hostname !== "localhost"
) {
location.hostname = this.hostname; location.hostname = this.hostname;
} }
@@ -412,15 +344,15 @@ module.exports = new Vue({
const debugStateChanges = false; const debugStateChanges = false;
if (debugStateChanges) { if (debugStateChanges) {
const data = omit(e.data, [ const data = omit(e.data, [
'vdd', "vdd",
'vin', "vin",
'vout', "vout",
'motor', "motor",
'temp', "temp",
'heartbeat', "heartbeat",
'load1', "load1",
'load2', "load2",
'rpi_temp' "rpi_temp",
]); ]);
if (Object.keys(data).length > 0) { if (Object.keys(data).length > 0) {
console.log(JSON.stringify(data, null, 4)); console.log(JSON.stringify(data, null, 4));
@@ -433,24 +365,24 @@ module.exports = new Vue({
Vue.set(this.state, "saw_probe_connected", true); Vue.set(this.state, "saw_probe_connected", true);
} }
if (this.state.cycle === 'idle') { if (this.state.cycle === "idle") {
if (this.state.wait_for_probing_complete) { if (this.state.wait_for_probing_complete) {
Vue.set(this.state, "wait_for_probing_complete", false); Vue.set(this.state, "wait_for_probing_complete", false);
this.$broadcast("probing_complete"); this.$broadcast("probing_complete");
} }
} }
this.$broadcast('update'); this.$broadcast("update");
}; };
this.sock.onopen = () => { this.sock.onopen = () => {
this.status = 'connected'; this.status = "connected";
this.$emit(this.status); this.$emit(this.status);
this.$broadcast(this.status); this.$broadcast(this.status);
}; };
this.sock.onclose = () => { this.sock.onclose = () => {
this.status = 'disconnected'; this.status = "disconnected";
this.$emit(this.status); this.$emit(this.status);
this.$broadcast(this.status); this.$broadcast(this.status);
}; };
@@ -460,11 +392,11 @@ module.exports = new Vue({
var hash = location.hash.substr(1); var hash = location.hash.substr(1);
if (!hash.trim().length) { if (!hash.trim().length) {
location.hash = 'control'; location.hash = "control";
return; return;
} }
var parts = hash.split(':'); var parts = hash.split(":");
if (parts.length == 2) this.index = parts[1]; if (parts.length == 2) this.index = parts[1];
@@ -472,35 +404,43 @@ module.exports = new Vue({
}, },
save: function () { save: function () {
const selected_tool = this.config.tool['selected-tool']; const selected_tool = this.config.tool["selected-tool"];
const saveModbus = selected_tool !== "pwm" && const saveModbus =
selected_tool !== "pwm" &&
selected_tool !== "laser" && selected_tool !== "laser" &&
selected_tool !== "router"; selected_tool !== "router";
const settings = { const settings = {
['tool']: { ...this.config.tool }, ["tool"]: { ...this.config.tool },
['pwm-spindle']: { ...this.config['pwm-spindle'] }, ["pwm-spindle"]: { ...this.config["pwm-spindle"] },
['modbus-spindle']: saveModbus ? { ...this.config['modbus-spindle'] } : undefined ["modbus-spindle"]: saveModbus
} ? { ...this.config["modbus-spindle"] }
delete settings.tool['tool-type']; : undefined,
};
delete settings.tool["tool-type"];
this.config['selected-tool-settings'][selected_tool] = settings; this.config["selected-tool-settings"][selected_tool] = settings;
api.put('config/save', this.config).done(function (data) { api
this.modified = false; .put("config/save", this.config)
}.bind(this)).fail(function (error) { .done(
api.alert('Save failed', error); function (data) {
}); this.modified = false;
}.bind(this)
)
.fail(function (error) {
api.alert("Save failed", error);
});
}, },
close_messages: function (action) { close_messages: function (action) {
if (action == 'stop') api.put('stop'); if (action == "stop") api.put("stop");
if (action == 'continue') api.put('unpause'); if (action == "continue") api.put("unpause");
// Acknowledge messages // Acknowledge messages
if (this.state.messages.length) { if (this.state.messages.length) {
var id = this.state.messages.slice(-1)[0].id var id = this.state.messages.slice(-1)[0].id;
api.put('message/' + id + '/ack'); api.put("message/" + id + "/ack");
} }
} },
} },
}) });

View File

@@ -1,35 +1,9 @@
//-/////////////////////////////////////////////////////////////////////////////
//- //
//- This file is part of the Buildbotics firmware. //
//- //
//- Copyright (c) 2015 - 2018, Buildbotics LLC //
//- All rights reserved. //
//- //
//- This file ("the software") is free software: you can redistribute it //
//- and/or modify it under the terms of the GNU General Public License, //
//- version 2 as published by the Free Software Foundation. You should //
//- have received a copy of the GNU General Public License, version 2 //
//- along with the software. If not, see <http://www.gnu.org/licenses/>. //
//- //
//- The software is distributed in the hope that it will be useful, but //
//- WITHOUT ANY WARRANTY; without even the implied warranty of //
//- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU //
//- Lesser General Public License for more details. //
//- //
//- You should have received a copy of the GNU Lesser General Public //
//- License along with the software. If not, see //
//- <http://www.gnu.org/licenses/>. //
//- //
//- For information regarding this software email: //
//- "Joseph Coffland" <joseph@buildbotics.com> //
//- //
//-/////////////////////////////////////////////////////////////////////////////
doctype html doctype html
html(lang="en") html(lang="en")
head head
meta(charset="utf-8") meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(name="viewport", content="width=device-width, initial-scale=1.0")
link(rel="preload" href="/fonts/material-symbols-outlined.woff2" as="font" type="font/woff2" crossorigin)
title Onefinity CNC - Web interface title Onefinity CNC - Web interface
@@ -39,12 +13,16 @@ html(lang="en")
style: include ../static/css/font-awesome.min.css style: include ../static/css/font-awesome.min.css
style: include ../static/css/Audiowide.css style: include ../static/css/Audiowide.css
style: include ../static/css/clusterize.css style: include ../static/css/clusterize.css
style: include ../svelte-components/node_modules/svelte-material-ui/bare.css
style: include ../../build/http/svelte-components/smui.css
style: include ../../build/http/svelte-components/style.css
style: include ../../build/http/svelte-components/material-symbols-outlined.css
style: include:stylus ../stylus/style.styl style: include:stylus ../stylus/style.styl
body(v-cloak) body(v-cloak)
#overlay(v-if="status != 'connected'") #overlay(v-if="status != 'connected'")
span {{status}} span {{status}}
#layout #layout
a#menuLink.menu-link(href="#menu"): span a#menuLink.menu-link(href="#menu"): span
@@ -100,36 +78,34 @@ html(lang="en")
#main #main
.header .header
.header-content .brand
.banner img(src="/images/onefinity_logo.png")
img(src="/images/onefinity_logo.png") .version
.title | v{{config.full_version}}
.fa.fa-thermometer-full(class="error", a.upgrade-link(v-if="show_upgrade()", href="#admin-general")
v-if="80 <= state.rpi_temp", | Upgrade to v{{latestVersion}}
title="Raspberry Pi temperature too high.") .upgrade-attention(v-if="show_upgrade()")
.subtitle | !
| CNC Controller #[b {{state.demo ? 'Demo ' : ''}}]
| v{{config.full_version}}
a.upgrade-version(v-if="show_upgrade()", href="#admin-general")
| Upgrade to v{{latestVersion}}
.fa.fa-check(v-if="!show_upgrade() && latestVersion",
title="Firmware up to date" style="font-size: inherit")
.p {{get_ip_address()}} {{get_ssid()}}
.estop(:class="{active: state.es}") .pi-temp-warning
estop(@click="estop") .fa.fa-thermometer-full(class="error",
v-if="80 <= state.rpi_temp",
title="Raspberry Pi temperature too high.")
.video(title="Plug camera into USB.\n" + .whitespace
"Left click to toggle video size.\n" +
"Right click to toggle crosshair.", @click="toggle_video",
@contextmenu="toggle_crosshair", :class="video_size")
.crosshair(v-if="crosshair")
.vertical
.horizontal
.box
img(src="/api/video")
.clear .video(title="Plug camera into USB.\n" +
"Left click to toggle video size.\n" +
"Right click to toggle crosshair.", @click="toggle_video",
@contextmenu="toggle_crosshair", :class="video_size")
.crosshair(v-if="crosshair")
.vertical
.horizontal
.box
img(src="/api/video")
.estop(:class="{active: state.es}")
estop(@click="estop")
.content(class="{{currentView}}-view") .content(class="{{currentView}}-view")
component(:is="currentView + '-view'", :index="index", component(:is="currentView + '-view'", :index="index",
@@ -211,4 +187,5 @@ html(lang="en")
script: include ../static/js/clusterize.min.js script: include ../static/js/clusterize.min.js
script: include ../static/js/three.min.js script: include ../static/js/three.min.js
script: include:browserify ../js/main.js script: include:browserify ../js/main.js
script: include ../../build/http/svelte-components/index.js
script: include ../static/js/ui.js script: include ../static/js/ui.js

View File

@@ -1,130 +1,3 @@
//-/////////////////////////////////////////////////////////////////////////////
//- //
//- This file is part of the Buildbotics firmware. //
//- //
//- Copyright (c) 2015 - 2018, Buildbotics LLC //
//- All rights reserved. //
//- //
//- This file ("the software") is free software: you can redistribute it //
//- and/or modify it under the terms of the GNU General Public License, //
//- version 2 as published by the Free Software Foundation. You should //
//- have received a copy of the GNU General Public License, version 2 //
//- along with the software. If not, see <http://www.gnu.org/licenses/>. //
//- //
//- The software is distributed in the hope that it will be useful, but //
//- WITHOUT ANY WARRANTY; without even the implied warranty of //
//- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU //
//- Lesser General Public License for more details. //
//- //
//- You should have received a copy of the GNU Lesser General Public //
//- License along with the software. If not, see //
//- <http://www.gnu.org/licenses/>. //
//- //
//- For information regarding this software email: //
//- "Joseph Coffland" <joseph@buildbotics.com> //
//- //
//-/////////////////////////////////////////////////////////////////////////////
script#admin-network-view-template(type="text/x-template") script#admin-network-view-template(type="text/x-template")
#admin-network #admin-network
h2 Hostname #svelte-root
.pure-form.pure-form-aligned
.pure-control-group
label(for="hostname") Hostname
input(name="hostname", v-model="hostname", @keyup.enter="set_hostname")
button.pure-button.pure-button-primary(@click="set_hostname") Set
message(:show.sync="hostnameSet")
h3(slot="header") Hostname Set
div(slot="body")
p Hostname was successfuly set to #[strong {{hostname}}].
p Rebooting to apply changes.
p Redirecting to new hostname in {{redirectTimeout}} seconds.
div(slot="footer")
h2 Remote SSH User
.pure-form.pure-form-aligned
.pure-control-group
label(for="username") Username
input(name="username", v-model="username", @keyup.enter="set_username")
button.pure-button.pure-button-primary(@click="set_username") Set
.pure-form.pure-form-aligned
.pure-control-group
label(for="current") Current Password
input(name="current", v-model="current", type="password")
.pure-control-group
label(for="pass1") New Password
input(name="pass1", v-model="password", type="password")
.pure-control-group
label(for="pass2") New Password
input(name="pass2", v-model="password2", type="password")
button.pure-button.pure-button-primary(@click="set_password") Set
message(:show.sync="passwordSet")
h3(slot="header") Password Set
p(slot="body")
message(:show.sync="usernameSet")
h3(slot="header") Username Set
p(slot="body")
h2 Wifi Setup
.pure-form.pure-form-aligned
.pure-control-group
label(for="wifi_mode") Mode
select(name="wifi_mode", v-model="wifi_mode",
title="Select client or access point mode")
option(value="disabled") Disabled
option(value="client") Client
option(value="ap") Access Point
button.pure-button.pure-button-primary(@click="wifiConfirm = true",
v-if="wifi_mode == 'disabled'") Set
.pure-control-group(v-if="wifi_mode == 'ap'")
label(for="wifi_ch") Channel
select(name="wifi_ch", v-model="wifi_ch")
each ch in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
option(value=ch)= ch
.pure-control-group(v-if="wifi_mode != 'disabled'")
label(for="ssid") Network (SSID)
input(name="ssid", v-model="wifi_ssid")
.pure-control-group(v-if="wifi_mode != 'disabled'")
label(for="wifi_pass") Password
input(name="wifi_pass", v-model="wifi_pass", type="password")
button.pure-button.pure-button-primary(@click="wifiConfirm = true") Set
p(v-if="wifi_mode != 'disabled'").
WARNING: WiFi may be unreliable in an electrically noisy environment
such as a machine shop.
message.wifi-confirm(:show.sync="wifiConfirm")
h3(slot="header") Configure Wifi and reboot?
div(slot="body")
p
| After configuring the Wifi settings the controller will
| automatically reboot.
table
tr
th Mode
td &nbsp;{{wifi_mode}}
tr(v-if="wifi_mode == 'ap'")
th Channel
td &nbsp;{{wifi_ch}}
tr(v-if="wifi_mode != 'disabled'")
th SSID
td &nbsp;{{wifi_ssid}}
tr(v-if="wifi_mode != 'disabled'")
th Auth
td &nbsp;{{wifi_pass ? 'WPA2' : 'Open'}}
div(slot="footer")
button.pure-button(@click="wifiConfirm = false") Cancel
button.pure-button.button-success(@click="config_wifi") OK
message(:show.sync="rebooting")
h3(slot="header") Rebooting
p(slot="body") Please wait...
div(slot="footer")

View File

@@ -103,7 +103,7 @@ enum {
#define VOLTAGE_REF_R2 1000 #define VOLTAGE_REF_R2 1000
#define CURRENT_REF_R2 137 #define CURRENT_REF_R2 137
#define CURRENT_REF_MUL (100.0 * 2700 / CURRENT_REF_R2) // 2700 from datasheet #define CURRENT_REF_MUL (100.0 * 2700 / CURRENT_REF_R2) // 2700 from datasheet
#define CAP_PRECHARGE_PERIOD 50 // ms #define CAP_PRECHARGE_PERIOD 2000 // ms
#define REG_SCALE 100 #define REG_SCALE 100
#define AVG_SCALE 3 #define AVG_SCALE 3

View File

@@ -76,6 +76,7 @@ static volatile uint8_t motor_overload = 0;
static volatile float shunt_joules = 0; static volatile float shunt_joules = 0;
static volatile bool initialized = false; static volatile bool initialized = false;
static volatile float vnom = 0; static volatile float vnom = 0;
static volatile bool rev4 = false;
void delay(uint16_t ms) { void delay(uint16_t ms) {
@@ -161,6 +162,8 @@ static float get_reg(int reg) {
static void update_shunt() { static void update_shunt() {
if (!initialized) return; if (!initialized) return;
if(!rev4) return;
static float joules = SHUNT_JOULES; // Power disipation budget static float joules = SHUNT_JOULES; // Power disipation budget
@@ -175,6 +178,8 @@ static void update_shunt() {
static void update_shunt_power() { static void update_shunt_power() {
if (!initialized) return; if (!initialized) return;
if(!rev4) return;
float vout = get_reg(VOUT_REG); float vout = get_reg(VOUT_REG);
@@ -369,14 +374,14 @@ static void validate_input_voltage() {
static void charge_caps() { static void charge_caps() {
IO_PORT_SET(SHUNT_PIN); // Disable shunt (hi) IO_PORT_SET(SHUNT_PIN); // Disable shunt (hi)
delay(1000); delay(100);
IO_PORT_SET(PC2_PIN); //Enable pre-charge circuit IO_PORT_SET(PC2_PIN); //Enable pre-charge circuit
delay(CAP_PRECHARGE_PERIOD); //Wait for Vs caps to charge delay(CAP_PRECHARGE_PERIOD); //Wait for Vs caps to charge
IO_PORT_CLR(PC2_PIN); //Disable pre-charge circuit IO_PORT_CLR(PC2_PIN); //Disable pre-charge circuit
delay(1); //delay(100);
IO_PORT_SET(MOTOR_PIN); // Motor voltage on IO_PORT_SET(MOTOR_PIN); // Motor voltage on
delay(CAP_CHARGE_TIME); //delay(CAP_CHARGE_TIME);
} }
@@ -412,8 +417,15 @@ void init() {
IO_DDR_CLR(LOAD1_PIN); // Tri-state IO_DDR_CLR(LOAD1_PIN); // Tri-state
IO_DDR_CLR(LOAD2_PIN); // Tri-state IO_DDR_CLR(LOAD2_PIN); // Tri-state
IO_PUE_SET(PWR_RESET); // Pull up reset line IO_PUE_SET(PWR_RESET); // Pull up reset line
IO_PORT_CLR(SHUNT_PIN); // Enable shunt
IO_DDR_SET(SHUNT_PIN); // Output //Rev 4 PCBs have a pull-up on the shunt pin
rev4 = IO_PIN_GET(SHUNT_PIN);
if(rev4)
{
IO_PORT_CLR(SHUNT_PIN); // Enable shunt
IO_DDR_SET(SHUNT_PIN); // Output
}
IO_PORT_CLR(PC2_PIN); // Disable cap precharge circuit IO_PORT_CLR(PC2_PIN); // Disable cap precharge circuit
IO_DDR_SET(PC2_PIN); //Output IO_DDR_SET(PC2_PIN); //Output
@@ -484,7 +496,7 @@ int main() {
init(); init();
adc_conversion(); // Start ADC adc_conversion(); // Start ADC
validate_input_voltage(); validate_input_voltage();
shunt_test(); if(rev4) shunt_test();
charge_caps(); charge_caps();
validate_measurements(); validate_measurements();
initialized = true; initialized = true;

View File

@@ -1,30 +1,3 @@
################################################################################
# #
# This file is part of the Buildbotics firmware. #
# #
# Copyright (c) 2015 - 2018, Buildbotics LLC #
# All rights reserved. #
# #
# This file ("the software") is free software: you can redistribute it #
# and/or modify it under the terms of the GNU General Public License, #
# version 2 as published by the Free Software Foundation. You should #
# have received a copy of the GNU General Public License, version 2 #
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
# #
# The software is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with the software. If not, see #
# <http://www.gnu.org/licenses/>. #
# #
# For information regarding this software email: #
# "Joseph Coffland" <joseph@buildbotics.com> #
# #
################################################################################
import os import os
import json import json
import tornado import tornado
@@ -36,50 +9,26 @@ from tornado.web import HTTPError
from tornado import gen from tornado import gen
import bbctrl import bbctrl
import iw_parse
def call_get_output(cmd): def call_get_output(cmd):
p = subprocess.Popen(cmd, stdout = subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
s = p.communicate()[0].decode('utf-8') s = p.communicate()[0].decode('utf-8')
if p.returncode: raise HTTPError(400, 'Command failed') if p.returncode:
raise HTTPError(400, 'Command failed')
return s return s
def get_username():
return call_get_output(['getent', 'passwd', '1001']).split(':')[0]
def set_username(username):
if subprocess.call(['usermod', '-l', username, get_username()]):
raise HTTPError(400, 'Failed to set username to "%s"' % username)
def check_password(password):
# Get current password
s = call_get_output(['getent', 'shadow', get_username()])
current = s.split(':')[1].split('$')
# Check password type
if len(current) < 2 or current[1] != '1':
raise HTTPError(401, "Password invalid")
# Check current password
cmd = ['openssl', 'passwd', '-salt', current[2], '-1', password]
s = call_get_output(cmd).strip()
if s.split('$') != current: raise HTTPError(401, 'Wrong password')
class RebootHandler(bbctrl.APIHandler): class RebootHandler(bbctrl.APIHandler):
def put_ok(self): def put_ok(self):
self.get_ctrl().lcd.goodbye('Rebooting...') self.get_ctrl().lcd.goodbye('Rebooting...')
subprocess.Popen('reboot') subprocess.Popen('reboot')
class ShutdownHandler(bbctrl.APIHandler): class ShutdownHandler(bbctrl.APIHandler):
def put_ok(self): def put_ok(self):
subprocess.Popen(['shutdown','-h','now']) subprocess.Popen(['shutdown', '-h', 'now'])
class LogHandler(bbctrl.RequestHandler): class LogHandler(bbctrl.RequestHandler):
@@ -87,7 +36,6 @@ class LogHandler(bbctrl.RequestHandler):
with open(self.get_ctrl().log.get_path(), 'r') as f: with open(self.get_ctrl().log.get_path(), 'r') as f:
self.write(f.read()) self.write(f.read())
def set_default_headers(self): def set_default_headers(self):
fmt = socket.gethostname() + '-%Y%m%d.log' fmt = socket.gethostname() + '-%Y%m%d.log'
filename = datetime.date.today().strftime(fmt) filename = datetime.date.today().strftime(fmt)
@@ -102,14 +50,16 @@ class MessageAckHandler(bbctrl.APIHandler):
class BugReportHandler(bbctrl.RequestHandler): class BugReportHandler(bbctrl.RequestHandler):
def get(self): def get(self):
import tarfile, io import tarfile
import io
buf = io.BytesIO() buf = io.BytesIO()
tar = tarfile.open(mode = 'w:bz2', fileobj = buf) tar = tarfile.open(mode='w:bz2', fileobj=buf)
def check_add(path, arcname = None): def check_add(path, arcname=None):
if os.path.isfile(path): if os.path.isfile(path):
if arcname is None: arcname = path if arcname is None:
arcname = path
tar.add(path, self.basename + '/' + arcname) tar.add(path, self.basename + '/' + arcname)
def check_add_basename(path): def check_add_basename(path):
@@ -128,7 +78,6 @@ class BugReportHandler(bbctrl.RequestHandler):
self.write(buf.getvalue()) self.write(buf.getvalue())
def set_default_headers(self): def set_default_headers(self):
fmt = socket.gethostname() + '-%Y%m%d-%H%M%S' fmt = socket.gethostname() + '-%Y%m%d-%H%M%S'
self.basename = datetime.datetime.now().strftime(fmt) self.basename = datetime.datetime.now().strftime(fmt)
@@ -138,8 +87,6 @@ class BugReportHandler(bbctrl.RequestHandler):
class HostnameHandler(bbctrl.APIHandler): class HostnameHandler(bbctrl.APIHandler):
def get(self): self.write_json(socket.gethostname())
def put(self): def put(self):
if self.get_ctrl().args.demo: if self.get_ctrl().args.demo:
raise HTTPError(400, 'Cannot set hostname in demo mode') raise HTTPError(400, 'Cannot set hostname in demo mode')
@@ -153,77 +100,59 @@ class HostnameHandler(bbctrl.APIHandler):
raise HTTPError(400, 'Failed to set hostname') raise HTTPError(400, 'Failed to set hostname')
class WifiHandler(bbctrl.APIHandler): class NetworkHandler(bbctrl.APIHandler):
def get(self): def get(self):
data = {'ssid': '', 'channel': 0}
try: try:
data = json.loads(call_get_output(['config-wifi', '-j'])) ipAddresses = call_get_output(['hostname', '-I']).split()
except: pass except:
self.write_json(data) ipAddresses = ""
hostname = socket.gethostname()
try:
wifi = json.loads(call_get_output(['config-wifi', '-j']))
except:
wifi = {'enabled': False}
try:
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
wifi['networks'] = iw_parse.get_parsed_cells(lines)
except:
wifi['networks'] = []
self.write_json({
'ipAddresses': ipAddresses,
'hostname': hostname,
'wifi': wifi
})
def put(self): def put(self):
if self.get_ctrl().args.demo: if self.get_ctrl().args.demo:
raise HTTPError(400, 'Cannot configure WiFi in demo mode') raise HTTPError(400, 'Cannot configure WiFi in demo mode')
if 'mode' in self.json: if not 'wifi' in self.json:
cmd = ['config-wifi', '-r'] raise HTTPError(400, 'Payload is missing wifi config information')
mode = self.json['mode']
if mode == 'disabled': cmd += ['-d'] wifi = self.json['wifi']
elif 'ssid' in self.json:
cmd += ['-s', self.json['ssid']]
if mode == 'ap': cmd = ['config-wifi', '-r']
cmd += ['-a']
if 'channel' in self.json:
cmd += ['-c', self.json['channel']]
if 'pass' in self.json: if not wifi['enabled']:
cmd += ['-p', self.json['pass']] cmd += ['-d']
else:
if 'ssid' in wifi:
cmd += ['-s', wifi['ssid']]
if subprocess.call(cmd) == 0: if 'password' in wifi:
self.write_json('ok') cmd += ['-p', wifi['password']]
return
if subprocess.call(cmd) == 0:
self.write_json('ok')
return
raise HTTPError(400, 'Failed to configure wifi') raise HTTPError(400, 'Failed to configure wifi')
class UsernameHandler(bbctrl.APIHandler):
def get(self): self.write_json(get_username())
def put_ok(self):
if self.get_ctrl().args.demo:
raise HTTPError(400, 'Cannot set username in demo mode')
if 'username' in self.json: set_username(self.json['username'])
else: raise HTTPError(400, 'Missing "username"')
class PasswordHandler(bbctrl.APIHandler):
def put(self):
if self.get_ctrl().args.demo:
raise HTTPError(400, 'Cannot set password in demo mode')
if 'current' in self.json and 'password' in self.json:
check_password(self.json['current'])
# Set password
s = '%s:%s' % (get_username(), self.json['password'])
s = s.encode('utf-8')
p = subprocess.Popen(['chpasswd', '-c', 'MD5'],
stdin = subprocess.PIPE)
p.communicate(input = s)
if p.returncode == 0:
self.write_json('ok')
return
raise HTTPError(401, 'Failed to set password')
class ConfigLoadHandler(bbctrl.APIHandler): class ConfigLoadHandler(bbctrl.APIHandler):
def get(self): def get(self):
self.write_json(self.get_ctrl().config.load()) self.write_json(self.get_ctrl().config.load())
@@ -238,7 +167,7 @@ class ConfigDownloadHandler(bbctrl.APIHandler):
'attachment; filename="%s"' % filename) 'attachment; filename="%s"' % filename)
def get(self): def get(self):
self.write_json(self.get_ctrl().config.load(), pretty = True) self.write_json(self.get_ctrl().config.load(), pretty=True)
class ConfigSaveHandler(bbctrl.APIHandler): class ConfigSaveHandler(bbctrl.APIHandler):
@@ -252,14 +181,14 @@ class ConfigResetHandler(bbctrl.APIHandler):
class FirmwareUpdateHandler(bbctrl.APIHandler): class FirmwareUpdateHandler(bbctrl.APIHandler):
def prepare(self): pass def prepare(self): pass
def put_ok(self): def put_ok(self):
if not 'firmware' in self.request.files: if not 'firmware' in self.request.files:
raise HTTPError(401, 'Missing "firmware"') raise HTTPError(401, 'Missing "firmware"')
firmware = self.request.files['firmware'][0] firmware = self.request.files['firmware'][0]
if not os.path.exists('firmware'): os.mkdir('firmware') if not os.path.exists('firmware'):
os.mkdir('firmware')
with open('firmware/update.tar.bz2', 'wb') as f: with open('firmware/update.tar.bz2', 'wb') as f:
f.write(firmware['body']) f.write(firmware['body'])
@@ -284,20 +213,23 @@ class PathHandler(bbctrl.APIHandler):
future = preplanner.get_plan(filename) future = preplanner.get_plan(filename)
try: try:
delta = datetime.timedelta(seconds = 1) delta = datetime.timedelta(seconds=1)
data = yield gen.with_timeout(delta, future) data = yield gen.with_timeout(delta, future)
except gen.TimeoutError: except gen.TimeoutError:
progress = preplanner.get_plan_progress(filename) progress = preplanner.get_plan_progress(filename)
self.write_json(dict(progress = progress)) self.write_json(dict(progress=progress))
return return
try: try:
if data is None: return if data is None:
return
meta, positions, speeds = data meta, positions, speeds = data
if dataType == '/positions': data = positions if dataType == '/positions':
elif dataType == '/speeds': data = speeds data = positions
elif dataType == '/speeds':
data = speeds
else: else:
self.get_ctrl().state.set_bounds(meta['bounds']) self.get_ctrl().state.set_bounds(meta['bounds'])
self.write_json(meta) self.write_json(meta)
@@ -316,12 +248,14 @@ class PathHandler(bbctrl.APIHandler):
self.write(chunk) self.write(chunk)
yield self.flush() yield self.flush()
except tornado.iostream.StreamClosedError as e: pass except tornado.iostream.StreamClosedError as e:
pass
class HomeHandler(bbctrl.APIHandler): class HomeHandler(bbctrl.APIHandler):
def put_ok(self, axis, action, *args): def put_ok(self, axis, action, *args):
if axis is not None: axis = ord(axis[1:2].lower()) if axis is not None:
axis = ord(axis[1:2].lower())
if action == '/set': if action == '/set':
if not 'position' in self.json: if not 'position' in self.json:
@@ -329,8 +263,10 @@ class HomeHandler(bbctrl.APIHandler):
self.get_ctrl().mach.home(axis, self.json['position']) self.get_ctrl().mach.home(axis, self.json['position'])
elif action == '/clear': self.get_ctrl().mach.unhome(axis) elif action == '/clear':
else: self.get_ctrl().mach.home(axis) self.get_ctrl().mach.unhome(axis)
else:
self.get_ctrl().mach.home(axis)
class StartHandler(bbctrl.APIHandler): class StartHandler(bbctrl.APIHandler):
@@ -386,7 +322,7 @@ class ModbusReadHandler(bbctrl.APIHandler):
class ModbusWriteHandler(bbctrl.APIHandler): class ModbusWriteHandler(bbctrl.APIHandler):
def put_ok(self): def put_ok(self):
self.get_ctrl().mach.modbus_write(int(self.json['address']), self.get_ctrl().mach.modbus_write(int(self.json['address']),
int(self.json['value'])) int(self.json['value']))
class JogHandler(bbctrl.APIHandler): class JogHandler(bbctrl.APIHandler):
@@ -402,7 +338,8 @@ class JogHandler(bbctrl.APIHandler):
last = self.app.last_jog.get(id, 0) last = self.app.last_jog.get(id, 0)
self.app.last_jog[id] = ts self.app.last_jog[id] = ts
if ts < last: return # Out of order if ts < last:
return # Out of order
self.get_ctrl().mach.jog(self.json) self.get_ctrl().mach.jog(self.json)
@@ -413,17 +350,14 @@ class ClientConnection(object):
self.app = app self.app = app
self.count = 0 self.count = 0
def heartbeat(self): def heartbeat(self):
self.timer = self.app.ioloop.call_later(3, self.heartbeat) self.timer = self.app.ioloop.call_later(3, self.heartbeat)
self.send({'heartbeat': self.count}) self.send({'heartbeat': self.count})
self.count += 1 self.count += 1
def send(self, msg): raise HTTPError(400, 'Not implemented') def send(self, msg): raise HTTPError(400, 'Not implemented')
def on_open(self, id=None):
def on_open(self, id = None):
self.ctrl = self.app.get_ctrl(id) self.ctrl = self.app.get_ctrl(id)
self.ctrl.state.add_listener(self.send) self.ctrl.state.add_listener(self.send)
@@ -432,7 +366,6 @@ class ClientConnection(object):
self.heartbeat() self.heartbeat()
self.app.opened(self.ctrl) self.app.opened(self.ctrl)
def on_close(self): def on_close(self):
self.app.ioloop.remove_timeout(self.timer) self.app.ioloop.remove_timeout(self.timer)
self.ctrl.state.remove_listener(self.send) self.ctrl.state.remove_listener(self.send)
@@ -440,7 +373,6 @@ class ClientConnection(object):
self.is_open = False self.is_open = False
self.app.closed(self.ctrl) self.app.closed(self.ctrl)
def on_message(self, data): def on_message(self, data):
self.ctrl.mach.mdi(data) self.ctrl.mach.mdi(data)
@@ -465,23 +397,24 @@ class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection):
ClientConnection.__init__(self, session.server.app) ClientConnection.__init__(self, session.server.app)
sockjs.tornado.SockJSConnection.__init__(self, session) sockjs.tornado.SockJSConnection.__init__(self, session)
def send(self, msg): def send(self, msg):
try: try:
sockjs.tornado.SockJSConnection.send(self, msg) sockjs.tornado.SockJSConnection.send(self, msg)
except: except:
self.close() self.close()
def on_open(self, info): def on_open(self, info):
cookie = info.get_cookie('client-id') cookie = info.get_cookie('client-id')
if cookie is None: self.send(dict(sid = '')) # Trigger client reset if cookie is None:
self.send(dict(sid='')) # Trigger client reset
else: else:
id = cookie.value id = cookie.value
ip = info.ip ip = info.ip
if 'X-Real-IP' in info.headers: ip = info.headers['X-Real-IP'] if 'X-Real-IP' in info.headers:
self.app.get_ctrl(id).log.get('Web').info('Connection from %s' % ip) ip = info.headers['X-Real-IP']
self.app.get_ctrl(id).log.get(
'Web').info('Connection from %s' % ip)
super().on_open(id) super().on_open(id)
@@ -499,10 +432,13 @@ class Web(tornado.web.Application):
# Init camera # Init camera
if not args.disable_camera: if not args.disable_camera:
if self.args.demo: log = bbctrl.log.Log(args, ioloop, 'camera.log') if self.args.demo:
else: log = self.get_ctrl().log log = bbctrl.log.Log(args, ioloop, 'camera.log')
else:
log = self.get_ctrl().log
self.camera = bbctrl.Camera(ioloop, args, log) self.camera = bbctrl.Camera(ioloop, args, log)
else: self.camera = None else:
self.camera = None
# Init controller # Init controller
if not self.args.demo: if not self.args.demo:
@@ -517,9 +453,7 @@ class Web(tornado.web.Application):
(r'/api/reboot', RebootHandler), (r'/api/reboot', RebootHandler),
(r'/api/shutdown', ShutdownHandler), (r'/api/shutdown', ShutdownHandler),
(r'/api/hostname', HostnameHandler), (r'/api/hostname', HostnameHandler),
(r'/api/wifi', WifiHandler), (r'/api/network', NetworkHandler),
(r'/api/remote/username', UsernameHandler),
(r'/api/remote/password', PasswordHandler),
(r'/api/config/load', ConfigLoadHandler), (r'/api/config/load', ConfigLoadHandler),
(r'/api/config/download', ConfigDownloadHandler), (r'/api/config/download', ConfigDownloadHandler),
(r'/api/config/save', ConfigSaveHandler), (r'/api/config/save', ConfigSaveHandler),
@@ -547,7 +481,7 @@ class Web(tornado.web.Application):
(r'/(.*)', StaticFileHandler, (r'/(.*)', StaticFileHandler,
{'path': bbctrl.get_resource('http/'), {'path': bbctrl.get_resource('http/'),
'default_filename': 'index.html'}), 'default_filename': 'index.html'}),
] ]
router = sockjs.tornado.SockJSRouter(SockJSConnection, '/sockjs') router = sockjs.tornado.SockJSRouter(SockJSConnection, '/sockjs')
router.app = self router.app = self
@@ -555,7 +489,7 @@ class Web(tornado.web.Application):
tornado.web.Application.__init__(self, router.urls + handlers) tornado.web.Application.__init__(self, router.urls + handlers)
try: try:
self.listen(args.port, address = args.addr) self.listen(args.port, address=args.addr)
except Exception as e: except Exception as e:
raise Exception('Failed to bind %s:%d: %s' % ( raise Exception('Failed to bind %s:%d: %s' % (
@@ -563,33 +497,32 @@ class Web(tornado.web.Application):
print('Listening on http://%s:%d/' % (args.addr, args.port)) print('Listening on http://%s:%d/' % (args.addr, args.port))
def opened(self, ctrl): ctrl.clear_timeout() def opened(self, ctrl): ctrl.clear_timeout()
def closed(self, ctrl): def closed(self, ctrl):
# Time out clients in demo mode # Time out clients in demo mode
if self.args.demo: ctrl.set_timeout(self._reap_ctrl, ctrl) if self.args.demo:
ctrl.set_timeout(self._reap_ctrl, ctrl)
def _reap_ctrl(self, ctrl): def _reap_ctrl(self, ctrl):
ctrl.close() ctrl.close()
del self.ctrls[ctrl.id] del self.ctrls[ctrl.id]
def get_ctrl(self, id=None):
def get_ctrl(self, id = None): if not id or not self.args.demo:
if not id or not self.args.demo: id = '' id = ''
if not id in self.ctrls: if not id in self.ctrls:
ctrl = bbctrl.Ctrl(self.args, self.ioloop, id) ctrl = bbctrl.Ctrl(self.args, self.ioloop, id)
self.ctrls[id] = ctrl self.ctrls[id] = ctrl
else: ctrl = self.ctrls[id] else:
ctrl = self.ctrls[id]
return ctrl return ctrl
# Override default logger # Override default logger
def log_request(self, handler): def log_request(self, handler):
log = self.get_ctrl(handler.get_cookie('client-id')).log.get('Web') log = self.get_ctrl(handler.get_cookie('client-id')).log.get('Web')
log.info("%d %s", handler.get_status(), handler._request_summary()) log.info("%d %s", handler.get_status(), handler._request_summary())

80
src/py/iw_parse/README.md Normal file
View File

@@ -0,0 +1,80 @@
iw_parse
========
Parse the output of iwlist scan to get the name, address, quality, channel, and encryption type of all networks broadcasting within your Wireless NIC's reach.
Dependencies
------------
* [pip](http://www.pip-installer.org/en/latest/installing.html "pip installation guide") - If you don't have pip installed, followed the link.
Installation
------------
```bash
pip install iw_parse
```
Usage
-----
```bash
iwlist <INTERFACE_NAME> scan | iw_parse
```
Replace `<INTERFACE_NAME>` with the system name for your wireless NIC. It is usually something like `wlan0`. The command `iwconfig` will list all of your network interfaces.
Example:
```bash
iwlist wlan0 scan | iw_parse
```
The result should look something like:
```
Name Address Quality Channel Encryption
wireless1 20:AA:4B:34:2C:F5 100 % 11 WEP
wireless2 00:26:F2:1E:FC:03 84 % 1 WPA v.1
wireless3 00:1D:D3:6A:3C:60 66 % 6 WEP
wireless4 20:10:7A:E5:02:98 64 % 1 WEP
wireless5 CC:A4:62:B7:D2:B0 54 % 8 WPA v.1
wireless6 30:46:9A:53:3C:76 47 % 11 WPA v.1
wireless7 A0:21:B7:5F:84:B0 44 % 11 WEP
wireless8 04:A1:51:18:E8:E0 41 % 6 WPA v.1
```
Example from Python shell:
```python
>>> import iw_parse
>>> networks = iw_parse.get_interfaces(interface='wlan0')
>>> print networks
[{'Address': 'F8:1E:DF:F9:B0:0B',
'Channel': '3',
'Encryption': 'WEP',
'Name': 'Francis',
'Bit Rates': '144 Mb/s',
'Signal Level': '42',
'Name': 'Francis',
'Quality': '100'},
{'Address': '86:1B:5E:33:17:D4',
'Channel': '6',
'Encryption': 'Open',
'Bit Rates': '54 Mb/s',
'Signal Level': '72',
'Name': 'optimumwifi',
'Quality': '100'},
...
```
Acknowledgements
----------------
* The vast majority of iw_parse was written by Hugo Chargois.
License
-------
iw_parse is free--as in BSD. Hack your heart out, hackers.

View File

@@ -0,0 +1 @@
from .iw_parse import *

25
src/py/iw_parse/iw_parse Normal file
View File

@@ -0,0 +1,25 @@
#! /usr/bin/env python
import sys
from iw_parse import get_parsed_cells, print_cells
def main():
""" Pretty prints the output of iwlist scan into a table. """
parsed_cells = get_parsed_cells(sys.stdin)
# You can choose which columns to display here, and most importantly
# in what order. Of course, they must exist as keys in the dict rules.
columns = [
"Name",
"Address",
"Quality",
"Channel",
"Signal Level",
"Encryption"
]
print_cells(parsed_cells, columns)
if __name__ == "__main__":
main()

337
src/py/iw_parse/iw_parse.py Normal file
View File

@@ -0,0 +1,337 @@
#! /usr/bin/env python
# Hugo Chargois - 17 jan. 2010 - v.0.1
# Parses the output of iwlist scan into a table
# You can add or change the functions to parse the properties
# of each AP (cell) below. They take one argument, the bunch of text
# describing one cell in iwlist scan and return a property of that cell.
import re
import subprocess
VERSION_RGX = re.compile("version\s+\d+", re.IGNORECASE)
def get_name(cell):
""" Gets the name / essid of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The name / essid of the network.
"""
essid = matching_line(cell, "ESSID:")
if not essid:
return ""
return essid[1:-1]
def get_quality(cell):
""" Gets the quality of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The quality of the network.
"""
quality = matching_line(cell, "Quality=")
if quality is None:
return ""
quality = quality.split()[0].split("/")
quality = matching_line(cell, "Quality=").split()[0].split("/")
return str(int(round(float(quality[0]) / float(quality[1]) * 100)))
def get_signal_level(cell):
""" Gets the signal level of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The signal level of the network.
"""
signal = matching_line(cell, "Signal level=")
if signal is None:
return ""
signal = signal.split("=")[1].split("/")
if len(signal) == 2:
return str(int(round(float(signal[0]) / float(signal[1]) * 100)))
elif len(signal) == 1:
return signal[0].split(' ')[0]
else:
return ""
def get_noise_level(cell):
""" Gets the noise level of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The noise level of the network.
"""
noise = matching_line(cell, "Noise level=")
if noise is None:
return ""
noise = noise.split("=")[1]
return noise.split(' ')[0]
def get_channel(cell):
""" Gets the channel of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The channel of the network.
"""
channel = matching_line(cell, "Channel:")
if channel:
return channel
frequency = matching_line(cell, "Frequency:")
channel = re.sub(r".*\(Channel\s(\d{1,3})\).*", r"\1", frequency)
return channel
def get_frequency(cell):
""" Gets the frequency of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The frequency of the network.
"""
frequency = matching_line(cell, "Frequency:")
if frequency is None:
return ""
return frequency.split()[0]
def get_encryption(cell, emit_version=False):
""" Gets the encryption type of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The encryption type of the network.
"""
enc = ""
if matching_line(cell, "Encryption key:") == "off":
enc = "Open"
else:
for line in cell:
matching = match(line,"IE:")
if matching == None:
continue
wpa = match(matching,"WPA")
if wpa == None:
continue
version_matches = VERSION_RGX.search(wpa)
if len(version_matches.regs) == 1:
version = version_matches \
.group(0) \
.lower() \
.replace("version", "") \
.strip()
wpa = wpa.replace(version_matches.group(0), "").strip()
if wpa == "":
wpa = "WPA"
if emit_version:
enc = "{0} v.{1}".format(wpa, version)
else:
enc = wpa
if wpa == "WPA2":
return enc
else:
enc = wpa
if enc == "":
enc = "WEP"
return enc
def get_mode(cell):
""" Gets the mode of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The IEEE 802.11 mode of the network.
"""
mode = matching_line(cell, "Extra:ieee_mode=")
if mode is None:
return ""
return mode
def get_address(cell):
""" Gets the address of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The address of the network.
"""
return matching_line(cell, "Address: ")
def get_bit_rates(cell):
""" Gets the bit rate of a network / cell.
@param string cell
A network / cell from iwlist scan.
@return string
The bit rate of the network.
"""
return matching_line(cell, "Bit Rates:")
# Here you can choose the way of sorting the table. sortby should be a key of
# the dictionary rules.
def sort_cells(cells):
sortby = "Quality"
reverse = True
cells.sort(key=lambda el:el[sortby], reverse=reverse)
# Below here goes the boring stuff. You shouldn't have to edit anything below
# this point
def matching_line(lines, keyword):
""" Returns the first matching line in a list of lines.
@see match()
"""
for line in lines:
matching = match(line,keyword)
if matching != None:
return matching
return None
def match(line, keyword):
""" If the first part of line (modulo blanks) matches keyword,
returns the end of that line. Otherwise checks if keyword is
anywhere in the line and returns that section, else returns None"""
line = line.lstrip()
length = len(keyword)
if line[:length] == keyword:
return line[length:]
else:
if keyword in line:
return line[line.index(keyword):]
else:
return None
def parse_cell(cell, rules):
""" Applies the rules to the bunch of text describing a cell.
@param string cell
A network / cell from iwlist scan.
@param dictionary rules
A dictionary of parse rules.
@return dictionary
parsed networks. """
parsed_cell = {}
for key in rules:
rule = rules[key]
parsed_cell.update({key: rule(cell)})
return parsed_cell
def print_table(table):
# Functional black magic.
widths = list(map(max, map(lambda l: map(len, l), zip(*table))))
justified_table = []
for line in table:
justified_line = []
for i, el in enumerate(line):
justified_line.append(el.ljust(widths[i] + 2))
justified_table.append(justified_line)
for line in justified_table:
print("\t".join(line))
def print_cells(cells, columns):
table = [columns]
for cell in cells:
cell_properties = []
for column in columns:
if column == 'Quality':
# make print nicer
cell[column] = cell[column].rjust(3) + " %"
cell_properties.append(cell[column])
table.append(cell_properties)
print_table(table)
def get_parsed_cells(iw_data, rules=None):
""" Parses iwlist output into a list of networks.
@param list iw_data
Output from iwlist scan.
A list of strings.
@return list
properties: Name, Address, Quality, Channel, Frequency, Encryption, Signal Level, Noise Level, Bit Rates, Mode.
"""
# Here's a dictionary of rules that will be applied to the description
# of each cell. The key will be the name of the column in the table.
# The value is a function defined above.
rules = rules or {
"Name": get_name,
"Quality": get_quality,
"Channel": get_channel,
"Frequency": get_frequency,
"Encryption": get_encryption,
"Address": get_address,
"Signal Level": get_signal_level,
"Noise Level": get_noise_level,
"Bit Rates": get_bit_rates,
"Mode": get_mode,
}
cells = [[]]
parsed_cells = []
for line in iw_data:
cell_line = match(line, "Cell ")
if cell_line != None:
cells.append([])
line = cell_line[-27:]
cells[-1].append(line.rstrip())
cells = cells[1:]
for cell in cells:
parsed_cells.append(parse_cell(cell, rules))
sort_cells(parsed_cells)
return parsed_cells
def call_iwlist(interface='wlan0'):
""" Get iwlist output via subprocess
@param string interface
interface to scan
default is wlan0
@return string
properties: iwlist output
"""
return subprocess.check_output(['iwlist', interface, 'scanning'])
def get_interfaces(interface="wlan0"):
""" Get parsed iwlist output
@param string interface
interface to scan
default is wlan0
@param list columns
default data attributes to return
@return dict
properties: dictionary of iwlist attributes
"""
return get_parsed_cells(call_iwlist(interface).split('\n'))

View File

@@ -0,0 +1,10 @@
Copyright (c) 2013, Cuzzo Yahn
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -29,25 +29,61 @@ tt
clear right clear right
.header .header
height 140px padding-left 60px
padding 0 display flex
.header-content .brand
max-width 90% display flex
margin auto flex-direction column
text-align left align-self center
white-space nowrap
.estop img
float right width 300px
margin 5px
.version
font-size 18pt
color #777
display flex
.upgrade-link
margin-left 20px
font-size 16pt
align-self center
color blue
.upgrade-attention
background-color red
width 20px
height 20px
display inline-block
border-radius 50%
color white
font-size 13pt
font-weight 1000
margin-left 5px
align-self center
.pi-temp-warning
align-self center
font-size 30pt
font-family Audiowide
display inline
margin 0 30px
.left
color #444
.right
color #e5aa3d
.whitespace
flex-grow 1
.video .video
position relative position relative
float right
width 174px width 174px
height 130px height 130px
margin 2px 5px border 2px solid transparent
border 2px solid #fff
border-radius 5px border-radius 5px
&:hover &:hover
@@ -87,29 +123,9 @@ tt
width 100% width 100%
height 100% height 100%
.banner .estop
float left align-self center
padding-top 40px margin 0 30px
white-space nowrap
img
vertical-align top
.title
font-size 30pt
font-family Audiowide
display inline
margin-right 0.5em
.left
color #444
.right
color #e5aa3d
.subtitle
font-size 18pt
font-weight 100
color #aaa
.error .error
background red background red
@@ -214,8 +230,6 @@ span.unit
.pure-control-group .pure-control-group
label.units label.units
width 6em width 6em
label.units
text-align left text-align left
textarea textarea
@@ -230,6 +244,7 @@ span.unit
padding 0.7em 1em padding 0.7em 1em
border-radius 3px border-radius 3px
display inline-block display inline-block
@keyframes blink @keyframes blink
50% 50%
@@ -848,18 +863,6 @@ tt.save
text-decoration none text-decoration none
.upgrade-version
display inline-block
border-radius 4px
padding 2px
margin-left 0.5em
color #555
background-color #e5aa3d
text-decoration none
&:hover
color #fff
.modal-mask .modal-mask
position fixed position fixed
z-index 9998 z-index 9998

24
src/svelte-components/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,48 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte + TS + Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

9507
src/svelte-components/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "svelte-components",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"postbuild": "smui-theme compile dist/smui.css -i src/theme",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/kit": "^1.0.0-next.357",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
"@tsconfig/svelte": "^3.0.0",
"node-sass": "^7.0.1",
"polyfill-object.fromentries": "^1.0.1",
"smui-theme": "^6.0.0-beta.16",
"svelte": "^3.48.0",
"svelte-check": "^2.8.0",
"svelte-material-ui": "^6.0.0-beta.16",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vite": "^2.9.13"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,28 @@
/* To browse the icons in material-symbols, see https://marella.me/material-symbols/demo/ */
@font-face {
font-family: "Material Symbols Outlined";
font-style: normal;
font-weight: 100 700;
font-display: block;
src: url("./fonts/material-symbols-outlined.woff2") format("woff2");
}
.material-symbols-outlined {
font-family: "Material Symbols Outlined";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 48;
}

View File

@@ -0,0 +1,204 @@
<script lang="ts">
import WifiConnectionDialog from "../dialogs/WifiConnectionDialog.svelte";
import ChangeHostnameDialog from "../dialogs/ChangeHostnameDialog.svelte";
import Paper from "@smui/paper";
import Button, { Label } from "@smui/button";
import List, { Item, Graphic, Text, Meta } from "@smui/list";
import Card from "@smui/card";
import { networkInfo } from "../lib/NetworkInfo";
import type { WifiNetwork } from "../lib/NetworkInfo";
let changeHostnameDialog = {
open: false,
};
let wifiConnectionDialog = {
open: false,
network: {} as WifiNetwork,
};
function getWifiStrengthIcon(network: WifiNetwork) {
const strength = Math.ceil((Number(network.Quality) / 100) * 4);
switch (strength) {
case 1:
return "";
case 2:
return "wifi_1_bar";
case 3:
return "wifi_2_bar";
case 4:
return "wifi";
}
}
function onChangeHostname() {
changeHostnameDialog = {
open: true,
};
}
function onNetworkSelected(network: WifiNetwork) {
wifiConnectionDialog = {
open: true,
network,
};
}
</script>
<WifiConnectionDialog {...wifiConnectionDialog} />
<ChangeHostnameDialog {...changeHostnameDialog} />
<div class="admin-network-view">
<h1>Network Info</h1>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="hostname">Hostname</label>
<Card id="hostname" variant="outlined">
<Text id="hostname">
{$networkInfo.hostname}
</Text>
</Card>
<Button on:click={onChangeHostname} touch variant="raised">
<Label>Change</Label>
</Button>
</div>
</div>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="ip-addresses">IP Addresses</label>
<Card id="ip-addresses" variant="outlined">
{#each $networkInfo.ipAddresses as ipAddress}
<div>
<Text id="hostname">
{ipAddress}
</Text>
</div>
{/each}
</Card>
</div>
</div>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="wifi">Wi-Fi</label>
<div class="wifi-networks">
<Card id="wifi" variant="outlined">
<List>
{#if $networkInfo.wifi.networks.length === 0}
<Item class="wifi-network">
<Text>Scanning...</Text>
</Item>
{:else}
{#each $networkInfo.wifi.networks as network}
<Item
class="wifi-network"
on:SMUI:action={() => onNetworkSelected(network)}
>
<Graphic
class="strength {$networkInfo.wifi.ssid === network.Name
? 'active'
: ''}"
>
<span class="material-symbols-outlined background"
>wifi</span
>
<span class="material-symbols-outlined">
{getWifiStrengthIcon(network)}
</span>
</Graphic>
<Text style="margin-right: 20px;">{network.Name}</Text>
{#if network.Encryption !== "Open"}
<Meta>
<span class="material-symbols-outlined lock">lock</span>
</Meta>
{/if}
</Item>
{/each}
{/if}
</List>
</Card>
<em style="display: block;">
Click on a Wi-Fi network to connect or disconnect.
</em>
</div>
</div>
</div>
</div>
<style lang="scss">
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
:global {
.admin-network-view {
.pure-form-aligned .pure-control-group label {
vertical-align: top;
font-size: 15pt;
font-weight: bold;
}
button {
margin: 0;
}
.mdc-card {
width: 400px;
min-height: 38px;
display: inline-block;
vertical-align: top;
margin-bottom: 20px;
margin-right: 20px;
padding: 5px 15px;
}
.wifi-networks {
display: inline-block;
.mdc-card {
padding: 0;
margin-bottom: 5px;
}
}
.wifi-network {
.lock {
font-size: 20px;
vertical-align: text-bottom;
}
.strength {
border-radius: 50%;
padding: 3px;
background-color: $light;
color: $very-dark;
margin-right: 10px;
position: relative;
&.active {
background-color: $primary;
color: white;
}
span {
position: absolute;
width: 24px;
height: 24px;
&.background {
opacity: 0.25;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import MessageDialog from "./MessageDialog.svelte";
import * as api from "../lib/api";
// https://man7.org/linux/man-pages/man7/hostname.7.html
//
// Each element of the hostname must be from 1 to 63 characters long
// and the entire hostname, including the dots, can be at most 253
// characters long. Valid characters for hostnames are ASCII(7)
// letters from a to z, the digits from 0 to 9, and the hyphen (-).
// A hostname may not start with a hyphen.
const pattern = /[a-zA-Z0-9][a-zA-Z0-9-]{0,62}/;
export let open = false;
let rebooting = false;
let redirectTimeout = 45;
let hostname = "";
$: setTimeout(() => {
hostname = (hostname.match(pattern) || [""])[0].toLowerCase();
}, 0);
$: if (open) {
hostname = "";
}
async function onConfirm() {
rebooting = true;
await api.PUT("hostname", { hostname });
await api.PUT("reboot");
const interval = setInterval(() => {
if (0 < redirectTimeout) {
redirectTimeout -= 1;
} else {
clearInterval(interval);
location.hostname = getRedirectTarget();
}
}, 1000);
}
function getRedirectTarget() {
if (location.hostname.endsWith(".local")) {
return `${hostname}.local`;
}
if (location.hostname.endsWith(".lan")) {
return `${hostname}.lan`;
}
return hostname;
}
</script>
<MessageDialog open={rebooting} title="Rebooting">
Rebooting to apply the hostname change...
</MessageDialog>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="simple-title"
aria-describedby="simple-content"
>
<Title id="simple-title">Change Hostname</Title>
<Content id="simple-content">
<TextField
bind:value={hostname}
label="New Hostname"
spellcheck="false"
variant="filled"
style="width: 100%;"
/>
<p>
<em>Clicking Confirm will reboot the controller to apply the change.</em>
</p>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button defaultAction on:click={onConfirm} disabled={hostname.length === 0}>
<Label>Confirm</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import Dialog, { Title, Content } from "@smui/dialog";
export let open: boolean;
export let title: string;
</script>
<Dialog
bind:open
scrimClickAction=""
escapeKeyAction=""
aria-labelledby="simple-title"
aria-describedby="simple-content"
>
<Title id="simple-title">{title}</Title>
<Content id="simple-content">
<slot />
</Content>
</Dialog>

View File

@@ -0,0 +1,101 @@
<script lang="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MessageDialog from "./MessageDialog.svelte";
import type { WifiNetwork } from "../lib/NetworkInfo";
import * as api from "../lib/api";
export let open = false;
export let network: WifiNetwork;
let rebooting = false;
let needPassword = false;
let password = "";
let showPassword = false;
let connectOrDisconnect: string;
let connectToOrDisconnectFrom: string;
$: needPassword = !network?.active && network?.Encryption !== "Open";
$: {
connectOrDisconnect = network?.active ? "Disconnect" : "Connect";
connectToOrDisconnectFrom = network?.active ? "Disconnect from" : "Connect to";
}
$: if (open) {
password = "";
}
async function onConfirm() {
rebooting = true;
await api.PUT("network", {
wifi: {
enabled: !network.active,
ssid: network.Name,
password,
},
});
}
</script>
<MessageDialog open={rebooting} title="Rebooting">
Rebooting to apply Wifi changes...
</MessageDialog>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="simple-title"
aria-describedby="simple-content"
>
<Title id="simple-title">{connectToOrDisconnectFrom} {network.Name}</Title>
<Content id="simple-content">
{#if needPassword}
<TextField
bind:value={password}
label="Password"
spellcheck="false"
variant="filled"
type={showPassword ? "text" : "password"}
style="width: 100%;"
>
<div
slot="trailingIcon"
on:click={() => (showPassword = !showPassword)}
>
<Icon class="material-symbols-outlined">
{showPassword ? "password" : "abc"}
</Icon>
</div>
<HelperText persistent slot="helper">
Wifi passwords must be 8 to 128 characters
</HelperText>
</TextField>
{/if}
<p>
<em>
Clicking {connectOrDisconnect} will reboot the controller to apply the changes.
</em>
</p>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
on:click={onConfirm}
disabled={needPassword && (password.length < 8 || password.length > 128)}
>
<Label>{connectOrDisconnect}</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -0,0 +1,93 @@
import { readable } from "svelte/store";
import * as api from "./api";
export type WifiNetwork = {
Quality: string;
Channel: string;
Frequency: string;
Mode: string;
"Bit Rates": string;
Name: string;
Address: string;
Encryption: string;
"Signal Level": string;
"Noise Level": string;
lastSeen: number;
active: boolean;
};
export type NetworkInfo = {
ipAddresses: Array<string>;
hostname: string;
wifi: {
ssid: string;
networks: Array<WifiNetwork>;
};
};
const empty: NetworkInfo = {
ipAddresses: [],
hostname: "",
wifi: {
ssid: "",
networks: []
}
}
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
getNetworkInfo();
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
async function getNetworkInfo() {
const networksByName: Record<string, WifiNetwork> = {}
try {
const networkInfo: NetworkInfo = await api.GET("network");
const now = Date.now();
for (let network of networkInfo.wifi.networks) {
if (network.Name) {
network.lastSeen = now;
network.active = networkInfo.wifi.ssid === network.Name;
networksByName[network.Name] = network;
}
}
for (let network of Object.values(networksByName)) {
if (network.lastSeen - now > 30000) {
delete networksByName[network.Name];
}
}
set({
ipAddresses: networkInfo.ipAddresses,
hostname: networkInfo.hostname,
wifi: {
ssid: networkInfo.wifi.ssid,
networks: Object.values(networksByName).sort((a, b) => {
switch (true) {
case a.active:
return -1;
case b.active:
return 1;
default:
return a.Name.localeCompare(b.Name);
}
})
}
});
} catch (error) {
console.debug("Failed to fetch network info", error);
}
}
return () => {
clearInterval(networkInfoIntervalId);
}
})
export function init() {
return networkInfo.subscribe(() => ({}));
}

View File

@@ -0,0 +1,41 @@
type HttpMethod = "GET" | "PUT" | "POST" | "DELETE";
async function doFetch(method: HttpMethod, url: string, data: any, config: RequestInit) {
try {
const response = await fetch(`/api/${url}`, {
...config,
method,
cache: "no-cache",
body: (typeof data === 'object')
? JSON.stringify(data)
: undefined,
headers: (typeof data === 'object')
? {
"Content-Type": 'application/json; charset=utf-8'
}
: {}
});
return await response.json();
} catch (error) {
console.debug('API Error: ' + url + ': ' + error);
throw error;
}
}
export async function GET(url: string, config: RequestInit = {}) {
return doFetch('GET', url, undefined, config);
}
export async function PUT(url: string, data: any = undefined, config: RequestInit = {}) {
return doFetch('PUT', url, data, config);
}
export async function POST(url: string, data: any = undefined, config: RequestInit = {}) {
return doFetch('POST', url, data, config);
}
export async function DELETE(url: string, config = {}) {
return doFetch('DELETE', url, undefined, config);
}

View File

@@ -0,0 +1,15 @@
import 'polyfill-object.fromentries';
import AdminNetworkView from './components/AdminNetworkView.svelte';
import { init as initNetworkInfo } from './lib/NetworkInfo';
export function create(component: string, target: HTMLElement, props: Record<string, any>) {
switch (component) {
case "AdminNetworkView":
return new AdminNetworkView({ target, props });
default:
throw new Error("Unknown component");
}
}
export { initNetworkInfo };

View File

@@ -0,0 +1,22 @@
@use 'sass:color';
@use '@material/theme/color-palette';
// Svelte Colors!
@use '@material/theme/index' as theme with (
$primary: #0078e7,
$secondary: #676778,
$surface: #fff,
$background: #fff,
$error: color-palette.$red-900,
$on-surface: #777
);
@use "@material/elevation/mdc-elevation";
@use "@material/list";
@include list.deprecated-core-styles;
:root {
--mdc-theme-text-primary-on-background: #777;
}

View File

@@ -0,0 +1,12 @@
@use 'sass:color';
@use '@material/theme/color-palette';
// Svelte Colors! (Dark Theme)
@use '@material/theme/index' as theme with (
$primary: #ff3e00,
$secondary: color.scale(#676778, $whiteness: -10%),
$surface: color.adjust(color-palette.$grey-900, $blue: +4),
$background: #000,
$error: color-palette.$red-700
);

View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import sveltePreprocess from 'svelte-preprocess'
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}

View File

@@ -0,0 +1,22 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleSuffixes": [".svelte", ""]
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
svelte()
],
build: {
target: "chrome60",
lib: {
entry: resolve(__dirname, 'src/main.ts'),
name: 'SvelteComponents',
formats: ['iife'],
fileName: () => "index.js"
}
}
})