├── LICENSE ├── README.md ├── app.js ├── assets ├── AssetManifest.json ├── FontManifest.json ├── NOTICES ├── fonts │ └── MaterialIcons-Regular.otf ├── img │ ├── ThatProject_Logo.png │ └── ThatProject_Rectangle.png └── packages │ ├── cupertino_icons │ └── assets │ │ └── CupertinoIcons.ttf │ └── line_icons │ └── lib │ └── assets │ └── fonts │ └── LineIcons.ttf ├── favicon.png ├── flutter_service_worker.js ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html ├── main.dart.js ├── manifest.json ├── misc └── demo.gif ├── styles.css └── version.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Map Viewer for MCU with LoRa & GPS Data 2 | 3 | * [ThatProject Channel](https://youtube.com/ThatProject) 4 | 5 | LoRa module has caught the attention of many people for a number of reasons. Being able to send data to a device that is far away is very interesting. This viewer I made is to display the GPS data sent to the LoRa module on a map. If you pass the data according to the JSON format I predefined, you can display the GPS data on the map without any problems. 6 | 7 | You can try it right away from here (But need to prepare MCU with LoRa and GPS module). 8 | 9 | 10 | 11 | 12 | ## For More Detail 13 | 14 | * [LoRa, GPS Data showing on the Map](https://youtu.be/zJvDw4UVDLc) 15 | 16 | ### Created & Maintained By 17 | 18 | [Eric Nam](https://github.com/0015) 19 | ([Youtube](https://youtube.com/ThatProject)) 20 | ([Facebook](https://www.facebook.com/groups/138965931539175)) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | let port; 2 | var keepReading = false; 3 | var reader; 4 | var inputDone; 5 | var outputDone; 6 | var outputStream; 7 | 8 | window.onload = function() { 9 | if(!detectBrowser()){ 10 | alert("Please Note\nTo access the Serial Port through the Web Serial API, you must use Google Chrome browser or Microsoft Edge for now."); 11 | } 12 | }; 13 | 14 | function startSerial() { 15 | openSerial() 16 | } 17 | 18 | async function closeSerial() { 19 | keepReading = false; 20 | 21 | reader.cancel(); 22 | await inputDone.catch(() => {}); 23 | 24 | outputStream.close(); 25 | await outputDone; 26 | 27 | await port.close(); 28 | window.SuccessMsg("Serial Port Closed", "The connected serial port is closed properly."); 29 | window.setSerialStatus(false); 30 | } 31 | 32 | function alertMessage(text) { 33 | alert(text) 34 | } 35 | 36 | function detectBrowser() { 37 | if(navigator.userAgent.indexOf("Chrome") != -1 ) { 38 | return true; 39 | }else { 40 | return false; 41 | } 42 | } 43 | 44 | async function openSerial() { 45 | try { 46 | port = await navigator.serial.requestPort({}); 47 | await port.open({ baudRate: 115200, bufferSize: 10000 }); 48 | 49 | const encoder = new TextEncoderStream(); 50 | outputDone = encoder.readable.pipeTo(port.writable); 51 | outputStream = encoder.writable; 52 | 53 | let decoder = new TextDecoderStream(); 54 | inputDone = port.readable.pipeTo(decoder.writable); 55 | inputStream = decoder.readable.pipeThrough( 56 | new TransformStream(new LineBreakTransformer()) 57 | ); 58 | 59 | reader = inputStream.getReader(); 60 | keepReading = true; 61 | readLoop(); 62 | 63 | } catch (e) { 64 | window.ErrorMsg("Serial Error", e.toString()); 65 | } 66 | } 67 | 68 | async function readLoop() { 69 | window.SuccessMsg("Serial Read", "Begin to read data from the connected serial port."); 70 | window.setSerialStatus(true); 71 | while (keepReading) { 72 | try{ 73 | const { value, done } = await reader.read(); 74 | if (value) { 75 | window.readSerialFunc(value); 76 | if (done) { 77 | reader.releaseLock(); 78 | break; 79 | } 80 | } 81 | }catch(e){ 82 | keepReading = false; 83 | window.ErrorMsg("Serial Error", e.toString()); 84 | } 85 | } 86 | } 87 | 88 | class LineBreakTransformer { 89 | constructor() { 90 | this.container = ""; 91 | } 92 | 93 | transform(chunk, controller) { 94 | this.container += chunk; 95 | const lines = this.container.split("\r\n"); 96 | this.container = lines.pop(); 97 | lines.forEach((line) => controller.enqueue(line)); 98 | } 99 | 100 | flush(controller) { 101 | controller.enqueue(this.container); 102 | } 103 | } -------------------------------------------------------------------------------- /assets/AssetManifest.json: -------------------------------------------------------------------------------- 1 | {"img/ThatProject_Logo.png":["img/ThatProject_Logo.png"],"img/ThatProject_Rectangle.png":["img/ThatProject_Rectangle.png"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"],"packages/line_icons/lib/assets/fonts/LineIcons.ttf":["packages/line_icons/lib/assets/fonts/LineIcons.ttf"]} -------------------------------------------------------------------------------- /assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]},{"family":"packages/line_icons/Awesome Line Icons 1.3.0","fonts":[{"asset":"packages/line_icons/lib/assets/fonts/LineIcons.ttf"}]}] -------------------------------------------------------------------------------- /assets/fonts/MaterialIcons-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/assets/fonts/MaterialIcons-Regular.otf -------------------------------------------------------------------------------- /assets/img/ThatProject_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/assets/img/ThatProject_Logo.png -------------------------------------------------------------------------------- /assets/img/ThatProject_Rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/assets/img/ThatProject_Rectangle.png -------------------------------------------------------------------------------- /assets/packages/cupertino_icons/assets/CupertinoIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf -------------------------------------------------------------------------------- /assets/packages/line_icons/lib/assets/fonts/LineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/assets/packages/line_icons/lib/assets/fonts/LineIcons.ttf -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/favicon.png -------------------------------------------------------------------------------- /flutter_service_worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const MANIFEST = 'flutter-app-manifest'; 3 | const TEMP = 'flutter-temp-cache'; 4 | const CACHE_NAME = 'flutter-app-cache'; 5 | const RESOURCES = { 6 | "version.json": "fc02c12240b8f0294c6f9692fe31734a", 7 | "index.html": "0ec1d7b36e342c308c36d9eacfd183cf", 8 | "/": "0ec1d7b36e342c308c36d9eacfd183cf", 9 | "styles.css": "01ddf5811b8593a3d721ede630e064d4", 10 | "main.dart.js": "52c415513a681be4cb2c0a6e8ec6599a", 11 | "favicon.png": "1e40889e06d69f317710d81c8e258a24", 12 | "icons/Icon-192.png": "c3cb040d036bcefa9ff841d5efb2a66a", 13 | "icons/Icon-maskable-192.png": "c3cb040d036bcefa9ff841d5efb2a66a", 14 | "icons/Icon-maskable-512.png": "eff34ea4d72e666dba390ee95a7d6df9", 15 | "icons/Icon-512.png": "eff34ea4d72e666dba390ee95a7d6df9", 16 | "manifest.json": "cc17fa77a11254d0ca3ab22b76ea51c2", 17 | "assets/AssetManifest.json": "07b3ef7468b4e1307a687edee8cef9bb", 18 | "assets/NOTICES": "da2eddb9070d7f53d75c36c711352254", 19 | "assets/img/ThatProject_Rectangle.png": "45b12f2fc5bb7faee306083dc1e616a4", 20 | "assets/img/ThatProject_Logo.png": "123bbdc30646627585866a220e26dd46", 21 | "assets/FontManifest.json": "e024588c84b5d20cb7869d6f908130e8", 22 | "assets/packages/line_icons/lib/assets/fonts/LineIcons.ttf": "23621397bc1906a79180a918e98f35b2", 23 | "assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "6d342eb68f170c97609e9da345464e5e", 24 | "assets/fonts/MaterialIcons-Regular.otf": "7e7a6cccddf6d7b20012a548461d5d81", 25 | "app.js": "e6e741cc8a2fd7ae665a97cb03857a86" 26 | }; 27 | 28 | // The application shell files that are downloaded before a service worker can 29 | // start. 30 | const CORE = [ 31 | "main.dart.js", 32 | "index.html", 33 | "assets/NOTICES", 34 | "assets/AssetManifest.json", 35 | "assets/FontManifest.json"]; 36 | // During install, the TEMP cache is populated with the application shell files. 37 | self.addEventListener("install", (event) => { 38 | self.skipWaiting(); 39 | return event.waitUntil( 40 | caches.open(TEMP).then((cache) => { 41 | return cache.addAll( 42 | CORE.map((value) => new Request(value, {'cache': 'reload'}))); 43 | }) 44 | ); 45 | }); 46 | 47 | // During activate, the cache is populated with the temp files downloaded in 48 | // install. If this service worker is upgrading from one with a saved 49 | // MANIFEST, then use this to retain unchanged resource files. 50 | self.addEventListener("activate", function(event) { 51 | return event.waitUntil(async function() { 52 | try { 53 | var contentCache = await caches.open(CACHE_NAME); 54 | var tempCache = await caches.open(TEMP); 55 | var manifestCache = await caches.open(MANIFEST); 56 | var manifest = await manifestCache.match('manifest'); 57 | // When there is no prior manifest, clear the entire cache. 58 | if (!manifest) { 59 | await caches.delete(CACHE_NAME); 60 | contentCache = await caches.open(CACHE_NAME); 61 | for (var request of await tempCache.keys()) { 62 | var response = await tempCache.match(request); 63 | await contentCache.put(request, response); 64 | } 65 | await caches.delete(TEMP); 66 | // Save the manifest to make future upgrades efficient. 67 | await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); 68 | return; 69 | } 70 | var oldManifest = await manifest.json(); 71 | var origin = self.location.origin; 72 | for (var request of await contentCache.keys()) { 73 | var key = request.url.substring(origin.length + 1); 74 | if (key == "") { 75 | key = "/"; 76 | } 77 | // If a resource from the old manifest is not in the new cache, or if 78 | // the MD5 sum has changed, delete it. Otherwise the resource is left 79 | // in the cache and can be reused by the new service worker. 80 | if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) { 81 | await contentCache.delete(request); 82 | } 83 | } 84 | // Populate the cache with the app shell TEMP files, potentially overwriting 85 | // cache files preserved above. 86 | for (var request of await tempCache.keys()) { 87 | var response = await tempCache.match(request); 88 | await contentCache.put(request, response); 89 | } 90 | await caches.delete(TEMP); 91 | // Save the manifest to make future upgrades efficient. 92 | await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); 93 | return; 94 | } catch (err) { 95 | // On an unhandled exception the state of the cache cannot be guaranteed. 96 | console.error('Failed to upgrade service worker: ' + err); 97 | await caches.delete(CACHE_NAME); 98 | await caches.delete(TEMP); 99 | await caches.delete(MANIFEST); 100 | } 101 | }()); 102 | }); 103 | 104 | // The fetch handler redirects requests for RESOURCE files to the service 105 | // worker cache. 106 | self.addEventListener("fetch", (event) => { 107 | if (event.request.method !== 'GET') { 108 | return; 109 | } 110 | var origin = self.location.origin; 111 | var key = event.request.url.substring(origin.length + 1); 112 | // Redirect URLs to the index.html 113 | if (key.indexOf('?v=') != -1) { 114 | key = key.split('?v=')[0]; 115 | } 116 | if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') { 117 | key = '/'; 118 | } 119 | // If the URL is not the RESOURCE list then return to signal that the 120 | // browser should take over. 121 | if (!RESOURCES[key]) { 122 | return; 123 | } 124 | // If the URL is the index.html, perform an online-first request. 125 | if (key == '/') { 126 | return onlineFirst(event); 127 | } 128 | event.respondWith(caches.open(CACHE_NAME) 129 | .then((cache) => { 130 | return cache.match(event.request).then((response) => { 131 | // Either respond with the cached resource, or perform a fetch and 132 | // lazily populate the cache. 133 | return response || fetch(event.request).then((response) => { 134 | cache.put(event.request, response.clone()); 135 | return response; 136 | }); 137 | }) 138 | }) 139 | ); 140 | }); 141 | 142 | self.addEventListener('message', (event) => { 143 | // SkipWaiting can be used to immediately activate a waiting service worker. 144 | // This will also require a page refresh triggered by the main worker. 145 | if (event.data === 'skipWaiting') { 146 | self.skipWaiting(); 147 | return; 148 | } 149 | if (event.data === 'downloadOffline') { 150 | downloadOffline(); 151 | return; 152 | } 153 | }); 154 | 155 | // Download offline will check the RESOURCES for all files not in the cache 156 | // and populate them. 157 | async function downloadOffline() { 158 | var resources = []; 159 | var contentCache = await caches.open(CACHE_NAME); 160 | var currentContent = {}; 161 | for (var request of await contentCache.keys()) { 162 | var key = request.url.substring(origin.length + 1); 163 | if (key == "") { 164 | key = "/"; 165 | } 166 | currentContent[key] = true; 167 | } 168 | for (var resourceKey of Object.keys(RESOURCES)) { 169 | if (!currentContent[resourceKey]) { 170 | resources.push(resourceKey); 171 | } 172 | } 173 | return contentCache.addAll(resources); 174 | } 175 | 176 | // Attempt to download the resource online before falling back to 177 | // the offline cache. 178 | function onlineFirst(event) { 179 | return event.respondWith( 180 | fetch(event.request).then((response) => { 181 | return caches.open(CACHE_NAME).then((cache) => { 182 | cache.put(event.request, response.clone()); 183 | return response; 184 | }); 185 | }).catch((error) => { 186 | return caches.open(CACHE_NAME).then((cache) => { 187 | return cache.match(event.request).then((response) => { 188 | if (response != null) { 189 | return response; 190 | } 191 | throw error; 192 | }); 193 | }); 194 | }) 195 | ); 196 | } 197 | -------------------------------------------------------------------------------- /icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/icons/Icon-192.png -------------------------------------------------------------------------------- /icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/icons/Icon-512.png -------------------------------------------------------------------------------- /icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | web_lora_map 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ThatProject - ESP32 LoRa GPS Data Viewer", 3 | "short_name": "ESP32 LoRa GPS Data Viewer", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "ESP32 LoRa, Map", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /misc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0015/LoRa_GPS_Viewer/b492b7a40212b8724cb142f9045fa6c81649d137/misc/demo.gif -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .center { 2 | margin: 0; 3 | position: absolute; 4 | top: 50%; 5 | left: 50%; 6 | margin-right: -50%; 7 | transform: translate(-50%, -50%) 8 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"app_name":"thatproject_esp32_lora_gps_viewer","version":"1.0.0","build_number":"1","package_name":"thatproject_esp32_lora_gps_viewer"} --------------------------------------------------------------------------------