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 / Restore data from meshtastic cl-app

+

Save / Restore data from cl-meshtastic

- 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)

- Restore from saved backup file: choose meshtastic-data.zip + Restore from saved backup file(s): +
 1) choose map.bin (offline map, optional) +
 2) choose mt-data.zip (app will close, restart needed)

- +

diff --git a/examples/meshtastic/platforms/ios/assets/Library/index.html b/examples/meshtastic/platforms/ios/assets/Library/index.html index 688621d..17ad02f 100644 --- a/examples/meshtastic/platforms/ios/assets/Library/index.html +++ b/examples/meshtastic/platforms/ios/assets/Library/index.html @@ -17,19 +17,22 @@ -

Save / Restore data from meshtastic cl-app

+

Save / Restore data from cl-meshtastic

- 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)

- Restore from saved backup file: choose meshtastic-data.zip + Restore from saved backup file(s): +
 1) choose map.bin (offline map, optional) +
 2) choose mt-data.zip (app will close, restart needed)

- +

diff --git a/examples/meshtastic/qml/ext/Toast.qml b/examples/meshtastic/qml/ext/Toast.qml index edc4060..0a05b93 100644 --- a/examples/meshtastic/qml/ext/Toast.qml +++ b/examples/meshtastic/qml/ext/Toast.qml @@ -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() diff --git a/examples/meshtastic/readme-usage.md b/examples/meshtastic/readme-usage.md index b8fdd5f..734ffe4 100644 --- a/examples/meshtastic/readme-usage.md +++ b/examples/meshtastic/readme-usage.md @@ -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). diff --git a/examples/meshtastic/readme.md b/examples/meshtastic/readme.md index 7053fce..2d64a4e 100644 --- a/examples/meshtastic/readme.md +++ b/examples/meshtastic/readme.md @@ -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 diff --git a/examples/meshtastic/run.lisp b/examples/meshtastic/run.lisp index c58f9fd..0b5e7e9 100644 --- a/examples/meshtastic/run.lisp +++ b/examples/meshtastic/run.lisp @@ -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*)