uci-utils-byte/src/byte.js

445 lines
10 KiB
JavaScript

// third party modules
import bits from 'bitwise/bits/index.js'
import { toBits } from 'bitwise/string/index.js'
import { readUInt } from 'bitwise/buffer/index.js'
import { create as createBuffer } from 'bitwise/buffer/index.js'
import { read as readByte, write as writeByte } from 'bitwise/byte/index.js'
//local
import * as array from './array.js'
import logger from '@uci-utils/logger'
let log = logger({package:'@uci-utils/byte'})
// 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({msg:'byte and state as passed', op:op, byte:byteFormat(byte, { in: format.in, out: 'ARY' }), value:this.toFmt('ARY')})
log.debug({msg:'byte and state as passed', byte:byte, value:this.value, })
let opbyte = bwop(
this.toFmt('ARY'),
byteFormat(byte, { in: format.in, out: 'ARY' })
)
// console.log('>>>>',opbyte)
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
}
log.debug({prev:prev, cur:curr, bitchanges:bitchanges, msg:'state changes'})
let nowon = curr.map((bit, i) => {
log.debug({bit:bit, bitchangesi:bitchanges[i], msg:'now on state '})
return bit & bitchanges[i]
})
nowon = byteFormat(nowon, {
in: 'ARY',
out: 'PLC'
})
log.debug({nowon:nowon, msg:'now on state '})
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
log.debug('input: ', byte, ' input format: ', ifmt, ' output format:', ofmt)
let decimal = 0
switch (ifmt) {
case 'STR':
decimal = writeByte(toBits(byte.padStart(8,'0')))
break
case 'BUF':
decimal = readUInt(byte, 0, 8)
break
case 'DEC':
decimal = +byte // make decimal as string is a number
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
}
if ( decimal<0 || decimal >255 || isNaN(decimal)) {
log.warn ({decimal:decimal, byte:byte, fmt:fmt, msg:'error - invalid byte - not able to convert to decmial, converting to 0'})
decimal = 0
}
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:
log.warn({byte:byte, fmt:fmt, msg:'error - unkown 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))
}