example 'meshtastic': fast (offline) tile backup (not using zip); revisions

This commit is contained in:
pls.153 2023-08-26 09:41:02 +02:00
parent e8e0bca0e0
commit 49c0f4a80f
15 changed files with 132 additions and 49 deletions

View file

@ -2,6 +2,7 @@
:serial t
:depends-on (#-depends-loaded :my-cl-protobufs
#-depends-loaded :trivial-package-local-nicknames
#-depends-loaded :cl-fad
#+mobile :s-http-server
#+mobile :zip) ; see 'hacks/zip/'
:components ((:file "lisp/meshtastic-proto")

View file

@ -145,14 +145,11 @@ QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues) {
// etc
QVariant QT::dataPath() {
QVariant QT::dataPath(const QVariant& prefix) {
// for desktop
static QString path;
if (path.isEmpty()) {
path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
path.truncate(path.lastIndexOf(QChar('/')));
path.append("/cl-meshtastic/data/");
}
path.append(QStringLiteral("/cl-meshtastic/") + prefix.toString());
return path;
}

View file

@ -34,7 +34,7 @@ public:
Q_INVOKABLE QVariant sqlQuery(const QVariant&, const QVariant&);
// etc
Q_INVOKABLE QVariant dataPath();
Q_INVOKABLE QVariant dataPath(const QVariant&);
Q_INVOKABLE QVariant localIp();
QT();

View file

@ -8,6 +8,7 @@
(qt:ini-positioning qt:*cpp*)
#+ios
(q> |active| ui:*position-source* t)
(check-offline-map)
#+mobile
(update-my-position))
@ -81,7 +82,7 @@
0)))
(defun tile-path () ; see QML
(namestring (app:in-data-path "tiles/")))
(namestring (app:in-data-path "" "tiles/")))
(defun tile-provider-path () ; see QML
(if (probe-file "qml/tile-provider/")
@ -119,3 +120,68 @@
(defun position-count () ; see QML
(length *positions*))
;;; save/restore tiles
(defun copy-stream (from to &optional (size most-positive-fixnum))
(let* ((buf-size (min 8192 size))
(buf (make-array buf-size :element-type (stream-element-type from))))
(loop :for pos = (read-sequence buf from :end (min buf-size size))
:do (write-sequence buf to :end pos)
(decf size pos)
:until (or (zerop pos)
(zerop size))))
(values))
(defun make-map-bin ()
"Writes all tiles in a single file, because images are already compressed.
This is meant to avoid useless (and possibly slow) zipping."
(with-open-file (out (app:in-data-path "map.bin" "")
:direction :output :if-exists :supersede
:element-type '(unsigned-byte 8))
(let ((directories (directory (app:in-data-path "**/" "tiles/"))))
(when directories
(let ((p (search "tiles/" (namestring (first directories)) :from-end t)))
(flet ((add-string (str)
(write-sequence (x:string-to-bytes str) out))
(sep ()
(write-byte #.(char-code #\|) out)))
(dolist (dir directories)
(add-string (subseq (namestring dir) p))
(sep))
(let ((files (directory (app:in-data-path "**/*.*" "tiles/"))))
(dolist (file files)
(with-open-file (in file :element-type (stream-element-type out))
(add-string (subseq (namestring file) p))
(sep)
(add-string (princ-to-string (file-length in)))
(sep)
(copy-stream in out))))))))))
(defun extract-map-bin (&optional delete)
"Restores tiles from a previously saved single binary file named 'map.bin'."
(let ((blob (app:in-data-path "map.bin" "")))
(when (probe-file blob)
(with-open-file (in blob :element-type '(unsigned-byte 8))
(flet ((read-string ()
(let ((bytes (loop :for byte = (read-byte in nil nil)
:while (and byte (/= byte #.(char-code #\|)))
:collect byte)))
(if bytes
(x:bytes-to-string bytes)
(progn
(when delete
(close in)
(delete-file blob))
(return-from extract-map-bin))))))
(loop
(let ((name (app:in-data-path (read-string) "")))
(if (cl-fad:directory-pathname-p name)
(ensure-directories-exist name)
(let ((size (parse-integer (read-string))))
(with-open-file (out name :direction :output :if-exists :supersede
:element-type (stream-element-type in))
(copy-stream in out size)))))))))))
(defun check-offline-map ()
(extract-map-bin t))

View file

@ -21,11 +21,11 @@
(ensure-permissions :bluetooth-scan :bluetooth-connect)) ; android >= 12
(lora:start-device-discovery (or (setting :device) "")))
(defun in-data-path (file)
(defun in-data-path (file &optional (prefix "data/"))
#+mobile
(merge-pathnames (x:cc "data/" file))
(merge-pathnames (x:cc prefix file))
#-mobile
(x:cc (qt:data-path qt:*cpp*) file))
(x:cc (qrun* (qt:data-path qt:*cpp* prefix)) file))
(defun view-index-changed (index) ; see QML
(when (and (= 1 index)
@ -108,6 +108,8 @@
;;; toast
(defun toast (message &optional (seconds 3))
"Shows a temporary message/notification. If the passed time is 0 seconds, the
message will be shown until the user taps on the message."
(qjs |message| ui:*toast* message seconds))
(qlater 'ini)

View file

@ -16,13 +16,13 @@
The model keys are:
:receiver :sender :sender-name :timestamp :hour :text :mid :ack-state :me
:hidden"
(x:when-it (getf message (if (getf message :me) :receiver :sender))
(unless (or loading (getf message :me))
(x:when-it (app:setting (getf message :sender) :custom-name)
(setf (getf message :sender-name) x:it)))
(x:when-it* (app:setting (getf message :sender) :custom-name)
(setf (getf message :sender-name) x:it*)))
(unless loading
(db:save-message (parse-integer (getf message :mid))
(parse-integer (getf message (if (getf message :me) :receiver :sender))
:radix 16)
(db:save-message (parse-integer (getf message :mid)) ; mid
(parse-integer x:it :radix 16) ; uid
(prin1-to-string message)))
(if (or loading (show-message-p message))
(qjs |addMessage| ui:*messages* message)
@ -31,7 +31,7 @@
(app:change-setting sender unread :sub-key :unread-messages)
(group:set-unread sender unread)))
(unless loading
(q! |positionViewAtEnd| ui:*message-view*)))
(q! |positionViewAtEnd| ui:*message-view*))))
(defun change-state (state mid)
(let* ((i-state (position state *states*))

View file

@ -105,9 +105,12 @@
#:*my-position*
#:*positions*
#:activate-map
#:check-offline-map
#:distance
#:extract-map-bin
#:ini
#:last-gps-position
#:make-map-bin
#:position*
#:position-count
#:set-position

View file

@ -52,7 +52,7 @@
"SLIME-listener"
(lambda () (swank/create-server interface port dont-close style))))
(x:when-it (app:my-ip)
(app:toast (x:cc "slime-connect " x:it) 15)))
(app:toast (x:cc "slime-connect " x:it) 0)))
(defun stop-swank (&optional (port 4005))
(when (find-package :swank)

View file

@ -33,7 +33,7 @@
(in-package :s-http-server)
(defvar *data-file* "meshtastic-data.zip")
(defvar *data-file* "mt-data.zip")
(defvar *web-server* nil)
(defvar *empty-line* #.(map 'vector 'char-code (list #\Return #\Linefeed
#\Return #\Linefeed)))
@ -67,7 +67,8 @@
(x:while-it (search boundary content :start2 start)
(let ((filename (form-data-filename content (+ start 2) (- x:it 2))))
(unless (x:empty-string filename)
(let ((pathname (merge-pathnames (x:cc "data/" filename))))
(let ((pathname (merge-pathnames (x:cc (if (string= "bin" (pathname-type filename)) "" "data/")
filename))))
(ensure-directories-exist pathname)
(with-open-file (out pathname :direction :output :if-exists :supersede
:element-type '(unsigned-byte 8))
@ -107,6 +108,8 @@ it saves uploaded files on the server."
(save-file (get-stream (get-http-connection http-request))
(parse-integer (cdr (assoc :content-length headers)))
boundary)
;; eventual tiles to extract
(loc:check-offline-map)
;; if uploaded file is *data-file*, unzip data, close app
;; (restart needed)
(let ((data.zip (x:cc "data/" *data-file*)))
@ -129,12 +132,13 @@ it saves uploaded files on the server."
(setf *web-server* (make-s-http-server)))
(start-server *web-server*)
(when ini
(qml::zip *data-file* "data/") ; zip all data, ready to be downloaded
(qml::zip *data-file* "data/") ; zip data for downloaded
(loc:make-map-bin) ; 'map.bin' for download (no need to zip compressed images)
(qml:qlog "data zipped, ready for download")
(register-context-handler *web-server* "/" 'static-resource/upload-handler
:arguments (list *default-pathname-defaults*))))
(x:when-it (app:my-ip)
(app:toast (format nil "http://~A:1701" x:it) 15)))
(app:toast (format nil "http://~A:1701" x:it) 0)))
(defun stop ()
(stop-server *web-server*)

View file

@ -17,19 +17,22 @@
</script>
</head>
<body>
<h3>Save / Restore data from meshtastic cl-app</h3>
<h3>Save / Restore data from cl-meshtastic</h3>
<p>
<b>Save</b> data first (backup): enter URL (see app message)
<br><code>http://192.168.1.x:1701/meshtastic-data.zip</code>
<b>Save</b> data first (backup):
<br>&nbsp;<a href="mt-data.zip" download><code><b>mt-data.zip</b></code></a>
<br>&nbsp;<a href="map.bin" download><code><b>map.bin</b></code></a> (offline map, optional)
</p>
<form enctype="multipart/form-data" method="post" action="/">
<p>
<b>Restore</b> from saved backup file: choose <code>meshtastic-data.zip</code>
<b>Restore</b> from saved backup file(s):
<br>&nbsp;1) choose <code><b>map.bin</b></code> (offline map, optional)
<br>&nbsp;2) choose <code><b>mt-data.zip</b></code> (app will close, restart needed)
<br>
<input name="file" type="file" onclick="setOrange()">
</p>
<p>
<input type="submit" value="Restore" id="upload" class="upload">
<input type="submit" value="Upload" id="upload" class="upload">
</p>
</form>
</body>

View file

@ -17,19 +17,22 @@
</script>
</head>
<body>
<h3>Save / Restore data from meshtastic cl-app</h3>
<h3>Save / Restore data from cl-meshtastic</h3>
<p>
<b>Save</b> data first (backup): enter URL (see app message)
<br><code>http://192.168.1.x:1701/meshtastic-data.zip</code>
<b>Save</b> data first (backup):
<br>&nbsp;<a href="mt-data.zip" download><code><b>mt-data.zip</b></code></a>
<br>&nbsp;<a href="map.bin" download><code><b>map.bin</b></code></a> (offline map, optional)
</p>
<form enctype="multipart/form-data" method="post" action="/">
<p>
<b>Restore</b> from saved backup file: choose <code>meshtastic-data.zip</code>
<b>Restore</b> from saved backup file(s):
<br>&nbsp;1) choose <code><b>map.bin</b></code> (offline map, optional)
<br>&nbsp;2) choose <code><b>mt-data.zip</b></code> (app will close, restart needed)
<br>
<input name="file" type="file" onclick="setOrange()">
</p>
<p>
<input type="submit" value="Restore" id="upload" class="upload">
<input type="submit" value="Upload" id="upload" class="upload">
</p>
</form>
</body>

View file

@ -16,7 +16,7 @@ Rectangle {
visible: false
function message(text, seconds) { // called from Lisp
pause.duration = 1000 * seconds
pause.duration = 1000 * ((seconds === 0) ? (24 * 60 * 60) : seconds)
toast.visible = true
msg.text = text
anim.start()

View file

@ -114,7 +114,7 @@ Just use special text message `:w` (for 'web-server') and `:ws` (for 'stop
web-server') after you're done.
After starting the server, just enter the shown URL in your desktop browser,
and follow the instructions.
and follow the instructions. To hide the URL message on the phone, tap on it.
Using this method you can easily transfer all data from one mobile device to
any other device.
@ -155,10 +155,11 @@ see RAK on github and file `reset-flash.ino` (re-flash firmware afterwards).
If you are a Lisp hacker, you may enjoy the integrated Swank server (on
mobile). Just type special text message `:s`. A message with the IP to connect
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, or even during auto-completion).
to will be shown once the server is running (just tap on it to make it
disappear). 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, or even during
auto-completion).
For full Swank/Slime power you'll need the desktop version anyway (this is how
this app was developed).

View file

@ -2,6 +2,8 @@
Info
----
Please note that this is WIP/experimental.
Currently the app can only send direct messages between the radios.
It's basically meant to be used in an emergency situation, where internet is

View file

@ -9,6 +9,7 @@
(asdf:load-system :my-cl-protobufs)
(asdf:load-system :trivial-package-local-nicknames)
(asdf:load-system :cl-fad)
(push :depends-loaded *features*)