mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 18:02:42 +00:00
add chord serialization, vitest
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
path: build
|
path: build
|
||||||
deploy:
|
deploy:
|
||||||
name: 🚀 Deploy
|
name: 🚀 Deploy
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && contains(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
977
package-lock.json
generated
977
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"test": "vitest run --coverage",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
@@ -29,6 +30,8 @@
|
|||||||
"flexsearch": "^0.7.31",
|
"flexsearch": "^0.7.31",
|
||||||
"@sveltejs/adapter-static": "^2.0.2",
|
"@sveltejs/adapter-static": "^2.0.2",
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"@material/material-color-utilities": "^0.2.7",
|
||||||
"fontkit": "^2.0.2",
|
"fontkit": "^2.0.2",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
"vitest": "^0.33.0",
|
||||||
"vite": "^4.3.6",
|
"vite": "^4.3.6",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.16.4",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
|
|||||||
8
src/lib/serial/chord.spec.ts
Normal file
8
src/lib/serial/chord.spec.ts
Normal 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
152
src/lib/serial/chord.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {CharaDevice} from "$lib/serial/device"
|
import {CharaDevice} from "$lib/serial/device"
|
||||||
|
import type {Chord} from "$lib/serial/chord"
|
||||||
|
|
||||||
export const serialPort = writable<CharaDevice>()
|
export const serialPort = writable<CharaDevice>()
|
||||||
|
|
||||||
@@ -10,11 +11,6 @@ export interface SerialLogEntry {
|
|||||||
|
|
||||||
export const serialLog = writable<SerialLogEntry[]>([])
|
export const serialLog = writable<SerialLogEntry[]>([])
|
||||||
|
|
||||||
export interface Chord {
|
|
||||||
actions: number[]
|
|
||||||
phrase: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chords = writable<Chord[]>([])
|
export const chords = writable<Chord[]>([])
|
||||||
|
|
||||||
export type CharaLayout = [number[], number[], number[]]
|
export type CharaLayout = [number[], number[], number[]]
|
||||||
@@ -43,7 +39,6 @@ export async function initSerial() {
|
|||||||
for (let i = 0; i < chordCount; i++) {
|
for (let i = 0; i < chordCount; i++) {
|
||||||
chordInfo.push(await device.getChord(i))
|
chordInfo.push(await device.getChord(i))
|
||||||
}
|
}
|
||||||
chordInfo.sort(({phrase: a}, {phrase: b}) => a.localeCompare(b))
|
|
||||||
chords.set(chordInfo)
|
chords.set(chordInfo)
|
||||||
syncing.set(false)
|
syncing.set(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
|
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
|
||||||
import {serialLog} from "$lib/serial/connection"
|
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
|
export const VENDOR_ID = 0x239a
|
||||||
|
|
||||||
@@ -130,26 +131,7 @@ export class CharaDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChord(index: number): Promise<Chord> {
|
async getChord(index: number): Promise<Chord> {
|
||||||
const chord = await this.send(`CML C1 ${index}`)
|
return chordFromCommandCompatible(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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLayoutKey(layer: number, id: number) {
|
async getLayoutKey(layer: number, id: number) {
|
||||||
|
|||||||
10
vitest.config.ts
Normal file
10
vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {defineConfig} from "vitest/config"
|
||||||
|
import {svelte} from "@sveltejs/vite-plugin-svelte"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte({hot: !process.env.VITEST})],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user