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