Skip to content

Commit 5ca1f64

Browse files
AndrewAndrew
authored andcommitted
pairing: connection status tracking, retry on API failure, double-start guard
- Emit 'connection-status' events when server connectivity changes - Retry initPINPairing every 5s when initial attempt fails - Send '------' as PIN until server assigns one (not a fake local PIN) - Guard against double-start (ignore if poll timers already running) - Add closeApp() to cleanly stop pairing wizard and polling timers - Clean up initRetryTimer in stop()
1 parent 1367ff5 commit 5ca1f64

2 files changed

Lines changed: 102 additions & 6 deletions

File tree

src/daemon.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,22 @@ export class DeviceDaemon extends EventEmitter {
187187
* If unpaired, starts the pairing flow.
188188
* If already paired, emits status info for the UI to display.
189189
*/
190+
/**
191+
* Called when the user closes the Allow2 app / UI.
192+
* Stops the pairing wizard if running (clears polling timers).
193+
*/
194+
closeApp() {
195+
if (this._pairingWizard) {
196+
this._pairingWizard.stop();
197+
this._pairingWizard = null;
198+
}
199+
// Stay in current state — unpaired devices remain unpaired,
200+
// paired devices keep enforcing
201+
if (!this._credentials || !this._credentials.pairId) {
202+
this._state = 'unpaired';
203+
}
204+
}
205+
190206
async openApp() {
191207
if (this._state === 'unpaired' || (!this._credentials || !this._credentials.pairId)) {
192208
// Start pairing flow
@@ -533,6 +549,10 @@ export class DeviceDaemon extends EventEmitter {
533549
self.emit('pairing-error', err);
534550
});
535551

552+
this._pairingWizard.on('connection-status', function (status) {
553+
self.emit('pairing-connection-status', status);
554+
});
555+
536556
try {
537557
var info = await this._pairingWizard.start();
538558

@@ -544,6 +564,7 @@ export class DeviceDaemon extends EventEmitter {
544564
port: info.port,
545565
url: info.url,
546566
qrUrl: info.qrUrl,
567+
connected: info.connected,
547568
});
548569
} catch (err) {
549570
this.emit('pairing-error', err);

src/pairing.js

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export class PairingWizard extends EventEmitter {
4747
this._app = null;
4848
this._pollTimer = null;
4949
this._qrUrl = null;
50+
this._connected = false; // tracks server connectivity
51+
this._consecutiveErrors = 0;
5052
}
5153

5254
/**
@@ -58,6 +60,12 @@ export class PairingWizard extends EventEmitter {
5860
* @returns {Promise<{ pin: string, port: number, url: string, qrUrl: string }>}
5961
*/
6062
async start() {
63+
// Guard: prevent double-start
64+
if (this._pollTimer || this._initRetryTimer) {
65+
console.warn('[pairing] start() called but already running — ignoring');
66+
return { pin: this._pin, port: this._port, url: 'http://localhost:' + this._port, qrUrl: this._qrUrl, connected: this._connected };
67+
}
68+
6169
this._paired = false;
6270
this._pairingResult = null;
6371

@@ -87,13 +95,17 @@ export class PairingWizard extends EventEmitter {
8795
// Use server-assigned PIN and session ID
8896
this._pin = String(apiResult.pin);
8997
this._sessionId = apiResult.sessionId || apiResult.pairingSessionId || null;
98+
this._connected = true;
99+
this._consecutiveErrors = 0;
90100
console.log('[pairing] Using server PIN: ' + this._pin + ' (session=' + this._sessionId + ')');
91101
} else {
92-
// Fallback: generate local PIN (won't work without API registration,
93-
// but at least shows something to the user)
94-
this._pin = _generatePin();
102+
// API unreachable — no valid PIN yet
103+
this._pin = '------';
95104
this._sessionId = null;
96-
console.warn('[pairing] Using local PIN (no API session): ' + this._pin);
105+
this._connected = false;
106+
console.warn('[pairing] API unreachable — will retry');
107+
// Start retry loop to get a valid session
108+
this._startInitRetry();
97109
}
98110

99111
// Build QR deep link URL
@@ -120,8 +132,9 @@ export class PairingWizard extends EventEmitter {
120132
port: port,
121133
url: 'http://localhost:' + port,
122134
qrUrl: this._qrUrl,
135+
connected: this._connected,
123136
};
124-
this.emit('started', { pin: this._pin, port: port, qrUrl: this._qrUrl });
137+
this.emit('started', { pin: this._pin, port: port, qrUrl: this._qrUrl, connected: this._connected });
125138
return info;
126139
}
127140

@@ -134,6 +147,10 @@ export class PairingWizard extends EventEmitter {
134147
clearInterval(this._pollTimer);
135148
this._pollTimer = null;
136149
}
150+
if (this._initRetryTimer) {
151+
clearInterval(this._initRetryTimer);
152+
this._initRetryTimer = null;
153+
}
137154

138155
if (!this._server) return;
139156

@@ -229,6 +246,50 @@ export class PairingWizard extends EventEmitter {
229246
return uuid;
230247
}
231248

249+
/**
250+
* Retry initPINPairing when the initial attempt failed (no connectivity).
251+
* Retries every 5s until a valid session is obtained.
252+
*/
253+
_startInitRetry() {
254+
if (this._initRetryTimer) return;
255+
256+
var self = this;
257+
this._initRetryTimer = setInterval(async function () {
258+
if (self._paired || self._sessionId) {
259+
clearInterval(self._initRetryTimer);
260+
self._initRetryTimer = null;
261+
return;
262+
}
263+
264+
try {
265+
var result = await self._api.initPINPairing({
266+
uuid: self._uuid,
267+
deviceName: self._deviceName,
268+
});
269+
270+
if (result && result.pin) {
271+
clearInterval(self._initRetryTimer);
272+
self._initRetryTimer = null;
273+
274+
self._pin = String(result.pin);
275+
self._sessionId = result.sessionId || result.pairingSessionId || null;
276+
self._qrUrl = 'https://app.allow2.com/pair?pin=' + self._pin;
277+
self._connected = true;
278+
self._consecutiveErrors = 0;
279+
280+
console.log('[pairing] Reconnected! PIN: ' + self._pin + ' (session=' + self._sessionId + ')');
281+
self.emit('connection-status', { connected: true, pin: self._pin, qrUrl: self._qrUrl });
282+
283+
// Now start polling for parent confirmation
284+
self._startPolling();
285+
}
286+
} catch (err) {
287+
console.warn('[pairing] Init retry failed:', err.message);
288+
self.emit('connection-status', { connected: false });
289+
}
290+
}, 5000);
291+
}
292+
232293
/**
233294
* Start polling the Allow2 API for pairing confirmation.
234295
*/
@@ -257,9 +318,23 @@ export class PairingWizard extends EventEmitter {
257318
self._api.checkPairingStatus(self._sessionId).then(function (result) {
258319
if (result && result.paired && result.userId && result.pairId && result.pairToken) {
259320
self.completePairing(result);
321+
return;
322+
}
323+
// Successful poll — mark as connected
324+
if (!self._connected) {
325+
self._connected = true;
326+
self._consecutiveErrors = 0;
327+
console.log('[pairing] Connection restored');
328+
self.emit('connection-status', { connected: true });
260329
}
261330
}).catch(function (err) {
262-
// Polling errors are non-fatal — just retry next interval
331+
self._consecutiveErrors++;
332+
// After 2 consecutive failures (~10s), mark as disconnected
333+
if (self._consecutiveErrors >= 2 && self._connected) {
334+
self._connected = false;
335+
console.warn('[pairing] Connection lost:', err.message);
336+
self.emit('connection-status', { connected: false });
337+
}
263338
if (pollCount % 12 === 0) { // log every minute
264339
console.warn('[pairing] Poll error:', err.message);
265340
}

0 commit comments

Comments
 (0)