diff --git a/examples/meshtastic/app.asd b/examples/meshtastic/app.asd index 850ba87..89ddcde 100644 --- a/examples/meshtastic/app.asd +++ b/examples/meshtastic/app.asd @@ -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") diff --git a/examples/meshtastic/cpp/qt.cpp b/examples/meshtastic/cpp/qt.cpp index 9f07b4f..cc88ac7 100644 --- a/examples/meshtastic/cpp/qt.cpp +++ b/examples/meshtastic/cpp/qt.cpp @@ -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); - path.truncate(path.lastIndexOf(QChar('/'))); - path.append("/cl-meshtastic/data/"); - } + QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + path.truncate(path.lastIndexOf(QChar('/'))); + path.append(QStringLiteral("/cl-meshtastic/") + prefix.toString()); return path; } diff --git a/examples/meshtastic/cpp/qt.h b/examples/meshtastic/cpp/qt.h index 48c101d..730bc9d 100644 --- a/examples/meshtastic/cpp/qt.h +++ b/examples/meshtastic/cpp/qt.h @@ -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(); diff --git a/examples/meshtastic/lisp/location.lisp b/examples/meshtastic/lisp/location.lisp index 042068e..aa0449d 100644 --- a/examples/meshtastic/lisp/location.lisp +++ b/examples/meshtastic/lisp/location.lisp @@ -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)) + diff --git a/examples/meshtastic/lisp/main.lisp b/examples/meshtastic/lisp/main.lisp index f063bf0..9501fe5 100644 --- a/examples/meshtastic/lisp/main.lisp +++ b/examples/meshtastic/lisp/main.lisp @@ -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) diff --git a/examples/meshtastic/lisp/messages.lisp b/examples/meshtastic/lisp/messages.lisp index a35e5fe..abb2a55 100644 --- a/examples/meshtastic/lisp/messages.lisp +++ b/examples/meshtastic/lisp/messages.lisp @@ -16,22 +16,22 @@ The model keys are: :receiver :sender :sender-name :timestamp :hour :text :mid :ack-state :me :hidden" - (unless (or loading (getf message :me)) - (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) - (prin1-to-string message))) - (if (or loading (show-message-p message)) - (qjs |addMessage| ui:*messages* message) - (let* ((sender (getf message :sender)) - (unread (1+ (or (app:setting sender :unread-messages) 0)))) - (app:change-setting sender unread :sub-key :unread-messages) - (group:set-unread sender unread))) - (unless loading - (q! |positionViewAtEnd| ui:*message-view*))) + (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*))) + (unless loading + (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) + (let* ((sender (getf message :sender)) + (unread (1+ (or (app:setting sender :unread-messages) 0)))) + (app:change-setting sender unread :sub-key :unread-messages) + (group:set-unread sender unread))) + (unless loading + (q! |positionViewAtEnd| ui:*message-view*)))) (defun change-state (state mid) (let* ((i-state (position state *states*)) diff --git a/examples/meshtastic/lisp/package.lisp b/examples/meshtastic/lisp/package.lisp index ca184b6..30d6d35 100644 --- a/examples/meshtastic/lisp/package.lisp +++ b/examples/meshtastic/lisp/package.lisp @@ -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 diff --git a/examples/meshtastic/lisp/swank-quicklisp.lisp b/examples/meshtastic/lisp/swank-quicklisp.lisp index 2d81c25..ea7090b 100644 --- a/examples/meshtastic/lisp/swank-quicklisp.lisp +++ b/examples/meshtastic/lisp/swank-quicklisp.lisp @@ -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) diff --git a/examples/meshtastic/lisp/upload-download.lisp b/examples/meshtastic/lisp/upload-download.lisp index eb9425a..d04dce5 100644 --- a/examples/meshtastic/lisp/upload-download.lisp +++ b/examples/meshtastic/lisp/upload-download.lisp @@ -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*) diff --git a/examples/meshtastic/platforms/android/assets/lib/index.html b/examples/meshtastic/platforms/android/assets/lib/index.html index 688621d..17ad02f 100644 --- a/examples/meshtastic/platforms/android/assets/lib/index.html +++ b/examples/meshtastic/platforms/android/assets/lib/index.html @@ -17,19 +17,22 @@
-
- Save data first (backup): enter URL (see app message)
- http://192.168.1.x:1701/meshtastic-data.zip
+ Save data first (backup):
+
mt-data.zip
+
map.bin (offline map, optional)
- Save data first (backup): enter URL (see app message)
- http://192.168.1.x:1701/meshtastic-data.zip
+ Save data first (backup):
+
mt-data.zip
+
map.bin (offline map, optional)