ember-mug 0.1.6 → 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
  }
@@ -16,6 +16,7 @@ export declare class BluetoothManager extends EventEmitter {
16
16
  private state;
17
17
  constructor();
18
18
  private setupNobleListeners;
19
+ private connectWithRetry;
19
20
  startScanning(): Promise<void>;
20
21
  stopScanning(): Promise<void>;
21
22
  private connect;
@@ -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
- try {
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
- if (getNobleState() === 'poweredOn') {
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([EMBER_SERVICE_UUID], false, (error) => {
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
- this.emit('scanning', true);
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
- reject(error);
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
- reject(err);
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
- this.peripheral.discoverAllServicesAndCharacteristics((error, services, characteristics) => {
174
+ emberService.discoverCharacteristics([], (error, characteristics) => {
123
175
  if (error) {
124
176
  reject(error);
125
177
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-mug",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "A CLI app for controlling Ember mugs via Bluetooth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",