farmbot
Version:
Farmbot Javascript client.
461 lines (460 loc) • 20.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Farmbot = void 0;
var mqtt_1 = require("mqtt");
var util_1 = require("./util");
var _1 = require(".");
var config_1 = require("./config");
var resource_adapter_1 = require("./resources/resource_adapter");
var constants_1 = require("./constants");
var is_celery_script_1 = require("./util/is_celery_script");
var time_1 = require("./util/time");
var rpc_request_1 = require("./util/rpc_request");
var Farmbot = /** @class */ (function () {
function Farmbot(input) {
var _this = this;
/** Get a Farmbot Constructor Parameter. */
this.getConfig = function (key) { return _this.config[key]; };
/** Set a Farmbot Constructor Parameter. */
this.setConfig = function (key, value) {
_this.config[key] = value;
};
/**
* Installs a "Farmware" (plugin) onto the bot's SD card.
* URL must point to a valid Farmware manifest JSON document.
*/
this.installFarmware = function (url) {
return _this.send((0, util_1.rpcRequest)([{ kind: "install_farmware", args: { url: url } }]));
};
/**
* Checks for updates on a particular Farmware plugin when given the name of
* a Farmware. `updateFarmware("take-photo")`
*/
this.updateFarmware = function (pkg) {
return _this.send((0, util_1.rpcRequest)([{
kind: "update_farmware",
args: { package: pkg }
}]));
};
/** Uninstall a Farmware plugin. */
this.removeFarmware = function (pkg) {
return _this.send((0, util_1.rpcRequest)([{
kind: "remove_farmware",
args: {
package: pkg
}
}]));
};
/**
* Installs "Farmware" (plugins) authored by FarmBot, Inc.
* onto the bot's SD card.
*/
this.installFirstPartyFarmware = function () {
return _this.send((0, util_1.rpcRequest)([{
kind: "install_first_party_farmware",
args: {}
}]));
};
/**
* Deactivate FarmBot OS completely (shutdown).
* Useful before unplugging the power.
*/
this.powerOff = function () {
return _this.send((0, util_1.rpcRequest)([{ kind: "power_off", args: {} }]));
};
/** Restart FarmBot OS. */
this.reboot = function () {
return _this.send((0, util_1.rpcRequest)([
{ kind: "reboot", args: { package: "farmbot_os" } }
]));
};
/** Reinitialize the FarmBot microcontroller firmware. */
this.rebootFirmware = function () {
return _this.send((0, util_1.rpcRequest)([
{ kind: "reboot", args: { package: "arduino_firmware" } }
]));
};
/** Check for new versions of FarmBot OS.
* Downloads and installs if available. */
this.checkUpdates = function () {
return _this.send((0, util_1.rpcRequest)([
{ kind: "check_updates", args: { package: "farmbot_os" } }
]));
};
/** THIS WILL RESET THE SD CARD, deleting all non-factory data!
* Be careful!! */
this.resetOS = function () {
return _this.publish((0, util_1.rpcRequest)([
{ kind: "factory_reset", args: { package: "farmbot_os" } }
]));
};
/** WARNING: will reset all firmware (hardware) settings! */
this.resetMCU = function () {
return _this.send((0, util_1.rpcRequest)([
{ kind: "factory_reset", args: { package: "arduino_firmware" } }
]));
};
this.flashFirmware = function (
/** one of: "arduino"|"express_k10"|"farmduino_k14"|"farmduino" */
firmware_name) {
return _this.send((0, util_1.rpcRequest)([{
kind: "flash_firmware",
args: {
package: firmware_name
}
}]));
};
/**
* Lock the bot from moving (E-STOP). Turns off peripherals and motors. This
* also will pause running regimens and cause any running sequences to exit.
*/
this.emergencyLock = function () {
var body = [{ kind: "emergency_lock", args: {} }];
var rpc = (0, util_1.rpcRequest)(body, rpc_request_1.Priority.HIGHEST);
return _this.send(rpc);
};
/** Unlock the bot when the user says it is safe. */
this.emergencyUnlock = function () {
var body = [{ kind: "emergency_unlock", args: {} }];
var rpc = (0, util_1.rpcRequest)(body, rpc_request_1.Priority.HIGHEST);
return _this.send(rpc);
};
/** Execute a sequence by its ID on the FarmBot API. */
this.execSequence = function (sequence_id, body) {
if (body === void 0) { body = []; }
return _this.send((0, util_1.rpcRequest)([
{ kind: "execute", args: { sequence_id: sequence_id }, body: body }
]));
};
/** Run an installed Farmware plugin on the SD Card. */
this.execScript = function (
/** Name of the Farmware. */
label,
/** Optional ENV vars to pass the Farmware. */
envVars) {
return _this.send((0, util_1.rpcRequest)([
{ kind: "execute_script", args: { label: label }, body: envVars }
]));
};
/** Bring a particular axis (or all of them) to position 0 in Z Y X order. */
this.home = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "home", args: args }]));
};
/** Use end stops or encoders to figure out where 0,0,0 is in Z Y X axis
* order. WON'T WORK WITHOUT ENCODERS OR END STOPS! A blockage or stall
* during this command will set that position as zero. Use carefully. */
this.findHome = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "find_home", args: args }]));
};
/** Move FarmBot to an absolute point. */
this.moveAbsolute = function (args) {
var x = args.x, y = args.y, z = args.z;
var speed = args.speed || config_1.CONFIG_DEFAULTS.speed;
return _this.send((0, util_1.rpcRequest)([
{
kind: "move_absolute",
args: {
location: (0, util_1.coordinate)(x, y, z),
offset: (0, util_1.coordinate)(0, 0, 0),
speed: speed
}
}
]));
};
/** Move FarmBot to position relative to its current position. */
this.moveRelative = function (args) {
var x = args.x, y = args.y, z = args.z;
var speed = args.speed || config_1.CONFIG_DEFAULTS.speed;
return _this.send((0, util_1.rpcRequest)([
{ kind: "move_relative", args: { x: x, y: y, z: z, speed: speed } }
]));
};
/** Set a GPIO pin to a particular value. */
this.writePin = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "write_pin", args: args }]));
};
/** Read the value of a GPIO pin. Will create a SensorReading if it's
* a sensor. */
this.readPin = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "read_pin", args: args }]));
};
/** Reverse the value of a digital pin. */
this.togglePin = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "toggle_pin", args: args }]));
};
/** Read the status of the bot. Should not be needed unless you are first
* logging in to the device, since the device pushes new states out on
* every update. */
this.readStatus = function (args) {
if (args === void 0) { args = {}; }
return _this.send((0, util_1.rpcRequest)([{ kind: "read_status", args: args }]));
};
/** Snap a photo and send to the API for post processing. */
this.takePhoto = function (args) {
if (args === void 0) { args = {}; }
return _this.send((0, util_1.rpcRequest)([{ kind: "take_photo", args: args }]));
};
/** Download/apply all of the latest FarmBot API JSON resources (plants,
* account info, etc.) to the device. */
this.sync = function (args) {
if (args === void 0) { args = {}; }
return _this.send((0, util_1.rpcRequest)([{ kind: "sync", args: args }]));
};
/**
* Set the current position of the given axis to 0.
* Example: Sending `bot.setZero("x")` at x: 255 will translate position
* 255 to 0, causing that position to be x: 0.
*/
this.setZero = function (axis) {
return _this.send((0, util_1.rpcRequest)([{
kind: "zero",
args: { axis: axis }
}]));
};
/**
* Set user ENV vars (usually used by 3rd-party Farmware plugins).
* Set value to `undefined` to unset.
*/
this.setUserEnv = function (configs) {
var body = Object
.keys(configs)
.map(function (label) {
return {
kind: "pair",
args: { label: label, value: (configs[label] || constants_1.Misc.NULL) }
};
});
return _this.send((0, util_1.rpcRequest)([{ kind: "set_user_env", args: {}, body: body }]));
};
this.sendMessage = function (message_type, message, channels) {
if (channels === void 0) { channels = []; }
_this.send((0, util_1.rpcRequest)([{
kind: "send_message",
args: {
message_type: message_type,
message: message
},
body: channels.map(function (channel_name) { return ({
kind: "channel",
args: {
channel_name: channel_name
}
}); })
}]));
};
/** Control servos on pins 4 and 5. */
this.setServoAngle = function (args) {
var result = _this.send((0, util_1.rpcRequest)([{ kind: "set_servo_angle", args: args }]));
// Celery script can't validate `pin_number` and `pin_value` the way we need
// for `set_servo_angle`. We will send the RPC command off, but also
// crash the client to aid debugging.
if (![4, 5, 6, 11].includes(args.pin_number)) {
throw new Error("Servos only work on pins 4 and 5");
}
if (args.pin_value > 180 || args.pin_value < 0) {
throw new Error("Pin value outside of 0...180 range");
}
return result;
};
/**
* Find the axis extents using encoder, motor, or end-stop feedback.
* Will set a new home position and a new axis length for the given axis.
*/
this.calibrate = function (args) {
return _this.send((0, util_1.rpcRequest)([{ kind: "calibrate", args: args }]));
};
this.lua = function (lua) {
return _this.send((0, util_1.rpcRequest)([
{ kind: "lua", args: { lua: lua } }
]));
};
/**
* Retrieves all of the event handlers for a particular event.
* Returns an empty array if the event did not exist.
*/
this.event = function (name) {
_this._events[name] = _this._events[name] || [];
return _this._events[name];
};
this.on = function (event, value, once) {
if (once === void 0) { once = false; }
_this.event(event).push({ value: value, once: once, event: event });
};
this.emit = function (event, data) {
var nextArray = [];
_this.event(event)
.concat(_this.event("*"))
.forEach(function (handler) {
try {
handler.value(data, event);
if (!handler.once && handler.event === event) {
nextArray.push(handler);
}
}
catch (e) {
var msg = "Exception thrown while handling '".concat(event, "' event.");
console.warn(msg);
console.dir(e);
}
});
if (nextArray.length === 0) {
delete _this._events[event];
}
else {
_this._events[event] = nextArray;
}
};
/** Low-level means of sending MQTT packets. Does not check format. Does not
* acknowledge confirmation. Probably not the one you want. */
this.publish = function (msg, important) {
if (important === void 0) { important = true; }
if (_this.client) {
_this.emit(constants_1.FbjsEventName.sent, msg);
/** SEE: https://github.com/mqttjs/MQTT.js#client */
_this.client.publish(_this.channel.toDevice, JSON.stringify(msg));
}
else {
if (important) {
throw new Error("Not connected to server");
}
}
};
/** Low-level means of sending MQTT RPC commands to the bot. Acknowledges
* receipt of message, but does not check formatting. Consider using higher
* level methods like .moveRelative(), .calibrate(), etc....
*/
this.send = function (input) {
return new Promise(function (resolve, reject) {
_this.publish(input);
function handler(response) {
switch (response.kind) {
case "rpc_ok": return resolve(response);
case "rpc_error":
var reason = (response.body || [])
.map(function (x) { return x.args.message; })
.join(", ");
return reject(new Error(reason));
default:
console.dir(response);
throw new Error("Got a bad CeleryScript node.");
}
}
_this.on(input.args.label, handler, true);
});
};
/** Main entry point for all MQTT packets. */
this._onmessage = function (chan, buffer) {
var original = (0, _1.bufferToString)(buffer);
var segments = chan.split(constants_1.Misc.MQTT_DELIM);
var emit = _this.emit;
try {
var msg = JSON.parse(original);
if (segments[0] == constants_1.MqttChanName.publicBroadcast) {
return emit(constants_1.MqttChanName.publicBroadcast, msg);
}
switch (segments[2]) {
case constants_1.MqttChanName.logs: return emit(constants_1.FbjsEventName.logs, msg);
case constants_1.MqttChanName.status: return emit(constants_1.FbjsEventName.status, msg);
case constants_1.MqttChanName.sync: return emit(constants_1.FbjsEventName.sync, msg);
case constants_1.MqttChanName.pong:
return emit(segments[3], msg);
default:
var ev = (0, is_celery_script_1.hasLabel)(msg) ? msg.args.label : constants_1.FbjsEventName.malformed;
return emit(ev, msg);
}
}
catch (error) {
console
.dir({ text: "Could not parse inbound message from MQTT.", error: error });
emit(constants_1.FbjsEventName.malformed, original);
}
};
this.ping = function (timeout, now) {
if (timeout === void 0) { timeout = 10000; }
if (now === void 0) { now = (0, time_1.timestamp)(); }
_this.setConfig("LAST_PING_OUT", now);
return _this.doPing(now, timeout);
};
// STEP 0: Subscribe to `bot/device_23/pong/#`
// STEP 0: Send `bot/device_23/ping/3123123`
// STEP 0: Receive `bot/device_23/pong/3123123`
this.doPing = function (startedAt, timeout) {
var timeoutPromise = new Promise(function (_, rej) { return setTimeout(function () { return rej(-0); }, timeout); });
var pingPromise = new Promise(function (res, _) {
var ok = function () {
var t = (0, time_1.timestamp)();
_this.setConfig("LAST_PING_IN", t);
res(t - startedAt);
};
_this.on("" + startedAt, ok, true);
var chan = _this.channel.ping(startedAt);
if (_this.client) {
_this.client.publish(chan, JSON.stringify(startedAt));
}
});
return Promise.race([timeoutPromise, pingPromise]);
};
/** Bootstrap the device onto the MQTT broker. */
this.connect = function () {
var _a = _this.config, mqttUsername = _a.mqttUsername, token = _a.token, mqttServer = _a.mqttServer;
var reconnectPeriod = constants_1.Misc.RECONNECT_THROTTLE_MS;
var client = mqtt_1.default.connect(mqttServer, {
clean: true,
clientId: "FBJS-".concat(Farmbot.VERSION, "-").concat((0, util_1.uuid)()),
password: token,
protocolId: "MQTT",
protocolVersion: 4,
reconnectPeriod: reconnectPeriod,
username: mqttUsername,
});
_this.client = client;
_this.resources = new resource_adapter_1.ResourceAdapter(_this, _this.config.mqttUsername);
client.on("message", _this._onmessage);
client.on("offline", function () { return _this.emit(constants_1.FbjsEventName.offline, {}); });
client.on("connect", function () { return _this.emit(constants_1.FbjsEventName.online, {}); });
var channels = [
_this.channel.logs,
_this.channel.status,
_this.channel.sync,
_this.channel.toClient,
_this.channel.pong
];
client.subscribe(channels);
return new Promise(function (resolve, _reject) {
if (_this.client) {
_this.client.once("connect", function () { return resolve(_this); });
}
else {
throw new Error("Please connect first.");
}
});
};
this._events = {};
this.config = (0, config_1.generateConfig)(input);
this.resources = new resource_adapter_1.ResourceAdapter(this, this.config.mqttUsername);
}
Object.defineProperty(Farmbot.prototype, "channel", {
/** Dictionary of all relevant MQTT channels the bot uses. */
get: function () {
var deviceName = this.config.mqttUsername;
return {
/** From the browser, usually. */
toDevice: "bot/".concat(deviceName, "/").concat(constants_1.MqttChanName.fromClients),
/** From farmbot */
toClient: "bot/".concat(deviceName, "/").concat(constants_1.MqttChanName.fromDevice),
status: "bot/".concat(deviceName, "/").concat(constants_1.MqttChanName.status),
logs: "bot/".concat(deviceName, "/").concat(constants_1.MqttChanName.logs),
sync: "bot/".concat(deviceName, "/").concat(constants_1.MqttChanName.sync, "/#"),
/** Read only */
pong: "bot/".concat(deviceName, "/pong/#"),
/** Write only: bot/${deviceName}/ping/${timestamp} */
ping: function (tStamp) { return "bot/".concat(deviceName, "/ping/").concat(tStamp); }
};
},
enumerable: false,
configurable: true
});
Farmbot.VERSION = "15.8.11";
return Farmbot;
}());
exports.Farmbot = Farmbot;