add chord serialization, vitest

This commit is contained in:
2023-07-08 13:29:57 +02:00
parent dbd02e9dec
commit 44f82f2b4e
8 changed files with 1150 additions and 34 deletions

View File

@@ -0,0 +1,8 @@
import {describe, it, expect} from "vitest"
import {serializeActions} from "$lib/serial/chord"
describe("chords", function () {
it("should serialize actions", function () {
expect(serializeActions([67, 2])).toBe(0xcc200000000000000000000000000n)
})
})

152
src/lib/serial/chord.ts Normal file
View File

@@ -0,0 +1,152 @@
export interface Chord {
actions: number[]
phrase: number[]
}
/**
* Turns a chord into a serial-command-compatible string
*
* @example "000CC200000000000000000000000000 7468726565"
*/
export function chordAsCommandCompatible(chord: Chord): string {
return `${serializeActions(chord.actions).toString(16).padStart(32)} ${chord.phrase.map(it =>
it.toString(16),
)}`
}
/**
* Turns a command response into a chord
*
* @see {chordAsCommandCompatible}
*/
export function chordFromCommandCompatible(command: string): Chord {
const [actions, phrase] = command.split(" ")
return {
actions: deserializeActions(BigInt(`0x${actions}`)),
phrase: Array.from({length: phrase.length / 2}).map((_, i) =>
Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16),
),
}
}
/**
* Binary serialization of actions
*
* Actions are represented as 10-bit codes, for a maximum of 12 actions
*/
export function serializeActions(actions: number[]): bigint {
let native = 0n
for (let i = 0; i < actions.length; i++) {
native |= BigInt(actions[i] & 0x3ff) << BigInt((11 - i) * 10)
}
return native
}
/**
* @see {serializeActions}
*/
export function deserializeActions(native: bigint): number[] {
const actions = []
for (let i = 0; i < 12; i++) {
const action = Number(native & 0x3ffn)
if (action !== 0) {
actions.push(action)
}
native >>= 10n
}
return actions
}
const CHL_VERSION = 1
const CHL_MAGIC = "CHL"
/**
* Binary serialization of the chord library
*
* Layout is as follows:
* ```rs
* struct Chords {
* magic: "CHL"
* version: u8
* chordCount: u32
* chords: chord[]
* }
*
* struct Chord {
* id: u32
* phrase: u128
* actionCount: u16
* actions: u8[]
* }
* ```
* Serialized as little endian.
*
* @param chords
*/
export function chordsToFile(chords: Chord[]): ArrayBuffer {
const actionsTotalCount = chords.reduce((size, chord) => size + chord.actions.length, 0)
const buffer = new ArrayBuffer(4 + 4 + chords.length * (4 + 16 + 2) + actionsTotalCount)
const view = new DataView(buffer)
let byteOffset = 0
for (const byte of CHL_MAGIC) {
view.setUint8(byteOffset++, byte.codePointAt(0)!)
}
view.setUint8(byteOffset++, CHL_VERSION)
view.setUint32(byteOffset, chords.length, true)
byteOffset += 4
for (const chord of chords) {
const actions = serializeActions(chord.actions)
view.setBigUint64(byteOffset, actions << 64n, true)
byteOffset += 8
view.setBigUint64(byteOffset, actions & 0xffff_ffff_ffff_ffffn, true)
byteOffset += 8
view.setUint16(byteOffset, chord.phrase.length, true)
byteOffset += 2
for (const action of chord.phrase) {
view.setUint8(byteOffset++, action)
}
}
return buffer
}
/**
* @see {chordsToFile}
*/
export function chordsFromFile(buffer: ArrayBuffer): Chord[] {
const view = new DataView(buffer)
let byteOffset = 0
let magic = ""
for (let i = 0; i < CHL_MAGIC.length; i++) {
magic += view.getUint8(byteOffset++)
}
if (magic !== CHL_MAGIC) throw new Error("Not a .chl file")
if (view.getUint8(byteOffset++) !== CHL_VERSION) throw Error("Invalid .chl [version]")
const chords: Chord[] = Array.from({length: view.getUint32(byteOffset, true)})
byteOffset += 4
for (let i = 0; i < chords.length; i++) {
let actions = view.getBigUint64(byteOffset, true) >> 64n
byteOffset += 8
actions |= view.getBigUint64(byteOffset, true)
byteOffset += 8
const phrase: number[] = Array.from({length: view.getUint16(byteOffset, true)})
byteOffset += 2
for (let i = 0; i < phrase.length; i++) {
phrase[i] = view.getUint8(byteOffset++)
}
chords[i] = {
actions: deserializeActions(actions),
phrase,
}
}
return chords
}

View File

@@ -1,5 +1,6 @@
import {writable} from "svelte/store"
import {CharaDevice} from "$lib/serial/device"
import type {Chord} from "$lib/serial/chord"
export const serialPort = writable<CharaDevice>()
@@ -10,11 +11,6 @@ export interface SerialLogEntry {
export const serialLog = writable<SerialLogEntry[]>([])
export interface Chord {
actions: number[]
phrase: string
}
export const chords = writable<Chord[]>([])
export type CharaLayout = [number[], number[], number[]]
@@ -43,7 +39,6 @@ export async function initSerial() {
for (let i = 0; i < chordCount; i++) {
chordInfo.push(await device.getChord(i))
}
chordInfo.sort(({phrase: a}, {phrase: b}) => a.localeCompare(b))
chords.set(chordInfo)
syncing.set(false)
}

View File

@@ -1,6 +1,7 @@
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
import {serialLog} from "$lib/serial/connection"
import type {Chord} from "$lib/serial/connection"
import type {Chord} from "$lib/serial/chord"
import {chordFromCommandCompatible} from "$lib/serial/chord"
export const VENDOR_ID = 0x239a
@@ -130,26 +131,7 @@ export class CharaDevice {
}
async getChord(index: number): Promise<Chord> {
const chord = await this.send(`CML C1 ${index}`)
const [keys, rawPhrase] = chord.split(" ")
let phrase = []
for (let i = 0; i < rawPhrase.length; i += 2) {
phrase.push(Number.parseInt(rawPhrase.substring(i, i + 2), 16))
}
let bigKeys = BigInt(`0x${keys}`)
let actions = []
for (let i = 0; i < 12; i++) {
const action = Number(bigKeys & BigInt(0b1111111111))
if (action !== 0) {
actions.push(action)
}
bigKeys >>= BigInt(10)
}
return {
actions,
phrase: String.fromCodePoint(...phrase),
}
return chordFromCommandCompatible(await this.send(`CML C1 ${index}`))
}
async getLayoutKey(layer: number, id: number) {