diff --git a/bun.lock b/bun.lock index e4b9111..d2e5fd1 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@types/bun": "latest", "@types/html-minifier": "4.0.5", "@types/inquirer": "9.0.7", + "@types/web-bluetooth": "0.0.21", "browser-detect": "0.2.28", "eruda": "3.2.3", "html-minifier": "4.0.0", @@ -131,6 +132,8 @@ "@types/uglify-js": ["@types/uglify-js@3.17.5", "", { "dependencies": { "source-map": "^0.6.1" } }, "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ=="], + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], + "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], "ace-builds": ["ace-builds@1.36.3", "", {}, "sha512-YcdwV2IIaJSfjkWAR1NEYN5IxBiXefTgwXsJ//UlaFrjXDX5hQpvPFvEePHz2ZBUfvO54RjHeRUQGX8MS5HaMQ=="], diff --git a/package.json b/package.json index 61f7831..90bed2d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/bun": "latest", "@types/html-minifier": "4.0.5", "@types/inquirer": "9.0.7", + "@types/web-bluetooth": "0.0.21", "browser-detect": "0.2.28", "eruda": "3.2.3", "html-minifier": "4.0.0", diff --git a/src/games/catprinter/index.tsx b/src/games/catprinter/index.tsx new file mode 100644 index 0000000..08fd273 --- /dev/null +++ b/src/games/catprinter/index.tsx @@ -0,0 +1,88 @@ +import { render } from "preact"; +import styles from './style.module.css'; +import { useCallback, useState } from "preact/hooks"; +import { Command, commandInt16, NOTIFY_CHARACTERISTIC, PRINT_CHARACTERISTIC, PRINTER_SERVICE } from "./printer"; + +const Printer = () => { + const [printer, setPrinter] = useState(); + const [service, setService] = useState(); + const [characteristic, setCharacteristic] = useState(); + const [notifyCharacteristic, setNotifyCharacteristic] = useState(); + const [logs, setLogs] = useState(''); + + const onConnect = useCallback(async () => { + const device = await navigator.bluetooth.requestDevice({ + filters: [{ namePrefix: "MX" }], + optionalServices: [PRINTER_SERVICE], + }); + + setPrinter(device); + + await device.gatt?.connect(); + const srv = await device.gatt?.getPrimaryService(PRINTER_SERVICE); + + setService(srv); + + const notify = await srv?.getCharacteristic(NOTIFY_CHARACTERISTIC); + notify?.startNotifications(); + notify?.addEventListener('characteristicvaluechanged', (event) => { + const value = notify?.value?.getUint8(6) ?? 0; + setLogs(l => l + `\nNotify ${value}`); + }); + + setNotifyCharacteristic(notify); + + const writeChar = await srv?.getCharacteristic(PRINT_CHARACTERISTIC); + setCharacteristic(writeChar); + }, []); + + const onDisconnect = useCallback(async () => { + printer?.gatt?.disconnect(); + setPrinter(undefined); + setService(undefined); + setCharacteristic(undefined); + }, []); + + const onFeed = useCallback(async () => { + if (characteristic) { + await commandInt16(characteristic, Command.FeedPaper, 10); + } + }, [characteristic]); + + const onRetract = useCallback(async () => { + if (characteristic) { + await commandInt16(characteristic, Command.RetractPaper, 10); + } + }, [characteristic]); + + const onPrint = useCallback(async () => { + if (characteristic) { + // TODO + } + }, [characteristic, notifyCharacteristic]); + + return
+

Printer

+ {printer + ? + : } +
+ {characteristic && <> +
+
+
+ } + {printer &&
+            Connected to {printer.id}
+            {service && `\nService: ${service.uuid}`}
+            {characteristic && `\nChar: ${characteristic.uuid}`}
+            {logs}
+        
} +
; +}; + +export default function main() { + const root = ; + + render(root, document.body); +} \ No newline at end of file diff --git a/src/games/catprinter/printer.ts b/src/games/catprinter/printer.ts new file mode 100644 index 0000000..371ae5e --- /dev/null +++ b/src/games/catprinter/printer.ts @@ -0,0 +1,140 @@ +const crc8_table = [ + 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31, + 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, + 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9, + 0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, + 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1, + 0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, + 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe, + 0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, + 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16, + 0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, + 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80, + 0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0x33, 0xa3, 0xa4, + 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8, + 0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, + 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10, + 0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, + 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f, + 0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, + 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7, + 0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, + 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef, + 0xfa, 0xfd, 0xf4, 0xf3 +] + +function crc8(data: number[] | Uint8Array): number { + let crc = 0; + for (let i = 0; i < data.length; i++) { + crc = crc8_table[(crc ^ data[i]) & 0xff]; + } + return crc; +} + +export enum Command { + RetractPaper = 0xA0, + FeedPaper = 0xA1, + DrawBitmap = 0xA2, + GetState = 0xA3, + ApplyEnergy = 0xBE, + SetEnergy = 0xAF, + SetQuality = 0xA4, + SetSpeed = 0xBD, + UpdateDevice = 0xA9, + Lattice = 0xA6, +} + +export enum State { + StateOutOfPaper = 0x01, + StateCover = 0x02, + StateOverheat = 0x04, + StateLowPower = 0x08, + StatePause = 0x10, + StateBusy = 0x80, +} + +export const PRINTER_SERVICE = "0000ae30-0000-1000-8000-00805f9b34fb"; +export const PRINT_CHARACTERISTIC = "0000ae01-0000-1000-8000-00805f9b34fb"; +export const NOTIFY_CHARACTERISTIC = "0000ae02-0000-1000-8000-00805f9b34fb"; +export const PRINTER_WIDTH = 384; +const MTU = PRINTER_WIDTH / 8; + +function formatMessage(command: Command, data: number[] | Uint8Array): Uint8Array { + const len = data.length; + + const message = new Uint8Array(len + 8); + message[0] = 0x51; + message[1] = 0x78; + message[2] = command; + message[3] = 0x00; + message[4] = len & 0xFF; + message[5] = (len >> 8) & 0xFF; + message.set(data, 6); + message[6 + len] = crc8(data); + message[7 + len] = 0xFF; + + return message; +} + +function formatInt8(command: Command, data: number): Uint8Array { + return formatMessage(command, [data & 0xFF]); +} + +function formatInt16(command: Command, data: number): Uint8Array { + const udata = new Uint8Array(2); + udata[0] = data & 0xFF; + udata[1] = (data >> 8) & 0xFF; + return formatMessage(command, udata); +} + +export async function command(char: BluetoothRemoteGATTCharacteristic, command: Command, data: number[] | Uint8Array) { + await char.writeValue(formatMessage(command, data)); +} + +export async function commandInt8(char: BluetoothRemoteGATTCharacteristic, command: Command, data: number) { + await char.writeValue(formatInt8(command, data)); +} + +export async function commandInt16(char: BluetoothRemoteGATTCharacteristic, command: Command, data: number) { + await char.writeValue(formatInt16(command, data)); +} + +async function startPrint(char: BluetoothRemoteGATTCharacteristic, energy = 0xFF00) { + await commandInt16(char, Command.SetEnergy, 0xFF00); + await commandInt8(char, Command.SetQuality, 50); + await commandInt16(char, Command.SetSpeed, 32); + await commandInt8(char, Command.ApplyEnergy, 0); + await commandInt8(char, Command.UpdateDevice, 0); + await command(char, Command.Lattice, [0xaa, 0x55, 0x17, 0x38, 0x44, 0x5f, 0x5f, 0x5f, 0x44, 0x38, 0x2c]); +}; + +async function endPrint(char: BluetoothRemoteGATTCharacteristic) { + await command(char, Command.Lattice, [0xaa, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17]); + await commandInt8(char, Command.GetState, 0); +} + +export async function printBitmap(char: BluetoothRemoteGATTCharacteristic, bitmap: number[] | Uint8Array, energy = 0xFF00) { + await startPrint(char, energy); + for (let i = 0; i < bitmap.length; i += MTU) { + const chunk = bitmap.slice(i, i + MTU); + await command(char, Command.DrawBitmap, chunk); + } + await endPrint(char); + + const notify = await char.service.getCharacteristic(NOTIFY_CHARACTERISTIC); + await notify.startNotifications(); + + const { promise, resolve } = Promise.withResolvers(); + + const handler = () => { + const value = notify.value?.getUint8(6) ?? 0; + if ((value & State.StateBusy) === 0) { + resolve(); + } + } + notify.addEventListener('characteristicvaluechanged', handler); + const interval = setInterval(() => commandInt8(char, Command.GetState, 0), 1000); + await promise; + clearInterval(interval); + notify.removeEventListener('characteristicvaluechanged', handler); +} \ No newline at end of file diff --git a/src/games/catprinter/style.module.css b/src/games/catprinter/style.module.css new file mode 100644 index 0000000..5c752b4 --- /dev/null +++ b/src/games/catprinter/style.module.css @@ -0,0 +1,4 @@ +.title { + font-size: 24px; + color: #333; +} \ No newline at end of file