Skip to content

Commit d84246a

Browse files
committed
[chore] consolidate published v0.6.1 source and platform updates
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
2 parents cf53cf5 + c8f549e commit d84246a

54 files changed

Lines changed: 8841 additions & 240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎app/lib/ble/fbp_ble_link.dart‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,23 @@ class FbpBleLink implements BleLink {
4747

4848
@override
4949
Future<void> disconnect() async {
50-
await _device.disconnect();
51-
_linkState.value = BleLinkState.disconnected;
52-
await _connSub.cancel();
50+
Object? firstError;
51+
StackTrace? firstStack;
52+
try {
53+
await _device.disconnect();
54+
_linkState.value = BleLinkState.disconnected;
55+
} catch (error, stack) {
56+
firstError = error;
57+
firstStack = stack;
58+
}
59+
try {
60+
await _connSub.cancel();
61+
} catch (error, stack) {
62+
firstError ??= error;
63+
firstStack ??= stack;
64+
}
65+
if (firstError != null) {
66+
Error.throwWithStackTrace(firstError, firstStack!);
67+
}
5368
}
5469
}

‎app/lib/pble/connection_manager.dart‎

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ class PbleConnectionManager implements ConnectionManager {
160160
ScanHit? _selected;
161161
Object? _lastError;
162162
bool _disposed = false;
163+
int _intent = 0;
164+
Future<void>? _disposeFuture;
165+
final Set<Future<void>> _retirements = <Future<void>>{};
166+
final Map<String, Future<void>> _openings = <String, Future<void>>{};
163167

164168
@override
165169
Connection get connection => _facade;
@@ -181,6 +185,7 @@ class PbleConnectionManager implements ConnectionManager {
181185

182186
@override
183187
Future<void> startScan({Duration? timeout}) async {
188+
_ensureActive();
184189
// Fire-and-forget: a broadcast-subscription cancel() future never completes
185190
// under `tester.pump()` (FakeAsync), which would stall connect/stopScan in
186191
// every widget test. The listener is removed synchronously either way; the
@@ -199,6 +204,7 @@ class PbleConnectionManager implements ConnectionManager {
199204

200205
@override
201206
Future<void> stopScan() async {
207+
_ensureActive();
202208
// Fire-and-forget: a broadcast-subscription cancel() future never completes
203209
// under `tester.pump()` (FakeAsync), which would stall connect/stopScan in
204210
// every widget test. The listener is removed synchronously either way; the
@@ -208,75 +214,170 @@ class PbleConnectionManager implements ConnectionManager {
208214
await _scanner.stopScan();
209215
// Only fall back to idle when no board is live (a connected session keeps
210216
// its connected/connecting phase).
211-
if (_liveBoard == null && _phase.value == ConnectPhase.scanning) {
217+
if (!_disposed &&
218+
_liveBoard == null &&
219+
_phase.value == ConnectPhase.scanning) {
212220
_phase.value = ConnectPhase.idle;
213221
}
214222
}
215223

216224
@override
217225
Future<void> connect(String id) async {
226+
_ensureActive();
227+
final int intent = ++_intent;
228+
_detachAndRetire();
218229
// Scanning and GATT do not run together — stop the scan first (FR-BLE-5).
219230
// Fire-and-forget: a broadcast-subscription cancel() future never completes
220231
// under `tester.pump()` (FakeAsync), which would stall connect/stopScan in
221232
// every widget test. The listener is removed synchronously either way; the
222233
// completion signal is not needed here (matches _StableFacade.detach below).
223234
unawaited(_scanSub?.cancel());
224235
_scanSub = null;
225-
await _scanner.stopScan();
226-
227236
_selected = _hits[id] ?? ScanHit(id: id, name: '', rssi: 0);
228237
_lastError = null;
229238
_phase.value = ConnectPhase.connecting;
230239
try {
231-
final Connection board = await _connectionFactory(id);
232-
_liveBoard = board;
233-
_facade.attach(board);
234-
// Derive the phase from the freshly attached board's live state (the
235-
// listener also fires on the sync-up, but re-derive to cover a board that
236-
// attaches already-ready with no state change).
237-
_phase.value = _phaseForConn(_facade.state.value);
240+
await _scanner.stopScan();
241+
// A previous Disconnect/replacement may still own a physical close.
242+
// Do not open another board until those exact retirements complete.
243+
await Future.wait(_retirements.toList());
244+
if (!_isCurrent(intent)) return;
245+
if (_openings[id] case final Future<void> previous) {
246+
// Native wrappers with the same ID share one physical link. Finish
247+
// the stale acquisition AND its teardown before opening that ID again.
248+
// Its factory error belongs to its caller; a failed physical close is
249+
// independently retained in _retirements and must still block us.
250+
try {
251+
await previous;
252+
} catch (_) {}
253+
await Future.wait(_retirements.toList());
254+
if (!_isCurrent(intent)) return;
255+
}
256+
final Future<void> opening = Future<void>.microtask(
257+
() => _openAndPublish(id, intent),
258+
);
259+
_openings[id] = opening;
260+
try {
261+
await opening;
262+
} finally {
263+
if (identical(_openings[id], opening)) _openings.remove(id);
264+
}
238265
} catch (error) {
239-
_lastError = error;
240-
_phase.value = ConnectPhase.failed;
266+
if (_isCurrent(intent)) {
267+
_lastError = error;
268+
_phase.value = ConnectPhase.failed;
269+
}
241270
rethrow;
242271
}
243272
}
244273

274+
Future<void> _openAndPublish(String id, int intent) async {
275+
if (!_isCurrent(intent)) return;
276+
final Connection board = await _connectionFactory(id);
277+
if (!_isCurrent(intent)) {
278+
await _retire(board);
279+
return;
280+
}
281+
_liveBoard = board;
282+
_facade.attach(board);
283+
_phase.value = _phaseForConn(_facade.state.value);
284+
}
285+
245286
@override
246287
Future<void> disconnect() async {
247-
final Connection? board = _liveBoard;
248-
// Null the live board BEFORE detaching so the facade's reset to
249-
// disconnected does not drive the phase (we set it to idle explicitly).
250-
_liveBoard = null;
251-
_facade.detach();
252-
await board?.dispose();
288+
_ensureActive();
289+
final int intent = ++_intent;
290+
_detachAndRetire();
253291
_selected = null;
254-
_phase.value = ConnectPhase.idle;
292+
try {
293+
await Future.wait(_retirements.toList());
294+
if (_isCurrent(intent)) {
295+
_lastError = null;
296+
_phase.value = ConnectPhase.idle;
297+
}
298+
} catch (error) {
299+
if (_isCurrent(intent)) {
300+
_lastError = error;
301+
_phase.value = ConnectPhase.failed;
302+
}
303+
rethrow;
304+
}
255305
}
256306

257307
@override
258-
Future<void> dispose() async {
259-
if (_disposed) return;
308+
Future<void> dispose() {
309+
if (_disposeFuture case final Future<void> pending) return pending;
260310
_disposed = true;
311+
_intent++;
312+
return _disposeFuture = Future<void>.microtask(_disposeResources);
313+
}
314+
315+
Future<void> _disposeResources() async {
316+
Object? firstError;
317+
StackTrace? firstStack;
318+
Future<void> attempt(FutureOr<void> Function() action) async {
319+
try {
320+
await action();
321+
} catch (error, stack) {
322+
firstError ??= error;
323+
firstStack ??= stack;
324+
}
325+
}
326+
261327
// Fire-and-forget: a broadcast-subscription cancel() future never completes
262328
// under `tester.pump()` (FakeAsync), which would stall connect/stopScan in
263329
// every widget test. The listener is removed synchronously either way; the
264330
// completion signal is not needed here (matches _StableFacade.detach below).
265331
unawaited(_scanSub?.cancel());
266332
_scanSub = null;
267333
_facade.state.removeListener(_onFacadeState);
334+
_detachAndRetire();
335+
_selected = null;
336+
await Future.wait(<Future<void>>[
337+
attempt(_scanner.stopScan),
338+
attempt(() => Future.wait(_retirements.toList())),
339+
]);
340+
await attempt(_scanResults.close);
341+
await attempt(_facade.dispose);
342+
await attempt(_phase.dispose);
343+
if (firstError != null) {
344+
Error.throwWithStackTrace(firstError!, firstStack!);
345+
}
346+
}
347+
348+
bool _isCurrent(int intent) => !_disposed && _intent == intent;
349+
350+
void _ensureActive() {
351+
if (_disposed) throw StateError('connection manager disposed');
352+
}
353+
354+
void _detachAndRetire() {
268355
final Connection? board = _liveBoard;
269356
_liveBoard = null;
270-
await board?.dispose();
271-
await _scanResults.close();
272-
await _facade.dispose();
273-
_phase.dispose();
357+
_facade.detach();
358+
if (board != null) _retire(board);
359+
}
360+
361+
Future<void> _retire(Connection board) {
362+
final Future<void> closing = Future<void>.sync(board.dispose);
363+
_retirements.add(closing);
364+
// Remove only successful retirements. A failed physical close is not proof
365+
// that another link can safely be opened; later operations must still see
366+
// that failure. Consume this bookkeeping branch's error without replacing
367+
// the original future or issuing an automatic disconnect retry.
368+
unawaited(
369+
closing.then<void>(
370+
(_) => _retirements.remove(closing),
371+
onError: (Object _, StackTrace _) {},
372+
),
373+
);
374+
return closing;
274375
}
275376

276377
// Track the session phase off the stable facade's live ConnState. Only
277378
// meaningful while a board is attached; scan/failure phases own the idle path.
278379
void _onFacadeState() {
279-
if (_liveBoard == null) return;
380+
if (_disposed || _liveBoard == null) return;
280381
_phase.value = _phaseForConn(_facade.state.value);
281382
}
282383

‎app/lib/pble/engine.dart‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class PbleEngine {
5959
}
6060
final _PendingRequest pending = _PendingRequest(cmd.opcode, timeout);
6161
_pending[cmd.id] = pending;
62+
// Disposal can reject this completion while an acknowledged write is
63+
// still pending. Observe errors now without replacing the original future
64+
// that the request later awaits (including its exact response/error).
65+
unawaited(
66+
pending.completer.future.then<void>(
67+
(_) {},
68+
onError: (Object _, StackTrace _) {},
69+
),
70+
);
6271
try {
6372
final List<Uint8List> packets = PbleFragmenter(
6473
mtu: _transport.mtu,
@@ -71,6 +80,12 @@ class PbleEngine {
7180
remaining,
7281
onTimeout: () => throw const _RequestDeadlineExpired(),
7382
);
83+
if (_disposed) {
84+
if (pending.completer.isCompleted) {
85+
return await pending.completer.future;
86+
}
87+
throw const PbleTimeoutException('engine disposed');
88+
}
7489
}
7590
if (pending.completer.isCompleted) {
7691
return await pending.completer.future;
@@ -109,6 +124,7 @@ class PbleEngine {
109124
).fragment(encodeFrame(cmd));
110125
for (final Uint8List packet in packets) {
111126
await _transport.send(packet, acknowledged: false);
127+
if (_disposed) throw const PbleTimeoutException('engine disposed');
112128
}
113129
}
114130

@@ -147,7 +163,14 @@ class PbleEngine {
147163
Future<void> dispose() async {
148164
if (_disposed) return;
149165
_disposed = true;
150-
await _inboundSub.cancel();
166+
Object? cancelError;
167+
StackTrace? cancelStack;
168+
try {
169+
await _inboundSub.cancel();
170+
} catch (error, stack) {
171+
cancelError = error;
172+
cancelStack = stack;
173+
}
151174
for (final _PendingRequest pending in _pending.values) {
152175
if (!pending.completer.isCompleted) {
153176
pending.completer.completeError(
@@ -157,6 +180,9 @@ class PbleEngine {
157180
}
158181
_pending.clear();
159182
await _events.close();
183+
if (cancelError != null) {
184+
Error.throwWithStackTrace(cancelError, cancelStack!);
185+
}
160186
}
161187
}
162188

0 commit comments

Comments
 (0)