diff --git a/.gitignore b/.gitignore index 9daa824..116ba54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store node_modules +.idea +npm-debug.log diff --git a/software/package.json b/software/package.json index 0463414..46523af 100755 --- a/software/package.json +++ b/software/package.json @@ -17,6 +17,16 @@ "node":"0.x.x" }, "dependencies":{ - "johnny-five":"0.7.x" + "johnny-five":"0.7.x", + "hapi": "8.4.0", + "argparse": "~0.1.10", + "path": "~0.4.9", + "temp": "~0.5.0", + "request": "~2.12.0", + "johnny-five": "git://github.com/rwaldron/johnny-five.git", + "prompt": "~0.2.14", + "request": "~2.12.0", + "sylvester":"0.0.21", + "wd": "0.3.11" } } diff --git a/software/src/bot.js b/software/src/bot.js index f313d6a..1161676 100755 --- a/software/src/bot.js +++ b/software/src/bot.js @@ -1,5 +1,5 @@ five = require("johnny-five"); -ik = require("./ik"); +ik = require("./lib/kinematics"); board = new five.Board({ debug: false }); diff --git a/software/src/calibrate.js b/software/src/calibrate.js new file mode 100755 index 0000000..7517654 --- /dev/null +++ b/software/src/calibrate.js @@ -0,0 +1,266 @@ +#! /usr/local/bin/node + +var prompt = require("prompt") + , fs = require("fs") + , eol = require('os').EOL + , ArgumentParser = require('argparse').ArgumentParser + , robot = require('./lib/server/robot_http_client').client("127.0.0.1","4242") + , wd = require('wd'); + +var args = {}, + newCalibrationData = {}; + +function CalibrationManager(argv) { + args = argv; + prompt.message = ''; + prompt.delimiter = ''; + prompt.start(); +} +exports.CalibrationManager = CalibrationManager; + +var getCommandLineArgs = function() { + var parser = new ArgumentParser({ + version: '0.0.1', + addHelp:true, + description: 'Tapster Calibration Script' + }); + + parser.addArgument( + [ '-o', '--output' ], { + defaultValue: "calibration.json" + , help: 'file to save calibration data to' + } + ); + + return parser.parseArgs(); +}; + +CalibrationManager.prototype.calibrate = function() { + robot.calibrationData(function (calibrationData) { + console.log("Receiving existing calibration data."); + newCalibrationData = calibrationData; + console.log(JSON.stringify(newCalibrationData)); + return askToCalibrateRobot(function() { + return askToCalibrateDevice(function() { + saveCalibrationData(function() { + console.log("Calibration Complete"); + }); + }) + }); + }); +}; + +var askToCalibrateRobot = function(cb) { + var schema = { + name:"answer", + description: 'Would you like to calibrate the robot arms?', + type: 'string' + }; + prompt.get(schema, function (err, result) { + if (result.answer.toLowerCase().substr(0,1) == "y") { + return calibrateRobot(cb); + } else { + return cb(); + } + }); +}; + +var askToCalibrateDevice = function(cb) { + var schema = { + name:"answer", + description: 'Would you like to calibrate the a device?', + type: 'string' + }; + prompt.get(schema, function (err, result) { + if (result.answer.toLowerCase().substr(0,1) == "y") { + return calibrateDevice(cb); + } else { + return cb(); + } + }); +}; + +var saveCalibrationData = function(cb) { + + console.log("New Calibration Data Generated."); + console.log(JSON.stringify(newCalibrationData)); + return robot.setCalibrationData(newCalibrationData, function () { + console.log("Robot is now calibrated!"); + return fs.writeFile(args.output, JSON.stringify(newCalibrationData), function (err) { + if (err) { + console.log('Calibration data could not be saved: ' + err); + } else { + console.log('Calibration data saved to "' + args.output + '"'); + } + return cb(); + }); + }); + + +}; + +var calibrateRobot = function(cb) { + var schema = { + description: 'Please remove the arms from the robot and press any key to continue...', + type: 'string' + }; + return prompt.get(schema, function () { + var calibrateServos = function(cb) { + + var calibrateServo = function(armIndex, isMin, cb) { + robot.angles(function (angles) { + var description = 'Enter an adjustment for arm #' + (armIndex +1) + ', enter 0 when the arm is ' + + (isMin ? 'parallel to the roof.' : 'perpendicular to the roof'); + var schema = { + name: "delta", + description: description, + type: 'number' + }; + + return prompt.get(schema, function (err, result) { + if (result.delta < 0.05 && result.delta > -0.05) { + newCalibrationData["servo" + (armIndex+1)][(isMin ? "min" : "max" ) + "imumAngle"] = angles[armIndex]; + return cb(); + } else { + console.log("Old Angles: " + angles); + angles[armIndex] = angles[armIndex] + result.delta; + console.log("New Angles: " + angles); + robot.setAngles(angles[0], angles[1], angles[2], function() { + return calibrateServo(armIndex, isMin, cb); + }); + } + }); + }); + }; + + var calibrateServoMinAndMax = function(armIndex, cb) { + return robot.reset(function () { + return calibrateServo(armIndex, true, function () { + return calibrateServo(armIndex, false, cb); + }); + }); + }; + + // calibrate the servos + return calibrateServoMinAndMax(0, function() { + return calibrateServoMinAndMax(1, function() { + return calibrateServoMinAndMax(2, function() { + return robot.reset(cb); + }); + }); + }); + }; + + return calibrateServos(function () { + return cb(); + }); + }); +}; + +var calibrateDevice = function(cb) { + newCalibrationData.device = { + contactPoint: { position:{},screenCoordinates:{} }, + point1: { position:{},screenCoordinates:{} }, + point2: { position:{},screenCoordinates:{} } + }; + var driver = wd.remote({port:4723}); + // optional extra logging + /* + driver.on('status', function(info) { + console.log(info.cyan); + }); + driver.on('command', function(eventType, command, response) { + console.log(' > ' + eventType.cyan, command, (response || '').grey); + }); + */ + driver.on('http', function(meth, path, data) { + console.log(' > ' + meth.magenta, path, (data || '').grey); + }); + + var lowerAndCheckForContact = function(x,y,currentZ, cb) { + return robot.setPosition(x,y,currentZ,function() { + setTimeout(function() { + + var coordRegex = /label[^\(,]+\((\d+\.*\d*),\s+(\d+\.*\d*)\)/ ; + return driver.source(function(err, pageSource) { + if (coordRegex.test(pageSource)) { + var match = coordRegex.exec(pageSource); + var screenX = parseFloat(match[1]); + var screenY = parseFloat(match[2]); + console.log("Found Point: (" + x + "," + y + ") => (" + screenX + "," + screenY + ")"); + return cb(x,y,screenX,screenY,currentZ); + } else { + if (currentZ < -150) { + return robot.reset(function() { + return cb(Error("Could not touch the screen.")); + }); + } else { + return lowerAndCheckForContact(x, y, currentZ - 2, cb); + } + } + }); + //*/ + + /* + return driver.elementByClassName("UIAStaticText", function (err, element) { + if (element) { + return robot.reset(function() { + return element.getLocation(function (err, location) { + if (err) { + return cb(Error("Could get the element's location.")); + } else { + return element.getSize(function (err, size) { + if (err) { + return cb(Error("Could get the element's size.")); + } else { + var screenX = location.x + (size.width / 2.0); + var screenY = location.y + (size.height / 2.0); + return cb(screenX, screenY, currentZ); + } + }); + } + }); + }); + } else { + if (currentZ < -150) { + return robot.reset(function() { + return cb(Error("Could not touch the screen.")); + }); + } else { + return lowerAndCheckForContact(x, y, currentZ - 2, cb); + } + } + }); + //*/ + + }, 2000); + }); + }; + + return driver.init( { + app:"Appium.RobotCalibration", + platform:"iOS", + platformVersion:"8.2", + udid: "481309bbf8a3c341687e617bb7104be41f3abb07" + }, function() { + driver.setImplicitWaitTimeout(1000, function () { + return lowerAndCheckForContact(0, 0, -145, function (x, y, screenX, screenY, robotZ) { + newCalibrationData.device.contactPoint.position = {x: x, y: y, z:robotZ}; + newCalibrationData.device.contactPoint.screenCoordinates = {x: screenX, y: screenY}; + return lowerAndCheckForContact(0, 20, -145, function (x, y, screenX, screenY, robotZ) { + newCalibrationData.device.point1.position = {x: x, y: y, z:robotZ}; + newCalibrationData.device.point1.screenCoordinates = {x: screenX, y: screenY}; + return lowerAndCheckForContact(20, 0, -145, function (x, y, screenX, screenY, robotZ) { + newCalibrationData.device.point2.position = {x: x, y: y, z:robotZ}; + newCalibrationData.device.point2.screenCoordinates = {x: screenX, y: screenY}; + return robot.reset(cb); + }); + }); + }); + }); + }); +}; + +if(require.main === module) { + new CalibrationManager(getCommandLineArgs()).calibrate(); +} diff --git a/software/src/calibration.json b/software/src/calibration.json new file mode 100644 index 0000000..d042a88 --- /dev/null +++ b/software/src/calibration.json @@ -0,0 +1 @@ +{"restPoint":{"x":0,"y":0,"z":-130},"servo1":{"minimumAngle":20,"maximumAngle":90},"servo2":{"minimumAngle":20,"maximumAngle":90},"servo3":{"minimumAngle":20,"maximumAngle":90},"device":{"contactPoint":{"position":{"x":0,"y":0,"z":-149},"screenCoordinates":{"x":184,"y":292}},"point1":{"position":{"x":0,"y":20,"z":-149},"screenCoordinates":{"x":188,"y":195}},"point2":{"position":{"x":20,"y":0,"z":-149},"screenCoordinates":{"x":280,"y":285}}}} \ No newline at end of file diff --git a/software/src/ik.js b/software/src/lib/kinematics.js similarity index 100% rename from software/src/ik.js rename to software/src/lib/kinematics.js diff --git a/software/src/lib/server/calibration.js b/software/src/lib/server/calibration.js new file mode 100644 index 0000000..64922cd --- /dev/null +++ b/software/src/lib/server/calibration.js @@ -0,0 +1,30 @@ +var fs = require("fs"); + +// default calibration +module.exports.defaultData = { + restPoint : { + x : 0, + y : 0, + z : -130 + }, + servo1 : { + minimumAngle : 20, + maximumAngle : 90 + }, + servo2 : { + minimumAngle : 20, + maximumAngle : 90 + }, + servo3 : { + minimumAngle : 20, + maximumAngle : 90 + } +}; + +module.exports.getDataFromFilePath = function(filePath) { + if (fs.existsSync(filePath)) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } else { + return null; + } +}; \ No newline at end of file diff --git a/software/src/lib/server/keyboards.js b/software/src/lib/server/keyboards.js new file mode 100644 index 0000000..5e79b85 --- /dev/null +++ b/software/src/lib/server/keyboards.js @@ -0,0 +1,142 @@ +var keymaps = {}; + +keymaps['iPhone 6'] = { + q: { position: {x: 0, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + w: { position: {x: 32, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + e: { position: {x: 64, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + r: { position: {x: 96, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + t: { position: {x: 128, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + y: { position: {x: 160, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + u: { position: {x: 192, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + i: { position: {x: 224, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + o: { position: {x: 256, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + p: { position: {x: 288, y: 363}, size: {width: 32, height:42 }, modifiers:[]}, + a: { position: {x: 16, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + s: { position: {x: 48, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + d: { position: {x: 80, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + f: { position: {x: 112, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + g: { position: {x: 144, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + h: { position: {x: 176, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + j: { position: {x: 208, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + k: { position: {x: 240, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + l: { position: {x: 272, y: 417}, size: {width: 32, height:42 }, modifiers:[]}, + z: { position: {x: 48, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + x: { position: {x: 80, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + c: { position: {x: 112, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + v: { position: {x: 144, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + b: { position: {x: 176, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + n: { position: {x: 208, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + m: { position: {x: 240, y: 471}, size: {width: 32, height:42 }, modifiers:[]}, + Q: { position: {x: 0, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + W: { position: {x: 32, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + E: { position: {x: 64, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + R: { position: {x: 96, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + T: { position: {x: 128, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + Y: { position: {x: 160, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + U: { position: {x: 192, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + I: { position: {x: 224, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + O: { position: {x: 256, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + P: { position: {x: 288, y: 363}, size: {width: 32, height:42 }, modifiers:["shift"]}, + A: { position: {x: 16, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + S: { position: {x: 48, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + D: { position: {x: 80, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + F: { position: {x: 112, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + G: { position: {x: 144, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + H: { position: {x: 176, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + J: { position: {x: 208, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + K: { position: {x: 240, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + L: { position: {x: 272, y: 417}, size: {width: 32, height:42 }, modifiers:["shift"]}, + Z: { position: {x: 48, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + X: { position: {x: 80, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + C: { position: {x: 112, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + V: { position: {x: 144, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + B: { position: {x: 176, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + N: { position: {x: 208, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + M: { position: {x: 240, y: 471}, size: {width: 32, height:42 }, modifiers:["shift"]}, + 1: { position: {x: 0, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 2: { position: {x: 32, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 3: { position: {x: 64, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 4: { position: {x: 96, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 5: { position: {x: 128, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 6: { position: {x: 160, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 7: { position: {x: 192, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 8: { position: {x: 224, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 9: { position: {x: 256, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + 0: { position: {x: 288, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '-': { position: {x: 0, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '/': { position: {x: 32, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + ':': { position: {x: 64, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + ';': { position: {x: 96, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '(': { position: {x: 128, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + ')': { position: {x: 160, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '$': { position: {x: 192, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '&': { position: {x: 224, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '@': { position: {x: 256, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '"': { position: {x: 288, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle"]}, + '.': { position: {x: 48, y: 471}, size: {width: 45, height:42 }, modifiers:["numberToggle"]}, + ',': { position: {x: 92, y: 471}, size: {width: 46, height:42 }, modifiers:["numberToggle"]}, + '?': { position: {x: 137, y: 471}, size: {width: 46, height:42 }, modifiers:["numberToggle"]}, + '!': { position: {x: 182, y: 471}, size: {width: 46, height:42 }, modifiers:["numberToggle"]}, + "'": { position: {x: 227, y: 471}, size: {width: 45, height:42 }, modifiers:["numberToggle"]}, + '[': { position: {x: 0, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + ']': { position: {x: 32, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '{': { position: {x: 64, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '}': { position: {x: 96, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '#': { position: {x: 128, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '%': { position: {x: 160, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '^': { position: {x: 192, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '*': { position: {x: 224, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '+': { position: {x: 256, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '=': { position: {x: 288, y: 363}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '_': { position: {x: 0, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '\\': { position: {x: 32, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '|': { position: {x: 64, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '~': { position: {x: 96, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '<': { position: {x: 128, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '>': { position: {x: 160, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '€': { position: {x: 192, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '£': { position: {x: 224, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '•': { position: {x: 256, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + '¥': { position: {x: 288, y: 417}, size: {width: 32, height:42 }, modifiers:["numberToggle", "shift"]}, + shift: { position: {x: 20, y: 471}, size: {width: 22, height:42 }, modifiers:[]}, + backspace: { position: {x: 278, y: 471}, size: {width: 42, height:42 }, modifiers:[]}, + numberToggle: { position: {x: 20, y: 525}, size: {width: 20, height:37 }, modifiers:[]}, + keyboardToggle: { position: {x: 40, y: 525}, size: {width: 40, height:37 }, modifiers:[]}, + dictation: { position: {x: 80, y: 525}, size: {width: 32, height:37 }, modifiers:[]}, + ' ': { position: {x: 112, y: 525}, size: {width: 128, height:37 }, modifiers:[]}, + done: { position: {x: 240, y: 525}, size: {width: 80, height:37 }, modifiers:[]} +}; + + +var keyboardModule = { + getKeyboard: function (deviceName) { + var keyboard = {}; + keyboard.keyMap = keymaps[deviceName]; + keyboard.getKeySequence = function (key) { + var sequence = []; + var postSequence = []; + var keyMapping = keyboard.keyMap[key]; + for (var modIndex=0; modIndex < keyMapping.modifiers.length; modIndex++) { + var modKeyName = keyMapping.modifiers[modIndex]; + var modKey = keyboard.keyMap[modKeyName]; + var keyPosition = { + x: modKey.position.x + (modKey.size.width / 2), + y: modKey.position.y + (modKey.size.height / 2) + }; + sequence.push(keyPosition); + if (modKeyName == "numberToggle") { + postSequence.push(keyPosition); + } + } + sequence.push({ + x: keyMapping.position.x + (keyMapping.size.width / 2), + y: keyMapping.position.y + (keyMapping.size.height / 2) + }); + sequence = sequence.concat(postSequence); + return sequence; + }; + return keyboard; + } +}; + +module.exports = keyboardModule; \ No newline at end of file diff --git a/software/src/lib/server/parser.js b/software/src/lib/server/parser.js new file mode 100644 index 0000000..68a21aa --- /dev/null +++ b/software/src/lib/server/parser.js @@ -0,0 +1,32 @@ +var ArgumentParser = require('argparse').ArgumentParser; + +// parse arguments +var parser = new ArgumentParser({ + version: '0.0.1', + addHelp:true, + description: 'Tapster Server' +}); + +parser.addArgument( + [ '-c', '--calibration'], { + help: 'file to load calibration data from' + }); + +parser.addArgument( + ['-p', '--port'] , { + defaultValue: 4242 + , required: false + , type: 'int' + , example: "4242" + , help: 'port to listen on' + }); + +parser.addArgument( + ['-a', '--address'], { + defaultValue: '127.0.0.1' + , required: false + , example: "127.0.0.1" + , help: 'IP Address to listen on' + }); + +module.exports = parser; \ No newline at end of file diff --git a/software/src/lib/server/robot.js b/software/src/lib/server/robot.js new file mode 100644 index 0000000..3671023 --- /dev/null +++ b/software/src/lib/server/robot.js @@ -0,0 +1,245 @@ +var kinematics = require("./../kinematics"); + +require("sylvester"); +var keyboards = require("./keyboards"); +var method = Robot.prototype; + +function Robot(servo1, servo2, servo3, calibration) { + this._servo1 = servo1; + this._servo2 = servo2; + this._servo3 = servo3; + this._calibration = calibration; + this._dancer_interval = null; +} + +var generateTranslationMatrix = function(calibration) { + + var r1x = calibration.device.contactPoint.position.x; + var r1y = calibration.device.contactPoint.position.y; + var r2x = calibration.device.point1.position.x; + var r2y = calibration.device.point1.position.y; + var r3x = calibration.device.point2.position.x; + var r3y = calibration.device.point2.position.y; + + var d1x = calibration.device.contactPoint.screenCoordinates.x; + var d1y = calibration.device.contactPoint.screenCoordinates.y; + var d2x = calibration.device.point1.screenCoordinates.x; + var d2y = calibration.device.point1.screenCoordinates.y; + var d3x = calibration.device.point2.screenCoordinates.x; + var d3y = calibration.device.point2.screenCoordinates.y; + + var deviceXVector = $M([[(d3x-d1x) / (r3x-r1x)], [(d3y-d1y) / (r3x-r1x) ]]); + var deviceYVector = $M([[(d2x-d1x) / (r2y-r1y)], [(d2y-d1y) / (r2y-r1y) ]]); + var offset = $M([d1x-r1x, d1y-r1y]); + var r2dMatrix = $M([[deviceXVector.elements[0], deviceYVector.elements[0]], [deviceXVector.elements[1], deviceYVector.elements[1]]]); + return {offset: offset, matrix: r2dMatrix}; + /* + + var b0x = calibration.device.point1.position.x, + b0y = calibration.device.point1.position.y, + b1x = calibration.device.point2.position.x, + b1y = calibration.device.point2.position.y; + var d0x = calibration.device.point1.screenCoordinates.x, + d0y = calibration.device.point1.screenCoordinates.y, + d1x = calibration.device.point2.screenCoordinates.x, + d1y = calibration.device.point2.screenCoordinates.y; + + var M = $M([ + [d0x, d0y, 1, 0], + [-d0y, d0x, 0, 1], + [d1x, d1y, 1, 0], + [-d1y, d1x, 0, 1] + ]); + var u = $M([ + [b0x], + [b0y], + [b1x], + [b1y] + ]); + var MI = M.inverse(); + return MI.multiply(u);*/ +}; + +var sin = function(degree) { + return Math.sin(Math.PI * (degree/180)); +}; + +var cos = function(degree) { + return Math.cos(Math.PI * (degree/180)); +}; + +var mapNumber = function (num, in_min , in_max , out_min , out_max ) { + return ( num - in_min ) * ( out_max - out_min ) / ( in_max - in_min ) + out_min; +}; + +var rotate = function(x,y) { + var theta = -60; + var x1 = x * cos(theta) - y * sin(theta); + var y1 = y * cos(theta) + x * sin(theta); + return [x1,y1] +}; + +var reflect = function(x,y) { + var theta = 0; + var x1 = x; + var y1 = x * sin(2*theta) - y * cos(2*theta); + return [x1,y1] +}; + + +method.getAngles = function() { + return [this._servo1.last.degrees, this._servo2.last.degrees, this._servo3.last.degrees]; +}; + +method.setAngles = function(t1,t2,t3) { + console.log("Setting Angles:" + [t1,t2,t3]); + t1 = isNaN(t1) ? this._calibration.servo1.minimumAngle : t1; + t2 = isNaN(t2) ? this._calibration.servo1.minimumAngle : t2; + t3 = isNaN(t3) ? this._calibration.servo1.minimumAngle : t3; + this._servo1.to(t1); + this._servo2.to(t2); + this._servo3.to(t3); +}; + +method.getPosition = function() { + var angles = this.getAngles(); + return kinematics.forward(angles[0], angles[1], angles[2]); +}; + +method.setPosition = function(x, y, z) { + console.log("Setting Position:" + [x,y,z]); + var reflected = reflect(x,y); + var rotated = rotate(reflected[0],reflected[1]); + var angles = kinematics.inverse(rotated[0], rotated[1], z); + var t1 = mapNumber(angles[1], 0 , 90 , this._calibration.servo1.minimumAngle , this._calibration.servo1.maximumAngle); + var t2 = mapNumber(angles[2], 0 , 90 , this._calibration.servo2.minimumAngle , this._calibration.servo2.maximumAngle); + var t3 = mapNumber(angles[3], 0 , 90 , this._calibration.servo3.minimumAngle , this._calibration.servo3.maximumAngle); + this.setAngles(t1,t2,t3); +}; + +method.resetPosition = function() { + this.setPosition(this._calibration.restPoint.x, this._calibration.restPoint.y, this._calibration.restPoint.z); +}; + +method.getPositionForAngles = function(t1,t2,t3) { + var points = kinematics.forward(t1,t2,t3); + return [points[1], points[2], points[3]]; +}; + +method.getAnglesForPosition = function(x,y,z) { + var angles = kinematics.inverse(x,y,z); + return [angles[1], angles[2], angles[3]]; +}; + +method.getPositionForScreenCoordinates = function(x,y) { + var calData = generateTranslationMatrix(this._calibration); + var matrix = calData.matrix; + var offset = calData.offset; + var vector = $M([ [x-offset.elements[0]],[y-offset.elements[1]] ]); + var converted = matrix.inverse().multiply(vector); + var newX = converted.elements[0]; + var newY = converted.elements[1]; + return {x:newX, y:newY}; + /* + var matrix = generateTranslationMatrix(this._calibration); + var a = matrix.elements[0][0], + b = matrix.elements[1][0], + c = matrix.elements[2][0], + d = matrix.elements[3][0]; + var yprime = a * x + b * y + c; + var xprime = b * x - a * y + d; + console.log("(" + x +"," + y +") => (" + xprime + "," + yprime +")"); + return {x:xprime, y:yprime}; + */ +}; + +method.getContactZ = function() { + return 1.01 * Math.min( + this._calibration.device.contactPoint.position.z, + this._calibration.device.point1.position.z, + this._calibration.device.point2.position.z + ); +}; + +method.tap = function(screenX, screenY, cb) { + var position = this.getPositionForScreenCoordinates(screenX, screenY); + var touchZ = this.getContactZ(); + this.setPosition(position.x, position.y, touchZ * 0.9); + return setTimeout(function() { + this.setPosition(position.x, position.y, touchZ); + return setTimeout(function() { + this.setPosition(position.x, position.y, touchZ * 0.9); + return setTimeout(cb, 400); + }.bind(this), 400); + }.bind(this), 400); +}; + +method.swipe = function(startX, startY, endX, endY, cb) { + var startPosition = this.getPositionForScreenCoordinates(startX, startY); + var endPosition = this.getPositionForScreenCoordinates(endX, endY); + var touchZ = this.getContactZ(); + this.setPosition(startPosition.x, startPosition.y, touchZ * 0.9); + return setTimeout(function() { + this.setPosition(startPosition.x, startPosition.y, touchZ); + return setTimeout(function() { + this.setPosition(endPosition.x, endPosition.y, touchZ); + return setTimeout(function() { + this.setPosition(endPosition.x, endPosition.y, touchZ * 0.9); + return setTimeout(cb, 100); + }.bind(this), 400); + }.bind(this), 400); + }.bind(this), 400); +}; + +method.sendKeys = function(keys, cb) { + var keyboard = keyboards.getKeyboard("iPhone 6"/*this._calibration.device.name*/); + var keystrokeSequence = []; + for (var keyIndex=0; keyIndex < keys.length; keyIndex++) { + keystrokeSequence = keystrokeSequence.concat(keyboard.getKeySequence(keys[keyIndex])); + } + var tapKey = function(keystrokes, cb) { + if (keystrokes.length == 0) { + return cb(); + } else { + var currentKeyPosition = keystrokes.shift(); + this.tap(currentKeyPosition.x, currentKeyPosition.y, function() { + return tapKey(keystrokes, cb); + }); + } + }.bind(this); + return tapKey(keystrokeSequence, cb); +}; + +method.startDancing = function() { + var _dance = function() { + var minAngle = 10; + var maxAngle = 20; + var range = maxAngle - minAngle; + var t1 = parseInt((Math.random() * range) + minAngle, 10); + var t2 = parseInt((Math.random() * range) + minAngle, 10); + var t3 = parseInt((Math.random() * range) + minAngle, 10); + this.setAngles(t1,t2,t3); + }.bind(this); + + if (!this._dancer_interval) { + this._dancer_interval = setInterval(_dance, 250); + } +}; + +method.stopDancing = function() { + if (this._dancer_interval) { + clearInterval(this._dancer_interval); + this._dancer_interval = null; + } +}; + +method.getCalibrationData = function() { + return this._calibration; +}; + +method.setCalibrationData = function(newData) { + this._calibration = newData; +}; + +module.exports = {}; +module.exports.Robot = Robot; \ No newline at end of file diff --git a/software/src/lib/server/robot_http_client.js b/software/src/lib/server/robot_http_client.js new file mode 100644 index 0000000..a7b5982 --- /dev/null +++ b/software/src/lib/server/robot_http_client.js @@ -0,0 +1,67 @@ +var http = require('http'); + +exports.client = function(address, port) { + var get = function(path, cb) { + return http.get({ host: address, port: port, path: path }, function(res){ + res.setEncoding('utf8'); + return res.on('data', function(chunk) { + var result = JSON.parse(chunk); + return cb(result.data); + }); + }).on("error", function(err){ + console.log("Got error: " + err.message); + return cb(null, err); + }); + }; + var post = function(path, bodyData, cb) { + var req = http.request({ host: address, port: port, path: path, method: 'POST'}, function(res) { + res.setEncoding('utf8'); + return res.on('data', function (chunk) { + var result = JSON.parse(chunk); + return cb(result.data); + }); + }).on('error', function(err) { + console.log("Got error: " + err.message); + return cb(null, err); + }); + req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); + req.write(bodyData); + req.write('\n'); + req.end(); + }; + return { + address : address, + port : port, + url : function(uri) { + return 'http://' + address + ":" + port + uri; + }, + angles : function(cb) { + return get('/angles', cb); + }, + setAngles : function(theta1, theta2, theta3, cb) { + var postData = "theta1=" + theta1 + "&theta2=" +theta2 + "&theta3=" + theta3; + return post('/setAngles', postData, cb); + }, + position : function(cb) { + return get('/position', cb); + }, + setPosition : function(x, y, z, cb) { + var postData = "x=" + x + "&y=" + y + "&z=" + z; + return post('/setPosition', postData, cb); + }, + tap : function(x, y, cb) { + var postData = "x=" + x + "&y=" + y; + return post('/tap', postData, cb); + }, + reset : function(cb) { + return post('/reset', '', cb); + }, + calibrationData : function(cb) { + return get('/calibrationData', cb); + }, + setCalibrationData : function(newData, cb) { + var postData = "newData=" + JSON.stringify(newData); + return post('/setCalibrationData', postData, cb); + } + }; +}; \ No newline at end of file diff --git a/software/src/server.js b/software/src/server.js new file mode 100755 index 0000000..ab9a82f --- /dev/null +++ b/software/src/server.js @@ -0,0 +1,232 @@ +#! /usr/local/bin/node + +var parser = require("./lib/server/parser") + , Hapi = require("hapi") + , path = require("path") + , five = require("johnny-five") + , calibration = require("./lib/server/calibration") + , Robot = require("./lib/server/robot").Robot; + +var args = parser.parseArgs(); +var robot, servo1, servo2, servo3; + +var board = new five.Board({ debug: false}); +board.on("ready", function() { + servo1 = five.Servo({ + pin: 9, + range: [0,120] + }); + + servo2 = five.Servo({ + pin: 10, + range: [0,120] + }); + + servo3 = five.Servo({ + pin: 11, + range: [0,120] + }); + + servo1.on("error", function() { + console.log(arguments); + }); + servo2.on("error", function() { + console.log(arguments); + }); + servo3.on("error", function() { + console.log(arguments); + }); + + // Initialize Objects + var calibrationData = calibration.getDataFromFilePath(args.calibration); + calibrationData = calibrationData == null ? calibration.defaultData : calibrationData; + robot = new Robot(servo1,servo2,servo3,calibrationData); + + // Move to starting point + robot.resetPosition(); + + // create a server with a host and port + var server = new Hapi.Server(); + server.connection({ + host: args.address, + port: args.port + }); + + var getCommonReponseObject = function(err, data) { + if (err) { + return { status:err.code, data: err }; + } else { + return { status: 0, data: data }; + } + }; + + server.route({ + method: 'GET', + path:'/status', + handler: function (request, reply) { + console.log("GET " + request.path + ": "); + reply(getCommonReponseObject(null, '"OK"')); + } + }); + + server.route({ + method: 'POST', + path:'/reset', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + robot.resetPosition(); + reply(getCommonReponseObject(null, robot.getAngles())); + } + }); + + server.route({ + method: 'POST', + path:'/dance', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + robot.startDancing(); + reply(getCommonReponseObject(null, '"Dancing!"')); + } + }); + + server.route({ + method: 'POST', + path:'/stopDancing', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + robot.stopDancing(); + reply(getCommonReponseObject(null, '"No more dancing."')); + } + }); + + server.route({ + method: 'POST', + path:'/setAngles', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var theta1 = parseFloat(request.payload.theta1); + var theta2 = parseFloat(request.payload.theta2); + var theta3 = parseFloat(request.payload.theta3); + robot.setAngles(theta1, theta2, theta3); + return reply(getCommonReponseObject(null, robot.getAngles())); + } + }); + + server.route({ + method: 'POST', + path:'/setPosition', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var x = parseFloat(request.payload.x); + var y = parseFloat(request.payload.y); + var z = parseFloat(request.payload.z); + robot.setPosition(x, y, z); + return reply(getCommonReponseObject(null, '"OK"')); + } + }); + + server.route({ + method: 'GET', + path:'/angles', + handler: function (request, reply) { + console.log("GET " + request.path + ": "); + return reply(getCommonReponseObject(null, robot.getAngles())); + } + }); + + server.route({ + method: 'GET', + path:'/position', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + return reply(getCommonReponseObject(null, robot.getPosition())); + } + }); + + server.route({ + method: 'GET', + path:'/anglesForPosition/x/{x}/y/{y}/z/{z}', + handler: function (request, reply) { + console.log("GET " + request.path + ": "); + var x = parseFloat(request.params.x); + var y = parseFloat(request.params.y); + var z = parseFloat(request.params.z); + return reply(getCommonReponseObject(null,robot.getAnglesForPosition(x,y,z))); + } + }); + + server.route({ + method: 'GET', + path:'/positionForScreenCoordinates/x/{x}/y/{y}', + handler: function (request, reply) { + console.log("GET " + request.path + ": "); + var x = parseFloat(request.params.x); + var y = parseFloat(request.params.y); + return reply(getCommonReponseObject(null,robot.getPositionForScreenCoordinates(x,y))); + } + }); + + server.route({ + method: 'POST', + path:'/tap', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var x = parseFloat(request.payload.x); + var y = parseFloat(request.payload.y); + return robot.tap(x,y,function() { + return reply(getCommonReponseObject(null, '"OK"')); + }); + } + }); + + server.route({ + method: 'POST', + path:'/swipe', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var startX = parseFloat(request.payload.startX); + var startY = parseFloat(request.payload.startY); + var endX = parseFloat(request.payload.endX); + var endY = parseFloat(request.payload.endY); + return robot.swipe(startX,startY,endX,endY,function() { + return reply(getCommonReponseObject(null, '"OK"')); + }); + } + }); + + server.route({ + method: 'POST', + path:'/sendKeys', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var keys = decodeURIComponent(request.payload.keys); + return robot.sendKeys(keys, function() { + return reply(getCommonReponseObject(null, '"OK"')); + }); + } + }); + + server.route({ + method: 'GET', + path:'/calibrationData', + handler: function (request, reply) { + console.log("GET " + request.path + ": "); + return reply(getCommonReponseObject(null, robot.getCalibrationData())); + } + }); + + server.route({ + method: 'POST', + path:'/setCalibrationData', + handler: function (request, reply) { + console.log("POST " + request.path + ": "); + var newData = JSON.parse(request.payload.newData); + robot.setCalibrationData(newData); + return reply(getCommonReponseObject(null, robot.getCalibrationData())); + } + }); + + server.start(); + console.log("Robot listening on port " + args.port); + +});