├── .gitignore ├── LICENSE ├── README.md ├── example ├── app.js ├── index.js ├── routes │ └── thermometer.js └── sensors │ ├── sensor.js │ └── thermometer.js ├── index.js ├── lib └── router.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Henry Li 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 | # coap-router [![npm version](https://badge.fury.io/js/coap-router.svg)](https://badge.fury.io/js/coap-router) 2 | A quick demo on how to leverage web router to build [CoAP (Constrained Application Protocol)](https://en.wikipedia.org/wiki/Constrained_Application_Protocol) server. 3 | 4 | 5 | 6 | ## 1. Motivation 7 | Currently I'm working on a Node.js based IoT (Internet of Things) platform. It allows Node.js powered DTU (Data Transfer Unit) to serve as CoAP server on smart devices like Raspberry PI. 8 | 9 | When you design a Node.js based HTTP server, web router is one of your best choices to manage incoming requests from clients. Unfortunately, CoAP doesn't have one like Express Router, this project is to demonstrate how to leverage web router concept to simplify CoAP server implementation. 10 | 11 | 12 | 13 | ## 2.Usage 14 | 15 | Inspired by many other JavaScript routers, `coap-router` does almost the same recursive routing as [Express.Router](http://expressjs.com/en/guide/routing.html). 16 | 17 | Let's take a look at the example. 18 | 19 | #### Example 20 | 21 | ##### ./server.js 22 | 23 | ```js 24 | const coap = require("coap"); 25 | const app = require("./app"); 26 | const server = coap.createServer(app); 27 | server.listen(() => { 28 | console.log("The CoAP server is now running.\n" + app.help); 29 | }); 30 | ``` 31 | 32 | ##### ./app.js 33 | 34 | ```js 35 | const Router = require("coap-router"); 36 | const app = Router(); 37 | 38 | app.get("/", (req, res) => { 39 | res.end("Hello, world"); 40 | }); 41 | 42 | app.use("/thermometer", require("./routes/thermometer")); 43 | ``` 44 | 45 | 46 | ##### ./routes/thermometer.js 47 | ```js 48 | const Router = require("../../lib/router"); 49 | const router = Router(); 50 | const thermometer = require("../sensors/thermometer"); 51 | router.get("/", (req, res) => { 52 | // route to "/thermometer/" 53 | writeJSON(res, { 54 | temperature: thermometer.temperature, 55 | humidity: thermometer.humidity, 56 | timestamp: new Date().getTime() 57 | }); 58 | res.end(); 59 | }); 60 | 61 | router.get("/temperature", (req, res) => { 62 | // route to "/thermometer/temperature" 63 | writeJSON(res, { 64 | temperature: thermometer.temperature, 65 | timestamp: new Date().getTime() 66 | }); 67 | res.end(); 68 | }); 69 | ``` 70 | 71 | 72 | 73 | ## 3. How to Install 74 | ### 3.1 Basic Requirements 75 | 76 | * Node.js 6 or above 77 | * UDP compatible network connected 78 | 79 | ### 3.2 Installation 80 | 81 | ```sh 82 | $ npm install 83 | ``` 84 | 85 | 86 | 87 | ## 4. How to Test 88 | 89 | ### 4.1 Install CoAP Client 90 | 91 | Although there're dozens of CoAP clients available, one of the easiest ways is to install Node.js based client named `coap-cli`. 92 | 93 | ```sh 94 | $ npm install coap-cli -g 95 | $ coap 96 | ``` 97 | 98 | ### 4.2 Tests 99 | 100 | 1. Start example server. By default the CoAP server is listening at port 5683. 101 | 102 | ```sh 103 | $ npm start 104 | ``` 105 | 106 | ​ 107 | 108 | 2. Get literal resource. 109 | 110 | ```sh 111 | $ coap coap://localhost/ 112 | ``` 113 | 114 | ​ 115 | 116 | 3. Get JSON resource. 117 | 118 | ```sh 119 | $ coap coap://localhost/thermometer 120 | $ coap coap://localhost/thermometer/temperature 121 | $ coap coap://localhost/thermometer/humidity 122 | ``` 123 | 124 | 4. Get the latest resource immediately after it has been changed (observing mode). 125 | 126 | ```sh 127 | $ coap coap://localhost/thermometer/ -o 128 | ``` 129 | 130 | ​ 131 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | const Router = require("../lib/router"); 2 | const app = Router(); 3 | 4 | app.help = `URL: coap://hostname/ 5 | Usage: 6 | GET / - Display this help document. 7 | GET /thermometer - Get the current temperature together with humidity. 8 | GET /thermometer observe - Immediately get the above information when changed. 9 | GET /thermometer/temperature - Get the current temperature only. 10 | GET /thermometer/humidity - Get the current humidity only.` 11 | 12 | app.get("/", (req, res) => { 13 | res.end(app.help); 14 | }); 15 | 16 | app.use("/thermometer", require("./routes/thermometer")); 17 | 18 | module.exports = app; 19 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const coap = require("coap"); 2 | const app = require("./app"); 3 | const server = coap.createServer(app); 4 | server.listen(() => { 5 | console.log("The CoAP server is now running.\n" + app.help); 6 | }); 7 | -------------------------------------------------------------------------------- /example/routes/thermometer.js: -------------------------------------------------------------------------------- 1 | const Router = require("../../lib/router"); 2 | const router = Router(); 3 | 4 | const thermometer = require("../sensors/thermometer"); 5 | 6 | router.get("/", (req, res) => { 7 | writeJSON(res, { 8 | temperature: thermometer.temperature, 9 | humidity: thermometer.humidity, 10 | timestamp: new Date().getTime() 11 | }); 12 | res.end(); 13 | }); 14 | 15 | router.observe("/", (req, res) => { 16 | function _onupdate() 17 | { 18 | writeJSON(res, { 19 | temperature: thermometer.temperature, 20 | humidity: thermometer.humidity, 21 | timestamp: new Date().getTime() 22 | }); 23 | } 24 | 25 | console.log("Start observing..."); 26 | thermometer.on("update", _onupdate); 27 | res.on("finish", err => { 28 | thermometer.removeListener("update", _onupdate); 29 | console.log("End observing."); 30 | }); 31 | }); 32 | 33 | router.get("/temperature", (req, res) => { 34 | writeJSON(res, { 35 | temperature: thermometer.temperature, 36 | timestamp: new Date().getTime() 37 | }); 38 | res.end(); 39 | }); 40 | 41 | router.get("/humidity", (req, res) => { 42 | writeJSON(res, { 43 | humidity: thermometer.humidity, 44 | timestamp: new Date().getTime() 45 | }); 46 | res.end(); 47 | }); 48 | 49 | 50 | 51 | 52 | function writeJSON(res, json) 53 | { 54 | res.setOption("Content-Format", "application/json"); 55 | res.write(JSON.stringify(json)); 56 | } 57 | 58 | module.exports = router; 59 | -------------------------------------------------------------------------------- /example/sensors/sensor.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class Sensor extends EventEmitter 4 | { 5 | update() 6 | { 7 | 8 | } 9 | } 10 | 11 | module.exports = Sensor; 12 | -------------------------------------------------------------------------------- /example/sensors/thermometer.js: -------------------------------------------------------------------------------- 1 | const Sensor = require("./sensor"); 2 | 3 | class Themometer extends Sensor 4 | { 5 | constructor() 6 | { 7 | super(); 8 | this._temperature = 29.2; 9 | this._humidity = 12; 10 | setInterval(() => { 11 | this.update(); 12 | }, 1500); 13 | } 14 | 15 | get temperature() 16 | { 17 | return this._temperature; 18 | } 19 | 20 | get humidity() 21 | { 22 | return this._humidity; 23 | } 24 | 25 | update() 26 | { 27 | this._simulateChanging(); 28 | 29 | this.emit("update"); 30 | } 31 | 32 | _simulateChanging() 33 | { 34 | this._temperature += Math.random() - 0.5; 35 | if (this._temperature > 36) 36 | { 37 | this._temperature = 36; 38 | } 39 | this._temperature = Math.round(this._temperature * 10) / 10; 40 | 41 | this._humidity += Math.random() - 0.5; 42 | if (this._humidity > 20) 43 | { 44 | this._humidity = 20; 45 | } 46 | this._humidity = Math.round(this._humidity * 10) / 10; 47 | } 48 | } 49 | 50 | const themometer = new Themometer(); 51 | module.exports = themometer; 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("./example"); 2 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | const URL = require("url"); 2 | 3 | const METHODS = ["all", "get", "observe", "post", "put", "delete"]; 4 | 5 | module.exports = function router() 6 | { 7 | const handlers = []; 8 | const router = function(req, res, next) { 9 | const url = URL.parse(req.url); 10 | const path = url.path; 11 | let method = req.method.toLowerCase(); 12 | if (method === "get" && req.headers['Observe'] === 0) 13 | { 14 | method = "observe"; 15 | } 16 | 17 | const handlersCloned = handlers.slice(0); 18 | function findAndExecuteNextHandler() 19 | { 20 | let result = undefined; 21 | while (result === undefined) 22 | { 23 | const handler = handlersCloned.shift(); 24 | if (handler) 25 | { 26 | if (handler.method === "all" || handler.method === method) 27 | { 28 | let handlerPath = null; 29 | if (router.basePath) 30 | { 31 | if (handler.path === "/") 32 | { 33 | handlerPath = router.basePath; 34 | } 35 | else 36 | { 37 | handlerPath = router.basePath + handler.path; 38 | } 39 | } 40 | else 41 | { 42 | handlerPath = handler.path; 43 | } 44 | 45 | if (handler.callback.isRouter) 46 | { 47 | if (path.startsWith(handlerPath)) 48 | { 49 | req.handled = true; 50 | result = handler; 51 | break; 52 | } 53 | } 54 | if (path === handlerPath) 55 | { 56 | req.handled = true; 57 | result = handler; 58 | break; 59 | } 60 | } 61 | } 62 | else 63 | { 64 | break; 65 | } 66 | } 67 | 68 | if (result) 69 | { 70 | result.callback(req, res, findAndExecuteNextHandler); 71 | } 72 | else 73 | { 74 | if (typeof(next) === "function") 75 | { 76 | next(); 77 | } 78 | else 79 | { 80 | if (!req.handled) 81 | { 82 | // 404 - Not found 83 | res.code = 404; 84 | res.end(); 85 | } 86 | } 87 | } 88 | } 89 | 90 | findAndExecuteNextHandler(); 91 | }; 92 | router.isRouter = true; 93 | 94 | 95 | 96 | router.method = function(method, path, callback) 97 | { 98 | method = method.toLowerCase(); 99 | if (METHODS.indexOf(method) === -1) 100 | { 101 | throw new Error(`In CoAP protocol, method only accepts GET, POST, PUT and DELETE.`); 102 | } 103 | if (typeof(callback) !== "function") 104 | { 105 | throw new Error("Callback must be a function."); 106 | } 107 | if (path !== "" && !path.startsWith("/")) 108 | { 109 | throw new Error("Path must starts with '/'."); 110 | } 111 | handlers.push({ 112 | method, 113 | path, 114 | callback 115 | }); 116 | return this; 117 | }; 118 | METHODS.forEach(method => { 119 | router[method] = function(path, callback) 120 | { 121 | return router.method(method, path, callback); 122 | }; 123 | }); 124 | 125 | 126 | router.use = function(path, subRouter) 127 | { 128 | subRouter.basePath = (router.basePath ? router.basePath : "") + path; 129 | this.all(path, subRouter); 130 | }; 131 | 132 | return router; 133 | }; 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coap-router", 3 | "version": "1.0.0", 4 | "description": "A quick demo on how to leverage web router to build CoAP server.", 5 | "main": "lib/router.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/MagicCube/coap-router.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/MagicCube/coap-router/issues" 17 | }, 18 | "homepage": "https://github.com/MagicCube/coap-router#readme", 19 | "dependencies": { 20 | "coap": "^0.17.0" 21 | } 22 | } 23 | --------------------------------------------------------------------------------