Capacitor BLE client implementation, instantiateClient function

This commit is contained in:
MultiMote 2024-11-17 10:32:41 +03:00
parent 8e15d2e7bf
commit dad965933e
8 changed files with 528 additions and 292 deletions

@ -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"
},

@ -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;
}
}

@ -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)) {

@ -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);
}
}
}

@ -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);
}
}
export {
NiimbotAbstractClient,
ConnectionInfo,
PrinterInfo,
NiimbotBluetoothClient,
NiimbotCapacitorBleClient,
NiimbotSerialClient,
};
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";

@ -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) {

@ -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",
};
}
}
/**

@ -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"