ember-mug 0.1.3
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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/components/App.d.ts +2 -0
- package/dist/components/App.js +120 -0
- package/dist/components/BatteryDisplay.d.ts +9 -0
- package/dist/components/BatteryDisplay.js +25 -0
- package/dist/components/ColorControl.d.ts +9 -0
- package/dist/components/ColorControl.js +71 -0
- package/dist/components/ConnectionStatus.d.ts +10 -0
- package/dist/components/ConnectionStatus.js +9 -0
- package/dist/components/Header.d.ts +7 -0
- package/dist/components/Header.js +5 -0
- package/dist/components/HelpDisplay.d.ts +6 -0
- package/dist/components/HelpDisplay.js +5 -0
- package/dist/components/Presets.d.ts +11 -0
- package/dist/components/Presets.js +19 -0
- package/dist/components/SettingsView.d.ts +11 -0
- package/dist/components/SettingsView.js +34 -0
- package/dist/components/TemperatureControl.d.ts +10 -0
- package/dist/components/TemperatureControl.js +38 -0
- package/dist/components/TemperatureDisplay.d.ts +10 -0
- package/dist/components/TemperatureDisplay.js +40 -0
- package/dist/hooks/useMug.d.ts +14 -0
- package/dist/hooks/useMug.js +112 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/bluetooth.d.ts +43 -0
- package/dist/lib/bluetooth.js +334 -0
- package/dist/lib/settings.d.ts +28 -0
- package/dist/lib/settings.js +73 -0
- package/dist/lib/types.d.ts +57 -0
- package/dist/lib/types.js +34 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.js +152 -0
- package/package.json +64 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MugState, TemperatureUnit, RGBColor } from '../lib/types.js';
|
|
2
|
+
interface UseMugReturn {
|
|
3
|
+
state: MugState;
|
|
4
|
+
isScanning: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
foundMugName: string | null;
|
|
7
|
+
startScanning: () => Promise<void>;
|
|
8
|
+
setTargetTemp: (temp: number) => Promise<void>;
|
|
9
|
+
setTemperatureUnit: (unit: TemperatureUnit) => Promise<void>;
|
|
10
|
+
setLedColor: (color: RGBColor) => Promise<void>;
|
|
11
|
+
disconnect: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function useMug(): UseMugReturn;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { getBluetoothManager } from '../lib/bluetooth.js';
|
|
3
|
+
import { LiquidState, TemperatureUnit } from '../lib/types.js';
|
|
4
|
+
const initialState = {
|
|
5
|
+
connected: false,
|
|
6
|
+
batteryLevel: 0,
|
|
7
|
+
isCharging: false,
|
|
8
|
+
currentTemp: 0,
|
|
9
|
+
targetTemp: 55,
|
|
10
|
+
liquidState: LiquidState.Empty,
|
|
11
|
+
temperatureUnit: TemperatureUnit.Celsius,
|
|
12
|
+
color: { r: 255, g: 147, b: 41, a: 255 },
|
|
13
|
+
mugName: '',
|
|
14
|
+
};
|
|
15
|
+
export function useMug() {
|
|
16
|
+
const [state, setState] = useState(initialState);
|
|
17
|
+
const [isScanning, setIsScanning] = useState(false);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [foundMugName, setFoundMugName] = useState(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const manager = getBluetoothManager();
|
|
22
|
+
const handleStateChange = (newState) => {
|
|
23
|
+
setState(newState);
|
|
24
|
+
};
|
|
25
|
+
const handleScanning = (scanning) => {
|
|
26
|
+
setIsScanning(scanning);
|
|
27
|
+
};
|
|
28
|
+
const handleError = (err) => {
|
|
29
|
+
setError(err.message);
|
|
30
|
+
};
|
|
31
|
+
const handleMugFound = (name) => {
|
|
32
|
+
setFoundMugName(name);
|
|
33
|
+
};
|
|
34
|
+
const handleConnected = () => {
|
|
35
|
+
setError(null);
|
|
36
|
+
};
|
|
37
|
+
const handleDisconnected = () => {
|
|
38
|
+
setFoundMugName(null);
|
|
39
|
+
};
|
|
40
|
+
manager.on('stateChange', handleStateChange);
|
|
41
|
+
manager.on('scanning', handleScanning);
|
|
42
|
+
manager.on('error', handleError);
|
|
43
|
+
manager.on('mugFound', handleMugFound);
|
|
44
|
+
manager.on('connected', handleConnected);
|
|
45
|
+
manager.on('disconnected', handleDisconnected);
|
|
46
|
+
return () => {
|
|
47
|
+
manager.off('stateChange', handleStateChange);
|
|
48
|
+
manager.off('scanning', handleScanning);
|
|
49
|
+
manager.off('error', handleError);
|
|
50
|
+
manager.off('mugFound', handleMugFound);
|
|
51
|
+
manager.off('connected', handleConnected);
|
|
52
|
+
manager.off('disconnected', handleDisconnected);
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
const startScanning = useCallback(async () => {
|
|
56
|
+
setError(null);
|
|
57
|
+
const manager = getBluetoothManager();
|
|
58
|
+
try {
|
|
59
|
+
await manager.startScanning();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : 'Failed to start scanning');
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
const setTargetTemp = useCallback(async (temp) => {
|
|
66
|
+
const manager = getBluetoothManager();
|
|
67
|
+
try {
|
|
68
|
+
await manager.setTargetTemp(temp);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
setError(err instanceof Error ? err.message : 'Failed to set temperature');
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
const setTemperatureUnit = useCallback(async (unit) => {
|
|
75
|
+
const manager = getBluetoothManager();
|
|
76
|
+
try {
|
|
77
|
+
await manager.setTemperatureUnit(unit);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
setError(err instanceof Error ? err.message : 'Failed to set temperature unit');
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
const setLedColor = useCallback(async (color) => {
|
|
84
|
+
const manager = getBluetoothManager();
|
|
85
|
+
try {
|
|
86
|
+
await manager.setLedColor(color);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
setError(err instanceof Error ? err.message : 'Failed to set LED color');
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
const disconnect = useCallback(async () => {
|
|
93
|
+
const manager = getBluetoothManager();
|
|
94
|
+
try {
|
|
95
|
+
await manager.disconnect();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
setError(err instanceof Error ? err.message : 'Failed to disconnect');
|
|
99
|
+
}
|
|
100
|
+
}, []);
|
|
101
|
+
return {
|
|
102
|
+
state,
|
|
103
|
+
isScanning,
|
|
104
|
+
error,
|
|
105
|
+
foundMugName,
|
|
106
|
+
startScanning,
|
|
107
|
+
setTargetTemp,
|
|
108
|
+
setTemperatureUnit,
|
|
109
|
+
setLedColor,
|
|
110
|
+
disconnect,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { MugState, TemperatureUnit, RGBColor } from './types.js';
|
|
3
|
+
export interface BluetoothManagerEvents {
|
|
4
|
+
stateChange: (state: MugState) => void;
|
|
5
|
+
connected: () => void;
|
|
6
|
+
disconnected: () => void;
|
|
7
|
+
scanning: (isScanning: boolean) => void;
|
|
8
|
+
error: (error: Error) => void;
|
|
9
|
+
mugFound: (name: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare class BluetoothManager extends EventEmitter {
|
|
12
|
+
private peripheral;
|
|
13
|
+
private characteristics;
|
|
14
|
+
private isConnected;
|
|
15
|
+
private pollInterval;
|
|
16
|
+
private state;
|
|
17
|
+
constructor();
|
|
18
|
+
private setupNobleListeners;
|
|
19
|
+
startScanning(): Promise<void>;
|
|
20
|
+
stopScanning(): Promise<void>;
|
|
21
|
+
private connect;
|
|
22
|
+
private discoverCharacteristics;
|
|
23
|
+
private setupNotifications;
|
|
24
|
+
private handlePushEvent;
|
|
25
|
+
private readInitialValues;
|
|
26
|
+
private startPolling;
|
|
27
|
+
private handleDisconnect;
|
|
28
|
+
private readCharacteristic;
|
|
29
|
+
private writeCharacteristic;
|
|
30
|
+
private readCurrentTemp;
|
|
31
|
+
private readTargetTemp;
|
|
32
|
+
private readBattery;
|
|
33
|
+
private readLiquidState;
|
|
34
|
+
private readTemperatureUnit;
|
|
35
|
+
private readLedColor;
|
|
36
|
+
setTargetTemp(temp: number): Promise<void>;
|
|
37
|
+
setTemperatureUnit(unit: TemperatureUnit): Promise<void>;
|
|
38
|
+
setLedColor(color: RGBColor): Promise<void>;
|
|
39
|
+
getState(): MugState;
|
|
40
|
+
private emitState;
|
|
41
|
+
disconnect(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export declare function getBluetoothManager(): BluetoothManager;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import noble from '@abandonware/noble';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { LiquidState, TemperatureUnit, EMBER_SERVICE_UUID, EMBER_CHARACTERISTICS, MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS, } from './types.js';
|
|
4
|
+
// Noble's state property exists but isn't properly typed
|
|
5
|
+
const getNobleState = () => noble.state;
|
|
6
|
+
export class BluetoothManager extends EventEmitter {
|
|
7
|
+
peripheral = null;
|
|
8
|
+
characteristics = new Map();
|
|
9
|
+
isConnected = false;
|
|
10
|
+
pollInterval = null;
|
|
11
|
+
state = {
|
|
12
|
+
connected: false,
|
|
13
|
+
batteryLevel: 0,
|
|
14
|
+
isCharging: false,
|
|
15
|
+
currentTemp: 0,
|
|
16
|
+
targetTemp: 0,
|
|
17
|
+
liquidState: LiquidState.Empty,
|
|
18
|
+
temperatureUnit: TemperatureUnit.Celsius,
|
|
19
|
+
color: { r: 255, g: 255, b: 255, a: 255 },
|
|
20
|
+
mugName: '',
|
|
21
|
+
};
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.setupNobleListeners();
|
|
25
|
+
}
|
|
26
|
+
setupNobleListeners() {
|
|
27
|
+
noble.on('stateChange', (state) => {
|
|
28
|
+
if (state === 'poweredOn') {
|
|
29
|
+
// Ready for scanning
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.emit('error', new Error(`Bluetooth state: ${state}`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
noble.on('discover', async (peripheral) => {
|
|
36
|
+
const name = peripheral.advertisement.localName || '';
|
|
37
|
+
if (name.toLowerCase().includes('ember')) {
|
|
38
|
+
this.emit('mugFound', name);
|
|
39
|
+
await this.stopScanning();
|
|
40
|
+
this.peripheral = peripheral;
|
|
41
|
+
await this.connect();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async startScanning() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
if (getNobleState() === 'poweredOn') {
|
|
48
|
+
this.emit('scanning', true);
|
|
49
|
+
noble.startScanning([EMBER_SERVICE_UUID], false, (error) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
reject(error);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
noble.once('stateChange', (state) => {
|
|
60
|
+
if (state === 'poweredOn') {
|
|
61
|
+
this.emit('scanning', true);
|
|
62
|
+
noble.startScanning([EMBER_SERVICE_UUID], false, (error) => {
|
|
63
|
+
if (error) {
|
|
64
|
+
reject(error);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
resolve();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
reject(new Error(`Bluetooth not available: ${state}`));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async stopScanning() {
|
|
79
|
+
this.emit('scanning', false);
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
noble.stopScanning(() => resolve());
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async connect() {
|
|
85
|
+
if (!this.peripheral) {
|
|
86
|
+
throw new Error('No peripheral to connect to');
|
|
87
|
+
}
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
this.peripheral.connect(async (error) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.isConnected = true;
|
|
95
|
+
this.state.connected = true;
|
|
96
|
+
this.state.mugName = this.peripheral.advertisement.localName || 'Ember Mug';
|
|
97
|
+
this.peripheral.once('disconnect', () => {
|
|
98
|
+
this.handleDisconnect();
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
await this.discoverCharacteristics();
|
|
102
|
+
await this.setupNotifications();
|
|
103
|
+
await this.readInitialValues();
|
|
104
|
+
this.startPolling();
|
|
105
|
+
this.emit('connected');
|
|
106
|
+
this.emitState();
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
reject(err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async discoverCharacteristics() {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
this.peripheral.discoverAllServicesAndCharacteristics((error, services, characteristics) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
reject(error);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const char of characteristics || []) {
|
|
123
|
+
const uuid = char.uuid.toLowerCase().replace(/-/g, '');
|
|
124
|
+
this.characteristics.set(uuid, char);
|
|
125
|
+
}
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async setupNotifications() {
|
|
131
|
+
const pushEventsChar = this.characteristics.get(EMBER_CHARACTERISTICS.PUSH_EVENTS);
|
|
132
|
+
if (pushEventsChar) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
pushEventsChar.subscribe((error) => {
|
|
135
|
+
if (error) {
|
|
136
|
+
reject(error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
pushEventsChar.on('data', (data) => {
|
|
140
|
+
this.handlePushEvent(data);
|
|
141
|
+
});
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
handlePushEvent(data) {
|
|
148
|
+
const eventType = data[0];
|
|
149
|
+
switch (eventType) {
|
|
150
|
+
case 1: // Battery changed
|
|
151
|
+
this.readBattery();
|
|
152
|
+
break;
|
|
153
|
+
case 2: // Started charging
|
|
154
|
+
this.state.isCharging = true;
|
|
155
|
+
this.emitState();
|
|
156
|
+
break;
|
|
157
|
+
case 3: // Stopped charging
|
|
158
|
+
this.state.isCharging = false;
|
|
159
|
+
this.emitState();
|
|
160
|
+
break;
|
|
161
|
+
case 4: // Target temp changed
|
|
162
|
+
this.readTargetTemp();
|
|
163
|
+
break;
|
|
164
|
+
case 5: // Current temp changed
|
|
165
|
+
this.readCurrentTemp();
|
|
166
|
+
break;
|
|
167
|
+
case 8: // Liquid state changed
|
|
168
|
+
this.readLiquidState();
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async readInitialValues() {
|
|
173
|
+
await Promise.all([
|
|
174
|
+
this.readCurrentTemp(),
|
|
175
|
+
this.readTargetTemp(),
|
|
176
|
+
this.readBattery(),
|
|
177
|
+
this.readLiquidState(),
|
|
178
|
+
this.readTemperatureUnit(),
|
|
179
|
+
this.readLedColor(),
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
startPolling() {
|
|
183
|
+
// Poll temperature every 2 seconds
|
|
184
|
+
this.pollInterval = setInterval(async () => {
|
|
185
|
+
if (this.isConnected) {
|
|
186
|
+
await this.readCurrentTemp();
|
|
187
|
+
await this.readLiquidState();
|
|
188
|
+
}
|
|
189
|
+
}, 2000);
|
|
190
|
+
}
|
|
191
|
+
handleDisconnect() {
|
|
192
|
+
this.isConnected = false;
|
|
193
|
+
this.state.connected = false;
|
|
194
|
+
if (this.pollInterval) {
|
|
195
|
+
clearInterval(this.pollInterval);
|
|
196
|
+
this.pollInterval = null;
|
|
197
|
+
}
|
|
198
|
+
this.characteristics.clear();
|
|
199
|
+
this.emit('disconnected');
|
|
200
|
+
this.emitState();
|
|
201
|
+
}
|
|
202
|
+
async readCharacteristic(uuid) {
|
|
203
|
+
const char = this.characteristics.get(uuid);
|
|
204
|
+
if (!char)
|
|
205
|
+
return null;
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
char.read((error, data) => {
|
|
208
|
+
if (error) {
|
|
209
|
+
resolve(null);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
resolve(data);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async writeCharacteristic(uuid, data) {
|
|
218
|
+
const char = this.characteristics.get(uuid);
|
|
219
|
+
if (!char)
|
|
220
|
+
return;
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
char.write(data, false, (error) => {
|
|
223
|
+
if (error) {
|
|
224
|
+
reject(error);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
resolve();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async readCurrentTemp() {
|
|
233
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.CURRENT_TEMP);
|
|
234
|
+
if (data && data.length >= 2) {
|
|
235
|
+
const temp = data.readUInt16LE(0) * 0.01;
|
|
236
|
+
this.state.currentTemp = temp;
|
|
237
|
+
this.emitState();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async readTargetTemp() {
|
|
241
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.TARGET_TEMP);
|
|
242
|
+
if (data && data.length >= 2) {
|
|
243
|
+
const temp = data.readUInt16LE(0) * 0.01;
|
|
244
|
+
this.state.targetTemp = temp;
|
|
245
|
+
this.emitState();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async readBattery() {
|
|
249
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.BATTERY);
|
|
250
|
+
if (data && data.length >= 2) {
|
|
251
|
+
this.state.batteryLevel = data[0];
|
|
252
|
+
this.state.isCharging = data[1] === 1;
|
|
253
|
+
this.emitState();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async readLiquidState() {
|
|
257
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.LIQUID_STATE);
|
|
258
|
+
if (data && data.length >= 1) {
|
|
259
|
+
const state = data[0];
|
|
260
|
+
if (Object.values(LiquidState).includes(state)) {
|
|
261
|
+
this.state.liquidState = state;
|
|
262
|
+
this.emitState();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async readTemperatureUnit() {
|
|
267
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.TEMP_UNIT);
|
|
268
|
+
if (data && data.length >= 1) {
|
|
269
|
+
this.state.temperatureUnit = data[0];
|
|
270
|
+
this.emitState();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async readLedColor() {
|
|
274
|
+
const data = await this.readCharacteristic(EMBER_CHARACTERISTICS.LED_COLOR);
|
|
275
|
+
if (data && data.length >= 4) {
|
|
276
|
+
this.state.color = {
|
|
277
|
+
r: data[0],
|
|
278
|
+
g: data[1],
|
|
279
|
+
b: data[2],
|
|
280
|
+
a: data[3],
|
|
281
|
+
};
|
|
282
|
+
this.emitState();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async setTargetTemp(temp) {
|
|
286
|
+
const clampedTemp = Math.max(MIN_TEMP_CELSIUS, Math.min(MAX_TEMP_CELSIUS, temp));
|
|
287
|
+
const value = Math.round(clampedTemp * 100);
|
|
288
|
+
const buffer = Buffer.alloc(2);
|
|
289
|
+
buffer.writeUInt16LE(value, 0);
|
|
290
|
+
await this.writeCharacteristic(EMBER_CHARACTERISTICS.TARGET_TEMP, buffer);
|
|
291
|
+
this.state.targetTemp = clampedTemp;
|
|
292
|
+
this.emitState();
|
|
293
|
+
}
|
|
294
|
+
async setTemperatureUnit(unit) {
|
|
295
|
+
const buffer = Buffer.from([unit]);
|
|
296
|
+
await this.writeCharacteristic(EMBER_CHARACTERISTICS.TEMP_UNIT, buffer);
|
|
297
|
+
this.state.temperatureUnit = unit;
|
|
298
|
+
this.emitState();
|
|
299
|
+
}
|
|
300
|
+
async setLedColor(color) {
|
|
301
|
+
const buffer = Buffer.from([color.r, color.g, color.b, color.a]);
|
|
302
|
+
await this.writeCharacteristic(EMBER_CHARACTERISTICS.LED_COLOR, buffer);
|
|
303
|
+
this.state.color = color;
|
|
304
|
+
this.emitState();
|
|
305
|
+
}
|
|
306
|
+
getState() {
|
|
307
|
+
return { ...this.state };
|
|
308
|
+
}
|
|
309
|
+
emitState() {
|
|
310
|
+
this.emit('stateChange', this.getState());
|
|
311
|
+
}
|
|
312
|
+
async disconnect() {
|
|
313
|
+
if (this.pollInterval) {
|
|
314
|
+
clearInterval(this.pollInterval);
|
|
315
|
+
this.pollInterval = null;
|
|
316
|
+
}
|
|
317
|
+
if (this.peripheral && this.isConnected) {
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
this.peripheral.disconnect(() => {
|
|
320
|
+
this.handleDisconnect();
|
|
321
|
+
resolve();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Singleton instance
|
|
328
|
+
let instance = null;
|
|
329
|
+
export function getBluetoothManager() {
|
|
330
|
+
if (!instance) {
|
|
331
|
+
instance = new BluetoothManager();
|
|
332
|
+
}
|
|
333
|
+
return instance;
|
|
334
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AppSettings, Preset, TemperatureUnit } from './types.js';
|
|
2
|
+
export declare function getSettings(): AppSettings;
|
|
3
|
+
export declare function getPresets(): Preset[];
|
|
4
|
+
export declare function setPresets(presets: Preset[]): void;
|
|
5
|
+
export declare function addPreset(preset: Preset): void;
|
|
6
|
+
export declare function removePreset(id: string): void;
|
|
7
|
+
export declare function updatePreset(id: string, updates: Partial<Preset>): void;
|
|
8
|
+
export declare function getTemperatureUnit(): TemperatureUnit;
|
|
9
|
+
export declare function setTemperatureUnit(unit: TemperatureUnit): void;
|
|
10
|
+
export declare function getLastTargetTemp(): number;
|
|
11
|
+
export declare function setLastTargetTemp(temp: number): void;
|
|
12
|
+
export declare function getLedColor(): {
|
|
13
|
+
r: number;
|
|
14
|
+
g: number;
|
|
15
|
+
b: number;
|
|
16
|
+
a: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function setLedColor(color: {
|
|
19
|
+
r: number;
|
|
20
|
+
g: number;
|
|
21
|
+
b: number;
|
|
22
|
+
a: number;
|
|
23
|
+
}): void;
|
|
24
|
+
export declare function getNotifyOnTemperatureReached(): boolean;
|
|
25
|
+
export declare function setNotifyOnTemperatureReached(enabled: boolean): void;
|
|
26
|
+
export declare function getNotifyAtBatteryPercentage(): number;
|
|
27
|
+
export declare function setNotifyAtBatteryPercentage(percentage: number): void;
|
|
28
|
+
export declare function resetSettings(): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import { TemperatureUnit, DEFAULT_PRESETS, } from './types.js';
|
|
3
|
+
const config = new Conf({
|
|
4
|
+
projectName: 'ember-mug-cli',
|
|
5
|
+
defaults: {
|
|
6
|
+
presets: DEFAULT_PRESETS,
|
|
7
|
+
temperatureUnit: TemperatureUnit.Celsius,
|
|
8
|
+
notifyOnTemperatureReached: true,
|
|
9
|
+
notifyAtBatteryPercentage: 15,
|
|
10
|
+
lastTargetTemp: 55,
|
|
11
|
+
ledColor: { r: 255, g: 147, b: 41, a: 255 }, // Warm orange
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
export function getSettings() {
|
|
15
|
+
return {
|
|
16
|
+
presets: config.get('presets'),
|
|
17
|
+
temperatureUnit: config.get('temperatureUnit'),
|
|
18
|
+
notifyOnTemperatureReached: config.get('notifyOnTemperatureReached'),
|
|
19
|
+
notifyAtBatteryPercentage: config.get('notifyAtBatteryPercentage'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getPresets() {
|
|
23
|
+
return config.get('presets');
|
|
24
|
+
}
|
|
25
|
+
export function setPresets(presets) {
|
|
26
|
+
config.set('presets', presets);
|
|
27
|
+
}
|
|
28
|
+
export function addPreset(preset) {
|
|
29
|
+
const presets = getPresets();
|
|
30
|
+
presets.push(preset);
|
|
31
|
+
setPresets(presets);
|
|
32
|
+
}
|
|
33
|
+
export function removePreset(id) {
|
|
34
|
+
const presets = getPresets().filter((p) => p.id !== id);
|
|
35
|
+
setPresets(presets);
|
|
36
|
+
}
|
|
37
|
+
export function updatePreset(id, updates) {
|
|
38
|
+
const presets = getPresets().map((p) => p.id === id ? { ...p, ...updates } : p);
|
|
39
|
+
setPresets(presets);
|
|
40
|
+
}
|
|
41
|
+
export function getTemperatureUnit() {
|
|
42
|
+
return config.get('temperatureUnit');
|
|
43
|
+
}
|
|
44
|
+
export function setTemperatureUnit(unit) {
|
|
45
|
+
config.set('temperatureUnit', unit);
|
|
46
|
+
}
|
|
47
|
+
export function getLastTargetTemp() {
|
|
48
|
+
return config.get('lastTargetTemp');
|
|
49
|
+
}
|
|
50
|
+
export function setLastTargetTemp(temp) {
|
|
51
|
+
config.set('lastTargetTemp', temp);
|
|
52
|
+
}
|
|
53
|
+
export function getLedColor() {
|
|
54
|
+
return config.get('ledColor');
|
|
55
|
+
}
|
|
56
|
+
export function setLedColor(color) {
|
|
57
|
+
config.set('ledColor', color);
|
|
58
|
+
}
|
|
59
|
+
export function getNotifyOnTemperatureReached() {
|
|
60
|
+
return config.get('notifyOnTemperatureReached');
|
|
61
|
+
}
|
|
62
|
+
export function setNotifyOnTemperatureReached(enabled) {
|
|
63
|
+
config.set('notifyOnTemperatureReached', enabled);
|
|
64
|
+
}
|
|
65
|
+
export function getNotifyAtBatteryPercentage() {
|
|
66
|
+
return config.get('notifyAtBatteryPercentage');
|
|
67
|
+
}
|
|
68
|
+
export function setNotifyAtBatteryPercentage(percentage) {
|
|
69
|
+
config.set('notifyAtBatteryPercentage', percentage);
|
|
70
|
+
}
|
|
71
|
+
export function resetSettings() {
|
|
72
|
+
config.clear();
|
|
73
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare enum LiquidState {
|
|
2
|
+
Empty = 1,
|
|
3
|
+
Filling = 2,
|
|
4
|
+
Cooling = 4,
|
|
5
|
+
Heating = 5,
|
|
6
|
+
StableTemperature = 6
|
|
7
|
+
}
|
|
8
|
+
export declare enum TemperatureUnit {
|
|
9
|
+
Celsius = 0,
|
|
10
|
+
Fahrenheit = 1
|
|
11
|
+
}
|
|
12
|
+
export interface MugState {
|
|
13
|
+
connected: boolean;
|
|
14
|
+
batteryLevel: number;
|
|
15
|
+
isCharging: boolean;
|
|
16
|
+
currentTemp: number;
|
|
17
|
+
targetTemp: number;
|
|
18
|
+
liquidState: LiquidState;
|
|
19
|
+
temperatureUnit: TemperatureUnit;
|
|
20
|
+
color: RGBColor;
|
|
21
|
+
mugName: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RGBColor {
|
|
24
|
+
r: number;
|
|
25
|
+
g: number;
|
|
26
|
+
b: number;
|
|
27
|
+
a: number;
|
|
28
|
+
}
|
|
29
|
+
export interface Preset {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
icon: string;
|
|
33
|
+
temperature: number;
|
|
34
|
+
}
|
|
35
|
+
export interface AppSettings {
|
|
36
|
+
presets: Preset[];
|
|
37
|
+
temperatureUnit: TemperatureUnit;
|
|
38
|
+
notifyOnTemperatureReached: boolean;
|
|
39
|
+
notifyAtBatteryPercentage: number;
|
|
40
|
+
}
|
|
41
|
+
export declare const DEFAULT_PRESETS: Preset[];
|
|
42
|
+
export declare const EMBER_SERVICE_UUID = "fc543622236c4c948fa9944a3e5353fa";
|
|
43
|
+
export declare const EMBER_CHARACTERISTICS: {
|
|
44
|
+
readonly MUG_NAME: "fc540001236c4c948fa9944a3e5353fa";
|
|
45
|
+
readonly CURRENT_TEMP: "fc540002236c4c948fa9944a3e5353fa";
|
|
46
|
+
readonly TARGET_TEMP: "fc540003236c4c948fa9944a3e5353fa";
|
|
47
|
+
readonly TEMP_UNIT: "fc540004236c4c948fa9944a3e5353fa";
|
|
48
|
+
readonly BATTERY: "fc540007236c4c948fa9944a3e5353fa";
|
|
49
|
+
readonly LIQUID_STATE: "fc540008236c4c948fa9944a3e5353fa";
|
|
50
|
+
readonly PUSH_EVENTS: "fc540012236c4c948fa9944a3e5353fa";
|
|
51
|
+
readonly LED_COLOR: "fc540014236c4c948fa9944a3e5353fa";
|
|
52
|
+
};
|
|
53
|
+
export declare const MIN_TEMP_CELSIUS = 50;
|
|
54
|
+
export declare const MAX_TEMP_CELSIUS = 63;
|
|
55
|
+
export declare const BATTERY_DRAIN_RATE_HEATING = 0.5;
|
|
56
|
+
export declare const BATTERY_DRAIN_RATE_MAINTAINING = 0.2;
|
|
57
|
+
export declare const BATTERY_CHARGE_RATE = 1.5;
|