159 lines
5.0 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|