Crudely adapt webapp to new firmware

This commit is contained in:
daylily
2026-01-09 16:35:10 +08:00
parent c0c65f1fae
commit c72aa66405
16 changed files with 3040 additions and 4529 deletions
+5 -1
View File
@@ -4,5 +4,9 @@
"semi": false, "semi": false,
"arrowParens": "avoid", "arrowParens": "avoid",
"singleQuote": true, "singleQuote": true,
"printWidth": 120 "printWidth": 100,
"plugins": [
"prettier-plugin-svelte"
],
"svelteIndentScriptAndStyle": false
} }
-4341
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -16,10 +16,13 @@
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/w3c-web-hid": "^1.0.6", "@types/w3c-web-hid": "^1.0.6",
"@variegated-coffee/serde-postcard-ts": "^0.1.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "^1.3.16", "bits-ui": "^1.3.16",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"mode-watcher": "^0.5.1", "mode-watcher": "^0.5.1",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.25.7", "svelte": "^5.25.7",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.1.0",
@@ -30,5 +33,6 @@
"unplugin-icons": "^22.1.0", "unplugin-icons": "^22.1.0",
"vite": "^6.2.5", "vite": "^6.2.5",
"wrangler": "^4.34.0" "wrangler": "^4.34.0"
} },
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
} }
+2667
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ModeWatcher } from 'mode-watcher' import { ModeWatcher } from 'mode-watcher'
import { Toaster } from 'svelte-sonner' import { Toaster } from 'svelte-sonner'
import Footer from '$lib/layouts/Footer.svelte' import Footer from '$lib/layouts/Footer.svelte'
import Main from '$lib/layouts/Main.svelte' import Main from '$lib/layouts/Main.svelte'
import Unsupported from '$lib/layouts/Unsupported.svelte' import Unsupported from '$lib/layouts/Unsupported.svelte'
const unsupported = navigator.hid === undefined const unsupported = navigator.requestMIDIAccess === undefined
</script> </script>
<ModeWatcher /> <ModeWatcher />
+49 -47
View File
@@ -1,13 +1,55 @@
import { DEVICE_PID, DEVICE_VID } from '$lib/constants' import { Device } from '$lib/image/device'
import { getContext, onDestroy, setContext } from 'svelte' import { getContext, setContext, untrack } from 'svelte'
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
function isInkclip(dev: HIDDevice) { function isInkclip(port: MIDIPort): boolean {
return dev.vendorId == DEVICE_VID && dev.productId == DEVICE_PID return !!(
port.manufacturer?.toLowerCase().includes('daylily') ||
port.name?.toLowerCase().includes('inkclip')
)
} }
export interface DeviceContext { export class DeviceContext {
device: HIDDevice | null private input: MIDIInput | null = $state(null)
private output: MIDIOutput | null = $state(null)
device: Device | null = $state(null)
constructor() {
$effect(() => {
if (untrack(() => this.device == null)) {
if (this.input == null || this.output == null) return
this.device = new Device(this.input, this.output)
toast.info('Device connected')
} else {
if (this.input != null && this.output != null) return
this.device = null
}
})
}
async initialize() {
try {
const midi = await navigator.requestMIDIAccess({ sysex: true })
console.log(Array.from(midi.inputs.values()), Array.from(midi.outputs.values()))
this.input = midi.inputs.values().find(isInkclip) ?? null
this.output = midi.outputs.values().find(isInkclip) ?? null
midi.addEventListener('statechange', e => {
console.log(e.port)
if (!e.port || !isInkclip(e.port)) return
if (e.port?.state === 'connected') {
if (e.port.type === 'input' && !this.input) this.input = e.port as MIDIInput
else if (e.port.type === 'output' && !this.output) this.output = e.port as MIDIOutput
} else {
if (e.port.type === 'input' && e.port === this.input) this.input = null
else if (e.port.type === 'output' && e.port === this.output) this.output = null
}
})
} catch (e) {
toast.error('Failed to acquire MIDI access. Please grant access manually in your browser.')
}
}
} }
const DeviceContextToken = Symbol('device') const DeviceContextToken = Symbol('device')
@@ -17,46 +59,6 @@ export function getDeviceContext(): DeviceContext {
} }
export function createDeviceContext(): DeviceContext { export function createDeviceContext(): DeviceContext {
const ctx: DeviceContext = $state({ device: null }) let ctx = new DeviceContext()
navigator.hid.getDevices().then(devices => {
const dev = devices.find(isInkclip)
if (dev !== undefined) ctx.device = dev
})
function connectIfIdle(e: HIDConnectionEvent) {
if (!isInkclip(e.device) || ctx.device !== null) return
toast.info('Device connected')
ctx.device = e.device
}
function disconnectIfSame(e: HIDConnectionEvent) {
if (ctx.device !== e.device) return
toast.info('Device disconnected')
ctx.device = null
}
navigator.hid.addEventListener('connect', connectIfIdle)
navigator.hid.addEventListener('disconnect', disconnectIfSame)
onDestroy(() => {
navigator.hid.removeEventListener('connect', connectIfIdle)
navigator.hid.removeEventListener('disconnect', disconnectIfSame)
})
return setContext(DeviceContextToken, ctx) return setContext(DeviceContextToken, ctx)
} }
export async function tryOpenDevice(device: HIDDevice): Promise<boolean> {
if (device.opened) return true
try {
await device.open()
return true
} catch (e) {
toast.error(`Unable to open device: ${e}`)
return false
}
}
+6 -8
View File
@@ -6,8 +6,8 @@ import { Scaler } from '$lib/image/scaler'
import { Quantizer } from '$lib/image/quantizer' import { Quantizer } from '$lib/image/quantizer'
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants' import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
export interface RenderedContext { export class RenderedContext {
rendered: number[] | null rendered: Uint8Array | null = $state(null)
} }
export const RenderedContextToken = Symbol('rendered') export const RenderedContextToken = Symbol('rendered')
@@ -20,18 +20,16 @@ const scaler = new Scaler(DEVICE_WIDTH, DEVICE_HEIGHT)
export function createRenderedContext( export function createRenderedContext(
imageCtx: Readonly<ImageContext>, imageCtx: Readonly<ImageContext>,
config: ConversionConfig config: ConversionConfig,
): Readonly<RenderedContext> { ): Readonly<RenderedContext> {
const ctx: RenderedContext = $state({ const ctx: RenderedContext = new RenderedContext()
rendered: null,
})
const quantizer = $derived( const quantizer = $derived(
new Quantizer({ new Quantizer({
ditheringKernel: config.ditheringKernel, ditheringKernel: config.ditheringKernel,
contrast: config.contrast, contrast: config.contrast,
brightness: config.brightness, brightness: config.brightness,
}) }),
) )
const canvas = new OffscreenCanvas(DEVICE_WIDTH, DEVICE_HEIGHT) const canvas = new OffscreenCanvas(DEVICE_WIDTH, DEVICE_HEIGHT)
@@ -56,7 +54,7 @@ export function createRenderedContext(
}) })
const quantizedData = quantizer.reduce(canvasCtx) const quantizedData = quantizer.reduce(canvasCtx)
ctx.rendered = quantizedData ctx.rendered = Uint8Array.from(quantizedData)
}) })
return setContext(RenderedContextToken, ctx) return setContext(RenderedContextToken, ctx)
+107
View File
@@ -0,0 +1,107 @@
import * as p from '@variegated-coffee/serde-postcard-ts'
import { decode7in8InPlace, encode7in8 } from './pack'
export type DeviceType = p.InferType<typeof deviceTypeSchema>
const deviceTypeSchema = p.enumType('DeviceType', {
BWRev1: p.unitVariant('BWRev1'),
})
export type Chroma = p.InferType<typeof chromaSchema>
const chromaSchema = p.enumType('Chroma', {
Black: p.unitVariant('Black'),
})
export type Request = p.InferType<typeof requestSchema>
const requestSchema = p.enumType('Request', {
GetIdentification: p.unitVariant('GetIdentification'),
UpdateDisplay: p.unitVariant('UpdateDisplay'),
SetPattern: p.structVariant('SetPattern', {
from: p.u32(),
to: p.u32(),
chroma: chromaSchema,
pattern: p.bytes(),
}),
})
export type Response = p.InferType<typeof responseSchema>
const responseSchema = p.enumType('Response', {
GetIdentification: p.structVariant('GetIdentification', {
serial: p.string(),
model: deviceTypeSchema,
}),
UpdateDisplay: p.unitVariant('UpdateDisplay'),
SetPattern: p.unitVariant('SetPattern'),
})
const MAGIC_NUMBER = [0x7d]
interface RecvOptions {
timeout?: number
filter?: (resp: Response) => boolean
}
export class Device {
constructor(
private input: MIDIInput,
private output: MIDIOutput,
) {}
private send(req: Request) {
const encResult = encode7in8(p.serialize(requestSchema, req))
const len = 1 + MAGIC_NUMBER.length + encResult.byteLength + 1
const buf = new Uint8Array(len)
// SysEx start
buf[0] = 0xf0
// Magic number
for (let ix = 0; ix < MAGIC_NUMBER.length; ix++) buf[1 + ix] = MAGIC_NUMBER[ix]
// Request body
for (let ix = 0; ix < encResult.byteLength; ix++)
buf[1 + MAGIC_NUMBER.length + ix] = encResult[ix]
// SysEx end
buf[1 + MAGIC_NUMBER.length + encResult.byteLength] = 0xf7
this.output.send(buf)
}
private recv({ timeout, filter }: RecvOptions): Promise<Response> {
return new Promise((resolve, reject) => {
const callback = (e: MIDIMessageEvent) => {
if (!e.data) return
if (e.data.length < 2 || e.data[0] !== 0xf0 || e.data[e.data.length - 1] !== 0xf7) return
const sysexPayload = e.data.slice(1, -1)
if (sysexPayload.length < MAGIC_NUMBER.length) return
for (let ix = 0; ix < MAGIC_NUMBER.length; ix++)
if (sysexPayload[ix] !== MAGIC_NUMBER[ix]) return
const payload = decode7in8InPlace(sysexPayload.slice(MAGIC_NUMBER.length))
try {
const decResult = p.deserialize(responseSchema, payload)
if (!filter || filter(decResult.value)) resolve(decResult.value)
} catch (e) {
reject(e)
}
this.input.removeEventListener('midimessage', callback)
}
if (timeout != null) setTimeout(() => reject(new Error('recv timed out')), timeout)
this.input.addEventListener('midimessage', callback)
})
}
request(
req: Request,
opts: RecvOptions = { timeout: 100, filter: resp => resp.type === req.type },
): Promise<Response> {
const recv = this.recv(opts)
this.send(req)
return recv
}
async open() {
if (this.input.connection !== 'open') await this.input.open()
if (this.output.connection !== 'open') await this.output.open()
}
}
+60
View File
@@ -0,0 +1,60 @@
// 7-in-8 encoding (SysEx-style).
/**
* Decode 7-in-8 data in-place.
* @param payload Buffer containing 7-in-8 encoded bytes.
* @returns A subarray view of `payload` containing the decoded bytes.
*/
export function decode7in8InPlace(payload: Uint8Array): Uint8Array {
let decodedLen = 0
const groups = Math.ceil(payload.length / 8)
for (let stride = 0; stride < groups; stride++) {
const base = 8 * stride
if (base >= payload.length) break
// Most significant bits of a 7-in-8 group
const msbs = payload[base]
for (let stroll = 1; stroll < 8; stroll++) {
const inIx = base + stroll
if (inIx >= payload.length) break
const low7 = payload[inIx] & 0x7f
const msb = (msbs << (8 - stroll)) & 0x80
payload[decodedLen] = (low7 | msb) & 0xff
decodedLen++
}
}
return payload.subarray(0, decodedLen)
}
/**
* Encode raw bytes into 7-in-8 form (SysEx-style), allocating the output buffer.
* Output length is: n + ceil(n/7)
*/
export function encode7in8(payload: Uint8Array): Uint8Array {
const outLen = payload.length + Math.ceil(payload.length / 7)
const output = new Uint8Array(outLen)
let encodedLen = 0
const groups = Math.ceil(payload.length / 7)
for (let stride = 0; stride < groups; stride++) {
const msbsIx = encodedLen
output[msbsIx] = 0
encodedLen++
for (let stroll = 1; stroll < 8; stroll++) {
const inIx = 7 * stride + (stroll - 1)
if (inIx >= payload.length) break
output[msbsIx] |= (payload[inIx] & 0x80) >> (8 - stroll)
output[encodedLen] = payload[inIx] & 0x7f
encodedLen++
}
}
return output
}
+19 -14
View File
@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import { Separator } from '$lib/components/ui/separator' import { Separator } from '$lib/components/ui/separator'
import ConnectSection from '$lib/layouts/connect/ConnectSection.svelte' import ConnectSection from '$lib/layouts/connect/ConnectSection.svelte'
import EditSection from '$lib/layouts/edit/EditSection.svelte' import EditSection from '$lib/layouts/edit/EditSection.svelte'
import WriteSection from '$lib/layouts/write/WriteSection.svelte' import WriteSection from '$lib/layouts/write/WriteSection.svelte'
import { createDeviceContext } from '$lib/contexts/device.svelte' import { createDeviceContext } from '$lib/contexts/device.svelte'
import { createConversionConfig } from '$lib/contexts/config.svelte' import { createConversionConfig } from '$lib/contexts/config.svelte'
import { createFilesContext } from '$lib/contexts/files.svelte' import { createFilesContext } from '$lib/contexts/files.svelte'
import { createImageContext } from '$lib/contexts/image.svelte' import { createImageContext } from '$lib/contexts/image.svelte'
import { createRenderedContext } from '$lib/contexts/rendered.svelte' import { createRenderedContext } from '$lib/contexts/rendered.svelte'
import { onMount } from 'svelte'
createDeviceContext() const deviceCtx = createDeviceContext()
const config = createConversionConfig() const config = createConversionConfig()
const filesCtx = createFilesContext() const filesCtx = createFilesContext()
const imageCtx = createImageContext(filesCtx) const imageCtx = createImageContext(filesCtx)
createRenderedContext(imageCtx, config) createRenderedContext(imageCtx, config)
onMount(() => {
deviceCtx.initialize()
})
</script> </script>
<main class="grow w-full stack gap-4"> <main class="grow w-full stack gap-4">
+2 -2
View File
@@ -16,12 +16,12 @@
<AlertDialog.Title>Browser not supported</AlertDialog.Title> <AlertDialog.Title>Browser not supported</AlertDialog.Title>
<AlertDialog.Description class="stack gap-2"> <AlertDialog.Description class="stack gap-2">
<p>Write to Inkclip uses the WebHID API, which is not supported by your browser.</p> <p>Write to Inkclip uses the Web MIDI API, which is not supported by your browser.</p>
<p> <p>
We recommend using a Chromium-based browser, such as a recent version of We recommend using a Chromium-based browser, such as a recent version of
<a href="https://www.google.com/chrome/">Google Chrome</a>, <a href="https://www.google.com/chrome/">Google Chrome</a>,
<a href="https://www.microsoft.com/edge">Microsoft Edge</a>, or <a href="https://www.microsoft.com/edge">Microsoft Edge</a>, or
<a href="https://www.opera.com/">Opera</a>. <a href="https://www.opera.com/">Opera</a>. Firefox may also work, although we are aware of more issues there.
</p> </p>
</AlertDialog.Description> </AlertDialog.Description>
</AlertDialog.Content> </AlertDialog.Content>
@@ -1,27 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import { getDeviceContext } from '$lib/contexts/device.svelte'
import { DEVICE_PID, DEVICE_VID } from '$lib/constants' const deviceCtx = getDeviceContext()
import { getDeviceContext } from '$lib/contexts/device.svelte'
const deviceCtx = getDeviceContext()
async function requestDevice() {
const devs = await navigator.hid.requestDevice({
filters: [
{
vendorId: DEVICE_VID,
productId: DEVICE_PID,
},
],
})
if (devs == undefined || devs[0] == undefined) return
deviceCtx.device = devs[0]
}
</script> </script>
<Button variant={deviceCtx.device === null ? 'default' : 'secondary'} onclick={requestDevice}> <Button
variant={deviceCtx.device === null ? 'default' : 'secondary'}
onclick={() => {
/* TODO: */
}}
>
{#if deviceCtx.device === null} {#if deviceCtx.device === null}
Select device Select device
{:else} {:else}
@@ -1,51 +1,50 @@
<script lang="ts"> <script lang="ts">
import IconPending from '~icons/material-symbols/pending' import IconPending from '~icons/material-symbols/pending'
import IconCheckCircle from '~icons/material-symbols/check-circle' import IconCheckCircle from '~icons/material-symbols/check-circle'
import ConnectButton from './ConnectButton.svelte' import ConnectButton from './ConnectButton.svelte'
import { getDeviceContext, tryOpenDevice } from '$lib/contexts/device.svelte' import { getDeviceContext } from '$lib/contexts/device.svelte'
import MoreInfo from '$lib/components/MoreInfo.svelte' import MoreInfo from '$lib/components/MoreInfo.svelte'
import { SERIAL_NUMBER_REPORT_ID } from '$lib/constants' import { assert } from '$lib/utils'
const deviceCtx = getDeviceContext() const deviceCtx = getDeviceContext()
let serial = $state('[Retrieving...]') let serial = $state('[Retrieving...]')
async function updateSerial() { async function updateSerial() {
if (deviceCtx.device === null) { if (deviceCtx.device === null) {
serial = '[Retrieving...]' serial = '[Retrieving...]'
return return
}
if (!(await tryOpenDevice(deviceCtx.device))) {
serial = '[Error]'
return
}
let updated = false
function setSerial(e: HIDInputReportEvent) {
if (updated || e.reportId !== SERIAL_NUMBER_REPORT_ID) return
serial = String.fromCharCode(...Array.from(new Uint8Array(e.data.buffer)))
updated = true
}
deviceCtx.device.addEventListener('inputreport', setSerial)
await deviceCtx.device.sendReport(SERIAL_NUMBER_REPORT_ID, new Uint8Array(1))
} }
$effect(() => { await deviceCtx.device.open()
updateSerial()
}) try {
const response = await deviceCtx.device.request({ type: 'GetIdentification' })
assert(response.type === 'GetIdentification')
console.log(response)
serial = response.value.serial
} catch (e) {
console.error(e)
serial = '[Error]'
}
}
$effect(() => {
updateSerial()
})
</script> </script>
<section class="row gap-2 max-lg:stack max-lg:items-stretch" aria-labelledby="connect-section-label"> <section
class="row gap-2 max-lg:stack max-lg:items-stretch"
aria-labelledby="connect-section-label"
>
<div class="grow"> <div class="grow">
<h2 class="font-semibold text-xl/8" id="connect-section-label">Connect to a device</h2> <h2 class="font-semibold text-xl/8" id="connect-section-label">Connect to a device</h2>
<div class="row gap-1 text-sm" aria-live="polite"> <div class="row gap-1 text-sm" aria-live="polite">
{#if deviceCtx.device === null} {#if deviceCtx.device === null}
<IconPending aria-hidden /> Not connected to any device yet. Plug in your device, and click on the button to select <IconPending aria-hidden /> Not connected to any device yet. Plug in your device to connect.
it.
{:else} {:else}
<MoreInfo> <MoreInfo>
{#snippet icon()} {#snippet icon()}
@@ -53,10 +52,10 @@
{/snippet} {/snippet}
The serial ID of this device is <code>{serial}</code>. The serial ID of this device is <code>{serial}</code>.
</MoreInfo> </MoreInfo>
Successfully conected to device. If you want to, you can connect to another device instead. Successfully conected to device.
{/if} {/if}
</div> </div>
</div> </div>
<ConnectButton /> <!-- <ConnectButton /> -->
</section> </section>
@@ -9,7 +9,11 @@ export function freshContext(el: HTMLCanvasElement) {
return ctx return ctx
} }
export function makeAltText(filesCtx: FilesContext, imageCtx: ImageContext, config: ConversionConfig): string { export function makeAltText(
filesCtx: FilesContext,
imageCtx: ImageContext,
config: ConversionConfig,
): string {
if (filesCtx.files.length < 1) return 'No image selected for e-paper preview' if (filesCtx.files.length < 1) return 'No image selected for e-paper preview'
const file = filesCtx.files[0] const file = filesCtx.files[0]
@@ -50,8 +54,10 @@ export function makeAltText(filesCtx: FilesContext, imageCtx: ImageContext, conf
return output.join(', ') return output.join(', ')
} }
export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: number[]) { export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: Uint8Array) {
console.log('attempting drawQuantizedData')
const imageData = ctx.createImageData(DEVICE_WIDTH, DEVICE_HEIGHT) const imageData = ctx.createImageData(DEVICE_WIDTH, DEVICE_HEIGHT)
console.log('finished creating')
for (let y = 0; y < DEVICE_HEIGHT; y++) { for (let y = 0; y < DEVICE_HEIGHT; y++) {
for (let x = 0; x < DEVICE_WIDTH; x++) { for (let x = 0; x < DEVICE_WIDTH; x++) {
@@ -66,5 +72,8 @@ export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: number[])
imageData.data[bitmapIx + 3] = 0xff imageData.data[bitmapIx + 3] = 0xff
} }
} }
console.log('finished writing')
ctx.putImageData(imageData, 0, 0) ctx.putImageData(imageData, 0, 0)
console.log('finished putting')
} }
+55 -51
View File
@@ -1,67 +1,71 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
import { getDeviceContext, tryOpenDevice } from '$lib/contexts/device.svelte' import { getDeviceContext } from '$lib/contexts/device.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte' import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { import { BYTES_IN_A_ROW, DEVICE_HEIGHT, DEVICE_WIDTH, WRITE_TIME } from '$lib/constants'
BYTES_IN_A_ROW,
DEVICE_HEIGHT,
DEVICE_WIDTH,
SERIAL_NUMBER_REPORT_ID,
WRITE_PATTERN_REPORT_ID,
WRITE_TIME,
} from '$lib/constants'
interface Props { interface Props {
onprogress: (inPropgress: boolean) => void onprogress: (inPropgress: boolean) => void
} }
const { onprogress }: Props = $props() const { onprogress }: Props = $props()
let inProgress = $state(false) let inProgress = $state(false)
const deviceCtx = getDeviceContext() const deviceCtx = getDeviceContext()
const renderedCtx = getRenderedContext() const renderedCtx = getRenderedContext()
let disabled = $derived(deviceCtx.device === null || renderedCtx.rendered === null || inProgress) let disabled = $derived(deviceCtx.device === null || renderedCtx.rendered === null || inProgress)
let secondary = $derived(deviceCtx.device === null) let secondary = $derived(deviceCtx.device === null)
async function connectAndWrite() { async function connectAndWrite() {
if (deviceCtx.device === null || renderedCtx.rendered === null) return if (deviceCtx.device === null || renderedCtx.rendered === null) return
if (!(await tryOpenDevice(deviceCtx.device))) return await deviceCtx.device.open()
const buffer = new Uint8Array(DEVICE_HEIGHT * BYTES_IN_A_ROW) const buffer = new Uint8Array(DEVICE_HEIGHT * BYTES_IN_A_ROW)
for (let y = 0; y < DEVICE_HEIGHT; y++) { for (let y = 0; y < DEVICE_HEIGHT; y++) {
for (let xStride = 0; xStride < BYTES_IN_A_ROW; xStride++) { for (let xStride = 0; xStride < BYTES_IN_A_ROW; xStride++) {
let cell = 0x0 let cell = 0x0
for (let xStroll = 0; xStroll < 8; xStroll++) { for (let xStroll = 0; xStroll < 8; xStroll++) {
const index = y * DEVICE_WIDTH + xStride * 8 + xStroll const index = y * DEVICE_WIDTH + xStride * 8 + xStroll
cell |= renderedCtx.rendered[index] << xStroll cell |= renderedCtx.rendered[index] << xStroll
}
buffer[y * BYTES_IN_A_ROW + xStride] = cell
} }
buffer[y * BYTES_IN_A_ROW + xStride] = cell
} }
inProgress = true
try {
await deviceCtx.device.sendReport(WRITE_PATTERN_REPORT_ID, buffer)
} catch (e) {
toast.error(`Error writing to device: ${e}`)
return
} finally {
setTimeout(() => {
inProgress = false
}, WRITE_TIME)
}
toast.success('Wrote pattern to device')
} }
$effect(() => { inProgress = true
onprogress(inProgress)
}) try {
for (let i = 0; i < buffer.byteLength; i += 500) {
await deviceCtx.device.request({
type: 'SetPattern',
value: {
from: i,
to: i + 500,
chroma: { type: 'Black' },
pattern: buffer.slice(i, i + 500),
},
})
}
await deviceCtx.device.request({ type: 'UpdateDisplay' })
} catch (e) {
toast.error(`Error writing to device: ${e}`)
return
} finally {
setTimeout(() => {
inProgress = false
}, WRITE_TIME)
}
toast.success('Wrote pattern to device')
}
$effect(() => {
onprogress(inProgress)
})
</script> </script>
<Button onclick={connectAndWrite} variant={secondary ? 'secondary' : 'default'} {disabled}> <Button onclick={connectAndWrite} variant={secondary ? 'secondary' : 'default'} {disabled}>
+4
View File
@@ -8,3 +8,7 @@ export function cn(...inputs: ClassValue[]) {
export function showNumber(n: number): string { export function showNumber(n: number): string {
return String(n).replace('-', '').replace('Infinity', '∞').replace('NaN', '⁇') return String(n).replace('-', '').replace('Infinity', '∞').replace('NaN', '⁇')
} }
export function assert(expr: unknown): asserts expr {
if (!expr) throw Error('assertion failed')
}