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