This repository has been archived on 2024-09-16. You can view files and clone it, but cannot push or open issues or pull requests.

159 lines
5.0 KiB
TypeScript

import { Mutex } from "async-mutex";
import { ConnectionInfo, NiimbotAbstractClient } from ".";
import { ConnectResult, NiimbotPacket, ResponseCommandId } from "../packets";
import { Utils } from "../utils";
import {
ConnectEvent,
DisconnectEvent,
PacketReceivedEvent,
RawPacketReceivedEvent,
RawPacketSentEvent,
} from "./events";
class BleConfiguration {
public static readonly SERVICE: string = "e7810a71-73ae-499d-8c15-faa9aef0c3f2";
public static readonly CHARACTERISTIC: string = "bef8d6c9-9c21-4c9e-b632-bd58c1009f9f";
public static readonly FILTER: BluetoothLEScanFilter[] = [
{ namePrefix: "D" },
{ namePrefix: "B" },
{ services: [BleConfiguration.SERVICE] },
];
}
export class NiimbotBluetoothClient extends NiimbotAbstractClient {
private gattServer?: BluetoothRemoteGATTServer = undefined;
private channel?: BluetoothRemoteGATTCharacteristic = undefined;
private mutex: Mutex = new Mutex();
public async connect(): Promise<ConnectionInfo> {
this.disconnect();
const options: RequestDeviceOptions = {
filters: BleConfiguration.FILTER,
};
const device: BluetoothDevice = await navigator.bluetooth.requestDevice(options);
if (device.gatt === undefined) {
throw new Error("Device has no Bluetooth Generic Attribute Profile");
}
const disconnectListener = async () => {
this.gattServer = undefined;
this.channel = undefined;
this.info = {};
this.stopHeartbeat();
this.dispatchTypedEvent("disconnect", new DisconnectEvent());
device.removeEventListener("gattserverdisconnected", disconnectListener);
};
device.addEventListener("gattserverdisconnected", disconnectListener);
const gattServer: BluetoothRemoteGATTServer = await device.gatt.connect();
const service: BluetoothRemoteGATTService = await gattServer.getPrimaryService(BleConfiguration.SERVICE);
const channel: BluetoothRemoteGATTCharacteristic = await service.getCharacteristic(BleConfiguration.CHARACTERISTIC);
channel.addEventListener("characteristicvaluechanged", async (event: Event) => {
const target = event.target as BluetoothRemoteGATTCharacteristic;
const packet = NiimbotPacket.fromBytes(target.value!);
this.dispatchTypedEvent("rawpacketreceived", new RawPacketReceivedEvent(target.value!));
this.dispatchTypedEvent("packetreceived", new PacketReceivedEvent(packet));
if (!(packet.command in ResponseCommandId)) {
console.warn(`Unknown response command: 0x${Utils.numberToHex(packet.command)}`);
}
});
await channel.startNotifications();
this.gattServer = gattServer;
this.channel = channel;
try {
await this.initialNegotiate();
await this.fetchPrinterInfo();
} catch (e) {
console.error(e);
}
const result: ConnectionInfo = {
deviceName: device.name,
result: this.info.connectResult ?? ConnectResult.FirmwareErrors
};
this.dispatchTypedEvent("connect", new ConnectEvent(result));
return result;
}
public isConnected(): boolean {
return this.gattServer !== undefined && this.channel !== undefined;
}
public async disconnect() {
this.stopHeartbeat();
this.gattServer?.disconnect();
this.gattServer = undefined;
this.channel = undefined;
this.info = {};
}
/**
* Send packet and wait for response.
* If packet.responsePacketCommandId is defined, it will wait for packet with this command id.
*/
public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs?: number): Promise<NiimbotPacket> {
return this.mutex.runExclusive(async () => {
await this.sendPacket(packet, true);
if (packet.oneWay) {
return new NiimbotPacket(-1, []); // or undefined is better?
}
// what if response received at this point?
return new Promise((resolve) => {
let timeout: NodeJS.Timeout | undefined = undefined;
const listener = (evt: PacketReceivedEvent) => {
if (
packet.validResponseIds.length === 0 ||
packet.validResponseIds.includes(evt.packet.command)
) {
clearTimeout(timeout);
this.removeEventListener("packetreceived", listener);
resolve(evt.packet);
}
};
timeout = setTimeout(() => {
this.removeEventListener("packetreceived", listener);
throw new Error(
`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`
);
}, timeoutMs ?? 1000);
this.addEventListener("packetreceived", listener);
});
});
}
public async sendRaw(data: Uint8Array, force?: boolean) {
const send = async () => {
if (this.channel === undefined) {
throw new Error("Channel is closed");
}
await this.channel.writeValueWithoutResponse(data);
this.dispatchTypedEvent("rawpacketsent", new RawPacketSentEvent(data));
};
if (force) {
return send();
} else {
await this.mutex.runExclusive(send);
}
}
}