diff --git a/API.md b/API.md index 18971ca..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. * * * @@ -624,7 +642,83 @@ 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 201 OK +Content-Type: application/json; charset=UTF-8 + +{ + // 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, without the overhead. + * Every command is run with `ignoreTimeout` set, so no need to include it in + 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 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/example/cncserver.client.api.js b/example/cncserver.client.api.js index 89b16bf..bc71c6c 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'; @@ -202,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 @@ -440,7 +399,152 @@ 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 = ",\n" + 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(); + console.time('process-batch'); + _post('batch', { + data: dump, + timeout: 1000 * 60 * 10, // Timeout of 10 mins! + success: function(d){ + console.timeEnd('process-batch'); + console.info(d); + 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 +588,25 @@ 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! + // ...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; + } + + // Send the request. if (!isNode) { $.ajax({ url: uri, @@ -499,10 +622,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); 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(); 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'); + } + diff --git a/machine_types/axidraw.ini b/machine_types/axidraw.ini index 682bd89..1274ca5 100755 --- a/machine_types/axidraw.ini +++ b/machine_types/axidraw.ini @@ -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 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/package.json b/package.json old mode 100755 new mode 100644 index 8679d91..1762bad --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "ini": "1.3.4", "nconf": "0.8.4", "node-ipc": "8.9.2", + "path-to-regexp": "^1.7.0", "request": "*", "serialport": "6.0.4", - "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 2180b53..ba62fbf 100644 --- a/src/cncserver.api.js +++ b/src/cncserver.api.js @@ -6,10 +6,25 @@ * */ +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.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.api = {}; + cncserver.api.handlers = {}; + // Return/Set CNCServer Configuration ======================================== - cncserver.createServerEndpoint("/v1/settings", function(req){ + //cncserver.createServerEndpoint("/v1/settings", ); + 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', @@ -18,9 +33,9 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; - cncserver.createServerEndpoint("/v1/settings/:type", function(req){ + cncserver.api.handlers['/v1/settings/:type'] = function settingsMain(req){ // Sanity check type var setType = req.params.type; if (setType !== 'global' && setType !== 'bot'){ @@ -57,11 +72,28 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Return/Set PEN state API ================================================= - cncserver.createServerEndpoint("/v1/pen", function(req, res){ + 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; @@ -123,10 +155,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Return/Set Motor state API ================================================ - cncserver.createServerEndpoint("/v1/motors", function(req){ + cncserver.api.handlers['/v1/motors'] = function motorsMain(req){ // Disable/unlock motors if (req.route.method === 'delete') { cncserver.run('custom', cncserver.buffer.cmdstr('disablemotors')); @@ -170,10 +202,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Command buffer API ======================================================== - cncserver.createServerEndpoint("/v1/buffer", function(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) @@ -301,10 +333,10 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; // Get/Change Tool API ======================================================= - cncserver.createServerEndpoint("/v1/tools", function(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')) @@ -312,9 +344,9 @@ module.exports = function(cncserver) { } else { return false; } - }); + }; - cncserver.createServerEndpoint("/v1/tools/:tool", function(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 @@ -336,5 +368,234 @@ module.exports = function(cncserver) { } else { return false; } + }; + + // Bind all the api.handlers into endpoints ================================== + _.each(cncserver.api.handlers, 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. + + // 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') { + // Internet file. + request.get(file, function (error, response, body) { + // Attempt to parse/process data. + if (body) { + try { + var commands = JSON.parse(body); + res.status(201).send(JSON.stringify({ + status: 'Parsed ' + commands.length + ' commands, queuing' + })); + processBatchData(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()); + res.status(201).send(JSON.stringify({ + status: 'Parsed ' + commands.length + ' commands, queuing' + })); + processBatchData(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 { + res.status(201).send(JSON.stringify({ + status: 'Parsed ' + req.body.length + ' commands, queuing' + })); + processBatchData(req.body); + } 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; + cncserver.api.batchRunning = true; + } + + var command = commands[index]; + if (typeof command !== 'undefined' && cncserver.api.batchRunning) { + 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.api.handlers), 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.api.handlers[handlerKey]) { + res.status = function(code) { + return {send: function(data) { + if (cncserver.gConf.get('debug')) { + console.log('#' + index, 'Batch Delay:', handlerKey, code, data); + } + + if (code.toString()[0] === '2') goodCount++; + process.nextTick(function() { + 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.api.handlers[handlerKey](req, res); + if (response !== true) { + if (cncserver.gConf.get('debug')) { + console.log('#' + index, 'Batch Immediate:', handlerKey, response); + } + + if (response !== false) goodCount++; + process.nextTick(function() { + 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 out of commands, or batch was cancelled. + cncserver.api.batchRunning = false; + if (callback) 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; + } }; 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.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. 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 fa683b2..7c2e495 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