├── ArduinoNATS.h ├── LICENSE ├── README.md ├── examples ├── blink │ └── blink.ino └── echo │ └── echo.ino └── library.json /ArduinoNATS.h: -------------------------------------------------------------------------------- 1 | #ifndef ARDUINO_NATS_H 2 | #define ARDUINO_NATS_H 3 | #if defined(ARDUINO) && ARDUINO >= 100 4 | #include "Arduino.h" 5 | #elif defined(SPARK) 6 | #include "application.h" 7 | #endif 8 | 9 | #define NATS_CLIENT_LANG "arduino" 10 | #define NATS_CLIENT_VERSION "1.0.0" 11 | 12 | #ifndef NATS_CONF_VERBOSE 13 | #define NATS_CONF_VERBOSE false 14 | #endif 15 | 16 | #ifndef NATS_CONF_PEDANTIC 17 | #define NATS_CONF_PEDANTIC false 18 | #endif 19 | 20 | #ifndef NATS_PING_INTERVAL 21 | #define NATS_PING_INTERVAL 120000UL 22 | #endif 23 | 24 | #ifndef NATS_RECONNECT_INTERVAL 25 | #define NATS_RECONNECT_INTERVAL 5000UL 26 | #endif 27 | 28 | #define NATS_DEFAULT_PORT 4222 29 | 30 | #define NATS_INBOX_PREFIX "_INBOX." 31 | #define NATS_INBOX_ID_LENGTH 22 32 | 33 | #define NATS_MAX_ARGV 5 34 | 35 | #define NATS_CR_LF "\r\n" 36 | #define NATS_CTRL_MSG "MSG" 37 | #define NATS_CTRL_OK "+OK" 38 | #define NATS_CTRL_ERR "-ERR" 39 | #define NATS_CTRL_PING "PING" 40 | #define NATS_CTRL_PONG "PONG" 41 | #define NATS_CTRL_INFO "INFO" 42 | 43 | namespace NATSUtil { 44 | 45 | static const char alphanums[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 46 | 47 | class MillisTimer { 48 | const unsigned long interval; 49 | unsigned long t; 50 | public: 51 | MillisTimer(const unsigned long interval) : 52 | interval(interval) {} 53 | bool process() { 54 | unsigned long ms = millis(); 55 | if (ms < t || (ms - t) > interval) { 56 | t = ms; 57 | return true; 58 | } 59 | return false; 60 | } 61 | }; 62 | 63 | template 64 | class Array { 65 | private: 66 | 67 | T* data; 68 | size_t len; 69 | size_t cap; 70 | 71 | public: 72 | 73 | Array(size_t cap = 32) : len(0), cap(cap) { 74 | data = (T*)malloc(cap * sizeof(T)); 75 | } 76 | 77 | ~Array() { 78 | free(data); 79 | } 80 | 81 | private: 82 | 83 | void resize() { 84 | if (cap == 0) cap = 1; 85 | else cap *= 2; 86 | data = (T*)realloc(data, cap * sizeof(T)); 87 | } 88 | 89 | public: 90 | 91 | size_t const size() const { return len; }; 92 | 93 | void erase(size_t idx) { 94 | for (size_t i = idx; i < len; i++) { 95 | data[i] = data[i+1]; 96 | } 97 | len--; 98 | } 99 | 100 | void empty() { 101 | len = 0; 102 | cap = 32; 103 | free(data); 104 | data = (T*)malloc(cap * sizeof(T)); 105 | } 106 | 107 | T const& operator[](size_t i) const { 108 | return data[i]; 109 | } 110 | 111 | T& operator[](size_t i) { 112 | while (i >= cap) resize(); 113 | return data[i]; 114 | } 115 | 116 | size_t push_back(T v) { 117 | size_t i = len++; 118 | if (len > cap) resize(); 119 | data[i] = v; 120 | return i; 121 | } 122 | 123 | T* ptr() { return data; } 124 | 125 | }; 126 | 127 | template 128 | class Queue { 129 | 130 | private: 131 | class Node { 132 | public: 133 | T data; 134 | Node* next; 135 | Node(T data, Node* next = NULL) : data(data), next(next) {} 136 | }; 137 | Node* root; 138 | size_t len; 139 | 140 | public: 141 | Queue() : root(NULL), len(0) {} 142 | ~Queue() { 143 | Node* tmp; 144 | Node* n = root; 145 | while (n != NULL) { 146 | tmp = n->next; 147 | free(n); 148 | n = tmp; 149 | } 150 | } 151 | bool empty() const { return root == NULL; } 152 | size_t const size() const { return len; } 153 | void push(T data) { 154 | root = new Node(data, root); 155 | len++; 156 | } 157 | T pop() { 158 | Node n = *root; 159 | free(root); 160 | root = n.next; 161 | len--; 162 | return n.data; 163 | } 164 | T peek() { 165 | return root->data; 166 | } 167 | }; 168 | 169 | }; 170 | 171 | class NATS { 172 | 173 | // Structures/Types ----------------------------------------- // 174 | 175 | public: 176 | 177 | typedef struct { 178 | const char* subject; 179 | const int sid; 180 | const char* reply; 181 | const char* data; 182 | const int size; 183 | } msg; 184 | 185 | private: 186 | 187 | typedef void (*sub_cb)(msg e); 188 | typedef void (*event_cb)(); 189 | 190 | class Sub { 191 | public: 192 | sub_cb cb; 193 | int received; 194 | int max_wanted; 195 | Sub(sub_cb cb, int max_wanted = 0) : 196 | cb(cb), received(0), max_wanted(max_wanted) {} 197 | void call(msg& e) { 198 | received++; 199 | cb(e); 200 | } 201 | bool maxed() { 202 | return (max_wanted == 0)? false : received >= max_wanted; 203 | } 204 | }; 205 | 206 | // Members --------------------------------------------- // 207 | 208 | private: 209 | 210 | Client* client; 211 | 212 | const char* hostname; 213 | const int port; 214 | const char* user; 215 | const char* pass; 216 | 217 | NATSUtil::Array subs; 218 | NATSUtil::Queue free_sids; 219 | 220 | NATSUtil::MillisTimer ping_timer; 221 | NATSUtil::MillisTimer reconnect_timer; 222 | 223 | int outstanding_pings; 224 | int reconnect_attempts; 225 | 226 | public: 227 | 228 | bool connected; 229 | 230 | int max_outstanding_pings; 231 | int max_reconnect_attempts; 232 | 233 | event_cb on_connect; 234 | event_cb on_disconnect; 235 | event_cb on_error; 236 | 237 | // Constructor ----------------------------------------- // 238 | 239 | public: 240 | 241 | NATS(Client* client, const char* hostname, 242 | int port = NATS_DEFAULT_PORT, 243 | const char* user = NULL, 244 | const char* pass = NULL) : 245 | client(client), 246 | hostname(hostname), 247 | port(port), 248 | user(user), 249 | pass(pass), 250 | ping_timer(NATS_PING_INTERVAL), 251 | reconnect_timer(NATS_RECONNECT_INTERVAL), 252 | outstanding_pings(0), 253 | reconnect_attempts(0), 254 | connected(false), 255 | max_outstanding_pings(3), 256 | max_reconnect_attempts(-1), 257 | on_connect(NULL), 258 | on_disconnect(NULL), 259 | on_error(NULL) { 260 | } 261 | 262 | // Methods --------------------------------------------- // 263 | 264 | private: 265 | 266 | void send(const char* msg) { 267 | if (msg == NULL) return; 268 | client->println(msg); 269 | } 270 | 271 | int vasprintf(char** strp, const char* fmt, va_list ap) { 272 | va_list ap2; 273 | va_copy(ap2, ap); 274 | char tmp[1]; 275 | int size = vsnprintf(tmp, 1, fmt, ap2); 276 | if (size <= 0) return size; 277 | va_end(ap2); 278 | size += 1; 279 | *strp = (char*)malloc(size * sizeof(char)); 280 | return vsnprintf(*strp, size, fmt, ap); 281 | } 282 | 283 | void send_fmt(const char* fmt, ...) { 284 | va_list args; 285 | va_start(args, fmt); 286 | char* buf; 287 | vasprintf(&buf, fmt, args); 288 | va_end(args); 289 | send(buf); 290 | free(buf); 291 | } 292 | 293 | void send_connect() { 294 | send_fmt( 295 | "CONNECT {" 296 | "\"verbose\": %s," 297 | "\"pedantic\": %s," 298 | "\"lang\": \"%s\"," 299 | "\"version\": \"%s\"," 300 | "\"user\":\"%s\"," 301 | "\"pass\":\"%s\"" 302 | "}", 303 | NATS_CONF_VERBOSE? "true" : "false", 304 | NATS_CONF_PEDANTIC? "true" : "false", 305 | NATS_CLIENT_LANG, 306 | NATS_CLIENT_VERSION, 307 | (user == NULL)? "null" : user, 308 | (pass == NULL)? "null" : pass); 309 | } 310 | 311 | char* client_readline(size_t cap = 128) { 312 | char* buf = (char*)malloc(cap * sizeof(char)); 313 | int i; 314 | for (i = 0; client->available();) { 315 | char c = client->read(); 316 | if (c == '\r') continue; 317 | if (c == '\n') break; 318 | if (c == -1) break; 319 | if (i >= cap) buf = (char*)realloc(buf, (cap *= 2) * sizeof(char) + 1); 320 | buf[i++] = c; 321 | } 322 | buf[i] = '\0'; 323 | return buf; 324 | } 325 | 326 | void recv() { 327 | // read line from client 328 | char* buf = client_readline(); 329 | 330 | // tokenize line by space 331 | size_t argc = 0; 332 | const char* argv[NATS_MAX_ARGV] = {}; 333 | for (int i = 0; i < NATS_MAX_ARGV; i++) { 334 | argv[i] = strtok((i == 0) ? buf : NULL, " "); 335 | if (argv[i] == NULL) break; 336 | argc++; 337 | } 338 | 339 | // switch off of control keyword 340 | if (argc == 0) {} 341 | else if (strcmp(argv[0], NATS_CTRL_MSG) == 0) { 342 | // sanity check 343 | if (argc != 4 && argc != 5) { free(buf); return; } 344 | 345 | // get subscription id 346 | int sid = atoi(argv[2]); 347 | 348 | // make sure sub for sid is not null 349 | if (subs[sid] == NULL) { free(buf); return; }; 350 | 351 | // receive payload 352 | int payload_size = atoi((argc == 5)? argv[4] : argv[3]) + 1; 353 | char* payload_buf = client_readline(payload_size); 354 | 355 | // put data into event struct 356 | msg e = { 357 | argv[1], 358 | sid, 359 | (argc == 5)? argv[3] : "", 360 | payload_buf, 361 | payload_size 362 | }; 363 | 364 | // call callback 365 | subs[sid]->call(e); 366 | if (subs[sid]->maxed()) unsubscribe(sid); 367 | 368 | free(payload_buf); 369 | } 370 | else if (strcmp(argv[0], NATS_CTRL_OK) == 0) { 371 | } 372 | else if (strcmp(argv[0], NATS_CTRL_ERR) == 0) { 373 | if (on_error != NULL) on_error(); 374 | disconnect(); 375 | } 376 | else if (strcmp(argv[0], NATS_CTRL_PING) == 0) { 377 | send(NATS_CTRL_PONG); 378 | } 379 | else if (strcmp(argv[0], NATS_CTRL_PONG) == 0) { 380 | outstanding_pings--; 381 | } 382 | else if (strcmp(argv[0], NATS_CTRL_INFO) == 0) { 383 | send_connect(); 384 | connected = true; 385 | if (on_connect != NULL) on_connect(); 386 | } 387 | 388 | free(buf); 389 | } 390 | 391 | void ping() { 392 | if (outstanding_pings > max_outstanding_pings) { 393 | client->stop(); 394 | return; 395 | } 396 | outstanding_pings++; 397 | send(NATS_CTRL_PING); 398 | } 399 | 400 | char* generate_inbox_subject() { 401 | size_t size = (sizeof(NATS_INBOX_PREFIX) + NATS_INBOX_ID_LENGTH) * sizeof(char); 402 | char* buf = (char*)malloc(size); 403 | strcpy(buf, NATS_INBOX_PREFIX); 404 | int i; 405 | for (i = sizeof(NATS_INBOX_PREFIX)-1; i < size-1; i++) { 406 | int random_idx = random(sizeof(NATSUtil::alphanums) - 1); 407 | buf[i] = NATSUtil::alphanums[random_idx]; 408 | } 409 | buf[i] = '\0'; 410 | return buf; 411 | } 412 | 413 | public: 414 | 415 | bool connect() { 416 | if (client->connect(hostname, port)) { 417 | outstanding_pings = 0; 418 | reconnect_attempts = 0; 419 | return true; 420 | } 421 | reconnect_attempts++; 422 | return false; 423 | } 424 | 425 | void disconnect() { 426 | if (!connected) return; 427 | connected = false; 428 | client->stop(); 429 | subs.empty(); 430 | if (on_disconnect != NULL) on_disconnect(); 431 | } 432 | 433 | void publish(const char* subject, const char* msg = NULL, const char* replyto = NULL) { 434 | if (subject == NULL || subject[0] == 0) return; 435 | if (!connected) return; 436 | send_fmt("PUB %s %s %lu", 437 | subject, 438 | (replyto == NULL)? "" : replyto, 439 | (unsigned long)strlen(msg)); 440 | send((msg == NULL)? "" : msg); 441 | } 442 | void publish(const char* subject, const bool msg) { 443 | publish(subject, (msg)? "true" : "false"); 444 | } 445 | void publish_fmt(const char* subject, const char* fmt, ...) { 446 | va_list args; 447 | va_start(args, fmt); 448 | char* buf; 449 | vasprintf(&buf, fmt, args); 450 | va_end(args); 451 | publish(subject, buf); 452 | free(buf); 453 | } 454 | void publishf(const char* subject, const char* fmt, ...) { 455 | va_list args; 456 | va_start(args, fmt); 457 | char* buf; 458 | vasprintf(&buf, fmt, args); 459 | va_end(args); 460 | publish(subject, buf); 461 | free(buf); 462 | } 463 | 464 | int subscribe(const char* subject, sub_cb cb, const char* queue = NULL, const int max_wanted = 0) { 465 | if (!connected) return -1; 466 | Sub* sub = new Sub(cb, max_wanted); 467 | 468 | int sid; 469 | if (free_sids.empty()) { 470 | sid = subs.push_back(sub); 471 | } else { 472 | sid = free_sids.pop(); 473 | subs[sid] = sub; 474 | } 475 | 476 | send_fmt("SUB %s %s %d", 477 | subject, 478 | (queue == NULL)? "" : queue, 479 | sid); 480 | return sid; 481 | } 482 | 483 | void unsubscribe(const int sid) { 484 | if (!connected) return; 485 | send_fmt("UNSUB %d", sid); 486 | free(subs[sid]); 487 | subs[sid] = NULL; 488 | free_sids.push(sid); 489 | } 490 | 491 | int request(const char* subject, const char* msg, sub_cb cb, const int max_wanted = 1) { 492 | if (subject == NULL || subject[0] == 0) return -1; 493 | if (!connected) return -1; 494 | char* inbox = generate_inbox_subject(); 495 | int sid = subscribe(inbox, cb, NULL, max_wanted); 496 | publish(subject, msg, inbox); 497 | free(inbox); 498 | return sid; 499 | } 500 | 501 | void process() { 502 | if (client->connected()) { 503 | if (client->available()) 504 | recv(); 505 | if (ping_timer.process()) 506 | ping(); 507 | } else { 508 | disconnect(); 509 | if (max_reconnect_attempts == -1 || reconnect_attempts < max_reconnect_attempts) { 510 | if (reconnect_timer.process()) 511 | connect(); 512 | } 513 | } 514 | } 515 | 516 | }; 517 | 518 | #endif 519 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Josh Glendenning 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 | # NATS - Arduino Client 2 | An Arduino / Spark Core / Particle Photon compatible C++ library for 3 | communicating with a [NATS](http://nats.io) server. 4 | 5 | ## Features 6 | * Header-only library 7 | * Compatible with Ethernet and WiFi-cabable Arduinos, [Particle Photon / Spark 8 | Core](https://www.particle.io/) devices, and even the ESP8266 (if using the 9 | Arduino extension) 10 | * Familiar C++ object-oriented API, similar usage to the official NATS client 11 | APIs 12 | * Automatically attempts to reconnect to NATS server if the connection is dropped 13 | 14 | ## Installation 15 | ### [PlatformIO](http://platformio.org/) 16 | `platformio lib install ArduinoNATS` 17 | 18 | ### Arduino IDE 19 | Download a zip from the [latest release](https://github.com/joshglendenning/arduino-nats/releases/latest) and add it 20 | via _Sketch > Include Library > Add .ZIP Library_. 21 | 22 | ### Manual 23 | Just download [`ArduinoNATS.h`](https://raw.githubusercontent.com/joshglendenning/arduino-nats/master/ArduinoNATS.h) and include it in your main `ino` file. 24 | 25 | ## API 26 | ```c 27 | class NATS { 28 | typedef struct { 29 | const char* subject; 30 | const int sid; 31 | const char* reply; 32 | const char* data; 33 | const int size; 34 | } msg; 35 | 36 | typedef void (*sub_cb)(msg e); 37 | typedef void (*event_cb)(); 38 | 39 | NATS( 40 | Client* client, 41 | const char* hostname, 42 | int port = NATS_DEFAULT_PORT, 43 | const char* user = NULL, 44 | const char* pass = NULL 45 | ); 46 | 47 | bool connect(); // initiate the connection 48 | void disconnect(); // close the connection 49 | 50 | bool connected; // whether or not the client is connected 51 | 52 | int max_outstanding_pings; // number of outstanding pings to allow before considering the connection closed (default 3) 53 | int max_reconnect_attempts; // number of times to attempt reconnects, -1 means no maximum (default -1) 54 | 55 | event_cb on_connect; // called after NATS finishes connecting to server 56 | event_cb on_disconnect; // called when a disconnect happens 57 | event_cb on_error; // called when an error is received 58 | 59 | void publish(const char* subject, const char* msg = NULL, const char* replyto = NULL); 60 | void publish(const char* subject, const bool msg); 61 | void publishf(const char* subject, const char* fmt, ...); 62 | 63 | int subscribe(const char* subject, sub_cb cb, const char* queue = NULL, const int max_wanted = 0); 64 | void unsubscribe(const int sid); 65 | 66 | int request(const char* subject, const char* msg, sub_cb cb, const int max_wanted = 1); 67 | 68 | void process(); // process pending messages from the buffer, must be called regularly in loop() 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /examples/blink/blink.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | const char* WIFI_SSID = "Internet"; 6 | const char* WIFI_PSK = "password"; 7 | 8 | WiFiClient client; 9 | NATS nats( 10 | &client, 11 | "demo.nats.io", NATS_DEFAULT_PORT 12 | ); 13 | 14 | void connect_wifi() { 15 | WiFi.mode(WIFI_STA); 16 | WiFi.begin(WIFI_SSID, WIFI_PSK); 17 | while (WiFi.status() != WL_CONNECTED) { 18 | yield(); 19 | } 20 | } 21 | 22 | void nats_blink_handler(NATS::msg msg) { 23 | int count = atoi(msg.data); 24 | while (count-- > 0) { 25 | digitalWrite(LED_BUILTIN, LOW); 26 | delay(100); 27 | digitalWrite(LED_BUILTIN, HIGH); 28 | delay(100); 29 | } 30 | } 31 | 32 | void nats_on_connect() { 33 | nats.subscribe("blink", nats_blink_handler); 34 | } 35 | 36 | void setup() { 37 | pinMode(LED_BUILTIN, OUTPUT); 38 | digitalWrite(LED_BUILTIN, HIGH); 39 | 40 | connect_wifi(); 41 | 42 | nats.on_connect = nats_on_connect; 43 | nats.connect(); 44 | } 45 | 46 | void loop() { 47 | if (WiFi.status() != WL_CONNECTED) connect_wifi(); 48 | nats.process(); 49 | yield(); 50 | } 51 | -------------------------------------------------------------------------------- /examples/echo/echo.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | const char* WIFI_SSID = "Internet"; 6 | const char* WIFI_PSK = "password"; 7 | 8 | WiFiClient client; 9 | NATS nats( 10 | &client, 11 | "demo.nats.io", NATS_DEFAULT_PORT 12 | ); 13 | 14 | void connect_wifi() { 15 | WiFi.mode(WIFI_STA); 16 | WiFi.begin(WIFI_SSID, WIFI_PSK); 17 | while (WiFi.status() != WL_CONNECTED) { 18 | yield(); 19 | } 20 | } 21 | 22 | void nats_echo_handler(NATS::msg msg) { 23 | nats.publish(msg.reply, msg.data); 24 | } 25 | 26 | void nats_on_connect() { 27 | nats.subscribe("echo", nats_echo_handler); 28 | } 29 | 30 | void setup() { 31 | connect_wifi(); 32 | 33 | nats.on_connect = nats_on_connect; 34 | nats.connect(); 35 | } 36 | 37 | void loop() { 38 | if (WiFi.status() != WL_CONNECTED) connect_wifi(); 39 | nats.process(); 40 | yield(); 41 | } 42 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ArduinoNATS", 3 | "version": "1.1.2", 4 | "description": "An Arduino / ESP8266 / Particle Photon compatible C++ library for communicating with a NATS (http://nats.io) server.", 5 | "keywords": "nats, messaging, microservice, tcp, network, arduino, esp8266", 6 | "authors": { 7 | "name": "Josh Glendenning", 8 | "email": "josh@isobit.io", 9 | "url": "https://www.isobit.io" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/isobit/arduino-nats.git" 14 | }, 15 | "frameworks": "arduino", 16 | "platforms": [ 17 | "atmelavr", 18 | "espressif8266" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------