Merge pull request #32 from hybridgroup/literate-coffeescript

Literate CoffeeScript
This commit is contained in:
Ron Evans 2013-11-15 13:07:28 -08:00
commit b2900c3d24
7 changed files with 451 additions and 156 deletions

View File

@ -36,7 +36,7 @@ module.exports = function (grunt) {
files: [{
expand: true,
cwd: '<%= config.src %>',
src: '{,*/}*.coffee',
src: '{,*/}*.{coffee,litcoffee}',
dest: '<%= config.dist %>',
ext: '.js'
}]

70
dist/api.js vendored
View File

@ -1,20 +1,12 @@
/*
* api
* cylonjs.com
*
* Copyright (c) 2013 The Hybrid Group
* Licensed under the Apache 2.0 license.
*/
(function() {
'use strict';
var express, namespace;
express = require('express.io');
namespace = require('node-namespace');
namespace("Api", function() {
namespace('Api', function() {
return this.Server = (function() {
var master;
@ -55,34 +47,36 @@
return _results;
})());
});
this.server.get("/robots/:robotid", function(req, res) {
return master.findRobot(req.params.robotid, function(err, robot) {
this.server.get("/robots/:robotname", function(req, res) {
return master.findRobot(req.params.robotname, function(err, robot) {
return res.json(err ? err : robot.data());
});
});
this.server.get("/robots/:robotid/devices", function(req, res) {
return master.findRobot(req.params.robotid, function(err, robot) {
this.server.get("/robots/:robotname/devices", function(req, res) {
return master.findRobot(req.params.robotname, function(err, robot) {
return res.json(err ? err : robot.data().devices);
});
});
this.server.get("/robots/:robotid/devices/:deviceid", function(req, res) {
var deviceid, robotid, _ref;
_ref = [req.params.robotid, req.params.deviceid], robotid = _ref[0], deviceid = _ref[1];
return master.findRobotDevice(robotid, deviceid, function(err, device) {
this.server.get("/robots/:robotname/devices/:devicename", function(req, res) {
var devicename, params, robotname;
params = [req.params.robotname, req.params.devicename];
robotname = params[0], devicename = params[1];
return master.findRobotDevice(robotname, devicename, function(err, device) {
return res.json(err ? err : device.data());
});
});
this.server.get("/robots/:robotid/devices/:deviceid/commands", function(req, res) {
var deviceid, robotid, _ref;
_ref = [req.params.robotid, req.params.deviceid], robotid = _ref[0], deviceid = _ref[1];
return master.findRobotDevice(robotid, deviceid, function(err, device) {
this.server.get("/robots/:robotname/devices/:devicename/commands", function(req, res) {
var devicename, params, robotname;
params = [req.params.robotname, req.params.devicename];
robotname = params[0], devicename = params[1];
return master.findRobotDevice(robotname, devicename, function(err, device) {
return res.json(err ? err : device.data().commands);
});
});
this.server.all("/robots/:robot/devices/:device/commands/:command", function(req, res) {
var commandid, deviceid, key, params, robotid, value, _ref;
params = [req.params.robot, req.params.device, req.params.command];
robotid = params[0], deviceid = params[1], commandid = params[2];
this.server.all("/robots/:robot/devices/:device/commands/:commandname", function(req, res) {
var commandname, devicename, key, params, robotname, value, _ref;
params = [req.params.robot, req.params.device, req.params.commandname];
robotname = params[0], devicename = params[1], commandname = params[2];
params = [];
if (typeof req.body === 'object') {
_ref = req.body;
@ -91,36 +85,38 @@
params.push(value);
}
}
return master.findRobotDevice(robotid, deviceid, function(err, device) {
return master.findRobotDevice(robotname, devicename, function(err, device) {
var result;
if (err) {
return res.json(err);
}
result = device[commandid].apply(device, params);
result = device[commandname].apply(device, params);
return res.json({
result: result
});
});
});
this.server.get("/robots/:robotid/connections", function(req, res) {
return master.findRobot(req.params.robotid, function(err, robot) {
this.server.get("/robots/:robotname/connections", function(req, res) {
return master.findRobot(req.params.robotname, function(err, robot) {
return res.json(err ? err : robot.data().connections);
});
});
this.server.get("/robots/:robot/connections/:connection", function(req, res) {
var connectionid, robotid, _ref;
_ref = [req.params.robot, req.params.connection], robotid = _ref[0], connectionid = _ref[1];
return master.findRobotConnection(robotid, connectionid, function(err, connection) {
var connectionname, params, robotname;
params = [req.params.robot, req.params.connection];
robotname = params[0], connectionname = params[1];
return master.findRobotConnection(robotname, connectionname, function(err, connection) {
return res.json(err ? err : connection.data());
});
});
this.server.get("/robots/:robotid/devices/:deviceid/events", function(req, res) {
this.server.get("/robots/:robotname/devices/:devicename/events", function(req, res) {
return req.io.route('events');
});
return this.server.io.route('events', function(req) {
var deviceid, robotid, _ref;
_ref = [req.params.robotid, req.params.deviceid], robotid = _ref[0], deviceid = _ref[1];
return master.findRobotDevice(robotid, deviceid, function(err, device) {
var devicename, params, robotname;
params = [req.params.robotname, req.params.devicename];
robotname = params[0], devicename = params[1];
return master.findRobotDevice(robotname, devicename, function(err, device) {
if (err) {
req.io.respond(err);
}

View File

@ -1,27 +0,0 @@
Cylon = require '..'
bots = [
{ port: '/dev/rfcomm0', name: 'Thelma' },
{ port: '/dev/rfcomm1', name: 'Louise' }
]
class SpheroRobot
connection:
name: 'Sphero', adaptor: 'sphero'
device:
name: 'sphero', driver: 'sphero'
work: (me) ->
every 1.seconds(), ->
Logger.info me.name
me.sphero.setRGB Math.floor(Math.random() * 100000)
me.sphero.roll 60, Math.floor(Math.random() * 360)
for bot in bots
robot = new SpheroRobot
robot.connection.port = bot.port
robot.name = bot.name
Cylon.robot robot
Cylon.start()

View File

@ -0,0 +1,67 @@
# Multiple Spheros
Let's do an example of Cylon controlling multiple Spheros at the same time. The
Spheros will both randomly roll around and change their colors every second.
Before we run this example, make sure to have `cylon-sphero` installed (`npm
install cylon-sphero`)
First, load up Cylon. Since we're in the Cylon repo, we can use the version
already here:
Cylon = require '..'
Since both of our Spheros are going to have faily similar behaviour, we can
define a class to hold their attributes:
class SpheroRobot
We'll define the Sphero's connection sans port, which we'll add later.
connection:
name: 'Sphero', adaptor: 'sphero'
Both Spheros have the same device configuration
device:
name: 'sphero', driver: 'sphero'
Now we can define the work for the Spheros. Every second, they'll print their
name, change to a random color, and roll in a random direction.
work: (my) ->
every 1.seconds(), ->
Logger.info my.name
my.sphero.setRandomColor()
my.sphero.roll 60, Math.floor(Math.random() * 360)
Next up, let's define what's different about our bots so we can tell them apart
later on. We'll give them each a different name, and define their connection
ports.
bots = [
{ name: "Thelma", port: "/dev/rfcomm0" },
{ name: "Louise", port: "/dev/rfcomm1" }
]
Now that the pieces are in place, we can start making our robots.
for bot in bots
We'll make a new instance of `SpheroRobot`, and modify it.
robot = new SpheroRobot
Now we'll tell the robot what it's name is, and what it's connection port should
be:
robot.name = bot.name
robot.connection.port = bot.port
Now our robot's ready, and we can add it to Cylon's collection of robots.
Cylon.robot robot
And now that our robots have all been set up, we can start them all at once:
Cylon.start()

116
examples/travis.litcoffee Normal file
View File

@ -0,0 +1,116 @@
# Cylon and Travis
For this Cylon example, we're going to check on a Travis build every ten
seconds, and change the color of a Sphero depending on the result.
Before you run this, make sure you install the following dependencies:
- **travis-ci** (npm install travis-ci)
- **cylon-sphero** (npm install cylon-sphero)
First of all, let's load up Cylon. We're going to load the version directly from
the repo, since we're here already:
Cylon = require '..'
Next, we'll set up Travis. We're going to be using the very useful [travis-ci][]
module.
[travis-ci]: https://github.com/pwmckenna/node-travis-ci
Travis = require 'travis-ci'
Now that we've got our Travis module imported, let's set it up:
travis = new Travis
version: '2.0.0'
Now we have a working interface to the Travis-CI API. Let's set up a username
and repo to query Travis about later, as long as we're here. Feel free to change
these if you want to try with your own repositories.
user = "hybridgroup"
name = "cylon"
## Robot
And with that last bit of setup done, let's start setting up our robot!
Cylon.robot
We use a connection to tell Cylon what port it can use to communicate with our
Sphero, along with what adaptor it should require (`cylon-sphero`) to connect to
it. We give it a name to make it easier to reference later on.
connection:
name: 'sphero', adaptor: 'sphero', port: '/dev/rfcomm0'
Devices are set up in a similar fashion, but allow us to directly issue commands
to the sphero. These are added to the robot's namespace directly to make them
easy to access.
device:
name: 'sphero', driver: 'sphero'
Now that we've told our robot what hardware it has access to, we can start
telling it what it should do. The work function passes along one argument,
a reference to the robot so we can access it's state and hardware.
work: (me) ->
We'll define a function to check Travis and change the Sphero's color depending
on the state of the last build.
checkTravis = ->
First, it will log that it's checking Travis to the logger:
Logger.info "Checking last build status for #{user}/#{name}"
Let's set the default color of the Sphero to blue until we know what the build
status is:
me.sphero.setColor 'blue', true
Now we'll fetch the Travis build status:
travis.repos {
owner_name: user
name: name
}, (err, res) ->
If we were returned a response, we'll check the status of the build and use that
to determine what color we should make the Sphero.
if res.repo
switch res.repo.last_build_state
When the build state is passed, then we'll set the Sphero's color to green:
when 'passed' then me.sphero.setColor 'green', true
And if the build has failed, let's set the Sphero's color to red:
when 'failed' then me.sphero.setColor 'red', true
Otherwise, we'll just set it to blue:
else me.sphero.setColor 'blue', true
And if we didn't get a response from the server, let's just set it to blue.
else me.sphero.setColor 'blue', true
Now that we've got that function defined, let's call it to set the initial color
of the Sphero:
do checkTravis
And every ten seconds, let's keep checking Travis:
every 10.seconds(), ->
do checkTravis
And now that we've got our work defined, let's start the robot!
.start()

View File

@ -1,91 +0,0 @@
###
* api
* cylonjs.com
*
* Copyright (c) 2013 The Hybrid Group
* Licensed under the Apache 2.0 license.
###
express = require 'express.io'
namespace = require 'node-namespace'
namespace "Api", ->
# The Cylon API Server provides an interface to communicate with master class
# and retrieve information about the robots being controlled.
class @Server
master = null
constructor: (opts = {}) ->
@host = opts.host || "127.0.0.1"
@port = opts.port || "3000"
master = opts.master
@server = express().http().io()
@server.set 'name', 'Cylon API Server'
@server.use(express.bodyParser())
@server.get "/*", (req, res, next) ->
res.set 'Content-Type', 'application/json'
do next
do @configureRoutes
@server.listen @port, @host, =>
Logger.info "#{@server.name} is listening at #{@host}:#{@port}"
configureRoutes: ->
@server.get "/robots", (req, res) ->
res.json (robot.data() for robot in master.robots())
@server.get "/robots/:robotid", (req, res) ->
master.findRobot req.params.robotid, (err, robot) ->
res.json if err then err else robot.data()
@server.get "/robots/:robotid/devices", (req, res) ->
master.findRobot req.params.robotid, (err, robot) ->
res.json if err then err else robot.data().devices
@server.get "/robots/:robotid/devices/:deviceid", (req, res) ->
[robotid, deviceid] = [req.params.robotid, req.params.deviceid]
master.findRobotDevice robotid, deviceid, (err, device) ->
res.json if err then err else device.data()
@server.get "/robots/:robotid/devices/:deviceid/commands", (req, res) ->
[robotid, deviceid] = [req.params.robotid, req.params.deviceid]
master.findRobotDevice robotid, deviceid, (err, device) ->
res.json if err then err else device.data().commands
@server.all "/robots/:robot/devices/:device/commands/:command", (req, res) ->
params = [req.params.robot, req.params.device, req.params.command]
[robotid, deviceid, commandid] = params
params = []
if typeof req.body is 'object'
params.push(value) for key, value of req.body
master.findRobotDevice robotid, deviceid, (err, device) ->
if err then return res.json err
result = device[commandid](params...)
res.json result: result
@server.get "/robots/:robotid/connections", (req, res) ->
master.findRobot req.params.robotid, (err, robot) ->
res.json if err then err else robot.data().connections
@server.get "/robots/:robot/connections/:connection", (req, res) ->
[robotid, connectionid] = [req.params.robot, req.params.connection]
master.findRobotConnection robotid, connectionid, (err, connection) ->
res.json if err then err else connection.data()
@server.get "/robots/:robotid/devices/:deviceid/events", (req, res) ->
req.io.route 'events'
@server.io.route 'events', (req) ->
[robotid, deviceid] = [req.params.robotid, req.params.deviceid]
master.findRobotDevice robotid, deviceid, (err, device) ->
req.io.respond(err) if err
device.on 'update', (data) ->
req.io.emit 'update', { data: data }

234
src/api.litcoffee Normal file
View File

@ -0,0 +1,234 @@
# API
The Cylon API provides an interface for retreiving information and communicating
with the currently running robots.
First of all, let's make sure we're running in ECMAScript 5's [strict mode][].
[strict mode]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode
'use strict';
## Dependencies
Our server needs to primarily respond to HTTP requests with JSON responses, but
also needs to handle a WebSocket connection to listen for events. To accomodate
both of these requirements, we're going to use [express.io][].
[express.io]: http://express-io.org
express = require 'express.io'
`express.io` integrates some [socket.io][] functionality on top of the popular
[express][] framework for Node. This lets us use the same server to handle both
types of connections, and also use Express-style routing to resolve to
a Socket.IO connection.
[socket.io]: http://socket.io
[express]: http://expressjs.com
To keep in line with the rest of Cylon, we'll also be namespacing our API server
with the [node-namespace][] module.
[node-namespace]: https://github.com/kaero/node-namespace
namespace = require 'node-namespace'
## Namespacing
First, let's namespace our server class so it's available to other parts of
Cylon:
namespace 'Api', ->
class @Server
Now from another module, you can just `require('./api')` and you automatically
have access to the API server via `Api.Server`.
## Master
We need to hold a class-wide reference to the Cylon instance that's started our
server instance, so we can inspect and send messages to all the robots it's
controlling. For now, we can set it to null, it will be changed later in the
constructor.
master = null
## Constructor
The server constructor accepts an arguments object, `opts`. This
defines options to be used when creating the server, as follows:
- **master** - required, reference to Cylon.Master instance that's creating the
Server instance
- **host** - optional, string IP address that the server should host content
from
- **port** - optional, string port number the server should listen for requests
on
constructor: (opts = {}) ->
@host = opts.host || "127.0.0.1"
@port = opts.port || "3000"
master = opts.master
Once we have this information, we can create our server:
@server = express().http().io()
Give it a name:
@server.set 'name', 'Cylon API Server'
And tell it to use the `express.bodyParser()` mixin, which will parse body
params for POST requests:
@server.use express.bodyParser()
We need to define a catch-all route here, that will specify that all our
responses will be JSON. Otherwise, Express will automatically send everything in
text/plain, possibly confusing clients.
@server.get "/*", (req, res, next) ->
res.set 'Content-Type', 'application/json'
do next
Next up, we run a function to define all our routes (you'll see it in a minute)
do @configureRoutes
And finally, we start our server, announcing so via the Logger.
@server.listen @port, @host, =>
Logger.info "#{@server.name} is listening at #{@host}:#{@port}"
## Routes
We'll use the previously referenced `@configureRoutes` function to define our
server's routes.
configureRoutes: ->
### GET /robots
Our first route returns all the Robots the master class knows about in JSON
format.
@server.get "/robots", (req, res) ->
res.json (robot.data() for robot in master.robots())
### GET /robots/:robotname
Given a robot's name, returns JSON information about the requested Robot:
@server.get "/robots/:robotname", (req, res) ->
master.findRobot req.params.robotname, (err, robot) ->
res.json if err then err else robot.data()
### GET /robots/:robotname/devices
Given a robot's name, returns JSON information about the devices belonging to
the requested Robot:
@server.get "/robots/:robotname/devices", (req, res) ->
master.findRobot req.params.robotname, (err, robot) ->
res.json if err then err else robot.data().devices
### GET /robots/:robotname/devices/:devicename
Given the names of a device and the robot it belongs to, returns data on the
specified device.
@server.get "/robots/:robotname/devices/:devicename", (req, res) ->
params = [req.params.robotname, req.params.devicename]
[robotname, devicename] = params
master.findRobotDevice robotname, devicename, (err, device) ->
res.json if err then err else device.data()
### GET /robots/:robotname/devices/:devicename/commands
Given the names of a device and the robot it belongs to, returns all commands
available for the specified device.
@server.get "/robots/:robotname/devices/:devicename/commands", (req, res) ->
params = [req.params.robotname, req.params.devicename]
[robotname, devicename] = params
master.findRobotDevice robotname, devicename, (err, device) ->
res.json if err then err else device.data().commands
### ALL /robots/:robotname/devices/:devicename/commands/:commandname
Given a robot name, device name, and command name, executes a robot's command
and returns the result.
@server.all "/robots/:robot/devices/:device/commands/:commandname", (req, res) ->
params = [
req.params.robot,
req.params.device,
req.params.commandname
]
[robotname, devicename, commandname] = params
This parses params from the request body into values that can be used while
calling the command, if params have been supplied.
params = []
if typeof req.body is 'object'
params.push(value) for key, value of req.body
Runs the command on the Robot's device, passing in params as provided.
master.findRobotDevice robotname, devicename, (err, device) ->
if err then return res.json err
result = device[commandname](params...)
res.json result: result
### GET /robots/:robotname/connections
Given a robot's name, returns JSON information about the connections belonging
to the requested Robot:
@server.get "/robots/:robotname/connections", (req, res) ->
master.findRobot req.params.robotname, (err, robot) ->
res.json if err then err else robot.data().connections
### GET /robots/:robotname/connections/:connectionname
Given a robot's name, returns JSON information about the connections belonging
to the requested Robot:
@server.get "/robots/:robot/connections/:connection", (req, res) ->
params = [req.params.robot, req.params.connection]
[robotname, connectionname] = params
master.findRobotConnection robotname, connectionname, (err, connection) ->
res.json if err then err else connection.data()
### GET /robots/:robotname/devices/:devicename/events
Routes to a Socket.IO route to handle WebSockets connections requesting updates
on device events.
@server.get "/robots/:robotname/devices/:devicename/events", (req, res) ->
req.io.route 'events'
### WS_GET /events
A Socket.IO route to handle updating clients whenever a device sends
an 'update' event.
Listens for the 'update' event on a particular Robot's device, and whenever the
device sends the 'update' event, passes the data along to the client.
@server.io.route 'events', (req) ->
params = [req.params.robotname, req.params.devicename]
[robotname, devicename] = params
master.findRobotDevice robotname, devicename, (err, device) ->
req.io.respond(err) if err
device.on 'update', (data) ->
req.io.emit 'update', { data: data }