From 2a5ac90efdd28a5ea0302b88c05eefd41795bb50 Mon Sep 17 00:00:00 2001 From: johntalton Date: Thu, 27 Jun 2024 03:53:32 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20battle=20tested=20refactor=20and?= =?UTF-8?q?=20enhancements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/codeql-config.yml | 7 -- .github/workflows/ANALYSIS.yml | 20 ---- .github/workflows/CI.yml | 19 +--- .github/workflows/RELEASE.yml | 15 +-- package.json | 89 +---------------- src/i2c-bus.ts | 177 ++++++++++++--------------------- src/utils/delay.ts | 1 + src/utils/read.ts | 97 ++++++++++++++++++ src/utils/ready.ts | 28 ++++++ src/utils/write.ts | 59 +++++++++++ test/i2c-bus.spec.js | 26 ----- tsconfig.json | 5 +- 12 files changed, 262 insertions(+), 281 deletions(-) delete mode 100644 .github/codeql-config.yml delete mode 100644 .github/workflows/ANALYSIS.yml create mode 100644 src/utils/delay.ts create mode 100644 src/utils/read.ts create mode 100644 src/utils/ready.ts create mode 100644 src/utils/write.ts delete mode 100644 test/i2c-bus.spec.js diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml deleted file mode 100644 index 69da499..0000000 --- a/.github/codeql-config.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: Security-and-Quality - -queries: - - uses: security-and-quality - -paths-ignore: - - node_modules diff --git a/.github/workflows/ANALYSIS.yml b/.github/workflows/ANALYSIS.yml deleted file mode 100644 index 26bea98..0000000 --- a/.github/workflows/ANALYSIS.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: CodeQL - -on: - push: - pull_request: - schedule: - - cron: '0 3 * * 6' - -jobs: - Analyse: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - uses: github/codeql-action/init@v2 - with: - languages: javascript - config-file: ./.github/codeql-config.yml - - - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5502231..8c6368d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,25 +2,12 @@ name: CI on: [ push, pull_request] jobs: - Build: + CI: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - run: npm install - run: npm run build --if-present - Lnt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - - run: npm install - run: npm run lint --if-present -- --quiet - Test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - - run: npm install - - run: npm run build --if-present - run: npm run test --if-present diff --git a/.github/workflows/RELEASE.yml b/.github/workflows/RELEASE.yml index 9063059..7a70809 100644 --- a/.github/workflows/RELEASE.yml +++ b/.github/workflows/RELEASE.yml @@ -1,4 +1,4 @@ -name: Publish Package (NPM / GitHub) +name: Publish Package (NPM) on: release: types: [ created ] @@ -6,9 +6,9 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Publish to NPM - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: registry-url: 'https://registry.npmjs.org/' - run: npm install @@ -16,12 +16,3 @@ jobs: - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - # Publish to GitHub - # - uses: actions/setup-node@v3 - # with: - # node-version: '14' - # registry-url: 'https://npm.pkg.github.com/' - # - run: npm publish - # env: - # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index d66f145..b025002 100644 --- a/package.json +++ b/package.json @@ -1,79 +1,21 @@ { "name": "@johntalton/i2c-bus-mcp2221", - "version": "2.0.1", - "description": "", + "version": "4.0.0", "type": "module", "exports": { "import": "./lib/index.js", "require": null }, "files": [ + "src/**/*.ts", "lib/**/*.js", "lib/**/*.d.ts", "lib/**/*.d.ts.map" ], "scripts": { - "lint": "npm run lint:ts", - "lint:ts": "./node_modules/.bin/eslint --no-inline-config --report-unused-disable-directives --ext .ts src/*.ts", - "test": "npm run test:mocha", - "test:mocha": "mocha", "build": "tsc -p .", "build:watch": "tsc -p . -w" }, - "eslintConfig": { - "ignorePatterns": [ - "lib", - "test" - ], - "extends": [ - "@johntalton/eslint-config/ts" - ], - "env": { - "node": true - }, - "rules": { - "no-tabs": "off", - "indent": [ - "error", - "tab" - ], - "max-len": [ - "warn", - { - "code": 120 - } - ], - "class-methods-use-this": [ "warn" ], - "spellcheck/spell-checker": [ - "warn", - { - "ignoreRequire": true, - "identifiers": false, - "minLength": 4, - "skipWords": [ - "mcp2221" - ] - } - ] - } - }, - "mocha": { - "spec": [ - "test/*.spec.js" - ], - "grep": "@broken|@slow", - "invert": true, - "parallel": true, - "watch": false, - "sort": false, - "forbitOnly": true, - "check-leaks": true, - "global": [], - "require": [ - "source-map-support/register", - "ts-node/register/transpile-only" - ] - }, "repository": { "type": "git", "url": "git+https://github.com/johntalton/i2c-bus-mcp2221.git" @@ -81,31 +23,10 @@ "author": "johntalton@gmail.com", "license": "MIT", "devDependencies": { - "@johntalton/eslint-config": "^2.0.0", - "@types/chai": "^4.2.14", - "@types/mocha": "^9.1.0", - "@types/node": "^17.0.21", - "@typescript-eslint/eslint-plugin": "^5.14.0", - "c8": "^7.3.0", - "chai": "^4.2.0", - "eslint": "^8.11.0", - "eslint-plugin-fp": "^2.3.0", - "eslint-plugin-immutable": "^1.0.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-mocha": "^10.0.3", - "eslint-plugin-no-loops": "^0.3.0", - "eslint-plugin-no-use-extend-native": "^0.5.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-security": "^1.4.0", - "eslint-plugin-spellcheck": "^0.0.19", - "mocha": "^9.2.2", - "nodemon": "^2.0.4", - "source-map-support": "^0.5.19", - "ts-node": "^10.7.0", - "typescript": "^4.2.3" + "typescript": "^5.5.2" }, "dependencies": { - "@johntalton/and-other-delights": "^6.0.0", - "@johntalton/mcp2221": "^3.0.3" + "@johntalton/and-other-delights": "^8.3.0", + "@johntalton/mcp2221": "^4.0.0" } } diff --git a/src/i2c-bus.ts b/src/i2c-bus.ts index 32e3540..ee68ec4 100644 --- a/src/i2c-bus.ts +++ b/src/i2c-bus.ts @@ -1,161 +1,110 @@ -/* eslint-disable fp/no-this */ -/* eslint-disable fp/no-mutation */ -/* eslint-disable immutable/no-this */ -/* eslint-disable no-magic-numbers */ -/* eslint-disable fp/no-unused-expression */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable immutable/no-mutation */ -/* eslint-disable fp/no-nil */ -/* eslint-disable fp/no-class */ -import { I2CBufferSource, I2CBus, I2CReadResult, I2CWriteResult } from '@johntalton/and-other-delights' +import { I2CBufferSource, I2CScannableBus, I2CReadResult, I2CWriteResult, I2CBus } from '@johntalton/and-other-delights' import { MCP2221 } from '@johntalton/mcp2221' -const delayMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) +import { ready } from './utils/ready.js' +import { read, readRepeatedStart } from './utils/read.js' +import { write, writeNoSTOP } from './utils/write.js' export type MCP2221Options = { opaquePrefix: string } const DEFAULT_OPTIONS: MCP2221Options = { opaquePrefix: '' } -export class I2CBusMCP2221 implements I2CBus { +export class I2CBusMCP2221 implements I2CScannableBus { private readonly device: MCP2221 private readonly options: MCP2221Options // factory - static from(device: MCP2221, options: Partial): I2CBus { + static from(device: MCP2221, options: Partial = {}): I2CScannableBus { return new I2CBusMCP2221(device, options) } - constructor(device: MCP2221, options: Partial) { + constructor(device: MCP2221, options: Partial = {}) { this.device = device this.options = { ...DEFAULT_OPTIONS, ...options } } - // eslint-disable-next-line class-methods-use-this - get name(): string { return '' } - // eslint-disable-next-line class-methods-use-this + get name(): string { return 'I²C MCP2221' } + close(): void { // await this.device.i2c.close() } - /** - * i2c_smbus_write_byte - * S Addr Wr [A] Data [A] P - */ - async sendByte(address: number, byteValue: number): Promise { - const opaque = this.options.opaquePrefix - await this.device.i2c.writeData({ opaque, address, buffer: Uint8Array.from([byteValue]) }) - } + async scan(): Promise { + function* range(start: number, end: number, step: number = 1): Generator { + yield start + if (start >= end) return + yield* range(start + step, end, step) + } - /** - * i2c_smbus_read_i2c_block_data - * S Addr Wr [A] Comm [A] - S Addr Rd [A] [Data] A [Data] A ... A [Data] NA P - */ - async readI2cBlock(address: number, cmd: number, length: number, _bufferSource: I2CBufferSource): Promise { - const opaque = this.options.opaquePrefix - console.log('readI2cBlock ', address, cmd, length) - const { status } = await this.device.i2c.writeData({ opaque, address, buffer: Uint8Array.from([cmd]) }) - if(status !== 'success') { throw new Error('write failed: ' + status) } - const statis = await this.device.common.status({ opaque }) - console.log('readI2cBlock - write command', statis) - - const result = await this.device.i2c.readData({ opaque, address, length }) // length - console.log('readI2cblock - request read', { result }) - // if(result.status !== 'success') { throw new Error('not successfull readData') } - const statis2 = await this.device.common.status({ opaque }) - console.log(statis2) - - await delayMs(100) - - const data = await this.device.i2c.readGetData({ opaque }) - console.log('readI2cBlock - get data', data) - // if(data.status !== 'success') { throw new Error('not successful readData') } - - const { buffer, readBackBytes, validData } = data - - if(!validData) { - console.log('invalid data', validData) - return { - bytesRead: -1, - buffer: Uint8Array.from([]) + async function* _scan(bus: I2CBus): AsyncGenerator { + // console.log('_scan') + for (const address of range(0x08, 0x77)) { + // console.log('try', address) + try { + await bus.i2cRead(address, 1) + // console.log('yield', address) + yield address + } + catch { + // + // console.log('continue', address) + continue + } } } - return { - bytesRead: readBackBytes, - buffer - } + return Array.fromAsync(_scan(this)) } - /** - * i2c_smbus_write_i2c_block_data - * S Addr Wr [A] Comm [A] Data [A] Data [A] ... [A] Data [A] P - */ - async writeI2cBlock(address: number, cmd: number, length: number, bufferSource: I2CBufferSource): Promise { - const opaque = this.options.opaquePrefix + async sendByte(address: number, byteValue: number): Promise { + const opaque = this.options.opaquePrefix + '::sendByte' + await ready(this.device, opaque) + return write(this.device, address, Uint8Array.from([ byteValue ]), opaque + '::write') + .then() // swallow return from call into void promise + } - const userData = ArrayBuffer.isView(bufferSource) ? - new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, bufferSource.byteLength) : - new Uint8Array(bufferSource) + async readI2cBlock(address: number, cmd: number, length: number, targetBuffer?: I2CBufferSource): Promise { + const opaque = this.options.opaquePrefix + '::readI2cBlock' + // console.log(opaque, address, cmd) + await ready(this.device, opaque + '::ready') + await writeNoSTOP(this.device, address, Uint8Array.from([ cmd ]), opaque + '::writeNoStop') + // await ready(this.device, opaque + '::ready::interim') + return readRepeatedStart(this.device, address, length, targetBuffer, opaque + '::readRepeatedStart') + } - // const { status } = await this.device.i2c.writeData({ opaque, address, buffer: Uint8Array.from([ cmd ]) }) - // if (status !== 'success') { throw new Error('write cmd failed') } - // console.log('writeI2cBlock - write command', cmd) + async writeI2cBlock(address: number, cmd: number, length: number, bufferSource: I2CBufferSource): Promise { + const opaque = this.options.opaquePrefix + '::writeI2cBlock' - const { status: status2 } = await this.device.i2c.writeData({ opaque, address, buffer: Uint8Array.from([cmd, ...userData]) }) - if(status2 !== 'success') { throw new Error('write failed') } + const userData = ArrayBuffer.isView(bufferSource) ? + new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, length) : + new Uint8Array(bufferSource, 0, length) + const scratch = new Blob([ Uint8Array.from([ cmd ]), userData ]) + const futureBuffer = scratch.arrayBuffer() - const foo = await this.device.common.status({ opaque }) - console.log(foo) - console.log('writeI2cBlock - write user data', foo, ...userData) + await ready(this.device, opaque + '::ready') + const buffer = await futureBuffer + await write(this.device, address, buffer, opaque + '::write') return { bytesWritten: length, - buffer: userData.buffer + buffer: userData } } - async i2cRead(address: number, length: number, _bufferSource: I2CBufferSource): Promise { - const opaque = this.options.opaquePrefix - - const status = await this.device.common.status({ opaque }) - if(status.i2cState !== 0) { - await this.device.common.status({ opaque, cancelI2c: true }) - } - - await delayMs(1) - - const res = await this.device.i2c.readData({ opaque, address, length }) - console.log({ res }) - const getRes = await this.device.i2c.readGetData({ opaque }) - console.log({ getRes }) - if(!getRes.validData) { throw new Error('invalid data') } - return { - bytesRead: length, - buffer: getRes.buffer - } + async i2cRead(address: number, length: number, targetBuffer?: I2CBufferSource): Promise { + const opaque = this.options.opaquePrefix + '::i2cRead' + await ready(this.device, opaque + '::ready') + return read(this.device, address, length, targetBuffer, opaque + '::read') } async i2cWrite(address: number, length: number, bufferSource: I2CBufferSource): Promise { - const opaque = this.options.opaquePrefix - - const status = await this.device.common.status({ opaque }) - if(status.i2cState !== 0) { - await this.device.common.status({ opaque, cancelI2c: true }) - - await delayMs(1) - } - - const res = await this.device.i2c.writeData({ opaque, address, buffer: bufferSource }) - - if(res.statusCode !== 0) { - console.log({ code: res.statusCode, state: res.i2cState }) - throw new Error('write data no good') - } - + const opaque = this.options.opaquePrefix + '::i2cWrite' + await ready(this.device, opaque + '::ready') + await write(this.device, address, bufferSource, opaque + '::write') + // console.log('i2cWrite', length) return { bytesWritten: length, buffer: new ArrayBuffer(0) diff --git a/src/utils/delay.ts b/src/utils/delay.ts new file mode 100644 index 0000000..980f6f0 --- /dev/null +++ b/src/utils/delay.ts @@ -0,0 +1 @@ +export const delayMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) \ No newline at end of file diff --git a/src/utils/read.ts b/src/utils/read.ts new file mode 100644 index 0000000..8ee83e8 --- /dev/null +++ b/src/utils/read.ts @@ -0,0 +1,97 @@ +import { I2CAddress, I2CBufferSource } from '@johntalton/and-other-delights' +import { MCP2221, Response } from '@johntalton/mcp2221' + +import { delayMs } from './delay.js' + +async function checkRead(device: MCP2221, expectedByteLength: number, result: Response, opaque: string) { + // console.log(opaque, result.status, result.i2cStateName, expectedByteLength) + if(result.status === 'success') { + if(true) { + // check for validation of read + const status = await device.common.status({ opaque }) + + if(status.i2c.transferredBytes !== expectedByteLength) { + console.warn(opaque, 'transferred bytes not expected length', status.i2c.transferredBytes, status.i2c.requestedTransferLength, expectedByteLength) + } + + // console.log(opaque + '::extra', { + // length: expectedByteLength, + // ack: status.i2c.ACKed, + // ms: status.i2c.timeoutMs, + // state: status.i2cStateName, + // rtx: status.i2c.requestedTransferLength, + // tx: status.i2c.transferredBytes, + // count: status.i2c.dataBufferCounter + // }) + } + + return + } + + if(result.i2cStateName === undefined) { + throw new Error('missing i2cStateName field in result') + } + + if (result.i2cStateName === 'ADDRESS_NACK_STOP') { + throw new Error('not acked') + } + + const startReadStates = [ + 'START', + ] + + if (!startReadStates.includes(result.i2cStateName)) { + console.warn(opaque, 'not started', result.i2cStateName) + } + + const status = await device.common.status({ opaque }) + + if(status.i2cStateName === undefined) { + throw new Error('missing i2cStateName field in status') + } + + // console.log(opaque, status.i2c.timeoutMs, status.i2cStateName, expectedByteLength, status.i2c.requestedTransferLength, status.i2c.transferredBytes) + + const okReadStates = [ + 'READ_DATA', + 'READ_DATA_', + 'READ_DATA_complete' + ] + + if(status.status !== 'success') { + console.warn(opaque, 'read data status check error', status) + } + + if(!okReadStates.includes(status.i2cStateName)) { + console.warn(opaque, 'no data to read', status.i2cStateName) + } + + throw new Error(`unknown i2c state ${status.i2cStateName}`) +} + +async function getData(device: MCP2221, targetBuffer: I2CBufferSource|undefined, opaque: string) { + // + await delayMs(5) + + const { validData, buffer, readBackBytes } = await device.i2c.readGetData({ opaque }, targetBuffer) + if (!validData) { throw new Error('data not valid') } + + return { + bytesRead: readBackBytes, + buffer + } +} + +export async function readRepeatedStart(device: MCP2221, address: I2CAddress, length: number, targetBuffer: I2CBufferSource|undefined = undefined, opaque: string) { + // console.log(opaque) + const result = await device.i2c.readRepeatedSTART({ opaque, address, length }) + await checkRead(device, length, result, opaque + '::checkRead') + return getData(device, targetBuffer, opaque + '::getData') +} + +export async function read(device: MCP2221, address: I2CAddress, length: number, targetBuffer: I2CBufferSource|undefined = undefined, opaque: string) { + // console.log(opaque) + const result = await device.i2c.readData({ opaque, address, length }) + await checkRead(device, length, result, opaque + '::checkRead') + return getData(device, targetBuffer, opaque + '::getData') +} diff --git a/src/utils/ready.ts b/src/utils/ready.ts new file mode 100644 index 0000000..1a54033 --- /dev/null +++ b/src/utils/ready.ts @@ -0,0 +1,28 @@ +import { MCP2221 } from '@johntalton/mcp2221' + +import { delayMs } from './delay.js' + +export async function ready(device: MCP2221, opaque: string): Promise { + // console.log(opaque, '?') + const status = await device.common.status({ opaque }) + + // console.log(opaque, { + // ack: status.i2c.ACKed, + // ms: status.i2c.timeoutMs, + // state: status.i2cStateName, + // rtx: status.i2c.requestedTransferLength, + // tx: status.i2c.transferredBytes, + // count: status.i2c.dataBufferCounter + // }) + + if(status.status !== 'success') { + throw new Error('device status error') + } + + if (status.i2cState !== 0) { + // console.warn(opaque, 'i2c status not clean (attempt cancel)', status) + await device.common.status({ opaque, cancelI2c: true }) + + await delayMs(100) + } +} \ No newline at end of file diff --git a/src/utils/write.ts b/src/utils/write.ts new file mode 100644 index 0000000..4b9a363 --- /dev/null +++ b/src/utils/write.ts @@ -0,0 +1,59 @@ +import { I2CAddress, I2CBufferSource } from '@johntalton/and-other-delights' +import { MCP2221, Response } from '@johntalton/mcp2221' + +async function checkWrite(device: MCP2221, expectedByteLength: number, result: Response, opaque: string) { + if(result.status !== 'success') { throw new Error('write failed') } + const status = await device.common.status({ opaque }) + + if(status.i2c.requestedTransferLength !== expectedByteLength) { + console.warn(opaque, 'transferred bytes not expected length', status.i2c.transferredBytes, status.i2c.requestedTransferLength, expectedByteLength) + } + + // console.log(opaque, { + // length: expectedByteLength, + // ack: status.i2c.ACKed, + // ms: status.i2c.timeoutMs, + // state: status.i2cStateName, + // rtx: status.i2c.requestedTransferLength, + // tx: status.i2c.transferredBytes, + // count: status.i2c.dataBufferCounter + // }) + + if(status.i2cStateName === undefined) { + throw new Error('missing i2cStateName field in status') + } + + if (status.i2cStateName === 'ADDRESS_NACK_STOP') { + throw new Error('not acked') + } + + const okStates = [ + 'IDLE', + 'WRITE_DATA_END_NO_STOP', + 'WRITE_DATA_WAIT_SEND' + // 'WRITE_DATA_ACK' + ] + + if(!okStates.includes(status.i2cStateName)) { + console.warn(opaque, 'not idle', status.i2cStateName) + throw new Error('Not Idle-like') + } +} + +export async function writeNoSTOP(device: MCP2221, address: I2CAddress, buffer: I2CBufferSource, opaque: string) { + // console.log(opaque) + const result = await device.i2c.writeNoSTOP({ opaque, address, buffer }) + return checkWrite(device, buffer.byteLength, result, opaque + '::checkWrite') +} + +export async function writeRepeatedSTART(device: MCP2221, address: I2CAddress, buffer: I2CBufferSource, opaque: string) { + // console.log(opaque) + const result = await device.i2c.writeRepeatedSTART({ opaque, address, buffer }) + return checkWrite(device, buffer.byteLength, result, opaque + '::checkWrite') +} + +export async function write(device: MCP2221, address: I2CAddress, buffer: I2CBufferSource, opaque: string) { + // console.log(opaque) + const result = await device.i2c.writeData({ opaque, address, buffer }) + return checkWrite(device, buffer.byteLength, result, opaque + '::checkWrite') +} diff --git a/test/i2c-bus.spec.js b/test/i2c-bus.spec.js deleted file mode 100644 index 9271600..0000000 --- a/test/i2c-bus.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it } from 'mocha' -import { expect } from 'chai' - -// import { I2CBus } from '@johntalton/and-other-delights' -import { MCP2221A } from '@johntalton/mcp2221' - -import { I2CBusMCP2221 } from '@johntalton/i2c-bus-mcp2221' - - -describe('I2CBusMCP2221', () => { - it('should', async () => { - const binding = { - read: byteLength => console.log({ byteLength }), - write: bufferSource => console.log({ bufferSource }) - } - - const device = MCP2221A.from(binding) - const bus /* {I2CBus} */ = I2CBusMCP2221.from(device) - - const address = 0x77 - const cmd = 0x00 - const buffer = new Uint8Array(1) - const result = await bus.readI2cBlock(address, cmd, buffer.byteLength, buffer) - - }) -}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e886142..b3d3834 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,11 @@ "target": "ESNext", "module": "ESNext", - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], + "esModuleInterop": true, "isolatedModules": false, - "moduleResolution": "node", + "moduleResolution": "Bundler", "strict": true,