smoke-signal-js
Version:
A JavaScript port of Tobster's TobComm.
521 lines (477 loc) • 15.1 kB
JavaScript
// Dependencies
var ByteBuffer = require('bytebuffer');
const helperFunctions = require('smoke-machine-js');
// Variables
const OBJECT_HEADER_SIZE = 17; //Type (1) + ObjectNo (4) + InstanceNo (4) + Start (4) + Length (4).
const COMMAND_HEADER_SIZE = 9; //Type (1) + CommandType (4) + InstanceNo (4). 🤷♂️
const MESSAGE_TYPE_REQUEST = 1;
const MESSAGE_TYPE_SEND = 2;
const MESSAGE_TYPE_COMMAND = 3;
const SIZEOF_INT = 4;
/**
* Encodes an object into a Tobject ( a bytebuffer prepended with metadata) to send data to a LaunchLike server.
* @param {object} { lObject, lInstanceNumber, lStart, cData }
* @returns {bytebuffer}
*/
const getSendObjectData = ({ lObject, lInstanceNumber, lStart, cData }) => {
const bb = new ByteBuffer(OBJECT_HEADER_SIZE + cData.length);
bb.writeByte(MESSAGE_TYPE_SEND);
bb.writeInt(lObject);
bb.writeInt(lInstanceNumber);
bb.writeInt(lStart);
bb.writeInt(cData.length);
bb.append(cData);
return bb.buffer;
};
/**
* Encodes data required to send a command to a LaunchLike server.
* @param {object} {lCommand, lInstanceNumber}
* @returns {bytebuffer}
*/
const getSendCommandData = ({ lCommand, lInstanceNumber }) => {
const bb = new ByteBuffer(COMMAND_HEADER_SIZE);
bb.writeByte(MESSAGE_TYPE_COMMAND);
bb.writeInt(lCommand);
bb.writeInt(lInstanceNumber);
return bb.buffer;
};
/**
* Encodes data required to make an object request to a LaunchLike server.
* @param {object} {lObject, lInstanceNumber, lStart, lLength}
* @returns {bytebuffer}
*/
const getRequestObjectData = ({
lObject,
lInstanceNumber,
lStart,
lLength,
}) => {
const bb = new ByteBuffer(OBJECT_HEADER_SIZE);
bb.writeByte(MESSAGE_TYPE_REQUEST);
bb.writeInt(lObject);
bb.writeInt(lInstanceNumber);
bb.writeInt(lStart);
bb.writeInt(lLength);
return bb.buffer;
};
/**
* Requests a command from a LaunchLike server with the given code.
* @param {object} {lObject, lInstanceNumber}
* @returns Nothing. This calls a function within the class context provided to it.
*/
const sendCommand = ({ lCommand, lInstanceNumber = 0 }, context) => {
context.bytesToSend({
cData: getSendCommandData({
lCommand,
lInstanceNumber,
}),
});
};
// Experimental
const sendCommandP = ({ lCommand, lInstanceNumber = 0 }, context) => {
return {
cData: getSendCommandData({
lCommand,
lInstanceNumber,
}),
};
};
/**
* Sends an object from a LaunchLike server with the given code and data.
* @param {object} {lObject, lInstanceNumber, lStart, cData}
* @returns Nothing. This calls a function within the class context provided to it.
*/
const sendObject = (
{ lObject, lInstanceNumber = 0, lStart = 0, cData },
context
) => {
context.bytesToSend({
cData: getSendObjectData({
lObject,
lInstanceNumber,
lStart,
cData,
}),
});
};
// Experimental
const sendObjectP = (
{ lObject, lInstanceNumber = 0, lStart = 0, cData },
context
) => {
return {
cData: getSendObjectData({
lObject,
lInstanceNumber,
lStart,
cData,
}),
};
};
/**
* Requests an object from a LaunchLike server with the given code.
* @param {object} {cData} - An object containing the request code, the instance number, the starting position, and the length. Generally only the data is provided.
* @returns Nothing. This calls a function within the class context provided to it.
*/
const requestObject = (
{ lObject, lInstanceNumber = 0, lStart = 0, lLength = 0 },
context
) => {
context.bytesToSend({
cData: getRequestObjectData({
lObject,
lInstanceNumber,
lStart,
lLength,
}),
});
};
// Experimental
const requestObjectP = (
{ lObject, lInstanceNumber = 0, lStart = 0, lLength = 0 },
context
) => {
return {
cData: getRequestObjectData({
lObject,
lInstanceNumber,
lStart,
lLength,
}),
};
};
/**
* Takes a Tobject and parses out a message type definition and length for the data.
* The function then calls a handler method of the context which was provided to it with the transcribed data.
* @param {bytebuffer} cData - The data which you wish to decode
* @param {class} context - The class context from which you're calling this function.
* @returns Nothing. This calls a function within the class context provided to it.
*/
const processBytes = async (cData, context) => {
if (!context) return;
// What stage of byte processing the function is in.
var processingState = 'idle';
var byteBufferInt;
var byteBufferData;
var cMessageType;
var lObjectType;
var lInstanceNumber;
var lStart;
var lLength;
const discharge = ({ reason }, context) => {
var lReceivedObjectType = lObjectType;
var lReceivedInstanceNumber = lInstanceNumber;
switch (cMessageType) {
case MESSAGE_TYPE_REQUEST:
{
let lReceivedStart = lStart;
let lReceivedLength = lLength;
context.ObjectRequested({
lObject: lReceivedObjectType,
lInstanceNumber: lReceivedInstanceNumber,
lOffset: lReceivedStart,
lLength: lReceivedLength,
});
}
break;
case MESSAGE_TYPE_SEND: {
const cData = [...byteBufferData.buffer];
const lReceivedStart = lStart;
context.ObjectReceived({
lObject: lReceivedObjectType,
lInstanceNumber: lReceivedInstanceNumber,
lOffset: lReceivedStart,
cData,
});
break;
}
case MESSAGE_TYPE_COMMAND:
{
context.CommandReceived({
lCommand: lReceivedObjectType,
lInstanceNumber,
});
}
break;
default:
console.log(
'Somehow an invalid data type in the discharge. Something is monumentally messed up'
);
break;
}
processingState = 'idle';
return;
};
cData.forEach((cByte, idx) => {
if (processingState == 'idle') {
cMessageType = cByte;
// Check if cMessageType equals a valid message type
if (
cMessageType == MESSAGE_TYPE_REQUEST ||
cMessageType == MESSAGE_TYPE_SEND ||
cMessageType == MESSAGE_TYPE_COMMAND
) {
// Change the processing state
processingState = 'objectNo';
byteBufferInt = new ByteBuffer(SIZEOF_INT);
return;
} else {
console.log(`Invalid MESSAGE_TYPE`);
return;
}
return;
}
if (processingState == 'objectNo') {
byteBufferInt.writeByte(cByte);
// Once all slots in the byte buffer are occupied, move to next state
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store object type
lObjectType = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
processingState = 'instanceNo';
return;
}
return;
}
if (processingState == 'instanceNo') {
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the instance number
lInstanceNumber = helperFunctions.intFromBytes([
...byteBufferInt.buffer,
]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
// Decide what to do depending on message type.
if (cMessageType == MESSAGE_TYPE_COMMAND) {
discharge({ reason: 'command' }, context);
return;
}
processingState = 'start';
return;
}
return;
}
if (processingState == 'start') {
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the start position
lStart = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
processingState = 'length';
return;
}
return;
}
if (processingState == 'length') {
// 0, 0, 4, 75
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the length
lLength = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
// Allocate length for data or Discharge, if no data payload
if (cMessageType == MESSAGE_TYPE_REQUEST) {
discharge({ reason: 'request' }, context);
return;
}
byteBufferData = new ByteBuffer(lLength);
processingState = 'data';
return;
}
return;
}
if (processingState == 'data') {
byteBufferData.writeByte(cByte);
// Once chunk of data is fully processed, discharge
if (byteBufferData.offset == byteBufferData.limit) {
discharge({ reason: 'data finished' }, context);
return;
}
return;
}
return;
});
};
const processBytesP = async (cData, context) => {
return new Promise(async (res, rej) => {
if (!context) return;
// What stage of byte processing the function is in.
var processingState = 'idle';
var byteBufferInt;
var byteBufferData;
var cMessageType;
var lObjectType;
var lInstanceNumber;
var lStart;
var lLength;
const discharge = ({ reason }, context) => {
var lReceivedObjectType = lObjectType;
var lReceivedInstanceNumber = lInstanceNumber;
switch (cMessageType) {
case MESSAGE_TYPE_REQUEST:
{
let lReceivedStart = lStart;
let lReceivedLength = lLength;
res({
messageType: 'request',
data: {
lObject: lReceivedObjectType,
lInstanceNumber: lReceivedInstanceNumber,
lOffset: lReceivedStart,
lLength: lReceivedLength,
},
});
}
break;
case MESSAGE_TYPE_SEND: {
const cData = [...byteBufferData.buffer];
const lReceivedStart = lStart;
res({
messageType: 'object',
data: {
lObject: lReceivedObjectType,
lInstanceNumber: lReceivedInstanceNumber,
lOffset: lReceivedStart,
cData,
},
});
break;
}
case MESSAGE_TYPE_COMMAND:
{
res({
messageType: 'command',
data: {
lCommand: lReceivedObjectType,
lInstanceNumber,
},
});
}
break;
default:
let e =
'Somehow an invalid data type in the discharge. Something is monumentally messed up';
console.log(e);
rej(e);
break;
}
processingState = 'idle';
return;
};
cData.forEach((cByte, idx) => {
if (processingState == 'idle') {
cMessageType = cByte;
// Check if cMessageType equals a valid message type
if (
cMessageType == MESSAGE_TYPE_REQUEST ||
cMessageType == MESSAGE_TYPE_SEND ||
cMessageType == MESSAGE_TYPE_COMMAND
) {
// Change the processing state
processingState = 'objectNo';
byteBufferInt = new ByteBuffer(SIZEOF_INT);
return;
} else {
console.log(`Invalid MESSAGE_TYPE`);
return;
}
return;
}
if (processingState == 'objectNo') {
byteBufferInt.writeByte(cByte);
// Once all slots in the byte buffer are occupied, move to next state
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store object type
lObjectType = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
processingState = 'instanceNo';
return;
}
return;
}
if (processingState == 'instanceNo') {
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the instance number
lInstanceNumber = helperFunctions.intFromBytes([
...byteBufferInt.buffer,
]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
// Decide what to do depending on message type.
if (cMessageType == MESSAGE_TYPE_COMMAND) {
discharge({ reason: 'command' }, context);
return;
}
processingState = 'start';
return;
}
return;
}
if (processingState == 'start') {
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the start position
lStart = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
processingState = 'length';
return;
}
return;
}
if (processingState == 'length') {
// 0, 0, 4, 75
byteBufferInt.writeByte(cByte);
if (byteBufferInt.offset == byteBufferInt.limit) {
// Store the length
lLength = helperFunctions.intFromBytes([...byteBufferInt.buffer]);
// Reset byteBufferInt
byteBufferInt = new ByteBuffer(SIZEOF_INT);
// Allocate length for data or Discharge, if no data payload
if (cMessageType == MESSAGE_TYPE_REQUEST) {
discharge({ reason: 'request' }, context);
return;
}
byteBufferData = new ByteBuffer(lLength);
processingState = 'data';
return;
}
return;
}
if (processingState == 'data') {
byteBufferData.writeByte(cByte);
// Once chunk of data is fully processed, discharge
if (byteBufferData.offset == byteBufferData.limit) {
discharge({ reason: 'data finished' }, context);
return;
}
return;
}
return;
});
});
};
module.exports.smokeSignal = {
getSendObjectData,
getSendCommandData,
getRequestObjectData,
sendCommand,
sendObject,
requestObject,
processBytes,
};
module.exports.smokeSignalPromise = {};
module.exports.constants = {
OBJECT_HEADER_SIZE,
COMMAND_HEADER_SIZE,
MESSAGE_TYPE_REQUEST,
MESSAGE_TYPE_SEND,
MESSAGE_TYPE_COMMAND,
SIZEOF_INT,
};