johnny-five
Version:
The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!
294 lines (242 loc) • 6.77 kB
JavaScript
const Board = require("./board");
const Emitter = require("./mixins/emitter");
const Fn = require("./fn");
const Led = require("./led");
const Sensor = require("./sensor");
const CALIBRATED_MIN_VALUE = 0;
const CALIBRATED_MAX_VALUE = 1000;
const LINE_ON_THRESHOLD = 200;
const LINE_NOISE_THRESHOLD = 50;
const priv = new Map();
const Controllers = {
DEFAULT: {
initialize: {
value(options) {
if (typeof options.emitter === "undefined") {
throw new Error("Emitter pin is required");
}
if (!options.pins || options.pins.length === 0) {
throw new Error("Pins must be defined");
}
const state = priv.get(this);
state.emitter = new Led({
board: this.board,
pin: options.emitter
});
state.sensorStates = options.pins.map((pin) => {
const sensor = new Sensor({
board: this.board,
freq: state.freq,
pin
});
const sensorState = {
sensor,
rawValue: 0,
dataReceived: false,
};
sensor.on("data", value => {
onData(this, sensorState, value);
});
return sensorState;
});
}
}
}
};
function onData(instance, sensorState, value) {
const state = priv.get(instance);
// Update this sensor state
sensorState.dataReceived = true;
sensorState.rawValue = value;
// Check if all sensors have been read
const allRead = state.sensorStates.every(({dataReceived}) => dataReceived);
if (allRead) {
instance.emit("data", instance.raw);
if (state.autoCalibrate) {
setCalibration(state.calibration, instance.raw);
}
if (instance.isCalibrated) {
instance.emit("calibratedData", instance.values);
instance.emit("line", instance.line);
}
state.sensorStates.forEach(sensorState => {
sensorState.dataReceived = false;
});
}
}
function setCalibration(calibration, values) {
values.forEach((value, i) => {
if (calibration.min[i] === undefined || value < calibration.min[i]) {
calibration.min[i] = value;
}
if (calibration.max[i] === undefined || value > calibration.max[i]) {
calibration.max[i] = value;
}
});
}
function calibrationIsValid(calibration, sensors) {
return calibration &&
(calibration.max && calibration.max.length === sensors.length) &&
(calibration.min && calibration.min.length === sensors.length);
}
function calibratedValues(instance) {
return instance.raw.map((value, i) => {
return Fn.constrain(
Fn.scale(
value,
instance.calibration.min[i],
instance.calibration.max[i],
CALIBRATED_MIN_VALUE,
CALIBRATED_MAX_VALUE
),
CALIBRATED_MIN_VALUE,
CALIBRATED_MAX_VALUE
);
});
}
function maxLineValue(instance) {
return (instance.sensors.length - 1) * CALIBRATED_MAX_VALUE;
}
// Returns a value between 0 and (n-1)*1000
// Given 5 sensors, the value will be between 0 and 4000
function getLine(instance, whiteLine) {
const state = priv.get(instance);
let onLine = false;
let avg = 0;
let sum = 0;
whiteLine = !!whiteLine;
instance.values.forEach((value, i) => {
value = whiteLine ? (CALIBRATED_MAX_VALUE - value) : value;
if (value > LINE_ON_THRESHOLD) {
onLine = true;
}
if (value > LINE_NOISE_THRESHOLD) {
avg += value * i * CALIBRATED_MAX_VALUE;
sum += value;
}
});
if (!onLine) {
const maxPoint = maxLineValue(instance) + 1;
const centerPoint = maxPoint / 2;
return state.lastLine < centerPoint ? 0 : maxPoint;
}
return state.lastLine = Math.floor(avg / sum);
}
class ReflectanceArray extends Emitter {
constructor(options) {
super();
Board.Component.call(
this, options = Board.Options(options)
);
Board.Controller.call(this, Controllers, options);
// Read event throttling
const {
autoCalibrate = false,
freq = 25,
} = options;
// Make private data entry
const state = {
autoCalibrate,
freq,
lastLine: 0,
isOn: false,
calibration: {
min: [],
max: []
},
};
priv.set(this, state);
if (typeof this.initialize === "function") {
this.initialize(options);
}
Object.defineProperties(this, {
isOn: {
get() {
return state.emitter.isOn;
}
},
isCalibrated: {
get() {
return calibrationIsValid(this.calibration, this.sensors);
}
},
isOnLine: {
get() {
const line = this.line;
return line > CALIBRATED_MIN_VALUE && line < maxLineValue(this);
}
},
sensors: {
get() {
return state.sensorStates.map(({sensor}) => sensor);
}
},
calibration: {
get() {
return state.calibration;
}
},
raw: {
get() {
return state.sensorStates.map(({rawValue}) => rawValue);
}
},
values: {
get() {
return this.isCalibrated ? calibratedValues(this) : this.raw;
}
},
line: {
get() {
return this.isCalibrated ? getLine(this) : 0;
}
}
});
}
// Public methods
enable() {
priv.get(this).emitter.on();
return this;
}
disable() {
priv.get(this).emitter.off();
return this;
}
// Calibrate will store the min/max values for this sensor array
// It should be called many times in order to get a lot of readings
// on light and dark areas. See calibrateUntil for a convenience
// for looping until a condition is met.
calibrate() {
this.once("data", values => {
setCalibration(priv.get(this).calibration, values);
this.emit("calibrated");
});
return this;
}
// This will continue to calibrate until the predicate is true.
// Allows the user to calibrate n-times, or wait for user input,
// or base it on calibration heuristics. However the user wants.
calibrateUntil(predicate) {
const loop = () => {
this.calibrate();
this.once("calibrated", () => {
if (!predicate()) {
loop();
}
});
};
loop();
return this;
}
// Let the user tell us what the calibration data is
// This allows the user to save calibration data and
// reload it without needing to calibrate every time.
loadCalibration(calibration) {
if (!calibrationIsValid(calibration, this.sensors)) {
throw new Error("Calibration data not properly set: {min: [], max: []}");
}
priv.get(this).calibration = calibration;
return this;
}
}
module.exports = ReflectanceArray;