@audiofab-io/fv1-core 0.1.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ export declare const MCP2221_VENDOR_ID = 1240;
2
+ export declare const MCP2221_PRODUCT_ID = 221;
3
+ export declare const DEFAULT_EEPROM_I2C_ADDRESS = 80;
4
+ export declare const DEFAULT_I2C_CLOCK_HZ = 400000;
5
+ export declare const DEFAULT_WRITE_PAGE_SIZE = 32;
6
+ export declare const PROGRAM_SLOT_COUNT = 8;
7
+ export declare const PROGRAM_SLOT_SIZE_BYTES = 512;
8
+ export declare const EEPROM_TOTAL_BYTES: number;
9
+ export declare function slotBaseAddress(slot: number): number;
10
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/pedal/constants.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,OAAS,CAAA;AACvC,eAAO,MAAM,kBAAkB,MAAS,CAAA;AAExC,eAAO,MAAM,0BAA0B,KAAO,CAAA;AAC9C,eAAO,MAAM,oBAAoB,SAAU,CAAA;AAC3C,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAC1C,eAAO,MAAM,kBAAkB,QAA+C,CAAA;AAE9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpD"}
@@ -0,0 +1,17 @@
1
+ // Audiofab pedal (MCP2221 + 24LC32 EEPROM) constants.
2
+ // The FV-1 reads 8 programs of 128 × 32-bit words each from the EEPROM.
3
+ export const MCP2221_VENDOR_ID = 0x04D8;
4
+ export const MCP2221_PRODUCT_ID = 0x00DD;
5
+ export const DEFAULT_EEPROM_I2C_ADDRESS = 0x50;
6
+ export const DEFAULT_I2C_CLOCK_HZ = 400000;
7
+ export const DEFAULT_WRITE_PAGE_SIZE = 32;
8
+ export const PROGRAM_SLOT_COUNT = 8;
9
+ export const PROGRAM_SLOT_SIZE_BYTES = 512;
10
+ export const EEPROM_TOTAL_BYTES = PROGRAM_SLOT_COUNT * PROGRAM_SLOT_SIZE_BYTES;
11
+ export function slotBaseAddress(slot) {
12
+ if (slot < 0 || slot >= PROGRAM_SLOT_COUNT) {
13
+ throw new RangeError(`Slot ${slot} out of range [0, ${PROGRAM_SLOT_COUNT})`);
14
+ }
15
+ return slot * PROGRAM_SLOT_SIZE_BYTES;
16
+ }
17
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/pedal/constants.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,wEAAwE;AAExE,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAA;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAExC,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAA;AAC9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAO,CAAA;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAA;AAEzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAA;AACnC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAA;AAC1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,kBAAkB,GAAG,uBAAuB,CAAA;AAE9E,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,kBAAkB,EAAE,CAAC;QAC3C,MAAM,IAAI,UAAU,CAAC,QAAQ,IAAI,qBAAqB,kBAAkB,GAAG,CAAC,CAAA;IAC9E,CAAC;IACD,OAAO,IAAI,GAAG,uBAAuB,CAAA;AACvC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare function hashProgram(binary: Uint8Array): Promise<string>;
2
+ /**
3
+ * True if the slot is unprogrammed — i.e. every byte is 0xFF (blank EEPROM).
4
+ */
5
+ export declare function isBlankSlot(binary: Uint8Array): boolean;
6
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/pedal/hash.ts"],"names":[],"mappings":"AAGA,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAOrE;AAUD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAKvD"}
@@ -0,0 +1,28 @@
1
+ // SHA-256 helpers for matching an EEPROM slot's contents against a known program.
2
+ // Works in modern browsers (crypto.subtle) and Node 20+ (globalThis.crypto.subtle).
3
+ export async function hashProgram(binary) {
4
+ // Copy into a fresh, ArrayBuffer-backed view so the type satisfies
5
+ // `BufferSource` on every TS/DOM-lib combo.
6
+ const copy = new Uint8Array(binary.byteLength);
7
+ copy.set(binary);
8
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', copy);
9
+ return bytesToHex(new Uint8Array(digest));
10
+ }
11
+ function bytesToHex(bytes) {
12
+ let out = '';
13
+ for (let i = 0; i < bytes.length; i++) {
14
+ out += bytes[i].toString(16).padStart(2, '0');
15
+ }
16
+ return out;
17
+ }
18
+ /**
19
+ * True if the slot is unprogrammed — i.e. every byte is 0xFF (blank EEPROM).
20
+ */
21
+ export function isBlankSlot(binary) {
22
+ for (let i = 0; i < binary.length; i++) {
23
+ if (binary[i] !== 0xFF)
24
+ return false;
25
+ }
26
+ return true;
27
+ }
28
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/pedal/hash.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,oFAAoF;AAEpF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAkB;IAClD,mEAAmE;IACnE,4CAA4C;IAC5C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IAC9C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAChB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IACrE,OAAO,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,UAAU,CAAC,KAAiB;IACnC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAA;IACtC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { MCP2221_VENDOR_ID, MCP2221_PRODUCT_ID, DEFAULT_EEPROM_I2C_ADDRESS, DEFAULT_I2C_CLOCK_HZ, DEFAULT_WRITE_PAGE_SIZE, EEPROM_TOTAL_BYTES, PROGRAM_SLOT_COUNT, PROGRAM_SLOT_SIZE_BYTES, slotBaseAddress, } from './constants.js';
2
+ export { hashProgram, isBlankSlot } from './hash.js';
3
+ export { PedalClient } from './pedal-client.js';
4
+ export type { PedalClientOptions } from './pedal-client.js';
5
+ export { createWebHidBinding } from './webhid-transport.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pedal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,GAChB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { MCP2221_VENDOR_ID, MCP2221_PRODUCT_ID, DEFAULT_EEPROM_I2C_ADDRESS, DEFAULT_I2C_CLOCK_HZ, DEFAULT_WRITE_PAGE_SIZE, EEPROM_TOTAL_BYTES, PROGRAM_SLOT_COUNT, PROGRAM_SLOT_SIZE_BYTES, slotBaseAddress, } from './constants.js';
2
+ export { hashProgram, isBlankSlot } from './hash.js';
3
+ export { PedalClient } from './pedal-client.js';
4
+ export { createWebHidBinding } from './webhid-transport.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pedal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,GAChB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAG/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,44 @@
1
+ import { MCP2221 } from '@johntalton/mcp2221';
2
+ import { EEPROM } from '@johntalton/eeprom';
3
+ type BindingBufferSource = ArrayBufferLike | ArrayBufferView;
4
+ export interface PedalClientOptions {
5
+ /** EEPROM I²C address. Defaults to 0x50 (standard 24LC32). */
6
+ i2cAddress?: number;
7
+ /** I²C clock speed in Hz. Defaults to 400 kHz. */
8
+ i2cClockHz?: number;
9
+ /** EEPROM write page size in bytes. Defaults to 32. */
10
+ writePageSize?: number;
11
+ }
12
+ export declare class PedalClient {
13
+ readonly device: MCP2221;
14
+ readonly eeprom: EEPROM;
15
+ private readonly i2cAddress;
16
+ private readonly writePageSize;
17
+ private constructor();
18
+ /**
19
+ * Open a pedal connection using a pre-built HID stream binding.
20
+ * For browsers, build one with `createWebHidBinding(hidDevice)`.
21
+ */
22
+ static open(binding: ReadableWritablePair<BindingBufferSource, BindingBufferSource>, options?: PedalClientOptions): Promise<PedalClient>;
23
+ /** Read one program slot (512 bytes) from EEPROM. */
24
+ readSlot(slot: number): Promise<Uint8Array>;
25
+ /** Read the entire EEPROM (all 8 slots) in one sweep. */
26
+ readAllSlots(): Promise<Uint8Array[]>;
27
+ /** Write one program slot (512 bytes) to EEPROM and verify the write. */
28
+ writeSlot(slot: number, data: Uint8Array): Promise<void>;
29
+ /**
30
+ * Issue a single EEPROM page write (address prefix + data) and then wait
31
+ * for the MCP2221 I²C state machine to return to IDLE. Handles the 24LC32
32
+ * internal write cycle (~5 ms) by retrying if the chip NACKs the address.
33
+ */
34
+ private writePageWithPolling;
35
+ /**
36
+ * Poll MCP2221 status until the I²C state is IDLE or a terminal error
37
+ * state. Returns the final state name.
38
+ */
39
+ private waitForIdle;
40
+ /** Force the I²C state machine to IDLE, cancelling any stuck transaction. */
41
+ private ensureIdle;
42
+ }
43
+ export {};
44
+ //# sourceMappingURL=pedal-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pedal-client.d.ts","sourceRoot":"","sources":["../../src/pedal/pedal-client.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAY3C,KAAK,mBAAmB,GAAG,eAAe,GAAG,eAAe,CAAA;AAE5D,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAID,qBAAa,WAAW;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IAEtC,OAAO;IAOP;;;OAGG;WACU,IAAI,CACf,OAAO,EAAE,oBAAoB,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,EACvE,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC;IAkBvB,qDAAqD;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAMjD,yDAAyD;IACnD,YAAY,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAU3C,yEAAyE;IACnE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC9D;;;;OAIG;YACW,oBAAoB;IA8BlC;;;OAGG;YACW,WAAW;IAazB,6EAA6E;YAC/D,UAAU;CAMzB"}
@@ -0,0 +1,137 @@
1
+ // High-level pedal API built on the @johntalton/* I2C / MCP2221 / EEPROM stack.
2
+ // Consumers pass in a ReadableWritablePair binding — use `createWebHidBinding`
3
+ // from './webhid-transport' in the browser, or any node-hid bridge on Node.
4
+ import { MCP2221 } from '@johntalton/mcp2221';
5
+ import { I2CBusMCP2221 } from '@johntalton/i2c-bus-mcp2221';
6
+ import { I2CAddressedBus } from '@johntalton/and-other-delights';
7
+ import { EEPROM } from '@johntalton/eeprom';
8
+ import { DEFAULT_EEPROM_I2C_ADDRESS, DEFAULT_I2C_CLOCK_HZ, DEFAULT_WRITE_PAGE_SIZE, EEPROM_TOTAL_BYTES, PROGRAM_SLOT_COUNT, PROGRAM_SLOT_SIZE_BYTES, slotBaseAddress, } from './constants.js';
9
+ const delayMs = (ms) => new Promise(resolve => setTimeout(resolve, ms));
10
+ export class PedalClient {
11
+ constructor(device, eeprom, i2cAddress, writePageSize) {
12
+ this.device = device;
13
+ this.eeprom = eeprom;
14
+ this.i2cAddress = i2cAddress;
15
+ this.writePageSize = writePageSize;
16
+ }
17
+ /**
18
+ * Open a pedal connection using a pre-built HID stream binding.
19
+ * For browsers, build one with `createWebHidBinding(hidDevice)`.
20
+ */
21
+ static async open(binding, options = {}) {
22
+ const { i2cAddress = DEFAULT_EEPROM_I2C_ADDRESS, i2cClockHz = DEFAULT_I2C_CLOCK_HZ, writePageSize = DEFAULT_WRITE_PAGE_SIZE, } = options;
23
+ const device = MCP2221.from(binding);
24
+ await device.common.status({
25
+ opaque: `fv1-core::i2c-speed-${i2cClockHz}`,
26
+ i2cClock: i2cClockHz,
27
+ });
28
+ const bus = I2CBusMCP2221.from(device);
29
+ const abus = new I2CAddressedBus(bus, i2cAddress);
30
+ const eeprom = new EEPROM(abus, { writePageSize });
31
+ return new PedalClient(device, eeprom, i2cAddress, writePageSize);
32
+ }
33
+ /** Read one program slot (512 bytes) from EEPROM. */
34
+ async readSlot(slot) {
35
+ const address = slotBaseAddress(slot);
36
+ const buffer = await this.eeprom.read(address, PROGRAM_SLOT_SIZE_BYTES);
37
+ return new Uint8Array(buffer);
38
+ }
39
+ /** Read the entire EEPROM (all 8 slots) in one sweep. */
40
+ async readAllSlots() {
41
+ const buffer = await this.eeprom.read(0, EEPROM_TOTAL_BYTES);
42
+ const all = new Uint8Array(buffer);
43
+ const slots = [];
44
+ for (let i = 0; i < PROGRAM_SLOT_COUNT; i++) {
45
+ slots.push(all.slice(i * PROGRAM_SLOT_SIZE_BYTES, (i + 1) * PROGRAM_SLOT_SIZE_BYTES));
46
+ }
47
+ return slots;
48
+ }
49
+ /** Write one program slot (512 bytes) to EEPROM and verify the write. */
50
+ async writeSlot(slot, data) {
51
+ if (data.length !== PROGRAM_SLOT_SIZE_BYTES) {
52
+ throw new Error(`writeSlot expects ${PROGRAM_SLOT_SIZE_BYTES} bytes, got ${data.length}`);
53
+ }
54
+ const baseAddress = slotBaseAddress(slot);
55
+ // Page-by-page write that bypasses @johntalton/i2c-bus-mcp2221's checkWrite.
56
+ // The library polls MCP2221 status immediately after each writeData and
57
+ // throws if the I²C state machine isn't in a terminal state, which races
58
+ // against the in-flight transaction. We drive writeData directly and then
59
+ // poll for IDLE before moving on.
60
+ for (let offset = 0; offset < data.length;) {
61
+ const pageAddr = baseAddress + offset;
62
+ const nextBoundary = Math.floor((pageAddr + this.writePageSize) / this.writePageSize) * this.writePageSize;
63
+ const chunkLen = Math.min(nextBoundary - pageAddr, data.length - offset);
64
+ const pageData = data.subarray(offset, offset + chunkLen);
65
+ const buffer = new Uint8Array(2 + chunkLen);
66
+ buffer[0] = (pageAddr >> 8) & 0xFF;
67
+ buffer[1] = pageAddr & 0xFF;
68
+ buffer.set(pageData, 2);
69
+ await this.writePageWithPolling(buffer, `writeSlot::${slot}::${offset}`);
70
+ offset += chunkLen;
71
+ }
72
+ const verifyBuffer = await this.eeprom.read(baseAddress, PROGRAM_SLOT_SIZE_BYTES);
73
+ const verify = new Uint8Array(verifyBuffer);
74
+ for (let i = 0; i < data.length; i++) {
75
+ if (data[i] !== verify[i]) {
76
+ throw new Error(`Slot ${slot} verification failed at byte ${i}`);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Issue a single EEPROM page write (address prefix + data) and then wait
82
+ * for the MCP2221 I²C state machine to return to IDLE. Handles the 24LC32
83
+ * internal write cycle (~5 ms) by retrying if the chip NACKs the address.
84
+ */
85
+ async writePageWithPolling(buffer, opaque) {
86
+ // Make sure the state machine is clean before we start. If a prior op
87
+ // left it non-idle (e.g., a racy status poll), cancel and give it a beat.
88
+ await this.ensureIdle(`${opaque}::pre`);
89
+ // Retry the whole page up to a few times in case the EEPROM is still in
90
+ // its internal write cycle from a previous page and NACKs the address.
91
+ const MAX_ATTEMPTS = 20;
92
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
93
+ await this.device.i2c.writeData({
94
+ opaque: `${opaque}::writeData::${attempt}`,
95
+ address: this.i2cAddress,
96
+ buffer,
97
+ });
98
+ const state = await this.waitForIdle(`${opaque}::poll::${attempt}`, 50);
99
+ if (state === 'IDLE')
100
+ return;
101
+ // Chip is in its internal write cycle; wait a bit and retry.
102
+ if (state === 'ADDRESS_NACK_STOP' || state === 'ADDRESS_NACK_STOP_END') {
103
+ await delayMs(2);
104
+ continue;
105
+ }
106
+ throw new Error(`EEPROM page write stuck at state ${state}`);
107
+ }
108
+ throw new Error(`EEPROM page write did not complete after ${MAX_ATTEMPTS} attempts`);
109
+ }
110
+ /**
111
+ * Poll MCP2221 status until the I²C state is IDLE or a terminal error
112
+ * state. Returns the final state name.
113
+ */
114
+ async waitForIdle(opaque, timeoutMs) {
115
+ const deadline = Date.now() + timeoutMs;
116
+ let lastState = 'UNKNOWN';
117
+ while (Date.now() < deadline) {
118
+ const status = await this.device.common.status({ opaque });
119
+ lastState = status.i2cStateName ?? 'UNKNOWN';
120
+ if (status.i2cState === 0)
121
+ return 'IDLE';
122
+ if (lastState === 'ADDRESS_NACK_STOP' || lastState === 'ADDRESS_NACK_STOP_END')
123
+ return lastState;
124
+ await delayMs(1);
125
+ }
126
+ return lastState;
127
+ }
128
+ /** Force the I²C state machine to IDLE, cancelling any stuck transaction. */
129
+ async ensureIdle(opaque) {
130
+ const status = await this.device.common.status({ opaque });
131
+ if (status.i2cState === 0)
132
+ return;
133
+ await this.device.common.status({ opaque: `${opaque}::cancel`, cancelI2c: true });
134
+ await delayMs(10);
135
+ }
136
+ }
137
+ //# sourceMappingURL=pedal-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pedal-client.js","sourceRoot":"","sources":["../../src/pedal/pedal-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ type BindingBufferSource = ArrayBufferLike | ArrayBufferView;
2
+ interface HIDDeviceLike extends EventTarget {
3
+ opened: boolean;
4
+ open(): Promise<void>;
5
+ close(): Promise<void>;
6
+ sendReport(reportId: number, data: BufferSource): Promise<void>;
7
+ }
8
+ /**
9
+ * Create a duplex stream binding for a WebHID MCP2221 device.
10
+ * The returned pair can be passed directly to `MCP2221.from(binding)`.
11
+ */
12
+ export declare function createWebHidBinding(device: HIDDeviceLike): ReadableWritablePair<BindingBufferSource, BindingBufferSource>;
13
+ export {};
14
+ //# sourceMappingURL=webhid-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhid-transport.d.ts","sourceRoot":"","sources":["../../src/pedal/webhid-transport.ts"],"names":[],"mappings":"AAOA,KAAK,mBAAmB,GAAG,eAAe,GAAG,eAAe,CAAA;AAO5D,UAAU,aAAc,SAAQ,WAAW;IACzC,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChE;AAKD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,GACpB,oBAAoB,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAuDhE"}
@@ -0,0 +1,75 @@
1
+ // Bridges a WebHID `HIDDevice` to a `ReadableWritablePair` so it can drive
2
+ // `@johntalton/mcp2221`, which expects a generic stream binding.
3
+ //
4
+ // The MCP2221 uses 64-byte reports with report ID 0. Each outgoing write
5
+ // is sent as a single report; each incoming `inputreport` event becomes one
6
+ // 64-byte chunk in the readable side of the pair.
7
+ const REPORT_ID = 0;
8
+ const REPORT_SIZE = 64;
9
+ /**
10
+ * Create a duplex stream binding for a WebHID MCP2221 device.
11
+ * The returned pair can be passed directly to `MCP2221.from(binding)`.
12
+ */
13
+ export function createWebHidBinding(device) {
14
+ const queue = [];
15
+ const waiters = [];
16
+ const onInputReport = (event) => {
17
+ const ev = event;
18
+ const src = new Uint8Array(ev.data.buffer, ev.data.byteOffset, ev.data.byteLength);
19
+ // Copy into a fresh, full-size (64-byte) buffer so downstream consumers
20
+ // always see a stable chunk size regardless of how the browser packages it.
21
+ const chunk = new Uint8Array(REPORT_SIZE);
22
+ chunk.set(src.subarray(0, Math.min(src.length, REPORT_SIZE)));
23
+ const waiter = waiters.shift();
24
+ if (waiter)
25
+ waiter(chunk);
26
+ else
27
+ queue.push(chunk);
28
+ };
29
+ device.addEventListener('inputreport', onInputReport);
30
+ const readable = new ReadableStream({
31
+ type: 'bytes',
32
+ pull(controller) {
33
+ const byob = controller;
34
+ if (queue.length > 0) {
35
+ byob.enqueue(queue.shift());
36
+ return;
37
+ }
38
+ return new Promise(resolve => {
39
+ waiters.push(chunk => {
40
+ byob.enqueue(chunk);
41
+ resolve();
42
+ });
43
+ });
44
+ },
45
+ cancel() {
46
+ device.removeEventListener('inputreport', onInputReport);
47
+ },
48
+ });
49
+ const writable = new WritableStream({
50
+ async write(chunk) {
51
+ const bytes = toBytes(chunk);
52
+ // MCP2221 always expects 64 bytes of payload. Always build a fresh,
53
+ // ArrayBuffer-backed view so it satisfies the DOM BufferSource type.
54
+ const padded = new Uint8Array(REPORT_SIZE);
55
+ padded.set(bytes.subarray(0, Math.min(bytes.length, REPORT_SIZE)));
56
+ await device.sendReport(REPORT_ID, padded);
57
+ },
58
+ close() {
59
+ device.removeEventListener('inputreport', onInputReport);
60
+ },
61
+ abort() {
62
+ device.removeEventListener('inputreport', onInputReport);
63
+ },
64
+ });
65
+ return { readable, writable };
66
+ }
67
+ function toBytes(chunk) {
68
+ if (chunk instanceof Uint8Array)
69
+ return chunk;
70
+ if (ArrayBuffer.isView(chunk)) {
71
+ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
72
+ }
73
+ return new Uint8Array(chunk);
74
+ }
75
+ //# sourceMappingURL=webhid-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhid-transport.js","sourceRoot":"","sources":["../../src/pedal/webhid-transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audiofab-io/fv1-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "FV-1 DSP toolchain: assembler, simulator, and Intel HEX parser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
+ },
13
+ "./pedal": {
14
+ "import": "./dist/pedal/index.js",
15
+ "types": "./dist/pedal/index.d.ts"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -19,6 +23,12 @@
19
23
  "clean": "rm -rf dist",
20
24
  "prepublishOnly": "npm run build"
21
25
  },
26
+ "dependencies": {
27
+ "@johntalton/and-other-delights": "^8.6.4",
28
+ "@johntalton/eeprom": "^1.1.1",
29
+ "@johntalton/i2c-bus-mcp2221": "^4.2.1",
30
+ "@johntalton/mcp2221": "^4.0.4"
31
+ },
22
32
  "devDependencies": {
23
33
  "typescript": "^5.7.0"
24
34
  },