muizenval

Observe mouse traps remotely
Log | Files | Refs

commit 73eabc84bd2f909779738ab51442828f702207c7
parent 4866667b7910b2e5e6b4287f2896145a2e704d76
Author: hooglo <[email protected]>
Date:   Thu, 30 Jun 2022 11:49:19 +0200

Merge branch 'master' of https://github.com/friedelschoen/muizenval.tk

Diffstat:
M.vscode/arduino.json | 2+-
Mclient/client.ino | 47++++++++++++++++++++++++++---------------------
Mclient/include/config.h | 51+++++++++++++++++++++++++++++----------------------
Mclient/include/interface.h | 2++
Mclient/interface.ino | 26+++++++++++++++++---------
Mclient/led.ino | 9+++++----
Mremote.py | 5++---
Mserver/forms.py | 2--
Mserver/models.py | 55++++++++++++++++++++++++++++++++-----------------------
Mserver/routes.py | 72+-----------------------------------------------------------------------
Mserver/site.db | 0
Mserver/socket.py | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mserver/static/main.css | 14+++++++++++++-
Mserver/static/trap.js | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mserver/templates/contact.html | 2+-
Mserver/templates/layout.html | 62++++++++++++++------------------------------------------------
Mserver/templates/trap.html | 57++++++++++++++++++---------------------------------------
Mserver/templates/updatetrap.html | 64+---------------------------------------------------------------
18 files changed, 416 insertions(+), 395 deletions(-)

diff --git a/.vscode/arduino.json b/.vscode/arduino.json @@ -1,6 +1,6 @@ { "sketch": "client/client.ino", "board": "SODAQ:samd:sodaq_sara", - "port": "/dev/tty.usbmodem14201", + "port": "/dev/tty.usbmodem14101", "output": "build" } \ No newline at end of file diff --git a/client/client.ino b/client/client.ino @@ -7,18 +7,20 @@ #include <Sodaq_UBlox_GPS.h> #include <Wire.h> -interface client; -Sodaq_LSM303AGR accel; +static interface client; +static Sodaq_LSM303AGR accel; +static bool next_scan, scan; void (*reset)() = 0; + void setup() { - pinMode(LED_RED, OUTPUT); - pinMode(LED_GREEN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); + pinMode(ledRed, OUTPUT); + pinMode(ledGreen, OUTPUT); + pinMode(ledBlue, OUTPUT); pinMode(trapPin, INPUT_PULLUP); - pinMode(BATVOLT_PIN, INPUT); - pinMode(CHARGER_STATUS, INPUT); + pinMode(batteryPin, INPUT); + pinMode(chargerPin, INPUT); config.open(); client.begin(); @@ -26,28 +28,27 @@ void setup() { if (!config.valid) config = config_default; - client.request["token"] = config.token; - client.request["domain"] = config.domain; - while (!client.send(interface::METHOD_POST, "/api/hello")) { + do { writeLED(COLOR_RED); - delay(500); - } + delay(2500); + client.request["token"] = config.token; + client.request["domain"] = config.domain; + } while (!client.send(interface::METHOD_POST, "/api/hello")); + writeLED(COLOR_WHITE); + + next_scan = scan = (bool) client.request["location_search"]; bool save = false; if (client.response.hasOwnProperty("token")) { strcpy(config.token, (const char*) client.response["token"]), save = true; - json req; - req["token"] = config.token; - client.remote("set-token", req); + client.sendToken(); } if (client.response.hasOwnProperty("domain")) strcpy(config.domain, (const char*) client.response["domain"]), save = true; - if (save) config.save(); - Wire.begin(); delay(1000); sodaq_gps.init(GPS_ENABLE); @@ -64,7 +65,7 @@ void loop() { int now = millis(); if (now - last > statusInterval * 1000) { - if (sodaq_gps.scan(true, gpsTimeout * 1000)) { + if (scan && sodaq_gps.scan(next_scan, gpsTimeout * 1000)) { client.request["latitude"] = sodaq_gps.getLat(); client.request["longitude"] = sodaq_gps.getLon(); client.request["accuracy"] = getAccuracy(); @@ -73,6 +74,7 @@ void loop() { client.request["longitude"] = 0; client.request["accuracy"] = 0; } + scan = next_scan; client.request["token"] = config.token; client.request["battery"] = batteryVoltage(); @@ -80,14 +82,17 @@ void loop() { client.request["charging"] = getCharging(); client.request["trap"] = getTrapStatus(); client.request["satellites"] = sodaq_gps.getNumberOfSatellites(); + client.request["searching"] = scan; - client.send(interface::METHOD_POST, "/api/update"); + if (client.send(interface::METHOD_POST, "/api/update")) { + next_scan = (bool) client.response["location_search"]; + } last = now; } } int batteryVoltage() { - return batteryFactor * analogRead(BATVOLT_PIN); + return batteryFactor * analogRead(batteryPin); } double getAccuracy() { // -> 100% the best, 0% the worst @@ -102,5 +107,5 @@ bool getTrapStatus() { } bool getCharging() { - return digitalRead(CHARGER_STATUS); + return digitalRead(chargerPin); } \ No newline at end of file diff --git a/client/include/config.h b/client/include/config.h @@ -8,31 +8,38 @@ #define modemPowerPin SARA_ENABLE // modem power pin #define modemEnablePin SARA_TX_ENABLE // modem enable pin #define modemVoltagePin SARA_R4XX_TOGGLE // modem voltage pin +#define batteryPin BAT_VOLT // messuring battery +#define chargerPin CHARGER_STATUS // messuring charging +#define ledRed LED_RED // rgb-led (red) +#define ledGreen LED_GREEN // rgb-led (green) +#define ledBlue LED_BLUE // rgb-led (blue) #define trapPin 10 // pin of magnet-sensor // -*- behaviour settings -*- -#define remoteBaud 115200 // baud-rate of usb-serial -#define modemBaud 115200 // baud-rate of modem-serial -#define remoteForce true // do not try connect to modem -#define remoteFirstTimeout 5.0 // seconds to wait for the first timeout -#define remoteTimeout 1.0 // seconds to wait for remote to timeout -#define lineBuffer 512 // buffer-size (bytes) to use to store lines -#define commandTimeout 10.0 // seconds to cancel a command -#define commandDelay 0.1 // delay after every command -#define ignoreDelay 2.0 // seconds to wait if command is run with COMMAND_IGNORE -#define commandDebug true // send debug information about command requests -#define eventDebug true // print '+'-events -#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 15 // seconds to gps-timeout -#define statusInterval 5 // send status every n seconds - -#define ADC_AREF 3.3f -#define BATVOLT_R1 4.7f -#define BATVOLT_R2 10.0f -#define BATVOLT_PIN BAT_VOLT -#define batteryFactor (0.978 * (BATVOLT_R1 / BATVOLT_R2 + 1) / ADC_AREF) +#define remoteBaud 115200 // baud-rate of usb-serial +#define modemBaud 115200 // baud-rate of modem-serial +#define remoteForce true // do not try connect to modem +#define lineBuffer 512 // buffer-size (bytes) to use to store lines +#define commandDebug true // send debug information about command requests +#define eventDebug true // print '+'-events +#define lineDebug false // print each line to debug +#define blockDebug true // print if command is blocking + +// -*- timing settings (seconds) -*- +#define remoteFirstTimeout 5 // seconds to wait for the first timeout +#define remoteTimeout 1 // seconds to wait for remote to timeout +#define commandTimeout 10 // seconds to cancel a command +#define commandDelay 0.1 // delay after every command +#define ignoreDelay 2 // seconds to wait if command is run with COMMAND_IGNORE +#define blinkInterval 0.25 // seconds to wait for blink +#define gpsTimeout 15 // seconds to gps-timeout +#define statusInterval 5 // send status every n seconds + +// -*- battery stuff -*- +#define adcAREF 3.3 +#define batteryR1 4.7 +#define batteryR2 10.0 +#define batteryFactor (0.978 * (batteryR1 / batteryR2 + 1) / adcAREF) struct configuration { diff --git a/client/include/interface.h b/client/include/interface.h @@ -45,6 +45,8 @@ struct interface { int send(method method, const char* endpoint); + void sendToken(); + command_status remote(const char* command, json params = nullptr, json& response = null_response, command_flags flags = COMMAND_NONE); command_status modem(const char* request, char* response, command_flags flags = COMMAND_NONE); diff --git a/client/interface.ino b/client/interface.ino @@ -55,9 +55,7 @@ void interface::beginRemote() { if (!usbSerial) return; - json req; - req["token"] = config.token; - remote("set_token", req, null_response, COMMAND_FORCE); + sendToken(); writeLED(COLOR_MAGENTA); remoteReady = true; @@ -87,16 +85,24 @@ int interface::send(interface::method method, const char* endpoint) { request = nullptr; response = cmd_response["body"]; return cmd_response["code"]; - } else { + } else if (modemReady) { endRemote(); - if (!modemReady) { - return 0; - } // modem - return 1; + return 0; + } else { + endRemote(); + writeLED(COLOR_RED); + return 0; } } +void interface::sendToken() { + json req; + req["token"] = config.token; + remote("set_token", req, null_response, COMMAND_FORCE); +} + + interface::command_status interface::remote(const char* command, json params, json& response, command_flags flags) { bool force = flags & COMMAND_FORCE; @@ -127,6 +133,8 @@ interface::command_status interface::remote(const char* command, json params, js } interface::command_status interface::modem(const char* request, char* response, command_flags flags) { + return STATUS_NOT_READY; + /* char line[lineBuffer]; size_t lineLen; char buf; @@ -146,7 +154,7 @@ interface::command_status interface::modem(const char* request, char* response, modemSerial.write("\r\n"); - delay(commandDelay * 1000); + delay(commandDelay * 1000);*/ } interface::command_status interface::modem(const char* request, command_flags flags) { diff --git a/client/led.ino b/client/led.ino @@ -1,6 +1,7 @@ +#include "include/config.h" #include "include/led.h" -static const bool colors[][3] = { +static bool colors[][3] = { [COLOR_NONE] = { 0, 0, 0 }, [COLOR_RED] = { 1, 0, 0 }, [COLOR_GREEN] = { 0, 1, 0 }, @@ -12,7 +13,7 @@ static const bool colors[][3] = { }; void writeLED(color c) { - digitalWrite(LED_RED, !colors[c][0]); - digitalWrite(LED_GREEN, !colors[c][1]); - digitalWrite(LED_BLUE, !colors[c][2]); + digitalWrite(ledRed, !colors[c][0]); + digitalWrite(ledGreen, !colors[c][1]); + digitalWrite(ledBlue, !colors[c][2]); } \ No newline at end of file diff --git a/remote.py b/remote.py @@ -14,8 +14,8 @@ import websockets WEBSOCKET_PORT = 1612 +host, port = 'localhost', 5000 -client = HTTPConnection('localhost', 5000) remote = Remote(115200) token: Optional[str] = None @@ -32,6 +32,7 @@ def send_http(params): print(body) + client = HTTPConnection(host, port) client.request(method, endpoint, json.dumps(body)) res = client.getresponse() response = json.load(res) @@ -48,8 +49,6 @@ async def websocket_handler(ws, _): if await ws.recv() == 'token': if token: await ws.send(token) - else: - await ws.send(None) await ws.close() diff --git a/server/forms.py b/server/forms.py @@ -88,9 +88,7 @@ class UpdateAccountForm(FlaskForm): class UpdateTrapForm(FlaskForm): - mac = StringField('MAC') name = StringField('Naam') - location = StringField('Locatie') submit = SubmitField('Bewerken') diff --git a/server/models.py b/server/models.py @@ -1,5 +1,6 @@ -from datetime import datetime -from typing import Any, Dict, Optional +from datetime import datetime, timedelta +from email.policy import default +from typing import Optional from flask_login import UserMixin from .app import db, login_manager @@ -27,51 +28,59 @@ class Trap(db.Model): 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) + owned_date: Optional[datetime] = db.Column(db.DateTime) + name: str = db.Column(db.Text, nullable=False, default='n/a') - 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) + last_status: datetime = db.Column(db.DateTime, nullable=False) + caught: bool = db.Column(db.Boolean, nullable=False, default=False) + battery: int = db.Column(db.Integer, nullable=False, default=0) + charging: bool = db.Column(db.Boolean, nullable=False, default=False) + temperature: int = db.Column(db.Integer, nullable=False, default=0) + location_search: bool = db.Column(db.Boolean, nullable=False, default=True) + location_searching: bool = db.Column( + db.Boolean, nullable=False, default=True) + location_acc: float = db.Column(db.Float, nullable=False, default=0) 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 offline(self): + return datetime.now() - self.last_status > timedelta(hours=1) - 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): + def to_json(self): owner = self.owner_class() - owner_name = owner.name if owner else '{nobody}' + owner_name = owner.name if owner else 'n/a' 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, + name=self.name, + offline=self.offline(), + locationSearch=self.location_search, + locationSearching=self.location_searching, latitude=self.location_lat, longitude=self.location_lon, - accuracy=self.location_acc, + accuracy=round(self.location_acc, 1), satellites=self.location_satellites, activated=self.caught, owner=owner_name, battery=self.battery, charging=self.charging, temperature=self.temperature, - byToken=token + lastStatus=self.last_status.strftime('%d-%m-%y %H:%M'), + ownedDate=self.owned_date.strftime( + '%d-%m-%y %H:%M') if self.owned_date else '-' ) +class Statistic(db.Model): + id: int = db.Column(db.Integer, primary_key=True) + user: int = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + date: datetime = db.Column(db.DateTime, nullable=False) + + @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,4 +1,3 @@ -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 @@ -9,16 +8,10 @@ from .models import Trap, User import secrets import os -import random -import string current_user: User -def validate_mac(mac): - return len(mac) == 16 and all(c in string.hexdigits for c in mac) - - """ index.html (home-page) route """ @@ -149,70 +142,7 @@ def account(): @app.route('/traps') @login_required def 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] - - return render_template('trap.html', traps=query, trap_json=trap_json) - - -""" [email protected]('/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() - if not trap: - flash('Muizenval niet gevonden', 'danger') - return redirect(url_for('trap_connect')) - - trap.owner = current_user.id - trap.connect_expired = None - trap.connect_code = None - db.session.commit() - flash('Muizenval toegevoegd!', 'success') - return redirect(url_for('traps')) - - return render_template('connect.html', form=form) -""" - - [email protected]('/trap/<trap_id>/update', methods=['POST', 'GET']) -@login_required -def trap_update(trap_id): - form = UpdateTrapForm() - trap = Trap.query.filter_by(mac=trap_id).first() - if form.validate_on_submit(): - trap.name = form.name.data - print(form.location.data) - if form.location.data: - trap.location_lat, trap.location_lon = form.location.data.split( - ' ', 2) - db.session.commit() - return redirect(url_for('traps')) - elif not trap: - flash('Muizenval niet gevonden', 'danger') - return redirect(url_for('traps')) - elif request.method == 'GET': - form.mac.data = trap.pretty_mac() - form.name.data = trap.name - return render_template('updatetrap.html', form=form, trap=trap) - - [email protected]('/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')) + return render_template('trap.html') @app.route('/contact') diff --git a/server/site.db b/server/site.db Binary files differ. diff --git a/server/socket.py b/server/socket.py @@ -1,16 +1,19 @@ -from datetime import datetime, timedelta +from datetime import datetime +import os import random from typing import Dict from flask import request, jsonify from flask_login import current_user -from flask_socketio import emit, Namespace +from flask_socketio import emit from .app import app, db, socket, domain -from .models import Trap, User +from .models import Statistic, Trap, User current_user: User -sockets: Dict[int, Namespace] = {} +sockets: Dict[int, str] = {} + +accuracy_min = 80 def make_token(): @@ -29,10 +32,15 @@ def register_trap(): token = make_token() if not Trap.query.filter_by(token=token).first(): break - trap = Trap(token=token) + + trap = Trap(token=token, last_status=datetime.now()) db.session.add(trap) db.session.commit() res['token'] = token + else: + trap: Trap = Trap.query.filter_by(token=req['token']).first() + + res['location_search'] = trap.location_search if 'domain' not in req or req['domain'] != domain: res['domain'] = domain @@ -50,52 +58,46 @@ def update_status(): if not trap: return jsonify(dict(error='invalid-token')) + if not trap.caught and req['trap']: + if trap.owner: + stc = Statistic(user=trap.owner, date=datetime.now()) + db.session.add(stc) + # os.system( + # f"echo -e -s \"Je muizenval '{trap.name}' heeft iets gevangen!\\n\\nGroetjes uw Team Benni!\" | mailx -s 'Muizenval werd geactiveerd' {trap.owner_class().email}") # type: ignore + print('Email sent!') + + trap.last_status = datetime.now() 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'] + trap.location_searching = req['searching'] + if trap.location_search: + trap.location_satellites = req['satellites'] + if req['accuracy'] != 0: + trap.location_acc = req['accuracy'] + trap.location_lat = req['latitude'] + trap.location_lon = req['longitude'] 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) + socket.emit('trap-change', trap.to_json(), to=sockets[trap.owner]) + socket.emit('statistics', make_statistics( + trap.owner), to=sockets[trap.owner]) - code = "" - while True: - code = ''.join(random.choice( - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(5)) - if not Trap.query.filter_by(connect_code=code).first(): - break + return jsonify(dict(location_search=trap.location_search)) - trap.owner = None - trap.connect_expired = datetime.utcnow() + timedelta(minutes=5) - trap.connect_code = code - db.session.commit() +def make_statistics(user: int): + year = datetime.now().year + months = [0] * 12 + stc: Statistic + for stc in Statistic.query.filter_by(user=user): + if stc.date.year == year: + months[stc.date.month-1] += 1 - return jsonify({"error": "ok"}) -""" + return months @socket.on('connect') @@ -103,18 +105,21 @@ def socket_connect(): if not current_user.is_authenticated: return - sockets[current_user.id] = request.namespace # type: ignore + sockets[current_user.id] = request.sid # type: ignore for trap in Trap.query.filter_by(owner=current_user.id): emit('trap-change', trap.to_json()) + emit('statistics', make_statistics(current_user.id)) + @socket.on('disconnect') def socket_disconnect(): if not current_user.is_authenticated: return - del sockets[current_user.id] + if current_user.id in sockets: + del sockets[current_user.id] @socket.on('token') @@ -122,5 +127,58 @@ 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)) + trap: Trap = Trap.query.filter_by(token=token).first() + if not trap or trap.owner == current_user.id: + return + + trap.owner = current_user.id + trap.owned_date = datetime.now() + db.session.commit() + + emit('trap-change', trap.to_json()) + + [email protected]('location-search') +def socket_location(data): + if not data or not current_user.is_authenticated: + return + + print(data['id']) + trap: Trap = Trap.query.get(data['id']) + if not trap or trap.owner != current_user.id: + return + + trap.location_search = data['search'] + db.session.commit() + + emit('trap-change', trap.to_json()) + + [email protected]('delete') +def socket_delete(data): + if not data or not current_user.is_authenticated: + return + + print(data['id']) + trap: Trap = Trap.query.get(data['id']) + if not trap or trap.owner != current_user.id: + return + + trap.owner = False + db.session.commit() + + [email protected]('name') +def socket_name(data): + if not data or not current_user.is_authenticated: + return + + print(data['id']) + trap: Trap = Trap.query.get(data['id']) + if not trap or trap.owner != current_user.id: + return + + trap.name = data['name'] + db.session.commit() + + emit('trap-change', trap.to_json()) diff --git a/server/static/main.css b/server/static/main.css @@ -1,3 +1,7 @@ +body { + background-color: #efefef; +} + #side_nav{ background: #000; list-style-type: none; @@ -53,5 +57,12 @@ border: #aaa solid 1px; } +#trap-chart { + height: 300px; + position: relative; +} - +i { + padding-left: 10px; + padding-right: 10px; +} +\ No newline at end of file diff --git a/server/static/trap.js b/server/static/trap.js @@ -1,56 +1,140 @@ /* -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 +trap { + id: int + name: str? + status: str + offline: bool + locationSearch: bool + latitude: float? + longitude: float? + accuracy: float? + satellites: int? + activated: bool + owner: str + battery: int + charging: bool + temperature: bool + lastStatus: str + ownedDate: str } */ +const errorDelay = 2500; + function addTrap(trap) { var clone, append = false; if (traps[trap.id]) { + Object.assign(traps[trap.id], trap); + clone = traps[trap.id].element; } else { - clone = document.getElementById('trap-template').content.cloneNode(true); + traps[trap.id] = trap; + + clone = document.getElementById('trap-template').content.cloneNode(true).querySelector('article'); + traps[trap.id].element = clone; + traps[trap.id].updating = false; + clone.id = `trap-${trap.id}`; 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]) - ); + if (!traps[trap.id].updating) { + var statusIcons = '', + statusString = '', + statusIcon, + batteryIcon, + tempIcon; + + if (trap.offline) (statusIcon = 'moon'), (statusString += 'offline'); + else if (trap.activated) (statusIcon = 'circle-exclamation'), (statusString += 'geactiveerd'); + else (statusIcon = 'clock'), (statusString += 'wachtend'); + statusIcons += `<i class='fas fa-${statusIcon}'></i>`; + clone.style.background = '#ffffff'; + + if (!trap.offline) { + if (trap.activated) { + clone.style.background = '#e8dcca'; + } + if (trap.charging) (batteryIcon = 'plug-circle-bolt'), (statusString += ', aan het opladen'); + else if (trap.battery == 0) batteryIcon = 'battery-empty'; + else if (trap.battery < 30) batteryIcon = 'battery-quarter'; + else if (trap.battery < 55) batteryIcon = 'battery-half'; + else if (trap.battery < 80) batteryIcon = 'battery-three-quarters'; + else if (trap.battery < 100) batteryIcon = 'battery-full'; + else (batteryIcon = 'plug-circle-xmark'), (statusString += ', problemen met batterij'); + statusIcons += `<i class='fas fa-${batteryIcon}'></i>`; + + if (trap.temperature > 50) (tempIcon = 'temperature-high'), (statusString += ', oververhit'); + else if (trap.temperature < -10) (tempIcon = 'temperature-low'), (statusString += ', onderkoeld'); + if (tempIcon) statusIcons += `<i class='fas fa-${tempIcon}'></i>`; + + if (trap.locationSearching) (statusIcons += '<i class="fas fa-satellite"></i>'), (statusString += ', zoekt naar locatie'); + } else { + clone.style.background = '#eeeeee'; + } + + clone.querySelector('a.update').onclick = function () { + var nameSpan = clone.querySelector('span.name'), + input = nameSpan.querySelector('input'); + if (input) { + clone.querySelector('a.update').innerHTML = 'bewerken'; + traps[trap.id].updating = false; + socket.emit('name', { id: trap.id, name: input.value }); + } else { + nameSpan.innerHTML = `<input type="entry" value="${trap.name}" />`; + traps[trap.id].updating = true; + clone.querySelector('a.update').innerHTML = 'klaar'; + } + }; + clone.querySelector('a.delete').onclick = function () { + socket.emit('delete', { id: trap.id }); + clone.remove(); + delete traps[trap.id]; + }; + clone.querySelector('a.location').onclick = function () { + socket.emit('location-search', { id: trap.id, search: !trap.locationSearch }); + }; + + clone.querySelector('span.location-button').innerHTML = trap.locationSearch ? 'locatie vastzetten' : 'locatie zoeken'; + clone.querySelector('span.status-icons').innerHTML = statusIcons; + clone.querySelector('span.status').innerHTML = statusString; + clone.querySelector('span.name').innerHTML = trap.name; + clone.querySelector('span.owner').innerHTML = trap.owner; + clone.querySelector('span.accuracy').innerHTML = trap.accuracy; + clone.querySelector('span.satellites').innerHTML = trap.satellites; + clone.querySelector('span.temperature').innerHTML = trap.temperature; + clone.querySelector('span.last-status').innerHTML = trap.lastStatus; + clone.querySelector('span.owned-date').innerHTML = trap.ownedDate; + if (trap.battery < 100) { + clone.querySelector('p.battery').style.display = 'inherit'; + clone.querySelector('span.battery').innerHTML = trap.battery; + } else { + clone.querySelector('p.battery').style.display = 'none'; + } + if (trap.locationSearch) { + clone.querySelector('p.accuracy').style.display = 'inherit'; + } else { + clone.querySelector('p.accuracy').style.display = 'none'; + } + } + + if (append) document.getElementById('trap-container').appendChild(clone); + + if (trap.accuracy) { + if (traps[trap.id].marker) { + traps[trap.id].marker.setLatLng([trap.latitude, trap.longitude]); + } else { + traps[trap.id].marker = L.marker([trap.latitude, trap.longitude]).addTo(map).bindPopup(trap.name); + map.fitBounds( + Object.values(traps) + .filter((x) => x.accuracy) + .map((x) => [x.latitude, x.longitude]) + ); + } + } else if (traps[trap.id].marker) { + traps[trap.id].marker.remove(); + traps[trap.id].marker = undefined; } } @@ -60,16 +144,16 @@ function removeTrap(trap) { 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); + if (token) { + socket.emit('token', token); + remote = true; + } }); ws.addEventListener('error', () => { token = null; @@ -93,5 +177,30 @@ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { socket.on('trap-change', addTrap); socket.on('trap-remove', removeTrap); +socket.on('statistics', function (months) { + var chart = new CanvasJS.Chart('trap-chart', { + data: [ + { + // Change type to "doughnut", "line", "splineArea", etc. + type: 'column', + dataPoints: [ + { label: 'Januari', y: months[0] }, + { label: 'Februari', y: months[1] }, + { label: 'Maart', y: months[2] }, + { label: 'April', y: months[3] }, + { label: 'Mei', y: months[4] }, + { label: 'Juni', y: months[5] }, + { label: 'Juli', y: months[6] }, + { label: 'Augustus', y: months[7] }, + { label: 'September', y: months[8] }, + { label: 'October', y: months[9] }, + { label: 'November', y: months[10] }, + { label: 'December', y: months[11] }, + ], + }, + ], + }); + chart.render(); +}); openWebSocket(); diff --git a/server/templates/contact.html b/server/templates/contact.html @@ -3,7 +3,7 @@ {% with contact = current_user.contact_class() %} <article class="media content-section"> <div class="media-body"> - <h2>Uw contactgegevens</h2> + <h2>Contactgegevens Service Punt</h2> {% if contact %} <p> <b>{{ contact.name }}</b> diff --git a/server/templates/layout.html b/server/templates/layout.html @@ -20,10 +20,12 @@ <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}"> - <!-- Bootstrap CSS--> + <!-- Bootstrap CSS <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css" rel="stylesheet"> - <link rel="stylesheet" href="style.css" /> - <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" /> + <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" />--> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" + integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" + crossorigin="anonymous" referrerpolicy="no-referrer" /> <!-- Google Font: Source Sans Pro, Source Code Pro --> <link rel="preconnect" href="https://fonts.googleapis.com"> @@ -42,6 +44,7 @@ <script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ==" crossorigin=""></script> + <script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script> <script type="text/javascript" charset="utf-8"> {% if user_token %} @@ -73,20 +76,17 @@ </div> <ul class="list-unstyled px-2"> - <li><a href="#" class="text-decoration-none px-3 py-2 d-block text-white"><i class="fas fa-home"></i> - Home</a></li> + <li><a href="{{ url_for('index') }}" class="text-decoration-none px-3 py-2 d-block text-white"><i + class="fas fa-home"></i>Home</a></li> {% if current_user.is_authenticated %} <li><a href="{{ url_for('traps') }}" class="text-decoration-none px-3 py-2 d-block text-white"><i - class="far fa-tachometer-alt"></i> Dashboard</a></li> - <li><a href="#" class="text-decoration-none px-3 py-2 d-block text-white"><i class="far fa-plug"></i> - Koppel een - val</a></li> + class="fas fa-chart-line"></i> Dashboard</a></li> <li><a href="{{ url_for('contact') }}" class="text-decoration-none px-3 py-2 d-block text-white"><i class="far fa-address-book"></i> Contact opnemen</a></li> {% endif %} <li><a href="#" class="text-decoration-none px-3 py-2 d-block text-white"><i - class="far fa-map-marker-question"></i> Over ons</a></li> + class="far fa-clipboard"></i> Over ons</a></li> </ul> <hr class="h-color mx-2"> @@ -95,51 +95,17 @@ {% if current_user.is_authenticated %} <li class=""><a href="{{ url_for('logout') }}" - class="text-decoration-none px-3 py-2 d-block text-white"><i class="fas fa-sign-out"></i> + class="text-decoration-none px-3 py-2 d-block text-white"><i + class="fas fa-arrow-right-from-bracket"></i> Uitloggen</a></li> {% else %} <li class=""><a href="{{ url_for('login') }}" class="text-decoration-none px-3 py-2 d-block text-white"><i - class="fas fa-sign-out"></i>Inloggen</a></li> + class="fas fa-arrow-right-to-bracket"></i>Inloggen</a></li> <li class=""><a href="{{ url_for('register') }}" - class="text-decoration-none px-3 py-2 d-block text-white"><i class="far fa-user"></i> + class="text-decoration-none px-3 py-2 d-block text-white"><i class="fas fa-square-pen"></i> Registreren</a></li> {% endif %} - <!--======= -======= ->>>>>>> cdc54f9efef31c60b062d347b41fb859b414092e - <ul class="list-unstyled px-2"> - <li class=""><a href="{{ url_for('index') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="far fa-map-marker-question"></i>Home</a></li> - {% if current_user.is_authenticated %} - <li class=""><a href="{{ url_for('traps') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="far fa-tachometer-alt"></i>Dashboard</a></li> - <li class=""><a href="#" class="text-decoration-none px-3 py-2 d-block"><i class="far fa-plug"></i>Koppel - een val</a></li> - <li class=""><a href="{{ url_for('contact') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="far fa-address-book"></i>Contact opnemen</a></li> - {% endif %} - <li class=""><a href="#" class="text-decoration-none px-3 py-2 d-block"><i - class="far fa-map-marker-question"></i>Over ons</a></li> - </ul> - <hr class="h-color mx-2"> - - <ul class="list-unstyled px-2"> - {% if current_user.is_authenticated %} - <li class=""><a href="{{ url_for('account') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="far fa-cogs"></i>Instellingen</a></li> - <li class=""><a href="{{ url_for('logout') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="fas fa-sign-out"></i>Uitloggen</a></li> - {% else %} - <li class=""><a href="{{ url_for('login') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="fas fa-sign-out"></i>Inloggen</a></li> - <li class=""><a href="{{ url_for('register') }}" class="text-decoration-none px-3 py-2 d-block"><i - class="fas fa-sign-out"></i>Registeren</a></li> - {% endif %} -<<<<<<< HEAD ->>>>>>> cdc54f9efef31c60b062d347b41fb859b414092e -======= ->>>>>>> cdc54f9efef31c60b062d347b41fb859b414092e--> </ul> <hr class="h-color mx-2"> </div> diff --git a/server/templates/trap.html b/server/templates/trap.html @@ -7,57 +7,36 @@ <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> - </p> - {% endif %} - {% if trap.owner %} - <b> - van {{ trap.owner_class().name }} - </b> - {% endif %} + <h2 style="text-align:center;">activiteit in muizen per maand</h2> + <div id="trap-chart"></div> </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="status-icons"></span> + | <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> + <p><i> + van <b><span class="owner"></span></b></span> + </i></p> + <p><b><span class="status"></span></b></p> + <p><i class="fas fa-face-smile"></i> geregistreerd sinds <span class="owned-date"></p> + <p><i class="fas fa-wave-square"></i> laaste update om <span class="last-status"></span></p> + <p class="accuracy"><i class="fas fa-location"></i> nauwkeurigheid: <span class="accuracy"></span>% + met <span class="satellites"></span> satelliet(en)</p> + <p><i class="fas fa-temperature-half"></i> temperatuur: <span class="temperature"></span>&deg;C</p> + <p class="battery"><i class="fas fa-battery-half"></i> batterij: <span class="battery"></span>%</p> + + <a class="btn btn-primary update" href="javascript:void(0)">bewerken</a> + <a class="btn btn-secondary location" href="javascript:void(0)"><span class="location-button"></span></a> + <a class="btn btn-danger delete" href="javascript:void(0)">verwijderen</a> </div> </article> </template> diff --git a/server/templates/updatetrap.html b/server/templates/updatetrap.html @@ -8,19 +8,6 @@ <h1>{{ legend }}</h1> </legend> <div class="form-group"> - {{ form.mac.label(class="form-control-label") }} - {% if form.mac.errors %} - {{ form.mac(class="form-control form-control-lg is-invalid") }} - <div class="invalid-feedback"> - {% for error in form.mac.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.mac(disabled=True, class="form-control form-control-lg") }} - {% endif %} - </div> - <div class="form-group"> {{ form.name.label(class="form-control-label") }} {% if form.name.errors %} {{ form.name(class="form-control form-control-lg is-invalid") }} @@ -33,60 +20,11 @@ {{ form.name(class="form-control form-control-lg") }} {% endif %} </div> - <div class="form-group"> - {{ form.location.label(class="form-control-label") }} - {% if form.location.errors %} - {{ form.location(class="form-control form-control-lg is-invalid") }} - <div class="invalid-feedback"> - {% for error in form.location.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.location(id='location-input', readonly=True, class="form-control form-control-lg") }} - {% endif %} - </div> </fieldset> - <p> - <div id="trap-map"></div> - </p> <div class="form-group"> {{ form.submit(class="btn btn-outline-info") }} - <a class="btn btn btn-danger" href="{{ url_for('trap_delete', trap_id=trap.mac) }}">Verwijderen</a> + <a class="btn btn btn-danger" href="{{ url_for('trap_delete', trap_id=trap.id) }}">Verwijderen</a> </div> </form> </div> -<script type="text/javascript"> - var trap = {{ trap.dict() | 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); - - - let marker = null; - - function setMarker(locArg) { - var loc = L.latLng(locArg); - if (marker) { - marker.setLatLng(loc); - } else { - marker = L.marker(loc).addTo(map); - } - - document.getElementById('location-input').value = `${loc.lat} ${loc.lng}`; - } - - if (trap.location_lat && trap.location_lon) { - setMarker([trap.location_lat, trap.location_lon]); - } - - function onMapClick(e) { - setMarker(e.latlng); - } - - map.on('click', onMapClick); -</script> {% endblock content %} \ No newline at end of file