ember-mug 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,5 +6,5 @@ export function ConnectionStatus({ isScanning, isConnected, foundMugName, error,
|
|
|
6
6
|
if (isConnected) {
|
|
7
7
|
return _jsx(_Fragment, {});
|
|
8
8
|
}
|
|
9
|
-
return (_jsx(Box, { justifyContent: "center", marginY: 2, children: _jsx(Panel, { title: error ? "[!] Error" : isScanning ? "[~] Scanning" : "C[_] Welcome", titleColor: error ? "red" : theme.primary, borderColor: error ? "red" : theme.border, width: width, height: height, children: error ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsx(Text, { color: "red", bold: true, children: error }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.dimText, children: ["Press", " ", _jsx(Text, { color: "yellow", bold: true, children: "[r]" }), " ", "to retry scanning"] }) })] })) : isScanning ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: theme.text, children: " Searching for Ember mug..." })] }), foundMugName && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "green", bold: true, children: ["* Found: ", foundMugName] }) })), foundMugName && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: theme.text, children: " Connecting..." })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.dimText, children: "Ensure mug is powered on and nearby" }) })] })) : (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsx(Text, { color: theme.text, children: "No Ember mug connected" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.dimText, children: ["Press", " ", _jsx(Text, { color: "green", bold: true, children: "[s]" }), " ", "to start scanning"] }) })] })) }) }));
|
|
9
|
+
return (_jsx(Box, { justifyContent: "center", marginY: 2, children: _jsx(Panel, { title: error ? "[!] Error" : isScanning ? "[~] Scanning" : "C[_] Welcome", titleColor: error ? "red" : theme.primary, borderColor: error ? "red" : theme.border, width: width, height: height, children: error ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsx(Text, { color: "red", bold: true, children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.dimText, children: "Note: Mug must be set up with the official Ember app first." }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.dimText, children: ["Press", " ", _jsx(Text, { color: "yellow", bold: true, children: "[r]" }), " ", "to retry scanning"] }) })] })) : isScanning ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: theme.text, children: " Searching for Ember mug..." })] }), foundMugName && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "green", bold: true, children: ["* Found: ", foundMugName] }) })), foundMugName && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: theme.text, children: " Connecting..." })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.dimText, children: "Ensure mug is powered on and nearby" }) })] })) : (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsx(Text, { color: theme.text, children: "No Ember mug connected" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.dimText, children: ["Press", " ", _jsx(Text, { color: "green", bold: true, children: "[s]" }), " ", "to start scanning"] }) })] })) }) }));
|
|
10
10
|
}
|
package/dist/lib/bluetooth.d.ts
CHANGED
package/dist/lib/bluetooth.js
CHANGED
|
@@ -38,20 +38,36 @@ export class BluetoothManager extends EventEmitter {
|
|
|
38
38
|
this.emit('mugFound', name);
|
|
39
39
|
await this.stopScanning();
|
|
40
40
|
this.peripheral = peripheral;
|
|
41
|
-
|
|
42
|
-
await this.connect();
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
46
|
-
}
|
|
41
|
+
await this.connectWithRetry();
|
|
47
42
|
}
|
|
48
43
|
});
|
|
49
44
|
}
|
|
45
|
+
async connectWithRetry(maxRetries = 3) {
|
|
46
|
+
let lastError = null;
|
|
47
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
await this.connect();
|
|
50
|
+
return; // Success
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
54
|
+
if (attempt < maxRetries) {
|
|
55
|
+
// Wait before retrying (exponential backoff: 1s, 2s, 4s)
|
|
56
|
+
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// All retries failed
|
|
62
|
+
this.emit('error', lastError || new Error('Connection failed after multiple attempts'));
|
|
63
|
+
}
|
|
50
64
|
async startScanning() {
|
|
51
65
|
return new Promise((resolve, reject) => {
|
|
52
|
-
|
|
66
|
+
// Scan without service UUID filter - Ember mugs don't always advertise
|
|
67
|
+
// the service UUID in their advertisement packets. We filter by name instead.
|
|
68
|
+
const startScan = () => {
|
|
53
69
|
this.emit('scanning', true);
|
|
54
|
-
noble.startScanning([
|
|
70
|
+
noble.startScanning([], false, (error) => {
|
|
55
71
|
if (error) {
|
|
56
72
|
reject(error);
|
|
57
73
|
}
|
|
@@ -59,19 +75,14 @@ export class BluetoothManager extends EventEmitter {
|
|
|
59
75
|
resolve();
|
|
60
76
|
}
|
|
61
77
|
});
|
|
78
|
+
};
|
|
79
|
+
if (getNobleState() === 'poweredOn') {
|
|
80
|
+
startScan();
|
|
62
81
|
}
|
|
63
82
|
else {
|
|
64
83
|
noble.once('stateChange', (state) => {
|
|
65
84
|
if (state === 'poweredOn') {
|
|
66
|
-
|
|
67
|
-
noble.startScanning([EMBER_SERVICE_UUID], false, (error) => {
|
|
68
|
-
if (error) {
|
|
69
|
-
reject(error);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
resolve();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
85
|
+
startScan();
|
|
75
86
|
}
|
|
76
87
|
else {
|
|
77
88
|
reject(new Error(`Bluetooth not available: ${state}`));
|
|
@@ -90,12 +101,37 @@ export class BluetoothManager extends EventEmitter {
|
|
|
90
101
|
if (!this.peripheral) {
|
|
91
102
|
throw new Error('No peripheral to connect to');
|
|
92
103
|
}
|
|
104
|
+
const CONNECTION_TIMEOUT = 10000; // 10 seconds timeout
|
|
93
105
|
return new Promise((resolve, reject) => {
|
|
106
|
+
let timeoutId = null;
|
|
107
|
+
let connected = false;
|
|
108
|
+
const cleanup = () => {
|
|
109
|
+
if (timeoutId) {
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
timeoutId = null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
timeoutId = setTimeout(() => {
|
|
115
|
+
if (!connected) {
|
|
116
|
+
cleanup();
|
|
117
|
+
// Try to cancel the connection attempt
|
|
118
|
+
try {
|
|
119
|
+
this.peripheral?.disconnect();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Ignore disconnect errors during timeout
|
|
123
|
+
}
|
|
124
|
+
reject(new Error('Connection timed out. Make sure your mug is nearby and awake.'));
|
|
125
|
+
}
|
|
126
|
+
}, CONNECTION_TIMEOUT);
|
|
94
127
|
this.peripheral.connect(async (error) => {
|
|
95
128
|
if (error) {
|
|
96
|
-
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(new Error(`Failed to connect: ${error}`));
|
|
97
131
|
return;
|
|
98
132
|
}
|
|
133
|
+
connected = true;
|
|
134
|
+
cleanup();
|
|
99
135
|
this.isConnected = true;
|
|
100
136
|
this.state.connected = true;
|
|
101
137
|
this.state.mugName = this.peripheral.advertisement.localName || 'Ember Mug';
|
|
@@ -112,14 +148,30 @@ export class BluetoothManager extends EventEmitter {
|
|
|
112
148
|
resolve();
|
|
113
149
|
}
|
|
114
150
|
catch (err) {
|
|
115
|
-
|
|
151
|
+
this.handleDisconnect();
|
|
152
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
116
153
|
}
|
|
117
154
|
});
|
|
118
155
|
});
|
|
119
156
|
}
|
|
120
157
|
async discoverCharacteristics() {
|
|
158
|
+
// First discover the Ember service specifically
|
|
159
|
+
const services = await new Promise((resolve, reject) => {
|
|
160
|
+
this.peripheral.discoverServices([EMBER_SERVICE_UUID], (error, services) => {
|
|
161
|
+
if (error) {
|
|
162
|
+
reject(error);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
resolve(services || []);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
if (services.length === 0) {
|
|
169
|
+
throw new Error('Ember service not found on device');
|
|
170
|
+
}
|
|
171
|
+
// Then discover characteristics for that service
|
|
172
|
+
const emberService = services[0];
|
|
121
173
|
return new Promise((resolve, reject) => {
|
|
122
|
-
|
|
174
|
+
emberService.discoverCharacteristics([], (error, characteristics) => {
|
|
123
175
|
if (error) {
|
|
124
176
|
reject(error);
|
|
125
177
|
return;
|