@hamstudy/flamp
Version:
JavaScript Amateur Multicast Protocol AMP-2 Version 3 Implemented from specification document http://www.w1hkj.com/files/flamp/Amp-2.V3.0.Protocol.pdf • Version 1.0.0 - W5ALT, Walt Fair, Jr. (Derived From) • Version 2.0.0 - W1HKJ, Dave Freese, w
316 lines (315 loc) • 12.3 kB
JavaScript
"use strict";
// JavaScript Amateur Multicast Protocol AMP-2 Version 3
// Implemented from specification document
// http://www.w1hkj.com/files/flamp/Amp-2.V3.0.Protocol.pdf
// • Version 1.0.0 - W5ALT, Walt Fair, Jr. (Derived From)
// • Version 2.0.0 - W1HKJ, Dave Freese, w1hkj@w1hkj.com
// • Version 2.0.1 - W1HKJ, Dave Freese, w1hkj@w1hkj.com, 5 Oct 2012
// • Version 3.0.0 - KK5VD, Robert Stiles, kk5vd@yahoo.com, 21 April 2013
// • Javascript Implementation by KV9G, Michael Stufflebeam cpuchip@gmail.com, 29 June 2018
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.Amp = exports.CompressionType = exports.BaseEncode = exports.ControlWord = exports.LTypes = exports.MODIFIED_TIME_REGEX = exports.lzmaCompressedPrefix = void 0;
exports.dateToString = dateToString;
exports.stringToDate = stringToDate;
exports.uint8ToBinaryString = uint8ToBinaryString;
const tslib_1 = require("tslib");
const block_1 = require("./block");
const crc16_1 = require("./crc16");
const compressor_1 = require("./compressor");
const base91 = tslib_1.__importStar(require("./base91"));
exports.lzmaCompressedPrefix = '\u0001LZMA';
exports.MODIFIED_TIME_REGEX = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/;
/**
* Takes a date and returns a string in format YYYYMMDDhhmmss
*/
function dateToString(d) {
const year = d.getFullYear();
const month = (`0${d.getMonth() + 1}`).slice(-2);
const day = (`0${d.getDate()}`).slice(-2);
const hours = (`0${d.getHours()}`).slice(-2);
const minutes = (`0${d.getMinutes()}`).slice(-2);
const seconds = (`0${d.getSeconds()}`).slice(-2);
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
/**
* Takes a string in format YYYYMMDDhhmmss and returns a date
*/
function stringToDate(str) {
return new Date(str.replace(exports.MODIFIED_TIME_REGEX, '$1-$2-$3T$4:$5:$6'));
}
function uint8ToBinaryString(bytes) {
let s = "";
const CHUNK = 0x8000; // 32768
for (let i = 0; i < bytes.length; i += CHUNK) {
s += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
}
return s;
}
const unprintableRegex = /[^ -~\n\r]+/;
function hasNotPrintable(c) {
return unprintableRegex.test(c);
}
function assertUnreachable(x) {
throw new Error("Invalid case");
}
var LTypes;
(function (LTypes) {
LTypes["FILE"] = "FILE";
LTypes["ID"] = "ID";
LTypes["SIZE"] = "SIZE";
LTypes["DESC"] = "DESC";
LTypes["DATA"] = "DATA";
LTypes["PROG"] = "PROG";
LTypes["CNTL"] = "CNTL";
})(LTypes || (exports.LTypes = LTypes = {}));
var ControlWord;
(function (ControlWord) {
ControlWord["EOF"] = "EOF";
ControlWord["EOT"] = "EOT";
})(ControlWord || (exports.ControlWord = ControlWord = {}));
var BaseEncode;
(function (BaseEncode) {
BaseEncode["b64"] = "base64";
BaseEncode["b91"] = "base91";
// b128 = 'base128',
// b256 = 'base256',
})(BaseEncode || (exports.BaseEncode = BaseEncode = {}));
var CompressionType;
(function (CompressionType) {
CompressionType["LZMA"] = "LZMA";
})(CompressionType || (exports.CompressionType = CompressionType = {}));
class Amp {
static toString(blocks, fromCallsign = '', toCallsign = '') {
if (!Object.keys(blocks || {}).length) {
return '';
}
let preProtocolHeaders;
if (toCallsign && fromCallsign) {
preProtocolHeaders = `${toCallsign} DE ${fromCallsign}\n\n`;
}
else if (toCallsign) {
preProtocolHeaders = `${toCallsign} DE ME\n\n`;
}
else if (fromCallsign) {
preProtocolHeaders = `QST DE ${fromCallsign}\n\n`;
}
else {
preProtocolHeaders = "QST\n\n";
}
const blockStrings = [];
if (preProtocolHeaders) {
blockStrings.push(preProtocolHeaders);
}
// Looping through the keywords in a specific order to build the output string
for (let key of [
LTypes.PROG,
LTypes.FILE,
LTypes.ID,
LTypes.SIZE,
LTypes.DESC,
LTypes.DATA,
ControlWord.EOF,
ControlWord.EOT
]) {
if (key === LTypes.DATA) {
for (const k of Object.keys(blocks)) {
if (isNaN(Number(k))) {
continue;
}
const block = blocks[k];
blockStrings.push(block.toString());
}
continue;
}
let block = blocks[key];
if (key === LTypes.ID) {
if (!fromCallsign) {
continue;
}
if (!block || fromCallsign !== block.data) {
let hash = '';
if (block) {
hash = block.hash;
}
else {
let k = Object.keys(blocks)[0];
if (k) {
hash = blocks[k].hash;
}
}
if (hash) {
block = block_1.Block.MakeBlock({ keyword: LTypes.ID, hash, data: fromCallsign });
}
}
}
if (!block) {
continue;
}
blockStrings.push(block.toString());
}
if (fromCallsign) {
blockStrings.push(fromCallsign);
}
return blockStrings.join('\n');
}
static getHash(filename, modified, compressed, baseConversion, blockSize) {
// | DTS : FN |C| B |BS|
// DTS = Date/Time Stamp
// FN = File Name
// C = Compression 1=ON,0=OFF
// B = Base Conversion (base64, base128, or base256)
// BS = Block Size, 1 or more characters
// | = Field separator.
let DTS = dateToString(modified);
return (0, crc16_1.crc16)(`${DTS}:${filename}${compressed ? '1' : '0'}${baseConversion}${blockSize}`);
}
fromCallsign;
toCallsign;
filename;
fileDescription;
fileModifiedTime;
inputBuffer;
blkSize;
PROGRAM = "JSAMP";
VERSION = "1.1.8";
base = "";
compression = false;
forceCompress = false;
blocks = {};
hash = '';
dataBlockCount = 0;
skipProgram = false;
useEOF = true;
useEOT = true;
// Fields used in receiving.
receivedFiles = {};
constructor(opts) {
this.fromCallsign = opts.fromCallsign || null;
this.toCallsign = opts.toCallsign || null;
this.filename = opts.filename;
this.fileDescription = opts.fileDescription || '';
this.fileModifiedTime = opts.fileModifiedTime;
this.blkSize = opts.blkSize;
this.compression = opts.compression || false;
this.forceCompress = !!opts.forceCompress;
this.skipProgram = !!opts.skipProgram;
this.useEOF = opts.useEOF !== false;
this.useEOT = opts.useEOT !== false;
if (opts.base) {
this.setBase(opts.base);
}
this.inputBuffer = opts.inputBuffer;
this.hash = Amp.getHash(this.filename, this.fileModifiedTime, !!this.compression, this.base, this.blkSize);
this.makeBlocks();
}
makeBlocks() {
this.blocks = {};
this.dataBlockCount = this.quantizeMessage();
if (!this.skipProgram) {
this.blocks[LTypes.PROG] = block_1.Block.MakeBlock({ keyword: LTypes.PROG, hash: this.hash, data: `${this.PROGRAM} ${this.VERSION}` });
}
this.blocks[LTypes.FILE] = block_1.Block.MakeBlock({ keyword: LTypes.FILE, hash: this.hash, data: `${dateToString(this.fileModifiedTime)}:${this.filename}` });
if (this.fromCallsign) {
this.blocks[LTypes.ID] = block_1.Block.MakeBlock({ keyword: LTypes.ID, hash: this.hash, data: this.fromCallsign });
}
if (this.fileDescription) {
this.blocks[LTypes.DESC] = block_1.Block.MakeBlock({ keyword: LTypes.DESC, hash: this.hash, data: this.fileDescription });
}
this.blocks[LTypes.SIZE] = block_1.Block.MakeBlock({ keyword: LTypes.SIZE, hash: this.hash, data: `${this.inputBuffer.length} ${this.dataBlockCount} ${this.blkSize}` });
if (this.useEOF) {
this.blocks[ControlWord.EOF] = block_1.Block.MakeBlock({ keyword: LTypes.CNTL, hash: this.hash, controlWord: ControlWord.EOF });
}
if (this.useEOT) {
this.blocks[ControlWord.EOT] = block_1.Block.MakeBlock({ keyword: LTypes.CNTL, hash: this.hash, controlWord: ControlWord.EOT });
}
}
toString(dataBlockList, includeHeaders = true) {
const blocks = {};
for (const key of Object.keys(this.blocks)) {
if ((isNaN(Number(key)) ? !includeHeaders : (!dataBlockList || dataBlockList.indexOf(Number(key)) === -1))
|| (key === LTypes.PROG && this.skipProgram)
|| (key === LTypes.ID && !this.fromCallsign)
|| (key === LTypes.DESC && !this.fileDescription)
|| (key === ControlWord.EOF && !this.useEOF)
|| (key === ControlWord.EOT && !this.useEOT)) {
continue;
}
blocks[key] = this.blocks[key];
}
return Amp.toString(blocks, this.fromCallsign || '', this.toCallsign || '');
}
getDataBlockCount() { return this.dataBlockCount; }
/**
* The base to use for transmitting the data, if any
* @param base base64 or base91
*/
setBase(base) {
this.base = base;
}
baseEncode(base, data) {
switch (base) {
case BaseEncode.b64:
return `[b64:start]${btoa(uint8ToBinaryString(data))}[b64:end]`;
case BaseEncode.b91:
return `[b91:start]${base91.encode(data)}[b91:end]`;
default:
return assertUnreachable(base);
}
}
quantizeMessage() {
let actualBuffer = this.inputBuffer;
let needsBase = hasNotPrintable(actualBuffer);
if (needsBase && !this.base) {
// For our purposes we're forcing some base conversion if there are unprintable characters
this.base = BaseEncode.b91;
}
let baseApplied = false;
// Apply compression if any
if (this.compression === CompressionType.LZMA) {
let c = compressor_1.Compressor.getCompressor(); // get the default compressor
try {
const compressedBuffer = c.compress(actualBuffer);
const newBuffer = this.baseEncode(this.base || BaseEncode.b91, compressedBuffer);
if (newBuffer.length < actualBuffer.length - 200 || this.forceCompress) {
// If compression doesn't save us at least 200 bytes it's not worth while
actualBuffer = newBuffer;
baseApplied = true;
}
}
catch (e) {
console.error('Compression failed, continuing without compression', e);
}
}
// Apply base64 or base91 encoding here
if (this.base && !baseApplied) {
actualBuffer = this.baseEncode(this.base, new TextEncoder().encode(actualBuffer));
baseApplied = true;
}
if (actualBuffer.length > this.inputBuffer.length
&& !needsBase && !this.forceCompress) {
// If all characters were printable and it's shorter without the conversion
// then let's just send it without
actualBuffer = this.inputBuffer;
}
let numbOfBlocks = Math.floor(actualBuffer.length / this.blkSize);
if (actualBuffer.length % this.blkSize > 0) {
numbOfBlocks++;
}
let blockNum = 1;
let start = 0;
while (start < actualBuffer.length) {
let block = block_1.Block.MakeBlock({
keyword: LTypes.DATA,
hash: this.hash,
data: actualBuffer.slice(start, start + this.blkSize),
blockNum
});
this.blocks[blockNum] = block;
start += this.blkSize;
blockNum++;
}
return numbOfBlocks;
}
}
exports.Amp = Amp;