mirror of
https://gitlab.com/eql/lqml.git
synced 2025-12-06 02:30:38 -08:00
example 'meshtastic': let user select device, remember latest one used, update for android 12
This commit is contained in:
parent
ae461af4ca
commit
96d6843267
29 changed files with 461 additions and 216 deletions
|
|
@ -7,7 +7,8 @@
|
|||
:components ((:file "lisp/package")
|
||||
(:file "lisp/qt")
|
||||
(:file "lisp/ui-vars")
|
||||
(:file "lisp/radio")
|
||||
(:file "lisp/lora")
|
||||
(:file "lisp/messages")
|
||||
(:file "lisp/radios")
|
||||
(:file "lisp/main")))
|
||||
|
||||
|
|
|
|||
|
|
@ -20,14 +20,18 @@ BLE::BLE(const QBluetoothUuid& uuid) : mainServiceUuid(uuid) {
|
|||
}
|
||||
|
||||
void BLE::startDeviceDiscovery() {
|
||||
connected = false;
|
||||
scanned = false;
|
||||
currentDevice = QBluetoothDeviceInfo();
|
||||
devices.clear();
|
||||
|
||||
qDebug() << "scanning for devices...";
|
||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||
}
|
||||
|
||||
void BLE::addDevice(const QBluetoothDeviceInfo& device) {
|
||||
if (deviceFilter(device)) {
|
||||
qDebug() << "device added:" << device.name() << device.address().toString();
|
||||
qDebug() << "device added:" << device.name();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +55,7 @@ void BLE::scanServices() {
|
|||
return;
|
||||
}
|
||||
if (!currentDevice.isValid()) {
|
||||
if (initialDeviceName.isNull()) {
|
||||
if (initialDeviceName.isEmpty()) {
|
||||
currentDevice = devices.at(0);
|
||||
} else {
|
||||
for (auto device : qAsConst(devices)) {
|
||||
|
|
@ -64,7 +68,7 @@ void BLE::scanServices() {
|
|||
}
|
||||
services.clear();
|
||||
qDebug() << "connecting to device...";
|
||||
if (controller && (previousAddress != currentDevice.address())) {
|
||||
if (controller) {
|
||||
Q_EMIT deviceDisconnecting();
|
||||
controller->disconnectFromDevice();
|
||||
delete controller; controller = nullptr;
|
||||
|
|
@ -85,7 +89,6 @@ void BLE::scanServices() {
|
|||
}
|
||||
|
||||
controller->connectToDevice();
|
||||
previousAddress = currentDevice.address();
|
||||
}
|
||||
|
||||
void BLE::setCurrentDevice(const QBluetoothDeviceInfo& device) {
|
||||
|
|
@ -160,8 +163,7 @@ void BLE::disconnectFromDevice() {
|
|||
}
|
||||
|
||||
void BLE::deviceDisconnected() {
|
||||
connected = false;
|
||||
qDebug() << "disconnect from device";
|
||||
qDebug() << "disconnected from device";
|
||||
}
|
||||
|
||||
void BLE::deviceScanError(QBluetoothDeviceDiscoveryAgent::Error error) {
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ Q_SIGNALS:
|
|||
|
||||
public Q_SLOTS:
|
||||
void startDeviceDiscovery();
|
||||
void disconnectFromDevice();
|
||||
|
||||
/*** </INTERFACE> *********************************************************/
|
||||
|
||||
void scanServices();
|
||||
void connectToService(const QString&);
|
||||
void disconnectFromDevice();
|
||||
|
||||
private Q_SLOTS:
|
||||
// QBluetoothDeviceDiscoveryAgent related
|
||||
|
|
@ -62,7 +62,6 @@ private:
|
|||
void retryScan();
|
||||
QBluetoothDeviceDiscoveryAgent* discoveryAgent;
|
||||
QList<QLowEnergyService*> services;
|
||||
QBluetoothAddress previousAddress;
|
||||
QLowEnergyController* controller = nullptr;
|
||||
bool connected = false;
|
||||
bool scanned = false;
|
||||
|
|
|
|||
|
|
@ -92,23 +92,23 @@ void BLE_ME::searchCharacteristics() {
|
|||
}
|
||||
|
||||
if (toRadio.isValid() && fromRadio.isValid() && fromNum.isValid()) {
|
||||
ecl_fun("radio:set-ready");
|
||||
ecl_fun("lora:set-ready");
|
||||
}
|
||||
}
|
||||
|
||||
void BLE_ME::characteristicChanged(const QLowEnergyCharacteristic&,
|
||||
const QByteArray& data) {
|
||||
if (!data.isEmpty()) {
|
||||
ecl_fun("radio:received-from-radio", data, "notified");
|
||||
ecl_fun("lora:received-from-radio", data, "notified");
|
||||
}
|
||||
}
|
||||
|
||||
void BLE_ME::characteristicRead(const QLowEnergyCharacteristic&,
|
||||
const QByteArray& data) {
|
||||
if (data.isEmpty()) {
|
||||
ecl_fun("radio:receiving-done");
|
||||
ecl_fun("lora:receiving-done");
|
||||
} else {
|
||||
ecl_fun("radio:received-from-radio", data);
|
||||
ecl_fun("lora:received-from-radio", data);
|
||||
QTimer::singleShot(0, this, &BLE_ME::read);
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ void BLE_ME::disconnecting() {
|
|||
// disable notifications
|
||||
mainService->writeDescriptor(notifications, QByteArray::fromHex("0000"));
|
||||
}
|
||||
ecl_fun("radio:set-ready", false);
|
||||
ecl_fun("lora:set-ready", false);
|
||||
delete mainService; mainService = nullptr;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,6 @@ QT::QT() : QObject() {
|
|||
ble = new BLE_ME;
|
||||
}
|
||||
|
||||
QVariant QT::setDevice(const QVariant& vName) {
|
||||
auto name = vName.toString();
|
||||
for (auto device : qAsConst(ble->devices)) {
|
||||
if (device.name().contains(name, Qt::CaseInsensitive)) {
|
||||
ble->setCurrentDevice(device);
|
||||
return vName;
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant QT::startDeviceDiscovery(const QVariant& vName) {
|
||||
auto name = vName.toString();
|
||||
if (!name.isNull()) {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ class QT : public QObject {
|
|||
|
||||
public:
|
||||
// BLE_ME
|
||||
Q_INVOKABLE QVariant setDevice(const QVariant&);
|
||||
Q_INVOKABLE QVariant startDeviceDiscovery(const QVariant& = QVariant());
|
||||
Q_INVOKABLE QVariant startDeviceDiscovery(const QVariant&);
|
||||
Q_INVOKABLE QVariant read2();
|
||||
Q_INVOKABLE QVariant write2(const QVariant&);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(in-package :radio)
|
||||
(in-package :lora)
|
||||
|
||||
(defvar *region* :eu-868) ; Europe 868 MHz
|
||||
(defvar *settings* (list :region :eu-868)) ; Europe 868 MHz
|
||||
|
||||
(defvar *my-channel* nil)
|
||||
(defvar *channels* nil)
|
||||
|
|
@ -32,11 +32,10 @@
|
|||
:element-type '(unsigned-byte 8)
|
||||
:initial-contents list))
|
||||
|
||||
(defun set-ready (&optional (ready t)) ; called from Qt
|
||||
(setf *ready* ready)
|
||||
(when ready
|
||||
(qlater 'start-config))
|
||||
(values))
|
||||
(defun start-device-discovery (&optional (name ""))
|
||||
(setf radios:*schedule-clear* t)
|
||||
(qt:start-device-discovery qt:*ble* name)
|
||||
(q> |playing| ui:*busy* t))
|
||||
|
||||
(defun start-config ()
|
||||
(when *ready*
|
||||
|
|
@ -44,6 +43,12 @@
|
|||
(send-to-radio
|
||||
(me:make-to-radio :want-config-id *config-id*))))
|
||||
|
||||
(defun set-ready (&optional (ready t)) ; called from Qt
|
||||
(setf *ready* ready)
|
||||
(when ready
|
||||
(qlater 'start-config))
|
||||
(values))
|
||||
|
||||
(defun send-message (text)
|
||||
"Sends TEXT to radio and adds it to QML item model."
|
||||
(incf msg:*message-id*)
|
||||
|
|
@ -133,7 +138,21 @@
|
|||
(let ((info (me:node-info struct)))
|
||||
(if (eql *my-node-info* (me:num info))
|
||||
(setf *my-node-info* info)
|
||||
(push info *node-infos*))))
|
||||
(setf *node-infos*
|
||||
(nconc *node-infos* (list info))))
|
||||
(when radios:*schedule-clear*
|
||||
(radios:clear))
|
||||
(let ((name (me:short-name (me:user info)))
|
||||
(current (= (me:num info)
|
||||
(me:num *my-node-info*))))
|
||||
(radios:add-radio
|
||||
(list :name name
|
||||
:hw-model (symbol-name (me:hw-model (me:user info)))
|
||||
:battery-level (me:battery-level (me:device-metrics info))
|
||||
:current current))
|
||||
(when current
|
||||
(setf (getf *settings* :device) name))))
|
||||
(app:save-settings))
|
||||
;; channel
|
||||
((me:from-radio.has-channel struct)
|
||||
(let ((channel (me:channel struct)))
|
||||
|
|
@ -176,7 +195,7 @@
|
|||
:set-config (me:make-config
|
||||
:lora (me:make-config.lo-ra-config
|
||||
:use-preset t
|
||||
:region *region*
|
||||
:region (getf *settings* :region)
|
||||
:hop-limit 3
|
||||
:tx-enabled t))))
|
||||
;; channel settings
|
||||
|
|
@ -1,16 +1,26 @@
|
|||
(in-package :app)
|
||||
|
||||
;; set here the 4 character short name of your 2 devices
|
||||
;; (see debug output during device discovery)
|
||||
|
||||
(defvar *device-1* "128c")
|
||||
(defvar *device-2* "1c9c")
|
||||
|
||||
(defun ini ()
|
||||
(qt:ini)
|
||||
(qt:start-device-discovery qt:*ble* *device-1*) ; set device (see above)
|
||||
(load-settings)
|
||||
(msg:load-messages)
|
||||
(q> |playing| ui:*loading* nil) ; shown during Lisp startup
|
||||
(q> |playing| ui:*busy* t)) ; shown during BLE setup
|
||||
(q> |playing| ui:*loading* nil)
|
||||
#+android
|
||||
(ensure-permissions :bluetooth-scan :bluetooth-connect) ; android >= 12
|
||||
(lora:start-device-discovery (getf lora:*settings* :device "")))
|
||||
|
||||
;;; settings
|
||||
|
||||
(defvar *file* (merge-pathnames "data/settings.exp"))
|
||||
|
||||
(defun load-settings ()
|
||||
(when (probe-file *file*)
|
||||
(with-open-file (s *file*)
|
||||
(setf lora:*settings* (read s)))))
|
||||
|
||||
(defun save-settings ()
|
||||
(with-open-file (s *file* :direction :output :if-exists :supersede)
|
||||
(let ((*print-pretty* nil))
|
||||
(prin1 lora:*settings* s))))
|
||||
|
||||
(qlater 'ini)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
(defpackage :app
|
||||
(:use :cl :qml)
|
||||
(:export))
|
||||
(:export
|
||||
#:load-settings
|
||||
#:save-settings))
|
||||
|
||||
(defpackage :radio
|
||||
(defpackage :lora
|
||||
(:use :cl :qml)
|
||||
(:local-nicknames (:pr :cl-protobufs)
|
||||
(:me :cl-protobufs.meshtastic))
|
||||
|
|
@ -16,10 +18,11 @@
|
|||
#:*reading*
|
||||
#:*ready*
|
||||
#:*received*
|
||||
#:*region*
|
||||
#:*remote-node*
|
||||
#:*settings*
|
||||
#:channel-to-url
|
||||
#:start-config
|
||||
#:start-device-discovery
|
||||
#:read-radio
|
||||
#:received-from-radio
|
||||
#:send-message
|
||||
|
|
@ -37,3 +40,12 @@
|
|||
#:change-state
|
||||
#:load-messages
|
||||
#:save-messages))
|
||||
|
||||
(defpackage :radios
|
||||
(:use :cl :qml)
|
||||
(:export
|
||||
#:*schedule-clear*
|
||||
#:add-radio
|
||||
#:change-radio
|
||||
#:clear))
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@
|
|||
|
||||
(in-package :qml-user)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
(:export
|
||||
#:*ble*
|
||||
#:ini
|
||||
#:set-device
|
||||
#:start-device-discovery
|
||||
#:read*
|
||||
#:write*))
|
||||
|
|
|
|||
18
examples/meshtastic/lisp/radios.lisp
Normal file
18
examples/meshtastic/lisp/radios.lisp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(in-package :radios)
|
||||
|
||||
(defvar *schedule-clear* nil)
|
||||
|
||||
(defun add-radio (radio)
|
||||
"Adds passed RADIO (a PLIST) to QML item model.
|
||||
The model keys are:
|
||||
:name :hw-model :battery-level :current"
|
||||
(qjs |addRadio| ui:*radios* radio))
|
||||
|
||||
(defun clear ()
|
||||
(setf *schedule-clear* nil)
|
||||
(q! |clear| ui:*radios*))
|
||||
|
||||
(defun change-radio (name) ; called from QML
|
||||
(qlater (lambda () (lora:start-device-discovery name)))
|
||||
(values))
|
||||
|
||||
|
|
@ -6,12 +6,12 @@
|
|||
#:*busy*
|
||||
#:*loading*
|
||||
#:*messages*
|
||||
#:*view*))
|
||||
#:*radios*))
|
||||
|
||||
(in-package :ui)
|
||||
|
||||
(defparameter *busy* "busy")
|
||||
(defparameter *loading* "loading")
|
||||
(defparameter *messages* "messages")
|
||||
(defparameter *view* "view")
|
||||
(defparameter *radios* "radios")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> <!-- android >= 12 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <!-- android >= 12 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
|
@ -77,5 +79,4 @@
|
|||
</activity>
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
34
examples/meshtastic/qml/ext/BatteryLevel.qml
Normal file
34
examples/meshtastic/qml/ext/BatteryLevel.qml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import QtQuick 2.15
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 10
|
||||
height: 25
|
||||
color: "#f0f0f0"
|
||||
radius: 2
|
||||
border.width: 1
|
||||
border.color: "#505050"
|
||||
|
||||
property int level: 0
|
||||
|
||||
Rectangle {
|
||||
x: 1
|
||||
width: parent.width - 2
|
||||
height: (parent.height - 2) * level / 100
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 1
|
||||
color: (level > 15) ? "#28c940" : "#ff5f57"
|
||||
}
|
||||
|
||||
Text {
|
||||
x: -4 - paintedWidth
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: fontMono.name
|
||||
color: "white"
|
||||
text: level + "%"
|
||||
}
|
||||
}
|
||||
|
||||
5
examples/meshtastic/qml/ext/Groups.qml
Normal file
5
examples/meshtastic/qml/ext/Groups.qml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick 2.15
|
||||
|
||||
Rectangle {
|
||||
color: "#ccebc5"
|
||||
}
|
||||
13
examples/meshtastic/qml/ext/MainIcon.qml
Normal file
13
examples/meshtastic/qml/ext/MainIcon.qml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import QtQuick 2.15
|
||||
|
||||
Image {
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
width: header.height
|
||||
height: width
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: view.currentIndex = parent.Positioner.index
|
||||
}
|
||||
}
|
||||
54
examples/meshtastic/qml/ext/MainView.qml
Normal file
54
examples/meshtastic/qml/ext/MainView.qml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import "." as Ext
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: header
|
||||
width: parent.width
|
||||
height: main.headerHeight
|
||||
color: "#f2f2f2"
|
||||
|
||||
Row {
|
||||
height: parent.height
|
||||
spacing: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Ext.MainIcon { source: "../img/group.png" }
|
||||
Ext.MainIcon { source: "../img/message.png" }
|
||||
Ext.MainIcon { source: "../img/radio.png" }
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: view
|
||||
y: header.height
|
||||
width: parent.width
|
||||
height: parent.height - header.height
|
||||
currentIndex: 1
|
||||
|
||||
Ext.Groups {}
|
||||
Ext.Messages {}
|
||||
Ext.Radios {}
|
||||
}
|
||||
|
||||
PageIndicator {
|
||||
id: control
|
||||
y: header.height - 12
|
||||
count: view.count
|
||||
currentIndex: view.currentIndex
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
delegate: Rectangle {
|
||||
width: header.height
|
||||
height: 5
|
||||
radius: width / 2
|
||||
color: "dodgerblue"
|
||||
opacity: (index === control.currentIndex) ? 1 : 0
|
||||
|
||||
Behavior on opacity { OpacityAnimator { duration: 500 }}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
examples/meshtastic/qml/ext/Messages.qml
Normal file
146
examples/meshtastic/qml/ext/Messages.qml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
Rectangle {
|
||||
id: main
|
||||
color: loading.visible ? "#1974d3" : "#e5d8bd"
|
||||
|
||||
ListView {
|
||||
id: view
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: rectEdit.height + 5
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
delegate: messageDelegate
|
||||
model: messages
|
||||
clip: true
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: messages
|
||||
objectName: "messages"
|
||||
|
||||
// hack to define all model key _types_
|
||||
ListElement {
|
||||
text: ""; sender: ""; me: true; timestamp: ""; mid: 0; ackState: 0
|
||||
}
|
||||
|
||||
function addMessage(message) {
|
||||
append(message)
|
||||
view.positionViewAtEnd()
|
||||
}
|
||||
|
||||
function changeState(state, mid) {
|
||||
for (var i = count - 1; i >= 0; i--) {
|
||||
if (get(i).mid === mid) {
|
||||
setProperty(i, "ackState", state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: remove(0) // see hack above
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageDelegate
|
||||
|
||||
Rectangle {
|
||||
id: delegate
|
||||
width: Math.max(text.contentWidth, rowSender.width + 4 * text.padding) + 2 * text.padding
|
||||
height: text.contentHeight + 2 * text.padding + sender.contentHeight
|
||||
color: model.me ? "#f2f2f2" : "#ffffcc"
|
||||
radius: 12
|
||||
|
||||
Row {
|
||||
id: rowSender
|
||||
padding: text.padding
|
||||
spacing: padding
|
||||
|
||||
AnimatedImage {
|
||||
id: semaphore
|
||||
playing: false
|
||||
y: 2
|
||||
width: 8
|
||||
height: width
|
||||
source: "../img/semaphore.gif"
|
||||
currentFrame: model.ackState
|
||||
visible: model.me
|
||||
}
|
||||
|
||||
Text {
|
||||
id: sender
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: fontMono.name
|
||||
color: "#8B0000"
|
||||
text: model.sender
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timestamp
|
||||
x: delegate.width - contentWidth - text.padding
|
||||
y: text.padding
|
||||
font.pixelSize: 10
|
||||
font.family: fontText.name
|
||||
color: "#505050"
|
||||
text: model.timestamp
|
||||
}
|
||||
|
||||
Text {
|
||||
id: text
|
||||
y: sender.contentHeight
|
||||
width: main.width
|
||||
padding: 5
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 18
|
||||
font.family: fontText.name
|
||||
color: "#303030"
|
||||
text: model.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rectEdit
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: edit.paintedHeight + 14
|
||||
border.width: 2
|
||||
border.color: edit.focus ? "dodgerblue" : "#c0c0c0"
|
||||
radius: 12
|
||||
|
||||
TextArea {
|
||||
id: edit
|
||||
anchors.fill: parent
|
||||
textFormat: TextEdit.PlainText
|
||||
font.pixelSize: 18
|
||||
font.family: fontText.name
|
||||
selectionColor: "#228ae3"
|
||||
selectedTextColor: "white"
|
||||
wrapMode: TextEdit.Wrap
|
||||
textMargin: 0
|
||||
placeholderText: qsTr("message")
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.top
|
||||
anchors.margins: 3
|
||||
width: 38
|
||||
height: width
|
||||
source: "../img/send.png"
|
||||
visible: edit.focus
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
edit.focus = Qt.NoFocus
|
||||
Lisp.call("lora:send-message", edit.text)
|
||||
edit.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
examples/meshtastic/qml/ext/Radios.qml
Normal file
88
examples/meshtastic/qml/ext/Radios.qml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import QtQuick 2.15
|
||||
import "." as Ext
|
||||
|
||||
Rectangle {
|
||||
color: "#b3cde3"
|
||||
|
||||
ListView {
|
||||
id: view
|
||||
anchors.fill: parent
|
||||
anchors.margins: 9
|
||||
spacing: 9
|
||||
delegate: radioDelegate
|
||||
model: radios
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: radios
|
||||
objectName: "radios"
|
||||
|
||||
// hack to define all model key _types_
|
||||
ListElement {
|
||||
name: ""; hwModel: ""; batteryLevel: 0; current: false
|
||||
}
|
||||
|
||||
function addRadio(radio) {
|
||||
append(radio)
|
||||
if (radio.current) {
|
||||
view.currentIndex = view.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: remove(0) // see hack above
|
||||
}
|
||||
|
||||
Component {
|
||||
id: radioDelegate
|
||||
|
||||
Rectangle {
|
||||
id: delegate
|
||||
width: Math.min(265, view.width)
|
||||
height: 35
|
||||
color: (index === view.currentIndex) ? "firebrick" : "steelblue"
|
||||
radius: height / 2
|
||||
|
||||
Rectangle {
|
||||
x: 10
|
||||
width: 42
|
||||
height: 15
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "#f0f0f0"
|
||||
radius: height / 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: fontMono.name
|
||||
color: "black"
|
||||
text: model.name
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 58
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pixelSize: 16
|
||||
font.family: fontText.name
|
||||
color: "white"
|
||||
text: model.hwModel
|
||||
}
|
||||
|
||||
Ext.BatteryLevel {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 14
|
||||
level: model.batteryLevel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
view.currentIndex = index
|
||||
Lisp.call("radios:change-radio", model.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
examples/meshtastic/qml/img/group.png
Normal file
BIN
examples/meshtastic/qml/img/group.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
examples/meshtastic/qml/img/location.png
Normal file
BIN
examples/meshtastic/qml/img/location.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
examples/meshtastic/qml/img/logo-128.png
Normal file
BIN
examples/meshtastic/qml/img/logo-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
examples/meshtastic/qml/img/message.png
Normal file
BIN
examples/meshtastic/qml/img/message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
examples/meshtastic/qml/img/radio.png
Normal file
BIN
examples/meshtastic/qml/img/radio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
examples/meshtastic/qml/img/settings.png
Normal file
BIN
examples/meshtastic/qml/img/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Window 2.15
|
||||
import "ext/" as Ext
|
||||
|
||||
Item {
|
||||
id: main
|
||||
|
|
@ -8,160 +9,21 @@ Item {
|
|||
width: 300
|
||||
height: 500
|
||||
|
||||
function availableHeight() {
|
||||
var h = Math.round(Qt.inputMethod.keyboardRectangle.y /
|
||||
((Qt.platform.os === "android") ? Screen.devicePixelRatio : 1))
|
||||
return (h === 0) ? main.height : h
|
||||
}
|
||||
property double headerHeight: 48
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: loading.visible ? "#1974d3" : "#e5d8bd"
|
||||
}
|
||||
Ext.MainView {}
|
||||
|
||||
ListView {
|
||||
id: view
|
||||
objectName: "view"
|
||||
width: parent.width
|
||||
height: availableHeight() - rectEdit.height - 3
|
||||
anchors.margins: 3
|
||||
spacing: 3
|
||||
delegate: messageDelegate
|
||||
model: messages
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: messages
|
||||
objectName: "messages"
|
||||
|
||||
// hack to define all model key _types_
|
||||
ListElement {
|
||||
text: ""; sender: ""; me: true; timestamp: ""; mid: 0; ackState: 0
|
||||
}
|
||||
|
||||
function addMessage(message) {
|
||||
append(message)
|
||||
view.positionViewAtEnd()
|
||||
}
|
||||
|
||||
function changeState(state, mid) {
|
||||
for (var i = count - 1; i >= 0; i--) {
|
||||
if (get(i).mid === mid) {
|
||||
setProperty(i, "ackState", state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: remove(0) // see hack above
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageDelegate
|
||||
|
||||
Item {
|
||||
id: delegate
|
||||
width: Math.max(text.contentWidth, rowSender.width + 4 * text.padding) + 2 * text.padding
|
||||
height: text.contentHeight + 2 * text.padding + sender.contentHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: model.me ? "#f2f2f2" : "#ffffcc"
|
||||
radius: 12
|
||||
border.width: 0
|
||||
border.color: "#dc1128"
|
||||
|
||||
Row {
|
||||
id: rowSender
|
||||
padding: text.padding
|
||||
spacing: padding
|
||||
|
||||
AnimatedImage {
|
||||
id: semaphore
|
||||
playing: false
|
||||
y: 2
|
||||
width: 8
|
||||
Image {
|
||||
source: "img/logo-128.png"
|
||||
width: headerHeight
|
||||
height: width
|
||||
source: "img/semaphore.gif"
|
||||
currentFrame: model.ackState
|
||||
visible: model.me
|
||||
}
|
||||
|
||||
Text {
|
||||
id: sender
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: fontMono.name
|
||||
color: "#8B0000"
|
||||
text: model.sender
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timestamp
|
||||
x: delegate.width - contentWidth - text.padding
|
||||
y: text.padding
|
||||
font.pixelSize: 10
|
||||
font.family: fontText.name
|
||||
color: "#505050"
|
||||
text: model.timestamp
|
||||
}
|
||||
|
||||
Text {
|
||||
id: text
|
||||
y: sender.contentHeight
|
||||
width: main.width
|
||||
padding: 5
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: 18
|
||||
font.family: fontText.name
|
||||
color: "#303030"
|
||||
text: model.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rectEdit
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: edit.paintedHeight + 14
|
||||
border.width: 2
|
||||
border.color: edit.focus ? "#228ae3" : "#c0c0c0"
|
||||
radius: 12
|
||||
|
||||
TextArea {
|
||||
id: edit
|
||||
anchors.fill: parent
|
||||
textFormat: TextEdit.PlainText
|
||||
font.pixelSize: 18
|
||||
font.family: fontText.name
|
||||
selectionColor: "#228ae3"
|
||||
selectedTextColor: "white"
|
||||
wrapMode: TextEdit.Wrap
|
||||
textMargin: 0
|
||||
placeholderText: qsTr("message")
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.top
|
||||
anchors.margins: 3
|
||||
width: 38
|
||||
source: "img/settings.png"
|
||||
width: headerHeight
|
||||
height: width
|
||||
source: "img/send.png"
|
||||
visible: edit.focus
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
edit.focus = Qt.NoFocus
|
||||
Lisp.call("radio:send-message", edit.text)
|
||||
edit.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
// shown while loading app (may take a while)
|
||||
|
|
|
|||
|
|
@ -55,19 +55,14 @@ Pairing might sometimes require some playing around. If it asks for a PIN and
|
|||
your device doesn't have a display (like the RAK starter kit), just use
|
||||
`123456`.
|
||||
|
||||
On iOS pairing is not needed beforehand, because it will ask for pairing and
|
||||
PIN during BLE ini.
|
||||
Pairing of your LoRa radios is generally not needed beforehand, except on
|
||||
android, where you need to start the app only after successful pairing.
|
||||
|
||||
If your android phone says "no BLE devices found" (see logcat output), you
|
||||
might need to uninstall an eventual app which used the LoRa radio before, and
|
||||
restart the phone.
|
||||
might need to unpair the devices and pair them again.
|
||||
|
||||
On Linux you might need to restart the bluetooth service if you want to pair
|
||||
a different device (after already pairing a first one).
|
||||
|
||||
To choose which app instance will use which device, set both name and address
|
||||
in [main.lisp](lisp/main.lisp), and set one app to `*device-1*` and the other
|
||||
to `*device-2*`.
|
||||
On Linux you might sometimes also need to unpair and pair again, if there are
|
||||
errors when trying to connect.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,5 +35,5 @@
|
|||
|
||||
(when (option "-slime")
|
||||
(load "~/slime/lqml-start-swank") ; for 'slime-connect' from Emacs
|
||||
(qlater (lambda () (in-package :radio))))
|
||||
(qlater (lambda () (in-package :lora))))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue