diff --git a/package.json b/package.json index 15b7799..c788446 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@johntalton/bitsmush": "^1.0.1", "@johntalton/boschieu": "^6.0.1", "@johntalton/ds1841": "^1.0.1", - "@johntalton/ds3231": "^1.0.1", + "@johntalton/ds3231": "^1.1.0", "@johntalton/ds3502": "^4.0.0", "@johntalton/excamera-i2cdriver": "^1.0.0", "@johntalton/ht16k33": "^1.0.3", diff --git a/public/css/ds3231.css b/public/css/ds3231.css index 5644448..a6921d7 100644 --- a/public/css/ds3231.css +++ b/public/css/ds3231.css @@ -5,6 +5,10 @@ ds3231-config { & > form input[type="checkbox"] { justify-self: start; + accent-color: var(--color-accent--darker, red); + width: 1.25em; + height: 1.25em; + margin: 0.5em; } & > form[data-output] { diff --git a/public/css/mcp23.css b/public/css/mcp23.css index 57d25f4..5c2774a 100644 --- a/public/css/mcp23.css +++ b/public/css/mcp23.css @@ -20,6 +20,37 @@ mcp23-config { + & :has([data-badge]) { + position: relative; + display: grid; + + & > * { + grid-column: 1 / -1; + grid-row: 1 / -1; + } + } + + & [data-badge] { + align-self: center; + justify-self: end; + margin-inline-end: 1em; + + width: 1em; + height: 1em; + border-radius: 0.5em; + + --percent-accent: 0; + --max-accent: 0.5em; + --lerp-value: clamp( + 0px, + calc(var(--percent-accent) / 100.0 * var(--max-accent)), + var(--max-accent)); + border: var(--lerp-value) solid var(--color-accent--light, red); + + transition: border-width 500ms; + } + + & div[data-port] { display: flex; @@ -58,6 +89,10 @@ mcp23-config { background-color: var(--color-accent--darker, red); color: var(--color-accent--darker-text, red); text-decoration: underline; + + & ~ [data-badge] { + border-color: var(--color-accent--lighter, red); + } } &[data-active]:not(:hover) { @@ -116,6 +151,17 @@ mcp23-config { } + & form[data-refresh] { + display: flex; + flex-direction: row; + margin-block-end: 2em; + gap: 2em; + + & progress { + accent-color: var(--color-accent--darker, red); + } + } + & form[data-status] { display: flex; flex-direction: row; @@ -132,6 +178,14 @@ mcp23-config { & output[data-flag="false"] { visibility: hidden; } + + & output:where([name ^= "portApin"], [name ^= "portBpin"]) { + justify-self: end; + } + + & output:where([name ^= "portApin"], [name ^= "portBpin"])[data-high] { + justify-self: start; + } } } } \ No newline at end of file diff --git a/public/custom-elements/ds3231.html b/public/custom-elements/ds3231.html index 08339af..7a5c5b3 100644 --- a/public/custom-elements/ds3231.html +++ b/public/custom-elements/ds3231.html @@ -10,7 +10,7 @@
- +
@@ -34,7 +34,7 @@ - diff --git a/public/custom-elements/mcp23.html b/public/custom-elements/mcp23.html index 2289a5f..e12aa83 100644 --- a/public/custom-elements/mcp23.html +++ b/public/custom-elements/mcp23.html @@ -65,14 +65,14 @@
@@ -124,80 +124,93 @@
-
- +
+ + + + + + -
- Port A +
+
+ Port A - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - -
-
- Port B + + + +
+
+ Port B - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - -
-
+ + + +
+ +
diff --git a/public/devices-i2c/ds3231.js b/public/devices-i2c/ds3231.js index a7be3c2..dd7b8ef 100644 --- a/public/devices-i2c/ds3231.js +++ b/public/devices-i2c/ds3231.js @@ -84,15 +84,16 @@ export class DS3231Builder { const alarm1Input = root.querySelector('input[name="enableAlarm1"]') const alarm2Input = root.querySelector('input[name="enableAlarm2"]') const enableSquareWaveInput = root.querySelector('input[name="enableSquareWave"]') - const enableBatteryOscillatorInput = root.querySelector('input[name="enableBatteryOscillator"]') const enableBatterySquareWaveInput = root.querySelector('input[name="enableBatterySquareWave"]') + const squareWaveFrequencySelect = root.querySelector('select[name="squareWaveFrequency"]') alarm1Input.checked = alarm1Enabled alarm2Input.checked = alarm2Enabled enableSquareWaveInput.checked = squareWaveEnabled enableBatteryOscillatorInput.checked = batteryBackupOscillatorEnabled enableBatterySquareWaveInput.checked = batteryBackupSquareWaveEnabled + squareWaveFrequencySelect.value = squareWaveFrequencyKHz // status const { @@ -202,21 +203,34 @@ export class DS3231Builder { const alarm1Checkbox = root.querySelector('input[name="enableAlarm1"]') const alarm2Checkbox = root.querySelector('input[name="enableAlarm2"]') - + const enableSquareWaveCheckbox = root.querySelector('input[name="enableSquareWave"]') const batteryOscillatorCheckbox = root.querySelector('input[name="enableBatteryOscillator"]') + const batterySquareWaveCheckbox = root.querySelector('input[name="enableBatterySquareWave"]') + const squareWaveFrequencySelect = root.querySelector('select[name="squareWaveFrequency"]') alarm1Checkbox.disabled = true alarm2Checkbox.disabled = true batteryOscillatorCheckbox.disabled = true + enableSquareWaveCheckbox.disabled = true + batterySquareWaveCheckbox.disabled = true + squareWaveFrequencySelect.disabled = true const enableAlarm1 = alarm1Checkbox.checked const enableAlarm2 = alarm2Checkbox.checked const enableOscillatorOnBatteryBackup = batteryOscillatorCheckbox.checked + const enableSquareWave = enableSquareWaveCheckbox.checked + const enableSquareWaveOnBatteryBackup = batterySquareWaveCheckbox.checked + const squareWaveFrequencyKHz = parseFloat(squareWaveFrequencySelect.value) await this.#device.setControl({ enableAlarm1, enableAlarm2, - enableOscillatorOnBatteryBackup + + enableSquareWave, + squareWaveFrequencyKHz, + + enableOscillatorOnBatteryBackup, + enableSquareWaveOnBatteryBackup }) await refreshView(root, this.#device) @@ -224,6 +238,9 @@ export class DS3231Builder { alarm1Checkbox.disabled = false alarm2Checkbox.disabled = false batteryOscillatorCheckbox.disabled = false + enableSquareWaveCheckbox.disabled = false + batterySquareWaveCheckbox.disabled = false + squareWaveFrequencySelect.disabled = false }) const alarm1Submit = root.querySelector('form[data-alarm1] button[submit]') diff --git a/public/devices-i2c/mcp23.js b/public/devices-i2c/mcp23.js index 163f2b7..dbb61fc 100644 --- a/public/devices-i2c/mcp23.js +++ b/public/devices-i2c/mcp23.js @@ -2,6 +2,7 @@ import { BANK_0, BANK_1, + DEFAULT, DIRECTION, HIGH, INTERRUPT_CONTROL, @@ -19,10 +20,31 @@ const delayMs = ms => new Promise(resolve => setTimeout(resolve, ms)) class InvalidModeError extends Error {} +function percentDifferentFromDefault(portCache, pin) { + // return Math.random() * 100 + + const tests = [ + portCache.direction[pin] !== DEFAULT.DIRECTION, + portCache.polarity[pin] !== DEFAULT.POLARITY, + portCache.interrupt[pin] !== DEFAULT.INTERRUPT, + portCache.defaultValue[pin] !== DEFAULT.DEFAULT_VALUE, + portCache.interruptControl[pin] !== DEFAULT.INPUT_CONTROL, + portCache.pullUp[pin] !== DEFAULT.PULL_UP, + portCache.outputLatchValue[pin] !== DEFAULT.OUTPUT_LATCH_VALUE + ] + + return tests.reduce((acc, value) => acc + (value ? 1 : 0), 0) / tests.length * 100 +} + +function modeMatch(mode, bank, sequential) { + if(bank && mode.bank !== bank) { return false } + if(sequential && mode.sequential !== sequential) { return false } + return true +} + function assertMode(mode, bank, sequential) { - if(mode.bank !== bank) { throw new InvalidModeError() } - if(sequential === undefined) { return } - if(mode.sequential !== sequential) { throw new InvalidModeError() } + if(!modeMatch(mode, bank, sequential)) { throw new InvalidModeError() } + return mode } const assertModeBank0 = mode => assertMode(mode, BANK_0) @@ -98,6 +120,48 @@ class MCP23GuardedMode { } +export class MCP23GuardedModeTransactional extends MCP23GuardedMode { + constructor(host, mode = MODE.INTERLACED_BLOCK) { + super(host, mode) + } + + async _getPort(port) { + console.log('getPort fallback transaction') + const [ + direction, + polarity, + interrupt, + defaultValue, + interruptControl, + pullUp, + outputLatchValue + ] = await Promise.all([ + this.getDirection(port), + this.getPolarity(port), + this.getInterrupt(port), + this.getDefaultValue(port), + this.getInterruptControl(port), + this.getPullUp(port), + this.getOutputLatchValue(port) + ]) + + return { + direction, + polarity, + interrupt, + defaultValue, + interruptControl, + pullUp, + outputLatchValue + } + } + + async getPort(port) { + if(modeMatch(this.mode, BANK_1, true)) { return super.getPort(port) } + return this._getPort(port) + } +} + export class MCP23Builder { #abus @@ -117,7 +181,7 @@ export class MCP23Builder { async open() { - this.#device = new MCP23GuardedMode(new MCP23(this.#abus), MODE.INTERLACED_BLOCK) + this.#device = new MCP23GuardedModeTransactional(new MCP23(this.#abus), MODE.INTERLACED_BLOCK) } async close() {} @@ -181,7 +245,7 @@ export class MCP23Builder { console.log('refresh cache from device', port) - const [ + const { direction, polarity, interrupt, @@ -189,15 +253,7 @@ export class MCP23Builder { interruptControl, pullUp, outputLatchValue - ] = await Promise.all([ - this.#device.getDirection(port), - this.#device.getPolarity(port), - this.#device.getInterrupt(port), - this.#device.getDefaultValue(port), - this.#device.getInterruptControl(port), - this.#device.getPullUp(port), - this.#device.getOutputLatchValue(port) - ]) + } = await this.#device.getPort(port) const update = { ...currentCache, @@ -223,6 +279,13 @@ export class MCP23Builder { const cache = JSON.parse(root.dataset.cache) const portCache = cache[port] + for(const pin of range(0, 7)) { + const percent = percentDifferentFromDefault(portCache, pin) + const badgeDiv = root.querySelector(`:has(> button[data-gpio="${pin}"]) > [data-badge]`) + badgeDiv.style.setProperty('--percent-accent', percent) + } + + const direction = portCache.direction[pin] const polarity = portCache.polarity[pin] const interrupt = portCache.interrupt[pin] @@ -443,37 +506,86 @@ export class MCP23Builder { const refreshPollButton = root.querySelector('button[data-refresh]') - refreshPollButton?.addEventListener('click', event => { + const refreshPollRateSelect = root.querySelector('select[name="refreshRate"]') + const refreshPollCounter = root.querySelector('output[name="counter"]') + const refreshPollProgress = root.querySelector('progress[name="progress"]') + refreshPollButton?.addEventListener('click', async event => { event.preventDefault() - refreshPollButton.disabled = true + const cacheMap = { } - const timeoutStr = refreshPollButton.getAttribute('data-refresh') - const timeout = parseInt(timeoutStr) + const pollSingle = async () => { - const poller = setInterval(async () => { - console.log('poll') for (const port of [PORT.A, PORT.B]) { const flags = await this.#device.getInterruptFlag(port) const caps = await this.#device.getInterruptCaptureValue(port) const outs = await this.#device.getOutputValue(port) for(const pin of range(0, 7)) { - const output = root.querySelector(`output[name="port${port}pin${pin}"]`) - const outputCapture = root.querySelector(`output[name="port${port}pin${pin}Capture"]`) + + if(cacheMap[`out${port}${pin}`] === undefined) { + cacheMap[`out${port}${pin}`] = root.querySelector(`output[name="port${port}pin${pin}"]`) + } + const output = cacheMap[`out${port}${pin}`] + + if(cacheMap[`outCap${port}${pin}`] === undefined) { + cacheMap[`outCap${port}${pin}`] = root.querySelector(`output[name="port${port}pin${pin}Capture"]`) + } + const outputCapture = cacheMap[`outCap${port}${pin}`] outputCapture?.setAttribute('data-flag', flags[pin]) outputCapture.innerText = caps[pin] === HIGH ? '🔔 (High)' : '🔔 (Low)' output?.toggleAttribute('data-high', outs[pin]) output.innerText = outs[pin] === HIGH ? 'High' : 'Low' - } } - }, 1000 * 1) + } + + const timeoutStr = refreshPollRateSelect.value + + if(timeoutStr === 'once') { + refreshPollButton.disabled = true + refreshPollProgress.max = 100 + refreshPollProgress.value = 0 + + await pollSingle() + + refreshPollButton.disabled = false + refreshPollProgress.value = 100 + + return + } + + const timeout = parseInt(timeoutStr) + var counter = 0 + const POLL_RATE_S = 0.5 + + refreshPollButton.disabled = true + refreshPollRateSelect.disabled = true + refreshPollCounter.innerText = '' + + refreshPollProgress.disabled = false + refreshPollProgress.value = counter + refreshPollProgress.max = timeout + + // + const poller = setInterval(async () => { + counter += POLL_RATE_S + refreshPollProgress.value = counter + + await pollSingle() + + }, 1000 * POLL_RATE_S) setTimeout(() => { clearInterval(poller) + + refreshPollRateSelect.disabled = false refreshPollButton.disabled = false + + refreshPollProgress.disabled = true + refreshPollProgress.value = 100 + refreshPollProgress.max = 100 }, 1000 * timeout) }) diff --git a/public/devices-serial/exc-i2cdriver.js b/public/devices-serial/exc-i2cdriver.js index c750cfd..5ab2ae5 100644 --- a/public/devices-serial/exc-i2cdriver.js +++ b/public/devices-serial/exc-i2cdriver.js @@ -260,12 +260,12 @@ export class ExcameraI2CDriverUIBuilder {
- + @@ -540,7 +540,8 @@ export class ExcameraI2CDriverUIBuilder { const humanUptime = uptimeToHuman(uptime) - const out = name => document.querySelector(`[data-info] output[name=${name}]`) + const exRoot = event.target.closest('excamera-i2cdriver') + const out = name => exRoot.querySelector(`[data-info] output[name=${name}]`) out('model').innerText = identifier out('serial').innerText = serial