Set time and time zone

This commit is contained in:
David Carley
2022-07-21 20:53:57 -07:00
parent 46d26deb8e
commit 3f3b609de6
16 changed files with 1303 additions and 141 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "bbctrl",
"version": "1.0.10b5",
"version": "1.0.10b6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bbctrl",
"version": "1.0.10b5",
"version": "1.0.10b6",
"license": "GPL-3.0+",
"dependencies": {
"browserify": "^17.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "bbctrl",
"version": "1.0.10b5",
"version": "1.0.10b6",
"homepage": "https://onefinitycnc.com/",
"repository": "https://github.com/OneFinityCNC/onefinity",
"license": "GPL-3.0+",

View File

@@ -4,7 +4,6 @@ const api = require("./api");
const cookie = require("./cookie")("bbctrl-");
const Sock = require("./sock");
SvelteComponents.initNetworkInfo();
SvelteComponents.createComponent("DialogHost",
document.getElementById("svelte-dialog-host")
);

View File

@@ -9,6 +9,7 @@ module.exports = {
data: function () {
return {
current_time: "",
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
mdi: '',
last_file: undefined,
@@ -253,6 +254,10 @@ module.exports = {
ready: function () {
this.load();
setInterval(() => {
this.current_time = new Date().toLocaleTimeString();
}, 1000);
SvelteComponents.registerControllerMethods({
stop: (...args) => this.stop(...args),
send: (...args) => this.send(...args),
@@ -527,6 +532,10 @@ module.exports = {
showProbeDialog: function (probeType) {
SvelteComponents.showDialog("Probe", { probeType });
},
showSetTimeDialog: function () {
SvelteComponents.showDialog("SetTime");
}
},

View File

@@ -265,6 +265,13 @@ script#control-view-template(type="text/x-template")
td
table.info
tr
th Current Time
td
span {{current_time}}
button.pure-button(@click="showSetTimeDialog", style="height: 28px; padding: 0px 10px")
.fa.fa-cog
tr
th Remaining
td(title="Total run time (days:hours:mins:secs)").
@@ -273,12 +280,7 @@ script#control-view-template(type="text/x-template")
tr
th ETA
td.eta {{eta}}
tr
th Line
td
| {{0 <= state.line ? state.line : 0 | number}}
span(v-if="toolpath.lines")
| &nbsp;of {{toolpath.lines | number}}
tr
th Progress
td.progress

View File

@@ -1,12 +1,24 @@
import traceback
import copy
import json
import uuid
import os
import socket
import bbctrl
import iw_parse
from tornado import gen
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
def call_get_output(cmd):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
s = p.communicate()[0].decode('utf-8')
if p.returncode:
raise HTTPError(400, 'Command failed')
return s
class UploadChangeHandler(FileSystemEventHandler):
def __init__(self, state):
self.state = state
@@ -64,6 +76,36 @@ class State(object):
self), self.ctrl.get_upload(), recursive=True)
observer.start()
self._updateNetworkInfo()
@gen.coroutine
def _updateNetworkInfo(self):
try:
ipAddresses = call_get_output(['hostname', '-I']).split()
except:
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.set('networkInfo', {
'ipAddresses': ipAddresses,
'hostname': hostname,
'wifi': wifi
})
self.timeout = self.ctrl.ioloop.call_later(5, self._updateNetworkInfo)
def reset(self):
# Unhome all motors
for i in range(4):
@@ -170,8 +212,11 @@ class State(object):
return name
def has(self, name): return self.resolve(name) in self.vars
def set_callback(self, name, cb): self.callbacks[self.resolve(name)] = cb
def has(self, name):
return self.resolve(name) in self.vars
def set_callback(self, name, cb):
self.callbacks[self.resolve(name)] = cb
def set(self, name, value):
name = self.resolve(name)
@@ -233,7 +278,8 @@ class State(object):
self.listeners.append(listener)
listener(self.vars)
def remove_listener(self, listener): self.listeners.remove(listener)
def remove_listener(self, listener):
self.listeners.remove(listener)
def set_machine_vars(self, vars):
# Record all machine vars, indexed or otherwise
@@ -284,7 +330,8 @@ class State(object):
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):
return motor
def is_axis_homed(self, axis): return self.get('%s_homed' % axis, False)
def is_axis_homed(self, axis):
return self.get('%s_homed' % axis, False)
def is_axis_enabled(self, axis):
motor = self.find_motor(axis)

View File

@@ -1,6 +1,5 @@
import os
import re
import json
import tornado
import sockjs.tornado
import datetime
@@ -10,7 +9,6 @@ from tornado.web import HTTPError
from tornado import gen
import bbctrl
import iw_parse
def call_get_output(cmd):
@@ -101,31 +99,6 @@ class HostnameHandler(bbctrl.APIHandler):
class NetworkHandler(bbctrl.APIHandler):
def get(self):
try:
ipAddresses = call_get_output(['hostname', '-I']).split()
except:
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):
if self.get_ctrl().args.demo:
raise HTTPError(400, 'Cannot configure WiFi in demo mode')
@@ -383,13 +356,34 @@ class ScreenRotationHandler(bbctrl.APIHandler):
text = config.read()
text = transformationMatrixPattern.sub(r'\1\2\3\5', text)
if rotated:
text = matchIsTouchscreenPattern.sub(r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
text = matchIsTouchscreenPattern.sub(
r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf", 'wt') as config:
config.write(text)
subprocess.run('reboot')
class TimeHandler(bbctrl.APIHandler):
def get(self):
timeinfo = call_get_output(['timedatectl'])
timezones = call_get_output(
['timedatectl', 'list-timezones', '--no-pager'])
self.get_log('TimeHandler').info(
'Time stuff: {}, {}'.format(timeinfo, timezones))
self.write_json({
'timeinfo': timeinfo,
'timezones': timezones
})
def put_ok(self):
datetime = self.json['datetime']
timezone = self.json['timezone']
subprocess.Popen(['timedatectl', 'set-time', datetime])
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
# Base class for Web Socket connections
class ClientConnection(object):
def __init__(self, app):
@@ -526,6 +520,7 @@ class Web(tornado.web.Application):
(r'/api/jog', JogHandler),
(r'/api/video', bbctrl.VideoHandler),
(r'/api/screen-rotation', ScreenRotationHandler),
(r'/api/time', TimeHandler),
(r'/(.*)', StaticFileHandler,
{'path': bbctrl.get_resource('http/'),
'default_filename': 'index.html'}),
@@ -545,7 +540,8 @@ class Web(tornado.web.Application):
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):
# Time out clients in demo mode

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,12 @@
"node-sass": "^7.0.1",
"polyfill-object.fromentries": "^1.0.1",
"smui-theme": "^6.0.0-beta.16",
"string.prototype.matchall": "^4.0.7",
"svelte": "^3.48.0",
"svelte-check": "^2.8.0",
"svelte-material-ui": "^6.0.0-beta.16",
"svelte-preprocess": "^4.10.7",
"svelte-tiny-virtual-list": "^2.0.5",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vite": "^2.9.13"

View File

@@ -4,6 +4,7 @@
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import UploadDialog from "$dialogs/UploadDialog.svelte";
import SetTimeDialog from "./SetTimeDialog.svelte";
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
type HomeMachineDialogPropsType = {
@@ -29,6 +30,11 @@
onComplete: () => void;
};
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
type SetTimeDialogPropsType = {
open: boolean;
};
export function showDialog(
dialog: "HomeMachine",
props: Omit<HomeMachineDialogPropsType, "open">
@@ -49,6 +55,11 @@
props: Omit<UploadDialogPropsType, "open">
);
export function showDialog(
dialog: "SetTime",
props: Omit<SetTimeDialogPropsType, "open">
);
export function showDialog(dialog: string, props: any) {
switch (dialog) {
case "HomeMachine":
@@ -67,6 +78,10 @@
UploadDialogProps.set({ ...props, open: true });
break;
case "SetTime":
SetTimeDialogProps.set({ ...props, open: true });
break;
default:
throw new Error(`Unknown dialog '${dialog}`);
}
@@ -77,3 +92,4 @@
<ProbeDialog {...$ProbeDialogProps} />
<ScreenRotationDialog {...$ScreenRotationDialogProps} />
<UploadDialog {...$UploadDialogProps} />
<SetTimeDialog {...$SetTimeDialogProps} />

View File

@@ -1,5 +1,18 @@
<script type="ts" context="module">
import { get, writable, type Writable } from "svelte/store";
<script type="ts">
import DimensionInput from "$components/DimensionInput.svelte";
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { Config } from "$lib/ConfigStore";
import { writable, type Writable } from "svelte/store";
import {
probingActive,
probeContacted,
probingComplete,
probingFailed,
probingStarted,
} from "$lib/ControllerState";
type Step =
| "None"
@@ -19,49 +32,8 @@
};
const cancelled = writable(false);
const probingActive = writable(false);
const probeContacted = writable(false);
const probingStarted = writable(false);
const probingFailed = writable(false);
const probingComplete = writable(false);
const userAcknowledged = writable(false);
export function handleControllerStateUpdate(state: Record<string, any>) {
if (!get(probingActive)) {
return;
}
switch (true) {
case state.pw === 0:
probeContacted.set(true);
break;
case state.log?.msg === "Switch not found":
probingFailed.set(true);
break;
case state.cycle !== "idle":
probingStarted.set(true);
break;
case state.cycle === "idle":
if (get(probingStarted)) {
probingStarted.set(false);
probingComplete.set(true);
}
break;
}
}
</script>
<script type="ts">
import DimensionInput from "$components/DimensionInput.svelte";
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { Config } from "$lib/ConfigStore";
const cutterDiameterOptions = [
{ value: 0.5, label: '1/2 "', metric: false },
{ value: 0.25, label: '1/4 "', metric: false },

View File

@@ -0,0 +1,240 @@
<script lang="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import CircularProgress from "@smui/circular-progress";
import VirtualList from "svelte-tiny-virtual-list";
import * as api from "$lib/api";
const itemHeight = 35;
type Timezone = {
label: string;
value: string;
};
export let open = false;
let value = "";
let wasOpen = false;
let loading = true;
let timezones: Timezone[] = [];
let currentTimezoneIndex: number;
let selectedTimezoneIndex: number;
let networkTimeSynchronized: boolean;
$: if (open != wasOpen) {
if (!wasOpen) {
loadData();
}
wasOpen = open;
}
async function loadData() {
loading = true;
const result = await api.GET("time");
parseTimezones(result.timezones);
parseTimeinfo(result.timeinfo);
value = getDateTimeValueString();
loading = false;
}
function parseTimeinfo(str: string) {
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
let currentTimezoneValue;
for (const match of matches) {
let [, label, value] = match;
switch (label) {
case "Time zone":
currentTimezoneValue = value.split(" ")[0];
break;
case "NTP synchronized":
networkTimeSynchronized = value === "yes";
break;
}
}
currentTimezoneIndex = timezones.findIndex(
(tz) => tz.value === currentTimezoneValue
);
selectedTimezoneIndex = currentTimezoneIndex;
}
function parseTimezones(str: string) {
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
timezones = [];
for (let [, value] of matches) {
timezones.push({
label: value.replace(/_/g, " "),
value,
});
}
// Sort alphabetically, but with the current timezone at the top of the list
timezones.sort((a, b) => {
switch (true) {
case a.value === "UTC":
return -1;
case b.value === "UTC":
return 1;
default:
return a.value.localeCompare(b.value);
}
});
}
function getDateTimeValueString() {
const date = new Date();
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
return `${year}-${month}-${day}T${hour}:${minute}:00`;
}
async function onConfirm() {
const date = new Date(value);
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
await api.PUT("time", {
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
timezone: timezones[selectedTimezoneIndex].value,
});
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="set-time-dialog-title"
aria-describedby="set-time-dialog-content"
>
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
<Content id="set-time-dialog-content">
{#if loading}
<div style="display: flex; justify-content: center">
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
</div>
{:else}
{#if networkTimeSynchronized}
<p>
Because this controller is connected to the internet, the time is
managed automatically, and cannot be manually set.
</p>
{:else}
<p>
Because this controller is not connected to the internet, you can
manually set the time.
</p>
<p>
Note: any time the controller is turned off, the time will need to be
reset. If you connect the controller to the internet, the time will be
managed automatically.
</p>
<Label>Date & Time</Label>
<TextField
bind:value
label="Time"
type="datetime-local"
variant="filled"
style="width: 100%;"
/>
{/if}
<p>
To display your local time correctly, the controller must know what
timezone it is in.
</p>
<div class="timezones-container" style="margin-top: 20px;">
<Label>Select your timezone</Label>
<VirtualList
width="100%"
height={itemHeight * 6}
itemCount={timezones.length}
itemSize={itemHeight}
scrollToIndex={currentTimezoneIndex}
scrollToAlignment="center"
>
<div
slot="item"
let:index
let:style
{style}
class="timezone"
class:selected={index === selectedTimezoneIndex}
on:click={() => (selectedTimezoneIndex = index)}
>
{timezones[index].label}
</div>
</VirtualList>
</div>
{/if}
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
disabled={selectedTimezoneIndex === -1}
on:click={onConfirm}
>
<Label>Confirm</Label>
</Button>
</Actions>
</Dialog>
<style lang="scss">
@use "sass:color";
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
.timezones-container {
:global {
.virtual-list-wrapper {
border: 1px solid #ccc;
border-radius: 3px;
overflow-x: hidden;
overflow-y: scroll;
}
}
.timezone {
font-size: 14px;
display: flex;
align-items: center;
margin: 0;
padding-left: 10px;
&.selected {
color: $primary;
background-color: color.adjust($primary, $lightness: 50%);
font-weight: bold;
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
import { get, writable } from "svelte/store";
import { processNetworkInfo } from "./NetworkInfo";
export const networkInfo = writable({});
export const probingActive = writable(false);
export const probeContacted = writable(false);
export const probingStarted = writable(false);
export const probingFailed = writable(false);
export const probingComplete = writable(false);
export function handleControllerStateUpdate(state: Record<string, any>) {
if (state.networkInfo) {
processNetworkInfo(state.networkInfo);
delete state.networkInfo;
}
if (get(probingActive)) {
if (state.pw === 0) {
probeContacted.set(true);
}
if (state.log?.msg === "Switch not found") {
probingFailed.set(true);
}
if (state.cycle !== "idle") {
probingStarted.set(true);
}
if (state.cycle === "idle" && get(probingStarted)) {
probingStarted.set(false);
probingComplete.set(true);
}
}
}

View File

@@ -1,5 +1,4 @@
import { readable } from "svelte/store";
import * as api from "$lib/api";
import { writable } from "svelte/store";
export type WifiNetwork = {
Quality: string;
@@ -34,21 +33,16 @@ const empty: NetworkInfo = {
}
}
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
getNetworkInfo();
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
export const networkInfo = writable<NetworkInfo>(empty);
async function getNetworkInfo() {
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
const now = Date.now();
const networksByName: Record<string, WifiNetwork> = {}
try {
const networkInfo: NetworkInfo = await api.GET("network");
const now = Date.now();
for (let network of networkInfo.wifi.networks) {
for (let network of rawNetworkInfo.wifi.networks) {
if (network.Name) {
network.lastSeen = now;
network.active = networkInfo.wifi.ssid === network.Name;
network.active = rawNetworkInfo.wifi.ssid === network.Name;
networksByName[network.Name] = network;
}
}
@@ -59,11 +53,11 @@ export const networkInfo = readable<NetworkInfo>(empty, (set) => {
}
}
set({
ipAddresses: networkInfo.ipAddresses,
hostname: networkInfo.hostname,
networkInfo.set({
ipAddresses: rawNetworkInfo.ipAddresses,
hostname: rawNetworkInfo.hostname,
wifi: {
ssid: networkInfo.wifi.ssid,
ssid: rawNetworkInfo.wifi.ssid,
networks: Object.values(networksByName).sort((a, b) => {
switch (true) {
case a.active:
@@ -78,16 +72,4 @@ export const networkInfo = readable<NetworkInfo>(empty, (set) => {
})
}
});
} catch (error) {
console.debug("Failed to fetch network info", error);
}
}
return () => {
clearInterval(networkInfoIntervalId);
}
})
export function init() {
return networkInfo.subscribe(() => ({}));
}

View File

@@ -1,11 +1,13 @@
import 'polyfill-object.fromentries';
import matchAll from "string.prototype.matchall";
matchAll.shim();
import AdminNetworkView from '$components/AdminNetworkView.svelte';
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
import Devmode from "$components/Devmode.svelte";
import { handleConfigUpdate } from '$lib/ConfigStore';
import { init as initNetworkInfo } from '$lib/NetworkInfo';
import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte";
import { handleControllerStateUpdate } from "$lib/ControllerState";
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
@@ -17,7 +19,7 @@ export function createComponent(component: string, target: HTMLElement, props: R
return new DialogHost({ target, props });
case "Devmode":
return new Devmode({target, props});
return new Devmode({ target, props });
default:
throw new Error("Unknown component");
@@ -25,7 +27,6 @@ export function createComponent(component: string, target: HTMLElement, props: R
}
export {
initNetworkInfo,
showDialog,
handleControllerStateUpdate,
handleConfigUpdate,

View File

@@ -15,6 +15,7 @@ export default defineConfig({
}
},
build: {
minify: false,
target: "chrome60",
lib: {
entry: resolve(__dirname, 'src/main.ts'),