broken out from uci-utils into it's own repository. fixed issues with PLC conversion of multiple bytes and big/little endian

master
David Kebler 2019-02-16 11:45:41 -08:00
commit 80b50b3b9a
9 changed files with 754 additions and 0 deletions

37
.eslintrc.js Normal file
View File

@ -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"
]
}
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules/
/coverage/

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
tests/
test/
*.test.js
testing/

39
package.json Normal file
View File

@ -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"
}
}

2
readme.md Normal file
View File

@ -0,0 +1,2 @@
### Byte Me
#### a uCOMmandIt Byte Class and Utility Functions

51
src/array.js Normal file
View File

@ -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 }

437
src/byte.js Normal file
View File

@ -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))
}

47
test/array.test.js Normal file
View File

@ -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'])
})
})

135
test/byte.test.js Normal file
View File

@ -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)
})
})