muizenval

Observe mouse traps remotely
Log | Files | Refs

commit 9f26274c0c34c492978eb28c059104d83c1f2365
parent 36f5485422a6695bbf6ceba200c430fe0bb73ad4
Author: Friedel Schön <[email protected]>
Date:   Tue, 28 Jun 2022 15:22:14 +0200

big remote commit

Diffstat:
M.gitignore | 5+++--
Mclient/client.ino | 64++++++++++++++++++++++++++++++++++++++++------------------------
Mclient/config.ino | 23++++++++++++++---------
Mclient/include/config.h | 24+++++-------------------
Mclient/include/interface.h | 8+++++---
Aclient/include/macro.h | 4++++
Mclient/interface.ino | 52+++++++++++++++++++++++++++-------------------------
Mcreate-db.py | 38++++++++++++++++++--------------------
Adirectories.txt | 6++++++
Resp-client/boot.py -> dump/esp-client/boot.py | 0
Resp-client/config.py -> dump/esp-client/config.py | 0
Resp-client/firmware/esp32.bin -> dump/esp-client/firmware/esp32.bin | 0
Resp-client/firmware/readme.txt -> dump/esp-client/firmware/readme.txt | 0
Resp-client/main.py -> dump/esp-client/main.py | 0
Resp-client/readme -> dump/esp-client/readme | 0
Mdump/remote.ino | 1+
Mreadme.md | 2+-
Mremote.py | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Aremote/__init__.py | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aremote/exception.py | 5+++++
Mrun-server.py | 13+++++++++++--
Mserver/app.py | 12+++++++++---
Mserver/forms.py | 75+++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mserver/models.py | 98++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mserver/routes.py | 178+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mserver/site.db | 0
Aserver/socket.py | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mserver/static/main.css | 1+
Aserver/static/trap.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mserver/templates/backup.html | 63+++++++++++++++++++++++++++++++++------------------------------
Mserver/templates/layout.html | 23+++++++++++++----------
Mserver/templates/trap.html | 145+++++++++++++++++++++++++++++++++----------------------------------------------
Aserver/utilities.py | 5+++++
Assl/muizenval.tk.issuer.crt | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Assl/private.key | 6++++++
Assl/public.crt | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest-server.py | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Atest.py | 4++++
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: '&copy; <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: '&copy; <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>&deg;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}&deg;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}&deg;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})')