diff --git a/package.json b/package.json
index 89c5db7..e892778 100644
--- a/package.json
+++ b/package.json
@@ -9,25 +9,29 @@
"@johntalton/adxl375": "^1.0.0",
"@johntalton/aht20": "^1.2.0",
"@johntalton/am2320": "^1.0.0",
- "@johntalton/and-other-delights": "../and-other-delights",
+ "@johntalton/and-other-delights": "^8.3.0",
"@johntalton/aw9523": "^2.0.0",
"@johntalton/bitsmush": "^1.0.1",
"@johntalton/boschieu": "^6.0.1",
"@johntalton/ds1841": "^2.0.0",
"@johntalton/ds3231": "^1.1.1",
"@johntalton/ds3502": "^5.0.0",
- "@johntalton/excamera-i2cdriver": "../excamera-i2cdriver",
- "@johntalton/ht16k33": "../ht16k33",
- "@johntalton/i2c-bus-excamera-i2cdriver": "../i2c-bus-excamera-i2cdriver",
- "@johntalton/i2c-bus-mcp2221": "../i2c-bus-mcp2221",
+ "@johntalton/excamera-i2cdriver": "^3.0.0",
+ "@johntalton/ht16k33": "^1.1.1",
+ "@johntalton/i2c-bus-excamera-i2cdriver": "^3.0.1",
+ "@johntalton/i2c-bus-mcp2221": "^4.0.0",
"@johntalton/i2c-bus-tca9548a": "^1.0.1",
- "@johntalton/i2c-port": "^1.0.0",
- "@johntalton/mcp2221": "../mcp2221",
+ "@johntalton/i2c-port": "^4.0.0",
+ "@johntalton/mcp2221": "^4.0.0",
"@johntalton/mcp23": "^6.0.2",
- "@johntalton/pca9536": "../pca9536",
- "@johntalton/pcf8523": "../pcf8523",
+ "@johntalton/pca9536": "^1.0.2",
+ "@johntalton/pcf8523": "^2.0.1",
"@johntalton/pcf8574": "^2.1.0",
- "@johntalton/tca9548a": "^5.0.0",
- "@johntalton/tsl2591": "^1.0.0"
+ "@johntalton/tca9548a": "^5.1.1",
+ "@johntalton/tsl2591": "^1.0.0",
+ "body-parser": "^1.20.2",
+ "cors": "^2.8.5",
+ "express": "^4.19.2",
+ "node-hid": "^3.1.0"
}
}
diff --git a/public/css/app/app-main.css b/public/css/app/app-main.css
index 90f3f92..7e3bcd9 100644
--- a/public/css/app/app-main.css
+++ b/public/css/app/app-main.css
@@ -38,6 +38,26 @@ main > section:not([data-active]) {
display: none;
}
+[data-no-support] {
+ margin-inline: 0 2em;
+ margin-block-end: 1em;
+ padding: 1em;
+ border-radius: 1em;
+ padding-block: 2em;
+
+ border: 7px groove var(--color-accent--darker-error, red);
+ background-color: var(--color-accent--lightest-error, red);
+ color: var(--color-accent--lightest-error-text, red);
+}
+
+[data-no-support][data-dismissed] {
+ display: none;
+}
+
+body:not([data-supports=""]) [data-no-support] {
+ display: none;
+}
+
main > section {
& p[data-loading] {
display: flex;
diff --git a/public/css/defs.css b/public/css/defs.css
index 892940f..ddf5a60 100644
--- a/public/css/defs.css
+++ b/public/css/defs.css
@@ -28,6 +28,10 @@
[data-theme] {
+ --color-white: white;
+ --color-black: black;
+ --color-gray: gray;
+
/* */
--color-disabled: var(--color-gray);
--color-opaque-black: rgba(50 50 50 / 0.25);
diff --git a/public/css/libs/form.css b/public/css/libs/form.css
index 3c177c8..8fb3b7b 100644
--- a/public/css/libs/form.css
+++ b/public/css/libs/form.css
@@ -128,19 +128,27 @@ form {
& input[type="text"] {
accent-color: var(--color-accent--darker, red);
- border: 0;
+ /* background-color: var(--color-accent--lightest, red);
+ color: var(--color-accent--lightest-text, red); */
+ background-color: var(--color-accent--lighter, red);
+ color: var(--color-accent--lighter-text, red);
- border-radius: 2em;
+ border-width: 2px;
+ border-style: solid;
+ border-color: var(--color-accent--darker, red);
+
+ border-radius: 1em;
padding-inline-start: 1em;
- padding-block: .5em;
+ padding-block: .75em;
- &:not(:focus-visible) {
- background-color: var(--color-accent--lighter, red);
- color: var(--color-accent--lighter-text, red);
+ width: 100%;
+
+ &:focus-visible {
+ background-color: var(--color-white, white);
+ color: var(--color-black, black);
}
&:disabled {
- border: 0;
background-color: #00000014;
}
diff --git a/public/custom-elements/web.html b/public/custom-elements/web.html
new file mode 100644
index 0000000..d8bd2cb
--- /dev/null
+++ b/public/custom-elements/web.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/devices-i2c/adt7410.js b/public/devices-i2c/adt7410.js
index dc90c07..1a55dd5 100644
--- a/public/devices-i2c/adt7410.js
+++ b/public/devices-i2c/adt7410.js
@@ -74,7 +74,9 @@ export class ADT7410Builder {
//
const refreshId = async () => {
+ // console.log('refresh id')
this.#id = await this.#device.getId()
+ .then(id => { console.log(id); return id })
.catch(e => ({ manufactureId: NaN, revisionId: NaN, matchedVendor: false }))
idOutput.value = `Manufacture: 0x${this.#id.manufactureId.toString(16).padStart(2, '0')}, Revision ${this.#id.revisionId} ${this.#id.matchedVendor ? '' : '🛑'}`
diff --git a/public/devices-i2c/tsl2591.js b/public/devices-i2c/tsl2591.js
index eacfdb4..75edb44 100644
--- a/public/devices-i2c/tsl2591.js
+++ b/public/devices-i2c/tsl2591.js
@@ -84,6 +84,7 @@ export class TSL2591Builder {
check('enabled', enabled)
check('powerOn', powerOn)
+ console.log({ gain, time })
const gainSelect = root?.querySelector('select[name="gain"]')
gainSelect.value = gain
@@ -278,7 +279,9 @@ export class TSL2591Builder {
yield device.getColor()
}
catch(e) {
- // console.log('again?', e)
+ console.log('break', e)
+ break
+ console.log('again?', e)
await delayMs(1000 / 10)
}
}
diff --git a/public/devices-serial/exc-i2cdriver.js b/public/devices-serial/exc-i2cdriver.js
index e9d10e5..1be63e7 100644
--- a/public/devices-serial/exc-i2cdriver.js
+++ b/public/devices-serial/exc-i2cdriver.js
@@ -326,6 +326,13 @@ export class ExcameraI2CDriverUIBuilder {
// bufferSize: 1
})
+ const signals = await this.#port.getSignals()
+ console.log(`Clear To Send: ${signals.clearToSend}`)
+ console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`)
+ console.log(`Data Set Ready: ${signals.dataSetReady}`)
+ console.log(`Ring Indicator: ${signals.ringIndicator}`)
+
+
// device author provided init script
await initScript(this.#port)
diff --git a/public/hydrate/theme.js b/public/hydrate/theme.js
index 5868b6a..386c69a 100644
--- a/public/hydrate/theme.js
+++ b/public/hydrate/theme.js
@@ -14,8 +14,13 @@ export async function hydrateTheme() {
themRollerButton.setAttribute('title', theme)
- const transition = document.startViewTransition(() => {
+ if (!document.startViewTransition) {
document.body.setAttribute('data-theme', theme)
- })
+ }
+ else {
+ const transition = document.startViewTransition(() => {
+ document.body.setAttribute('data-theme', theme)
+ })
+ }
})
}
\ No newline at end of file
diff --git a/public/hydrate/ui.js b/public/hydrate/ui.js
index c8e06ac..d175827 100644
--- a/public/hydrate/ui.js
+++ b/public/hydrate/ui.js
@@ -14,6 +14,8 @@ import {
EXCAMERA_LABS_MINI_PRODUCT_ID
} from '@johntalton/excamera-i2cdriver'
import { asyncEvent } from '../util/async-event.js'
+import { deviceGuessByAddress } from '../devices-i2c/guesses.js'
+import { appendDeviceListItem } from '../util/device-list.js'
const MCP2221_USB_FILTER = {
@@ -113,7 +115,7 @@ export function buildDeviceListItem(deviceListElem, builder) {
return
}
- const transition = document.startViewTransition(() => {
+ const transitionView = () => {
deviceListElem.querySelectorAll(':scope > li').forEach(li => {
li.removeAttribute('data-active')
const bElem = li.querySelector('button')
@@ -126,7 +128,15 @@ export function buildDeviceListItem(deviceListElem, builder) {
mainElem.querySelectorAll(':scope > section').forEach(s => s.removeAttribute('data-active'))
sectionElem.toggleAttribute('data-active', true)
- })
+ }
+
+ if(!document.startViewTransition) {
+ transitionView()
+ } else {
+ const transition = document.startViewTransition(transitionView)
+ }
+
+
}), { once: false })
// demolisher
@@ -255,29 +265,42 @@ export async function addI2CDevice(definition) {
export class I2CBusWeb {
#url
- constructor(url = 'http://localhost:3000/mcp') {
+ constructor(url = 'http://localhost:3000/port') {
this.#url = url
}
+ get name() { return `WebI²C(${this.#url})`}
+
async postCommand(command, options) {
try {
- const result = await fetch(this.#url, {
+ const response = await fetch(this.#url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
+ namespace: window.origin,
+ opaque: '🤷🏻♂️',
type: command,
...options
})
})
- if(!result.ok) { throw new Error('result not ok') }
+ if(!response.ok) { throw new Error(`response not ok ${response.status}`) }
+
+ const result = await response.json()
+
+ if(result.type === 'error') {
+ throw new Error('WebI²C Remote Error: ' + result.why)
+ }
- return result.json()
+ return {
+ ...result,
+ buffer: (result.buffer !== undefined) ? Uint8Array.from(result.buffer) : undefined
+ }
}
catch(e) {
- console.warn('fetch exception', e)
+ // console.warn('fetch exception', e)
throw e
}
}
@@ -319,8 +342,105 @@ export class I2CBusWeb {
}
}
-// addI2CDevice({
-// type: 'ht16k33',
-// address: 0x71,
-// bus: new I2CBusWeb()
-// })
+addI2CDevice({
+ type: 'ht16k33',
+ address: 0x71,
+ bus: new I2CBusWeb()
+})
+
+const deviceListElem = document.getElementById('deviceList')
+const builder = {
+ title: '☠️',
+ open() {
+
+ },
+ async buildCustomView() {
+ const response = await fetch('./custom-elements/web.html')
+ if (!response.ok) { throw new Error('no html for view') }
+ const view = await response.text()
+ const doc = (new DOMParser()).parseFromString(view, 'text/html')
+
+ const root = doc?.querySelector('web-config')
+ if (root === null) { throw new Error('no root for template') }
+
+ const urlText = root.querySelector('input[name="url"]')
+ const scanButton = root.querySelector('button[data-scan]')
+ const deviceList = root.querySelector('[data-device-list]')
+ const addressElem = root.querySelector('addr-display[name="scanResults"]')
+
+ scanButton?.addEventListener('click', asyncEvent(async event => {
+ scanButton.disabled = true
+
+ const bus = new I2CBusWeb(urlText.value)
+
+ const existingHexs = addressElem.querySelectorAll('hex-display')
+ existingHexs.forEach(eh => eh.remove())
+
+ const existingLis = root.querySelectorAll('li')
+ existingLis.forEach(el => el.remove())
+
+
+ try {
+ const { addresses: ackedList } = await bus.scan()
+
+ ackedList.forEach(addr => {
+ const acked = true
+
+
+ const hexElem = document.createElement('hex-display')
+
+ hexElem.setAttribute('slot', addr)
+
+ hexElem.toggleAttribute('acked', true)
+ // hexElem.toggleAttribute('arbitration', arbitration)
+ // hexElem.toggleAttribute('timedout', timedout)
+
+ hexElem.textContent = addr.toString(16).padStart(2, '0')
+
+ addressElem.append(hexElem)
+
+ //
+ const listElem = document.createElement('li')
+ listElem.textContent = addr
+
+ listElem.setAttribute('slot', 'vdevice-guess-list')
+ listElem.toggleAttribute('data-acked', true)
+
+ const guesses = deviceGuessByAddress(addr)
+ const item = appendDeviceListItem(deviceList, addr, { acked, guesses })
+
+ item.button.addEventListener('click', e => {
+ e.preventDefault()
+
+ //
+ item.button.disabled = true
+ const deviceGuess = item.select.value
+
+ const controller = new AbortController()
+ const { signal } = controller
+
+ UI_HOOKS.addI2CDevice({
+ type: deviceGuess,
+ bus,
+ address: addr,
+
+ port: undefined,
+ signal
+ })
+
+
+ }, { once: true })
+
+ })
+ }
+ catch(e) {
+ console.log(e)
+ }
+
+ scanButton.disabled = false
+ }))
+
+ return root
+ }
+}
+const demolisher = buildDeviceListItem(deviceListElem, builder)
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index a1bb7b9..e50993a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -59,7 +59,7 @@
-
+
⚙️ Web I²C Playground
@@ -84,6 +84,11 @@ ⚙️ Web I²C Playground
+
+ 😢
+ No supported bus types
+
+
diff --git a/public/index.js b/public/index.js
index 158b71d..beaa9f8 100644
--- a/public/index.js
+++ b/public/index.js
@@ -10,6 +10,7 @@ import {
hydrateUI
} from './hydrate/ui.js'
import { hydrateTheme } from './hydrate/theme.js'
+import { DOMTokenListLike } from './util/dom-token-list.js';
async function onContentLoaded() {
if (!HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
@@ -20,13 +21,31 @@ async function onContentLoaded() {
const requestUSBButton = document.getElementById('requestUSB')
const requestHIDButton = document.getElementById('requestHID')
+ const supportSerial = 'serial' in navigator
+ const supportHID = 'hid' in navigator
+ const supportUSB = 'usb' in navigator
+
+ const supportAttr = document.body.getAttributeNode('data-supports')
+ supportAttr.value = ''
+ const dtl = new DOMTokenListLike(supportAttr)
+ dtl.toggle('serial', supportSerial)
+ dtl.toggle('hid', supportHID)
+ dtl.toggle('usb', supportUSB)
+
+ const dismissNoSupportButton = document.querySelector('button[name="dismissNoSupport"]')
+ dismissNoSupportButton?.addEventListener('click', event => {
+ event.preventDefault()
+ const noSupportDialog = event.target.closest('[data-no-support]')
+ noSupportDialog.toggleAttribute('data-dismissed')
+ })
+
await Promise.all([
hydrateCustomElements(),
hydrateUI(),
- hydrateSerial(requestSerialButton, UI_HOOKS),
- hydrateUSB(requestUSBButton, UI_HOOKS),
- hydrateHID(requestHIDButton, UI_HOOKS),
+ supportSerial ? hydrateSerial(requestSerialButton, UI_HOOKS) : null,
+ supportUSB ? hydrateUSB(requestUSBButton, UI_HOOKS) : null,
+ supportHID ? hydrateHID(requestHIDButton, UI_HOOKS) : null,
hydrateTheme(),
hydrateEffects()
diff --git a/public/util/dom-token-list.js b/public/util/dom-token-list.js
index 0c40363..2ab4b80 100644
--- a/public/util/dom-token-list.js
+++ b/public/util/dom-token-list.js
@@ -4,13 +4,23 @@ const EMPTY_SPACE = ''
export class DOMTokenListLike {
#set
#attr
+ // #observer
+
+ static fromValue(value) {
+ return new Set(value.split(SINGLE_SPACE).filter(v => v !== EMPTY_SPACE))
+ }
/**
* @param {Attr} attr
*/
constructor(attr) {
- this.#set = new Set(attr.value.split(SINGLE_SPACE).filter(v => v !== EMPTY_SPACE))
+ this.#set = DOMTokenListLike.fromValue(attr.value)
this.#attr = attr
+
+ // this.#observer = new MutationObserver(mutations => {
+ // console.log('ATTR', mutations)
+ // })
+ // this.#observer.observe(attr.ownerElement, { attributeFilter: [ attr.name ], attributes: true })
}
_update() {
@@ -22,6 +32,7 @@ export class DOMTokenListLike {
}
get value() { return this.toString() }
+ set value(value) { this.#set = DOMTokenListLike.fromValue(value) }
/**
* @param {String} token
diff --git a/service/hid.js b/service/hid.js
new file mode 100644
index 0000000..c220551
--- /dev/null
+++ b/service/hid.js
@@ -0,0 +1,87 @@
+
+import express from 'express'
+import bodyParser from 'body-parser'
+import cors from 'cors'
+
+import HID from 'node-hid'
+
+
+import { MCP2221 } from '@johntalton/mcp2221'
+import { I2CBusMCP2221 } from '@johntalton/i2c-bus-mcp2221'
+import { I2CPort, I2CPortBus, I2CPortService } from '@johntalton/i2c-port'
+import { NodeHIDStreamSource } from './node-hid-stream.js'
+
+const VENDOR_ID = 1240
+const PRODUCT_ID = 221
+
+function addDevice(device) {
+ const controller = new AbortController()
+ const { signal } = controller
+
+ // add
+ console.log('add', device)
+}
+
+async function hydrateHIDBackgroundDevices() {
+ const devices = await HID.devicesAsync(VENDOR_ID, PRODUCT_ID)
+ devices.forEach(addDevice)
+}
+
+async function hydrateHID() {
+ return Promise.all([
+ hydrateHIDBackgroundDevices()
+ ])
+}
+
+await hydrateHID()
+
+
+
+const device = await HID.HIDAsync.open(VENDOR_ID, PRODUCT_ID)
+const source = new NodeHIDStreamSource(device)
+const chip = MCP2221.from(source)
+
+chip.common.status({ i2cClock: 400 })
+
+//
+const channel = new MessageChannel()
+const mbus = I2CBusMCP2221.from(chip, { opaquePrefix: 'AsAService' })
+const service = I2CPortService.from(channel.port2, mbus)
+const pbus = await I2CPortBus.openPromisified(channel.port1)
+
+//
+const busMap = new Map()
+busMap.set('mcp', {
+ bus(req) { return I2CBusMCP2221.from(chip, { opaquePrefix: req.body.namespace + '::' + req.body.opaque }) }
+})
+busMap.set('port', { bus(req) { return pbus }})
+
+const e = express()
+e.use(bodyParser.json())
+e.use(cors())
+
+e.options('/:id', cors())
+e.post('/:id', async (request, response) => {
+
+ const config = busMap.get(request.params.id)
+ if(config === undefined) {
+ response.status(404)
+ response.json({ error: 'unknown id' })
+ return
+ }
+
+ const bus = config.bus(request)
+
+ const message = {
+ ...request.body,
+ buffer: (request.body.buffer !== undefined) ? Uint8Array.from( request.body.buffer ) : undefined
+ }
+ const result = await I2CPort.handleMessage(bus, message)
+ response.json({
+ name: bus.name,
+ ...result,
+ buffer: (result.buffer !== undefined) ? [ ...(new Uint8Array(result.buffer)) ] : undefined
+ })
+})
+
+e.listen(3000)
diff --git a/service/node-hid-stream.js b/service/node-hid-stream.js
new file mode 100644
index 0000000..462f7cf
--- /dev/null
+++ b/service/node-hid-stream.js
@@ -0,0 +1,117 @@
+
+
+// import HID from 'node-hid'
+
+export const REPORT_ID = 0
+
+function getReadStream(device) {
+ // because this is a 'bytes' stream, this queuing strategy
+ // is always ByteLengthQueuingStrategy (https://streams.spec.whatwg.org/#blqs-class)
+ const queuingStrategy = {
+ highWaterMark: 64
+ }
+
+ return new ReadableStream({
+ type: 'bytes',
+ start(controller) {
+ device.on('data', data => {
+
+ // buffer contains first byte as reportId IFF device supports
+ // https://github.com/signal11/hidapi/blob/master/hidapi/hidapi.h#L224C35-L224C51
+ const reportId = REPORT_ID
+
+ // for now only report zero, until we return in buffer
+ if(reportId !== REPORT_ID) { controller.error('report id miss-match') }
+
+ // if (controller.byobRequest !== null) {
+ // const { view } = controller.byobRequest
+ // const bytesRead = data.byteLength
+
+ // // normalize data from a DataView to Uint8Array
+ // const input = ArrayBuffer.isView(data) ?
+ // new Uint8Array(data.buffer, data.byteOffset, data.byteLength) :
+ // new Uint8Array(data)
+
+ // // copy-into
+ // // console.log({ input })
+ // view.set(input)
+
+ // controller.byobRequest.respond(bytesRead)
+ // }
+ // else {
+ // console.log('queuing data in readable hid stream')
+ controller.enqueue(data)
+ // }
+ })
+ },
+ cancel() {
+ console.log('hid stream canceled')
+ device.removeAllListeners()
+ }
+ },
+ queuingStrategy)
+}
+
+function getWriteStream(device) {
+ const queuingStrategy = new ByteLengthQueuingStrategy({ highWaterMark: 64 })
+
+ const reportIdZeroByte = Uint8Array.from([ REPORT_ID ])
+
+ return new WritableStream({
+ start(controller) {},
+ async write(chunk, controller) {
+ // console.log('send report')
+
+ const blob = new Blob([ reportIdZeroByte, chunk ])
+ const buffer = await blob.arrayBuffer()
+ // console.log([ ...(new Uint8Array(buffer)) ])
+ return device.write([ ...(new Uint8Array(buffer)) ])
+
+ // const chunk8 = ArrayBuffer.isView(chunk) ?
+ // new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) :
+ // new Uint8Array(chunk)
+
+ // const report8 = new Uint8Array(chunk8.byteLength)
+ // report8[0] = REPORT_ID
+ // report8.set(chunk8)
+ // return hid.write([ ...(new Uint8Array(report8)) ])
+
+ // const report = ArrayBuffer.isView(chunk) ? chunk.buffer.transfer(chunk.byteLength + 1) : chunk.transfer(chunk.byteLength + 1)
+ // const report8 = (new Uint8Array(report)).copyWithin(1, 0)
+ // report8[0] = REPORT_ID
+ // return hid.write([ ...report8 ])
+ },
+ close(controller) {
+ console.log('hid stream writer close')
+ },
+ abort(reason) {
+ console.log('hid stream writer abort')
+ }
+ },
+ queuingStrategy)
+}
+
+
+export class NodeHIDStreamSource {
+ #r
+ #w
+ #device
+
+ constructor(device) { this.#device = device }
+
+ get readable() {
+ if(this.#r === undefined) {
+ this.#r = getReadStream(this.#device)
+ }
+
+ return this.#r
+ }
+
+ get writable() {
+ if(this.#w === undefined) {
+ this.#w = getWriteStream(this.#device)
+ }
+
+ return this.#w
+ }
+}