commit 9f26274c0c34c492978eb28c059104d83c1f2365
parent 36f5485422a6695bbf6ceba200c430fe0bb73ad4
Author: Friedel Schön <[email protected]>
Date: Tue, 28 Jun 2022 15:22:14 +0200
big remote commit
Diffstat:
38 files changed, 1112 insertions(+), 434 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,4 @@
__pycache__
.DS_Store
-build/
-\ No newline at end of file
+build/
+.vscode/
+\ No newline at end of file
diff --git a/client/client.ino b/client/client.ino
@@ -1,6 +1,7 @@
#include "include/config.h"
#include "include/interface.h"
#include "include/led.h"
+#include "include/macro.h"
#include <Sodaq_LSM303AGR.h>
#include <Sodaq_UBlox_GPS.h>
@@ -19,13 +20,28 @@ void setup() {
pinMode(BATVOLT_PIN, INPUT);
pinMode(CHARGER_STATUS, INPUT);
- config_current = config_flash.read();
-
+ config.open();
client.begin();
- json req;
- // req["mac"] = macAddress;
- client.send(interface::METHOD_POST, "/api/hello", req);
+ if (!config.valid)
+ config = config_default;
+
+ client.request["token"] = config.token;
+ client.request["domain"] = config.domain;
+ while (!client.send(interface::METHOD_POST, "/api/hello")) {
+ writeLED(COLOR_RED);
+ delay(500);
+ }
+
+ bool save = false;
+ if (client.response.hasOwnProperty("token"))
+ strcpy(config.token, (const char*) client.response["token"]), save = true;
+ if (client.response.hasOwnProperty("domain"))
+ strcpy(config.domain, (const char*) client.response["domain"]), save = true;
+
+ if (save)
+ config.save();
+
Wire.begin();
delay(1000);
@@ -43,37 +59,37 @@ void loop() {
int now = millis();
if (now - last > statusInterval * 1000) {
- json gps;
if (sodaq_gps.scan(true, gpsTimeout * 1000)) {
- gps["signal"] = true;
- gps["latitude"] = sodaq_gps.getLat();
- gps["longitude"] = sodaq_gps.getLon();
- gps["accuracy"] = getAccuracy(); // -> 100% the best, 0% the worst
+ client.request["latitude"] = sodaq_gps.getLat();
+ client.request["longitude"] = sodaq_gps.getLon();
+ client.request["accuracy"] = getAccuracy();
} else {
- gps["signal"] = false;
+ client.request["latitude"] = 0;
+ client.request["longitude"] = 0;
+ client.request["accuracy"] = 0;
}
- gps["satellites"] = sodaq_gps.getNumberOfSatellites();
-
- json req;
- req["battery"] = batteryVoltage();
- req["temperature"] = accel.getTemperature();
- req["charging"] = getCharging();
- req["trap"] = getTrapStatus();
- req["gps"] = gps;
+ client.request["token"] = config.token;
+ client.request["battery"] = batteryVoltage();
+ client.request["temperature"] = accel.getTemperature();
+ client.request["charging"] = getCharging();
+ client.request["trap"] = getTrapStatus();
+ client.request["satellites"] = sodaq_gps.getNumberOfSatellites();
- client.send(interface::METHOD_POST, "/api/update", req);
+ client.send(interface::METHOD_POST, "/api/update");
last = now;
}
}
-double batteryVoltage() {
- return batteryFactor * (double) analogRead(BATVOLT_PIN);
+int batteryVoltage() {
+ return batteryFactor * analogRead(BATVOLT_PIN);
}
-double getAccuracy() {
+double getAccuracy() { // -> 100% the best, 0% the worst
double hdop = sodaq_gps.getHDOP();
- return hdop > 1 ? 1.0 / hdop * 100 : hdop * 100;
+ if (hdop > 1)
+ hdop = 1.0 / hdop;
+ return hdop * 100;
}
bool getTrapStatus() {
diff --git a/client/config.ino b/client/config.ino
@@ -2,13 +2,18 @@
FlashStorage(config_flash, configuration);
-configuration default_config{
- /*.valid =*/true,
- /*.simPIN =*/"",
- /*.simPUK =*/"",
- /*.simAPN =*/"",
- /*.domain =*/"muizenval.tk",
- /*.userToken =*/""
+configuration config_default{
+ true, // valid
+ "", // token
+ "", // domain,
};
-configuration config_current;
-\ No newline at end of file
+configuration config;
+
+void configuration::open() {
+ *this = config_flash.read();
+}
+
+void configuration::save() {
+ config_flash.write(*this);
+}
+\ No newline at end of file
diff --git a/client/include/config.h b/client/include/config.h
@@ -25,7 +25,7 @@
#define lineDebug false // print each line to debug
#define blockDebug true // print if command is blocking
#define blinkInterval 0.25 // seconds to wait for blink
-#define gpsTimeout 5 // seconds to gps-timeout
+#define gpsTimeout 15 // seconds to gps-timeout
#define statusInterval 5 // send status every n seconds
#define ADC_AREF 3.3f
@@ -34,32 +34,18 @@
#define BATVOLT_PIN BAT_VOLT
#define batteryFactor (0.978 * (BATVOLT_R1 / BATVOLT_R2 + 1) / ADC_AREF)
-// -*- sim settings -*-
-//#define simPin "0000" // PIN of the sim
-//#define simAPN "lpwa.vodafone.iot" // APN-network of the sim
-//#define apiHostname "muizenval.tk"
-
-// -*- prefixes -*-
-#define prefixInfo "info | "
-#define prefixDebug "debug | "
-#define prefixError "error | "
-#define prefixLine "line | "
-#define prefixWarn "warn | "
-#define prefixEvent "event | "
-
struct configuration {
bool valid;
- char simPIN[4];
- char simPUK[8];
- char simAPN[50];
+ char token[17];
char domain[50];
- char userToken[16];
+ void open();
+ void save();
};
extern FlashStorageClass<configuration> config_flash;
extern configuration config_default;
-extern configuration config_current;
+extern configuration config;
diff --git a/client/include/interface.h b/client/include/interface.h
@@ -31,17 +31,19 @@ struct interface {
bool remoteReady = false;
bool modemReady = false;
- char debugToken[16];
-
void beginModem();
void beginRemote();
void endRemote();
public:
+ char token[17];
+
+ json request, response;
+
void begin();
- int send(method method, const char* endpoint, json body = nullptr, json& response = null_response);
+ int send(method method, const char* endpoint);
command_status remote(const char* command, json params = nullptr, json& response = null_response, command_flags flags = COMMAND_NONE);
diff --git a/client/include/macro.h b/client/include/macro.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#define strempty(s) ((s)[0] == '\0')
+\ No newline at end of file
diff --git a/client/interface.ino b/client/interface.ino
@@ -52,11 +52,14 @@ void interface::beginRemote() {
if (remoteReady) // already initalizised
return;
- writeLED(COLOR_RED);
- while (!usbSerial)
- ;
- writeLED(COLOR_YELLOW);
+ if (!usbSerial)
+ return;
+
+ json req;
+ req["token"] = config.token;
+ remote("set_token", req, null_response, COMMAND_FORCE);
+ writeLED(COLOR_MAGENTA);
remoteReady = true;
}
@@ -65,40 +68,33 @@ void interface::endRemote() {
return;
writeLED(COLOR_BLUE);
-
- json response;
- remote("hello", nullptr, response, COMMAND_FORCE);
- const char* debug = response["debugToken"];
- memcpy(debugToken, debug, sizeof(debugToken));
-
remoteReady = false;
}
-int interface::send(interface::method method, const char* endpoint, json body, json& response) {
- int code;
-
- if (usbSerial || !modemReady) {
+int interface::send(interface::method method, const char* endpoint) {
+ if (usbSerial) {
beginRemote();
- json request;
-
- body["debugToken"] = debugToken;
-
- request["method"] = method_strings[method];
- request["endpoint"] = endpoint;
- request["body"] = body;
+ json cmd_request;
+ cmd_request["method"] = method_strings[method];
+ cmd_request["endpoint"] = endpoint;
+ cmd_request["body"] = request;
json cmd_response;
- if (remote("send", request, cmd_response))
+ if (remote("send", cmd_request, cmd_response))
return 0;
+ request = nullptr;
response = cmd_response["body"];
return cmd_response["code"];
} else {
endRemote();
+ if (!modemReady) {
+ return 0;
+ }
// modem
+ return 1;
}
- return code;
}
interface::command_status interface::remote(const char* command, json params, json& response, command_flags flags) {
@@ -112,11 +108,17 @@ interface::command_status interface::remote(const char* command, json params, js
params.printTo(usbSerial);
usbSerial.print("\n");
- String status = usbSerial.readStringUntil(' ');
+ String line = usbSerial.readStringUntil('\n');
+ String status;
+ if (line.indexOf(' ') != -1) {
+ status = line.substring(0, line.indexOf(' '));
+ response = json::parse(line.substring(line.indexOf(' ') + 1));
+ } else {
+ status = line;
+ }
if (!status.length()) {
return interface::STATUS_TIMEOUT;
} else if (status == "ok") {
- response = json::parse(usbSerial.readStringUntil('\n'));
return interface::STATUS_OK;
} else {
response = status;
diff --git a/create-db.py b/create-db.py
@@ -1,14 +1,12 @@
-from random import randint
+from random import randint, choice
from server.app import db, bcrypt
-from server.models import User, UserType
+from server.models import User
-#name = input('Naam? ')
-#email = input('E-Mail? ')
-#typ = input('Type [admin,manager,technician,catcher,client]? ')
+TOKEN_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
users = [
- (1, UserType.CLIENT, 'Boer Herman', '[email protected]', 2),
- (2, UserType.ADMIN, 'Administrator Ralf', '[email protected]', None),
+ (1, False, 'Boer Herman', '[email protected]', 2),
+ (2, True, 'Administrator Ralf', '[email protected]', None),
]
address = 'Kerklaan 69\n9876XY Groningen'
@@ -17,19 +15,19 @@ hashed_password = bcrypt.generate_password_hash('hallo').decode('utf-8')
db.create_all()
-for id, typ, name, email, contact in users:
- phone = '06-' + str(randint(10000000, 99999999))
- user = User(
- id=id,
- type=typ,
- name=name,
- email=email,
- password=hashed_password,
- phone=phone,
- address = address,
- contact=contact
- )
- db.session.add(user)
+for id, admin, name, email, contact in users:
+ phone = '06-' + str(randint(10000000, 99999999))
+ user = User(
+ id=id,
+ admin=admin,
+ name=name,
+ email=email,
+ password=hashed_password,
+ phone=phone,
+ address=address,
+ contact=contact
+ )
+ db.session.add(user)
db.session.commit()
print('Added')
diff --git a/directories.txt b/directories.txt
@@ -0,0 +1,5 @@
+/build - temporary build directory for Arduino IDE
+/client - arduino code for sara
+/dump - thing I don't want to delete but are unneeded
+/server - the main Flask server
+/ssl - https certificates
+\ No newline at end of file
diff --git a/esp-client/boot.py b/dump/esp-client/boot.py
diff --git a/esp-client/config.py b/dump/esp-client/config.py
diff --git a/esp-client/firmware/esp32.bin b/dump/esp-client/firmware/esp32.bin
Binary files differ.
diff --git a/esp-client/firmware/readme.txt b/dump/esp-client/firmware/readme.txt
diff --git a/esp-client/main.py b/dump/esp-client/main.py
diff --git a/esp-client/readme b/dump/esp-client/readme
diff --git a/dump/remote.ino b/dump/remote.ino
@@ -26,6 +26,7 @@ void serial_remote::begin() {
if (res_json["error"] != nullptr) {
// :(
}
+ write
}
bool serial_remote::available() {
diff --git a/readme.md b/readme.md
@@ -16,7 +16,7 @@ $ git clone https://github.com/friedelschoen/muizenval.tk/
**Alle afhankelijkheden installeren:**
```
-$ pip3 install flask wtforms flask_sqlalchemy flask-wtf email_validator flask-bcrypt flask-login pillow flask_socketio simple-websocket gevent-websocket
+$ pip3 install flask wtforms flask_sqlalchemy flask-wtf email_validator flask-bcrypt flask-login pillow flask_socketio simple-websocket gevent-websocket flask-sslify
```
**Is de database leeg? Test-gebruikers toevoegen:**
diff --git a/remote.py b/remote.py
@@ -1,37 +1,143 @@
+from threading import Thread
+from time import sleep
+import tkinter as tk
+from tkinter import Button, OptionMenu, StringVar, Tk, Label
from http.client import HTTPConnection
+from typing import Optional
+
+from remote import Remote
-import serial
-import random
-import sys
import json
+import sys
+import asyncio
+import websockets
+
+
+WEBSOCKET_PORT = 1612
+
+client = HTTPConnection('localhost', 5000)
+remote = Remote(115200)
+token: Optional[str] = None
+
+
[email protected]("set_token")
+def set_token(req):
+ global token
+ token = req['token']
+
+
[email protected]("send")
+def send_http(params):
+ method, endpoint, body = params["method"], params["endpoint"], params["body"]
+
+ print(body)
+
+ client.request(method, endpoint, json.dumps(body))
+ res = client.getresponse()
+ response = json.load(res)
+
+ print(response)
+
+ return dict(code=res.status, body=response)
+
+
+token = 'abcdefghijklmnoq'
+
+
+async def websocket_handler(ws, _):
+ if await ws.recv() == 'token':
+ if token:
+ await ws.send(token)
+ else:
+ await ws.send(None)
+ await ws.close()
+
+
+class RemoteWindow(Tk):
+ running = False
+ closed = False
+ disconnecting = False
+
+ def __init__(self):
+ super().__init__()
+
+ self.title('Team Benni - Remote')
+ self.geometry('500x100')
+ self.protocol("WM_DELETE_WINDOW", self.on_close)
+
+ self.columnconfigure(0, weight=1)
+ self.columnconfigure(1, weight=3)
+# self.columnconfigure(2, weight=3)
+
+ self.devices = Remote.list_ports()
+ self.device_names = [
+ f'{p.name} ({p.description})' for p in self.devices]
+
+ self.dev_var = StringVar(self, self.device_names[0])
+
+ self.label = Label(self, text='Not connected')
+ self.label['anchor'] = tk.CENTER
+ self.label.grid(column=0, row=0, sticky=tk.W,
+ padx=5, pady=5, columnspan=2)
+
+ self.dev_label = Label(self, text='Device:')
+ self.dev_label.grid(column=0, row=1, sticky=tk.E, padx=5, pady=5)
+
+ self.dev_menu = OptionMenu(self, self.dev_var, *self.device_names)
+ self.dev_menu.grid(column=1, row=1, sticky=tk.E, padx=5, pady=5)
+
+ self.connect_button = Button(
+ self, text="Connect", command=self.on_connect)
+ self.connect_button.grid(column=1, row=3, sticky=tk.E, padx=5, pady=5)
+
+ async def run_websocket(self):
+ async with websockets.serve(websocket_handler, '0.0.0.0', # type: ignore
+ WEBSOCKET_PORT):
+ while self.running:
+ await asyncio.sleep(1)
+
+ def on_connect(self):
+ if self.disconnecting:
+ return
+ self.running = not self.running
+ if self.running:
+ port = self.devices[self.device_names.index(self.dev_var.get())]
+
+ self.websocket_thread = Thread(
+ target=lambda: asyncio.run(self.run_websocket()))
+ self.remote_thread = Thread(
+ target=lambda: remote.run(port.device))
+
+ self.websocket_thread.start()
+ self.remote_thread.start()
-if len(sys.argv) < 2:
- print(f'{sys.argv[0]} <serial>')
+ self.label['text'] = f'Connected to {port.name}'
+ if port.description != 'n/a':
+ self.label['text'] += f' ({port.description})'
+ self.connect_button['text'] = 'Disconnect'
+ else:
+ remote.stop()
+ self.disconnecting = True
-server_address = 'localhost', 5000
-serial_port = serial.Serial(port=sys.argv[1], baudrate=115200)
-debug_chars = '0123456789abcdefghijklmnopqrstuvwxyz'
+ self.connect_button['text'] = 'Disconnecting...'
-client = HTTPConnection(server_address[0], server_address[1])
+ def on_close(self):
+ if self.running:
+ self.on_connect()
-debug_token = ''.join(random.choice(debug_chars) for _ in range(16))
+ self.closed = True
-while serial_port.is_open:
- try:
- command, params_raw = serial_port.readline().decode().split(' ', 1)
- params = json.loads(params_raw)
+ def run(self):
+ while not self.closed or self.disconnecting:
+ if self.disconnecting and not self.remote_thread.is_alive() and not self.websocket_thread.is_alive():
+ self.label['text'] = 'Not connected'
+ self.connect_button['text'] = 'Connect'
+ self.disconnecting = False
- if command == 'hello':
- serial_port.write(f'ok {json.dumps(dict(debugToken=debug_token))}\n'.encode())
- elif command == 'send':
- method, endpoint, body = params["method"], params["endpoint"], params["body"]
- print(f'-> {method} {endpoint} {body}')
+ sleep(0.1)
+ self.update()
- client.request(method, endpoint, json.dumps(body))
- res = client.getresponse()
- response = res.read().decode()
- print(f'<- {res.status} {response}')
- serial_port.write(f'ok {json.dumps(dict(code=res.status, body=response))}\n'.encode())
- except:
- serial_port.write(b'0 {}\n')
+if __name__ == "__main__":
+ win = RemoteWindow()
+ win.run()
diff --git a/remote/__init__.py b/remote/__init__.py
@@ -0,0 +1,79 @@
+from datetime import datetime
+from typing import Any, Callable, Dict, Optional
+from serial import Serial
+from serial.tools.list_ports import comports
+from .exception import RemoteException
+
+import json
+import traceback
+
+
+CommandHandler = Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
+
+
+class Remote:
+ commands: Dict[str, CommandHandler] = dict()
+ running = True
+
+ @staticmethod
+ def list_ports():
+ return comports()
+
+ def __init__(self, baud: int):
+ self.baud = baud
+
+ def command(self, name: str):
+ def inner(func: CommandHandler):
+ self.commands[name] = func
+ return func
+
+ return inner
+
+ def run(self, serial_path, timeout=1):
+ serial = Serial(port=serial_path, baudrate=self.baud, timeout=timeout)
+ self.running = True
+ while self.running:
+ command = ''
+ status = ''
+ params = None
+ response = None
+ try:
+ line = serial.readline().decode(errors='ignore')
+ try:
+ if line == '':
+ continue
+ if ' ' in line:
+ command, params_raw = line.split(' ', 1)
+ params = json.loads(params_raw)
+ else:
+ command = line
+ params = dict()
+ except json.JSONDecodeError:
+ raise RemoteException('bad-request')
+
+ if command not in self.commands:
+ raise RemoteException('bad-command')
+
+ res = self.commands[command](params) or dict()
+
+ status = 'ok'
+ response = json.dumps(res)
+ except RemoteException as err:
+ status = err.name
+ except KeyboardInterrupt:
+ break
+ except Exception as err:
+ print(f'Error handling {command} ({params}):')
+ traceback.print_exc()
+ status = 'unknown'
+
+ print(
+ f'{datetime.now().strftime("%d/%m/%y %H:%M:%S")} | {command} -> { status }')
+ serial.write(status.encode())
+ if response is not None:
+ serial.write(b' ' + response.encode())
+ serial.write(b'\n')
+ serial.close()
+
+ def stop(self):
+ self.running = False
diff --git a/remote/exception.py b/remote/exception.py
@@ -0,0 +1,5 @@
+class RemoteException(Exception):
+ name: str
+
+ def __init__(self, name: str):
+ self.name = name
diff --git a/run-server.py b/run-server.py
@@ -1,4 +1,13 @@
+import sys
+
from server.app import socket, app
-if __name__ == '__main__':
- socket.run(app, "0.0.0.0", 80, debug=True)
+if len(sys.argv) > 1:
+ if not sys.argv[1].isnumeric():
+ print(f'Usage: {sys.argv[0]} [port=80]')
+ exit(1)
+ port = int(sys.argv[1])
+else:
+ port = 80
+
+socket.run(app, "0.0.0.0", port, debug=True)
diff --git a/server/app.py b/server/app.py
@@ -4,17 +4,23 @@ from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_socketio import SocketIO
+domain = 'muizenval.tk'
+
app = Flask(__name__)
app.config['SECRET_KEY'] = 'iot_project'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
-socket = SocketIO(app)
+socket = SocketIO(app, logger=True)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'
+#sslify = SSLify(app)
+#ssl_files = ('ssl/public.crt', 'ssl/private.key')
# to run 'routes.py' and make the routes available
-from .routes import *
-from .models import *
+# '#noqa' is nessesary for my formatter to not put them to the top!
+from .models import * # noqa
+from .routes import * # noqa
+from .socket import * # noqa
diff --git a/server/forms.py b/server/forms.py
@@ -5,28 +5,38 @@ from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationE
from .models import User
+current_user: User
+
""" registration form for register.html """
+
+
class RegistrationForm(FlaskForm):
- name = StringField('Naam', validators=[ DataRequired(), Length(min=5, max=20) ])
- email = StringField('E-Mail', validators=[ DataRequired(), Email() ])
- password = PasswordField('Wachtwoord', validators=[ DataRequired() ])
- confirm_password = PasswordField('Wachtwoord herhalen', validators=[ DataRequired(), EqualTo('password') ])
- phone = StringField('Telefoon', validators=[ DataRequired(), Length(min=5) ])
- street = StringField('Straat', validators=[ DataRequired() ])
- housenumber = IntegerField('Huisnummer', validators=[ DataRequired() ])
- postcode = StringField('Postcode', validators=[ DataRequired() ])
- place = StringField('Plaats', validators=[ DataRequired() ])
+ name = StringField('Naam', validators=[
+ DataRequired(), Length(min=5, max=20)])
+ email = StringField('E-Mail', validators=[DataRequired(), Email()])
+ password = PasswordField('Wachtwoord', validators=[DataRequired()])
+ confirm_password = PasswordField('Wachtwoord herhalen', validators=[
+ DataRequired(), EqualTo('password')])
+ phone = StringField('Telefoon', validators=[DataRequired(), Length(min=5)])
+ street = StringField('Straat', validators=[DataRequired()])
+ housenumber = IntegerField('Huisnummer', validators=[DataRequired()])
+ postcode = StringField('Postcode', validators=[DataRequired()])
+ place = StringField('Plaats', validators=[DataRequired()])
submit = SubmitField('Registeren')
""" validates whether name is already in use """
+
def validate_name(self, name):
if User.query.filter_by(name=name.data).first():
- raise ValidationError('Deze gebruikersnaam bestaat al, kies een andere.')
+ raise ValidationError(
+ 'Deze gebruikersnaam bestaat al, kies een andere.')
""" validates whether e-mail is already in use """
+
def validate_email(self, email):
if User.query.filter_by(email=email.data).first():
- raise ValidationError('Deze e-mail bestaat al, log in als dat uw e-mail is.')
+ raise ValidationError(
+ 'Deze e-mail bestaat al, log in als dat uw e-mail is.')
def validate_phone(self, phone):
for c in phone.data:
@@ -39,31 +49,43 @@ class RegistrationForm(FlaskForm):
""" login form for login.html """
+
+
class LoginForm(FlaskForm):
- email = StringField('E-Mail', validators=[ DataRequired(), Email() ])
- password = PasswordField('Wachtwoord', validators=[ DataRequired() ])
+ email = StringField('E-Mail', validators=[DataRequired(), Email()])
+ password = PasswordField('Wachtwoord', validators=[DataRequired()])
remember = BooleanField('Herinneren')
submit = SubmitField('Inloggen')
""" update account form for account.html """
+
+
class UpdateAccountForm(FlaskForm):
- name = StringField('Naam', validators=[ DataRequired(), Length(min=2, max=20) ])
- email = StringField('E-Mail', validators=[ DataRequired(), Email() ])
+ name = StringField('Naam', validators=[
+ DataRequired(), Length(min=2, max=20)])
+ email = StringField('E-Mail', validators=[DataRequired(), Email()])
password = PasswordField('Wachtwoord', validators=[])
- confirm_password = PasswordField('Wachtwoord herhalen', validators=[ EqualTo('password') ])
- picture = FileField('Profielfoto bewerken', validators=[ FileAllowed(['jpg', 'png']) ])
+ confirm_password = PasswordField(
+ 'Wachtwoord herhalen', validators=[EqualTo('password')])
+ picture = FileField('Profielfoto bewerken', validators=[
+ FileAllowed(['jpg', 'png'])])
submit = SubmitField('Bewerken')
""" validates whether name is already in use """
+
def validate_name(self, name):
if name.data != current_user.name and User.query.filter_by(name=name.data).first():
- raise ValidationError('Deze gebruikersnaam bestaat al, kies een andere.')
+ raise ValidationError(
+ 'Deze gebruikersnaam bestaat al, kies een andere.')
""" validates whether e-mail is already in use """
+
def validate_email(self, email):
if email.data != current_user.email and User.query.filter_by(email=email.data).first():
- raise ValidationError('Deze e-mail bestaat al, log in als dat uw e-mail is')
+ raise ValidationError(
+ 'Deze e-mail bestaat al, log in als dat uw e-mail is')
+
class UpdateTrapForm(FlaskForm):
mac = StringField('MAC')
@@ -71,19 +93,20 @@ class UpdateTrapForm(FlaskForm):
location = StringField('Locatie')
submit = SubmitField('Bewerken')
-class ConnectTrapForm(FlaskForm):
- code = StringField('Koppel-Code', validators=[ Length(min=16, max=16) ])
- submit = SubmitField('Verbinden')
-
+""" search form for admin.html """
-""" search form for admin.html """
class SearchForm(FlaskForm):
- username = StringField('Naam', validators=[ DataRequired(), Length(min=2, max=20)])
+ username = StringField('Naam', validators=[
+ DataRequired(), Length(min=2, max=20)])
submit = SubmitField('Zoeken')
+
""" account-settings form for admin_user.html """
+
+
class AdminForm(FlaskForm):
- type = SelectField('Type', choices=[('client', 'Klant'), ('admin', 'Administrator')])
+ type = SelectField('Type', choices=[
+ ('client', 'Klant'), ('admin', 'Administrator')])
submit = SubmitField('Bewerken')
diff --git a/server/models.py b/server/models.py
@@ -1,55 +1,77 @@
-from enum import Enum
+from datetime import datetime
+from typing import Any, Dict, Optional
from flask_login import UserMixin
from .app import db, login_manager
-""" function to load a user from database """
-@login_manager.user_loader
-def load_user(user_id):
- return User.query.get(int(user_id))
-
-class UserType(Enum):
- ADMIN = 0
- CLIENT = 1
class User(db.Model, UserMixin):
- id = db.Column(db.Integer, primary_key=True)
- type = db.Column(db.Enum(UserType), nullable=False, default=UserType.CLIENT)
- email = db.Column(db.String(120), unique=True, nullable=False)
- name = db.Column(db.String(20), unique=True, nullable=False)
- password = db.Column(db.String(60), nullable=False)
- image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
- phone = db.Column(db.Text, nullable=False)
- address = db.Column(db.Text)
+ id: int = db.Column(db.Integer, primary_key=True)
+ admin: bool = db.Column(db.Boolean, nullable=False, default=False)
+ email: str = db.Column(db.String(120), unique=True, nullable=False)
+ name: str = db.Column(db.String(20), unique=True, nullable=False)
+ password: str = db.Column(db.String(60), nullable=False)
+ image_file: str = db.Column(db.String(20), nullable=False,
+ default='default.jpg')
+ phone: str = db.Column(db.Text, nullable=False)
+ address: Optional[str] = db.Column(db.Text)
- contact = db.Column(db.Integer, db.ForeignKey('user.id')) # set if user
+ contact: Optional[int] = db.Column(
+ db.Integer, db.ForeignKey('user.id')) # set if user
def contact_class(self):
return User.query.filter_by(id=self.contact).first()
class Trap(db.Model):
- mac = db.Column(db.String(16), primary_key=True, nullable=False)
- name = db.Column(db.Text)
- last_heartbeat = db.Column(db.DateTime)
- caught = db.Column(db.Boolean, nullable=False, default=False)
- owner = db.Column(db.Integer, db.ForeignKey('user.id'))
- connect_expired = db.Column(db.DateTime)
- connect_code = db.Column(db.String(5))
- location_lat = db.Column(db.Float)
- location_lon = db.Column(db.Float)
-
- def pretty_mac(self):
- upper = self.mac.upper()
- return ':'.join([ upper[i] + upper[i+1] for i in range(0, len(upper), 2) ])
-
- def owner_class(self):
- return User.query.filter_by(id=self.owner).first()
-
- def status_color(self):
+ id: int = db.Column(db.Integer, primary_key=True)
+ token: str = db.Column(db.String(16), unique=True, nullable=False)
+ owner: Optional[int] = db.Column(db.Integer, db.ForeignKey('user.id'))
+ name: Optional[str] = db.Column(db.Text)
+
+ last_status: Optional[datetime] = db.Column(db.DateTime)
+ caught: Optional[bool] = db.Column(db.Boolean)
+ battery: Optional[int] = db.Column(db.Integer)
+ charging: Optional[bool] = db.Column(db.Boolean)
+ temperature: Optional[int] = db.Column(db.Integer)
+ location_lat: Optional[float] = db.Column(db.Float)
+ location_lon: Optional[float] = db.Column(db.Float)
+ location_acc: Optional[float] = db.Column(db.Float)
+ location_satellites: Optional[int] = db.Column(db.Integer)
+
+ def owner_class(self) -> Optional[User]:
+ return User.query.get(self.owner)
+
+ def status_color(self) -> str:
if self.caught:
return '#f4a900'
return 'currentColor'
- def dict(self):
- return { c.name: getattr(self, c.name) for c in self.__table__.columns }
+ def dict(self) -> Dict[str, Any]:
+ return {c.name: getattr(self, c.name) for c in self.__table__.columns}
+
+ def to_json(self, token: bool = False):
+ owner = self.owner_class()
+ owner_name = owner.name if owner else '{nobody}'
+
+ return dict(
+ id=self.id,
+ name=self.name or '<code>unnamed</code>',
+ status=self.status_color(),
+ location=self.location_acc and self.location_acc > 0,
+ latitude=self.location_lat,
+ longitude=self.location_lon,
+ accuracy=self.location_acc,
+ satellites=self.location_satellites,
+ activated=self.caught,
+ owner=owner_name,
+ battery=self.battery,
+ charging=self.charging,
+ temperature=self.temperature,
+ byToken=token
+ )
+
+
+@login_manager.user_loader
+def load_user(user_id: int) -> User:
+ return User.query.get(user_id)
diff --git a/server/routes.py b/server/routes.py
@@ -1,87 +1,44 @@
-import random
-import os
-import secrets
-from datetime import datetime, timedelta
-import string
-
-from flask import flash, redirect, render_template, request, url_for, jsonify
+from datetime import datetime
+from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required, login_user, logout_user
from PIL import Image
-from .app import app, bcrypt, db, socket
-from .forms import AdminForm, ConnectTrapForm, LoginForm, RegistrationForm, SearchForm, UpdateAccountForm, UpdateTrapForm
-from .models import Trap, User, UserType
-
-def clean_traps():
- query = Trap.query.filter((Trap.connect_expired < datetime.utcnow()) & (Trap.owner == None))
- i = len(query.all())
- query.delete()
- db.session.commit()
- print(f'[*] {i} traps cleaned')
-
-def validate_mac(mac):
- return len(mac) == 16 and all(c in string.hexdigits for c in mac)
-
-
[email protected]("/api/update_status", methods=['POST', 'GET'])
-def update_status():
- if not request.json:
- return jsonify({ "error": "invalid-json" })
- if not validate_mac(request.json['mac']):
- return jsonify({ "error": "invalid-mac" })
- trap = Trap.query.filter_by(mac=request.json['mac'].lower()).first()
- if not trap:
- return jsonify({ "error": "not-found" })
-
- trap.caught = request.json['status']
- db.session.commit()
-
- if trap.owner:
- socket.emit('trap-change', { 'user': trap.owner })
+from .app import app, bcrypt, db
+from .forms import AdminForm, LoginForm, RegistrationForm, SearchForm, UpdateAccountForm, UpdateTrapForm
+from .models import Trap, User
- return jsonify({ "error": "ok" })
-
[email protected]("/api/search_connect", methods=['POST', 'GET'])
-def search_connect():
- if not request.json:
- return jsonify({ "error": "invalid-json" })
- if not validate_mac(request.json['mac']):
- return jsonify({ "error": "invalid-mac" })
-
- mac = request.json['mac'].lower()
+import secrets
+import os
+import random
+import string
- trap = Trap.query.filter_by(mac=mac).first()
- if not trap:
- trap = Trap(mac=mac)
- db.session.add(trap)
+current_user: User
- code = ""
- while True:
- code = ''.join([ random.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(5) ])
- if not Trap.query.filter_by(connect_code=code).first():
- break
- trap.owner = None
- trap.connect_expired = datetime.utcnow() + timedelta(minutes=5)
- trap.connect_code = code
+def validate_mac(mac):
+ return len(mac) == 16 and all(c in string.hexdigits for c in mac)
- db.session.commit()
- return jsonify({ "error": "ok" })
+""" index.html (home-page) route """
-""" index.html (home-page) route """
@app.route("/")
def index():
form = LoginForm()
return render_template('index.html', form=form)
+
""" about.html route """
+
+
@app.route("/about")
def about():
return render_template('about.html', title='Over ons')
+
""" register.html route """
+
+
@app.route("/register", methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
@@ -90,14 +47,15 @@ def register():
form = RegistrationForm()
if form.validate_on_submit():
- hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ hashed_password = bcrypt.generate_password_hash(
+ form.password.data).decode('utf-8')
address = f"{form.street} {form.housenumber}\n{form.postcode} {form.place}"
user = User(
- name=form.name.data,
- email=form.email.data,
+ name=form.name.data,
+ email=form.email.data,
password=hashed_password,
phone=form.phone.data,
- address = address
+ address=address
)
db.session.add(user)
db.session.commit()
@@ -110,7 +68,10 @@ def register():
def producten():
return render_template('producten.html')
+
""" login.html route """
+
+
@app.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
@@ -122,25 +83,33 @@ def login():
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
if bcrypt.check_password_hash(user.password, form.email.data):
- flash('Wij zullen aanbevelen uw wachtwoord weer te veranderen', 'warning')
+ flash(
+ 'Wij zullen aanbevelen uw wachtwoord weer te veranderen', 'warning')
next_page = request.args.get('next')
return redirect(next_page if next_page else '/')
else:
flash('Kon niet inloggen, is uw e-mail en wachtwoord juist?', 'danger')
return render_template('login.html', title='Inloggen', form=form)
+
""" logout route """
+
+
@app.route("/logout")
def logout():
logout_user()
return redirect('/')
+
""" save-picture function for account.html """
+
+
def save_picture(form_picture):
random_hex = secrets.token_hex(8)
_, f_ext = os.path.splitext(form_picture.filename)
picture_fn = random_hex + f_ext
- picturepath = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+ picturepath = os.path.join(
+ app.root_path, 'static/profile_pics', picture_fn)
output_size = (125, 125)
i = Image.open(form_picture)
@@ -149,8 +118,11 @@ def save_picture(form_picture):
return picture_fn
+
""" account.html route """
[email protected]("/user/self", methods=[ 'GET', 'POST' ])
+
+
[email protected]("/user/self", methods=['GET', 'POST'])
@login_required
def account():
form = UpdateAccountForm()
@@ -161,7 +133,8 @@ def account():
picture_file = save_picture(form.picture.data)
current_user.image_file = picture_file
if form.password.data:
- current_user.password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ current_user.password = bcrypt.generate_password_hash(
+ form.password.data).decode('utf-8')
db.session.commit()
flash('Uw profiel is bewerkt!', 'success')
return redirect(url_for('account'))
@@ -169,29 +142,33 @@ def account():
elif request.method == 'GET':
form.name.data = current_user.name
form.email.data = current_user.email
- image_file = url_for('static', filename='profile_pics/' + current_user.image_file)
+ image_file = url_for(
+ 'static', filename='profile_pics/' + current_user.image_file)
return render_template('account.html', title='Profiel', image_file=image_file, form=form)
@app.route('/traps')
@login_required
def traps():
- if current_user.type == UserType.ADMIN:
- clean_traps()
+ if current_user.admin:
+ # clean_traps()
query = Trap.query.all()
else:
query = Trap.query.filter_by(owner=current_user.id)
- trap_json = [ trap.dict() for trap in query ]
+ trap_json = [trap.dict() for trap in query]
return render_template('trap.html', traps=query, trap_json=trap_json)
+
+"""
@app.route('/traps/connect', methods=['POST', 'GET'])
@login_required
def trap_connect():
form = ConnectTrapForm()
if form.validate_on_submit() and form.code.data:
- trap = Trap.query.filter_by(mac=form.code.data.replace(':', '').replace(' ', '').lower()).filter(Trap.connect_expired > datetime.utcnow()).first()
+ trap = Trap.query.filter_by(mac=form.code.data.replace(':', '').replace(
+ ' ', '').lower()).filter(Trap.connect_expired > datetime.utcnow()).first()
if not trap:
flash('Muizenval niet gevonden', 'danger')
return redirect(url_for('trap_connect'))
@@ -204,6 +181,7 @@ def trap_connect():
return redirect(url_for('traps'))
return render_template('connect.html', form=form)
+"""
@app.route('/trap/<trap_id>/update', methods=['POST', 'GET'])
@@ -215,7 +193,8 @@ def trap_update(trap_id):
trap.name = form.name.data
print(form.location.data)
if form.location.data:
- trap.location_lat, trap.location_lon = form.location.data.split(' ', 2)
+ trap.location_lat, trap.location_lon = form.location.data.split(
+ ' ', 2)
db.session.commit()
return redirect(url_for('traps'))
elif not trap:
@@ -226,15 +205,17 @@ def trap_update(trap_id):
form.name.data = trap.name
return render_template('updatetrap.html', form=form, trap=trap)
+
@app.route('/trap/<trap_id>/delete')
@login_required
def trap_delete(trap_id):
trap = Trap.query.filter_by(mac=trap_id.lower()).first()
db.session.delete(trap)
db.session.commit()
-
+
return redirect(url_for('traps'))
+
@app.route('/contact')
@login_required
def contact():
@@ -242,46 +223,56 @@ def contact():
""" admin.html route """
[email protected]("/users", methods=['GET','POST'])
+
+
[email protected]("/users", methods=['GET', 'POST'])
@login_required
def admin():
- if current_user.type != UserType.ADMIN:
+ if not current_user.admin:
flash('U mag deze website niet bereiken', 'error')
return redirect('/')
form = SearchForm()
if form.validate_on_submit():
user = User.query.filter_by(name=form.username.data).first()
if user == None:
- flash(f'Geen gebrukers gevonden met de gebruikersnaam: {form.username.data}!', 'danger')
+ flash(
+ f'Geen gebrukers gevonden met de gebruikersnaam: {form.username.data}!', 'danger')
else:
- flash(f'Gebruiker gevonden met gebruikersnaam: {form.username.data}!', 'success')
- return redirect(url_for('admin_user', user_id= user.id))
+ flash(
+ f'Gebruiker gevonden met gebruikersnaam: {form.username.data}!', 'success')
+ return redirect(url_for('admin_user', user_id=user.id))
return render_template('admin.html', form=form)
+
""" account-admin route """
[email protected]("/user/<int:user_id>", methods=['GET','POST'])
+
+
[email protected]("/user/<int:user_id>", methods=['GET', 'POST'])
@login_required
def admin_user(user_id):
- if current_user.type != UserType.ADMIN:
+ if not current_user.admin:
flash('U mag deze website niet bereiken', 'error')
return redirect('/')
form = AdminForm()
user = User.query.filter_by(id=user_id).first()
image_file = url_for('static', filename='profile_pics/' + user.image_file)
if form.validate_on_submit():
- user.type = form.type.data
+ user.admin = form.type.data == 'admin'
db.session.commit()
flash(f'De gebruiker {user.username} is nu een {user.type}', 'success')
return redirect(url_for('admin'))
elif request.method == 'GET':
- form.type.data = user.type
+ form.type.data = 'admin' if user.admin else 'client'
return render_template('admin_user.html', form=form, user=user, image_file=image_file)
+
""" delete-user route """
[email protected]("/user/<int:user_id>/delete", methods=['GET','POST'])
+
+
[email protected]("/user/<int:user_id>/delete", methods=['GET', 'POST'])
@login_required
def delete_user(user_id):
- if current_user.type != UserType.ADMIN:
+ if not current_user.admin:
flash('U mag deze website niet bereiken', 'danger')
return redirect('/')
user = User.query.get_or_404(user_id)
@@ -290,11 +281,14 @@ def delete_user(user_id):
flash(f'De gebruiker {user.username} werd verwijdert', 'success')
return redirect(url_for('admin'))
+
""" reset user's password route """
[email protected]("/user/<int:user_id>/reset", methods=['GET','POST'])
+
+
[email protected]("/user/<int:user_id>/reset", methods=['GET', 'POST'])
@login_required
def reset_user(user_id):
- if current_user.type != UserType.ADMIN:
+ if not current_user.admin:
flash('U mag deze website niet bereiken', 'danger')
return redirect('/')
user = User.query.get_or_404(user_id)
@@ -305,7 +299,9 @@ def reset_user(user_id):
""" 404 not found handler """
+
+
@app.errorhandler(404)
def not_found(error):
flash(f"Deze pagina werd niet gevonden", 'danger')
- return index() # geen redirect om de '/bla' te houden
+ return index() # geen redirect om de '/bla' te houden
diff --git a/server/site.db b/server/site.db
Binary files differ.
diff --git a/server/socket.py b/server/socket.py
@@ -0,0 +1,126 @@
+from datetime import datetime, timedelta
+import random
+from typing import Dict
+from flask import request, jsonify
+from flask_login import current_user
+from flask_socketio import emit, Namespace
+
+from .app import app, db, socket, domain
+from .models import Trap, User
+
+current_user: User
+
+sockets: Dict[int, Namespace] = {}
+
+
+def make_token():
+ return ''.join(random.choice('0123456789abcdefghijklmnopqrstuvwxyz') for _ in range(16))
+
+
[email protected]("/api/hello")
+def register_trap():
+ req = request.get_json(True)
+ if not req:
+ return jsonify(dict(error='invalid-request'))
+
+ res = dict()
+ if 'token' not in req or not req['token'] or not Trap.query.filter_by(token=req['token']).first():
+ while True:
+ token = make_token()
+ if not Trap.query.filter_by(token=token).first():
+ break
+ trap = Trap(token=token)
+ db.session.add(trap)
+ db.session.commit()
+ res['token'] = token
+
+ if 'domain' not in req or req['domain'] != domain:
+ res['domain'] = domain
+
+ return jsonify(res)
+
+
[email protected]("/api/update")
+def update_status():
+ req = request.get_json(True)
+ if not req:
+ return jsonify(dict(error='invalid-request'))
+
+ trap: Trap = Trap.query.filter_by(token=req['token']).first()
+ if not trap:
+ return jsonify(dict(error='invalid-token'))
+
+ trap.caught = req['trap']
+ trap.battery = req['battery']
+ trap.temperature = req['temperature']
+ trap.charging = req['charging']
+ trap.location_lat = req['latitude']
+ trap.location_lon = req['longitude']
+ trap.location_acc = req['accuracy']
+ trap.location_satellites = req['satellites']
+
+ db.session.commit()
+
+ if trap.owner and trap.owner in sockets:
+ sockets[trap.owner].emit('trap-change', trap.to_json())
+
+ return jsonify(dict())
+
+
+"""@app.route("/api/search_connect", methods=['POST', 'GET'])
+def search_connect():
+ if not request.json:
+ return jsonify({"error": "invalid-json"})
+ # if not validate_mac(request.json['mac']):
+ # return jsonify({"error": "invalid-mac"})
+
+ mac = request.json['mac'].lower()
+
+ trap = Trap.query.filter_by(mac=mac).first()
+ if not trap:
+ trap = Trap(mac=mac)
+ db.session.add(trap)
+
+ code = ""
+ while True:
+ code = ''.join(random.choice(
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(5))
+ if not Trap.query.filter_by(connect_code=code).first():
+ break
+
+ trap.owner = None
+ trap.connect_expired = datetime.utcnow() + timedelta(minutes=5)
+ trap.connect_code = code
+
+ db.session.commit()
+
+ return jsonify({"error": "ok"})
+"""
+
+
[email protected]('connect')
+def socket_connect():
+ if not current_user.is_authenticated:
+ return
+
+ sockets[current_user.id] = request.namespace # type: ignore
+
+ for trap in Trap.query.filter_by(owner=current_user.id):
+ emit('trap-change', trap.to_json())
+
+
[email protected]('disconnect')
+def socket_disconnect():
+ if not current_user.is_authenticated:
+ return
+
+ del sockets[current_user.id]
+
+
[email protected]('token')
+def socket_token(token):
+ if not token or not current_user.is_authenticated:
+ return
+
+ for trap in Trap.query.filter_by(token=token):
+ emit('trap-change', trap.to_json(True))
diff --git a/server/static/main.css b/server/static/main.css
@@ -98,6 +98,7 @@ a.article-title:hover {
#trap-map {
height: 300px;
+ border: #aaa solid 1px;
}
#sidebar {
diff --git a/server/static/trap.js b/server/static/trap.js
@@ -0,0 +1,97 @@
+/*
+trap: {
+ id: int,
+ name: string,
+ status: string (color),
+ location: bool,
+ latitude: float,
+ longitude: float,
+ accuracy: float,
+ activated: bool,
+ owner: string,
+ battery: int (percent) | null,
+ charging: bool,
+ temperature: int,
+ byToken: bool
+}
+*/
+
+function addTrap(trap) {
+ var clone,
+ append = false;
+
+ if (traps[trap.id]) {
+ clone = traps[trap.id].element;
+ } else {
+ clone = document.getElementById('trap-template').content.cloneNode(true);
+ append = true;
+ }
+
+ traps[trap.id] = trap;
+ traps[trap.id].element = clone;
+
+ clone.id = `trap-${trap.id}`;
+
+ clone.querySelector('a.link').href = `/trap/${trap.id}/update`;
+ clone.querySelector('svg').fill = trap.status;
+ clone.querySelector('span.name').innerHTML = trap.name;
+ clone.querySelector('span.owner').innerHTML = trap.byToken ? `<strike>${trap.owner}</strike> <a href='#'>Register!</a>` : trap.owner;
+ clone.querySelector('span.accuracy').innerHTML = trap.accuracy;
+ clone.querySelector('span.battery').innerHTML = trap.battery;
+ clone.querySelector('span.satellites').innerHTML = trap.satellites;
+ clone.querySelector('span.charging').innerHTML = trap.charging ? 'yes' : 'no';
+ clone.querySelector('span.temperature').innerHTML = trap.temperature;
+
+ if (append) document.getElementById('trap-container').append(clone);
+
+ if (trap.location) {
+ traps[trap.id].marker = L.marker([trap.latitude, trap.longitude]).addTo(map).bindPopup(trap.name);
+ map.fitBounds(
+ Object.values(traps)
+ .filter((x) => x.location)
+ .map((x) => [x.latitude, x.longitude])
+ );
+ }
+}
+
+function removeTrap(trap) {
+ if (traps[trap.id].marker) traps[trap.id].marker.remove();
+ traps[trap.id].element.remove();
+
+ delete traps[trap.id];
+}
+const successDelay = 10000;
+const errorDelay = 2500;
+function openWebSocket() {
+ let ws = new WebSocket('ws://localhost:1612/');
+ ws.addEventListener('open', () => ws.send('token'));
+ ws.addEventListener('message', (evt) => (token = evt.data));
+ ws.addEventListener('close', () => {
+ socket.emit('token', token);
+ if (token) remote = true;
+ setTimeout(openWebSocket, successDelay);
+ });
+ ws.addEventListener('error', () => {
+ token = null;
+ remote = false;
+ setTimeout(openWebSocket, errorDelay);
+ });
+}
+
+var map = L.map('trap-map'),
+ socket = io();
+
+let token = null,
+ remote = false,
+ traps = {},
+ markers = [];
+
+map.setView([52.283333, 5.666667], 7);
+L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
+}).addTo(map);
+
+socket.on('trap-change', addTrap);
+socket.on('trap-remove', removeTrap);
+
+openWebSocket();
diff --git a/server/templates/backup.html b/server/templates/backup.html
@@ -36,7 +36,7 @@
<li class="list-group-item list-group-item-light">
<a href="{{ url_for('trap_connect') }}">Muizenval verbinden</a>
</li>
- {% if current_user.type == 'admin' %}
+ {% if current_user.admin %}
<li class="list-group-item list-group-item-light">
<a href="#">Gebruikers bewerken</a>
</li>
@@ -93,54 +93,57 @@
</div>
</nav>
</header>
-<!--nav bar--><body>
+<!--nav bar-->
+
+<body>
<nav class="navbar fixed-top bg-light">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
- <img src="static/logo.svg" alt="" width="50%" height="50%">
- Home
+ <img src="static/logo.svg" alt="" width="50%" height="50%">
+ Home
</a>
<ul class="navbar-nav">
<li class="nav-item">
- <a class="nav-link" aria-current="page" href="{{ url_for('login') }}">Inloggen</a>
+ <a class="nav-link" aria-current="page" href="{{ url_for('login') }}">Inloggen</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('register') }}">Registeren</a>
</li>
- </ul>
+ </ul>
</div>
- </nav>
+ </nav>
<div class="container" style="padding-top:20px;">
<div class="row">
<!-- sidebar -->
<div class="col-3">
- <ul class="nav nav-pills flex-column nav-justified">
- <li class="nav-item">
- <a class="nav-link" href="{{url_for('index')}}}">Home</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="{{url_for('producten')}}">Producten</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#">Link</a>
- </li>
- <li class="nav-item">
- <a class="nav-link disabled">Disabled</a>
- </li>
- {% if current_user.is_authenticated %}
- <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">{{current_user.name}}
-
- </a>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="{{ url_for('account') }}">Instellingen</a></li>
- <li><a class="dropdown-item" href="{{ url_for('logout') }}">Uitloggen</a></li>
+ <ul class="nav nav-pills flex-column nav-justified">
+ <li class="nav-item">
+ <a class="nav-link" href="{{url_for('index')}}}">Home</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{url_for('producten')}}">Producten</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="#">Link</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link disabled">Disabled</a>
+ </li>
+ {% if current_user.is_authenticated %}
+ <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button"
+ aria-expanded="false">{{current_user.name}}
+
+ </a>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="{{ url_for('account') }}">Instellingen</a></li>
+ <li><a class="dropdown-item" href="{{ url_for('logout') }}">Uitloggen</a></li>
+ </ul>
+ {% endif %}
</ul>
- {% endif %}
- </ul>
</div>
<!-- content-->
<div class="col-7">
- {% block content %}{% endblock %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/server/templates/layout.html b/server/templates/layout.html
@@ -39,14 +39,17 @@
crossorigin=""></script>
<script type="text/javascript" charset="utf-8">
- var socket = io();
- var current_user = {{ current_user.id if current_user.is_authenticated else none | tojson }};
-// socket.on('connect', function () { });
+ {% if user_token %}
+ var userToken = {{ user_token | tojson }};
+ {% else %}
+ var userToken = null;
+ {% endif %}
+
$('.dropdown-toggle').dropdown()
- $(document).ready(function() {
+ $(document).ready(function () {
$('li.active a').removeClass('active');
- $('a[href="' + location.pathname + '"]').closest('li ').addClass('active');
+ $('a[href="' + location.pathname + '"]').closest('li ').addClass('active');
});
</script>
@@ -94,14 +97,14 @@
<a class="nav-link" href="{{ url_for('traps') }}">Dashboard</a>
</li>
<li class="nav-item">
- <a class="nav-link" href="{{ url_for('trap_connect') }}">Koppel een val</a>
+ <a class="nav-link" href="#">Koppel een val</a>
</li>
{% if current_user.contact %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('contact') }}">Contact opnemen</a>
</li>
{% endif %}
- {% if current_user.type.name == 'ADMIN' %}
+ {% if current_user.admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin') }}">Gebruikers beheerden</a>
</li>
@@ -152,11 +155,11 @@
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="settings-tab">.3..</div>
</div>
-->
- <script>
+ <script>
$(function () {
- $('#myTab li:last-child a').tab('show')
+ $('#myTab li:last-child a').tab('show')
})
- </script>
+ </script>
<!-- content-->
<div class="col-7">
{% for category, message in get_flashed_messages(with_categories=true) %}
diff --git a/server/templates/trap.html b/server/templates/trap.html
@@ -1,88 +1,65 @@
{% extends "layout.html" %}
{% block content %}
-<article class="media content-section">
- <div class="media-body">
- <h1 style="text-align:center;">Dashboard</h1>
- <div id="trap-map"></div>
- </div>
-</article>
-{% for trap in traps %}
-<article class="media content-section">
- <div class="media-body">
- <h3><a class="article-title" href="{{ url_for('trap_update', trap_id=trap.mac) }}">
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="{{ trap.status_color() }}"
- class="bi bi-circle-fill" viewBox="0 0 20 20">
- <circle cx="10" cy="10" r="10" />
- </svg>
- -
- {% if trap.name %}
- {{ trap.name }}
- {% else %}
+<div id="trap-container">
+ <article class="media content-section">
+ <div class="media-body">
+ <h1 style="text-align:center;">Dashboard</h1>
+ <div id="trap-map"></div>
+ </div>
+ </article>
+ {#}
+ {% for trap in traps %}
+ <article class="media content-section">
+ <div class="media-body">
+ <h3><a class="article-title" href="{{ url_for('trap_update', trap_id=trap.mac) }}">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="{{ trap.status_color() }}"
+ class="bi bi-circle-fill" viewBox="0 0 20 20">
+ <circle cx="10" cy="10" r="10" />
+ </svg>
+ -
+ {% if trap.name %}
+ {{ trap.name }}
+ {% else %}
+ <code>[{{ trap.pretty_mac() }}]</code>
+ {% endif %}
+ </a>
+ </h3>
+ {% if trap.name %}
+ <p>
<code>[{{ trap.pretty_mac() }}]</code>
- {% endif %}
- </a>
- </h3>
- {% if trap.name %}
- <p>
- <code>[{{ trap.pretty_mac() }}]</code>
- </p>
- {% endif %}
- {% if trap.owner %}
- <b>
- van {{ trap.owner_class().name }}
- </b>
- {% endif %}
- </div>
-
- {#} <div class="media-body">
- <h3>Naam: {{ trap.name }}</h3>
- <p> Mac adres: {{ trap.mac }} </p>
- {% if trap.caught %}
- <p> Status: Gevangen! </p>
- {% else %}
- <p>Status: Leeg!</p>
- {% endif %}
- </div>{#}
-</article>
-{% endfor %}
-
-<script type="text/javascript">
- function prettyMAC(str) {
- var res = []
-
- for (var i = 0; i < 16; i += 2) {
- res.push(str.substr(i, 2).toUpperCase())
- }
-
- return res.join(':')
- }
-
- socket.on('trap-change', function (data) {
- if (data['user'] == current_user)
- location.reload();
- });
-
- // var trap_macs = [
- /* {% for trap in traps %} */
- //"{{ trap.mac }}" }
- /* {% endfor %} */
- //];
-
- var traps = {{ trap_json | tojson }};
-
- var map = L.map('trap-map').setView([52.283333, 5.666667], 7);
-
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
- }).addTo(map);
-
- for (var index in traps) {
- var trap = traps[index];
- if (trap.location_lat && trap.location_lon) {
- L.marker([trap.location_lat, trap.location_lon])
- .addTo(map)
- .bindPopup(`[${prettyMAC(trap.mac)}] ${trap.name || ''}`);
- }
- }
-</script>
+ </p>
+ {% endif %}
+ {% if trap.owner %}
+ <b>
+ van {{ trap.owner_class().name }}
+ </b>
+ {% endif %}
+ </div>
+ </article>
+ {% endfor %}
+ {#}
+</div>
+<template id="trap-template">
+ <article class="media content-section">
+ <div class="media-body">
+ <h3><a class="article-title link">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="bi bi-circle-fill"
+ viewBox="0 0 20 20">
+ <circle cx="10" cy="10" r="10" />
+ </svg>
+ -
+ <span class="name"></span>
+ </a>
+ </h3>
+ <b>
+ van <span class="owner"></span>
+ </b>
+ <p>Battery: <span class="battery"></span></p>
+ <p>Charging: <span class="charging"></span></p>
+ <p>Accuracy: <span class="accuracy"></span>% (<span class="satellites"></span> satellites)</p>
+ <p>Temperature: <span class="temperature"></span>°C</p>
+ </div>
+ </article>
+</template>
+<script type="text/javascript" src="{{ url_for('static', filename='trap.js') }}"></script>
{% endblock content %}
\ No newline at end of file
diff --git a/server/utilities.py b/server/utilities.py
@@ -0,0 +1,5 @@
+from random import choice
+
+
+def generate_token(chars: str, size: int):
+ return ''.join(choice(chars) for _ in range(size))
diff --git a/ssl/muizenval.tk.issuer.crt b/ssl/muizenval.tk.issuer.crt
@@ -0,0 +1,63 @@
+
+-----BEGIN CERTIFICATE-----
+MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
+WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
+RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
+R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
+sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
+NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
+Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
+/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
+Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
+FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
+AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
+Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
+gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
+PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
+ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
+CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
+lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
+avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
+yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
+yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
+hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
+HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
+MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
+nLRbwHOoq7hHwg==
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
+ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
+wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
+LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
+4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
+bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
+sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
+Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
+FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
+SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
+PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
+TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
+c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
+ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
+U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
+MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
+5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
+9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
+WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
+he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
+Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
+-----END CERTIFICATE-----
diff --git a/ssl/private.key b/ssl/private.key
@@ -0,0 +1,6 @@
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDBIlNtB1IyZMxaGnYWuJP9mQ2ftm0evXty/jUnitnr8Zpe3neyFF+c4
+Jy7ayOycnTegBwYFK4EEACKhZANiAAQ6MBrVC6MjKRf2dEZp3VQ/qHxwzwU98wtZ
+arOaRBHCVbyqX2ZVWbvmcc7JoyIYGcxfB33cdPP0W7RQGyx6Z6PHnnvQlVjlB5TE
+39eKCvQox8RcojrfqfcPjyGju8fIoPg=
+-----END EC PRIVATE KEY-----
diff --git a/ssl/public.crt b/ssl/public.crt
@@ -0,0 +1,89 @@
+-----BEGIN CERTIFICATE-----
+MIIEcDCCA1igAwIBAgISA7joNosNzihXp9wQ3+fOqebqMA0GCSqGSIb3DQEBCwUA
+MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
+EwJSMzAeFw0yMjA2MjcxMjE2NDNaFw0yMjA5MjUxMjE2NDJaMBcxFTATBgNVBAMT
+DG11aXplbnZhbC50azB2MBAGByqGSM49AgEGBSuBBAAiA2IABDowGtULoyMpF/Z0
+RmndVD+ofHDPBT3zC1lqs5pEEcJVvKpfZlVZu+ZxzsmjIhgZzF8Hfdx08/RbtFAb
+LHpno8eee9CVWOUHlMTf14oK9CjHxFyiOt+p9w+PIaO7x8ig+KOCAkcwggJDMA4G
+A1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYD
+VR0TAQH/BAIwADAdBgNVHQ4EFgQUvFk8cm2DlCWQ9zFJH0QyHEp5C8YwHwYDVR0j
+BBgwFoAUFC6zF7dYVsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsG
+AQUFBzABhhVodHRwOi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6
+Ly9yMy5pLmxlbmNyLm9yZy8wFwYDVR0RBBAwDoIMbXVpemVudmFsLnRrMEwGA1Ud
+IARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0
+dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDw
+AHYAQcjKsd8iRkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvYAAAGBpU6lMgAABAMA
+RzBFAiBMQzJycnsDwp9Vn8kpFdwjrKR13B6Qkj8fwu6ZgQvVJwIhAL3l6NXN275b
+maqxb7J2mPMEzJQbYSbgj6XoiWI8kVvjAHYAKXm+8J45OSHwVnOfY6V35b5XfZxg
+Cvj5TV0mXCVdx4QAAAGBpU6nHAAABAMARzBFAiAubKyl9xQtz784YfNyfU/0uJXS
+Bg79MLRAXFhP0DPQFAIhAOr9lT8MZBHiJ423KNmUhZe7z8YsJVBwjCeM7rusRbp7
+MA0GCSqGSIb3DQEBCwUAA4IBAQBMZB2GMQ20cbjCEjPt8BGi6JD52riue8vH9uQk
+17UrlwGH4tJbdL02XtDyYsTePG7XxPVpJiewYkI9qJQh1IuTV22f+C1Jd0V2YupF
+2AYt17C+CD27e8ptS9JD7lZhFFXnQRvIO+nTBaN+QzyUkfFR5+MjjYV0jLvLvTsn
+dguTkE+4uZ8HFGn0uhTmPL4Qw/Dm8O3oIXLPgmcWSYXiSbSfvqlMGyOgL1eZlvMA
+gkpMdgYDYVbecEs9QnHkkH7aHzbK/D7IqVnQW2NZnb4hMHeassT9ex9O1wXG0Ydz
+wRqzA47R8txfIVDutn0o24yowSo39WySQgxriuL5YnDeZY6G
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
+WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
+RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
+R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
+sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
+NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
+Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
+/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
+Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
+FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
+AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
+Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
+gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
+PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
+ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
+CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
+lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
+avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
+yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
+yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
+hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
+HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
+MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
+nLRbwHOoq7hHwg==
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
+ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
+wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
+LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
+4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
+bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
+sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
+Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
+FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
+SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
+PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
+TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
+c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
+ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
+U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
+MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
+5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
+9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
+WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
+he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
+Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
+-----END CERTIFICATE-----
diff --git a/test-server.py b/test-server.py
@@ -1,37 +1,72 @@
-from flask import json
+from flask import json, jsonify
from werkzeug.exceptions import HTTPException
from flask import Flask, request
+
+class Status:
+ trap = False
+ latitude = 0.0
+ longitude = 0.0
+ accuracy = 0.0
+ satellites = 0
+ battery = 0
+ temperature = 0
+ charging = False
+
+ def update(self, req: dict):
+ self.trap = req['trap']
+ self.battery = req['battery']
+ self.temperature = req['temperature']
+ self.charging = req['charging']
+ self.latitude = req['latitude']
+ self.longitude = req['longitude']
+ self.accuracy = req['accuracy']
+ self.satellites = req['satellites']
+
+
app = Flask(__name__)
-status = {}
+status = Status()
+
@app.post("/api/update")
-def update():
- global status
+def update():
+ global status
+
+ req = request.get_json(True)
+ if req:
+ status.update(req)
+
+ return jsonify({})
+
+
[email protected]('/api/hello')
+def hello():
+
+ return jsonify({})
- req = request.get_json(True)
- if not req:
- return
- status = req
- return {}
@app.get("/")
def index():
- return f'''
- <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
- integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
- crossorigin=""/>
+ return f'''
+ <link
+ rel="stylesheet"
+ href="https://unpkg.com/[email protected]/dist/leaflet.css"
+ integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
+ crossorigin="" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
- integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
- crossorigin=""></script>
+ integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
+ crossorigin=""></script>
<h1>Status update</h1>
- <p>latitude: <code>{status['']:.10f}</code></p>
- <p>longitude: <code>{longitude:.10f}</code></p>
- <p>accuracy: <code>{accuracy:.2f}%</code></p>
- <p>battery: <code>{battery}V</code></p>
- <p>temperature: <code>{temperature}°c</code></p>
+ <p>trap: <code>{'yes' if status.trap else 'no'}</code></p>
+ <p>latitude: <code>{status.latitude:.10f}</code></p>
+ <p>longitude: <code>{status.longitude:.10f}</code></p>
+ <p>accuracy: <code>{status.accuracy:.1f}%</code></p>
+ <p>satellites: <code>{status.satellites}</code></p>
+ <p>battery: <code>{status.battery}V</code></p>
+ <p>temperature: <code>{status.temperature}°c</code></p>
+ <p>charging: <code>{'yes' if status.charging else 'no'}</code></p>
<div id="map" style='height: 50%;'></div>
@@ -41,10 +76,11 @@ def index():
maxZoom: 19,
attribution: '© OpenStreetMap'
}}).addTo(map);
- var marker = L.marker([{latitude}, {longitude}]).addTo(map);
+ var marker = L.marker([{status.latitude}, {status.longitude}]).addTo(map);
</script>
'''
+
@app.errorhandler(HTTPException)
def handle_exception(e):
response = e.get_response()
@@ -56,4 +92,5 @@ def handle_exception(e):
response.content_type = "application/json"
return response
+
app.run('0.0.0.0', 5000)
diff --git a/test.py b/test.py
@@ -0,0 +1,4 @@
+from remote import Remote
+
+for port in Remote.list_ports():
+ print(f'{port.name} at {port.device} ({port.description})')