commit 80b50b3b9a65d40abbd0dd95aa98635ff39d54e4 Author: David Kebler Date: Sat Feb 16 11:45:41 2019 -0800 broken out from uci-utils into it's own repository. fixed issues with PLC conversion of multiple bytes and big/little endian diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..49bac18 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,37 @@ +module.exports = { + "ecmaFeatures": { + "modules": true, + "spread" : true, + "restParams" : true + }, + // "plugins": [ + // "unicorn" + // ], + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 2017, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + // "unicorn/no-array-instanceof": "error", + "no-console": 0, + "semi": ["error", "never"], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61051f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/coverage/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f16fc41 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +tests/ +test/ +*.test.js +testing/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..f6e5974 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "@uci-utils/byte", + "version": "0.2.0", + "description": "Byte Class and related functions", + "main": "src/byte.js", + "scripts": { + "test": "./node_modules/.bin/mocha -r esm --timeout 30000", + "testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000' || exit 0", + "testdd": "UCI_LOG_LEVEL='trace' npm run testd", + "testde": "UCI_LOG_LEVEL='warn' npm run testd", + "testl": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log 0 npm run test || exit 0" + }, + "author": "David Kebler", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/uCOMmandIt/uci-utils.git" + }, + "keywords": [ + "node.js", + "communication", + "serial", + "utilities", + "helpers" + ], + "bugs": { + "url": "https://github.com/uCOMmandIt/uci-utils/issues" + }, + "homepage": "https://github.com/uCOMmandIt/uci-utils#readme", + "dependencies": { + "bitwise": "^2.0.1" + }, + "devDependencies": { + "chai": "^4.2.0", + "esm": "^3.2.4", + "mocha": "^5.2.0", + "nodemon": "^1.18.10" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6c22c54 --- /dev/null +++ b/readme.md @@ -0,0 +1,2 @@ +### Byte Me +#### a uCOMmandIt Byte Class and Utility Functions diff --git a/src/array.js b/src/array.js new file mode 100644 index 0000000..fc86ef6 --- /dev/null +++ b/src/array.js @@ -0,0 +1,51 @@ +// example jsdoc syntax +// // ----------------------------------- +// // Values +// +// /** +// * Get the object type string +// * @param {any} value +// * @returns {string} +// */ +// function getObjectType (value /* :mixed */) /* :string */ { +// return Object.prototype.toString.call(value) +// } + +function flatten(array) { + const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []) + return flatten(array) +} + +function sum(arr) { + return arr.reduce(function (a, b) { + return a + b + }) +} + +function padStart (arr, len, c) { + len = len || 8 + c = c || 0 + if (len < arr.length) { + return false + } + let pad = len - arr.length + for (var i = 0; i < pad; i++) { + arr.unshift(c) + } + return arr +} + +function padEnd (arr, len, c) { + len = len || 8 + c = c || 0 + if (len < arr.length) { + return false + } + let pad = len - arr.length + for (var i = 0; i < pad; i++) { + arr.push(c) + } + return arr +} + +export { flatten, sum, padStart, padEnd } diff --git a/src/byte.js b/src/byte.js new file mode 100644 index 0000000..ad213f2 --- /dev/null +++ b/src/byte.js @@ -0,0 +1,437 @@ +// third party modules +import bits from 'bitwise/bits' +import toBits from 'bitwise/string/to-bits' +import readUInt from 'bitwise/buffer/read-u-int' +import createBuffer from 'bitwise/buffer/create' +import readByte from 'bitwise/byte/read' +import writeByte from 'bitwise/byte/write' +//local +import * as array from './array' + +// Classes + +// Byte Class is a single byte value with an attached format designation which allows easy +// conversion of the byte to any of the other formats +// of ['STR', 'BUF', 'BUF', 'DEC', 'HEX', 'ARY', 'PLC'] like this +// examples: +// '1100101', 'STR' will be left padded to 8 chars with 0 +// Buffer.from([0x65]), 'BUF' +// 101, 'DEC' +// 65, 'HEX' +// [1,1,0,0,1,0,1], 'ARY' will be left padded/unshifted to 8 elements, with 0 +// [7,3,1,6], 'PLC' any order, the number of the bit that is "on" or "1" from 1-8, 1=rightmost LSB + +/** + * Byte - A class to hold and manipulate a bitwise byte + */ +class Byte { + /** + * constructor - Description + * + * @param {number} [byte=0] Description + * @param {string} [format=DEC] Description + * + * @returns {Object} instance of Byte Class + */ + + constructor(byte = 0, format = 'DEC') { + this.cur = byte + this.prv = byte + this.format = format + } + + static validateFmt(fmt) { + return ['STR', 'BUF', 'DEC', 'HEX', 'ARY', 'PLC'].indexOf(fmt) === -1 + ? false + : true + } + + // getter/setters + get value() { + return this.cur + } + set value(byte) { + this.prv = this.cur + this.cur = byte + } + + get fmt() { + return this.format + } + + set fmt(f) { + this.cur = this.toFmt(f) + this.prv = byteFormat(this.prv, { in: this.format, out: f }) + this.format = f + } + + get prev() { + return this.prv + } + + toFmt(fmt) { + return byteFormat(this.cur, { in: this.format, out: fmt }) + } + + toFmtPrev(fmt) { + return byteFormat(this.prv, { in: this.format, out: fmt }) + } + + reset(byte, fmt) { + this.cur = byte + this.prv = byte + this.format = fmt ? fmt : 'DEC' + } + + // used for setting bytes when passed byte format needs to be adapted to current instance's format + adaptFmt(byte, format) { + return byteFormat(byte, { in: format, out: this.format }) + } + + bwOp(byte, op, format = { in: 'DEC', out: 'DEC' }) { + let bwop + switch (op) { + case 'toggle': + bwop = bits.xor + break + case 'off': // NOT mask then AND which is not the same as NAND = AND then NOT result + bwop = (byte, mask) => { + return bits.and(byte, bits.not(mask)) + } + break + case 'on': + bwop = bits.or + break + case 'check': + bwop = bits.and + break + } + // console.log('byte and state as passed', byte, this.value) + let opbyte = bwop( + this.toFmt('ARY'), + byteFormat(byte, { in: format.in, out: 'ARY' }) + ) + return byteFormat(opbyte, { in: 'ARY', out: format.out }) + } + + changes() { + return bitChanges(this.toFmt('ARY'), this.toFmtPrev('ARY')) + } + + stateChanges() { + return stateChanges(this.toFmt('ARY'), this.toFmtPrev('ARY')) + } +} // end Byte Class + +// Library functions and objects + +const BYTE_FORMATS = ['STR', 'BUF', 'DEC', 'HEX', 'ARY', 'PLC'] + +//can be used with the BYTE Class or stand alone if you supply by and fmt object +//can accept single multiple byte input +// multiple bytes stored in array with most significant byte with index 0 +function byteFormat(bytes, fmt) { + // TODO support STR and ARY formats that a long string or single flat array rather than array of each + + if (fmt.in === 'PLC') { + bytes = plc2bytes(bytes) + return byteFormat(bytes.length === 1 ? bytes[0] : bytes, { + in: 'DEC', + out: fmt.out + }) + } + + if (fmt.in === 'BUF') { + bytes = buf2ary(bytes) + return byteFormat(bytes.length === 1 ? bytes[0] : bytes, { + in: 'DEC', + out: fmt.out + }) + } + + // It's a single byte + if ( + (fmt.in === 'ARY' && !Array.isArray(bytes[0])) || + (fmt.in !== 'ARY' && !Array.isArray(bytes)) + ) { + return format(bytes, fmt) + } + + // process multiple bytes + + if (fmt.out === 'PLC') { + return bytes2plc(byteFormat(bytes, { in: fmt.in, out: 'DEC' })) + } + if (fmt.out === 'BUF') { + if (fmt.in !== 'DEC') { + bytes = byteFormat(bytes, { in: fmt.in, out: 'DEC' }) + } + return Buffer.from(bytes) + } + // everything else + return bytes.map(byte => { + return format(byte, fmt) + }) +} + +// curr and prev need to be arrays of bits as arrays ARY +// returns changes if any otherwise false +function bitChanges(curr, prev) { + let changes = bits.xor(curr, prev) + + // nothing changed + if (!array.sum(changes)) { + return false + } + // something changed + return changes +} + +// all bit changes +// changes and curr need to be an array of arrays of 8 bits/byte in ARY format +// returns array of array pairs +// where each pair is the bit that changed and it's current state (1=on,0=off) +function stateChanges(curr, prev) { + let bitchanges = bitChanges(curr, prev) + if (!bitchanges) { + return false + } + console.log(prev) + console.log(curr) + console.log(bitchanges) + + let nowon = curr.map((bit, i) => { + console.log(bit, bitchanges[i], bit & bitchanges[i]) + return bit & bitchanges[i] + }) + nowon = byteFormat(nowon, { + in: 'ARY', + out: 'PLC' + }) + + console.log('nowon', nowon) + + return byteFormat(bitchanges, { + in: 'ARY', + out: 'PLC' + }).map(swtch => { + return [swtch, nowon.indexOf(swtch) === -1 ? 'off' : 'on'] + }) +} + +function bitsReverse(byte) { + // in DEC out DEC + return writeByte(readByte(byte).reverse()) +} + +// exports here +export default Byte +export { Byte, byteFormat, BYTE_FORMATS, bitsReverse, bitChanges, stateChanges } + + +///////// module scoped not exported functions ///// + +/* + * + * + * + * + * + * + */ +// Takes a SINGLE byte in one format and returns it in another, +// byteFormat function above allows multiple bytes and uses this +const format = function(byte, fmt) { + let ifmt = fmt.in || 'STR' // default STR + let ofmt = fmt.out || 'DEC' // default DEC + // debug.L2('input: ', byte, ' input format: ', ifmt, ' output format:', ofmt); + + let decimal = NaN + switch (ifmt) { + case 'STR': + decimal = writeByte(toBits(byte.padStart(8,'0'))) + break + case 'BUF': + decimal = readUInt(byte, 0, 8) + break + case 'DEC': + decimal = byte + break + case 'HEX': + decimal = parseInt(byte, 16) + break + case 'ARY': + decimal = writeByte(array.padStart(byte)) + break + case 'PLC': + decimal = plc2dec(byte) + break + default: + ofmt = false + break + } + + let output + //TODO only BIT and DEC are working + switch (ofmt) { + case 'STR': + output = bits.toString(readByte(decimal)) + break + case 'BUF': + output = createBuffer(readByte(decimal)) + break + case 'DEC': + output = decimal + break + case 'HEX': + output = decimal.toString(16) + break + case 'ARY': + output = readByte(decimal) + break + case 'PLC': + output = dec2plc(decimal) + break + default: + console.log('unknown input or output format') + output = false + break + } + + return output +} + +/* + * + * + * + */ +function bytes2plc(bytes) { + // bytes must be in 'DEC' format first + + let plc = bytes.map((byte, bytenum) => { + return format(byte, { + in: 'DEC', + out: 'PLC' + }).map(cmpt => { + return bytenum * 8 + cmpt + }) + }) + + plc = plc.reduce(function(a, b) { + // combine plc arrays + return a.concat(b) + }, []) + plc.sort((a, b) => a - b) + return plc +} + +/* + * + * + * + */ +function plc2bytes(cmpts) { + // cmpts is an array of bit numbers that can be of any value (beyond 8 ok) + // returns an object of pairs, each pair a byte number (bank) and the value for that byte(bank) in + // in 'DEC' format. + + let UCI_ENDIAN = process.env.UCI_ENDIAN || 'little' + + let byte = 0, + cmpt = 0, + maxbyte = 0, + bytes = {} + + for (let i in cmpts) { + cmpt = cmpts[i] + byte = Math.floor((cmpt - 1) / 8) + 1 + if (byte > maxbyte) { + maxbyte = byte + } + if (bytes[byte] === undefined) { + bytes[byte] = [] + } + bytes[byte].push(cmpt % 8 === 0 ? 8 : cmpt % 8) + } + let bytearr = [] + // bytes index 0 with be least signficant, need to reverse that + for (byte in bytes) { + bytes[byte] = format(bytes[byte], { + in: 'PLC', + out: 'DEC' + }) + if (UCI_ENDIAN==='little') bytearr[byte-1] = bytes[byte] + else bytearr[maxbyte - byte] = bytes[byte] + } + + // pad in "zero" for missing byte elements + for (let i = 0; i < maxbyte-1; i++) { + if (!bytearr[i]) { + bytearr[i] = 0 + } + } + // log.debug({places:cmpts, bytes:bytearr, endian:UCI_ENDIAN, msg:'plc to bytes conversion') + return bytearr +} + +// can't just do split map because split doesn't work on single number +function numStr2Arr(str, del) { + del = del || ' ' //default delimimter is a space, see string.protoype.split + // let arr = [] + // TODO change to single line + if (typeof str === 'number') { + // just a single number + // arr[0] = parseInt(str, 10) + // return arr; + return parseInt(str, 10) + } else { + //it's a string of numbers + return str.split(del).map(Number) + } +} + +function bits2dec(bits) { + let decimal = 0 + let place = bits.length + bits.split('').forEach(function(bit) { + decimal += Math.pow(2, bit * (place - 1)) + --place + }) + return decimal +} + +function plc2dec(nums, string) { + let decimal = 0 + // log.debug('nums', nums) + + if (string) { + nums = numStr2Arr(nums) + } + nums.forEach(function(num) { + decimal += Math.pow(2, num - 1) + // log.debug('place', num, 'dec ', decimal); + }) + return decimal +} + +function dec2plc(decimal, toStr) { + //outputs an array unless str is true + let place = 8 + let plc = [] + readByte(decimal).forEach(function(bit) { + if (bit) { + plc.push(place) + } + --place + }) + if (toStr) { + return plc.join(' ') + } else { + return plc + } +} + +function buf2ary(buf) { + return Array.from(new Uint8Array(buf)) +} diff --git a/test/array.test.js b/test/array.test.js new file mode 100644 index 0000000..cd74c6c --- /dev/null +++ b/test/array.test.js @@ -0,0 +1,47 @@ +let date = new Date(Date.now()) +console.log('run:', date.getMinutes(), ':', date.getSeconds()) + +import { expect } from 'chai' +import * as _ from '../src/array' + + +describe('Array Library - ', function () { + + it('Should flatten an array', function () { + expect(_.flatten([ + [1], + [2], + [3] + ])).deep.equal([1, 2, 3]) + expect(_.flatten([1, 2, 3])).deep.equal([1, 2, 3]) + }) + it('Should sum an array', function () { + expect(_.sum([1, 2, 3])).to.equal(6) + expect(_.sum([1, 'shut', 'up'])).to.equal('1shutup') + }) + + it('Should pad left an array to 8 with zeros by defaul', function () { + expect(_.padStart([1, 2, 3])).to.deep.equal([0,0,0,0,0,1,2,3]) + }) + + it('Should ignore pad when not longer than array', function () { + expect(_.padStart([1, 2, 3],3,'a')).to.deep.equal([1,2,3]) + }) + + it('Should pad left an array with character', function () { + expect(_.padStart([1, 2, 3],5,'a')).to.deep.equal(['a','a',1,2,3]) + }) + + it('Should pad right an array to 8 with zeros by defaul', function () { + expect(_.padEnd([1, 2, 3])).to.deep.equal([1,2,3,0,0,0,0,0]) + }) + + it('Should ignore pad when not longer than array', function () { + expect(_.padEnd([1, 2, 3],3,'a')).to.deep.equal([1,2,3]) + }) + + it('Should pad left an array with character', function () { + expect(_.padEnd([1, 2, 3],5,'a')).to.deep.equal([1,2,3,'a','a']) + }) + +}) diff --git a/test/byte.test.js b/test/byte.test.js new file mode 100644 index 0000000..74fc8fc --- /dev/null +++ b/test/byte.test.js @@ -0,0 +1,135 @@ +let date = new Date(Date.now()) +console.log('run:', date.getMinutes(), ':', date.getSeconds()) + +import { expect } from 'chai' +import * as _ from '../src/byte' + +describe('Byte Library - ', function () { + + it('Should Convert Byte Format for Single Byte', function () { + + let byte = '10000001' + let fmt = {} + + let results = ['10000001', Buffer.from([0x81]), 129, '81', [1, 0, 0, 0, 0, 0, 0, 1], + [8, 1] + ] + + _.BYTE_FORMATS.forEach((ifmt, i) => { + byte = results[i] + fmt.in = ifmt + // console.log('input', ifmt, byte) + _.BYTE_FORMATS.forEach((ofmt, o) => { + fmt.out = ofmt + // console.log(fmt.out, _.byteFormat(byte, fmt)) + expect(_.byteFormat(byte, fmt)).deep.equal(results[o]) + }) + }) + + }) + + it('Should Convert from Any Format to Any Other', function () { + + let bytes + let fmt = {} + + let results = [ + ['10000001', '01010111'], + Buffer.from([0x81, 0x57]), [129, 87], + ['81', '57'], + [ + [1, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 1, 0, 1, 1, 1] + ], + [1, 8, 9, 10, 11, 13, 15] + ] + + _.BYTE_FORMATS.forEach((ifmt, i) => { + bytes = results[i] + fmt.in = ifmt + // console.log('input', ifmt, bytes) + _.BYTE_FORMATS.forEach((ofmt, o) => { + fmt.out = ofmt + // console.log('=',i, fmt.in, bytes, o, fmt.out, _.byteFormat(bytes, fmt)) + expect(_.byteFormat(bytes, fmt)).deep.equal(results[o]) + }) + }) + + }) + + it('It should deal with missing byte when converting from PLC', function () { + + let fmt={} + + // check for missing place + fmt.in = 'PLC' + fmt.out = 'DEC' + let bytes = [1, 8, 17] + expect(_.byteFormat(bytes, fmt)).deep.equal([129, 0, 1]) + + }) + + it('PLC should return as with Big Endian when set in environment', function () { + // check Big endian + + let fmt={} + process.env.UCI_ENDIAN='big' + fmt.in = 'PLC' + fmt.out = 'DEC' + let bytes = [1, 8, 17] + expect(_.byteFormat(bytes, fmt)).deep.equal([1, 0, 129]) + + }) + +}) + +describe('Byte Class - ', function () { + + let byte = new _.Byte(20, 'HEX') + let byte2 = new _.Byte(20) + + it('Should set the default format to DEC', function () { + + expect(byte2.fmt).to.equal('DEC') + }) + + it('Should output an alternative format', function () { + expect(byte.toFmt('DEC')).to.equal(32) + expect(byte.toFmt('STR')).to.equal('00100000') + }) + + it('Verify getters and setters ', function () { + // value getter and setter + expect(byte.value, 'value getter failed').to.equal(20) + byte.value = 40 + expect(byte.value, 'value setter failed').to.equal(40) + expect(byte.prev, 'previous value not set').to.equal(20) + + // format getter/setter + expect(byte.fmt, 'format getter failed').to.equal('HEX') + byte.fmt = 'STR' + expect(byte.fmt, 'format setter failed').to.equal('STR') + expect(byte.value, 'format getter value change failed').to.equal('01000000') + }) + + it('should update value with format', function () { + // change the value with same format + // change value and set format + byte.reset(32) + expect(byte.value, 'value reset failed').to.equal(32) + expect(byte.value, 'previous value reset failed').to.equal(32) + expect(byte.fmt, 'default format reset failed').to.equal('DEC') + byte.reset(20, 'HEX') + expect(byte.value, 'value reset failed').to.equal(20) + expect(byte.fmt, 'format reset failed').to.equal('HEX') + byte.reset('10000000', 'STR') + expect(byte.toFmt('DEC'), 'string reset and convert failed').to.equal(128) + }) + + it('!!!-should find bit changes and state of those changes between previous and current', function () { + // change the value with same format + // change value and set format + expect(true, 'value reset failed').to.equal(true) + }) + +})