From d1993f835a3aa6d6ba0a632be0cc721f7f45a052 Mon Sep 17 00:00:00 2001 From: techninja Date: Mon, 21 Nov 2016 22:43:13 -0800 Subject: [PATCH 01/16] Initial working effort towards batch run API with updated postman json --- API.md | 75 ++- cncserver_api.postman.json | 931 ++++++++++++++++++++++--------------- package.json | 4 +- src/cncserver.api.js | 259 ++++++++++- 4 files changed, 891 insertions(+), 378 deletions(-) diff --git a/API.md b/API.md index 18971ca..10c2fd5 100755 --- a/API.md +++ b/API.md @@ -624,7 +624,80 @@ Content-Type: application/json; charset=UTF-8 called. This might have to change though... -## 6. Socket.IO & real-time event data streaming +## 6. Batch +Overhead on individual ReSTful requests isn't so bad, but add that up over 100k+ +instructions/commands, and that's minutes wasted waiting for all the data just +to get where it needs to go. If you don't need to know what's happening after +every request, then _this endpoint is for you!_ + +This final resource is meant as a catch all, allowing for ordered lists of +ReSTful endpoint commands to be batch uploaded as either literal JSON data, or +JSON file & path that the server has access to (either local/or remote HTTP). + +### POST /v1/batch +Post the batch data into the queue. Returns when processing is complete, or +there's an error. + +#### Request: Direct batch data. +```javascript +POST /v1/batch +Content-Type: application/json; charset=UTF-8 + +[ + {"DELETE v1/pen": {}}, // Park. + {"PUT v1/pen": {"x": 50, "y": 50}}, // Move to 50, 50. + {"PUT v1/pen": {"x": 10, "y": 50}}, // Move to 10, 50. + {"DELETE v1/pen": {}} // Park. +] +``` + +#### Request: Batch file (local) +```javascript +POST /v1/batch +Content-Type: application/json; charset=UTF-8 + +{ + // JSON file to read from (make sure the server has permission to read the file!) + "file": "/tmp/data/cncsererver-batch-example.json" +} +``` + +#### Request: Batch file (remote) +```javascript +POST /v1/batch +Content-Type: application/json; charset=UTF-8 + +{ + // Again, make sure the server has HTTP & file access to the file. + "file": "http://example.com/batch-data.json" +} +``` + +### Response +```javascript +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +{ + // If successful, the number of valid commands will be listed. + // If you park while already parked, or any commands fail, these will count + // against the final reported success rate. + "status" : "Batch parsed and queued 3/4 commands" +} +``` + +##### Usage Notes + * Command items are parsed and run semi-asynchronously, with each being run as + if an individual ReSTful request had been called. + * Every command is run with `ignoreTimeout` set, so no need to include it in + batch command data. + * Detailed error catching is mostly non-existent, make sure your data is well + formed and tested without batching before moving to batch submission. + * Return data reporting is exceedingly minimal, so make sure you've + decoupled your client from return data (just tie into stream updates). + + +## 7. Socket.IO & real-time event data streaming The API has served the project incredibly well, but it was lacking in one important aspect: a remote client can receive no updates without asking first, no events or data without polling or some other kludge. That is now all diff --git a/cncserver_api.postman.json b/cncserver_api.postman.json index 5111ed5..7f74074 100755 --- a/cncserver_api.postman.json +++ b/cncserver_api.postman.json @@ -1,365 +1,570 @@ { - "id": "6ed2f056-f044-da06-28aa-6025b812dab8", - "name": "CNC Server API", - "timestamp": 1365738583038, - "order": [ - "cfd1b0a7-6b78-5120-fe2e-fcfba5c3b09a", - "72b33574-693f-cf8d-0aef-d14b4ba06162", - "e85829d1-c897-636a-687f-41c659f12ee8", - "89941fd0-9ccd-1b1a-3b81-13112ae7c917", - "d7d4c368-2f3b-c666-ff86-3d91b4aca87d", - "77c30fc7-8884-e099-f535-d354429b3ad5", - "cb9638bb-94a1-9fd9-c910-8eb97a8f95e9", - "12044a31-0d86-f367-26d2-96d7a0d918d6", - "7065aa8b-188f-cd83-44f6-48da0db125da", - "5c97876c-a835-a9c8-a58d-d2524e7ada6f", - "8bd9b3dd-fb3a-bf4d-0e79-5835ad987b86", - "018053b5-0a68-4027-0c8a-182f7206e0bb", - "1efab236-e56a-1268-ca00-2e8938a2054a", - "199a3586-8ac1-bae8-0c50-2671fd17a34a", - "7c10785d-134d-1a4a-6f94-c1308915a3b2", - "b131cd64-cac4-08ee-7908-599de452765f", - "899a0cc6-8e2c-1952-482e-8947610fb873", - "746528c7-47f2-c0ff-09ff-17d6f9ceec03", - "b8dbd92e-91d4-9a88-3bc0-38b60dcb730a", - "e241fb57-9c0c-baa0-4222-b0eb487bc4ef", - "b6825285-5a48-444a-7f36-408013f86af4", - "4ce5e190-7d00-9be4-703e-e4f5d3201ef6", - "d72fb956-4f95-ae22-7f12-545f746d6fd3" - ], - "requests": [ - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "018053b5-0a68-4027-0c8a-182f7206e0bb", - "name": "Tools: Enumerate", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/tools", - "method": "GET", - "headers": "", - "data": [], - "dataMode": "params", - "timestamp": 0, - "version": 2, - "time": 1375823770402 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "12044a31-0d86-f367-26d2-96d7a0d918d6", - "name": "Pen: Go to 100%,100%", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n\"x\": 100,\n\"y\": 100\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824095011 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "199a3586-8ac1-bae8-0c50-2671fd17a34a", - "name": "Motors: Reset offset", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/motors", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"reset\": 1\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824541916 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "1efab236-e56a-1268-ca00-2e8938a2054a", - "name": "Motors: Unlock", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/motors", - "method": "DELETE", - "headers": "", - "data": "", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824504090 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "4ce5e190-7d00-9be4-703e-e4f5d3201ef6", - "name": "Buffer: Pause operations", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"paused\": true\n}", - "dataMode": "raw", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "5c97876c-a835-a9c8-a58d-d2524e7ada6f", - "name": "Pen: Park", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "DELETE", - "headers": "", - "data": [], - "dataMode": "params", - "timestamp": 0, - "version": 2, - "time": 1375823758505 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "7065aa8b-188f-cd83-44f6-48da0db125da", - "name": "Pen: Reset distance counter", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"resetCounter\": 1\n}", - "dataMode": "raw", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "72b33574-693f-cf8d-0aef-d14b4ba06162", - "name": "Pen: Pen Down", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"state\": 1\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824107577 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "746528c7-47f2-c0ff-09ff-17d6f9ceec03", - "name": "Settings: Enable Debug Mode", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/global", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"debug\": true\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1384933117223 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "77c30fc7-8884-e099-f535-d354429b3ad5", - "name": "Pen: Go to 0,0", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n\"x\": 0,\n\"y\": 0\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824101642 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "7c10785d-134d-1a4a-6f94-c1308915a3b2", - "name": "Settings: Get All Settings", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings", - "method": "GET", - "headers": "", - "data": "", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1384932946112 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "89941fd0-9ccd-1b1a-3b81-13112ae7c917", - "name": "Pen: Up Halfway", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"state\": 0.50\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1380610903028 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "899a0cc6-8e2c-1952-482e-8947610fb873", - "name": "Settings: Change Servo Duration", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/bot", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"servo:duration\": 400\n}", - "dataMode": "raw", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "8bd9b3dd-fb3a-bf4d-0e79-5835ad987b86", - "name": "Tools: Change", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/tools/water0dip", - "method": "PUT", - "headers": "", - "data": [], - "dataMode": "params", - "timestamp": 0, - "version": 2, - "time": 1375823762939 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "b131cd64-cac4-08ee-7908-599de452765f", - "name": "Settings: Get Global Settings", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/global", - "method": "GET", - "headers": "", - "data": "", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1384932986222 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "b6825285-5a48-444a-7f36-408013f86af4", - "name": "Buffer: Clear buffer", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", - "method": "DELETE", - "headers": "", - "data": [], - "dataMode": "params", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "b8dbd92e-91d4-9a88-3bc0-38b60dcb730a", - "name": "Settings: Add height preset", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/bot", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"servo:presets:foo\": 42\n}", - "dataMode": "raw", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "cb9638bb-94a1-9fd9-c910-8eb97a8f95e9", - "name": "Pen: Go to center (50%, 50%)", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n\n\"x\": 50,\n\"y\": 50\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824098563 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "cfd1b0a7-6b78-5120-fe2e-fcfba5c3b09a", - "name": "Pen: Get Data", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "GET", - "headers": "", - "data": "{\n \"state\": 0\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375823851308 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "d72fb956-4f95-ae22-7f12-545f746d6fd3", - "name": "Buffer: Resume operations", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"paused\": false\n}", - "dataMode": "raw", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "d7d4c368-2f3b-c666-ff86-3d91b4aca87d", - "name": "Pen: Down to Wash", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"state\": \"wash\"\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1380610973747 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "e241fb57-9c0c-baa0-4222-b0eb487bc4ef", - "name": "Buffer: Get status", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", - "method": "GET", - "headers": "", - "data": [ - { - "key": "name", - "value": "api-tauntonstore", - "type": "text" - }, - { - "key": "cleartext", - "value": "T@unton1", - "type": "text" - } - ], - "dataMode": "urlencoded", - "timestamp": 0, - "responses": [], - "version": 2 - }, - { - "collectionId": "6ed2f056-f044-da06-28aa-6025b812dab8", - "id": "e85829d1-c897-636a-687f-41c659f12ee8", - "name": "Pen: Pen Up", - "description": "", - "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", - "method": "PUT", - "headers": "Content-Type: application/json; charset=UTF-8\n", - "data": "{\n \"state\": 0\n}", - "dataMode": "raw", - "timestamp": 0, - "version": 2, - "time": 1375824104522 - } - ] + "id": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "name": "CNC Server API", + "description": "", + "order": [], + "folders": [ + { + "id": "0bde2516-84a7-05a8-3b22-4a54a75ae331", + "name": "Batch", + "description": "", + "order": [ + "fef0f1ad-cde7-90cf-3b32-f83579e57b84", + "9f2dbeec-84c6-1c40-5dce-16d1cb9b2ee9", + "30b86d4a-cd8b-8e28-9f0b-9703654e99f2" + ], + "owner": 0 + }, + { + "id": "4a2986e5-9a55-35da-5e41-25c7d580bf39", + "name": "Buffer", + "description": "", + "order": [ + "323d7179-676f-9e79-18ce-718f97c34414", + "d6ec1182-6f1f-0d07-4244-d1972ffafa54", + "9b58295e-595f-05a2-7aac-d2272e7139ba", + "6a418d59-6db5-00d9-3bb1-74566562b281" + ], + "owner": 0 + }, + { + "id": "389b21df-2d74-c667-590c-aec658d7f8eb", + "name": "Motors", + "description": "", + "order": [ + "94cab647-676c-d57b-0617-7c07adfe3f86", + "f15fd6c8-f5e7-bf6c-4551-79a818dd5e4a" + ], + "owner": 0 + }, + { + "id": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "name": "Pen", + "description": "", + "order": [ + "f4e48842-96f1-0a91-1e58-2df842a5c2d7", + "2d30b1ca-c6aa-d178-85bb-3be00d58a7cf", + "738eae88-349d-c347-83a2-a5bbd01b36bc", + "23937438-f6c0-7511-83e8-95fe06ca506f", + "027bbbb4-41eb-dba3-8dc7-dba72a628411", + "a68a8cf8-ca52-56a6-cb22-34a179c99f0b", + "e9e8a40e-31e7-4b33-12cb-9f06ad095cb0", + "8de0efb6-1ff0-8a29-3d51-505dd669fc2e", + "7be84213-f4f7-f355-7d27-d47ada80e86f", + "8de5264d-25d4-759d-a915-79e75e67e2c6" + ], + "owner": 0 + }, + { + "id": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "name": "Settings", + "description": "", + "order": [ + "c2168d9b-23c1-89ee-7366-8acc5205d2f6", + "d21b1ec9-278e-bb1b-4d1a-b9c9509b7bc8", + "72ce0933-de7a-35e8-1ff5-ed5e0507153f", + "81ff847c-4a98-e954-8b3c-88328c702a70", + "844a8811-f3d0-d45f-8543-5a68adb9cb40" + ], + "owner": 0 + }, + { + "id": "e432b157-b9ed-25d9-107d-25b9486de4d7", + "name": "Tools", + "description": "", + "order": [ + "5ba9aee4-8848-622f-9992-a0456f4c4764", + "571ae8a8-6d39-4b3b-61d4-8083cf106a21" + ], + "owner": 0 + } + ], + "timestamp": 1365738583038, + "owner": 0, + "public": false, + "requests": [ + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "027bbbb4-41eb-dba3-8dc7-dba72a628411", + "name": "Pen: Down to Wash", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1380610973747, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"state\": \"wash\"\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "23937438-f6c0-7511-83e8-95fe06ca506f", + "name": "Pen: Pen Up", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824104522, + "preRequestScript": "", + "tests": "", + "isLastRequest": true, + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"state\": 0\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "2d30b1ca-c6aa-d178-85bb-3be00d58a7cf", + "name": "Pen: Get Data", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "GET", + "headers": "", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375823851308, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"state\": 0\n}" + }, + { + "id": "30b86d4a-cd8b-8e28-9f0b-9703654e99f2", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/batch", + "preRequestScript": "", + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1479791132017, + "name": "Batch: Submit remote batch file", + "description": "", + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "responses": [], + "rawModeData": "{\n \"file\": \"http://example.com/batch.json\"\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "323d7179-676f-9e79-18ce-718f97c34414", + "name": "Buffer: Get status", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", + "method": "GET", + "headers": "", + "data": [ + { + "key": "name", + "value": "api-tauntonstore", + "type": "text" + }, + { + "key": "cleartext", + "value": "T@unton1", + "type": "text" + } + ], + "dataMode": "urlencoded", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "4a2986e5-9a55-35da-5e41-25c7d580bf39" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "571ae8a8-6d39-4b3b-61d4-8083cf106a21", + "name": "Tools: Enumerate", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/tools", + "method": "GET", + "headers": "", + "data": [], + "dataMode": "params", + "timestamp": 0, + "version": 2, + "time": 1375823770402, + "preRequestScript": "", + "tests": "", + "folder": "e432b157-b9ed-25d9-107d-25b9486de4d7" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "5ba9aee4-8848-622f-9992-a0456f4c4764", + "name": "Tools: Change", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/tools/water0dip", + "method": "PUT", + "headers": "", + "data": [], + "dataMode": "params", + "timestamp": 0, + "version": 2, + "time": 1375823762939, + "preRequestScript": "", + "tests": "", + "folder": "e432b157-b9ed-25d9-107d-25b9486de4d7" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "6a418d59-6db5-00d9-3bb1-74566562b281", + "name": "Buffer: Resume operations", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "4a2986e5-9a55-35da-5e41-25c7d580bf39", + "rawModeData": "{\n \"paused\": false\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "72ce0933-de7a-35e8-1ff5-ed5e0507153f", + "name": "Settings: Change Servo Duration", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/bot", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "rawModeData": "{\n \"servo:duration\": 400\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "738eae88-349d-c347-83a2-a5bbd01b36bc", + "name": "Pen: Up Halfway", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1380610903028, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"state\": 0.50\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "7be84213-f4f7-f355-7d27-d47ada80e86f", + "name": "Pen: Reset distance counter", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"resetCounter\": 1\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "81ff847c-4a98-e954-8b3c-88328c702a70", + "name": "Settings: Enable Debug Mode", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/global", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1384933117223, + "preRequestScript": "", + "tests": "", + "folder": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "rawModeData": "{\n \"debug\": true\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "844a8811-f3d0-d45f-8543-5a68adb9cb40", + "name": "Settings: Add height preset", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/bot", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "rawModeData": "{\n \"servo:presets:foo\": 42\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "8de0efb6-1ff0-8a29-3d51-505dd669fc2e", + "name": "Pen: Go to 100%,100%", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824095011, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n\"x\": 100,\n\"y\": 100\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "8de5264d-25d4-759d-a915-79e75e67e2c6", + "name": "Pen: Park", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "DELETE", + "headers": "", + "data": [], + "dataMode": "params", + "timestamp": 0, + "version": 2, + "time": 1375823758505, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "94cab647-676c-d57b-0617-7c07adfe3f86", + "name": "Motors: Reset offset", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/motors", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824541916, + "preRequestScript": "", + "tests": "", + "folder": "389b21df-2d74-c667-590c-aec658d7f8eb", + "rawModeData": "{\n \"reset\": 1\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "9b58295e-595f-05a2-7aac-d2272e7139ba", + "name": "Buffer: Pause operations", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "4a2986e5-9a55-35da-5e41-25c7d580bf39", + "rawModeData": "{\n \"paused\": true\n}" + }, + { + "id": "9f2dbeec-84c6-1c40-5dce-16d1cb9b2ee9", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/batch", + "preRequestScript": "", + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1479791139664, + "name": "Batch: Submit local batch file", + "description": "", + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "responses": [], + "rawModeData": "{\n \"file\": \"batch.json\"\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "a68a8cf8-ca52-56a6-cb22-34a179c99f0b", + "name": "Pen: Go to 0,0", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824101642, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n\"x\": 0,\n\"y\": 0\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "c2168d9b-23c1-89ee-7366-8acc5205d2f6", + "name": "Settings: Get All Settings", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings", + "method": "GET", + "headers": "", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1384932946112, + "preRequestScript": "", + "tests": "", + "folder": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "rawModeData": "" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "d21b1ec9-278e-bb1b-4d1a-b9c9509b7bc8", + "name": "Settings: Get Global Settings", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/settings/global", + "method": "GET", + "headers": "", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1384932986222, + "preRequestScript": "", + "tests": "", + "folder": "cf6829a8-2d31-c13c-1d17-3e42ac623722", + "rawModeData": "" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "d6ec1182-6f1f-0d07-4244-d1972ffafa54", + "name": "Buffer: Clear buffer", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/buffer", + "method": "DELETE", + "headers": "", + "data": [], + "dataMode": "params", + "timestamp": 0, + "responses": [], + "version": 2, + "preRequestScript": "", + "tests": "", + "folder": "4a2986e5-9a55-35da-5e41-25c7d580bf39" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "e9e8a40e-31e7-4b33-12cb-9f06ad095cb0", + "name": "Pen: Go to center (50%, 50%)", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824098563, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n\n\"x\": 50,\n\"y\": 50\n}" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "f15fd6c8-f5e7-bf6c-4551-79a818dd5e4a", + "name": "Motors: Unlock", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/motors", + "method": "DELETE", + "headers": "", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824504090, + "preRequestScript": "", + "tests": "", + "folder": "389b21df-2d74-c667-590c-aec658d7f8eb", + "rawModeData": "" + }, + { + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "id": "f4e48842-96f1-0a91-1e58-2df842a5c2d7", + "name": "Pen: Pen Down", + "description": "", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/pen", + "method": "PUT", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "data": [], + "dataMode": "raw", + "timestamp": 0, + "version": 2, + "time": 1375824107577, + "preRequestScript": "", + "tests": "", + "folder": "a16eb87f-4484-46dc-f1f7-12bc1e2750fc", + "rawModeData": "{\n \"state\": 1\n}" + }, + { + "id": "fef0f1ad-cde7-90cf-3b32-f83579e57b84", + "headers": "Content-Type: application/json; charset=UTF-8\n", + "url": "http://{{cncserver-host}}:{{cncserver-port}}/v1/batch", + "pathVariables": {}, + "preRequestScript": "", + "method": "POST", + "collectionId": "12ad3ed7-7124-c4a7-b839-6b8da3f022b5", + "data": [], + "dataMode": "raw", + "name": "Batch: Submit batch data directly", + "description": "", + "descriptionFormat": "html", + "time": 1479792255823, + "version": 2, + "responses": [], + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "folder": "0bde2516-84a7-05a8-3b22-4a54a75ae331", + "rawModeData": "[\n {\"DELETE v1/pen\": {}},\n {\"PUT v1/tools/water0\": {}},\n {\"PUT v1/tools/water2\": {}},\n {\"PUT v1/tools/color2\": {}},\n {\"PUT v1/pen\": {\"x\": 10, \"y\": 10}},\n {\"PUT v1/pen\": {\"state\": 1}},\n {\"PUT v1/pen\": {\"x\": 50, \"y\": 10}},\n {\"PUT v1/pen\": {\"x\": 50, \"y\": 50}},\n {\"PUT v1/pen\": {\"x\": 10, \"y\": 50}},\n {\"PUT v1/pen\": {\"x\": 10, \"y\": 10}},\n {\"DELETE v1/pen\": {}}\n]" + } + ] } \ No newline at end of file diff --git a/package.json b/package.json index 11f338d..d1a2e96 100755 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "ini": "1.3.4", "nconf": "0.8.4", "node-ipc": "8.9.2", + "path-to-regexp": "^1.7.0", "request": "*", "serialport": "4.0.1", - "socket.io": "^1.4.8" + "socket.io": "^1.4.8", + "underscore": "^1.8.3" }, "devDependencies": { "mocha": "*", diff --git a/src/cncserver.api.js b/src/cncserver.api.js index 83fd5d0..29345db 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -6,10 +6,24 @@ * */ +var fs = require('fs'); +var request = require('request'); +var _ = require('underscore'); +var querystring = require('querystring'); +var pathToRegexp = require('path-to-regexp'); + module.exports = function(cncserver) { // CNC Server API ============================================================ + // Enpoints are created and assigned via a server path to respond to, and + // and callback function that manages handles the request and response. + // We hold all of these in cncserver.apiHandlers to be able to call them + // directly from the batch API endpoint. These are actually only turned into + // endpoints at the end via createServerEndpoint(). + cncserver.apiHandlers = {}; + // Return/Set CNCServer Configuration ======================================== - cncserver.createServerEndpoint("/v1/settings", function(req){ + //cncserver.createServerEndpoint("/v1/settings", ); + cncserver.apiHandlers['/v1/settings'] = function settingsGet(req) { if (req.route.method === 'get') { // Get list of tools return {code: 200, body: { global: '/v1/settings/global', @@ -18,9 +32,9 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; - cncserver.createServerEndpoint("/v1/settings/:type", function(req){ + cncserver.apiHandlers['/v1/settings/:type'] = function settingsMain(req){ // Sanity check type var setType = req.params.type; if (setType !== 'global' && setType !== 'bot'){ @@ -57,10 +71,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Return/Set PEN state API ================================================= - cncserver.createServerEndpoint("/v1/pen", function(req, res){ + cncserver.apiHandlers['/v1/pen'] = function penMain(req, res){ if (req.route.method === 'put') { // SET/UPDATE pen status cncserver.control.setPen(req.body, function(stat){ @@ -123,10 +137,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Return/Set Motor state API ================================================ - cncserver.createServerEndpoint("/v1/motors", function(req){ + cncserver.apiHandlers['/v1/motors'] = function motorsMain(req){ // Disable/unlock motors if (req.route.method === 'delete') { if (req.body.skipBuffer) { @@ -177,10 +191,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Command buffer API ======================================================== - cncserver.createServerEndpoint("/v1/buffer", function(req, res){ + cncserver.apiHandlers['/v1/buffer'] = function bufferMain(req, res){ var buffer = cncserver.buffer; if (req.route.method === 'get' || req.route.method === 'put') { // Pause/resume (normalize input) @@ -308,10 +322,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Get/Change Tool API ======================================================= - cncserver.createServerEndpoint("/v1/tools", function(req){ + cncserver.apiHandlers['/v1/tools'] = function toolsGet(req){ if (req.route.method === 'get') { // Get list of tools return {code: 200, body:{ tools: Object.keys(cncserver.botConf.get('tools')) @@ -319,9 +333,9 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; - cncserver.createServerEndpoint("/v1/tools/:tool", function(req, res){ + cncserver.apiHandlers['/v1/tools/:tool'] = function toolsMain(req, res){ var toolName = req.params.tool; // TODO: Support other tool methods... (needs API design!) if (req.route.method === 'put') { // Set Tool @@ -344,5 +358,224 @@ module.exports = function(cncserver) { } else { return false; } + }; + + // Bind all the apiHandlers into endpoints =================================== + _.each(cncserver.apiHandlers, function(callback, path) { + cncserver.createServerEndpoint(path, callback); + }); + + // Batch Command API ========================================================= + cncserver.createServerEndpoint("/v1/batch", function(req, res){ + if (req.route.method === 'post') { // Create a new batch set. + if (req.body.file) { + var file = req.body.file; + if (file.substr(0, 4) === 'http') { + // Internet file. + request.get(file, function (error, response, body) { + // Attempt to parse/process data. + if (body) { + try { + var commands = JSON.parse(body); + processBatchData(commands, function(count) { + res.status(200).send(JSON.stringify({ + status: 'Batch parsed and queued ' + count + '/' + + commands.length + ' commands' + })); + }); + } catch(err) { + error = err; + } + } + + // Catch response for errors (on parsing or reading). + if (error) { + res.status(400).send(JSON.stringify({ + status: 'Error reading file "' + file + '"', + remoteHTTPCode: response.statusCode, + data: error + })); + } + }); + } else { + // Local file. + fs.readFile(file, function(error, data) { + // Attempt to read the data. + if (data) { + try { + var commands = JSON.parse(data.toString()); + processBatchData(commands, function(count) { + res.status(200).send(JSON.stringify({ + status: 'Batch parsed and queued ' + count + '/' + + commands.length + ' commands' + })); + }); + } catch (err) { + error = err; + } + } + + // Catch response for errors (on parsing or reading). + if (error) { + res.status(400).send(JSON.stringify({ + status: 'Error reading file "' + file + '"', + data: error + })); + } + }); + } + } else { + // Raw command data (not from a file); + try { + processBatchData(req.body, function(count) { + res.status(200).send(JSON.stringify({ + status: 'Batch parsed and queued ' + count + '/' + + req.body.length + ' commands' + })); + }); + } catch (err) { + res.status(400).send(JSON.stringify({ + status: 'Error reading/processing batch data', + data: err + })); + } + } + + return true; // Tell endpoint wrapper we'll handle the response + } else { + return false; + } }); + + /** + * Process a flat array of semi-abstracted commands into the queue. + * + * @param {array} commands + * Flat array of command objects in the following format: + * {"[POST|PUT|DELETE] /v1/[ENDPOINT]": {data: 'for the endpoint'}} + * @param {function} callback + * Callback function when command processing is complete. + * @param {int} index + * Array index of commands to process. Ignore/Pass as undefined to init. + * Function calls itself via callbacks to ensure delayed api handlers remain + * queued in order while async. + * @param {int} goodCount + * Running tally of successful commands, to be returned to callback once + * complete. + */ + function processBatchData(commands, callback, index, goodCount) { + // Initiate for the first loop run. + if (typeof index === 'undefined') { + index = 0; + goodCount = 0; + } + + var command = commands[index]; + if (typeof command !== 'undefined') { + var key = Object.keys(command)[0]; + var data = command[key]; + var method = key.split(' ')[0]; + var path = key.split(' ')[1].split('?')[0]; + if (path[0] !== '/') path = '/' + path; + + var query = path.split('?')[1]; // Query params. + var params = {}; // URL Params. + + // Batch runs are send and forget, force ignoreTimeout. + data.ignoreTimeout = '1'; + + var req = getDummyObject('request'); + var res = getDummyObject('response'); + var handlerKey = ''; + + // Attempt to match the path to a requstHandler by express path match. + _.each(Object.keys(cncserver.apiHandlers), function(pattern) { + var keys = []; + var match = pathToRegexp(pattern, keys).exec(path); + if (match) { + handlerKey = pattern; + + // If there's keyed url params, inject them. + if (keys.length) { + _.each(keys, function(p, index) { + params[p.name] = match[index + 1]; + }); + } + } + }); + + // Fill out request details: + req.route.method = method.toLowerCase(); + req.route.path = path; + req.query = query ? querystring.parse(query) : {}; + req.params = params; + req.body = data; + + // Call the api handler (send and forget via batch!) + if (cncserver.apiHandlers[handlerKey]) { + res.status = function(code) { + return {send: function(data) { + console.log('#' + index, 'Delayed:', handlerKey, code, data); + if (code.toString()[0] === '2') goodCount++; + processBatchData(commands, callback, index + 1, goodCount); + }}; + }; + + // Naively check to see if the request was successful. + // Technically if there's a wait for return (=== true), we could only + // see it in the .status() return callback. + var response = cncserver.apiHandlers[handlerKey](req, res); + if (response !== true) { + console.log('#' + index, 'Immediate:', handlerKey, response); + if (response !== false) goodCount++; + processBatchData(commands, callback, index + 1, goodCount); + } + } else { + // Unhandled path, not a valid API handler available. Move on. + processBatchData(commands, callback, index + 1, goodCount); + } + } else { + // We're done! + callback(goodCount); + } + } + + /** + * Return a dummy 'request' or 'response' object for faking express requests. + * + * @param {string} type + * Either 'request' or 'response'. + * + * @return {object} + * The dummy object with minimum required parts for the handlers to use the + * same code as the express handler arguments. + */ + function getDummyObject(type) { + var out = {}; + + switch (type) { + case 'request': + out = { + route: { + method: '', + path: '' + }, + query: {}, + params: {}, + body: {}, + }; + break; + + case 'response': + out = { + status: function() { + return {send: function() {}}; + } + }; + break; + + } + + return out; + } }; From bc1e2e70754611ea57ee7d414c0809411888a65a Mon Sep 17 00:00:00 2001 From: techninja Date: Fri, 25 Nov 2016 21:29:09 -0800 Subject: [PATCH 02/16] Move odd debug output to debug only. --- src/cncserver.api.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cncserver.api.js b/src/cncserver.api.js index 29345db..0b25a37 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -515,7 +515,10 @@ module.exports = function(cncserver) { if (cncserver.apiHandlers[handlerKey]) { res.status = function(code) { return {send: function(data) { - console.log('#' + index, 'Delayed:', handlerKey, code, data); + if (cncserver.gConf.get('debug')) { + console.log('#' + index, 'Batch Delay:', handlerKey, code, data); + } + if (code.toString()[0] === '2') goodCount++; processBatchData(commands, callback, index + 1, goodCount); }}; @@ -526,7 +529,10 @@ module.exports = function(cncserver) { // see it in the .status() return callback. var response = cncserver.apiHandlers[handlerKey](req, res); if (response !== true) { - console.log('#' + index, 'Immediate:', handlerKey, response); + if (cncserver.gConf.get('debug')) { + console.log('#' + index, 'Batch Immediate:', handlerKey, response); + } + if (response !== false) goodCount++; processBatchData(commands, callback, index + 1, goodCount); } From 1040e3de6b26e9a99ffbf99432d63b1bd79d0793 Mon Sep 17 00:00:00 2001 From: techninja Date: Fri, 25 Nov 2016 23:43:59 -0800 Subject: [PATCH 03/16] Add js client wrapper support for batch --- example/cncserver.client.api.js | 150 +++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index 89b16bf..db7d1e7 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -440,7 +440,146 @@ cncserver.api = { } ); } - } + }, + + // Batch API for lowering command send overhead. + batch: { + skipSend: false, + saveFile: '', + firstCommand: true, + data: [], + + /** + * Once enabled, all commands sent through this wrapper will be saved for + * batch sending instead of actually being sent, until end is run. + * + * @return {[type]} [description] + */ + start: function(options) { + options = options || {}; + cncserver.api.batch.endClear(); + cncserver.api.batch.skipSend = true; + + // If node and a file path send, try to save it + // TODO: Add in some file access checks to keep this from dying. + if (isNode && options.saveToFile) { + cncserver.api.batch.fs = require('fs'); + cncserver.api.batch.saveFile = options.saveToFile; + cncserver.api.batch.fs.writeFileSync(options.saveToFile, '['); + } + }, + + /** + * Write final file ending to the JSON file storage. + */ + finishFile: function() { + if (cncserver.api.batch.saveFile) { + cncserver.api.batch.fs.appendFileSync( + cncserver.api.batch.saveFile, + ']' + ); + } + }, + + /** + * Add an entry into the batch data storage. + * + * @param {string} key + * The key holding the method and path + * @param {object} data + * The data arguments being sent to modify the command. + */ + addEntry: function(key, data) { + // Unset ignore timeout as it's just dead weight with batch. + if (data.ignoreTimeout) delete data.ignoreTimeout; + + var entry = {}; + entry[key] = data; + + // Store the data. + if (cncserver.api.batch.saveFile) { + var line = JSON.stringify(entry); + + // Add a comma if the command isn't first. + if (!cncserver.api.batch.firstCommand) { + line = ',' + line; + } + cncserver.api.batch.fs.appendFileSync( + cncserver.api.batch.saveFile, + line + ); + } else { + cncserver.api.batch.data.push(entry); + } + cncserver.api.batch.firstCommand = false; + }, + + /** + * End the batch and return the data saved. + * + * @return {object|string} + * Full data array if local, otherwise the file path if saved directly. + */ + endReturn: function() { + var dump = []; + + if (cncserver.api.batch.saveFile) { + cncserver.api.batch.finishFile(); + dump = cncserver.api.batch.saveFile; + } else { + dump = cncserver.api.batch.data; + } + + cncserver.api.batch.endClear(); + return dump; + }, + + /** + * End the batch and send the data immediately. + * + * @param {Function} callback + * Function called when sending is complete. + * @param {object} options + * Keyed options object, currently supports: + * * fileOverride: path/URL that the server should have read access to + * as opposed to the one that the node client has write access to. + */ + endSend: function(callback, options) { + var dump; + options = options || {}; + + // File or raw data? + if (cncserver.api.batch.saveFile) { + cncserver.api.batch.finishFile(); + + // Allow client to specify the end send file path differently. + if (options.fileOverride) { + dump = {file: options.fileOverride}; + } else { + dump = {file: cncserver.api.batch.saveFile}; + } + } else { + // Send the actual data directly. + dump = cncserver.api.batch.data; + } + + cncserver.api.batch.endClear(); + _post('batch', { + data: dump, + success: callback, + }); + }, + + /** + * Reset all batch state to default. + */ + endClear: function() { + cncserver.api.batch.firstCommand = true; + cncserver.api.batch.data = []; + cncserver.api.batch.saveFile = ''; + cncserver.api.batch.skipSend = false; + } + }, }; function _get(path, options) { @@ -484,6 +623,15 @@ function _request(method, path, options) { } var uri = srv.protocol + '://' + srv.domain + ':' + srv.port + srvPath; + + // If we're batching commands.. we don't actually send them, we store them! + if (cncserver.api.batch.skipSend) { + cncserver.api.batch.addEntry(method + ' ' + srvPath, options.data); + options.success(); + return; + } + + // Send the request. if (!isNode) { $.ajax({ url: uri, From f9a093745bac2ac50ea63f856c75663c0d6615c8 Mon Sep 17 00:00:00 2001 From: techninja Date: Sat, 26 Nov 2016 14:16:51 -0800 Subject: [PATCH 04/16] Removing broken streaming option --- example/cncserver.client.api.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index db7d1e7..25573ae 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -59,25 +59,6 @@ cncserver.api = { if (typeof options !== 'object') options = {}; options.state = value; - // If we're on node and we have a socket, shortcut via WebSockets. - // TODO: FIX this as causes socket.io call stack overflows - if (isNode && cncserver.global.socket && false) { - var data = {state: value, returnData: !!callback}; - cncserver.global.socket.emit('height', data); - if (callback) { - var catchMove = function(d){ - callback(d); - cncserver.global.socket.removeListener('height', catchMove); - }; - - cncserver.global.socket.on('height', catchMove); - } - - // Leave this entire function to avoid doing the regular request. - return; - } - - // Ignore timeout with no callback by default if (typeof options.ignoreTimeout === 'undefined') { options.ignoreTimeout = callback ? '' : '1'; From ad49842a21a9c1f8a4fc06d8fbaeab4ea9fdef8b Mon Sep 17 00:00:00 2001 From: techninja Date: Sat, 26 Nov 2016 14:17:18 -0800 Subject: [PATCH 05/16] Write files with line breaks --- example/cncserver.client.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index 25573ae..d014f81 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -483,7 +483,7 @@ cncserver.api = { // Add a comma if the command isn't first. if (!cncserver.api.batch.firstCommand) { - line = ',' + line; + line = ",\n" + line; } cncserver.api.batch.fs.appendFileSync( cncserver.api.batch.saveFile, From 806a073c85fc123c0b5ace7a1a9163e77854dfd4 Mon Sep 17 00:00:00 2001 From: techninja Date: Sat, 26 Nov 2016 21:20:57 -0800 Subject: [PATCH 06/16] Use nextTick/setTimeout(0) to avoid call stack overflows --- example/cncserver.client.api.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index d014f81..8a460ae 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -606,9 +606,19 @@ function _request(method, path, options) { var uri = srv.protocol + '://' + srv.domain + ':' + srv.port + srvPath; // If we're batching commands.. we don't actually send them, we store them! - if (cncserver.api.batch.skipSend) { - cncserver.api.batch.addEntry(method + ' ' + srvPath, options.data); - options.success(); + // ...unless this command is meant to skipBuffer. + if (cncserver.api.batch.skipSend && !options.data.skipBuffer) { + if (isNode) { + process.nextTick(function() { + cncserver.api.batch.addEntry(method + ' ' + srvPath, options.data); + options.success(); + }); + } else { + setTimeout(function() { + cncserver.api.batch.addEntry(method + ' ' + srvPath, options.data); + options.success(); + }, 0); + } return; } From eddc4bdf21ba3d70f170bdd983865bca708f0cbf Mon Sep 17 00:00:00 2001 From: techninja Date: Sat, 26 Nov 2016 21:21:29 -0800 Subject: [PATCH 07/16] Add better node error reporting, and timeout option overrides --- example/cncserver.client.api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index 8a460ae..e8dbcf9 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -638,10 +638,10 @@ function _request(method, path, options) { json: true, method: method, body: options.data, - timeout: 1000 + timeout: options.timeout || 1000 }, function(error, response, body){ if (error) { - console.error(error); + console.error('API: Node request error', uri, method, error); if (options.error) options.error(error, response, body); } else { if (options.success) options.success(body, response); From 29fc2a58a0142fd5b4a505cdc291c3182eda13cf Mon Sep 17 00:00:00 2001 From: techninja Date: Sat, 26 Nov 2016 21:21:54 -0800 Subject: [PATCH 08/16] Add batch time and data debug for now --- example/cncserver.client.api.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index e8dbcf9..8808c69 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -545,9 +545,15 @@ cncserver.api = { } cncserver.api.batch.endClear(); + console.time('process-batch'); _post('batch', { data: dump, - success: callback, + timeout: 1000 * 60 * 10, // Timeout of 10 mins! + success: function(d){ + console.timeEnd('process-batch'); + console.info(d); + callback(); + }, }); }, From 10bf25d44cb0a9b60d019000d2787884882af13a Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 27 Nov 2016 15:17:54 -0800 Subject: [PATCH 09/16] Run calls via nextTick to ensure we don't call stack overflow --- src/cncserver.api.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cncserver.api.js b/src/cncserver.api.js index 0b25a37..1f6b29f 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -520,7 +520,9 @@ module.exports = function(cncserver) { } if (code.toString()[0] === '2') goodCount++; - processBatchData(commands, callback, index + 1, goodCount); + process.nextTick(function() { + processBatchData(commands, callback, index + 1, goodCount); + }); }}; }; @@ -534,7 +536,9 @@ module.exports = function(cncserver) { } if (response !== false) goodCount++; - processBatchData(commands, callback, index + 1, goodCount); + process.nextTick(function() { + processBatchData(commands, callback, index + 1, goodCount); + }); } } else { // Unhandled path, not a valid API handler available. Move on. From 34e99d529478de855d39abe8acc0ba1d5d7bfa21 Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 27 Nov 2016 15:31:02 -0800 Subject: [PATCH 10/16] Return batch request immediately with 201 after successful parse This means less information to the client, but at least we're not killing the socket waiting for a reply. --- src/cncserver.api.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/cncserver.api.js b/src/cncserver.api.js index 1f6b29f..bfc7332 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -368,6 +368,10 @@ module.exports = function(cncserver) { // Batch Command API ========================================================= cncserver.createServerEndpoint("/v1/batch", function(req, res){ if (req.route.method === 'post') { // Create a new batch set. + + // For exceedingly large batches over 50k commands, batching in takes + // longer than the socket will stay open, so we simply respond with a "201 + // queued" immediately after counting. if (req.body.file) { var file = req.body.file; if (file.substr(0, 4) === 'http') { @@ -377,12 +381,10 @@ module.exports = function(cncserver) { if (body) { try { var commands = JSON.parse(body); - processBatchData(commands, function(count) { - res.status(200).send(JSON.stringify({ - status: 'Batch parsed and queued ' + count + '/' + - commands.length + ' commands' - })); - }); + res.status(201).send(JSON.stringify({ + status: 'Batch parsed ' + commands.length + ' command queuing' + })); + processBatchData(commands); } catch(err) { error = err; } @@ -404,12 +406,10 @@ module.exports = function(cncserver) { if (data) { try { var commands = JSON.parse(data.toString()); - processBatchData(commands, function(count) { - res.status(200).send(JSON.stringify({ - status: 'Batch parsed and queued ' + count + '/' + - commands.length + ' commands' - })); - }); + res.status(201).send(JSON.stringify({ + status: 'Batch parsed ' + commands.length + ' command queuing' + })); + processBatchData(commands); } catch (err) { error = err; } @@ -427,12 +427,10 @@ module.exports = function(cncserver) { } else { // Raw command data (not from a file); try { - processBatchData(req.body, function(count) { - res.status(200).send(JSON.stringify({ - status: 'Batch parsed and queued ' + count + '/' + - req.body.length + ' commands' - })); - }); + res.status(201).send(JSON.stringify({ + status: 'Batch parsed ' + req.body.length + ' commands queuing' + })); + processBatchData(req.body); } catch (err) { res.status(400).send(JSON.stringify({ status: 'Error reading/processing batch data', @@ -546,7 +544,7 @@ module.exports = function(cncserver) { } } else { // We're done! - callback(goodCount); + if (callback) callback(goodCount); } } From 2069b71d408cc3b2b5578bb4d845b1aea7d14dfa Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 27 Nov 2016 15:54:53 -0800 Subject: [PATCH 11/16] Add support for killing batch queuing --- src/cncserver.api.js | 35 +++++++++++++++++++---------------- src/cncserver.queue.js | 3 +++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/cncserver.api.js b/src/cncserver.api.js index bfc7332..ee265a2 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -16,14 +16,15 @@ module.exports = function(cncserver) { // CNC Server API ============================================================ // Enpoints are created and assigned via a server path to respond to, and // and callback function that manages handles the request and response. - // We hold all of these in cncserver.apiHandlers to be able to call them + // We hold all of these in cncserver.api.handlers to be able to call them // directly from the batch API endpoint. These are actually only turned into // endpoints at the end via createServerEndpoint(). - cncserver.apiHandlers = {}; + cncserver.api = {}; + cncserver.api.handlers = {}; // Return/Set CNCServer Configuration ======================================== //cncserver.createServerEndpoint("/v1/settings", ); - cncserver.apiHandlers['/v1/settings'] = function settingsGet(req) { + cncserver.api.handlers['/v1/settings'] = function settingsGet(req) { if (req.route.method === 'get') { // Get list of tools return {code: 200, body: { global: '/v1/settings/global', @@ -34,7 +35,7 @@ module.exports = function(cncserver) { } }; - cncserver.apiHandlers['/v1/settings/:type'] = function settingsMain(req){ + cncserver.api.handlers['/v1/settings/:type'] = function settingsMain(req){ // Sanity check type var setType = req.params.type; if (setType !== 'global' && setType !== 'bot'){ @@ -74,7 +75,7 @@ module.exports = function(cncserver) { }; // Return/Set PEN state API ================================================= - cncserver.apiHandlers['/v1/pen'] = function penMain(req, res){ + cncserver.api.handlers['/v1/pen'] = function penMain(req, res){ if (req.route.method === 'put') { // SET/UPDATE pen status cncserver.control.setPen(req.body, function(stat){ @@ -140,7 +141,7 @@ module.exports = function(cncserver) { }; // Return/Set Motor state API ================================================ - cncserver.apiHandlers['/v1/motors'] = function motorsMain(req){ + cncserver.api.handlers['/v1/motors'] = function motorsMain(req){ // Disable/unlock motors if (req.route.method === 'delete') { if (req.body.skipBuffer) { @@ -194,7 +195,7 @@ module.exports = function(cncserver) { }; // Command buffer API ======================================================== - cncserver.apiHandlers['/v1/buffer'] = function bufferMain(req, res){ + cncserver.api.handlers['/v1/buffer'] = function bufferMain(req, res){ var buffer = cncserver.buffer; if (req.route.method === 'get' || req.route.method === 'put') { // Pause/resume (normalize input) @@ -325,7 +326,7 @@ module.exports = function(cncserver) { }; // Get/Change Tool API ======================================================= - cncserver.apiHandlers['/v1/tools'] = function toolsGet(req){ + cncserver.api.handlers['/v1/tools'] = function toolsGet(req){ if (req.route.method === 'get') { // Get list of tools return {code: 200, body:{ tools: Object.keys(cncserver.botConf.get('tools')) @@ -335,7 +336,7 @@ module.exports = function(cncserver) { } }; - cncserver.apiHandlers['/v1/tools/:tool'] = function toolsMain(req, res){ + cncserver.api.handlers['/v1/tools/:tool'] = function toolsMain(req, res){ var toolName = req.params.tool; // TODO: Support other tool methods... (needs API design!) if (req.route.method === 'put') { // Set Tool @@ -360,8 +361,8 @@ module.exports = function(cncserver) { } }; - // Bind all the apiHandlers into endpoints =================================== - _.each(cncserver.apiHandlers, function(callback, path) { + // Bind all the api.handlers into endpoints ================================== + _.each(cncserver.api.handlers, function(callback, path) { cncserver.createServerEndpoint(path, callback); }); @@ -466,10 +467,11 @@ module.exports = function(cncserver) { if (typeof index === 'undefined') { index = 0; goodCount = 0; + cncserver.api.batchRunning = true; } var command = commands[index]; - if (typeof command !== 'undefined') { + if (typeof command !== 'undefined' && cncserver.api.batchRunning) { var key = Object.keys(command)[0]; var data = command[key]; var method = key.split(' ')[0]; @@ -487,7 +489,7 @@ module.exports = function(cncserver) { var handlerKey = ''; // Attempt to match the path to a requstHandler by express path match. - _.each(Object.keys(cncserver.apiHandlers), function(pattern) { + _.each(Object.keys(cncserver.api.handlers), function(pattern) { var keys = []; var match = pathToRegexp(pattern, keys).exec(path); if (match) { @@ -510,7 +512,7 @@ module.exports = function(cncserver) { req.body = data; // Call the api handler (send and forget via batch!) - if (cncserver.apiHandlers[handlerKey]) { + if (cncserver.api.handlers[handlerKey]) { res.status = function(code) { return {send: function(data) { if (cncserver.gConf.get('debug')) { @@ -527,7 +529,7 @@ module.exports = function(cncserver) { // Naively check to see if the request was successful. // Technically if there's a wait for return (=== true), we could only // see it in the .status() return callback. - var response = cncserver.apiHandlers[handlerKey](req, res); + var response = cncserver.api.handlers[handlerKey](req, res); if (response !== true) { if (cncserver.gConf.get('debug')) { console.log('#' + index, 'Batch Immediate:', handlerKey, response); @@ -543,7 +545,8 @@ module.exports = function(cncserver) { processBatchData(commands, callback, index + 1, goodCount); } } else { - // We're done! + // We're out of commands, or batch was cancelled. + cncserver.api.batchRunning = false; if (callback) callback(goodCount); } } diff --git a/src/cncserver.queue.js b/src/cncserver.queue.js index 5d41452..17a01c3 100644 --- a/src/cncserver.queue.js +++ b/src/cncserver.queue.js @@ -135,6 +135,9 @@ module.exports = function(cncserver) { cncserver.buffer.pausePen = null; // Resuming with an empty buffer is silly cncserver.buffer.paused = false; + // If we're clearing, we need to kill any batch processes running. + cncserver.api.batchRunning = false; + // Reset the state of the buffer tip pen to the state of the actual robot. // If this isn't done, it will be assumed to be a state that was deleted // and never sent out. From 9dc14be18bee7b41fa68cf6fb546c24605ef0eaa Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 27 Nov 2016 19:43:51 -0800 Subject: [PATCH 12/16] Adjust batch return wording for consistency --- API.md | 17 ++++++++++------- src/cncserver.api.js | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/API.md b/API.md index 10c2fd5..288548a 100755 --- a/API.md +++ b/API.md @@ -675,26 +675,29 @@ Content-Type: application/json; charset=UTF-8 ### Response ```javascript -HTTP/1.1 200 OK +HTTP/1.1 201 OK Content-Type: application/json; charset=UTF-8 { - // If successful, the number of valid commands will be listed. - // If you park while already parked, or any commands fail, these will count - // against the final reported success rate. - "status" : "Batch parsed and queued 3/4 commands" + // Because some batch processing takes longer than the connection would be + // held open for, we simply return a 201 that the command shave been read + // and that these will be pushed into the buffer as quickly as possible. + "status" : "Parsed 4 commands, queuing" } ``` ##### Usage Notes * Command items are parsed and run semi-asynchronously, with each being run as - if an individual ReSTful request had been called. + if an individual ReSTful request had been called, without the overhead. * Every command is run with `ignoreTimeout` set, so no need to include it in - batch command data. + batch command data. If using the JS wrapper, this is done for you. * Detailed error catching is mostly non-existent, make sure your data is well formed and tested without batching before moving to batch submission. * Return data reporting is exceedingly minimal, so make sure you've decoupled your client from return data (just tie into stream updates). + * For batches of > 15k commands, data will stream into the buffer internally + for some time, so you have to be careful not to queue any commands in + afterwards or commands will be executed in the wrong order. ## 7. Socket.IO & real-time event data streaming diff --git a/src/cncserver.api.js b/src/cncserver.api.js index ee265a2..30a2851 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -383,7 +383,7 @@ module.exports = function(cncserver) { try { var commands = JSON.parse(body); res.status(201).send(JSON.stringify({ - status: 'Batch parsed ' + commands.length + ' command queuing' + status: 'Parsed ' + commands.length + ' commands, queuing' })); processBatchData(commands); } catch(err) { @@ -408,7 +408,7 @@ module.exports = function(cncserver) { try { var commands = JSON.parse(data.toString()); res.status(201).send(JSON.stringify({ - status: 'Batch parsed ' + commands.length + ' command queuing' + status: 'Parsed ' + commands.length + ' commands, queuing' })); processBatchData(commands); } catch (err) { @@ -429,7 +429,7 @@ module.exports = function(cncserver) { // Raw command data (not from a file); try { res.status(201).send(JSON.stringify({ - status: 'Batch parsed ' + req.body.length + ' commands queuing' + status: 'Parsed ' + req.body.length + ' commands, queuing' })); processBatchData(req.body); } catch (err) { From 6510da74ec0c02cd49daea696b855282cc60c2ba Mon Sep 17 00:00:00 2001 From: techninja Date: Thu, 8 Dec 2016 12:59:36 -0800 Subject: [PATCH 13/16] Add support for absolute position pen put arguments --- API.md | 20 ++++++++++++- machine_types/watercolorbot.ini | 5 ++++ src/cncserver.api.js | 17 +++++++++++ src/cncserver.control.js | 4 +-- src/cncserver.settings.js | 15 ++++++++++ src/cncserver.utils.js | 51 +++++++++++++++++++++++++++++++-- 6 files changed, 107 insertions(+), 5 deletions(-) diff --git a/API.md b/API.md index 288548a..ce98031 100755 --- a/API.md +++ b/API.md @@ -114,7 +114,7 @@ Content-Type: application/json; charset=UTF-8 ``` -#### Request Example (set position) +#### Request Example (set position via default relative percentage) ```javascript PUT /v1/pen Content-Type: application/json; charset=UTF-8 @@ -126,6 +126,19 @@ Content-Type: application/json; charset=UTF-8 ``` +#### Request Example (set position via absolute measurement) +```javascript +PUT /v1/pen +Content-Type: application/json; charset=UTF-8 + +{ + "abs": "mm", // Can be 'mm', or 'in' + "x": 20.5, + "y": 230.25 +} + +``` + #### Request Example (reset distance counter) ```javascript PUT /v1/pen @@ -181,6 +194,11 @@ position. In those cases, the response will return a `202 Accepted` instead of a `200 OK`. * `lastDuration` in return data can be used in conjunction with ignoreTimeout to allow the client to manage timings instead of waiting on the server. + * Absolute measurement is only supported by bot configurations that define +`maxAreaMM` width and height. For non-planar bots like the EggBot, absolute +measurements of the X axis could never reliably match to real world constants as +a differing egg size would change this measurement, so sending an `abs` option +will result in a `406` error. * * * diff --git a/machine_types/watercolorbot.ini b/machine_types/watercolorbot.ini index 469a583..c6412a1 100755 --- a/machine_types/watercolorbot.ini +++ b/machine_types/watercolorbot.ini @@ -41,6 +41,11 @@ moving = 75 width = 6315 height = 3600 +[maxAreaMM] +; Measured in millimeters +width = 361.5 +height = 206.25 + ; Position measured in percentage of maxArea [park] x = 0 diff --git a/src/cncserver.api.js b/src/cncserver.api.js index 30a2851..fd51629 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -77,6 +77,23 @@ module.exports = function(cncserver) { // Return/Set PEN state API ================================================= cncserver.api.handlers['/v1/pen'] = function penMain(req, res){ if (req.route.method === 'put') { + // Verify absolute measurement input. + if (req.body.abs) { + if (req.body.abs !== 'in' && req.body.abs !== 'mm') { + return [ + 406, + 'Input not acceptable, absolute measurement must be: in, mm' + ]; + } else { + if (!cncserver.bot.maxAreaMM) { + return [ + 406, + 'Input not acceptable, bot does not support absolute position.' + ]; + } + } + } + // SET/UPDATE pen status cncserver.control.setPen(req.body, function(stat){ var code = 200; diff --git a/src/cncserver.control.js b/src/cncserver.control.js index 73ca122..7c671b0 100644 --- a/src/cncserver.control.js +++ b/src/cncserver.control.js @@ -95,8 +95,8 @@ module.exports = function(cncserver) { return; } - // Convert the percentage values into real absolute and appropriate values - var absInput = cncserver.utils.centToSteps(inPen); + // Convert the percentage or absolute in/mm XY values into absolute steps. + var absInput = cncserver.utils.inPenToSteps(inPen); absInput.limit = 'workArea'; // Are we parking? diff --git a/src/cncserver.settings.js b/src/cncserver.settings.js index f3f6843..c61d914 100644 --- a/src/cncserver.settings.js +++ b/src/cncserver.settings.js @@ -114,6 +114,10 @@ module.exports = function(cncserver) { width: Number(cncserver.botConf.get('maxArea:width')), height: Number(cncserver.botConf.get('maxArea:height')) }, + maxAreaMM: { + width: Number(cncserver.botConf.get('maxAreaMM:width')), + height: Number(cncserver.botConf.get('maxAreaMM:height')) + }, park: { x: Number(cncserver.botConf.get('park:x')), y: Number(cncserver.botConf.get('park:y')) @@ -148,6 +152,17 @@ module.exports = function(cncserver) { y: bot.workArea.relCenter.y + bot.workArea.top }; + // If supplied, add conversions for abs distance. + if (bot.maxAreaMM.width) { + bot.stepsPerMM = { + x: bot.maxArea.width / bot.maxAreaMM.width, + y: bot.maxArea.height / bot.maxAreaMM.height + }; + //bot.stepsPerMM + } else { + cncserver.bot.maxAreaMM = false; + } + // Set initial pen position at park position var park = cncserver.utils.centToSteps(bot.park, true); cncserver.pen.x = park.x; diff --git a/src/cncserver.utils.js b/src/cncserver.utils.js index 38d8e0f..4c1a469 100644 --- a/src/cncserver.utils.js +++ b/src/cncserver.utils.js @@ -169,10 +169,57 @@ module.exports = function(cncserver) { } }; + /** + * Convert an incoming pen object absolute step coordinate values. + * + * @param {{x: number, y: number, abs: [in|mm]}} pen + * Pen/Coordinate measured in percentage of total draw area, or absolute + * distance to be converted to steps. + * + * @returns {{x: number, y: number}} + * Converted coordinate in absolute steps. + */ + cncserver.utils.inPenToSteps = function(inPen) { + if (inPen.abs === 'in' || inPen.abs === 'mm') { + return cncserver.utils.absToSteps({x: inPen.x, y: inPen.y}, inPen.abs); + } else { + return cncserver.utils.centToSteps({x: inPen.x, y: inPen.y}); + } + }; + + /** + * Convert an absolute point object to absolute step coordinate values. + * + * @param {{x: number, y: number, abs: [in|mm]}} point + * Coordinate measured in percentage of total draw area, or absolute + * distance to be converted to steps. + * @param {string} scale + * Either 'in' for inches, or 'mm' for millimeters. + * @param {boolean} inMaxArea + * Pass "true" if percent vals should be considered within the maximum area + * otherwise steps will be calculated as part of the global work area. + * + * @returns {{x: number, y: number}} + * Converted coordinate in absolute steps. + */ + cncserver.utils.absToSteps = function(point, scale, inMaxArea) { + var bot = cncserver.bot; + + // Convert Inches to MM. + if (scale === 'in') { + point = {x: point.x * 25.4, y: point.y * 25.4}; + } + + // Return absolute calculation. + return { + x: (!inMaxArea ? bot.workArea.left : 0) + (point.x * bot.stepsPerMM.x), + y: (!inMaxArea ? bot.workArea.top : 0) + (point.y * bot.stepsPerMM.y) + }; + }; /** - * Convert percent of total area coordinates into absolute step coordinate - * values + * Convert percent of total area coordinates into absolute step coordinates. + * * @param {{x: number, y: number}} point * Coordinate (measured in steps) to be converted. * @param {boolean} inMaxArea From f9d3e4a8c9be31d280bebbde258ca00fc35db86e Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 23 Apr 2017 17:06:02 -0700 Subject: [PATCH 14/16] Draw Ruler WIP code --- example/index.html | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/example/index.html b/example/index.html index cb75ce6..1016741 100755 --- a/example/index.html +++ b/example/index.html @@ -473,6 +473,79 @@

Loading tool set...

run('custom', runTest); } + function drawRuler(){ + var pos = {x: 0, y: 70}; + var border = 3; // 3mm border from edge. + var width = 20 * 10; // N cm ruler. + var height = 30; // 30mm high. + var tickSizes = [2.5, 5, 8.25]; // Ticks at 2.5, 5 and 8.25 mm height. + + // Draw ruler border. + run('move', {x:pos.x, y:pos.y, abs: 'mm'}); // Top left + run('down'); // Draw. + run('move', {x:pos.x + width + border * 2, y: pos.y, abs: 'mm'}); // Top right + run('move', {x:pos.x + width + border * 2, y: pos.y + height, abs: 'mm'}); // Bottom right + run('move', {x:pos.x, y:pos.y + height, abs: 'mm'}); // Bottom left + run('move', {x:pos.x, y:pos.y, abs: 'mm'}); // Top left + run('up'); + + // Loop over X positions from left to right. + for (var x = 0; x <= width; x++) { + var drawX = pos.x + border + x; + + var hIndex = 0; // Default to small tick. + if (x % 5 === 0) { + hIndex = x % 10 === 0 ? 2 : 1; + } + + // Skip small ticks! + if (hIndex != 0) { + run('move', {x: drawX, y:pos.y, abs: 'mm'}); // Top of X pos + run('down'); + run('move', {x: drawX, y: pos.y + tickSizes[hIndex], abs: 'mm'}); // Top of X pos + run('up'); + } + } + run('park'); + } + + + function drawRulerVert(){ + var pos = {x: 180, y: 0}; + var border = 3; // 3mm border from edge. + var width = 19 * 10; // N cm ruler. + var height = 30; // 30mm high. + var tickSizes = [2.5, 5, 8.25]; // Ticks at 2.5, 5 and 8.25 mm height. + + // Draw ruler border. + run('move', {x:pos.x, y:pos.y, abs: 'mm'}); // Top left + run('down'); // Draw. + run('move', {x:pos.x + height, y: pos.y, abs: 'mm'}); // Top right + run('move', {x:pos.x + height, y: pos.y + width + border * 2, abs: 'mm'}); // Bottom right + run('move', {x:pos.x, y:pos.y + width + border * 2, abs: 'mm'}); // Bottom left + run('move', {x:pos.x, y:pos.y, abs: 'mm'}); // Top left + run('up'); + + // Loop over Y positions from top to bottom. + for (var y = 0; y <= width; y++) { + var drawY = pos.y + border + y; + + var hIndex = 0; // Default to small tick. + if (y % 5 === 0) { + hIndex = y % 10 === 0 ? 2 : 1; + } + + // Skip small ticks! + if (hIndex != 0) { + run('move', {x: pos.x, y: drawY, abs: 'mm'}); + run('down'); + run('move', {x: pos.x + tickSizes[hIndex], y: drawY, abs: 'mm'}); + run('up'); + } + } + run('park'); + } + From 96bc851223ad6ce1a52709f82e17a597d782ba8d Mon Sep 17 00:00:00 2001 From: techninja Date: Sun, 23 Apr 2017 17:06:17 -0700 Subject: [PATCH 15/16] WIP Absolute Code --- example/cncserver.client.api.js | 28 +++------------------------ example/cncserver.client.commander.js | 12 ++++++------ 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/example/cncserver.client.api.js b/example/cncserver.client.api.js index 8808c69..bc71c6c 100755 --- a/example/cncserver.client.api.js +++ b/example/cncserver.client.api.js @@ -183,34 +183,12 @@ cncserver.api = { if (!isNode) $(cncserver.api).trigger('movePoint', [point]); // Sanity check inputs - point.x = point.x > 100 ? 100 : point.x; - point.y = point.y > 100 ? 100 : point.y; point.x = point.x < 0 ? 0 : point.x; point.y = point.y < 0 ? 0 : point.y; - // If we're on node and we have a socket, shortcut via WebSockets. - // TODO: FIX this as causes socket.io call stack overflows - if (isNode && cncserver.global.socket && false) { - if (typeof point.returnData === 'undefined') { - point.returnData = !!callback; - } - - cncserver.global.socket.emit('move', point); - if (callback) { - if (!point.returnData){ - callback({}); - } else { - var catchMove = function(d){ - callback(d); - cncserver.global.socket.removeListener('move', catchMove); - }; - - cncserver.global.socket.on('move', catchMove); - } - } - - // Leave this entire function to avoid doing the regular request. - return; + if (!point.abs) { + point.x = point.x > 100 ? 100 : point.x; + point.y = point.y > 100 ? 100 : point.y; } // Ignore timeout with no callback by default diff --git a/example/cncserver.client.commander.js b/example/cncserver.client.commander.js index a6a025b..340cdb5 100755 --- a/example/cncserver.client.commander.js +++ b/example/cncserver.client.commander.js @@ -48,28 +48,28 @@ cncserver.cmd = { next = [next]; } + // These are all run as send and forgets, so ignore the timeout. switch (next[0]) { case "move": + next[1].ignoreTimeout = true; cncserver.api.pen.move(next[1], cncserver.cmd.cb); break; case "tool": + next[1].ignoreTimeout = true; cncserver.api.tools.change(next[1], cncserver.cmd.cb); break; case "up": - cncserver.api.pen.up(cncserver.cmd.cb); + cncserver.api.pen.up(cncserver.cmd.cb, {ignoreTimeout: true}); break; case "down": - cncserver.api.pen.down(cncserver.cmd.cb); + cncserver.api.pen.down(cncserver.cmd.cb, {ignoreTimeout: true}); break; case "status": cncserver.utils.status(next[1], next[2]); cncserver.cmd.cb(true); break; - case "wash": - cncserver.wcb.fullWash(cncserver.cmd.cb); - break; case "park": - cncserver.api.pen.park(cncserver.cmd.cb); + cncserver.api.pen.park(cncserver.cmd.cb, {ignoreTimeout: true}); break; case "custom": cncserver.cmd.cb(); From d57870a0967fb35a5949b0c87071e0a2b30d17e9 Mon Sep 17 00:00:00 2001 From: techninja Date: Fri, 21 Jul 2017 17:59:48 -0700 Subject: [PATCH 16/16] Add AxiDraw calibrated maxAreaMM :smile: --- machine_types/axidraw.ini | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/machine_types/axidraw.ini b/machine_types/axidraw.ini index 9f6a52b..1274ca5 100755 --- a/machine_types/axidraw.ini +++ b/machine_types/axidraw.ini @@ -1,4 +1,4 @@ -name = AxiDraw +name = AxiDraw ; Add support for AxiDraw 2.0 to cncserver (and RoboPaint) ; These are used to automatically detect/connect to the board. Very important! @@ -43,6 +43,11 @@ moving = 30 width = 12000 height = 8720 +[maxAreaMM] +; Measured in millimeters +width = 300 +height = 216.5 + ; Position measured in percentage of maxArea [park] x = 0