example 'meshtastic': inform of text length limit, add search function

This commit is contained in:
pls.153 2023-07-08 13:56:33 +02:00
parent f602dd4ebd
commit 3afa583c73
18 changed files with 292 additions and 87 deletions

View file

@ -47,6 +47,7 @@ void BLE::deviceScanFinished() {
} else {
qDebug() << "device scan done";
}
QTimer::singleShot(0, this, &BLE::scanServices);
}

View file

@ -92,7 +92,7 @@ void BLE_ME::searchCharacteristics() {
}
if (toRadio.isValid() && fromRadio.isValid() && fromNum.isValid()) {
ecl_fun("lora:set-ready");
ecl_fun("lora:set-ready", currentDevice.name().right(4));
}
}
@ -151,7 +151,7 @@ void BLE_ME::disconnecting() {
// disable notifications
mainService->writeDescriptor(notifications, QByteArray::fromHex("0000"));
}
ecl_fun("lora:set-ready", false);
ecl_fun("lora:set-ready", "-", false);
delete mainService; mainService = nullptr;
}

View file

@ -7,6 +7,7 @@
(qjs |addPerson| ui:*group* person))
(defun clear ()
(setf lora:*schedule-clear* nil)
(q! |clear| ui:*group*))
(defun radio-names ()

View file

@ -29,6 +29,7 @@
(defvar *ready* nil)
(defvar *reading* nil)
(defvar *received* nil)
(defvar *schedule-clear* nil)
(defun to-bytes (list)
(make-array (length list)
@ -36,20 +37,25 @@
:initial-contents list))
(defun start-device-discovery (&optional (name ""))
(setf radios:*schedule-clear* t)
(setf *schedule-clear* t)
(setf *ble-names* nil)
(qt:start-device-discovery qt:*cpp* name)
(q> |playing| ui:*busy* t))
(defun start-config ()
(when *ready*
(setf *schedule-clear* t)
(setf *channels* nil
*node-infos* nil)
(incf *config-id*)
(send-to-radio
(me:make-to-radio :want-config-id *config-id*))))
(me:make-to-radio :want-config-id *config-id*))
(q> |playing| ui:*busy* t)))
(defun set-ready (&optional (ready t)) ; called from Qt
(defun set-ready (name &optional (ready t)) ; called from Qt
(setf *ready* ready)
(when ready
(app:toast (x:cc (tr "radio") ": " name) 2)
(qlater 'start-config))
(values))
@ -169,7 +175,7 @@
(setf *my-node-info* info)
(setf *node-infos*
(nconc *node-infos* (list info))))
(when radios:*schedule-clear*
(when *schedule-clear*
(radios:clear)
(group:clear))
(let ((name (me:short-name (me:user info)))

View file

@ -15,10 +15,21 @@
(ensure-permissions :bluetooth-scan :bluetooth-connect) ; android >= 12
(lora:start-device-discovery (or (setting :device) "")))
(defun view-index-changed (index)
(when (and (= 1 index) ; 'Messages'
(defun view-index-changed (index) ; called from QML
(when (and (= 1 index)
(not (app:setting :latest-receiver)))
(q> |currentIndex| ui:*main-view* 0))) ; 'Group'
(q> |currentIndex| ui:*main-view* 0))
(q> |visible| ui:*find* (= 1 index))
(values))
(defun icon-press-and-hold (name) ; called from QML
(cond ((string= ui:*radio-icon* name)
;; force update devices
(lora:start-device-discovery (or (setting :device) "")))
((string= ui:*group-icon* name)
;; force update nodes
(lora:start-config)))
(values))
;;; settings
@ -62,7 +73,7 @@
;;; toast
(defun toast (message)
(qjs |message| ui:*toast* message))
(defun toast (message &optional (seconds 3))
(qjs |message| ui:*toast* message seconds))
(qlater 'ini)

View file

@ -45,9 +45,64 @@
(q! |clear| ui:*messages*)
(dolist (message (db:load-messages (parse-integer x:it :radix 16)))
(add-message (read-from-string message) t))
(q! |positionViewAtEnd| ui:*message-view*)))
(qsingle-shot 100 (lambda () (q! |positionViewAtEnd| ui:*message-view*)))))
(defun receiver-changed ()
(qsleep 0.1)
(q> |currentIndex| ui:*main-view* 1) ; 'Messages'
(show-messages))
(defun check-utf8-length (text) ; called from 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."
(let ((len (length (qto-utf8 text)))
(too-long (q< |tooLong| ui:*edit*)))
(cond ((and (not too-long)
(> len 234))
(q> |tooLong| ui:*edit* t))
((and too-long
(<= len 234))
(q> |tooLong| ui:*edit* nil))))
(values))
(defun message-press-and-hold (text) ; called from QML
(set-clipboard-text text)
(app:toast (tr "message copied") 2)
(values))
(defun find-clicked ()
(let ((show (not (q< |visible| ui:*find-text*))))
(q> |visible| ui:*find-text* show)
(if show
(progn
(q! |selectAll| ui:*find-text*)
(q! |forceActiveFocus| ui:*find-text*))
(q! |clear| ui:*find-text*)
(clear-find))))
(defun find-text (text)
(unless (x:empty-string text)
(qjs |clearFind| ui:*messages*)
(qjs |find| ui:*messages* text)))
(defun clear-find ()
(qjs |clearFind| ui:*messages*)
(qlater (lambda () (qjs |positionViewAtEnd| ui:*message-view*))))
(defun highlight-term (text term)
"Highlights TERM in red, returns NIL if TERM is not found."
(let ((len (length term))
found)
(with-output-to-string (s)
(do ((e (search term text :test 'string-equal)
(search term text :test 'string-equal :start2 (+ e len)))
(b 0 (+ e len)))
((not e) (if found
(write-string (subseq text b) s)
(return-from highlight-term)))
(setf found t)
(write-string (subseq text b e) s)
(format s "<font color='red'>~A</font>"
(subseq text e (+ e len)))))))

View file

@ -2,6 +2,7 @@
(:use :cl :qml)
(:export
#:change-setting
#:icon-press-and-hold
#:ini
#:load-settings
#:save-settings
@ -26,6 +27,7 @@
#:*received*
#:*receiver*
#:*remote-node*
#:*schedule-clear*
#:*settings*
#:change-receiver
#:change-region
@ -68,13 +70,16 @@
#:*states*
#:add-message
#:change-state
#:check-utf8-length
#:clear-find
#:find-text
#:message-press-and-hold
#:receiver-changed
#:show-messages))
(defpackage :radios
(:use :cl :qml)
(:export
#:*schedule-clear*
#:add-radio
#:change-radio
#:clear))

View file

@ -1,7 +1,5 @@
(in-package :radios)
(defvar *schedule-clear* nil)
(defun add-radio (radio)
"Adds passed RADIO (a PLIST) to QML item model.
The model keys are:
@ -9,10 +7,14 @@
(qjs |addRadio| ui:*radios* radio))
(defun clear ()
(setf *schedule-clear* nil)
(setf lora:*schedule-clear* nil)
(q! |clear| ui:*radios*))
(defun change-radio (name) ; called from QML
(qlater (lambda () (lora:start-device-discovery name)))
(values))
(defun reset-configured ()
;; TODO: add in UI settings
(app:change-setting :configured nil))

View file

@ -6,11 +6,15 @@
#:*busy*
#:*edit*
#:*group*
#:*group-icon*
#:*find*
#:*find-text*
#:*loading*
#:*main-view*
#:*messages*
#:*message-view*
#:*modem*
#:*radio-icon*
#:*radios*
#:*region*
#:*toast*
@ -21,11 +25,15 @@
(defparameter *busy* "busy")
(defparameter *edit* "edit")
(defparameter *group* "group")
(defparameter *group-icon* "group_icon")
(defparameter *find* "find")
(defparameter *find-text* "find_text")
(defparameter *loading* "loading")
(defparameter *main-view* "main_view")
(defparameter *messages* "messages")
(defparameter *message-view* "message_view")
(defparameter *modem* "modem")
(defparameter *radio-icon* "radio_icon")
(defparameter *radios* "radios")
(defparameter *region* "region")
(defparameter *toast* "toast")

View file

@ -8,6 +8,7 @@ Image {
MouseArea {
anchors.fill: parent
onClicked: view.currentIndex = parent.Positioner.index
onClicked: swipeView.currentIndex = parent.Positioner.index
onPressAndHold: Lisp.call("app:icon-press-and-hold", parent.objectName)
}
}

View file

@ -17,6 +17,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
Ext.MainIcon {
objectName: "group_icon"
source: "../img/group.png"
Rectangle {
@ -33,17 +34,19 @@ Item {
}
Ext.MainIcon {
objectName: "message_icon"
source: "../img/message.png"
}
Ext.MainIcon {
objectName: "radio_icon"
source: "../img/radio.png"
}
}
}
SwipeView {
id: view
id: swipeView
objectName: "main_view"
y: header.height
width: parent.width
@ -61,8 +64,8 @@ Item {
PageIndicator {
id: control
y: header.height - 12
count: view.count
currentIndex: view.currentIndex
count: swipeView.count
currentIndex: swipeView.currentIndex
anchors.horizontalCenter: parent.horizontalCenter
delegate: Rectangle {

View file

@ -8,10 +8,10 @@ Rectangle {
ListView {
id: view
objectName: "message_view"
anchors.topMargin: rectFind.height + 4
anchors.fill: parent
anchors.bottomMargin: rectEdit.height + 5
anchors.bottomMargin: rectEdit.height + 3
anchors.margins: 5
spacing: 5
delegate: messageDelegate
model: messages
clip: true
@ -23,7 +23,8 @@ Rectangle {
// hack to define all model key _types_
ListElement {
receiver: ""; sender: ""; senderName: ""; timestamp: ""; hour: ""; text: ""; mid: ""; ackState: 0; me: true
receiver: ""; sender: ""; senderName: ""; timestamp: ""; hour: "";
text: ""; text2: ""; mid: ""; ackState: 0; me: true
}
function addMessage(message) { append(message) }
@ -37,19 +38,55 @@ Rectangle {
}
}
function find(term) {
for (var i = 0; i < count; i++) {
var text = get(i).text
var highlighted = Lisp.call("msg:highlight-term", text, term)
if (highlighted) {
if (get(i).text2 === undefined) {
setProperty(i, "text2", text)
}
setProperty(i, "text", highlighted)
}
view.itemAtIndex(i).visible = !!highlighted
}
view.positionViewAtBeginning()
}
function clearFind() {
for (var i = 0; i < count; i++) {
var text2 = get(i).text2
if (text2) {
setProperty(i, "text", text2)
setProperty(i, "text2", undefined)
}
view.itemAtIndex(i).visible = true
}
}
Component.onCompleted: remove(0) // see hack above
}
Component {
id: messageDelegate
Rectangle {
Item {
id: delegate
width: Math.max(text.contentWidth, rowSender.width + 4 * text.padding) + 2 * text.padding
height: text.contentHeight + 2 * text.padding + sender.contentHeight
width: Math.max(text.paintedWidth, rowSender.width + 4 * text.padding) + 2 * text.padding
height: visible ? (text.contentHeight + 2 * text.padding + sender.contentHeight + 8) : 0
Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height - 4
color: model.me ? "#f2f2f2" : "#ffffcc"
radius: 12
MouseArea {
anchors.fill: parent
onPressAndHold: Lisp.call("msg:message-press-and-hold", model.text)
}
Row {
id: rowSender
padding: text.padding
@ -94,18 +131,50 @@ Rectangle {
font.pixelSize: 18
font.family: fontText.name
color: "#303030"
textFormat: Text.StyledText // for 'paintedWidth' to always work
text: model.text
}
}
}
}
// find text
TextField {
id: findText
objectName: "find_text"
y: 1
width: parent.width
height: visible ? (edit.paintedHeight + 14) : 0
font.pixelSize: 18
font.family: fontText.name
selectionColor: "#228ae3"
selectedTextColor: "white"
placeholderText: qsTr("search")
visible: false
background: Rectangle {
id: rectFind
color: "white"
border.width: 3
border.color: findText.focus ? "dodgerblue" : "#c0c0c0"
radius: 12
}
onEditingFinished: Lisp.call("msg:find-text", text)
}
// send text
Rectangle {
id: rectEdit
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
width: parent.width
height: edit.paintedHeight + 14
color: "white"
border.width: 3
border.color: edit.focus ? "dodgerblue" : "#c0c0c0"
border.color: edit.focus ? (edit.tooLong ? "#ff5f57" : "dodgerblue") : "#c0c0c0"
radius: 12
TextArea {
@ -120,6 +189,10 @@ Rectangle {
wrapMode: TextEdit.Wrap
textMargin: 0
placeholderText: qsTr("message")
property bool tooLong: false
onLengthChanged: if (length > 150) Lisp.call("msg:check-utf8-length", text)
}
Image {
@ -129,7 +202,7 @@ Rectangle {
width: 38
height: width
source: "../img/send.png"
visible: edit.focus
visible: edit.focus && !edit.tooLong
MouseArea {
anchors.fill: parent

View file

@ -107,6 +107,7 @@ Rectangle {
anchors.fill: parent
onClicked: {
if (index > 0) { // current radio is 0
view.currentIndex = index
Lisp.call("radios:change-radio", model.name)
}
@ -114,3 +115,4 @@ Rectangle {
}
}
}
}

View file

@ -11,11 +11,12 @@ Rectangle {
color: "#303030"
border.width: 2
border.color: "white"
radius: height / 2
radius: Math.min(25, height / 2)
opacity: 0
visible: false
function message(text) { // called from Lisp
function message(text, seconds) { // called from Lisp
pause.duration = 1000 * seconds
toast.visible = true
msg.text = text
anim.start()
@ -46,6 +47,7 @@ Rectangle {
}
PauseAnimation {
id: pause
duration: 3000
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -6,12 +6,12 @@ import "ext/" as Ext
Item {
id: main
objectName: "main"
width: 300
height: 500
width: 350
height: 550
property double headerHeight: 48
Ext.MainView {}
Ext.MainView { id: view }
Image {
source: "img/logo-128.png"
@ -20,11 +20,16 @@ Item {
}
Image {
source: "img/settings.png"
objectName: "find"
source: "img/find.png"
width: headerHeight
height: width
anchors.right: parent.right
visible: false // currently not needed
MouseArea {
anchors.fill: parent
onClicked: Lisp.call("msg:find-clicked")
}
}
// shown while loading app (may take a while)

View file

@ -39,6 +39,8 @@ Messages
The initial view shows the messages between you and a chosen person. You choose
the desired person in the **Group** view (swipe to the left).
To copy a message to the clipboard, simply press-and-hold it.
Group
-----
@ -62,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.
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.
Unread messages
---------------
@ -72,3 +78,23 @@ inform you of new, unread messages from another user.
Switching to **Group**, a red circle with the number of unread messages is
shown on the right of every person.
Tips
----
If (for some reason) you want to redo the bluetooth discovery of your radio(s),
just press-and-hold on the **Radios** icon.
If (for some reason) you want to receive again the mesh node configuration from
your radio, just press-and-hold on the **Group** icon.
Both of above is meant to avoid app restart.
Hacker tips
-----------
If it occurs that a RAK device goes into an undefined state and doesn't seem
to work anymore, you can try to reset the flash memory from an arduino IDE,
see the RAK github and file `reset-flash.ino`. I successfully recovered a
RAK device with corrupted memory using this method.

View file

@ -51,6 +51,8 @@ Tested
Tested on Linux, macOS, android, iOS. The macOS version shows an ECL exception
during BLE ini, but works nevertheless.
It should also work on Windows >= 10, but this is not tested yet.
Since this is WIP, it may currently not work on all platforms (e.g. mobile).
@ -67,9 +69,11 @@ Pairing of your LoRa radios is generally not needed beforehand, the app will
ask for pairing/PIN during BLE ini. If your device doesn't have a display, use
`123456` as your PIN.
It may occur that the devices are sometimes not found. For me it worked again
after unpairing the devices. Remember to unpair them from all computers/mobile
devices.
It may occur that the devices are sometimes not found; in those cases
* try to turn bluetooth off and on again, and/or:
* try to reboot your radios, and/or:
* try to unpair your radios from all computers/devices
A generic bluetooth app like **nRF Connect** may help in order to see if the
devices themselves work and are able to connect.