example 'meshtastic': send phone GPS position to radio; revisions

This commit is contained in:
pls.153 2023-07-15 10:48:34 +02:00
parent 80acb3c92d
commit 9699a0ce6f
28 changed files with 1566 additions and 79 deletions

View file

@ -12,5 +12,6 @@
(:file "lisp/messages")
(:file "lisp/radios")
(:file "lisp/lora")
(:file "lisp/location")
(:file "lisp/main")))

View file

@ -27,7 +27,7 @@ QMAKE_EXTRA_COMPILERS += lisp
win32: PRE_TARGETDEPS = tmp/app.lib
!win32: PRE_TARGETDEPS = tmp/libapp.a
QT += quick qml bluetooth sql
QT += quick qml bluetooth sql positioning
TEMPLATE = app
CONFIG += c++17 no_keywords release
DEFINES += DESKTOP_APP INI_ECL_CONTRIB QT_EXTENSION BACKGROUND_INI_LISP

View file

@ -31,7 +31,9 @@ void BLE::startDeviceDiscovery() {
void BLE::addDevice(const QBluetoothDeviceInfo& device) {
if (deviceFilter(device)) {
qDebug() << "device added:" << device.name();
QString name(device.name());
qDebug() << "device added:" << name;
Q_EMIT deviceDiscovered(name);
}
}

View file

@ -33,6 +33,7 @@ public:
Q_SIGNALS:
// notify
void deviceDiscovered(const QString&);
void mainServiceReady();
void deviceDisconnecting();

View file

@ -1,9 +1,15 @@
#include "qt.h"
#include "ble_meshtastic.h"
#include <ecl_fun.h>
#include <QSqlQuery>
#include <QSqlError>
#include <QtDebug>
#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include <QAndroidJniEnvironment>
#endif
QT_BEGIN_NAMESPACE
QObject* ini() {
@ -15,7 +21,12 @@ QObject* ini() {
}
QT::QT() : QObject() {
// BLE
ble = new BLE_ME;
ble->connect(ble, &BLE::deviceDiscovered,
[](const QString& fullName) {
ecl_fun("radios:device-discovered", fullName.right(4));
});
}
// BLE_ME
@ -47,6 +58,48 @@ QVariant QT::write2(const QVariant& bytes) {
return QVariant();
}
// GPS
#ifdef Q_OS_ANDROID
static void clearEventualExceptions() {
QAndroidJniEnvironment env;
if (env->ExceptionCheck()) {
env->ExceptionClear();
}
}
static qlonglong getLongField(const char* name) {
QAndroidJniObject activity = QtAndroid::androidActivity();
return static_cast<qlonglong>(activity.getField<jlong>(name));
}
static double getDoubleField(const char* name) {
QAndroidJniObject activity = QtAndroid::androidActivity();
return static_cast<double>(activity.getField<jdouble>(name));
}
#endif
QVariant QT::iniPositioning() {
#ifdef Q_OS_ANDROID
QtAndroid::runOnAndroidThread([] {
QAndroidJniObject activity = QtAndroid::androidActivity();
activity.callMethod<void>("iniLocation", "()V");
clearEventualExceptions();
});
#endif
return QVariant();
}
QVariant QT::lastPosition() {
QVariantList pos;
#ifdef Q_OS_ANDROID
pos << getDoubleField("_position_lat_")
<< getDoubleField("_position_lon_")
<< QString::number(getLongField("_position_time_")); // 'QString': see QML 'lastPosition()'
#endif
return pos;
}
// SQLite
QVariant QT::iniDb(const QVariant& name) {

View file

@ -25,6 +25,10 @@ public:
Q_INVOKABLE QVariant read2();
Q_INVOKABLE QVariant write2(const QVariant&);
// GPS
Q_INVOKABLE QVariant iniPositioning();
Q_INVOKABLE QVariant lastPosition();
// SQLite
Q_INVOKABLE QVariant iniDb(const QVariant&);
Q_INVOKABLE QVariant sqlQuery(const QVariant&, const QVariant&);

View file

@ -1,4 +1,4 @@
QT += bluetooth sql
QT += bluetooth sql positioning
TEMPLATE = lib
CONFIG += c++17 plugin release no_keywords
DEFINES += PLUGIN

View file

@ -13,7 +13,7 @@
(defun radio-names ()
(qjs |radioNames| ui:*group*))
(defun name-edited (radio name) ; called from QML
(defun name-edited (radio name) ; see QML
(app:change-setting radio name :sub-key :custom-name)
(values))

View file

@ -0,0 +1,41 @@
(in-package :loc)
(defvar *positions* nil)
(defvar *my-position* nil)
(defun ini ()
#+android
(qt:ini-positioning qt:*cpp*)
#+mobile
(progn
(q> |active| ui:*position-source* t)
(update-my-position)))
(defun update-my-position ()
"Mobile only: update position from GPS of mobile device."
(unless (getf *positions* (lora:my-num))
(destructuring-bind (lat lon time)
#+android
(qt:last-position qt:*cpp*)
#-android
(qjs |lastPosition| ui:*position-source*)
(if (zerop lat)
(qsingle-shot 1000 'update-my-position)
(let ((pos (list :lat lat
:lon lon
:time (if (zerop (length time)) 0 (parse-integer time)))))
(setf *my-position* pos)
(qlog "position-updated: ~A" pos)
(set-position (lora:my-num) pos)
(send-to-radio)))))) ; just once on startup (for now)
(defun send-to-radio ()
(if lora:*config-complete*
(lora:send-position *my-position*)
(qsingle-shot 1000 'send-to-radio)))
(defun set-position (node pos)
(let ((lat (getf pos :lat)))
(when (and node lat (not (zerop lat)))
(setf (getf *positions* node) pos))))

View file

@ -25,11 +25,12 @@
;;; ini/send/receive
(defvar *config-id* 0)
(defvar *config-complete* nil)
(defvar *notify-id* nil)
(defvar *ready* nil)
(defvar *reading* nil)
(defvar *received* nil)
(defvar *schedule-clear* nil)
(defvar *schedule-clear* t)
(defun to-bytes (list)
(make-array (length list)
@ -45,14 +46,15 @@
(defun start-config ()
(when *ready*
(setf *schedule-clear* t)
(setf *channels* nil
(setf *config-complete* nil
*channels* nil
*node-infos* nil)
(incf *config-id*)
(send-to-radio
(me:make-to-radio :want-config-id *config-id*))
(q> |playing| ui:*busy* t)))
(defun set-ready (name &optional (ready t)) ; called from Qt
(defun set-ready (name &optional (ready t)) ; see Qt
(setf *ready* ready)
(when ready
(app:toast (x:cc (tr "radio") ": " name) 2)
@ -62,15 +64,25 @@
(defun add-line-breaks (text)
(x:string-substitute "<br>" (string #\Newline) text))
(defun my-name ()
(when *config-complete*
(me:short-name (me:user *my-node-info*))))
(defun my-num ()
(when *config-complete*
(me:num *my-node-info*)))
(defun send-message (text)
"Sends TEXT to radio and adds it to QML item model."
(msg:check-utf8-length (q< |text| ui:*edit*))
(unless (q< |tooLong| ui:*edit*)
(incf msg:*message-id*)
(when (stringp *receiver*)
(setf *receiver* (name-to-node *receiver*)))
(send-to-radio
(me:make-to-radio
:packet (me:make-mesh-packet
:from (me:num *my-node-info*)
:from (my-num)
:to *receiver*
:id msg:*message-id*
:want-ack t
@ -85,7 +97,7 @@
:text (add-line-breaks text)
:mid (princ-to-string msg:*message-id*) ; STRING for JS
:ack-state (position :sending msg:*states*)
:me t)))
:me t))))
(defun read-radio ()
"Triggers a read on the radio. Will call RECEIVED-FROM-RADIO on success."
@ -99,7 +111,7 @@
(qt:write* qt:*cpp* (header (length bytes)))
(qt:write* qt:*cpp* bytes))))
(defun received-from-radio (bytes &optional notified) ; called from Qt
(defun received-from-radio (bytes &optional notified) ; see Qt
(if notified
(progn
(setf *notify-id* bytes)
@ -110,7 +122,7 @@
(push from-radio *received*)))
(values))
(defun receiving-done () ; called from Qt
(defun receiving-done () ; see Qt
(setf *reading* nil)
(process-received)
(values))
@ -125,14 +137,20 @@
(when (string= name (me:short-name (me:user info)))
(return (me:num info)))))
(defun my-name ()
(me:short-name (me:user *my-node-info*)))
(defun timestamp-to-hour (&optional (secs (get-universal-time)))
(multiple-value-bind (_ m h)
(decode-universal-time secs)
(format nil "~D:~2,'0D" h m)))
(defun set-gps-position (node pos)
(flet ((to-float (i)
(float (/ i (expt 10 7)))))
(when (me:latitude-i pos)
(loc:set-position node (list :lat (to-float (me:latitude-i pos))
:lon (to-float (me:longitude-i pos))
:alt (me:altitude pos)
:time (me:time pos))))))
(defun process-received ()
"Walks *RECEIVED* FROM-RADIOs and saves relevant data."
(setf *received* (nreverse *received*))
@ -167,7 +185,12 @@
(t
(qlog "message state changed: ~A" state)
:not-received))
(princ-to-string (me:request-id decoded)))))))))) ; STRING for JS
(princ-to-string (me:request-id decoded))))) ; STRING for JS
;; GPS location
(:position-app
(unless (zerop (length payload))
(set-gps-position (me:from packet)
(pr:deserialize-from-bytes 'me:position payload)))))))))
;; my-info
((me:from-radio.has-my-info struct)
(setf *my-node-info* (me:my-node-num (me:my-info struct))))
@ -178,6 +201,8 @@
(setf *my-node-info* info)
(setf *node-infos*
(nconc *node-infos* (list info))))
(x:when-it (me:position info)
(set-gps-position (me:num info) x:it))
(when *schedule-clear*
(radios:clear)
(group:clear))
@ -192,6 +217,7 @@
:node-num (princ-to-string (me:num info)) ; STRING for JS
:current (equal name (app:setting :latest-receiver)))))
(when (find name *ble-names* :test 'string=)
(setf radios:*found* t)
(radios:add-radio
(list :name name
:hw-model (symbol-name (me:hw-model (me:user info)))
@ -213,6 +239,7 @@
;; config-complete-id
((me:from-radio.has-config-complete-id struct)
(when (= *config-id* (me:config-complete-id struct))
(setf *config-complete* t)
(q> |playing| ui:*busy* nil)
(qlog "config-complete id: ~A" *config-id*)
(unless (find (my-name) (app:setting :configured) :test 'string=)
@ -256,12 +283,12 @@
(qsleep 20)
(start-device-discovery (app:setting :device)))
(defun change-region (region) ; called from QML
(defun change-region (region) ; see QML
(app:change-setting :region (app:kw region))
(qlater 'change-lora-config)
(values))
(defun change-modem-preset (modem-preset) ; called from QML
(defun change-modem-preset (modem-preset) ; see QML
(app:change-setting :modem-preset (app:kw modem-preset))
(qlater 'change-lora-config)
(values))
@ -277,6 +304,27 @@
:role :primary))
(change-lora-config))
(defun send-position (pos &optional (to (my-num)))
"Send GPS position to radio."
(flet ((to-int (f)
(floor (* f (expt 10 7)))))
(send-to-radio
(me:make-to-radio
:packet (me:make-mesh-packet
:from (my-num)
:to to
:id (incf msg:*message-id*)
:hop-limit 3
:priority :background
:decoded (me:make-data
:portnum :position-app
:payload (pr:serialize-to-bytes
(me:make-position
:latitude-i (to-int (getf pos :lat))
:longitude-i (to-int (getf pos :lon))
:time (getf pos :time)))
:want-response t))))))
(defun channel-to-url (&optional channel)
(let ((base64 (base64:usb8-array-to-base64-string
(pr:serialize-to-bytes (or channel *my-channel*)))))
@ -295,7 +343,7 @@
(set-channel channel)
channel))))
(defun change-receiver (receiver) ; called from QML
(defun change-receiver (receiver) ; see QML
(setf *receiver* (parse-integer receiver)) ; STRING for JS
(app:change-setting :latest-receiver (node-to-name *receiver*))
(msg:receiver-changed)

View file

@ -5,6 +5,7 @@
(load-settings)
(lora:ini)
(db:ini)
(loc:ini)
(setf msg:*message-id* (1+ (db:max-message-id)))
(if (setting :latest-receiver)
(msg:show-messages)
@ -12,17 +13,19 @@
(q> |playing| ui:*loading* nil)
(q> |interactive| ui:*main-view* t)
#+android
(ensure-permissions :bluetooth-scan :bluetooth-connect) ; android >= 12
(progn
(ensure-permissions :access-fine-location) ; for sharing location
(ensure-permissions :bluetooth-scan :bluetooth-connect)) ; android >= 12
(lora:start-device-discovery (or (setting :device) "")))
(defun view-index-changed (index) ; called from QML
(defun view-index-changed (index) ; see QML
(when (and (= 1 index)
(not (app:setting :latest-receiver)))
(q> |currentIndex| ui:*main-view* 0))
(q> |visible| ui:*find* (= 1 index))
(values))
(defun icon-press-and-hold (name) ; called from QML
(defun icon-press-and-hold (name) ; see QML
(cond ((string= ui:*radio-icon* name)
;; force update devices
(lora:start-device-discovery (or (setting :device) "")))

View file

@ -52,7 +52,7 @@
(q> |currentIndex| ui:*main-view* 1) ; 'Messages'
(show-messages))
(defun check-utf8-length (text) ; called from QML
(defun check-utf8-length (&optional (text (q< |text| ui:*edit*))) ; see QML
"Checks the actual number of bytes to send (e.g. an emoji is 4 utf8 bytes),
because we can't exceed 234 bytes, which will give 312 bytes encoded protobuf
payload."
@ -66,7 +66,7 @@
(q> |tooLong| ui:*edit* nil))))
(values))
(defun message-press-and-hold (text) ; called from QML
(defun message-press-and-hold (text) ; see QML
(set-clipboard-text text)
(app:toast (tr "message copied") 2)
(values))
@ -89,7 +89,7 @@
(qjs |clearFind| ui:*messages*)
(qlater (lambda () (qjs |positionViewAtEnd| ui:*message-view*))))
(defun highlight-term (text term) ; called from QML
(defun highlight-term (text term) ; see QML
"Highlights TERM in red, returns NIL if TERM is not found."
(let ((len (length term))
found)

View file

@ -18,6 +18,7 @@
(:export
#:*channel*
#:*channels*
#:*config-complete*
#:*config-lora*
#:*my-node-info*
#:*node-infos*
@ -34,6 +35,9 @@
#:change-modem-preset
#:channel-to-url
#:ini
#:my-name
#:my-num
#:send-position
#:start-config
#:start-device-discovery
#:read-radio
@ -80,7 +84,21 @@
(defpackage :radios
(:use :cl :qml)
(:export
#:*found*
#:add-radio
#:change-radio
#:clear))
#:clear
#:device-discovered
#:reset-configured
#:reset-default-radio))
(defpackage :location
(:nicknames :loc)
(:use :cl :qml)
(:export
#:*my-position*
#:*positions*
#:ini
#:set-position
#:update-my-position))

View file

@ -4,6 +4,8 @@
#:*cpp*
#:ini
#:ini-db
#:ini-positioning
#:last-position
#:start-device-discovery
#:read*
#:short-names

View file

@ -1,5 +1,16 @@
(in-package :radios)
(defvar *found* nil)
(defun device-discovered (name)
"Show discovered (cached) device, which may not be reachable / turned on."
(unless *found*
(add-radio
(list :name name
:hw-model "Meshtastic" ; we don't know yet
:current (equal name (app:setting :device))
:ini t))))
(defun add-radio (radio)
"Adds passed RADIO (a PLIST) to QML item model.
The model keys are:
@ -10,10 +21,17 @@
(setf lora:*schedule-clear* nil)
(q! |clear| ui:*radios*))
(defun change-radio (name) ; called from QML
(defun change-radio (name) ; see QML
(app:change-setting :device name)
(unless *found*
(clear))
(qlater (lambda () (lora:start-device-discovery name)))
(values))
(defun reset-default-radio ()
;; TODO: add in UI settings
(app:change-setting :device nil))
(defun reset-configured ()
;; TODO: add in UI settings
(app:change-setting :configured nil))

View file

@ -14,6 +14,7 @@
#:*messages*
#:*message-view*
#:*modem*
#:*position-source*
#:*radio-icon*
#:*radios*
#:*region*
@ -33,6 +34,7 @@
(defparameter *messages* "messages")
(defparameter *message-view* "message_view")
(defparameter *modem* "modem")
(defparameter *position-source* "position_source")
(defparameter *radio-icon* "radio_icon")
(defparameter *radios* "radios")
(defparameter *region* "region")

View file

@ -28,6 +28,10 @@
<string>This file was generated by Qt/QMake.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>For connecting to meshtastic radio devices.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>To share location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>To share location.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>

View file

@ -183,3 +183,4 @@ Rectangle {
}
}
}

View file

@ -72,7 +72,7 @@ Rectangle {
Item {
id: delegate
width: Math.max(text.paintedWidth, rowSender.width + 4 * text.padding) + 2 * text.padding
width: Math.max(text.paintedWidth, rowSender.width + 4 * text.padding) + 2 * text.padding + 4
height: visible ? (text.contentHeight + 2 * text.padding + sender.contentHeight + 8) : 0
Rectangle {
@ -95,7 +95,7 @@ Rectangle {
AnimatedImage {
id: semaphore
playing: false
y: 3
y: 4
width: 8
height: width
source: "../img/semaphore.gif"
@ -105,7 +105,7 @@ Rectangle {
Text {
id: sender
font.pixelSize: 11
font.pixelSize: 12
font.family: fontText.name
color: "#8B0000"
text: model.senderName ? model.senderName : model.sender
@ -116,7 +116,7 @@ Rectangle {
id: timestamp
x: delegate.width - contentWidth - text.padding
y: text.padding
font.pixelSize: 11
font.pixelSize: 12
font.family: fontText.name
color: "#505050"
text: model.hour

View file

@ -46,7 +46,7 @@ Rectangle {
// hack to define all model key _types_
ListElement {
name: ""; hwModel: ""; batteryLevel: 0; current: false
name: ""; ini: false; hwModel: ""; batteryLevel: 0; current: false
}
function addRadio(radio) {
@ -66,7 +66,7 @@ Rectangle {
id: delegate
width: Math.min(265, view.width)
height: 35
color: (index === view.currentIndex) ? "firebrick" : "steelblue"
color: (index === view.currentIndex) ? "firebrick" : (model.ini ? "#808080" : "steelblue")
radius: height / 2
Rectangle {
@ -101,6 +101,7 @@ Rectangle {
anchors.right: parent.right
anchors.rightMargin: 14
level: model.batteryLevel
visible: !model.ini
}
MouseArea {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

@ -1,6 +1,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import QtPositioning 5.15
import "ext/" as Ext
Item {
@ -74,6 +75,31 @@ Item {
playing: false
}
// GPS
PositionSource {
objectName: "position_source"
updateInterval: 2000
active: false
property double lat: 0
property double lon: 0
property string time: "" // no 'long' in JS
onPositionChanged: {
if (position.latitudeValid && position.longitudeValid) {
var coor = position.coordinate;
lat = coor.latitude
lon = coor.longitude
time = coor.timestamp ? String(coor.timestamp) : ""
}
}
function lastLocation() {
return [lat, lon, time]
}
}
Ext.Toast {}
FontLoader { id: fontText; source: "fonts/Ubuntu.ttf" }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
64a65,70
> // for hack
> import android.location.LocationManager;
> import android.location.LocationListener;
> import android.location.LocationRequest;
> import android.util.Log;
>
66a73,123
> // hack
> public double _position_lat_ = 0.0;
> public double _position_lon_ = 0.0;
> public double _position_alt_ = 0.0;
> public long _position_time_ = 0;
> private static final String LQML = "[LQML]";
>
> // hack
> public void iniLocation()
> {
> LocationListener mLocationListenerGPS = new LocationListener() {
> @Override
> public void onLocationChanged(android.location.Location location) {
> _position_lat_ = location.getLatitude();
> _position_lon_ = location.getLongitude();
> _position_alt_ = location.getAltitude();
> _position_time_ = location.getTime();
> //String msg = "lat: " + latitude + " lon: " + longitude;
> //Log.d(LQML, msg);
> }
>
> @Override
> public void onStatusChanged(String provider, int status, Bundle extras) {
> //Log.d(LQML, "GPS: onStatusChanged");
> }
>
> @Override
> public void onProviderEnabled(String provider) {
> //Log.d(LQML, "GPS: onProviderEnabled");
> }
>
> @Override
> public void onProviderDisabled(String provider) {
> //Log.d(LQML, "GPS: onProviderDisabled");
> }
> };
>
> try {
> //Log.d(LQML, "ini GPS location...");
> LocationManager mLocationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
> mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
> 2000, // 2 secs
> 100, // HIGH_ACCURACY
> mLocationListenerGPS);
> //Log.d(LQML, "ini GPS location OK");
> }
> catch (Exception e) {
> Log.e(LQML, Log.getStackTraceString(e));
> }
> }
>

View file

@ -0,0 +1,25 @@
**android only**
Why
---
This is needed on android because a mobile device which doesn't move only
receives one (or maybe two) GPS updates before stopping the updates.
Since we have a long startup time with this app, we would completely lose
the first GPS updates using the convenient QML wrapper `PositionSource`,
hence not being able to communicate our position to the radio device.
The only workaround seems to be to directly access the Java `Location`
functions.
HowTo
-----
Just copy `QtActivity.java` to path in `path.txt` (for Qt 5.15);
alternatively apply patch `git.diff`.
See also [cpp/qt.cpp](../cpp/qt.cpp).

View file

@ -10,6 +10,8 @@ See also notes in [my-cl-protobufs.asd](my-cl-protobufs.asd).
You will also need **uiop** installed under e.g. `~/quicklisp/local-projects/`
(see ASDF sources).
For android please see [qt-location-hack](qt-location-hack/).
Prepare

View file

@ -64,6 +64,10 @@ If you use more than 1 radio, just switch here to the radio you want to use.
Changing a radio will take several seconds, because the initial configuration
needs to be repeated.
Initially, the list will contain gray items, so you can choose another radio
if you experience that the connection didn't work, in case the selected radio
is currently not available.
A press-and-hold on the radio icon will restart bluetooth device discovery.
This may be useful if you forgot to enable bluetooth before starting the app,
or if your radio is not being discovered the first time.
@ -79,6 +83,13 @@ Switching to **Group**, a red circle with the number of unread messages is
shown on the right of every person.
GPS position
------------
On mobile, and if the radio doesn't have a GPS module, the location of the
phone is sent once (at startup) to the radio.
Tips
----

View file

@ -5,7 +5,10 @@ Info
Please note: this is **WIP!**
Currently it can be used to send direct messages between any number of radios.
Eventually it will (hopefully) catch up with the official app versions.
It's basically meant to be used in an emergency situation, where internet is
not available, in order to communicate with simple text messages. This kind of
mesh network is limited to about 70 nodes/radios/users to remain reliable.
@ -82,25 +85,6 @@ See also [readme-usage](readme-usage.md).
TODO / ideas
------------
(1) Since this uses Lisp, there's the possibility to integrate a programmable
interface, where users can define their own functions and extend the UI.
Think of simple scripts, which send protobufs to the radio and process the
received data, while having access to all the variables and functions of
the app itself.
Those extensions could also be shared among users.
(2) Regarding encryption: since I don't like QR codes for sharing a channel,
there's the possibility to just share a made up phrase which can easily be
remembered, and use the letters of the phrase as encryption key, so people can
share their channel in a simple way.
Run
---
```