UNPKG

i2c-ftdi-d2xx

Version:

i2c protocol implementation with ftdi-d2xx on standard boards that features no level translators on SDA and SCL

453 lines (416 loc) 20.4 kB
const {isArray, groupBy, isObject} = require("lodash"); const ftdi = require("./src/ftdi"); const i2c = require("./src/i2c"); const regmap = require("./src/regmap"); const utils = require("./src/utils"); const DEFAULT_FILTER = "FT2232H.*[^B]$" /** * Represents an FTDI-D2XX device. * @typedef {Object} Device * @property {string} serial_number - The device serial number. * @property {string} description - The device custom description. * @property {string} type - The device type specified by the manufacturer. * @property {boolean} is_open - Boolean specifying if the device is already opened or not. * @property {number} usb_vid - * @property {number} usb_pid - * @property {number} usb_loc_id - * @property {string} usb_speed - */ /** * Represents a base response as success/fail and possible errors * @typedef {Object} BaseResponse * @property {boolean} success - True is request was successfully accomplisced and false otherwise * @property {string[]} [errors] - Possible errors encountered */ /** * Represents a minimal I2C response as ack or nack and data for read requests * @typedef {Object} I2cResponse * @property {boolean} ack - True if all 9th bits of the I2C communication are a logic 0, false otherwise * @property {number|number[]} [data] - Optional returned data for read requests */ /** * Represents a standard I2C write or write burst request * @typedef {Object} I2cWriteRequest * @property {number} slave - I2C slave address * @property {number} address - I2C register address * @property {number|number[]} data - I2C data * @property {boolean} simulated - I2C simulated call flag (read from file if true) */ /** * Represents a standard I2C read or read burst request * @typedef {Object} I2cReadRequest * @property {number} slave - I2C slave address * @property {number} address - I2C register address * @property {number} [nByte] - Number of bytes to read [optional], default is 1 * @property {boolean} simulated - I2C simulated call flag (read from file if true) */ /** * Represent a register based I2C read or write request mediated by a provided register map * @typedef {Object} RegisterRequest - Abstract object * @property {number} slave - I2C slave address * @property {string|number} register - I2C register name or register address * @property {number} [data] - I2C data * @property {boolean} simulated - I2C simulated call flag (read from file if true) /* /** * Represents a field based I2C read or write request mediated by a provided register map * @typedef {Object} I2cFieldRequest * @property {number} slave - I2C slave address * @property {string} field - register map field * @property {number|number[]} [data] - I2C data * @property {boolean} simulated - I2C simulated call flag (read from file if true) */ /** * Represents a multiple fields I2C read request mediated by a provided register map * @typedef {Object} I2cReadFieldsRequest * @property {number} slave - I2C slave address * @property {string[]} fields - register map fields * @property {boolean} simulated - I2C simulated call flag (read from file if true) /** * Represents a multiple fields I2C write request mediated by a provided register map * @typedef {Object} I2cWriteFieldsRequest * @property {number} slave - I2C slave address * @property {Object} fields - register map fields dictionary {fieldName: value} * @property {boolean} simulated - I2C simulated call flag (read from file if true) */ /** * Represents a regmap response constituted of a pass\fail result and possibly data and\or error encountered * @typedef {Object} I2cRegmapResponse * @property {string} result - pass\fail result of the operation * @property {number|Object} [data] - possible data read * @property {string} [error] - possible error encountered */ /** * Returns the default USB device description filter used is open function is used without providing a device serial_number. * The default filter is valid for MOST orcasemi boards and ev-kits * @returns {string} */ const getDefaultFilter = () => DEFAULT_FILTER; /** * Lists all available devices connected to the USB ports * @param {Object} observer - Observer object containing filter details. * @param {string} observer.filter - The USB device description filter. * @returns {Device[]} */ const list = async ({filter = ''} = {}) => await ftdi.list(filter); /** * Opens the device for I2C communication specified by the serial number of it * @param {Object} device - Device object containing USB device details (supposed to be an FTDI-D2XX device). * @param {serial_number} device.serial_number - Device serial number * @returns {BaseResponse} */ const open = async ({serial_number} = {}) => { let res; if (serial_number) { res = ftdi.open(serial_number); } else { const defaultSerialNumber = (await list({filter: DEFAULT_FILTER}))[0].serial_number; res = await ftdi.open(defaultSerialNumber); } return res; }; const getFtdiDevice = ftdi.getFtdiDevice /** * Returns true if device is open and connected, false otherwise * @returns {boolean} */ const isConnected = ftdi.isConnected; /** * Closes any opened device * @returns {BaseResponse} */ const close = async () => await ftdi.close(); /** * Scans I2C bus for all available and respondig I2C slaves * @returns {number[]} */ const i2cScan = async () => await i2c.scanI2cBus(); /** * Scans specified slave for all available and responding registers * @param {Object} listRequest - Abstract object * @param {number} listRequest.slave - I2C slave address * @returns {number[]} */ const i2cListRegisters = async ({slave} = {}) => await i2c.scanDeviceRegisters({slaveId: slave}); /** * Writes data on the specified slave starting at the specified register address * @param {I2cWriteRequest} - I2C Request Object composed of slave, address and data * @returns {I2cResponse} */ const i2cWrite = async ({slave, address, data, simulated = false} = {}) => { if(isArray(data)) { return await i2c.i2cWriteBurst({slaveId: slave, regAddress: address, data: data, simulated: simulated}); } else { return await i2c.i2cWriteAddress({slaveId: slave, regAddress: address, data: data, simulated: simulated}); } }; const read = async ({slave, target, n=1, simulated=false} = {}) => { let res = {data: {}, acks: {}}; const registerMap = regmap.getRegisterMap(); if (isArray(target)) { const readChunks = utils.buildReadInfo(target, registerMap); for (const c of Object.entries(readChunks)) { const sameAddressTargets = groupBy(c[1], t => t.address); const firstAddress = Number(Object.keys(sameAddressTargets).sort((a, b) => a - b)[0]); const burstLength = Object.keys(sameAddressTargets).length; const r = await i2c.i2cReadBurst({slaveId: slave, regAddress: firstAddress, nBytes: burstLength, simulated: simulated}); Object.entries(sameAddressTargets).forEach(a => { if (r.ack) { const addressData = r.data[Number(a[0]) - firstAddress]; a[1].forEach(t => { if (t.kind === utils.TARGET_KIND.FIELD) { const field = regmap.getFieldRegister(t.target).fields[t.target]; const data = utils.extractData(addressData, field.offset, field.size); res.data[t.target] = data; } else { res.data[t.target] = addressData; } res.acks[t.target] = r.ack; }); } else { a[1].forEach(t => { res.acks[t.target] = r.ack; }) } }); } res.usbCalls = Object.entries(readChunks).length; } else { let r; if (typeof target === 'number') { r = await i2c.i2cReadBurst({slaveId: slave, regAddress: target, nBytes: n, simulated: simulated}); } else if (typeof target === 'string') { if (n !== 1) { console.warn('WARNING: n is not 1 in a read field request through "read" method .. that may be misleading .. n has been ingored anyway ..') } if(registerMap) { const fieldRegister = regmap.getFieldRegister(target); if(fieldRegister) { r = await i2c.i2cReadBurst({slaveId: slave, regAddress: fieldRegister.address, nBytes: 1, simulated: simulated}); r.data = utils.extractData(r.data, fieldRegister.fields[target].offset, fieldRegister.fields[target].size); } else if (registerMap[target]) { r = await i2c.i2cReadBurst({slaveId: slave, regAddress: registerMap[target].address, nBytes: 1, simulated: simulated}); } else { throw new Error('Unrecognized read target'); } } else { throw('ERROR: no register map loaded .. before any register based or field based communication you MUST load a valid register map with setRegisterMap'); } } else { throw new Error('Unrecognized read target'); } const {data, ack} = r; res.data[target] = data; res.acks[target] = ack; res.usbCalls = 1; } return res; } const write = async ({slave, target, data = [], simulated=false} = {}) => { let res = {acks: {}}; const registerMap = regmap.getRegisterMap(); if(isObject(target) && !isArray(target)) { if (typeof data === 'number' || data.length > 0){ console.warn('WARNING: data is not empty in a structured multi-write request through "write" method .. that may be misleading .. data has been ingored anyway ..') } const modifyingFields = utils.getNon8BitFieldsFromHybridTarget(target, registerMap); const readTarget = {}; preRead = {}; modifyingFields.forEach(f => { readTarget[f] = Object.values(registerMap).find(r => Object.keys(r.fields).includes(f)).address }) let preReadRes = {}; if(modifyingFields.length > 0) { preReadRes = await read({slave: slave, target: Object.values(readTarget), simulated: simulated}); } Object.keys(readTarget).forEach(t => { preRead[t] = preReadRes.data[readTarget[t]] }) if((preReadRes.acks && Object.values(preReadRes.acks).every(a => a)) || modifyingFields.length === 0) { const writeChunks = utils.buildWriteInfo(target, preRead, registerMap); for (const c of Object.entries(writeChunks)) { const sameAddressTargets = groupBy(c[1], t => t.address); const firstAddress = Number(Object.keys(sameAddressTargets).sort((a, b) => a.address - b.address)[0]); const burstData = Object.values(sameAddressTargets).map(t => t[0].data); const r = await i2c.i2cWriteBurst({slaveId: slave, regAddress: firstAddress, data: burstData, simulated: simulated}); Object.entries(sameAddressTargets).forEach(a => { a[1].forEach(t => { res.acks[t.target] = r.ack; }); }); } } else { throw new Error('Error: NACK on multifield preread for read modify write'); } } else if (typeof data === 'number' || (isArray(data) && data.length > 0)) { let r; if (typeof target === 'number') { r = await i2c.i2cWriteBurst({slaveId: slave, regAddress: target, data: typeof data === 'number' ? [data] : data, simulated:simulated}) } else if (typeof target === 'string') { if(registerMap) { const fieldRegister = regmap.getFieldRegister(target); if(fieldRegister) { r = await i2c.i2cReadBurst({slaveId: slave, regAddress: fieldRegister.address, nBytes: 1, simulated: simulated}); if(r.ack) { const fieldData = utils.insertData(typeof data === 'number' ? data : data[0], fieldRegister.fields[target].offset, fieldRegister.fields[target].size, r.data); r = await i2c.i2cWriteBurst({slaveId: slave, regAddress: fieldRegister.address, data: [fieldData], simulated: simulated}); } else { throw new Error('Error: NACK during field based read-modify-write'); } } else if (registerMap[target]) { r = await i2c.i2cWriteBurst({slaveId: slave, regAddress: registerMap[target].address, data: typeof data === 'number' ? [data] : data, simulated: simulated}); } else { throw new Error('Unrecognized write target'); } } else { throw('ERROR: no register map loaded .. before any register based or field based communication you MUST load a valid register map with setRegisterMap'); } } else { throw new Error('Unrecognized write target'); } res.acks[target] = r.ack; res.usbCalls = 1; } else { throw new Error('Incompatible write data'); } return res; } /** * Reads data on the specified slave starting at the specified register address * @param {I2cReadRequest} - I2C Request Object composed of slave, address and optionally the number of byte to read * @returns {I2cResponse} */ const i2cRead = async ({slave, address, nByte = 1, simulated = false} = {}) => { if(nByte != 1 ) { return await i2c.i2cReadBurst({slaveId: slave, regAddress: address, nBytes: nByte, simulated: simulated}); } else { return await i2c.i2cReadAddress({slaveId: slave, regAddress: address, simulated: simulated}); } }; /** * Set the registermap for field based or register based mediated I2C requests * @param {Object} regmapRequest - Abstract object * @param {string | Object} regmapRequest.registerMap - regmap filepath or regmap object itself * @returns {string} */ const setRegisterMap = async ({registerMap} = {}) => { let output; if ( typeof registerMap === 'string') { output = await regmap.setRegisterMapByFilePath({filepath: registerMap}); } else if ( typeof registerMap === 'object') { output = regmap.setRegisterMap({registerMap}); } else { console.log(`provided register map ${registerMap} is not valid`) } return output; }; /** * Get the set registermap * @returns {Object} */ const getRegisterMap = () => regmap.getRegisterMap(); /** * Reads the content of a register based on register map name or address * @param {RegisterRequest} * @returns {I2cRegmapResponse} */ const i2cReadRegister = async ({slave, register, simulated = false} = {}) => await regmap.i2cReadRegister({slaveId: slave, register: register, simulated: simulated}); /** * Reads the content of a regmap field * @param {I2cFieldRequest} * @returns {I2cRegmapResponse} */ const i2cReadField = async ({slave, field, simulated = false} = {}) => await regmap.i2cReadField({slaveId: slave, field: field, simulated: simulated}); /** * Reads the content of multiple regmap fields * @param {I2cReadFieldsRequest} * @returns {I2cRegmapResponse} */ const i2cReadFields = async ({slave, fields, simulated = false} = {}) => await regmap.i2cReadFields({slaveId: slave, fields: fields, simulated: simulated}); /** * Reads the content of multiple regmap fields * @param {Object} readRegisterFieldsRequest - Abstract object * @param {number} readRegisterFieldsRequest.slave - I2C slave address * @param {string[]|number[]} readRegisterFieldsRequest.registers - I2C register map register * @param {boolean} readRegisterFieldsRequest.simulated - I2C simulated call flag (read from file if true) * @returns {I2cRegmapResponse} */ const i2cReadRegistersFields = async ({slave, registers, simulated = false} = {}) => await regmap.i2cReadRegistersFields({slaveId : slave, registers: registers, simulated: simulated}); /** * Reads the content of multiple regmap registers and return registers hierarchical structure * @param {Object} readRegisterWithDetailsRequest - Abstract object * @param {number} readRegisterWithDetailsRequest.slave - I2C slave address * @param {string[]|number[]} readRegisterWithDetailsRequest.registers - I2C register map register * @param {boolean} readRegisterWithDetailsRequest.simulated - I2C simulated call flag (read from file if true) * @returns {I2cRegmapResponse} */ const i2cReadRegistersWithDetails = async ({slave, registers, simulated = false} = {}) => await regmap.i2cReadRegistersWithDetails({slaveId : slave, registers: isArray(registers) ? registers : [registers], simulated: simulated}); /** * Reads the content of multiple regmap fields * @param {Object} readAllsRequest - Abstract object * @param {number} readAllsRequest.slave - I2C slave address * @param {number} [readAllsRequest.cap] - maximum address to read * @param {boolean} readAllsRequest.simulated - I2C simulated call flag (read from file if true) * @returns {I2cRegmapResponse} */ const i2cReadAllFields = async ({slave, cap, simulated = false} = {}) => await regmap.i2cReadAllFields({slaveId: slave, capAddress: cap, simulated: simulated}) /** * Writes the content of a register based on register map name or address * @param {RegisterRequest} * @returns {I2cRegmapResponse} */ const i2cWriteRegister = async ({slave, register, data, simulated = false}) => await regmap.i2cWriteRegister({slaveId: slave, register: register, data: data, simulated: simulated}); /** * Performs a read modified write on a register map field * @param {I2cFieldRequest} - Abstract object * @returns {I2cRegmapResponse} */ const i2cWriteField = async ({slave, field, data, simulated = false}) => await regmap.i2cWriteField({slaveId: slave, field: field, data: data, simulated: simulated}); /** * Performs multiple read modified writes on register map fields * @param {I2cWriteFieldsRequest} * @returns {I2cRegmapResponse} */ const i2cWriteFields = async ({slave, fields, simulated = false}) => await regmap.i2cWriteFields({slaveId: slave, fieldsDictionary: fields, simulated: simulated}); /** * Resets all registers to the register map default * @param {Object} resetRequest - Abstract object * @param {number} resetRequest.slave - I2C slave address * @param {boolean} resetRequest.simulated - I2C simulated call flag (read from file if true) * @returns {I2cRegmapResponse} */ const i2cResetAllRegisters = async ({slave, bannedRegisters = [], simulated = false}) => await regmap.i2cResetAllRegisters({slaveId: slave, bannedRegisters: bannedRegisters, simulated: simulated}); /** * Set a file to act as emulated device registers memory * @param {string} filepath - path to the emulated device file * @returns {string} */ const setEmulatedDevicePath = i2c.setEmulatedDevicePath module.exports = { getDefaultFilter: getDefaultFilter, list: list, open: open, isConnected: isConnected, close: close, read: read, write: write, i2cScan: i2cScan, i2cListRegisters: i2cListRegisters, i2cWrite: i2cWrite, i2cRead: i2cRead, setRegisterMap: setRegisterMap, getRegisterMap: getRegisterMap, i2cReadRegister: i2cReadRegister, i2cReadField: i2cReadField, i2cReadFields: i2cReadFields, i2cReadRegistersFields: i2cReadRegistersFields, i2cReadRegistersWithDetails: i2cReadRegistersWithDetails, i2cReadAllFields: i2cReadAllFields, i2cWriteRegister: i2cWriteRegister, i2cWriteField: i2cWriteField, i2cWriteFields: i2cWriteFields, i2cResetAllRegisters: i2cResetAllRegisters, setEmulatedDevicePath: setEmulatedDevicePath, getFtdiDevice: getFtdiDevice }