QL code and tests for C#/C++/JavaScript.

This commit is contained in:
Pavel Avgustinov
2018-08-02 17:53:23 +01:00
commit b55526aa58
10684 changed files with 581163 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
// semmle-extractor-options: --platform
// semmle-extractor-options: node

View File

@@ -0,0 +1,4 @@
import javascript
from Folder d
select d.getRelativePath(), count(File f | f = d.getAFile() and f.getExtension() = "js")

View File

@@ -0,0 +1 @@
| tst.js:31:1:33:1 | functio ... ++i);\\n} | This function uses i like a local variable. |

View File

@@ -0,0 +1,6 @@
import javascript
from Function f, GlobalVariable gv
where gv.getAnAccess().getEnclosingFunction() = f and
not f.getStartBB().isLiveAtEntry(gv, _)
select f, "This function uses " + gv + " like a local variable."

View File

@@ -0,0 +1,4 @@
| tst.js:21:12:21:12 | x | Dead store of local variable. |
| tst.js:31:12:31:12 | x | Dead store of local variable. |
| tst.js:31:15:31:15 | y | Dead store of local variable. |
| tst.js:31:18:31:18 | x | Dead store of local variable. |

View File

@@ -0,0 +1,6 @@
import javascript
from VarDef def, LocalVariable v
where v = def.getAVariable() and
not exists (VarUse use | def = use.getADef())
select def, "Dead store of local variable."

View File

@@ -0,0 +1,8 @@
import javascript
from SimpleParameter res, DataFlow::Node resNode, MethodCallExpr send
where res.getName() = "res" and
resNode = DataFlow::parameterNode(res) and
resNode.getASuccessor+() = DataFlow::valueNode(send.getReceiver()) and
send.getMethodName() = "send"
select send

View File

@@ -0,0 +1,7 @@
import javascript
from StrictEqualityTest eq, DataFlow::AnalyzedNode nd, NullLiteral null
where eq.hasOperands(nd.asExpr(), null) and
not nd.getAValue().isIndefinite(_) and
not nd.getAValue() instanceof AbstractNull
select eq, "Spurious null check."

View File

@@ -0,0 +1 @@
| tst.js:9:1:9:3 | h() | Unable to find a callee for this call site. |

View File

@@ -0,0 +1,6 @@
import javascript
from CallSite cs
where not cs.isIncomplete() and
not exists(cs.getACallee())
select cs, "Unable to find a callee for this call site."

View File

@@ -0,0 +1,12 @@
import javascript
class TrackedStringLiteral extends DataFlow::TrackedNode {
TrackedStringLiteral() {
this.asExpr() instanceof ConstantString
}
}
from TrackedStringLiteral source, DataFlow::Node sink, SsaExplicitDefinition def
where source.flowsTo(sink) and sink = DataFlow::ssaDefinitionNode(def) and
def.getSourceVariable().getName().toLowerCase() = "password"
select sink

View File

@@ -0,0 +1,28 @@
import javascript
class PasswordTracker extends DataFlow::Configuration {
PasswordTracker() {
// unique identifier for this configuration
this = "PasswordTracker"
}
override predicate isSource(DataFlow::Node nd) {
nd.asExpr() instanceof StringLiteral
}
override predicate isSink(DataFlow::Node nd) {
passwordVarAssign(_, nd)
}
predicate passwordVarAssign(Variable v, DataFlow::Node nd) {
exists (SsaExplicitDefinition def |
nd = DataFlow::ssaDefinitionNode(def) and
def.getSourceVariable() = v and
v.getName().toLowerCase() = "password"
)
}
}
from PasswordTracker pt, DataFlow::Node source, DataFlow::Node sink, Variable v
where pt.hasFlow(source, sink) and pt.passwordVarAssign(v, sink)
select sink, "Password variable " + v + " is assigned a constant string."

View File

@@ -0,0 +1 @@
| m.js:1:1:3:0 | <toplevel> | 0 |

View File

@@ -0,0 +1,4 @@
import javascript
from NodeModule m
select m, count(m.getAnImportedModule())

View File

@@ -0,0 +1,7 @@
import javascript
from NPMPackage pkg, PackageDependencies deps, string name
where deps = pkg.getPackageJSON().getDependencies() and
deps.getADependency(name, _) and
not exists (Require req | req.getTopLevel() = pkg.getAModule() | name = req.getImportedPath().getValue())
select deps, "Unused dependency '" + name + "'."

View File

@@ -0,0 +1 @@
| tst.js:25:3:25:3 | , | Omitted array elements are bad style. |

View File

@@ -0,0 +1,12 @@
import javascript
class CommaToken extends PunctuatorToken {
CommaToken() {
getValue() = ","
}
}
from CommaToken comma
where comma.getNextToken() instanceof CommaToken
select comma, "Omitted array elements are bad style."

View File

@@ -0,0 +1,5 @@
import javascript
from SQL::SqlString ss
where ss instanceof AddExpr
select ss, "Use templating instead of string concatenation."

View File

@@ -0,0 +1 @@
| tst.js:19:4:19:9 | @param | @param tag is missing name. |

View File

@@ -0,0 +1,6 @@
import javascript
from JSDocTag t
where t.getTitle() = "param" and
not exists(t.getName())
select t, "@param tag is missing name."

View File

@@ -0,0 +1 @@
| tst.js:27:1:27:4 | <!-- | Do not use HTML comments. |

View File

@@ -0,0 +1,4 @@
import javascript
from HtmlLineComment c
select c, "Do not use HTML comments."

View File

@@ -0,0 +1 @@
| tst.js:29:1:29:5 | 1 + 2 | This expression should be bracketed to clarify precedence rules. |

View File

@@ -0,0 +1,5 @@
import javascript
from ShiftExpr shift, AddExpr add
where add = shift.getAnOperand()
select add, "This expression should be bracketed to clarify precedence rules."

View File

@@ -0,0 +1,5 @@
import javascript
from FunctionExpr fe
where fe.getBody() instanceof Expr
select fe, "Use arrow expressions instead of expression closures."

View File

@@ -0,0 +1 @@
| tst.js:31:1:33:1 | functio ... ++i);\\n} | This function has two parameters that bind the same variable. |

View File

@@ -0,0 +1,8 @@
import javascript
from Function fun, Parameter p, Parameter q, int i, int j
where p = fun.getParameter(i) and
q = fun.getParameter(j) and
i < j and
p.getAVariable() = q.getAVariable()
select fun, "This function has two parameters that bind the same variable."

View File

@@ -0,0 +1 @@
| tst.js:35:1:35:9 | var j, j; | Variable j is declared both $@ and $@. | tst.js:35:5:35:5 | j | here | tst.js:35:8:35:8 | j | here |

View File

@@ -0,0 +1,10 @@
import javascript
from DeclStmt ds, VariableDeclarator d1, VariableDeclarator d2, Variable v, int i, int j
where d1 = ds.getDecl(i) and
d2 = ds.getDecl(j) and
i < j and
v = d1.getBindingPattern().getAVariable() and
v = d2.getBindingPattern().getAVariable() and
not ds.getTopLevel().isMinified()
select ds, "Variable " + v.getName() + " is declared both $@ and $@.", d1, "here", d2, "here"

View File

@@ -0,0 +1 @@
| tst.js:1:2:1:24 | { x: 23 ... x: 56 } | Property x is defined both $@ and $@. | tst.js:1:4:1:8 | x: 23 | here | tst.js:1:18:1:22 | x: 56 | here |

View File

@@ -0,0 +1,9 @@
import javascript
from ObjectExpr oe, Property p1, Property p2, int i, int j
where p1 = oe.getProperty(i) and
p2 = oe.getProperty(j) and
i < j and
p1.getName() = p2.getName() and
not oe.getTopLevel().isMinified()
select oe, "Property " + p1.getName() + " is defined both $@ and $@.", p1, "here", p2, "here"

View File

@@ -0,0 +1,2 @@
| tst.js:3:1:3:15 | function f() {} | tst.js:6:5:6:19 | function f() {} |
| tst.js:6:5:6:19 | function f() {} | tst.js:3:1:3:15 | function f() {} |

View File

@@ -0,0 +1,7 @@
import javascript
from FunctionDeclStmt f, FunctionDeclStmt g
where f != g and f.getVariable() = g.getVariable() and
not f.getTopLevel().isMinified() and
not g.getTopLevel().isMinified()
select f, g

View File

@@ -0,0 +1,35 @@
({ x: 23, y: 42, x: 56 })
function f() {}
function g() {}
{
function f() {}
}
h();
goog.require('foo');
goog.require('foo');
/*global imm, mut:true*/
imm = 23;
mut = 42;
/**
* @param
*/
function k(x) {}
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[1,,3]
<!--
1 + 2 << 3
function l(x, y, x) {
for (i=0;i<10;++i);
}
var j, j;

View File

@@ -0,0 +1 @@
Example queries from the tutorials; if these have to be fixed, fix the tutorials as well.

View File

@@ -0,0 +1,8 @@
/**
* Some commonly used HTTP verbs.
*/
string httpVerb() {
result = "get" or result = "put" or
result = "post" or result = "delete"
}

View File

@@ -0,0 +1,7 @@
| robonode/src/app.js:43:13:43:27 | res.status(500) | Response 500 is not specified by $@. | robonode/src/assets/raml/api.raml:9:5:14:2 | description: \| | description: \| |
| robonode/src/app.js:78:17:78:31 | res.status(422) | Response 422 is not specified by $@. | robonode/src/assets/raml/api.raml:30:9:54:4 | description: \| | description: \| |
| robonode/src/app.js:91:21:91:35 | res.status(500) | Response 500 is not specified by $@. | robonode/src/assets/raml/api.raml:30:9:54:4 | description: \| | description: \| |
| robonode/src/app.js:115:13:115:27 | res.status(422) | Response 422 is not specified by $@. | robonode/src/assets/raml/api.raml:30:9:54:4 | description: \| | description: \| |
| robonode/src/app.js:122:9:122:23 | res.status(500) | Response 500 is not specified by $@. | robonode/src/assets/raml/api.raml:30:9:54:4 | description: \| | description: \| |
| robonode/src/app.js:139:13:139:27 | res.status(500) | Response 500 is not specified by $@. | robonode/src/assets/raml/api.raml:56:9:72:14 | description: \| | description: \| |
| robonode/src/app.js:144:13:144:27 | res.status(202) | Response 202 is not specified by $@. | robonode/src/assets/raml/api.raml:56:9:72:14 | description: \| | description: \| |

View File

@@ -0,0 +1,25 @@
/**
* @name Missing status code specification
* @description Using undocumented status codes in REST API methods may confuse client applications.
* @kind problem
* @problem.severity warning
*/
import javascript
import Osprey
import RAML
RAMLMethod getSpecification(OspreyMethod om) {
exists (RAMLResource rr, File f, string rPath |
rr.getLocation().getFile() = f and
f = om.getDefinition().getAPI().getSpecFile() and
rPath = om.getResourcePath() and
rr.getPath() = rPath.regexpReplaceAll("/:([^/]+)/", "/{$1}/") and
result = rr.getMethod(om.getVerb())
)
}
from MethodResponseSetStatus mrss, RAMLMethod rm
where rm = getSpecification(mrss.getMethod()) and
not exists(rm.getResponse(mrss.getStatusCode()))
select mrss, "Response " + mrss.getStatusCode() + " is not specified by $@.", rm, rm.toString()

View File

@@ -0,0 +1,123 @@
/* Model of Osprey API implementations. */
import javascript
import HTTP
/** An import of the Osprey module. */
class OspreyImport extends Require {
OspreyImport() {
getImportedPath().getValue() = "osprey"
}
}
/** A variable that holds the Osprey module. */
class Osprey extends Variable {
Osprey() {
getAnAssignedExpr() instanceof OspreyImport
}
}
/** A call to `osprey.create`. */
class OspreyCreateAPICall extends MethodCallExpr {
OspreyCreateAPICall() {
getReceiver().(VarAccess).getVariable() instanceof Osprey and
getMethodName() = "create"
}
/** Get the specification file for the API definition. */
File getSpecFile() {
exists (ObjectExpr oe, PathExpr p |
oe = getArgument(2) and
p = oe.getPropertyByName("ramlFile").getInit() and
result = p.resolve()
)
}
}
/** A variable in which an Osprey API object is stored. */
class OspreyAPI extends Variable {
OspreyAPI() {
getAnAssignedExpr() instanceof OspreyCreateAPICall
}
File getSpecFile() {
result = getAnAssignedExpr().(OspreyCreateAPICall).getSpecFile()
}
}
/** An Osprey REST method definition. */
class OspreyMethodDefinition extends MethodCallExpr {
OspreyMethodDefinition() {
exists (OspreyAPI api | getReceiver() = api.getAnAccess()) and
getMethodName() = httpVerb()
}
/** Get the API to which this method belongs. */
OspreyAPI getAPI() {
getReceiver() = result.getAnAccess()
}
/** Get the verb which this method implements. */
string getVerb() {
result = getMethodName()
}
/** Get the resource path to which this method belongs. */
string getResourcePath() {
result = getArgument(0).(ConstantString).getStringValue()
}
}
/** A callback function bound to a REST method. */
class OspreyMethod extends FunctionExpr {
OspreyMethod() {
exists (OspreyMethodDefinition omd | this = omd.getArgument(1))
}
OspreyMethodDefinition getDefinition() {
this = result.getArgument(1)
}
string getVerb() {
result = getDefinition().getVerb()
}
string getResourcePath() {
result = getDefinition().getResourcePath()
}
}
/** A variable that is bound to a response object. */
class MethodResponse extends Variable {
MethodResponse() {
exists (OspreyMethod m, SimpleParameter res |
res = m.getParameter(1) and
this = res.getVariable()
)
}
OspreyMethod getMethod() {
this = result.getParameter(1).(SimpleParameter).getVariable()
}
}
/** A call that sets the status on a response object. */
class MethodResponseSetStatus extends MethodCallExpr {
MethodResponseSetStatus() {
exists (MethodResponse mr |
getReceiver() = mr.getAnAccess() and
getMethodName() = "status"
)
}
OspreyMethod getMethod() {
exists (MethodResponse mr |
getReceiver() = mr.getAnAccess() and
result = mr.getMethod()
)
}
int getStatusCode() {
result = getArgument(0).getIntValue()
}
}

View File

@@ -0,0 +1,55 @@
/* Model of RAML specifications. */
import javascript
import HTTP
/** A RAML specification. */
class RAMLSpec extends YAMLDocument, YAMLMapping {
RAMLSpec() {
getLocation().getFile().getExtension() = "raml"
}
}
/** A RAML resource specification. */
class RAMLResource extends YAMLMapping {
RAMLResource() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping m, string name |
this = m.lookup(name) and
name.matches("/%")
)
}
/** Get the path of this resource relative to the API root. */
string getPath() {
exists (RAMLSpec spec |
this = spec.lookup(result)
) or
exists (RAMLResource that, string p |
this = that.lookup(p) and
result = that.getPath() + p
)
}
/** Get the method for this resource with the given verb. */
RAMLMethod getMethod(string verb) {
verb = httpVerb() and
result = lookup(verb)
}
}
/** A RAML method specification. */
class RAMLMethod extends YAMLValue {
RAMLMethod() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping obj |
this = obj.lookup(httpVerb())
)
}
/** Get the response specification for the given status code. */
YAMLValue getResponse(int code) {
exists (YAMLMapping obj, string s |
obj = this.(YAMLMapping).lookup("responses") and
result = obj.lookup(s) and
code = s.toInt()
)
}
}

View File

@@ -0,0 +1 @@
| robonode/src/assets/raml/api.raml:2:1:72:14 | title: ... in Mini |

View File

@@ -0,0 +1,11 @@
import javascript
/** A RAML specification. */
class RAMLSpec extends YAMLDocument, YAMLMapping {
RAMLSpec() {
getLocation().getFile().getExtension() = "raml"
}
}
from RAMLSpec s
select s

View File

@@ -0,0 +1,6 @@
WARNING: Unused method getMethod (query2.ql:33,14-23)
WARNING: Unused method getResponse (query2.ql:47,13-24)
| robonode/src/assets/raml/api.raml:8:3:72:14 | get: | /robots |
| robonode/src/assets/raml/api.raml:15:5:72:14 | uriParameters: | /robots/{robotId} |
| robonode/src/assets/raml/api.raml:24:7:54:4 | get: | /robots/{robotId}/commands |
| robonode/src/assets/raml/api.raml:55:7:72:14 | get: | /robots/{robotId}/state |

View File

@@ -0,0 +1,56 @@
import javascript
string httpVerb() {
result = "get" or result = "put" or
result = "post" or result = "delete"
}
/** A RAML specification. */
class RAMLSpec extends YAMLDocument, YAMLMapping {
RAMLSpec() {
getLocation().getFile().getExtension() = "raml"
}
}
/** A RAML resource specification. */
class RAMLResource extends YAMLMapping {
RAMLResource() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping m, string name |
this = m.lookup(name) and
name.matches("/%")
)
}
/** Get the path of this resource relative to the API root. */
string getPath() {
exists (RAMLSpec spec |
this = spec.lookup(result)
) or
exists (RAMLResource that, string p |
this = that.lookup(p) and
result = that.getPath() + p
)
}
/** Get the method for this resource with the given verb. */
RAMLMethod getMethod(string verb) {
verb = httpVerb() and
result = lookup(verb)
}
}
/** A RAML method specification. */
class RAMLMethod extends YAMLValue {
RAMLMethod() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping obj |
this = obj.lookup(httpVerb())
)
}
/** Get the response specification for the given status code. */
YAMLValue getResponse(int code) {
exists (YAMLMapping obj, string s |
obj = this.(YAMLMapping).lookup("responses") and
result = obj.lookup(s) and
code = s.toInt()
)
}
}
from RAMLResource rr
select rr, rr.getPath()

View File

@@ -0,0 +1,2 @@
WARNING: Unused method getMethod (query3.ql:32,14-23)
| 1 |

View File

@@ -0,0 +1,45 @@
import javascript
string httpVerb() {
result = "get" or result = "put" or
result = "post" or result = "delete"
}
/** A RAML specification. */
class RAMLSpec extends YAMLDocument, YAMLMapping {
RAMLSpec() {
getLocation().getFile().getExtension() = "raml"
}
}
/** A RAML resource specification. */
class RAMLResource extends YAMLMapping {
RAMLResource() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping m, string name |
this = m.lookup(name) and
name.matches("/%")
)
}
/** Get the path of this resource relative to the API root. */
string getPath() {
exists (RAMLSpec spec |
this = spec.lookup(result)
) or
exists (RAMLResource that, string p |
this = that.lookup(p) and
result = that.getPath() + p
)
}
/** Get the method for this resource with the given verb. */
RAMLMethod getMethod(string verb) {
verb = httpVerb() and
result = lookup(verb)
}
}
class RAMLMethod extends YAMLValue {
RAMLMethod() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping obj |
this = obj.lookup(httpVerb())
)
}
}
select 1

View File

@@ -0,0 +1,6 @@
WARNING: Unused method getMethod (query4.ql:35,14-23)
WARNING: Unused method getResponse (query4.ql:50,13-24)
| robonode/src/assets/raml/api.raml:9:5:14:2 | description: \| |
| robonode/src/assets/raml/api.raml:25:9:29:6 | description: \| |
| robonode/src/assets/raml/api.raml:30:9:54:4 | description: \| |
| robonode/src/assets/raml/api.raml:56:9:72:14 | description: \| |

View File

@@ -0,0 +1,60 @@
import javascript
string httpVerb() {
result = "get" or result = "put" or
result = "post" or result = "delete"
}
/** A RAML specification. */
class RAMLSpec extends YAMLDocument, YAMLMapping {
RAMLSpec() {
getLocation().getFile().getExtension() = "raml"
}
}
/** A RAML resource specification. */
class RAMLResource extends YAMLMapping {
RAMLResource() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping m, string name |
this = m.lookup(name) and
name.matches("/%")
)
}
/** Get the path of this resource relative to the API root. */
string getPath() {
exists (RAMLSpec spec |
this = spec.lookup(result)
) or
exists (RAMLResource that, string p |
this = that.lookup(p) and
result = that.getPath() + p
)
}
/** Get the method for this resource with the given verb. */
RAMLMethod getMethod(string verb) {
verb = httpVerb() and
result = lookup(verb)
}
}
/** A RAML method specification. */
class RAMLMethod extends YAMLValue {
RAMLMethod() {
getDocument() instanceof RAMLSpec and
exists (YAMLMapping obj |
this = obj.lookup(httpVerb())
)
}
/** Get the response specification for the given status code. */
YAMLValue getResponse(int code) {
exists (YAMLMapping obj, string s |
obj = this.(YAMLMapping).lookup("responses") and
result = obj.lookup(s) and
code = s.toInt()
)
}
}
from RAMLMethod m
select m

View File

@@ -0,0 +1,43 @@
var path = require('path');
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: ['src/**/*.js']
},
express: {
options: {
port: process.env.PORT || 3000,
script: 'src/app.js'
},
development: {
options: {
node_env: 'development'
}
},
test: {
options: {
node_env: 'test',
port: 3001
}
}
},
watch: {
express: {
files: ['src/**/*.js', 'src/assets/raml/**/*.*'],
tasks: ['jshint', 'express:development'],
options: {
spawn: false,
atBegin: true
}
}
}
});
require('load-grunt-tasks')(grunt);
grunt.registerTask('default', ['watch']);
};

View File

@@ -0,0 +1,13 @@
Copyright 2014 (c) MuleSoft, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the specific
language governing permissions and limitations under the License.

View File

@@ -0,0 +1,25 @@
{
"name": "robonode",
"version": "0.1.0",
"private": true,
"dependencies": {
"express": "3.4.4",
"osprey": "0.1.1",
"cors": ">=2.4.1",
"bluetooth-serial-port": ">=1.1",
"xml2js": ">=0.4.4"
},
"devDependencies": {
"grunt": "~0.4.2",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-mocha-test": "~0.8.1",
"mocha": "1.15.1",
"should": "2.1.1",
"grunt-express-server": "~0.4.13",
"load-grunt-tasks": "~0.2.1",
"supertest": "~0.8.2",
"grunt-contrib-jshint": "~0.8.0"
}
}

View File

@@ -0,0 +1,54 @@
// See http://support.robotis.com/en/product/dynamixel2/communication/crc.htm
var crcTable =
[
0x0000, 0x8005, 0x800F, 0x000A, 0x801B, 0x001E, 0x0014, 0x8011,
0x8033, 0x0036, 0x003C, 0x8039, 0x0028, 0x802D, 0x8027, 0x0022,
0x8063, 0x0066, 0x006C, 0x8069, 0x0078, 0x807D, 0x8077, 0x0072,
0x0050, 0x8055, 0x805F, 0x005A, 0x804B, 0x004E, 0x0044, 0x8041,
0x80C3, 0x00C6, 0x00CC, 0x80C9, 0x00D8, 0x80DD, 0x80D7, 0x00D2,
0x00F0, 0x80F5, 0x80FF, 0x00FA, 0x80EB, 0x00EE, 0x00E4, 0x80E1,
0x00A0, 0x80A5, 0x80AF, 0x00AA, 0x80BB, 0x00BE, 0x00B4, 0x80B1,
0x8093, 0x0096, 0x009C, 0x8099, 0x0088, 0x808D, 0x8087, 0x0082,
0x8183, 0x0186, 0x018C, 0x8189, 0x0198, 0x819D, 0x8197, 0x0192,
0x01B0, 0x81B5, 0x81BF, 0x01BA, 0x81AB, 0x01AE, 0x01A4, 0x81A1,
0x01E0, 0x81E5, 0x81EF, 0x01EA, 0x81FB, 0x01FE, 0x01F4, 0x81F1,
0x81D3, 0x01D6, 0x01DC, 0x81D9, 0x01C8, 0x81CD, 0x81C7, 0x01C2,
0x0140, 0x8145, 0x814F, 0x014A, 0x815B, 0x015E, 0x0154, 0x8151,
0x8173, 0x0176, 0x017C, 0x8179, 0x0168, 0x816D, 0x8167, 0x0162,
0x8123, 0x0126, 0x012C, 0x8129, 0x0138, 0x813D, 0x8137, 0x0132,
0x0110, 0x8115, 0x811F, 0x011A, 0x810B, 0x010E, 0x0104, 0x8101,
0x8303, 0x0306, 0x030C, 0x8309, 0x0318, 0x831D, 0x8317, 0x0312,
0x0330, 0x8335, 0x833F, 0x033A, 0x832B, 0x032E, 0x0324, 0x8321,
0x0360, 0x8365, 0x836F, 0x036A, 0x837B, 0x037E, 0x0374, 0x8371,
0x8353, 0x0356, 0x035C, 0x8359, 0x0348, 0x834D, 0x8347, 0x0342,
0x03C0, 0x83C5, 0x83CF, 0x03CA, 0x83DB, 0x03DE, 0x03D4, 0x83D1,
0x83F3, 0x03F6, 0x03FC, 0x83F9, 0x03E8, 0x83ED, 0x83E7, 0x03E2,
0x83A3, 0x03A6, 0x03AC, 0x83A9, 0x03B8, 0x83BD, 0x83B7, 0x03B2,
0x0390, 0x8395, 0x839F, 0x039A, 0x838B, 0x038E, 0x0384, 0x8381,
0x0280, 0x8285, 0x828F, 0x028A, 0x829B, 0x029E, 0x0294, 0x8291,
0x82B3, 0x02B6, 0x02BC, 0x82B9, 0x02A8, 0x82AD, 0x82A7, 0x02A2,
0x82E3, 0x02E6, 0x02EC, 0x82E9, 0x02F8, 0x82FD, 0x82F7, 0x02F2,
0x02D0, 0x82D5, 0x82DF, 0x02DA, 0x82CB, 0x02CE, 0x02C4, 0x82C1,
0x8243, 0x0246, 0x024C, 0x8249, 0x0258, 0x825D, 0x8257, 0x0252,
0x0270, 0x8275, 0x827F, 0x027A, 0x826B, 0x026E, 0x0264, 0x8261,
0x0220, 0x8225, 0x822F, 0x022A, 0x823B, 0x023E, 0x0234, 0x8231,
0x8213, 0x0216, 0x021C, 0x8219, 0x0208, 0x820D, 0x8207, 0x0202
];
// Given a data string like 'fffffd00c807000342000300', calculates the CRC16
// and appends it to the end, low byte first
module.exports = function addCRC(dataString)
{
var buffer = new Buffer(dataString, 'hex');
var crcAccum = 0;
for (var j=0; j<buffer.length; j++)
{
var i = ( (crcAccum >> 8) ^ buffer.readUInt8(j) ) & 0xFF;
crcAccum = (crcAccum << 8) ^ crcTable[i];
}
var crc16 = (crcAccum + 0x10000).toString(16).substr(-4); // e.g. '7fda'
crcHigh = crc16.substr(0, 2);
crcLow = crc16.substr(2, 2);
return dataString + crcLow + crcHigh;
};

View File

@@ -0,0 +1,154 @@
var express = require('express');
var cors = require('cors');
var path = require('path');
var osprey = require('osprey');
var robot = require('./robot');
var populateMotionData = require('./populateMotionData');
var app = module.exports = express();
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.compress());
app.use(express.logger('dev'));
// Enable CORS, including preflighting
app.use(cors());
app.options('*', cors());
app.set('port', process.env.PORT || 3000);
function normalizeCommandName(name)
{
return name.replace(/\s+/g, '').toLowerCase();
}
var motionData = {};
populateMotionData(motionData, normalizeCommandName);
api = osprey.create('/api', app,
{
ramlFile: path.join(__dirname, '/assets/raml/api.raml'),
logLevel: 'debug' // logLevel: off->No logs | info->Show Osprey modules initializations | debug->Show all
});
// GET information about the commands
api.get('/robots', function (req, res)
{
robot.findRobots(function onFound(err, robotDevices)
{
if (err)
{
res.status(500).send({ error: err });
}
else
{
res.send(robotDevices);
}
});
});
// GET information about the commands
// TODO: One day we'll need to ask the robot for its motion file
// instead of assuming all robots use the assumedMotionFile.mtnx bundled here.
api.get('/robots/:robotId/commands', function (req, res)
{
res.send(motionData);
});
// POST a command for the robot to execute
api.post('/robots/:robotId/commands', function (req, res)
{
try
{
var robotId = req.params.robotId;
var commandNumber = req.param('number');
var commandName = req.param('name');
var normalizedCommandName = commandName ? normalizeCommandName(commandName) : null;
var async = req.param('async') || false;
if (commandName && !commandNumber)
{
commandNumber = motionData.flowsByName[normalizedCommandName];
if (!commandNumber)
{
res.status(422).send({ error: 'Unrecognized command name' });
return;
}
}
if (commandNumber in motionData.flowsByNumber)
{
var command = motionData.flowsByNumber[commandNumber];
console.info('Executing command: ' + command.name + ' for ' + command.time + 'ms');
robot.connect(robotId, function onConnect(err, btSerial)
{
if (err)
{
res.status(500).send({ error: err });
}
else
{
robot.sendCommand(btSerial, Number(commandNumber));
if (async) // respond immediately, with the execution time
{
console.log('Command sent, not waiting for execution to finish')
res.status(202).send({ executionTimeInMs: command.time });
}
else // respond only after the execution time has passed
{
setTimeout(function ()
{
console.log('Command sent, finished waiting for execution');
res.status(204).send('');
}, command.time);
}
}
});
}
else
{
res.status(422).send({ error: 'Unrecognized command number' });
}
}
catch (e)
{
console.error(e);
res.status(500).send('');
}
});
// GET the state of the robot by reading the control table at a certain address
// DOES NOT WORK RIGHT YET
// Only asks the robot to read the data, but the data itself is only logged and not returned
api.get('/robots/:robotId/state', function (req, res)
{
var robotId = req.params.robotId;
var address = Number(req.param('address'));
var bytesToRead = Number(req.param('bytesToRead'));
console.info('Reading state: ' + [ address, bytesToRead ]);
robot.connect(robotId, function onConnect(err, btSerial)
{
if (err)
{
res.status(500).send({ error: err });
}
else
{
robot.readState(btSerial, address, bytesToRead);
res.status(202).send('');
}
});
});
if (!module.parent)
{
var port = app.get('port');
app.listen(port);
console.log('listening on port ' + port);
}

View File

@@ -0,0 +1,72 @@
#%RAML 0.8
title: RESTful API for Robotis Darwin Mini
version: v1
mediaType: application/json
baseUri: http://localhost:3000/api # optionally, change to http://<subdomain>.ngrok.com/api
/robots:
get:
description: |
Retrieve information about any robots paired with your computer.
Useful to see what's your robotId.
responses:
200:
/{robotId}:
uriParameters:
robotId:
description: |
The last 4 characters of the bluetooth address of your Robotis device.
Use `GET /robots` to see all paired robots so you know the valid options
for `robotId`. Or, you can see its address on a Mac if you hold down 'option'
while viewing the menu bar's bluetooth dropdown.
e.g. if the address is 'b8-63-bc-00-12-16' your robotId is 1216
/commands:
get:
description: |
Returns information about the commands in the motion file currently loaded into the robot.
responses:
200:
post:
description: |
Execute a command, and return when the robot has finished.
You can submit either the number of the command or its name,
based on the motion file currently loaded into the robot.
If you submit both, only the number is used.
By default, the API call returns only after the robot
(is supposed to have) finished executing the command.
If you specify `async` of `true`, the API call returns immediately,
responding with the number of ms you should wait for the robot to finish.
body:
example: |
{ "number": 3, "name": "Sit", "async": false }
responses:
202:
description: |
The command is now executing, and should finish within the number of ms
returned in the response. This is the response when `async` was true.
body:
example: |
{ "executionTimeInMs": 800 }
204:
description: |
The command finished executing. This is the default behavior, when `async`
was not specified or was `false`.
/state:
get:
description: |
DOES NOT WORK PROPERLY YET.
It only logs the data it sees to the console.
See http://support.robotis.com/en/product/dynamixel_pro/dynamixelpro/control_table.htm
queryParameters:
address:
description: The address in the control table.
type: number
required: true
example: 610
bytesToRead:
description: How many bytes to read starting from that address.
type: number
required: true
example: 1
responses:
200:

View File

@@ -0,0 +1,39 @@
var robot = require('./robot');
var fs = require('fs');
var parseString = require('xml2js').parseString;
// robotId should be the last 4 characters of the bluetooth address of your Robotis device
// You can see its address on a Mac if you hold down 'option' while viewing the menu bar's bluetooth dropdown
// Example: node src/command.js 1216 19
var args = process.argv.slice(2);
var robotId = args[0]; // e.g. if the address is 'b8-63-bc-00-12-16' your robotId is 1216
var command = args[1] || '1'; // 1 should be set to mean: return to initial position
parseString(fs.readFileSync(__dirname + '/assets/assumedMotionFile.mtnx', { encoding: 'utf8' }), function (err, result)
{
var flows = result.Root.BucketRoot[0].Bucket[0].callFlows[0].callFlow.map(function (obj) { return obj.$; });
var flowsByNumber = {};
flows.forEach(function (flow)
{
flowsByNumber[flow.callIndex] = flow.flow;
});
// console.log(JSON.stringify(flowsByNumber, null, 2));
if (command in flowsByNumber)
{
console.log('>> ' + flowsByNumber[command]);
robot.connect(robotId, function onConnect(err)
{
robot.sendCommand(Number(command));
});
}
else
{
console.log('Unrecognized command: ' + command);
}
});

View File

@@ -0,0 +1,64 @@
var fs = require('fs');
var parseString = require('xml2js').parseString;
var pageData = {};
var flowData = {};
var MILLIS_PER_FRAME = 8; // This is a guess based on reading docs such as http://support.robotis.com/en/software/roboplus/roboplus_motion/motionedit/stepedit/roboplus_motion_pausetime.htm
var UNIT_PADDING_TIME = 100; // Another guess of how long to wait before motion is truly done
var RECOVERY_TIME = 300; // Another guess of how long to wait before motion is truly done
function elapsedTime(flow)
{
var millis = 0;
if (flow.$.return == '-1') // This flow has a finite time and returns
{
var units = flow.units[0].unit;
units.forEach(function (unit)
{
var pageName = unit.$.main;
var steps = pageData[pageName].steps[0].step;
var lastFrame = Number(steps[steps.length - 1].$.frame);
millis += (lastFrame * MILLIS_PER_FRAME + UNIT_PADDING_TIME) * unit.$.exitSpeed * unit.$.loop;
millis += RECOVERY_TIME;
});
}
return millis;
}
module.exports = function populateMotionData(motionData, normalizeCommandName)
{
motionData.flowsByNumber = {};
motionData.flowsByName = {};
parseString(fs.readFileSync(__dirname + '/assets/assumedMotionFile.mtnx', { encoding: 'utf8' }), function (err, parsed)
{
motionData.raw = parsed;
// Create a reference map of pages, to use in calculating the elapsed time for each command
parsed.Root.PageRoot[0].Page.forEach(function (page)
{
pageData[page.$.name] = page;
});
// Create a reference map of flows
parsed.Root.FlowRoot[0].Flow.forEach(function (flow)
{
flowData[flow.$.name] = flow;
});
var callFlows = parsed.Root.BucketRoot[0].Bucket[0].callFlows[0].callFlow;
callFlows.forEach(function (callFlow)
{
var flowNumber = callFlow.$.callIndex;
var flowName = callFlow.$.flow;
var flow = flowData[flowName];
motionData.flowsByNumber[flowNumber] =
{
name: flowName,
time: elapsedTime(flow)
};
motionData.flowsByName[normalizeCommandName(flowName)] = flowNumber;
});
});
};

View File

@@ -0,0 +1,145 @@
var addCRC = require('./addCRC');
var PREFIX = 'fffffd00'; // Defined by http://support.robotis.com/en/techsupport_eng.htm#product/dynamixel_pro/communication.htm
var DYNAMIXEL_ID = 'c8'; // Not sure why, but I'm told this is 200
var COMMAND_PACKET_LENGTH = '0700';
var STATE_PACKET_LENGTH = '0700';
var INSTRUCTION_READ = '02';
var INSTRUCTION_WRITE = '03';
var ADDRESS_EXECUTE = '4200'; // 66 decimal expressed in hex, low byte first; "66 is the address value that states the motion will be executed"
var ROBOT_NAME_REGEXP = /Robotis/i;
exports.findRobots = findRobots = function findRobots(onFound)
{
var btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();
btSerial.listPairedDevices(function (pairedDevices)
{
var robotDevices = pairedDevices.filter(function (device)
{
return (device.name.search(ROBOT_NAME_REGEXP) != -1);
});
robotDevices.forEach(function (device)
{
device.robotId = device.address.substr(-5).replace(/\-/g, '');
});
onFound(null, robotDevices);
});
};
// Setup a little registry of btSerial ports, one per robot
var robotPorts = {};
function getPort(robotId)
{
if (!(robotId in robotPorts))
{
robotPorts[robotId] = new (require('bluetooth-serial-port')).BluetoothSerialPort();
}
return robotPorts[robotId];
}
// Find the Robotis device among the paired devices, and connect to it
// robotId should be the last 4 characters of the bluetooth address of your Robotis device
// You can see its address on a Mac if you hold down 'option' while viewing the menu bar's bluetooth dropdown
// e.g. if the address is 'b8-63-bc-00-12-16' your robotId is 1216
exports.connect = function connect(robotId, onConnect)
{
findRobots(function onFound(err, robots)
{
var candidates = robots.filter(function (r)
{
return (r.robotId == robotId);
});
if (candidates.length === 0)
{
onConnect('No robots found!');
}
else
{
if (candidates.length > 1) console.info('Found ' + candidates.length + ' robots; using the first');
robot = candidates[0];
channel = robot.services[0].channel;
var btSerial = getPort(robotId);
connectOne(btSerial, robot.address, channel, onConnect);
}
});
};
// Actually connect to the device, on its address and channel
function connectOne(btSerial, address, channel, onConnect)
{
if (btSerial.isOpen())
{
console.info('already connected');
onConnect(null, btSerial);
}
else
{
console.info('connecting...');
btSerial.connect(address, channel, function()
{
console.info('connected');
onConnect(null, btSerial);
// Log any returning data:
btSerial.on('data', function (buffer)
{
console.info('<< ' + buffer.toString('hex'));
});
}, function ()
{
onConnect('cannot connect', btSerial);
});
}
}
function toHexLowHigh(number)
{
var hex = (number + 0x10000).toString(16).substr(-4); // e.g. '0262'
hexHigh = hex.substr(0, 2);
hexLow = hex.substr(2, 2);
return hexLow + hexHigh; // e.g. '6202'
}
// write a command to the bluetooth port
// command should be a number from 1-255
exports.sendCommand = function sendCommand(btSerial, command, onSent)
{
var hexCommand = toHexLowHigh(command);
var packet = PREFIX + DYNAMIXEL_ID + COMMAND_PACKET_LENGTH + INSTRUCTION_WRITE + ADDRESS_EXECUTE + hexCommand;
packet = addCRC(packet);
console.info('Sending packet: ' + packet);
btSerial.write(new Buffer(packet, 'hex'), function (err, bytesWritten)
{
console.info('sent');
if (err) console.error(err);
if (onSent) onSent(err, btSerial);
});
};
// read a state from the bluetooth port
// address and lengthInBytes should be numbers
exports.readState = function readState(btSerial, address, lengthInBytes, onSent)
{
var addr16 = toHexLowHigh(address);
var len16 = toHexLowHigh(lengthInBytes);
var packet = PREFIX + DYNAMIXEL_ID + STATE_PACKET_LENGTH + INSTRUCTION_READ + addr16 + len16;
packet = addCRC(packet);
console.info('Sending packet: ' + packet);
btSerial.write(new Buffer(packet, 'hex'), function (err, bytesWritten)
{
console.info('sent');
if (err) console.error(err);
if (onSent) onSent(err, btSerial);
});
};

View File

@@ -0,0 +1,2 @@
| tst.js:3:3:3:14 | "use strict" | This expression has no effect. |
| tst.js:5:3:5:18 | point.bias == -1 | This expression has no effect. |

View File

@@ -0,0 +1,6 @@
import javascript
from Expr e
where e.isPure() and
e.getParent() instanceof ExprStmt
select e, "This expression has no effect."

View File

@@ -0,0 +1 @@
| tst.js:5:3:5:18 | point.bias == -1 | This expression has no effect. |

View File

@@ -0,0 +1,7 @@
import javascript
from Expr e
where e.isPure() and
e.getParent() instanceof ExprStmt and
not e.getParent() instanceof Directive // new check
select e, "This expression has no effect."

View File

@@ -0,0 +1,6 @@
!function () {
"use strict"; // jshint ;_;
point.bias == -1;
}();