From dad965933eb9cfb1049be2ce0908b1a8ed153ebf Mon Sep 17 00:00:00 2001 From: MultiMote <contact@mmote.ru> Date: Sun, 17 Nov 2024 10:32:41 +0300 Subject: [PATCH] Capacitor BLE client implementation, instantiateClient function --- package.json | 2 + src/client/abstract_client.ts | 287 ++++++++++++++++++++++++++++ src/client/bluetooth_impl.ts | 5 +- src/client/capacitor_ble_impl.ts | 162 ++++++++++++++++ src/client/index.ts | 312 +++---------------------------- src/client/serial_impl.ts | 5 +- src/utils.ts | 28 +++ yarn.lock | 19 ++ 8 files changed, 528 insertions(+), 292 deletions(-) create mode 100644 src/client/abstract_client.ts create mode 100644 src/client/capacitor_ble_impl.ts diff --git a/package.json b/package.json index d92d131..1f28be0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "typescript": "^5.4.5" }, "dependencies": { + "@capacitor-community/bluetooth-le": "^6.0.2", + "@capacitor/core": "^6.0.0", "async-mutex": "^0.5.0", "eventemitter3": "^5.0.1" }, diff --git a/src/client/abstract_client.ts b/src/client/abstract_client.ts new file mode 100644 index 0000000..efa748f --- /dev/null +++ b/src/client/abstract_client.ts @@ -0,0 +1,287 @@ +import { EventEmitter } from "eventemitter3"; +import { Mutex } from "async-mutex"; +import { + Abstraction, + AutoShutdownTime, + BatteryChargeLevel, + ConnectResult, + LabelType, + NiimbotPacket, + PrinterErrorCode, + PrintError, + ResponseCommandId, +} from "../packets"; +import { PrinterModelMeta, getPrinterMetaById } from "../printer_models"; +import { + ClientEventMap, + PacketSentEvent, + PrinterInfoFetchedEvent, + HeartbeatEvent, + HeartbeatFailedEvent, + PacketReceivedEvent, +} from "../events"; +import { findPrintTask, PrintTaskName } from "../print_tasks"; +import { Utils, Validators } from "../utils"; + +/** + * Represents the connection result information. + * + * @category Client + */ +export type ConnectionInfo = { + deviceName?: string; + result: ConnectResult; +}; + +/** + * Interface representing printer information. + * + * @category Client + */ +export interface PrinterInfo { + connectResult?: ConnectResult; + protocolVersion?: number; + modelId?: number; + serial?: string; + mac?: string; + charge?: BatteryChargeLevel; + autoShutdownTime?: AutoShutdownTime; + labelType?: LabelType; + softwareVersion?: string; + hardwareVersion?: string; +} + +/** + * Abstract class representing a client with common functionality for interacting with a printer. + * Hardware interface must be defined after extending this class. + * + * @category Client + */ +export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> { + public readonly abstraction: Abstraction; + protected info: PrinterInfo = {}; + private heartbeatTimer?: NodeJS.Timeout; + private heartbeatFails: number = 0; + private heartbeatIntervalMs: number = 2_000; + protected mutex: Mutex = new Mutex(); + protected debug: boolean = false; + + /** @see https://github.com/MultiMote/niimblue/issues/5 */ + protected packetIntervalMs: number = 10; + + constructor() { + super(); + this.abstraction = new Abstraction(this); + this.on("connect", () => this.startHeartbeat()); + this.on("disconnect", () => this.stopHeartbeat()); + } + + /** + * Connect to printer port. + **/ + public abstract connect(): Promise<ConnectionInfo>; + + /** + * Disconnect from printer port. + **/ + public abstract disconnect(): Promise<void>; + + /** + * Check if the client is connected. + */ + public abstract isConnected(): boolean; + + /** + * Send packet and wait for response for {@link timeoutMs} milliseconds. + * + * If {@link NiimbotPacket.validResponseIds() validResponseIds} is defined, it will wait for packet with this command id. + * + * @throws {@link PrintError} when {@link ResponseCommandId.In_PrintError} or {@link ResponseCommandId.In_NotSupported} received. + * + * @returns {NiimbotPacket} Printer response object. + */ + public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs: number = 1000): Promise<NiimbotPacket> { + return this.mutex.runExclusive(async () => { + await this.sendPacket(packet, true); + + if (packet.oneWay) { + return new NiimbotPacket(ResponseCommandId.Invalid, []); // or undefined is better? + } + + return new Promise((resolve, reject) => { + let timeout: NodeJS.Timeout | undefined = undefined; + + const listener = (evt: PacketReceivedEvent) => { + const pktIn = evt.packet; + const cmdIn = pktIn.command as ResponseCommandId; + + if ( + packet.validResponseIds.length === 0 || + packet.validResponseIds.includes(cmdIn) || + [ResponseCommandId.In_PrintError, ResponseCommandId.In_NotSupported].includes(cmdIn) + ) { + clearTimeout(timeout); + this.off("packetreceived", listener); + + if (cmdIn === ResponseCommandId.In_PrintError) { + Validators.u8ArrayLengthEquals(pktIn.data, 1); + const errorName = PrinterErrorCode[pktIn.data[0]] ?? "unknown"; + reject(new PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0])); + } else if (cmdIn === ResponseCommandId.In_NotSupported) { + reject(new PrintError("Feature not supported", 0)); + } else { + resolve(pktIn); + } + } + }; + + timeout = setTimeout(() => { + this.off("packetreceived", listener); + reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`)); + }, timeoutMs ?? 1000); + + this.on("packetreceived", listener); + }); + }); + } + + /** + * Send raw bytes to the printer port. + * + * @param data Bytes to send. + * @param force Ignore mutex lock. It used internally and you should avoid using it. + */ + public abstract sendRaw(data: Uint8Array, force?: boolean): Promise<void>; + + public async sendPacket(packet: NiimbotPacket, force?: boolean) { + await this.sendRaw(packet.toBytes(), force); + this.emit("packetsent", new PacketSentEvent(packet)); + } + + /** + * Send "connect" packet and fetch the protocol version. + **/ + protected async initialNegotiate(): Promise<void> { + const cfg = this.info; + cfg.connectResult = await this.abstraction.connectResult(); + cfg.protocolVersion = 0; + + if (cfg.connectResult === ConnectResult.ConnectedNew) { + cfg.protocolVersion = 1; + } else if (cfg.connectResult === ConnectResult.ConnectedV3) { + const statusData = await this.abstraction.getPrinterStatusData(); + cfg.protocolVersion = statusData.protocolVersion; + } + } + + /** + * Fetches printer information and stores it. + */ + public async fetchPrinterInfo(): Promise<PrinterInfo> { + this.info.modelId = await this.abstraction.getPrinterModel(); + + this.info.serial = (await this.abstraction.getPrinterSerialNumber().catch(console.error)) ?? undefined; + this.info.mac = (await this.abstraction.getPrinterBluetoothMacAddress().catch(console.error)) ?? undefined; + this.info.charge = (await this.abstraction.getBatteryChargeLevel().catch(console.error)) ?? undefined; + this.info.autoShutdownTime = (await this.abstraction.getAutoShutDownTime().catch(console.error)) ?? undefined; + this.info.labelType = (await this.abstraction.getLabelType().catch(console.error)) ?? undefined; + this.info.hardwareVersion = (await this.abstraction.getHardwareVersion().catch(console.error)) ?? undefined; + this.info.softwareVersion = (await this.abstraction.getSoftwareVersion().catch(console.error)) ?? undefined; + + this.emit("printerinfofetched", new PrinterInfoFetchedEvent(this.info)); + return this.info; + } + + /** + * Get the stored information about the printer. + */ + public getPrinterInfo(): PrinterInfo { + return this.info; + } + + /** + * Set interval for {@link startHeartbeat}. + * + * @param intervalMs Heartbeat interval, default is 1000ms + */ + public setHeartbeatInterval(intervalMs: number): void { + this.heartbeatIntervalMs = intervalMs; + } + + /** + * Starts the heartbeat timer, "heartbeat" is emitted after packet received. + * + * If you need to change interval, call {@link setHeartbeatInterval} before. + */ + public startHeartbeat(): void { + this.heartbeatFails = 0; + + this.stopHeartbeat(); + + this.heartbeatTimer = setInterval(() => { + this.abstraction + .heartbeat() + .then((data) => { + this.heartbeatFails = 0; + this.emit("heartbeat", new HeartbeatEvent(data)); + }) + .catch((e) => { + console.error(e); + this.heartbeatFails++; + this.emit("heartbeatfailed", new HeartbeatFailedEvent(this.heartbeatFails)); + }); + }, this.heartbeatIntervalMs); + } + + /** + * Stops the heartbeat by clearing the interval timer. + */ + public stopHeartbeat(): void { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; + } + + /** + * Checks if the heartbeat timer has been started. + */ + public isHeartbeatStarted(): boolean { + return this.heartbeatTimer === undefined; + } + + /** + * Get printer capabilities based on the printer model. Model library is hardcoded. + **/ + public getModelMetadata(): PrinterModelMeta | undefined { + if (this.info.modelId === undefined) { + return undefined; + } + return getPrinterMetaById(this.info.modelId); + } + + /** + * Determine print task version if any. + **/ + public getPrintTaskType(): PrintTaskName | undefined { + const meta = this.getModelMetadata(); + + if (meta === undefined) { + return undefined; + } + + return findPrintTask(meta.model, this.getPrinterInfo().protocolVersion); + } + + /** + * Set the interval between packets in milliseconds. + */ + public setPacketInterval(milliseconds: number) { + this.packetIntervalMs = milliseconds; + } + + /** + * Enable some debug information logging. + */ + public setDebug(value: boolean) { + this.debug = value; + } +} diff --git a/src/client/bluetooth_impl.ts b/src/client/bluetooth_impl.ts index 0f411a0..a32f37f 100644 --- a/src/client/bluetooth_impl.ts +++ b/src/client/bluetooth_impl.ts @@ -65,10 +65,13 @@ export class NiimbotBluetoothClient extends NiimbotAbstractClient { channel.addEventListener("characteristicvaluechanged", (event: Event) => { const target = event.target as BluetoothRemoteGATTCharacteristic; + const data = new Uint8Array(target.value!.buffer); - const packet = NiimbotPacket.fromBytes(data); this.emit("rawpacketreceived", new RawPacketReceivedEvent(data)); + + const packet = NiimbotPacket.fromBytes(data); + this.emit("packetreceived", new PacketReceivedEvent(packet)); if (!(packet.command in ResponseCommandId)) { diff --git a/src/client/capacitor_ble_impl.ts b/src/client/capacitor_ble_impl.ts new file mode 100644 index 0000000..03d15a3 --- /dev/null +++ b/src/client/capacitor_ble_impl.ts @@ -0,0 +1,162 @@ +import { + ConnectEvent, + DisconnectEvent, + PacketReceivedEvent, + RawPacketReceivedEvent, + RawPacketSentEvent, +} from "../events"; +import { ConnectionInfo, NiimbotAbstractClient } from "."; +import { NiimbotPacket } from "../packets/packet"; +import { ConnectResult } from "../packets"; +import { Utils } from "../utils"; +import { BleCharacteristic, BleClient, BleDevice, BleService } from "@capacitor-community/bluetooth-le"; + +/** + * Uses [@capacitor-community/bluetooth-le](https://github.com/capacitor-community/bluetooth-le) + * + * @category Client + */ +export class NiimbotCapacitorBleClient extends NiimbotAbstractClient { + private deviceId?: string; + private serviceUUID?: string; + private characteristicUUID?: string; + private packetBuf = new Uint8Array(); + + public async connect(): Promise<ConnectionInfo> { + await this.disconnect(); + + await BleClient.initialize({ androidNeverForLocation: true }); + + const bluetoothEnabled = await BleClient.isEnabled(); + + if (!bluetoothEnabled) { + throw new Error("Bluetooth is not enabled"); + } + + const device: BleDevice = await BleClient.requestDevice(); + + await BleClient.connect(device.deviceId, () => this.onBleDisconnect()); + + const { service, characteristic } = await this.findSuitableCharacteristic(device.deviceId).finally(() => + this.onBleDisconnect() + ); + + this.deviceId = device.deviceId; + this.serviceUUID = service; + this.characteristicUUID = characteristic; + + if (this.debug) { + console.log("Suitable channel found:", { service, characteristic }); + } + + await BleClient.startNotifications(this.deviceId, this.serviceUUID, this.characteristicUUID, (value: DataView) => { + this.onBlePacketReceived(value); + }); + + try { + await this.initialNegotiate(); + await this.fetchPrinterInfo(); + } catch (e) { + console.error("Unable to fetch printer info."); + console.error(e); + } + + const result: ConnectionInfo = { + deviceName: device.name, + result: this.info.connectResult ?? ConnectResult.FirmwareErrors, + }; + + this.emit("connect", new ConnectEvent(result)); + + return result; + } + + private async findSuitableCharacteristic(devId: string): Promise<{ service: string; characteristic: string }> { + const services: BleService[] = await BleClient.getServices(devId); + + for (const service of services) { + if (service.uuid.length < 5) { + continue; + } + + const characteristics: BleCharacteristic[] = service.characteristics; + + for (const ch of characteristics) { + if (ch.properties.notify && ch.properties.writeWithoutResponse) { + return { + characteristic: ch.uuid, + service: service.uuid, + }; + } + } + } + throw new Error("Unable to find suitable channel characteristic"); + } + + private onBlePacketReceived(dv: DataView) { + if (dv.byteLength === 0) { + return; + } + + this.packetBuf = Utils.u8ArrayAppend(this.packetBuf, new Uint8Array(dv.buffer)); + + try { + const packets: NiimbotPacket[] = NiimbotPacket.fromBytesMultiPacket(this.packetBuf); + + if (packets.length > 0) { + this.emit("rawpacketreceived", new RawPacketReceivedEvent(this.packetBuf)); + + packets.forEach((p) => { + this.emit("packetreceived", new PacketReceivedEvent(p)); + }); + + this.packetBuf = new Uint8Array(); + } + } catch (_e) { + if (this.debug) { + console.info(`Incomplete packet, ignoring:${Utils.bufToHex(this.packetBuf)}`); + } + } + } + + private onBleDisconnect() { + this.deviceId = undefined; + this.serviceUUID = undefined; + this.characteristicUUID = undefined; + this.info = {}; + this.emit("disconnect", new DisconnectEvent()); + } + + public isConnected(): boolean { + return this.deviceId !== undefined; + } + + public async disconnect() { + this.stopHeartbeat(); + if (this.deviceId !== undefined) { + await BleClient.stopNotifications(this.deviceId, this.serviceUUID!, this.characteristicUUID!); + await BleClient.disconnect(this.deviceId); + } + this.deviceId = undefined; + this.info = {}; + } + + public async sendRaw(data: Uint8Array, force?: boolean) { + const send = async () => { + if (!this.isConnected()) { + throw new Error("Channel is closed"); + } + await Utils.sleep(this.packetIntervalMs); + + const dw = new DataView(data.buffer, data.byteOffset, data.byteLength); + await BleClient.write(this.deviceId!, this.serviceUUID!, this.characteristicUUID!, dw); + + this.emit("rawpacketsent", new RawPacketSentEvent(data)); + }; + if (force) { + await send(); + } else { + await this.mutex.runExclusive(send); + } + } +} diff --git a/src/client/index.ts b/src/client/index.ts index 57b2f8a..13910f1 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,290 +1,28 @@ -import { EventEmitter } from "eventemitter3"; -import { Mutex } from "async-mutex"; -import { - Abstraction, - AutoShutdownTime, - BatteryChargeLevel, - ConnectResult, - LabelType, - NiimbotPacket, - PrinterErrorCode, - PrintError, - ResponseCommandId, -} from "../packets"; -import { PrinterModelMeta, getPrinterMetaById } from "../printer_models"; -import { - ClientEventMap, - PacketSentEvent, - PrinterInfoFetchedEvent, - HeartbeatEvent, - HeartbeatFailedEvent, - PacketReceivedEvent, -} from "../events"; -import { findPrintTask, PrintTaskName } from "../print_tasks"; -import { Utils, Validators } from "../utils"; +import { NiimbotAbstractClient, ConnectionInfo, PrinterInfo } from "./abstract_client"; +import { NiimbotBluetoothClient } from "./bluetooth_impl"; +import { NiimbotCapacitorBleClient } from "./capacitor_ble_impl"; +import { NiimbotSerialClient } from "./serial_impl"; -/** - * Represents the connection result information. - * - * @category Client - */ -export type ConnectionInfo = { - deviceName?: string; - result: ConnectResult; +/** Client type for {@link instantiateClient} */ +export type NiimbotClientType = "bluetooth" | "serial" | "capacitor-ble"; + +/** Create new client instance */ +export const instantiateClient = (t: NiimbotClientType): NiimbotAbstractClient => { + if (t === "bluetooth") { + return new NiimbotBluetoothClient(); + } else if (t === "capacitor-ble") { + return new NiimbotCapacitorBleClient(); + } else if (t === "serial") { + return new NiimbotSerialClient(); + } + throw new Error("Invalid client type"); }; -/** - * Interface representing printer information. - * - * @category Client - */ -export interface PrinterInfo { - connectResult?: ConnectResult; - protocolVersion?: number; - modelId?: number; - serial?: string; - mac?: string; - charge?: BatteryChargeLevel; - autoShutdownTime?: AutoShutdownTime; - labelType?: LabelType; - softwareVersion?: string; - hardwareVersion?: string; -} - -/** - * Abstract class representing a client with common functionality for interacting with a printer. - * Hardware interface must be defined after extending this class. - * - * @category Client - */ -export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> { - public readonly abstraction: Abstraction; - protected info: PrinterInfo = {}; - private heartbeatTimer?: NodeJS.Timeout; - private heartbeatFails: number = 0; - private heartbeatIntervalMs: number = 2_000; - protected mutex: Mutex = new Mutex(); - protected debug: boolean = false; - - /** @see https://github.com/MultiMote/niimblue/issues/5 */ - protected packetIntervalMs: number = 10; - - constructor() { - super(); - this.abstraction = new Abstraction(this); - this.on("connect", () => this.startHeartbeat()); - this.on("disconnect", () => this.stopHeartbeat()); - } - - /** - * Connect to printer port. - **/ - public abstract connect(): Promise<ConnectionInfo>; - - /** - * Disconnect from printer port. - **/ - public abstract disconnect(): Promise<void>; - - /** - * Check if the client is connected. - */ - public abstract isConnected(): boolean; - - /** - * Send packet and wait for response for {@link timeoutMs} milliseconds. - * - * If {@link NiimbotPacket.validResponseIds() validResponseIds} is defined, it will wait for packet with this command id. - * - * @throws {@link PrintError} when {@link ResponseCommandId.In_PrintError} or {@link ResponseCommandId.In_NotSupported} received. - * - * @returns {NiimbotPacket} Printer response object. - */ - public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs: number = 1000): Promise<NiimbotPacket> { - return this.mutex.runExclusive(async () => { - await this.sendPacket(packet, true); - - if (packet.oneWay) { - return new NiimbotPacket(ResponseCommandId.Invalid, []); // or undefined is better? - } - - return new Promise((resolve, reject) => { - let timeout: NodeJS.Timeout | undefined = undefined; - - const listener = (evt: PacketReceivedEvent) => { - const pktIn = evt.packet; - const cmdIn = pktIn.command as ResponseCommandId; - - if ( - packet.validResponseIds.length === 0 || - packet.validResponseIds.includes(cmdIn) || - [ResponseCommandId.In_PrintError, ResponseCommandId.In_NotSupported].includes(cmdIn) - ) { - clearTimeout(timeout); - this.off("packetreceived", listener); - - if (cmdIn === ResponseCommandId.In_PrintError) { - Validators.u8ArrayLengthEquals(pktIn.data, 1); - const errorName = PrinterErrorCode[pktIn.data[0]] ?? "unknown"; - reject(new PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0])); - } else if (cmdIn === ResponseCommandId.In_NotSupported) { - reject(new PrintError("Feature not supported", 0)); - } else { - resolve(pktIn); - } - } - }; - - timeout = setTimeout(() => { - this.off("packetreceived", listener); - reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`)); - }, timeoutMs ?? 1000); - - this.on("packetreceived", listener); - }); - }); - } - - /** - * Send raw bytes to the printer port. - * - * @param data Bytes to send. - * @param force Ignore mutex lock. It used internally and you should avoid using it. - */ - public abstract sendRaw(data: Uint8Array, force?: boolean): Promise<void>; - - public async sendPacket(packet: NiimbotPacket, force?: boolean) { - await this.sendRaw(packet.toBytes(), force); - this.emit("packetsent", new PacketSentEvent(packet)); - } - - /** - * Send "connect" packet and fetch the protocol version. - **/ - protected async initialNegotiate(): Promise<void> { - const cfg = this.info; - cfg.connectResult = await this.abstraction.connectResult(); - cfg.protocolVersion = 0; - - if (cfg.connectResult === ConnectResult.ConnectedNew) { - cfg.protocolVersion = 1; - } else if (cfg.connectResult === ConnectResult.ConnectedV3) { - const statusData = await this.abstraction.getPrinterStatusData(); - cfg.protocolVersion = statusData.protocolVersion; - } - } - - /** - * Fetches printer information and stores it. - */ - public async fetchPrinterInfo(): Promise<PrinterInfo> { - this.info.modelId = await this.abstraction.getPrinterModel(); - - this.info.serial = (await this.abstraction.getPrinterSerialNumber().catch(console.error)) ?? undefined; - this.info.mac = (await this.abstraction.getPrinterBluetoothMacAddress().catch(console.error)) ?? undefined; - this.info.charge = (await this.abstraction.getBatteryChargeLevel().catch(console.error)) ?? undefined; - this.info.autoShutdownTime = (await this.abstraction.getAutoShutDownTime().catch(console.error)) ?? undefined; - this.info.labelType = (await this.abstraction.getLabelType().catch(console.error)) ?? undefined; - this.info.hardwareVersion = (await this.abstraction.getHardwareVersion().catch(console.error)) ?? undefined; - this.info.softwareVersion = (await this.abstraction.getSoftwareVersion().catch(console.error)) ?? undefined; - - this.emit("printerinfofetched", new PrinterInfoFetchedEvent(this.info)); - return this.info; - } - - /** - * Get the stored information about the printer. - */ - public getPrinterInfo(): PrinterInfo { - return this.info; - } - - /** - * Set interval for {@link startHeartbeat}. - * - * @param intervalMs Heartbeat interval, default is 1000ms - */ - public setHeartbeatInterval(intervalMs: number): void { - this.heartbeatIntervalMs = intervalMs; - } - - /** - * Starts the heartbeat timer, "heartbeat" is emitted after packet received. - * - * If you need to change interval, call {@link setHeartbeatInterval} before. - */ - public startHeartbeat(): void { - this.heartbeatFails = 0; - - this.stopHeartbeat(); - - this.heartbeatTimer = setInterval(() => { - this.abstraction - .heartbeat() - .then((data) => { - this.heartbeatFails = 0; - this.emit("heartbeat", new HeartbeatEvent(data)); - }) - .catch((e) => { - console.error(e); - this.heartbeatFails++; - this.emit("heartbeatfailed", new HeartbeatFailedEvent(this.heartbeatFails)); - }); - }, this.heartbeatIntervalMs); - } - - /** - * Stops the heartbeat by clearing the interval timer. - */ - public stopHeartbeat(): void { - clearInterval(this.heartbeatTimer); - this.heartbeatTimer = undefined; - } - - /** - * Checks if the heartbeat timer has been started. - */ - public isHeartbeatStarted(): boolean { - return this.heartbeatTimer === undefined; - } - - /** - * Get printer capabilities based on the printer model. Model library is hardcoded. - **/ - public getModelMetadata(): PrinterModelMeta | undefined { - if (this.info.modelId === undefined) { - return undefined; - } - return getPrinterMetaById(this.info.modelId); - } - - /** - * Determine print task version if any. - **/ - public getPrintTaskType(): PrintTaskName | undefined { - const meta = this.getModelMetadata(); - - if (meta === undefined) { - return undefined; - } - - return findPrintTask(meta.model, this.getPrinterInfo().protocolVersion); - } - - /** - * Set the interval between packets in milliseconds. - */ - public setPacketInterval(milliseconds: number) { - this.packetIntervalMs = milliseconds; - } - - /** - * Enable some debug information logging. - */ - public setDebug(value: boolean) { - this.debug = value; - } -} - -export * from "./bluetooth_impl"; -export * from "./serial_impl"; +export { + NiimbotAbstractClient, + ConnectionInfo, + PrinterInfo, + NiimbotBluetoothClient, + NiimbotCapacitorBleClient, + NiimbotSerialClient, +}; diff --git a/src/client/serial_impl.ts b/src/client/serial_impl.ts index 192c39f..812e042 100644 --- a/src/client/serial_impl.ts +++ b/src/client/serial_impl.ts @@ -73,10 +73,7 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { console.info(`<< serial chunk ${Utils.bufToHex(result.value)}`); } - const newBuf = new Uint8Array(buf.length + result.value.length); - newBuf.set(buf, 0); - newBuf.set(result.value, buf.length); - buf = newBuf; + buf = Utils.u8ArrayAppend(buf, result.value); } if (result.done) { diff --git a/src/utils.ts b/src/utils.ts index 5b4962c..5945619 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,11 @@ +import { Capacitor } from "@capacitor/core"; + +export interface AvailableTransports { + webSerial: boolean; + webBluetooth: boolean; + capacitorBle: boolean; +} + /** * Utility class for various common operations. * @category Helpers @@ -126,6 +134,13 @@ export class Utils { return a.length === b.length && a.every((el, i) => el === b[i]); } + public static u8ArrayAppend(src: Uint8Array, data: Uint8Array): Uint8Array { + const newBuf = new Uint8Array(src.length + data.length); + newBuf.set(src, 0); + newBuf.set(data, src.length); + return newBuf; + } + /** * Asynchronously pauses the execution for the specified amount of time. */ @@ -135,6 +150,7 @@ export class Utils { /** * Checks if the browser supports Bluetooth functionality. + * @deprecated use {@link getAvailableTransports} */ public static isBluetoothSupported(): boolean { return typeof navigator.bluetooth?.requestDevice !== "undefined"; @@ -142,10 +158,22 @@ export class Utils { /** * Checks if the browser supports the Web Serial API for serial communication. + * @deprecated use {@link getAvailableTransports} */ public static isSerialSupported(): boolean { return typeof navigator.serial?.requestPort !== "undefined"; } + + /** + * Checks environment functionality + */ + public static getAvailableTransports(): AvailableTransports { + return { + capacitorBle: Capacitor.getPlatform() !== "web", + webBluetooth: typeof navigator.bluetooth?.requestDevice !== "undefined", + webSerial: typeof navigator.serial?.requestPort !== "undefined", + }; + } } /** diff --git a/yarn.lock b/yarn.lock index c653b1d..4589997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,20 @@ # yarn lockfile v1 +"@capacitor-community/bluetooth-le@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@capacitor-community/bluetooth-le/-/bluetooth-le-6.0.2.tgz#ba7bcb72126bd4cac4b70e79e8cfed24251b4f6c" + integrity sha512-X7ytHoHQzMTo89IR1UAu4JCmAJ3hraf+CPPLH/9zy9mUIlpZqcw0eN14vSMAtEBhb0vYz8qGQBQvN8FTRoz1Jw== + dependencies: + "@types/web-bluetooth" "^0.0.20" + +"@capacitor/core@^6.0.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-6.1.2.tgz#a072e78b46693a6a47047cfc90d5f7cc48299fe9" + integrity sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg== + dependencies: + tslib "^2.1.0" + "@types/node@^20.14.2": version "20.14.2" resolved "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz" @@ -31,6 +45,11 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== +tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.4.0: version "2.6.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz"