2026-02-02 18:23:31 +07:00
import { EventEmitter } from "node:events" ;
EventEmitter . defaultMaxListeners = 20 ;
import type { BLEConfig , ProtocolConfig } from "../protocol/index.ts" ;
import { PacketType } from "../protocol/index.ts" ;
import { DeviceNotFoundError , ConnectionError } from "../utils/errors.ts" ;
import { PayloadBuilder , ResponseParser } from "../protocol/index.ts" ;
import { logger , LogEventType } from "../utils/logger.ts" ;
2026-06-08 22:38:42 +07:00
import type { BleBackend , BleCharacteristic } from "./backend.ts" ;
import { resolveBackendType } from "./backend.ts" ;
import { NobleBackend } from "./backends/noble-backend.ts" ;
import { DBusBackend } from "./backends/dbus-backend.ts" ;
function createBackend () : BleBackend {
const type = resolveBackendType ();
return type === "dbus" ? new DBusBackend () : new NobleBackend ();
}
2026-02-02 18:23:31 +07:00
/**
* Handles notifications from the BeamBox device
*/
class NotificationHandler {
waitingForAck = false ;
packetSuccessCount = 0 ;
errorFlag = false ;
lastNotification : Record < string , unknown > | null = null ;
deviceStatus : Record < string , unknown > | null = null ;
deviceReady = false ;
deviceStatusReceived = false ;
allNotifications : Array < { time : number ; data : Buffer ; parsed : any } > = [];
private notificationResolve : (() => void ) | null = null ;
private statusResolve : (() => void ) | null = null ;
2026-02-04 11:36:58 +07:00
public expectedAckCount = 0 ;
2026-02-02 18:23:31 +07:00
constructor ( private verbose : boolean = false ) {}
/**
* Handle incoming notification data from device
* @param data Notification data buffer
*/
public handleNotification ( data : Buffer ) : void {
try {
const response = ResponseParser . parse ( data );
// Verbose mode logging
if ( this . verbose ) {
const timestamp = new Date (). toISOString ();
this . allNotifications . push ({
time : Date.now (),
data ,
parsed : response ,
});
logger . debug (
`[RECV] ${ timestamp } | Bytes: ${ data . length } | Hex: ${ data . toString ( "hex" ) } | Text: ${ response . rawText || "(empty)" } ` ,
);
if ( response . jsonData ) {
logger . debug ( `[RECV] JSON: ${ JSON . stringify ( response . jsonData ) } ` );
}
}
if ( ! response . rawText ) {
return ;
}
if ( ResponseParser . isSuccess ( response )) {
this . packetSuccessCount ++ ;
2026-02-04 11:36:58 +07:00
logger . info (
`Device ack received: GetPacketSuccess ( ${ this . packetSuccessCount } / ${ this . expectedAckCount } )` ,
);
if (
this . waitingForAck &&
this . notificationResolve &&
this . packetSuccessCount >= this . expectedAckCount
) {
2026-02-02 18:23:31 +07:00
this . notificationResolve ();
this . notificationResolve = null ;
}
return ;
}
if ( ResponseParser . isFail ( response )) {
logger . warning ( `Device reported packet fail: ${ response . rawText } ` );
if ( this . notificationResolve ) {
this . notificationResolve ();
this . notificationResolve = null ;
}
return ;
}
if ( ResponseParser . isError ( response )) {
this . errorFlag = true ;
logger . error ( "Device error flag reported: 1111111111" );
if ( this . notificationResolve ) {
this . notificationResolve ();
this . notificationResolve = null ;
}
return ;
}
// Handle JSON responses
if ( response . jsonData ) {
this . lastNotification = response . jsonData ;
// Handle PacketType.DEVICE_STATUS messages
if ( response . isStatus ) {
this . deviceStatus = response . jsonData ;
this . deviceReady = true ;
// Only resolve the first time we get status
if ( ! this . deviceStatusReceived ) {
this . deviceStatusReceived = true ;
logger . info (
`Device status received: ${ JSON . stringify ( response . jsonData ) } ` ,
LogEventType . STATUS_RECEIVED ,
response . jsonData ,
);
} else {
logger . debug ( "Duplicate status notification, ignoring" );
}
if ( this . statusResolve ) {
this . statusResolve ();
this . statusResolve = null ;
}
} else {
logger . debug (
`Device notification payload: ${ JSON . stringify ( response . jsonData ) } ` ,
);
}
if ( this . notificationResolve ) {
this . notificationResolve ();
this . notificationResolve = null ;
}
return ;
}
// Fallback: print raw text if we couldn't parse anything
logger . debug ( `Device notification text: ${ response . rawText } ` );
if ( this . notificationResolve ) {
this . notificationResolve ();
this . notificationResolve = null ;
}
} catch ( error ) {
logger . error ( `Error handling notification: ${ error } ` );
}
}
/**
* Log sent packet in verbose mode
* @param packet Sent packet buffer
* @param description Description of the packet
*/
public logSentPacket ( packet : Buffer , description : string ) : void {
if ( this . verbose ) {
const timestamp = new Date (). toISOString ();
logger . debug (
`[SEND] ${ timestamp } | Bytes: ${ packet . length } | Hex: ${ packet . toString ( "hex" ) } | ${ description } ` ,
);
}
}
/**
* Get all received notifications
* @returns Array of notifications with timestamp, data, and parsed content
*/
2026-02-04 11:35:44 +07:00
public getNotifications () : Array < {
time : number ;
data : Buffer ;
parsed : any ;
} > {
2026-02-02 18:23:31 +07:00
return this . allNotifications ;
}
/**
* Wait for device status to be received
* @returns Promise that resolves when status is received
*/
public waitForStatus () : Promise < void > {
return new Promise (( resolve ) => {
if ( this . deviceStatusReceived ) {
resolve ();
} else {
this . statusResolve = resolve ;
}
});
}
/**
* Wait for any notification from device
* @returns Promise that resolves when a notification is received
*/
public waitForNotification () : Promise < void > {
return new Promise (( resolve ) => {
this . notificationResolve = resolve ;
});
}
/**
* Reset internal state
*/
public reset () : void {
this . notificationResolve = null ;
this . statusResolve = null ;
this . errorFlag = false ;
}
2026-02-04 11:36:58 +07:00
/**
* Set the expected number of acknowledgments to wait for
* @param count Number of packets that will be sent
*/
public setExpectedAckCount ( count : number ) : void {
this . expectedAckCount = count ;
this . packetSuccessCount = 0 ;
}
2026-02-02 18:23:31 +07:00
}
/**
* Manages BLE connection and data transfer to BeamBox device
*/
export class BleUploader {
2026-06-08 22:38:42 +07:00
private backend : BleBackend ;
private writeCharacteristic : BleCharacteristic | null = null ;
private notifyCharacteristic : BleCharacteristic | null = null ;
2026-02-02 18:23:31 +07:00
private notificationHandler : NotificationHandler ;
private payloadBuilder : PayloadBuilder ;
private chunkDelay : number ;
private verbose : boolean = false ;
2026-02-04 11:35:44 +07:00
private isInitialized : boolean = false ;
2026-06-08 22:38:42 +07:00
private connectedAddress : string | null = null ;
2026-02-02 18:23:31 +07:00
constructor (
private deviceAddress : string | null ,
chunkDelay : number | null ,
private bleConfig : BLEConfig ,
private protocolConfig : ProtocolConfig ,
verbose : boolean = false ,
) {
this . chunkDelay = chunkDelay ?? protocolConfig . packetDelay ;
this . verbose = verbose ;
this . notificationHandler = new NotificationHandler ( verbose );
this . payloadBuilder = new PayloadBuilder ( protocolConfig );
2026-06-08 22:38:42 +07:00
this . backend = createBackend ();
logger . debug ( `Using BLE backend: ${ this . backend . name } ` );
2026-02-02 18:23:31 +07:00
}
/**
2026-06-08 22:38:42 +07:00
* Initialize Bluetooth adapter and wait for it to be ready
2026-02-02 18:23:31 +07:00
*/
private async initBluetooth () : Promise < void > {
2026-02-04 11:35:44 +07:00
if ( this . isInitialized ) {
return ;
2026-02-02 18:23:31 +07:00
}
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
await this . backend . init ();
this . isInitialized = true ;
2026-02-04 11:35:44 +07:00
}
/**
* Normalize UUID for comparison
* Handles both full 128-bit UUIDs and short 16/32-bit UUIDs
* Short UUIDs use the Bluetooth Base UUID: 00000000-0000-1000-8000-00805f9b34fb
*/
private normalizeUUID ( uuid : string ) : string {
// Remove dashes and lowercase
const cleaned = uuid . replace ( /-/g , "" ). toLowerCase ();
// If it's a full 128-bit UUID using Bluetooth Base UUID, extract the short form
// Bluetooth Base UUID pattern: 0000XXXX-0000-1000-8000-00805f9b34fb
const bluetoothBasePattern = /^0000([0-9a-f]{4})00001000800000805f9b34fb$/ ;
const match = cleaned . match ( bluetoothBasePattern );
if ( match && match [ 1 ]) {
return match [ 1 ]; // Return short UUID part
}
2026-02-04 15:27:27 +07:00
// For short UUIDs (3-4 hex chars), pad with zeros to 4 chars for consistent comparison
// This handles cases like "1f1" vs "01f1"
if ( cleaned . length <= 4 ) {
return cleaned . padStart ( 4 , "0" );
}
2026-02-04 11:35:44 +07:00
return cleaned ;
2026-02-02 18:23:31 +07:00
}
/**
* Scan for the BeamBox device
* @returns Device address or null if not found
*/
public async findDevice () : Promise < string | null > {
2026-02-04 11:35:44 +07:00
await this . initBluetooth ();
2026-02-02 18:23:31 +07:00
logger . info ( "Starting device scan..." , LogEventType . SCAN_START );
2026-06-08 22:38:42 +07:00
const targetName = this . bleConfig . deviceName . toLowerCase ();
const timeout = this . bleConfig . scanTimeout * 1000 ;
2026-02-02 18:23:31 +07:00
2026-06-08 22:38:42 +07:00
const found = await this . backend . scanFor (( device ) => {
return (
!! device . name && device . name . toLowerCase (). includes ( targetName )
);
}, timeout );
2026-02-02 18:23:31 +07:00
2026-06-08 22:38:42 +07:00
if ( ! found ) {
return null ;
}
2026-02-02 18:23:31 +07:00
2026-06-08 22:38:42 +07:00
logger . info (
`Found device: ${ found . name } ( ${ found . address } )` ,
LogEventType . DEVICE_FOUND ,
{ name : found.name , address : found.address },
);
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
return found . address ;
2026-02-02 18:23:31 +07:00
}
/**
* Connect to the BeamBox device and wait for device status
* @returns True if connected successfully
*/
public async connect () : Promise < boolean > {
try {
await this . initBluetooth ();
2026-06-08 22:38:42 +07:00
if ( ! this . connectedAddress ) {
2026-02-02 18:23:31 +07:00
if ( ! this . deviceAddress ) {
2026-02-04 11:35:44 +07:00
logger . info ( "Scanning for device..." , LogEventType . SCAN_START );
const address = await this . findDevice ();
if ( ! address ) {
throw new DeviceNotFoundError (
`Could not find ' ${ this . bleConfig . deviceName } '` ,
);
}
this . deviceAddress = address ;
} else {
logger . info (
"Scanning for device by address..." ,
LogEventType . SCAN_START ,
2026-02-02 18:23:31 +07:00
);
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
const targetAddress = this . deviceAddress . toLowerCase ();
const timeout = this . bleConfig . scanTimeout * 1000 ;
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
const found = await this . backend . scanFor (
( device ) => device . address . toLowerCase () === targetAddress ,
timeout ,
2026-02-04 11:35:44 +07:00
);
2026-06-08 22:38:42 +07:00
if ( ! found ) {
2026-02-04 11:35:44 +07:00
throw new DeviceNotFoundError (
`Could not find device with address ' ${ this . deviceAddress } '` ,
);
}
2026-02-02 18:23:31 +07:00
}
}
2026-06-08 22:38:42 +07:00
await this . backend . stopScan ();
2026-02-04 15:27:27 +07:00
2026-02-02 18:23:31 +07:00
// Connect to device
logger . info ( "Connecting to device..." , LogEventType . CONNECT_START );
2026-02-04 15:27:27 +07:00
const connectStartTime = Date . now ();
2026-06-08 22:38:42 +07:00
await this . backend . connect ( this . deviceAddress ! );
this . connectedAddress = this . deviceAddress ;
2026-02-04 15:27:27 +07:00
const connectDuration = Date . now () - connectStartTime ;
logger . info (
`Connected to device (took ${ connectDuration } ms)` ,
LogEventType . CONNECTED ,
);
2026-02-02 18:23:31 +07:00
2026-02-04 11:35:44 +07:00
// Handle disconnect events
2026-06-08 22:38:42 +07:00
this . backend . onDisconnect (() => {
2026-02-04 11:35:44 +07:00
logger . info ( "Device disconnected" );
2026-06-08 22:38:42 +07:00
this . connectedAddress = null ;
2026-02-04 11:35:44 +07:00
this . writeCharacteristic = null ;
this . notifyCharacteristic = null ;
});
2026-02-02 18:23:31 +07:00
2026-02-04 11:35:44 +07:00
// Discover services and characteristics
2026-02-02 18:23:31 +07:00
await this . discoverCharacteristics ();
// Setup notifications
if ( this . notifyCharacteristic ) {
2026-06-08 22:38:42 +07:00
await this . notifyCharacteristic . subscribe (( data : Buffer ) => {
2026-02-04 11:35:44 +07:00
this . notificationHandler . handleNotification ( data );
2026-02-02 18:23:31 +07:00
});
}
// Wait for device status (PacketType.DEVICE_STATUS) to be received
logger . info ( "Waiting for device status..." , LogEventType . STATUS_WAIT );
const statusPromise = this . notificationHandler . waitForStatus ();
const timeoutPromise = this . sleep ( 5000 );
await Promise . race ([ statusPromise , timeoutPromise ]);
if ( this . notificationHandler . deviceReady ) {
logger . info ( "Device status received" , LogEventType . STATUS_RECEIVED );
} else {
logger . warning (
"Device status not received within timeout, proceeding anyway" ,
);
}
return true ;
} catch ( error ) {
logger . error ( `Connection error: ${ error } ` );
2026-02-04 11:35:44 +07:00
await this . disconnect ();
2026-02-02 18:23:31 +07:00
return false ;
}
}
/**
* Discover required characteristics on the device
*/
private async discoverCharacteristics () : Promise < void > {
2026-02-04 11:35:44 +07:00
const normalizedWriteUuid = this . normalizeUUID (
this . bleConfig . writeCharacteristicUUID ,
);
const normalizedNotifyUuid = this . normalizeUUID (
this . bleConfig . notifyCharacteristicUUID ,
);
2026-02-02 18:23:31 +07:00
2026-02-04 11:35:44 +07:00
logger . debug ( `Looking for write UUID: ${ normalizedWriteUuid } ` );
logger . debug ( `Looking for notify UUID: ${ normalizedNotifyUuid } ` );
2026-06-08 22:38:42 +07:00
const { write , notify } = await this . backend . discoverCharacteristics (
this . bleConfig . writeCharacteristicUUID ,
this . bleConfig . notifyCharacteristicUUID ,
( uuid ) => this . normalizeUUID ( uuid ),
);
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
this . writeCharacteristic = write ;
this . notifyCharacteristic = notify ;
2026-02-02 18:23:31 +07:00
2026-06-08 22:38:42 +07:00
logger . debug (
`Found write characteristic: ${ write . uuid } ` ,
LogEventType . DISCOVER_CHAR ,
{ type : "write" , uuid : write.uuid },
);
logger . debug (
`Found notify characteristic: ${ notify . uuid } ` ,
LogEventType . DISCOVER_CHAR ,
{ type : "notify" , uuid : notify.uuid },
);
2026-02-02 18:23:31 +07:00
}
/**
* Disconnect from the device
*/
public async disconnect () : Promise < void > {
try {
2026-02-04 11:35:44 +07:00
if ( this . notifyCharacteristic ) {
2026-06-08 22:38:42 +07:00
await this . notifyCharacteristic . unsubscribe ();
2026-02-02 18:23:31 +07:00
}
2026-02-04 11:35:44 +07:00
2026-06-08 22:38:42 +07:00
await this . backend . disconnect ();
this . connectedAddress = null ;
this . writeCharacteristic = null ;
this . notifyCharacteristic = null ;
2026-02-02 18:23:31 +07:00
} catch ( error ) {
logger . warning ( `Error during disconnect: ${ error } ` );
}
}
/**
* Send image info packet to device
* Tells device how many images to expect
* @param payload Image info payload bytes (e.g., {"type":6,"number":1})
*/
public async sendImageInfo ( payload : Buffer ) : Promise < void > {
if ( ! this . writeCharacteristic ) {
throw new ConnectionError ( "BLE client not connected" );
}
if ( ! this . notificationHandler . deviceReady ) {
logger . warning ( "Device status not received, but proceeding with upload" );
}
const packet = this . payloadBuilder . createPacket (
payload ,
0 ,
0 ,
PacketType . IMAGE ,
);
logger . debug (
`Sending image info packet (type ${ PacketType . IMAGE } ): ${ payload . length } bytes` ,
LogEventType . IMAGE_INFO_SEND ,
{ size : payload.length },
);
if ( this . verbose ) {
const imageInfoHex = packet . toString ( "hex" );
logger . debug (
`Full image info packet hex ( ${ packet . length } bytes): ${ imageInfoHex } ` ,
);
}
this . notificationHandler . logSentPacket ( packet , "Image info packet" );
2026-02-04 11:35:44 +07:00
// Write without response
2026-06-08 22:38:42 +07:00
await this . writeCharacteristic . write ( packet , true );
2026-02-02 18:23:31 +07:00
await this . sleep ( this . protocolConfig . imageInfoDelay * 1000 );
}
/**
2026-02-04 11:36:58 +07:00
* Send image data packets to device with batched acknowledgment waiting
2026-02-02 18:23:31 +07:00
* @param fullData Complete image data payload
2026-02-04 12:37:40 +07:00
* @param packetType Packet type for header
2026-02-04 11:36:58 +07:00
* @param onProgress Progress callback with (progress, status)
2026-02-02 18:23:31 +07:00
* @returns True if successful
*/
public async sendData (
fullData : Buffer ,
2026-02-04 12:37:40 +07:00
packetType : PacketType ,
2026-02-04 11:36:58 +07:00
onProgress ?: ( progress : number , status? : string ) => void ,
2026-02-02 18:23:31 +07:00
) : Promise < boolean > {
if ( ! this . writeCharacteristic ) {
throw new ConnectionError ( "BLE client not connected" );
}
const totalSize = fullData . length ;
const chunkSize = this . protocolConfig . chunkSize ;
const totalChunks = Math . ceil ( totalSize / chunkSize );
2026-02-04 11:36:58 +07:00
const batchSize = 10 ; // Send 10 packets then wait for acks
// Set expected acknowledgment count before sending
this . notificationHandler . setExpectedAckCount ( totalChunks );
2026-02-02 18:23:31 +07:00
logger . info (
2026-02-04 11:36:58 +07:00
`Starting data transfer: ${ totalChunks } packets in batches of ${ batchSize } ` ,
2026-02-02 18:23:31 +07:00
LogEventType . DATA_SEND_START ,
2026-02-04 11:36:58 +07:00
{ totalChunks , totalSize , batchSize },
2026-02-02 18:23:31 +07:00
);
if ( this . verbose ) {
const firstChunkPreview = fullData
. subarray ( 0 , Math . min ( 64 , fullData . length ))
. toString ( "hex" );
logger . debug ( `First 64 bytes of payload: ${ firstChunkPreview } ` );
}
for ( let i = 0 ; i < totalChunks ; i ++ ) {
const start = i * chunkSize ;
const end = Math . min ( start + chunkSize , totalSize );
const chunk = fullData . subarray ( start , end );
const remainingPackets = totalChunks - 1 - i ;
const packet = this . payloadBuilder . createPacket (
chunk ,
totalChunks ,
remainingPackets ,
2026-02-04 12:37:40 +07:00
packetType ,
2026-02-02 18:23:31 +07:00
);
logger . info (
`Sending packet ${ i + 1 } / ${ totalChunks } (remaining= ${ remainingPackets } , bytes= ${ chunk . length } )` ,
LogEventType . DATA_SEND_PROGRESS ,
{ current : i + 1 , total : totalChunks , remaining : remainingPackets },
);
if ( this . verbose ) {
this . notificationHandler . logSentPacket (
packet ,
`Data packet ${ i + 1 } / ${ totalChunks } ` ,
);
if ( i === 0 ) {
const headerHex = packet . subarray ( 0 , 8 ). toString ( "hex" );
const checksumHex = packet [ packet . length - 1 ]
? . toString ( 16 )
. padStart ( 2 , "0" );
logger . debug ( `First packet header: ${ headerHex } ` );
logger . debug ( `First packet checksum: ${ checksumHex } ` );
}
}
2026-02-04 11:35:44 +07:00
// Write without response
2026-06-08 22:38:42 +07:00
await this . writeCharacteristic . write ( packet , true );
2026-02-02 18:23:31 +07:00
if ( this . notificationHandler . errorFlag ) {
logger . error ( "Device error flag set; aborting send." );
return false ;
}
// Report progress
if ( onProgress ) {
2026-02-04 11:36:58 +07:00
// Sending contributes 50% of this packet's progress
const sendProgress = (( i + 1 ) / totalChunks ) * 50 ;
// Acks contribute the other 50%
const ackProgress =
( this . notificationHandler . packetSuccessCount / totalChunks ) * 50 ;
const combinedProgress = sendProgress + ackProgress ;
onProgress (
combinedProgress ,
`Uploading ( ${ i + 1 } sent, ${ this . notificationHandler . packetSuccessCount } confirmed)` ,
);
2026-02-02 18:23:31 +07:00
}
await this . sleep ( this . chunkDelay * 1000 );
2026-02-04 11:36:58 +07:00
// Wait for acks every batchSize packets or at the end
if (( i + 1 ) % batchSize === 0 || i === totalChunks - 1 ) {
const expectedAcksAtThisPoint = i + 1 ;
const ackTimeout = 5.0 ; // 5 seconds to wait for batch acks
const startTime = Date . now ();
logger . info ( `Waiting for acknowledgments up to packet ${ i + 1 } ...` );
while (
this . notificationHandler . packetSuccessCount < expectedAcksAtThisPoint
) {
if (( Date . now () - startTime ) / 1000 > ackTimeout ) {
logger . warning (
`Ack timeout: expected ${ expectedAcksAtThisPoint } , received ${ this . notificationHandler . packetSuccessCount } ` ,
);
break ;
}
if ( onProgress ) {
// Update progress as acks come in
const sendProgress = (( i + 1 ) / totalChunks ) * 50 ;
const ackProgress =
( this . notificationHandler . packetSuccessCount / totalChunks ) * 50 ;
const combinedProgress = sendProgress + ackProgress ;
onProgress (
combinedProgress ,
`Uploading ( ${ i + 1 } sent, ${ this . notificationHandler . packetSuccessCount } confirmed)` ,
);
}
await this . sleep ( 100 );
}
}
2026-02-02 18:23:31 +07:00
}
logger . info ( "Data transfer complete" , LogEventType . DATA_SEND_COMPLETE );
return ! this . notificationHandler . errorFlag ;
}
/**
2026-02-04 11:36:58 +07:00
* Wait for all remaining device acknowledgments after upload
* @param onProgress Optional progress callback
* @returns True if all acks received without error
2026-02-02 18:23:31 +07:00
*/
2026-02-04 11:36:58 +07:00
public async waitForResponse (
onProgress ?: ( progress : number , status? : string ) => void ,
) : Promise < boolean > {
this . notificationHandler . waitingForAck = true ;
2026-02-02 18:23:31 +07:00
2026-02-04 11:36:58 +07:00
// Dynamic timeout: 0.1s per expected packet, minimum 5s, maximum 30s
const dynamicTimeout = Math . min (
Math . max ( this . notificationHandler . expectedAckCount * 0.1 , 5.0 ),
30.0 ,
);
logger . info (
`Waiting for final acknowledgments ( ${ this . notificationHandler . packetSuccessCount } / ${ this . notificationHandler . expectedAckCount } ), timeout: ${ dynamicTimeout . toFixed ( 1 ) } s` ,
);
const startTime = Date . now ();
while (
this . notificationHandler . packetSuccessCount <
this . notificationHandler . expectedAckCount
) {
if (( Date . now () - startTime ) / 1000 > dynamicTimeout ) {
logger . warning (
`Timeout waiting for acks: received ${ this . notificationHandler . packetSuccessCount } / ${ this . notificationHandler . expectedAckCount } ` ,
);
break ;
}
if ( onProgress ) {
// All packets sent (50%), waiting for remaining acks (0-50%)
const sendProgress = 50 ; // All packets already sent
const ackProgress =
( this . notificationHandler . packetSuccessCount /
this . notificationHandler . expectedAckCount ) *
50 ;
const combinedProgress = sendProgress + ackProgress ;
onProgress (
combinedProgress ,
`Uploading ( ${ this . notificationHandler . expectedAckCount } sent, ${ this . notificationHandler . packetSuccessCount } confirmed)` ,
);
}
await this . sleep ( 100 ); // Check every 100ms
}
const success =
! this . notificationHandler . errorFlag &&
this . notificationHandler . packetSuccessCount >=
this . notificationHandler . expectedAckCount ;
if ( success ) {
logger . info ( "All acknowledgments received" );
if ( onProgress ) {
onProgress ( 100 , "Upload complete" );
}
} else if (
this . notificationHandler . packetSuccessCount <
this . notificationHandler . expectedAckCount
) {
logger . warning (
`Incomplete acknowledgments: received ${ this . notificationHandler . packetSuccessCount } / ${ this . notificationHandler . expectedAckCount } ` ,
);
}
this . notificationHandler . waitingForAck = false ;
2026-02-02 18:23:31 +07:00
this . notificationHandler . reset ();
2026-02-04 11:36:58 +07:00
return success ;
2026-02-02 18:23:31 +07:00
}
public hasError () : boolean {
return this . notificationHandler . errorFlag ;
}
public isDeviceReady () : boolean {
return this . notificationHandler . deviceReady ;
}
public getDeviceStatus () : Record < string , unknown > | null {
return this . notificationHandler . deviceStatus ;
}
2026-02-04 11:35:44 +07:00
public getNotifications () : Array < {
time : number ;
data : Buffer ;
parsed : any ;
} > {
2026-02-02 18:23:31 +07:00
return this . notificationHandler . getNotifications ();
}
private sleep ( ms : number ) : Promise < void > {
return new Promise (( resolve ) => setTimeout ( resolve , ms ));
}
}