Crudely adapt webapp to new firmware
This commit is contained in:
+5
-1
@@ -4,5 +4,9 @@
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte"
|
||||
],
|
||||
"svelteIndentScriptAndStyle": false
|
||||
}
|
||||
|
||||
Generated
-4341
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -16,10 +16,13 @@
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/w3c-web-hid": "^1.0.6",
|
||||
"@variegated-coffee/serde-postcard-ts": "^0.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^1.3.16",
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^0.5.1",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"svelte": "^5.25.7",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
@@ -30,5 +33,6 @@
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite": "^6.2.5",
|
||||
"wrangler": "^4.34.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||
}
|
||||
|
||||
Generated
+2667
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ModeWatcher } from 'mode-watcher'
|
||||
import { Toaster } from 'svelte-sonner'
|
||||
import { ModeWatcher } from 'mode-watcher'
|
||||
import { Toaster } from 'svelte-sonner'
|
||||
|
||||
import Footer from '$lib/layouts/Footer.svelte'
|
||||
import Main from '$lib/layouts/Main.svelte'
|
||||
import Unsupported from '$lib/layouts/Unsupported.svelte'
|
||||
import Footer from '$lib/layouts/Footer.svelte'
|
||||
import Main from '$lib/layouts/Main.svelte'
|
||||
import Unsupported from '$lib/layouts/Unsupported.svelte'
|
||||
|
||||
const unsupported = navigator.hid === undefined
|
||||
const unsupported = navigator.requestMIDIAccess === undefined
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
import { DEVICE_PID, DEVICE_VID } from '$lib/constants'
|
||||
import { getContext, onDestroy, setContext } from 'svelte'
|
||||
import { Device } from '$lib/image/device'
|
||||
import { getContext, setContext, untrack } from 'svelte'
|
||||
import { toast } from 'svelte-sonner'
|
||||
|
||||
function isInkclip(dev: HIDDevice) {
|
||||
return dev.vendorId == DEVICE_VID && dev.productId == DEVICE_PID
|
||||
function isInkclip(port: MIDIPort): boolean {
|
||||
return !!(
|
||||
port.manufacturer?.toLowerCase().includes('daylily') ||
|
||||
port.name?.toLowerCase().includes('inkclip')
|
||||
)
|
||||
}
|
||||
|
||||
export interface DeviceContext {
|
||||
device: HIDDevice | null
|
||||
export class DeviceContext {
|
||||
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')
|
||||
@@ -17,46 +59,6 @@ export function getDeviceContext(): DeviceContext {
|
||||
}
|
||||
|
||||
export function createDeviceContext(): DeviceContext {
|
||||
const ctx: DeviceContext = $state({ device: null })
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
let ctx = new DeviceContext()
|
||||
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 +6,8 @@ import { Scaler } from '$lib/image/scaler'
|
||||
import { Quantizer } from '$lib/image/quantizer'
|
||||
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
|
||||
|
||||
export interface RenderedContext {
|
||||
rendered: number[] | null
|
||||
export class RenderedContext {
|
||||
rendered: Uint8Array | null = $state(null)
|
||||
}
|
||||
|
||||
export const RenderedContextToken = Symbol('rendered')
|
||||
@@ -20,18 +20,16 @@ const scaler = new Scaler(DEVICE_WIDTH, DEVICE_HEIGHT)
|
||||
|
||||
export function createRenderedContext(
|
||||
imageCtx: Readonly<ImageContext>,
|
||||
config: ConversionConfig
|
||||
config: ConversionConfig,
|
||||
): Readonly<RenderedContext> {
|
||||
const ctx: RenderedContext = $state({
|
||||
rendered: null,
|
||||
})
|
||||
const ctx: RenderedContext = new RenderedContext()
|
||||
|
||||
const quantizer = $derived(
|
||||
new Quantizer({
|
||||
ditheringKernel: config.ditheringKernel,
|
||||
contrast: config.contrast,
|
||||
brightness: config.brightness,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const canvas = new OffscreenCanvas(DEVICE_WIDTH, DEVICE_HEIGHT)
|
||||
@@ -56,7 +54,7 @@ export function createRenderedContext(
|
||||
})
|
||||
|
||||
const quantizedData = quantizer.reduce(canvasCtx)
|
||||
ctx.rendered = quantizedData
|
||||
ctx.rendered = Uint8Array.from(quantizedData)
|
||||
})
|
||||
|
||||
return setContext(RenderedContextToken, ctx)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator'
|
||||
import ConnectSection from '$lib/layouts/connect/ConnectSection.svelte'
|
||||
import EditSection from '$lib/layouts/edit/EditSection.svelte'
|
||||
import WriteSection from '$lib/layouts/write/WriteSection.svelte'
|
||||
import { Separator } from '$lib/components/ui/separator'
|
||||
import ConnectSection from '$lib/layouts/connect/ConnectSection.svelte'
|
||||
import EditSection from '$lib/layouts/edit/EditSection.svelte'
|
||||
import WriteSection from '$lib/layouts/write/WriteSection.svelte'
|
||||
|
||||
import { createDeviceContext } from '$lib/contexts/device.svelte'
|
||||
import { createConversionConfig } from '$lib/contexts/config.svelte'
|
||||
import { createFilesContext } from '$lib/contexts/files.svelte'
|
||||
import { createImageContext } from '$lib/contexts/image.svelte'
|
||||
import { createRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import { createDeviceContext } from '$lib/contexts/device.svelte'
|
||||
import { createConversionConfig } from '$lib/contexts/config.svelte'
|
||||
import { createFilesContext } from '$lib/contexts/files.svelte'
|
||||
import { createImageContext } from '$lib/contexts/image.svelte'
|
||||
import { createRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
createDeviceContext()
|
||||
const config = createConversionConfig()
|
||||
const filesCtx = createFilesContext()
|
||||
const imageCtx = createImageContext(filesCtx)
|
||||
createRenderedContext(imageCtx, config)
|
||||
const deviceCtx = createDeviceContext()
|
||||
const config = createConversionConfig()
|
||||
const filesCtx = createFilesContext()
|
||||
const imageCtx = createImageContext(filesCtx)
|
||||
createRenderedContext(imageCtx, config)
|
||||
|
||||
onMount(() => {
|
||||
deviceCtx.initialize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<main class="grow w-full stack gap-4">
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
<AlertDialog.Title>Browser not supported</AlertDialog.Title>
|
||||
|
||||
<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>
|
||||
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.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>
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Content>
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
<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'
|
||||
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]
|
||||
}
|
||||
const deviceCtx = getDeviceContext()
|
||||
</script>
|
||||
|
||||
<Button variant={deviceCtx.device === null ? 'default' : 'secondary'} onclick={requestDevice}>
|
||||
<Button
|
||||
variant={deviceCtx.device === null ? 'default' : 'secondary'}
|
||||
onclick={() => {
|
||||
/* TODO: */
|
||||
}}
|
||||
>
|
||||
{#if deviceCtx.device === null}
|
||||
Select device
|
||||
{:else}
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
<script lang="ts">
|
||||
import IconPending from '~icons/material-symbols/pending'
|
||||
import IconCheckCircle from '~icons/material-symbols/check-circle'
|
||||
import ConnectButton from './ConnectButton.svelte'
|
||||
import IconPending from '~icons/material-symbols/pending'
|
||||
import IconCheckCircle from '~icons/material-symbols/check-circle'
|
||||
import ConnectButton from './ConnectButton.svelte'
|
||||
|
||||
import { getDeviceContext, tryOpenDevice } from '$lib/contexts/device.svelte'
|
||||
import MoreInfo from '$lib/components/MoreInfo.svelte'
|
||||
import { SERIAL_NUMBER_REPORT_ID } from '$lib/constants'
|
||||
import { getDeviceContext } from '$lib/contexts/device.svelte'
|
||||
import MoreInfo from '$lib/components/MoreInfo.svelte'
|
||||
import { assert } from '$lib/utils'
|
||||
|
||||
const deviceCtx = getDeviceContext()
|
||||
const deviceCtx = getDeviceContext()
|
||||
|
||||
let serial = $state('[Retrieving...]')
|
||||
let serial = $state('[Retrieving...]')
|
||||
|
||||
async function updateSerial() {
|
||||
if (deviceCtx.device === null) {
|
||||
serial = '[Retrieving...]'
|
||||
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))
|
||||
async function updateSerial() {
|
||||
if (deviceCtx.device === null) {
|
||||
serial = '[Retrieving...]'
|
||||
return
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updateSerial()
|
||||
})
|
||||
await deviceCtx.device.open()
|
||||
|
||||
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>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
{#if deviceCtx.device === null}
|
||||
<IconPending aria-hidden /> Not connected to any device yet. Plug in your device, and click on the button to select
|
||||
it.
|
||||
<IconPending aria-hidden /> Not connected to any device yet. Plug in your device to connect.
|
||||
{:else}
|
||||
<MoreInfo>
|
||||
{#snippet icon()}
|
||||
@@ -53,10 +52,10 @@
|
||||
{/snippet}
|
||||
The serial ID of this device is <code>{serial}</code>.
|
||||
</MoreInfo>
|
||||
Successfully conected to device. If you want to, you can connect to another device instead.
|
||||
Successfully conected to device.
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConnectButton />
|
||||
<!-- <ConnectButton /> -->
|
||||
</section>
|
||||
|
||||
@@ -9,7 +9,11 @@ export function freshContext(el: HTMLCanvasElement) {
|
||||
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'
|
||||
|
||||
const file = filesCtx.files[0]
|
||||
@@ -50,8 +54,10 @@ export function makeAltText(filesCtx: FilesContext, imageCtx: ImageContext, conf
|
||||
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)
|
||||
console.log('finished creating')
|
||||
|
||||
for (let y = 0; y < DEVICE_HEIGHT; y++) {
|
||||
for (let x = 0; x < DEVICE_WIDTH; x++) {
|
||||
@@ -66,5 +72,8 @@ export function drawQuantizedData(ctx: CanvasRenderingContext2D, data: number[])
|
||||
imageData.data[bitmapIx + 3] = 0xff
|
||||
}
|
||||
}
|
||||
console.log('finished writing')
|
||||
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
console.log('finished putting')
|
||||
}
|
||||
|
||||
@@ -1,67 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { getDeviceContext, tryOpenDevice } from '$lib/contexts/device.svelte'
|
||||
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import {
|
||||
BYTES_IN_A_ROW,
|
||||
DEVICE_HEIGHT,
|
||||
DEVICE_WIDTH,
|
||||
SERIAL_NUMBER_REPORT_ID,
|
||||
WRITE_PATTERN_REPORT_ID,
|
||||
WRITE_TIME,
|
||||
} from '$lib/constants'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { getDeviceContext } from '$lib/contexts/device.svelte'
|
||||
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
|
||||
import { BYTES_IN_A_ROW, DEVICE_HEIGHT, DEVICE_WIDTH, WRITE_TIME } from '$lib/constants'
|
||||
|
||||
interface Props {
|
||||
onprogress: (inPropgress: boolean) => void
|
||||
}
|
||||
interface Props {
|
||||
onprogress: (inPropgress: boolean) => void
|
||||
}
|
||||
|
||||
const { onprogress }: Props = $props()
|
||||
const { onprogress }: Props = $props()
|
||||
|
||||
let inProgress = $state(false)
|
||||
let inProgress = $state(false)
|
||||
|
||||
const deviceCtx = getDeviceContext()
|
||||
const renderedCtx = getRenderedContext()
|
||||
const deviceCtx = getDeviceContext()
|
||||
const renderedCtx = getRenderedContext()
|
||||
|
||||
let disabled = $derived(deviceCtx.device === null || renderedCtx.rendered === null || inProgress)
|
||||
let secondary = $derived(deviceCtx.device === null)
|
||||
let disabled = $derived(deviceCtx.device === null || renderedCtx.rendered === null || inProgress)
|
||||
let secondary = $derived(deviceCtx.device === null)
|
||||
|
||||
async function connectAndWrite() {
|
||||
if (deviceCtx.device === null || renderedCtx.rendered === null) return
|
||||
if (!(await tryOpenDevice(deviceCtx.device))) return
|
||||
async function connectAndWrite() {
|
||||
if (deviceCtx.device === null || renderedCtx.rendered === null) return
|
||||
await deviceCtx.device.open()
|
||||
|
||||
const buffer = new Uint8Array(DEVICE_HEIGHT * BYTES_IN_A_ROW)
|
||||
for (let y = 0; y < DEVICE_HEIGHT; y++) {
|
||||
for (let xStride = 0; xStride < BYTES_IN_A_ROW; xStride++) {
|
||||
let cell = 0x0
|
||||
for (let xStroll = 0; xStroll < 8; xStroll++) {
|
||||
const index = y * DEVICE_WIDTH + xStride * 8 + xStroll
|
||||
cell |= renderedCtx.rendered[index] << xStroll
|
||||
}
|
||||
buffer[y * BYTES_IN_A_ROW + xStride] = cell
|
||||
const buffer = new Uint8Array(DEVICE_HEIGHT * BYTES_IN_A_ROW)
|
||||
for (let y = 0; y < DEVICE_HEIGHT; y++) {
|
||||
for (let xStride = 0; xStride < BYTES_IN_A_ROW; xStride++) {
|
||||
let cell = 0x0
|
||||
for (let xStroll = 0; xStroll < 8; xStroll++) {
|
||||
const index = y * DEVICE_WIDTH + xStride * 8 + xStroll
|
||||
cell |= renderedCtx.rendered[index] << xStroll
|
||||
}
|
||||
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(() => {
|
||||
onprogress(inProgress)
|
||||
})
|
||||
inProgress = true
|
||||
|
||||
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>
|
||||
|
||||
<Button onclick={connectAndWrite} variant={secondary ? 'secondary' : 'default'} {disabled}>
|
||||
|
||||
@@ -8,3 +8,7 @@ export function cn(...inputs: ClassValue[]) {
|
||||
export function showNumber(n: number): string {
|
||||
return String(n).replace('-', '−').replace('Infinity', '∞').replace('NaN', '⁇')
|
||||
}
|
||||
|
||||
export function assert(expr: unknown): asserts expr {
|
||||
if (!expr) throw Error('assertion failed')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user