diff --git a/examples/meshtastic/cpp/qt.cpp b/examples/meshtastic/cpp/qt.cpp index be02749..3db56a7 100644 --- a/examples/meshtastic/cpp/qt.cpp +++ b/examples/meshtastic/cpp/qt.cpp @@ -1,5 +1,6 @@ #include "qt.h" #include "ble_meshtastic.h" +#include "tile_server.h" #include #include #include @@ -170,4 +171,14 @@ QVariant QT::localIp() { return QVariant(); } +QVariant QT::startTileServer() { + static bool start = true; + int port = 1702; + if (start) { + start = false; + new TileServer(port); + } + return port; +} + QT_END_NAMESPACE diff --git a/examples/meshtastic/cpp/qt.h b/examples/meshtastic/cpp/qt.h index ccc1165..68098b6 100644 --- a/examples/meshtastic/cpp/qt.h +++ b/examples/meshtastic/cpp/qt.h @@ -35,6 +35,7 @@ public: // etc Q_INVOKABLE QVariant localIp(); + Q_INVOKABLE QVariant startTileServer(); QT(); diff --git a/examples/meshtastic/cpp/qt.pro b/examples/meshtastic/cpp/qt.pro index 1351c34..b160cc2 100644 --- a/examples/meshtastic/cpp/qt.pro +++ b/examples/meshtastic/cpp/qt.pro @@ -12,6 +12,7 @@ MOC_DIR = ./tmp/ HEADERS += \ ble.h \ ble_meshtastic.h \ + tile_server.h \ qt.h SOURCES += \ diff --git a/examples/meshtastic/cpp/tile_server.h b/examples/meshtastic/cpp/tile_server.h new file mode 100644 index 0000000..622b4e4 --- /dev/null +++ b/examples/meshtastic/cpp/tile_server.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +// trivial local tile web-server which doesn't need an API key + +class TileServer : public QTcpServer { + Q_OBJECT + +public: + int port; + + TileServer(int p = 0, QObject* parent = nullptr) : QTcpServer(parent) { + listen(QHostAddress::Any, p); + port = serverPort(); + } + + void incomingConnection(qintptr socket) override { + QTcpSocket* s = new QTcpSocket(this); + connect(s, &QTcpSocket::readyRead, this, &TileServer::readClient); + connect(s, &QTcpSocket::disconnected, this, &TileServer::discardClient); + s->setSocketDescriptor(socket); + } + +public Q_SLOTS: + void readClient() { + QString xml = QStringLiteral( + "{\"UrlTemplate\" : \"https://tile.openstreetmap.org/%z/%x/%y.png\"," + " \"ImageFormat\" : \"png\"," + " \"QImageFormat\" : \"Indexed8\"," + " \"ID\" : \"wmf-intl-1x\"," + " \"MaximumZoomLevel\" : 19," + " \"MapCopyRight\" : \"OpenStreetMap\"," + " \"DataCopyRight\" : \"\"}"); + + QTcpSocket* socket = static_cast(sender()); + if (socket->canReadLine()) { + QString line = socket->readLine(); + QStringList tokens = line.split(QRegExp("[ \r\n][ \r\n]*")); + if (tokens.at(0) == "GET") { + QTextStream s(socket); + s.setCodec("UTF-8"); + s << QStringLiteral("HTTP/1.0 200 Ok\r\n" + "Content-Type: text/html; charset=\"utf-8\"\r\n\r\n") + << xml + << QStringLiteral("\r\n"); + socket->close(); + + if (socket->state() == QTcpSocket::UnconnectedState) { + delete socket; + } + } + } + } + + void discardClient() { + QTcpSocket* socket = static_cast(sender()); + socket->deleteLater(); + } +}; + diff --git a/examples/meshtastic/lisp/location.lisp b/examples/meshtastic/lisp/location.lisp index 55b4739..ccb63a0 100644 --- a/examples/meshtastic/lisp/location.lisp +++ b/examples/meshtastic/lisp/location.lisp @@ -1,6 +1,6 @@ (in-package :loc) -(defvar *positions* nil) ; will be shown on an offline map (TODO) +(defvar *positions* nil) (defvar *my-position* nil) (defun ini () @@ -42,6 +42,13 @@ (lora:send-position *my-position*)) (qsingle-shot 1000 'send-to-radio))) +(defun position* (node) + (when (stringp node) + (setf node (parse-integer node))) ; for JS + (x:when-it (getf *positions* node) + (list (getf x:it :lat) + (getf x:it :lon)))) + (defun set-position (node pos) (let ((lat (getf pos :lat))) (when (and node lat (not (zerop lat))) @@ -56,7 +63,7 @@ (defun distance (from to) ;; Haversine formula - (destructuring-bind ((lat-1 . lon-1) (lat-2 . lon-2)) + (destructuring-bind ((lat-1 lon-1) (lat-2 lon-2)) (list from to) (if (and (numberp lat-1) (numberp lat-2)) @@ -73,3 +80,36 @@ (floor (+ 0.5 (* x +earth-mean-radius+ 1000))))) 0))) +(defun tile-path () + (namestring (merge-pathnames "data/tiles/"))) + +(defun activate-map () + (unless (q< |active| ui:*map-loader*) + (qt:start-tile-server qt:*cpp*) + (q> |active| ui:*map-loader* t) + #+mobile + (destructuring-bind (lat lon time) + (last-gps-position) + (unless (zerop lat) + (qjs |setCenter| ui:*map* lat lon))) + #-mobile + (let ((my-pos (position* (lora:my-num)))) + (when my-pos + (qjs |setCenter| ui:*map* my-pos))))) + +(defun show-map-clicked () ; see QML + (let ((show (not (q< |visible| ui:*map-view*)))) + (when show + (activate-map) + (qjs |updatePositions| ui:*map* + (find-quick-item ui:*group*))) + (q> |visible| ui:*map-view* show) + ;; move map (not page) when swiping to left + (q> |interactive| ui:*main-view* (not show)) + (unless show + (q> |active| ui:*map-loader* nil))) + (values)) + +(defun position-count () + (length *positions*)) + diff --git a/examples/meshtastic/lisp/main.lisp b/examples/meshtastic/lisp/main.lisp index 1e72241..9d5a386 100644 --- a/examples/meshtastic/lisp/main.lisp +++ b/examples/meshtastic/lisp/main.lisp @@ -22,7 +22,8 @@ (when (and (= 1 index) (not (app:setting :latest-receiver))) (q> |currentIndex| ui:*main-view* 0)) - (q> |visible| ui:*find* (= 1 index)) + (q> |visible| ui:*location* (= 0 index)) + (q> |visible| ui:*find* (= 1 index)) (values)) (defun icon-press-and-hold (name) ; see QML diff --git a/examples/meshtastic/lisp/messages.lisp b/examples/meshtastic/lisp/messages.lisp index 6270d44..3c77b21 100644 --- a/examples/meshtastic/lisp/messages.lisp +++ b/examples/meshtastic/lisp/messages.lisp @@ -112,7 +112,7 @@ another mobile node is moving to different places (using GPS of phone), sending an ':e ...' text message, which will be echoed with info about signal strength, position and distance." - (let ((from-pos (getf loc:*positions* from)) + (let ((from-pos (loc:position* from)) (my-pos #+mobile (loc:last-gps-position) #-mobile nil)) (format nil "~A~%~%snr: ~F rssi: ~D~%lat: ~,5F lon: ~,5F~%distance: ~:D m" @@ -120,7 +120,6 @@ (if my-pos (first my-pos) "-") (if my-pos (second my-pos) "-") (if (and from-pos my-pos) - (loc:distance (cons (first my-pos) (second my-pos)) - (cons (getf from-pos :lat) (getf from-pos :lon))) + (loc:distance my-pos from-pos) "-")))) diff --git a/examples/meshtastic/lisp/package.lisp b/examples/meshtastic/lisp/package.lisp index 4999aad..b837cbc 100644 --- a/examples/meshtastic/lisp/package.lisp +++ b/examples/meshtastic/lisp/package.lisp @@ -99,9 +99,13 @@ (:export #:*my-position* #:*positions* + #:activate-map #:distance #:ini #:last-gps-position + #:position* + #:position-count #:set-position + #:tile-path #:update-my-position)) diff --git a/examples/meshtastic/lisp/qt.lisp b/examples/meshtastic/lisp/qt.lisp index e8171ab..3026a56 100644 --- a/examples/meshtastic/lisp/qt.lisp +++ b/examples/meshtastic/lisp/qt.lisp @@ -8,6 +8,7 @@ #:last-position #:local-ip #:start-device-discovery + #:start-tile-server #:read* #:short-names #:sql-query diff --git a/examples/meshtastic/lisp/ui-vars.lisp b/examples/meshtastic/lisp/ui-vars.lisp index 7c7594f..1099f78 100644 --- a/examples/meshtastic/lisp/ui-vars.lisp +++ b/examples/meshtastic/lisp/ui-vars.lisp @@ -11,7 +11,11 @@ #:*find* #:*find-text* #:*loading* + #:*location* #:*main-view* + #:*map* + #:*map-loader* + #:*map-view* #:*messages* #:*message-view* #:*modem* @@ -32,6 +36,10 @@ (defparameter *find* "find") (defparameter *find-text* "find_text") (defparameter *loading* "loading") +(defparameter *location* "location") +(defparameter *map* "map") +(defparameter *map-loader* "map_loader") +(defparameter *map-view* "map_view") (defparameter *main-view* "main_view") (defparameter *messages* "messages") (defparameter *message-view* "message_view") diff --git a/examples/meshtastic/qml/ext/Group.qml b/examples/meshtastic/qml/ext/Group.qml index 6b29175..5c581ce 100644 --- a/examples/meshtastic/qml/ext/Group.qml +++ b/examples/meshtastic/qml/ext/Group.qml @@ -221,5 +221,11 @@ Rectangle { } } } + + Ext.Map { + objectName: "map_view" + anchors.fill: rect + visible: false + } } diff --git a/examples/meshtastic/qml/ext/MainView.qml b/examples/meshtastic/qml/ext/MainView.qml index b91476d..88b2b6d 100644 --- a/examples/meshtastic/qml/ext/MainView.qml +++ b/examples/meshtastic/qml/ext/MainView.qml @@ -54,7 +54,7 @@ Item { currentIndex: 1 interactive: false - Ext.Group {} + Ext.Group { id: group } Ext.Messages {} Ext.Radios {} diff --git a/examples/meshtastic/qml/ext/Map.qml b/examples/meshtastic/qml/ext/Map.qml new file mode 100644 index 0000000..4f9d333 --- /dev/null +++ b/examples/meshtastic/qml/ext/Map.qml @@ -0,0 +1,85 @@ +import QtQuick 2.15 +import QtLocation 5.15 +import QtPositioning 5.15 +import "." as Ext + +Item { + anchors.fill: parent + + Component { + id: mapComponent + + Map { + id: map + objectName: "map" + anchors.fill: parent + plugin: mapPlugin + zoomLevel: 14 + + function coordinate(pos) { + return QtPositioning.coordinate(pos[0], pos[1]) + } + + function setCenter(pos) { + center = coordinate(pos) + } + + function updatePositions(group) { + for (var i = 0; i < group.count; i++) { + var data = group.get(i) + var pos = Lisp.call("loc:position*", data.nodeNum) + if (pos) { + var marker = markers.itemAt(i) + marker.radioName = data.radioName + marker.customName = data.customName + marker.coordinate = coordinate(pos) + marker.visible = true + } + } + } + + Plugin { + id: mapPlugin + name: "osm" // Open Street Map + + // for downloading tiles + PluginParameter { + name: "osm.mapping.cache.directory" + value: Lisp.call("loc:tile-path") + } + // for offline tiles (from cache) + PluginParameter { + name: "osm.mapping.offline.directory" + value: Lisp.call("loc:tile-path") + } + // number tiles (instead of MB) + PluginParameter { + name: "osm.mapping.cache.disk.cost_strategy" + value: "unitary" + } + // max number cached/offline tiles + PluginParameter { + name: "osm.mapping.cache.disk.size" + value: 10000 + } + // local tile web-server (no API key needed), see 'cpp/tile_server.h' + PluginParameter { + name: "osm.mapping.providersrepository.address" + value: "http://127.0.0.1:1702/" + } + } + + Ext.Markers { + id: markers + } + } + } + + Loader { + id: mapLoader + objectName: "map_loader" + anchors.fill: parent + sourceComponent: mapComponent + active: false + } +} diff --git a/examples/meshtastic/qml/img/location.png b/examples/meshtastic/qml/img/location.png index 3d65ee3..2e12795 100644 Binary files a/examples/meshtastic/qml/img/location.png and b/examples/meshtastic/qml/img/location.png differ diff --git a/examples/meshtastic/qml/img/marker.png b/examples/meshtastic/qml/img/marker.png new file mode 100644 index 0000000..2dc79f2 Binary files /dev/null and b/examples/meshtastic/qml/img/marker.png differ diff --git a/examples/meshtastic/qml/main.qml b/examples/meshtastic/qml/main.qml index b5821e8..f10b509 100644 --- a/examples/meshtastic/qml/main.qml +++ b/examples/meshtastic/qml/main.qml @@ -20,12 +20,27 @@ Item { height: width } - Image { + Image { // location icon ('Group') + objectName: "location" + source: "img/location.png" + width: headerHeight + height: width + anchors.right: parent.right + visible: false + + MouseArea { + anchors.fill: parent + onClicked: Lisp.call("loc:show-map-clicked") + } + } + + Image { // find icon ('Messages') objectName: "find" source: "img/find.png" width: headerHeight height: width anchors.right: parent.right + visible: false MouseArea { anchors.fill: parent diff --git a/examples/meshtastic/readme-usage.md b/examples/meshtastic/readme-usage.md index e366cd9..168c631 100644 --- a/examples/meshtastic/readme-usage.md +++ b/examples/meshtastic/readme-usage.md @@ -55,6 +55,11 @@ person is associated to a specific radio. This view is populated automatically. You can set a name to every person associated to a radio, which defaults to 'Anonym': a press-and-hold on the name will enter edit mode. +A tap on the location item on the right shows a map with all known positions of +the persons. The map tiles are cached automatically for offline usage, which +means: once you visited a place on the map, it will remain available even +without internet connection. + Radios ------ @@ -137,3 +142,4 @@ to will be shown once the server is running. Beware though that Swank on mobile isn't very stable, but it's perfect for simple debugging purposes, or to get/set variables on the fly (but it might crash regularily if you try to eval some buffer). +