├── .gitignore ├── Gateway └── Gateway.ino ├── LICENSE ├── LoRaMesh └── LoRaMesh.ino ├── README.md ├── SetNodeId └── SetNodeId.ino └── mesh-server ├── .DS_Store ├── app.js ├── package.json └── public ├── index.html └── js ├── mesh.js └── toxiclibs.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Gateway/Gateway.ino: -------------------------------------------------------------------------------- 1 | 2 | #define IOTEXPERIMENTER 3 | 4 | #include 5 | #include 6 | #include 7 | #ifdef IOTEXPERIMENTER 8 | #include 9 | #include 10 | #include 11 | #endif 12 | #include 13 | 14 | #ifdef IOTEXPERIMENTER 15 | #define OLED 16 | Adafruit_SSD1306 display(-1); 17 | #define LED_PIN 15 18 | Adafruit_NeoPixel led = Adafruit_NeoPixel(1, LED_PIN, NEO_RGB + NEO_KHZ800); 19 | #endif 20 | 21 | const char* ssid = "your-ssid"; 22 | const char* password = "your-wifi-password"; 23 | const char* mqtt_server = "your-mqtt-server"; 24 | int mqtt_port = 8883; 25 | const char* mqtt_username = "your-mqtt-username"; 26 | const char* mqtt_password = "your-mqtt-password"; 27 | const char* dataTopic = "mesh_gateway/data"; 28 | 29 | WiFiClientSecure espClient; 30 | PubSubClient mqtt_client(espClient); 31 | char data[128]; 32 | 33 | // Connect to MQTT broker 34 | void mqtt_connect() { 35 | // Loop until we're reconnected 36 | while (!mqtt_client.connected()) { 37 | log("Connecting to MQTT..."); 38 | // Attempt to connect 39 | String mqtt_clientId = "mesh_gateway-"; 40 | mqtt_clientId += String(random(0xffff), HEX); 41 | if (mqtt_client.connect(mqtt_clientId.c_str(), mqtt_username, mqtt_password)) { 42 | log("connected"); 43 | } else { 44 | log("failed, rc=", false); 45 | log(mqtt_client.state()); 46 | delay(2000); 47 | } 48 | } 49 | } 50 | 51 | void setup() { 52 | pinMode(2, OUTPUT); // ESP8266 LED 53 | digitalWrite(2, LOW); 54 | delay(200); 55 | digitalWrite(2, HIGH); 56 | 57 | Serial.begin(115200); 58 | while (!Serial); 59 | 60 | #ifdef IOTEXPERIMENTER 61 | led.begin(); 62 | led.show(); // Initialize all pixels to 'off' 63 | 64 | display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false); 65 | display.clearDisplay(); 66 | display.setTextSize(1); 67 | display.setTextColor(WHITE); 68 | display.setCursor(0, 36); 69 | #endif 70 | 71 | log("Connecting to "); 72 | log(ssid, false); 73 | log("..."); 74 | 75 | WiFi.mode(WIFI_STA); 76 | WiFi.begin(ssid, password); 77 | while (WiFi.status() != WL_CONNECTED) { 78 | delay(250); 79 | log(".", false); 80 | } 81 | log(""); 82 | log("connected"); 83 | 84 | mqtt_client.setServer(mqtt_server, mqtt_port); 85 | mqtt_connect(); 86 | } 87 | 88 | void log(const char *s) { 89 | #ifdef OLED 90 | if (display.getCursorY() > 58) { 91 | display.clearDisplay(); 92 | display.setCursor(0, 0); 93 | } 94 | display.println(s); 95 | display.display(); delay(1); 96 | #endif 97 | } 98 | 99 | void log(const char *s, boolean newline) { 100 | if (newline) { 101 | return log(s); 102 | } 103 | #ifdef OLED 104 | if (display.getCursorY() > 58) { 105 | display.clearDisplay(); 106 | display.setCursor(0, 0); 107 | } 108 | display.print(s); 109 | display.display(); delay(1); 110 | #endif 111 | } 112 | 113 | 114 | void loop() { 115 | if (!mqtt_client.connected()) { 116 | mqtt_connect(); 117 | } 118 | mqtt_client.loop(); 119 | 120 | delay(50); // Give the ESP time to handle network. 121 | 122 | if (Serial.available()) { 123 | Serial.setTimeout(100); 124 | String s = Serial.readStringUntil('\n'); 125 | log(s.c_str()); 126 | if (s.startsWith("node: ")) { 127 | String data; 128 | int end = s.indexOf('\r'); 129 | if (end > 0) { 130 | data = s.substring(6, end); 131 | } else { 132 | data = s.substring(6); 133 | } 134 | mqtt_client.publish(dataTopic, data.c_str()); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 nootropic design 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 | -------------------------------------------------------------------------------- /LoRaMesh/LoRaMesh.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #define RH_HAVE_SERIAL 7 | #define LED 9 8 | #define N_NODES 4 9 | 10 | uint8_t nodeId; 11 | uint8_t routes[N_NODES]; // full routing table for mesh 12 | int16_t rssi[N_NODES]; // signal strength info 13 | 14 | // Singleton instance of the radio driver 15 | RH_RF95 rf95; 16 | 17 | // Class to manage message delivery and receipt, using the driver declared above 18 | RHMesh *manager; 19 | 20 | // message buffer 21 | char buf[RH_MESH_MAX_MESSAGE_LEN]; 22 | 23 | int freeMem() { 24 | extern int __heap_start, *__brkval; 25 | int v; 26 | return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 27 | } 28 | 29 | void setup() { 30 | randomSeed(analogRead(0)); 31 | pinMode(LED, OUTPUT); 32 | Serial.begin(115200); 33 | while (!Serial) ; // Wait for serial port to be available 34 | 35 | nodeId = EEPROM.read(0); 36 | if (nodeId > 10) { 37 | Serial.print(F("EEPROM nodeId invalid: ")); 38 | Serial.println(nodeId); 39 | nodeId = 1; 40 | } 41 | Serial.print(F("initializing node ")); 42 | 43 | manager = new RHMesh(rf95, nodeId); 44 | 45 | if (!manager->init()) { 46 | Serial.println(F("init failed")); 47 | } else { 48 | Serial.println("done"); 49 | } 50 | rf95.setTxPower(23, false); 51 | rf95.setFrequency(915.0); 52 | rf95.setCADTimeout(500); 53 | 54 | // Possible configurations: 55 | // Bw125Cr45Sf128 (the chip default) 56 | // Bw500Cr45Sf128 57 | // Bw31_25Cr48Sf512 58 | // Bw125Cr48Sf4096 59 | 60 | // long range configuration requires for on-air time 61 | boolean longRange = false; 62 | if (longRange) { 63 | RH_RF95::ModemConfig modem_config = { 64 | 0x78, // Reg 0x1D: BW=125kHz, Coding=4/8, Header=explicit 65 | 0xC4, // Reg 0x1E: Spread=4096chips/symbol, CRC=enable 66 | 0x08 // Reg 0x26: LowDataRate=On, Agc=Off. 0x0C is LowDataRate=ON, ACG=ON 67 | }; 68 | rf95.setModemRegisters(&modem_config); 69 | if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) { 70 | Serial.println(F("set config failed")); 71 | } 72 | } 73 | 74 | Serial.println("RF95 ready"); 75 | 76 | for(uint8_t n=1;n<=N_NODES;n++) { 77 | routes[n-1] = 0; 78 | rssi[n-1] = 0; 79 | } 80 | 81 | Serial.print(F("mem = ")); 82 | Serial.println(freeMem()); 83 | } 84 | 85 | const __FlashStringHelper* getErrorString(uint8_t error) { 86 | switch(error) { 87 | case 1: return F("invalid length"); 88 | break; 89 | case 2: return F("no route"); 90 | break; 91 | case 3: return F("timeout"); 92 | break; 93 | case 4: return F("no reply"); 94 | break; 95 | case 5: return F("unable to deliver"); 96 | break; 97 | } 98 | return F("unknown"); 99 | } 100 | 101 | void updateRoutingTable() { 102 | for(uint8_t n=1;n<=N_NODES;n++) { 103 | RHRouter::RoutingTableEntry *route = manager->getRouteTo(n); 104 | if (n == nodeId) { 105 | routes[n-1] = 255; // self 106 | } else { 107 | routes[n-1] = route->next_hop; 108 | if (routes[n-1] == 0) { 109 | // if we have no route to the node, reset the received signal strength 110 | rssi[n-1] = 0; 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Create a JSON string with the routing info to each node 117 | void getRouteInfoString(char *p, size_t len) { 118 | p[0] = '\0'; 119 | strcat(p, "["); 120 | for(uint8_t n=1;n<=N_NODES;n++) { 121 | strcat(p, "{\"n\":"); 122 | sprintf(p+strlen(p), "%d", routes[n-1]); 123 | strcat(p, ","); 124 | strcat(p, "\"r\":"); 125 | sprintf(p+strlen(p), "%d", rssi[n-1]); 126 | strcat(p, "}"); 127 | if (n")); 154 | Serial.print(n); 155 | Serial.print(F(" :")); 156 | Serial.print(buf); 157 | 158 | // send an acknowledged message to the target node 159 | uint8_t error = manager->sendtoWait((uint8_t *)buf, strlen(buf), n); 160 | if (error != RH_ROUTER_ERROR_NONE) { 161 | Serial.println(); 162 | Serial.print(F(" ! ")); 163 | Serial.println(getErrorString(error)); 164 | } else { 165 | Serial.println(F(" OK")); 166 | // we received an acknowledgement from the next hop for the node we tried to send to. 167 | RHRouter::RoutingTableEntry *route = manager->getRouteTo(n); 168 | if (route->next_hop != 0) { 169 | rssi[route->next_hop-1] = rf95.lastRssi(); 170 | } 171 | } 172 | if (nodeId == 1) printNodeInfo(nodeId, buf); // debugging 173 | 174 | // listen for incoming messages. Wait a random amount of time before we transmit 175 | // again to the next node 176 | unsigned long nextTransmit = millis() + random(3000, 5000); 177 | while (nextTransmit > millis()) { 178 | int waitTime = nextTransmit - millis(); 179 | uint8_t len = sizeof(buf); 180 | uint8_t from; 181 | if (manager->recvfromAckTimeout((uint8_t *)buf, &len, waitTime, &from)) { 182 | buf[len] = '\0'; // null terminate string 183 | Serial.print(from); 184 | Serial.print(F("->")); 185 | Serial.print(F(" :")); 186 | Serial.println(buf); 187 | if (nodeId == 1) printNodeInfo(from, buf); // debugging 188 | // we received data from node 'from', but it may have actually come from an intermediate node 189 | RHRouter::RoutingTableEntry *route = manager->getRouteTo(from); 190 | if (route->next_hop != 0) { 191 | rssi[route->next_hop-1] = rf95.lastRssi(); 192 | } 193 | } 194 | } 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRa Mesh Networking 2 | 3 | This project implements the components of a system that demonstrates mesh networking between LoRa nodes and a way to visualize the network on a web page. For full details of the project, see the [full project writeup on the Project Lab blog](https://nootropicdesign.com/projectlab/2018/10/20/lora-mesh-networking/). 4 | 5 | Nodes in the network are Arduino-compatible boards with a LoRa tranceiver. For example, [Moteino](https://lowpowerlab.com/guide/moteino/lora-support/) boards. 6 | 7 | There are several components of this project: 8 | 9 | ### SetNodeId 10 | 11 | Arduino sketch to set the node's ID in EEPROM so that every node can have the same source code (without hard-coding the node ID). This is a one-time process for each node. Set the node ID in this sketch then upload to a node (e.g. a Moteino). When it runs it saves the node ID in EEPROM. Then you can load the LoRaMesh sketch to the node. 12 | 13 | ### LoRaMesh 14 | 15 | Arduino sketch that attempts to talk to all other nodes in the mesh. Each node sends its routing information to every other node. The process of sending data and receiving acknowledgements lets a node determine which nodes it can successfully communicate with directly. This is how each node builds up it's routing table. You must set N_NODES to the max number of nodes in your mesh. 16 | 17 | Dependencies: 18 | 19 | * [RadioHead library](http://www.airspayce.com/mikem/arduino/RadioHead/) 20 | 21 | 22 | ### Gateway 23 | 24 | ESP8266 Arduino sketch that talks to a connected LoRa node via Serial (node number 1 in the mesh) and publishes mesh routing information to an MQTT topic. Node 1 in the mesh will eventually receive routing info from every other node. 25 | 26 | Dependencies: 27 | 28 | * [PubSubClient](https://github.com/knolleary/pubsubclient) 29 | 30 | 31 | ### mesh-server 32 | 33 | Node.js server provides a web visualization of the mesh. Runs on port 4200. Install with `npm install`. The server subscribes to the MQTT topic to receive routing info about nodes. This server sends the received routing info to the web client using Socket.IO. The web client uses p5.js to draw a representation of the mesh based on the routing information received from each node. 34 | 35 | Dependencies (install with `npm install`) 36 | 37 | * express 38 | * jquery 39 | * mqtt 40 | * socket.io 41 | * rxjs 42 | * p5 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /SetNodeId/SetNodeId.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | // change this to be the ID of your node in the mesh network 5 | uint8_t nodeId = 1; 6 | 7 | void setup() { 8 | Serial.begin(115200); 9 | while (!Serial) ; // Wait for serial port to be available 10 | 11 | Serial.println("setting nodeId..."); 12 | 13 | EEPROM.write(0, nodeId); 14 | Serial.print(F("set nodeId = ")); 15 | Serial.println(nodeId); 16 | 17 | uint8_t readVal = EEPROM.read(0); 18 | 19 | Serial.print(F("read nodeId: ")); 20 | Serial.println(readVal); 21 | 22 | if (nodeId != readVal) { 23 | Serial.println(F("*** FAIL ***")); 24 | } else { 25 | Serial.println(F("SUCCESS")); 26 | } 27 | } 28 | 29 | void loop() { 30 | 31 | } 32 | -------------------------------------------------------------------------------- /mesh-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nootropicdesign/lora-mesh/841e8dace1f57fc1d636ee54ef109bf5facdefa0/mesh-server/.DS_Store -------------------------------------------------------------------------------- /mesh-server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var server = require('http').createServer(app); 4 | var io = require('socket.io')(server); 5 | var mqtt = require('mqtt'); 6 | const { Observable, Subject, ReplaySubject, from, of, interval } = require('rxjs'); 7 | const { map, take, filter, switchMap } = require('rxjs/operators'); 8 | 9 | app.use(express.static(__dirname + '/node_modules')); 10 | app.use(express.static(__dirname + '/public' )); 11 | app.get('/', function(req, res,next) { 12 | res.sendFile(__dirname + '/index.html'); 13 | }); 14 | 15 | io.on('connection', function(client) { 16 | client.on('join', function(data) { 17 | console.log(data); 18 | }); 19 | 20 | client.on('mouse-click', 21 | function(index) { 22 | var data = testdata[index % testdata.length]; 23 | var message = JSON.stringify(data); 24 | io.sockets.emit('mesh-data', message); 25 | } 26 | ); 27 | }); 28 | 29 | 30 | 31 | var options = { 32 | host: 'your-mqtt-server', 33 | port: 1883, 34 | username: 'your-mqtt-username', 35 | password: 'your-mqtt-password' 36 | } 37 | 38 | var mqttClient = mqtt.connect(options) 39 | 40 | mqttClient.on('connect', function () { 41 | mqttClient.subscribe('mesh_gateway/data', function (err) { 42 | if (!err) { 43 | console.log("successfully subscribed to topic"); 44 | } else { 45 | console.log("failed to subscrib to topic"); 46 | } 47 | }) 48 | }) 49 | 50 | mqttClient.on('message', function (topic, message) { 51 | console.log(message.toString()) 52 | io.sockets.emit('mesh-data', message.toString()); 53 | }) 54 | 55 | /* 56 | var testdata = [ 57 | {"2": [{"n":1,"r":-44},{"n":255,"r":0},{"n":3,"r":-13}]}, 58 | {"1": [{"n":255,"r":0},{"n":0,"r":0},{"n":2,"r":0}]}, 59 | {"3": [{"n":2,"r":0},{"n":2,"r":-24},{"n":255,"r":0}]}, 60 | {"1": [{"n":255,"r":0},{"n":0,"r":0},{"n":2,"r":0}]}, 61 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 62 | {"3": [{"n":2,"r":0},{"n":2,"r":-24},{"n":255,"r":0}]}, 63 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 64 | {"1": [{"n":255,"r":0},{"n":2,"r":-54},{"n":2,"r":0}]}, 65 | {"1": [{"n":255,"r":0},{"n":2,"r":-55},{"n":2,"r":0}]}, 66 | {"2": [{"n":0,"r":0},{"n":255,"r":0},{"n":3,"r":-27}]}, 67 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 68 | {"1": [{"n":255,"r":0},{"n":2,"r":-54},{"n":2,"r":0}]}, 69 | {"2": [{"n":1,"r":-43},{"n":255,"r":0},{"n":0,"r":0}]}, 70 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 71 | {"1": [{"n":255,"r":0},{"n":2,"r":-53},{"n":0,"r":0}]}, 72 | {"2": [{"n":1,"r":-43},{"n":255,"r":0},{"n":3,"r":-27}]}, 73 | {"3": [{"n":0,"r":0},{"n":0,"r":-27},{"n":255,"r":0}]}, 74 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 75 | {"1": [{"n":255,"r":0},{"n":2,"r":-54},{"n":2,"r":0}]}, 76 | {"2": [{"n":1,"r":-43},{"n":255,"r":0},{"n":3,"r":-26}]}, 77 | {"3": [{"n":2,"r":0},{"n":2,"r":-12},{"n":255,"r":0}]}, 78 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 79 | {"1": [{"n":255,"r":0},{"n":2,"r":-53},{"n":2,"r":0}]}, 80 | {"2": [{"n":1,"r":-43},{"n":255,"r":0},{"n":3,"r":-26}]}, 81 | {"3": [{"n":2,"r":0},{"n":2,"r":-12},{"n":255,"r":0}]}, 82 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 83 | {"1": [{"n":255,"r":0},{"n":2,"r":-51},{"n":2,"r":0}]}, 84 | {"2": [{"n":1,"r":-42},{"n":255,"r":0},{"n":3,"r":-26}]}, 85 | {"3": [{"n":2,"r":0},{"n":2,"r":-12},{"n":255,"r":0}]}, 86 | {"1": [{"n":255,"r":0},{"n":2,"r":-42},{"n":2,"r":0}]}, 87 | {"1": [{"n":255,"r":0},{"n":2,"r":-53},{"n":2,"r":0}]}, 88 | {"2": [{"n":1,"r":-41},{"n":255,"r":0},{"n":3,"r":-26}]}, 89 | {"1": [{"n":255,"r":0},{"n":2,"r":-41},{"n":2,"r":0}]}, 90 | {"1": [{"n":255,"r":0},{"n":2,"r":-50},{"n":2,"r":0}]}, 91 | {"2": [{"n":1,"r":-42},{"n":255,"r":0},{"n":3,"r":-15}]}, 92 | {"1": [{"n":255,"r":0},{"n":2,"r":-41},{"n":2,"r":0}]}, 93 | {"1": [{"n":255,"r":0},{"n":2,"r":-41},{"n":0,"r":0}]}, 94 | {"2": [{"n":0,"r":0},{"n":255,"r":0},{"n":3,"r":-26}]}, 95 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":0,"r":0}]}, 96 | {"2": [{"n":1,"r":-54},{"n":255,"r":0},{"n":3,"r":-26}]}, 97 | {"1": [{"n":255,"r":0},{"n":2,"r":-42},{"n":2,"r":0}]}, 98 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":2,"r":0}]}, 99 | {"1": [{"n":255,"r":0},{"n":2,"r":-43},{"n":0,"r":0}]}, 100 | {"1": [{"n":255,"r":0},{"n":2,"r":-54},{"n":0,"r":0}]}, 101 | {"1": [{"n":255,"r":0},{"n":2,"r":-54},{"n":0,"r":0}]}, 102 | {"2": [{"n":1,"r":-41},{"n":255,"r":0},{"n":0,"r":0}]}, 103 | {"3": [{"n":2,"r":0},{"n":2,"r":-24},{"n":255,"r":0}]}, 104 | {"1": [{"n":255,"r":0},{"n":2,"r":-41},{"n":0,"r":0}]}, 105 | {"1": [{"n":255,"r":0},{"n":2,"r":-52},{"n":2,"r":0}]}, 106 | {"3": [{"n":2,"r":0},{"n":2,"r":-24},{"n":255,"r":0}]} 107 | ]; 108 | 109 | 110 | var source = interval(1000).pipe(map(i => JSON.stringify(testdata[i % testdata.length]))) 111 | .subscribe(message => { 112 | console.log(message); 113 | if (io.sockets) { 114 | io.sockets.emit('mesh-data', message.toString()); 115 | } else { 116 | console.log("no socket client"); 117 | } 118 | }); 119 | */ 120 | 121 | server.listen(4200); 122 | -------------------------------------------------------------------------------- /mesh-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mesh-server", 3 | "version": "1.0.0", 4 | "description": "web server that provides realtime mesh network routing info", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Michael Krumpus", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.16.3", 13 | "jquery": "^3.3.1", 14 | "mqtt": "^2.18.8", 15 | "p5": "^0.7.2", 16 | "rxjs": "^6.3.2", 17 | "socket.io": "^2.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mesh-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mesh-server/public/js/mesh.js: -------------------------------------------------------------------------------- 1 | // Keep track of our socket connection 2 | var socket; 3 | 4 | var VerletPhysics2D = toxi.physics2d.VerletPhysics2D, 5 | VerletParticle2D = toxi.physics2d.VerletParticle2D, 6 | VerletSpring2D = toxi.physics2d.VerletSpring2D, 7 | VerletMinDistanceSpring2D = toxi.physics2d.VerletMinDistanceSpring2D, 8 | Vec2D = toxi.geom.Vec2D, 9 | Rect = toxi.geom.Rect; 10 | 11 | var options = { 12 | nodeRadius: 40, 13 | ageThreshold: 30000, 14 | springStrength: 0.001, 15 | minDistanceSpringStrength: 0.05, 16 | strengthScale: 5.0 17 | }; 18 | 19 | var mesh, 20 | nodes, 21 | physics, 22 | nodeColors; 23 | 24 | var dataCounter = 0; 25 | var bottomPadding = 50; 26 | var selectedNode; 27 | var showIndirectRoutes = true; 28 | 29 | // utility to provide an iterator function with everly element 30 | // and every element after that element 31 | function forEachNested(arr, fn){ 32 | for(var i=0; i options.ageThreshold) { 231 | continue; 232 | } 233 | fill(nodeColors[n.nodeNum - 1]); 234 | for(var j=0;j n2.y) { 286 | ty = n.y - abs(n.y-n2.y)/2.0; 287 | } else { 288 | ty = n.y + abs(n.y-n2.y)/2.0; 289 | } 290 | tx = tx - (tOffset*q) - textWidth(s); 291 | ty = ty - (tOffset*mp*q); 292 | strokeWeight(1); 293 | text(s, tx, ty); 294 | } else { 295 | // indirect route, draw dashed line 296 | if (showIndirectRoutes && via) { 297 | dashedLine(n.x - (lineOffset*q), n.y - (lineOffset*mp*q), n2.x - (lineOffset*q), n2.y - (lineOffset*mp*q), nodeColors[n.nodeNum - 1], nodeColors[via.nodeNum - 1]); 298 | } 299 | } 300 | } 301 | } 302 | } 303 | } 304 | } 305 | 306 | Object.values(this.nodes).forEach(function (n) { 307 | n.display(); 308 | }); 309 | 310 | }; 311 | 312 | 313 | 314 | // Node inherits from `toxi.physic2d.VerletParticle2D` 315 | // and adds a `display()` function for rendering with p5.js 316 | function Node(nodeNum, routes, pos){ 317 | // extend VerletParticle2D! 318 | this.nodeNum = nodeNum; 319 | this.routes = routes; 320 | VerletParticle2D.call(this, pos); 321 | } 322 | 323 | Node.prototype = Object.create(VerletParticle2D.prototype); 324 | 325 | Node.prototype.display = function(){ 326 | fill(nodeColors[this.nodeNum - 1]); 327 | stroke(0, 50); 328 | //noStroke(); 329 | ellipse(this.x, this.y, options.nodeRadius, options.nodeRadius); 330 | fill('white'); 331 | var s = "" + this.nodeNum; 332 | var tx = this.x - textWidth(s)/2.0; 333 | var ty = this.y + textAscent()/2.0 - textDescent()/2.0; 334 | text(this.nodeNum, tx, ty); 335 | }; 336 | --------------------------------------------------------------------------------