mirror of
https://gitlab.com/eql/lqml.git
synced 2026-01-04 08:11:52 -08:00
example 'meshtastic': fast (offline) tile backup (not using zip); revisions
This commit is contained in:
parent
e8e0bca0e0
commit
49c0f4a80f
15 changed files with 132 additions and 49 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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*))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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*)
|
||||
|
|
|
|||
|
|
@ -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> <a href="mt-data.zip" download><code><b>mt-data.zip</b></code></a>
|
||||
<br> <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> 1) choose <code><b>map.bin</b></code> (offline map, optional)
|
||||
<br> 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>
|
||||
|
|
|
|||
|
|
@ -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> <a href="mt-data.zip" download><code><b>mt-data.zip</b></code></a>
|
||||
<br> <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> 1) choose <code><b>map.bin</b></code> (offline map, optional)
|
||||
<br> 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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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*)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue