ember-mug 0.1.3 → 0.1.4
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/dist/cli.js +37 -7
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +64 -53
- package/dist/components/BatteryDisplay.d.ts +8 -3
- package/dist/components/BatteryDisplay.js +7 -18
- package/dist/components/ConnectionStatus.d.ts +7 -2
- package/dist/components/ConnectionStatus.js +5 -4
- package/dist/components/Header.d.ts +7 -2
- package/dist/components/Header.js +11 -3
- package/dist/components/HelpDisplay.d.ts +5 -2
- package/dist/components/HelpDisplay.js +8 -3
- package/dist/components/HorizontalRule.d.ts +2 -0
- package/dist/components/HorizontalRule.js +7 -0
- package/dist/components/Panel.d.ts +18 -0
- package/dist/components/Panel.js +57 -0
- package/dist/components/Presets.d.ts +8 -3
- package/dist/components/Presets.js +13 -8
- package/dist/components/SettingsView.d.ts +9 -3
- package/dist/components/SettingsView.js +82 -16
- package/dist/components/TemperatureControl.d.ts +8 -3
- package/dist/components/TemperatureControl.js +13 -18
- package/dist/components/TemperatureDisplay.d.ts +8 -3
- package/dist/components/TemperatureDisplay.js +8 -35
- package/dist/hooks/useMug.d.ts +1 -1
- package/dist/hooks/useMug.js +22 -22
- package/dist/lib/bluetooth.d.ts +2 -0
- package/dist/lib/bluetooth.js +8 -0
- package/dist/lib/mock-bluetooth.d.ts +65 -0
- package/dist/lib/mock-bluetooth.js +214 -0
- package/dist/lib/settings.d.ts +1 -1
- package/dist/lib/settings.js +20 -20
- package/dist/lib/theme.d.ts +135 -0
- package/dist/lib/theme.js +112 -0
- package/dist/lib/types.d.ts +0 -1
- package/dist/lib/types.js +12 -12
- package/dist/lib/utils.d.ts +1 -2
- package/dist/lib/utils.js +19 -35
- package/package.json +2 -1
- package/dist/components/ColorControl.d.ts +0 -9
- package/dist/components/ColorControl.js +0 -71
|
@@ -1,34 +1,100 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from
|
|
3
|
-
import { Box, Text, useInput } from
|
|
4
|
-
import { TemperatureUnit } from
|
|
5
|
-
import { formatTemperature } from
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import { TemperatureUnit } from "../lib/types.js";
|
|
5
|
+
import { formatTemperature, rgbToHex } from "../lib/utils.js";
|
|
6
|
+
import { Panel } from "./Panel.js";
|
|
7
|
+
const PRESET_COLORS = [
|
|
8
|
+
{ name: "Orange", color: { r: 255, g: 147, b: 41, a: 255 } },
|
|
9
|
+
{ name: "Red", color: { r: 255, g: 0, b: 0, a: 255 } },
|
|
10
|
+
{ name: "Green", color: { r: 0, g: 255, b: 0, a: 255 } },
|
|
11
|
+
{ name: "Blue", color: { r: 0, g: 128, b: 255, a: 255 } },
|
|
12
|
+
{ name: "Purple", color: { r: 128, g: 0, b: 255, a: 255 } },
|
|
13
|
+
{ name: "Pink", color: { r: 255, g: 105, b: 180, a: 255 } },
|
|
14
|
+
{ name: "White", color: { r: 255, g: 255, b: 255, a: 255 } },
|
|
15
|
+
{ name: "Teal", color: { r: 0, g: 255, b: 200, a: 255 } },
|
|
16
|
+
];
|
|
17
|
+
export function SettingsView({ presets, temperatureUnit, ledColor, onTemperatureUnitChange, onPresetUpdate, onColorChange, onClose, isActive, theme, }) {
|
|
18
|
+
// 0 = Unit Toggle, 1 = LED Color, 2...N = Presets
|
|
19
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
20
|
+
// Calculate total items: 1 (unit) + 1 (color) + presets count
|
|
21
|
+
const totalItems = 2 + presets.length;
|
|
22
|
+
const { stdout } = useStdout();
|
|
23
|
+
const terminalWidth = stdout?.columns || 80;
|
|
24
|
+
const panelWidth = Math.floor(terminalWidth * 0.6); // 60% of terminal width
|
|
8
25
|
useInput((input, key) => {
|
|
9
26
|
if (!isActive)
|
|
10
27
|
return;
|
|
11
|
-
if (key.escape || input ===
|
|
28
|
+
if (key.escape || input === "q") {
|
|
12
29
|
onClose();
|
|
13
30
|
return;
|
|
14
31
|
}
|
|
15
|
-
if (key.upArrow || input ===
|
|
16
|
-
|
|
32
|
+
if (key.upArrow || input === "k") {
|
|
33
|
+
setSelectedIndex((current) => Math.max(0, current - 1));
|
|
17
34
|
}
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
if (key.downArrow || input === "j") {
|
|
36
|
+
setSelectedIndex((current) => Math.min(totalItems - 1, current + 1));
|
|
20
37
|
}
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
// Toggle Unit
|
|
39
|
+
if (selectedIndex === 0) {
|
|
40
|
+
if (key.return || input === " ") {
|
|
23
41
|
const newUnit = temperatureUnit === TemperatureUnit.Celsius
|
|
24
42
|
? TemperatureUnit.Fahrenheit
|
|
25
43
|
: TemperatureUnit.Celsius;
|
|
26
44
|
onTemperatureUnitChange(newUnit);
|
|
27
45
|
}
|
|
28
46
|
}
|
|
47
|
+
else if (selectedIndex === 1) {
|
|
48
|
+
// LED Color
|
|
49
|
+
if (key.leftArrow || input === "h" || key.rightArrow || input === "l") {
|
|
50
|
+
const currentColorIndex = PRESET_COLORS.findIndex((p) => p.color.r === ledColor.r &&
|
|
51
|
+
p.color.g === ledColor.g &&
|
|
52
|
+
p.color.b === ledColor.b);
|
|
53
|
+
let nextIndex = currentColorIndex;
|
|
54
|
+
if (key.leftArrow || input === "h") {
|
|
55
|
+
nextIndex = currentColorIndex - 1;
|
|
56
|
+
if (nextIndex < 0)
|
|
57
|
+
nextIndex = PRESET_COLORS.length - 1;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
nextIndex = currentColorIndex + 1;
|
|
61
|
+
if (nextIndex >= PRESET_COLORS.length)
|
|
62
|
+
nextIndex = 0;
|
|
63
|
+
}
|
|
64
|
+
onColorChange(PRESET_COLORS[nextIndex].color);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Edit Preset
|
|
69
|
+
const presetIndex = selectedIndex - 2;
|
|
70
|
+
const preset = presets[presetIndex];
|
|
71
|
+
let delta = 0;
|
|
72
|
+
if (key.leftArrow || input === "h") {
|
|
73
|
+
delta = -0.5;
|
|
74
|
+
}
|
|
75
|
+
else if (key.rightArrow || input === "l") {
|
|
76
|
+
delta = 0.5;
|
|
77
|
+
}
|
|
78
|
+
if (delta !== 0 && preset) {
|
|
79
|
+
onPresetUpdate({
|
|
80
|
+
...preset,
|
|
81
|
+
temperature: preset.temperature + delta,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
29
85
|
}, { isActive });
|
|
30
|
-
|
|
86
|
+
const currentColorPreset = PRESET_COLORS.find((p) => p.color.r === ledColor.r &&
|
|
87
|
+
p.color.g === ledColor.g &&
|
|
88
|
+
p.color.b === ledColor.b);
|
|
89
|
+
const colorDisplayValue = currentColorPreset
|
|
90
|
+
? currentColorPreset.name
|
|
91
|
+
: rgbToHex(ledColor.r, ledColor.g, ledColor.b);
|
|
92
|
+
return (_jsx(Box, { justifyContent: "center", marginY: 1, children: _jsxs(Panel, { title: "[=] Settings", titleColor: theme.primary, borderColor: theme.border, width: panelWidth, height: 16, children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(SettingRow, { label: "Temperature Unit", value: temperatureUnit === TemperatureUnit.Celsius
|
|
93
|
+
? "Celsius (°C)"
|
|
94
|
+
: "Fahrenheit (°F)", isSelected: selectedIndex === 0, hint: "Enter/Space to toggle", theme: theme }), _jsx(SettingRow, { label: "LED Color", value: colorDisplayValue, valueColor: rgbToHex(ledColor.r, ledColor.g, ledColor.b), isSelected: selectedIndex === 1, hint: "Left/Right to change", theme: theme }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.primary, bold: true, children: "Presets:" }), presets.map((preset, index) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dimText, children: [_jsxs(Text, { color: selectedIndex === index + 2
|
|
95
|
+
? theme.primary
|
|
96
|
+
: theme.dimText, children: [selectedIndex === index + 2 ? "> " : " ", index + 1, "."] }), " ", preset.name, ":", " ", _jsx(Text, { color: selectedIndex === index + 2 ? theme.text : theme.dimText, bold: selectedIndex === index + 2, children: formatTemperature(preset.temperature, temperatureUnit) }), selectedIndex === index + 2 && (_jsx(Text, { dimColor: true, children: " (Left/Right to edit)" }))] }) }, preset.id)))] })] }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsxs(Text, { color: theme.dimText, children: [_jsx(Text, { color: theme.primary, bold: true, children: "[Esc]" }), " ", "or", " ", _jsx(Text, { color: theme.primary, bold: true, children: "[q]" }), " ", "to close"] }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.dimText, dimColor: true, children: "(Use Arrow Keys to navigate and edit)" }) })] }) }));
|
|
31
97
|
}
|
|
32
|
-
function SettingRow({ label, value, isSelected, hint, }) {
|
|
33
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ?
|
|
98
|
+
function SettingRow({ label, value, valueColor, isSelected, hint, theme, }) {
|
|
99
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.primary : theme.text, children: [isSelected ? "> " : " ", label, ":", " ", _jsx(Text, { bold: true, color: valueColor, children: value }), hint && isSelected && _jsxs(Text, { dimColor: true, children: [" (", hint, ")"] })] }) }));
|
|
34
100
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { TemperatureUnit } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TemperatureUnit } from "../lib/types.js";
|
|
3
|
+
import { TERMINAL_COLORS } from "../lib/theme.js";
|
|
4
|
+
type TerminalTheme = (typeof TERMINAL_COLORS)[keyof typeof TERMINAL_COLORS];
|
|
3
5
|
interface TemperatureControlProps {
|
|
4
6
|
targetTemp: number;
|
|
5
7
|
temperatureUnit: TemperatureUnit;
|
|
6
8
|
onTempChange: (temp: number) => void;
|
|
7
9
|
isActive: boolean;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
theme: TerminalTheme;
|
|
8
13
|
}
|
|
9
|
-
export declare function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, }: TemperatureControlProps): React.ReactElement;
|
|
14
|
+
export declare function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, width, height, theme, }: TemperatureControlProps): React.ReactElement;
|
|
10
15
|
export {};
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from
|
|
3
|
-
import { MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS } from
|
|
4
|
-
import { formatTemperature, clampTemperature } from
|
|
5
|
-
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS, } from "../lib/types.js";
|
|
4
|
+
import { formatTemperature, clampTemperature } from "../lib/utils.js";
|
|
5
|
+
import { Panel } from "./Panel.js";
|
|
6
|
+
export function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, width, height, theme, }) {
|
|
6
7
|
useInput((input, key) => {
|
|
7
8
|
if (!isActive)
|
|
8
9
|
return;
|
|
9
10
|
let delta = 0;
|
|
10
|
-
if (key.leftArrow
|
|
11
|
+
if (key.leftArrow) {
|
|
11
12
|
delta = -0.5;
|
|
12
13
|
}
|
|
13
|
-
else if (key.rightArrow
|
|
14
|
+
else if (key.rightArrow) {
|
|
14
15
|
delta = 0.5;
|
|
15
16
|
}
|
|
16
|
-
else if (key.upArrow || input === 'k') {
|
|
17
|
-
delta = 1;
|
|
18
|
-
}
|
|
19
|
-
else if (key.downArrow || input === 'j') {
|
|
20
|
-
delta = -1;
|
|
21
|
-
}
|
|
22
17
|
if (delta !== 0) {
|
|
23
18
|
const newTemp = clampTemperature(targetTemp + delta);
|
|
24
19
|
onTempChange(newTemp);
|
|
@@ -26,13 +21,13 @@ export function TemperatureControl({ targetTemp, temperatureUnit, onTempChange,
|
|
|
26
21
|
}, { isActive });
|
|
27
22
|
const minTempDisplay = formatTemperature(MIN_TEMP_CELSIUS, temperatureUnit);
|
|
28
23
|
const maxTempDisplay = formatTemperature(MAX_TEMP_CELSIUS, temperatureUnit);
|
|
29
|
-
return (_jsxs(
|
|
24
|
+
return (_jsxs(Panel, { title: "Temperature Adjust", titleColor: theme.primary, borderColor: theme.border, width: width, height: height, children: [_jsxs(Box, { justifyContent: "center", marginY: 1, children: [_jsx(Text, { color: theme.dimText, children: minTempDisplay }), _jsx(Text, { children: " " }), _jsx(TemperatureSlider, { value: targetTemp, min: MIN_TEMP_CELSIUS, max: MAX_TEMP_CELSIUS, isActive: isActive, theme: theme }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.dimText, children: maxTempDisplay })] }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.dimText, children: _jsx(Text, { color: theme.primary, children: "\u2190/\u2192" }) }) })] }));
|
|
30
25
|
}
|
|
31
|
-
function TemperatureSlider({ value, min, max, isActive, }) {
|
|
32
|
-
const totalWidth =
|
|
26
|
+
function TemperatureSlider({ value, min, max, isActive, theme, }) {
|
|
27
|
+
const totalWidth = 16;
|
|
33
28
|
const normalizedValue = (value - min) / (max - min);
|
|
34
29
|
const position = Math.round(normalizedValue * (totalWidth - 1));
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return (_jsx(Text, { color: isActive ?
|
|
30
|
+
const leftPart = "━".repeat(position);
|
|
31
|
+
const rightPart = "━".repeat(totalWidth - position - 1);
|
|
32
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: isActive ? theme.primary : theme.dimText, children: leftPart }), _jsx(Text, { color: theme.text, bold: true, children: "\u25C9" }), _jsx(Text, { color: theme.dimText, children: rightPart })] }));
|
|
38
33
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { LiquidState, TemperatureUnit } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { LiquidState, TemperatureUnit } from "../lib/types.js";
|
|
3
|
+
import { TERMINAL_COLORS } from "../lib/theme.js";
|
|
4
|
+
type TerminalTheme = (typeof TERMINAL_COLORS)[keyof typeof TERMINAL_COLORS];
|
|
3
5
|
interface TemperatureDisplayProps {
|
|
4
6
|
currentTemp: number;
|
|
5
7
|
targetTemp: number;
|
|
6
8
|
liquidState: LiquidState;
|
|
7
9
|
temperatureUnit: TemperatureUnit;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
theme: TerminalTheme;
|
|
8
13
|
}
|
|
9
|
-
export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, }: TemperatureDisplayProps): React.ReactElement;
|
|
14
|
+
export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, width, height, theme, }: TemperatureDisplayProps): React.ReactElement;
|
|
10
15
|
export {};
|
|
@@ -1,40 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import { LiquidState } from
|
|
4
|
-
import { formatTemperature, getLiquidStateText,
|
|
5
|
-
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { LiquidState } from "../lib/types.js";
|
|
4
|
+
import { formatTemperature, getLiquidStateText, estimateTimeToTargetTemp, formatDuration, } from "../lib/utils.js";
|
|
5
|
+
import { Panel } from "./Panel.js";
|
|
6
|
+
export function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, width, height, theme, }) {
|
|
6
7
|
const isEmpty = liquidState === LiquidState.Empty;
|
|
7
8
|
const isAtTarget = Math.abs(currentTemp - targetTemp) < 0.5 && !isEmpty;
|
|
8
|
-
const tempDiff = currentTemp - targetTemp;
|
|
9
|
-
let tempColor;
|
|
10
|
-
if (isEmpty) {
|
|
11
|
-
tempColor = 'gray';
|
|
12
|
-
}
|
|
13
|
-
else if (Math.abs(tempDiff) < 1) {
|
|
14
|
-
tempColor = 'green';
|
|
15
|
-
}
|
|
16
|
-
else if (tempDiff > 0) {
|
|
17
|
-
tempColor = 'red';
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
tempColor = 'blue';
|
|
21
|
-
}
|
|
22
9
|
const timeToTarget = estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState);
|
|
23
|
-
return (_jsxs(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
switch (state) {
|
|
27
|
-
case LiquidState.Empty:
|
|
28
|
-
return 'gray';
|
|
29
|
-
case LiquidState.Filling:
|
|
30
|
-
return 'cyan';
|
|
31
|
-
case LiquidState.Cooling:
|
|
32
|
-
return 'blue';
|
|
33
|
-
case LiquidState.Heating:
|
|
34
|
-
return 'red';
|
|
35
|
-
case LiquidState.StableTemperature:
|
|
36
|
-
return 'green';
|
|
37
|
-
default:
|
|
38
|
-
return 'white';
|
|
39
|
-
}
|
|
10
|
+
return (_jsxs(Panel, { title: "Temperature", titleColor: theme.primary, borderColor: theme.border, width: width, height: height, children: [_jsxs(Box, { marginY: 1, justifyContent: "center", width: "100%", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", minWidth: 16, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { color: theme.dimText, children: "Current" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: theme.primary, children: isEmpty
|
|
11
|
+
? "---"
|
|
12
|
+
: formatTemperature(currentTemp, temperatureUnit) }) })] }), _jsx(Box, { marginX: 2, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: theme.primary, bold: true, children: "-->" }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", minWidth: 16, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { color: theme.dimText, children: "Target" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: theme.text, children: formatTemperature(targetTemp, temperatureUnit) }) })] })] }), _jsx(Box, { justifyContent: "center", children: _jsxs(Text, { children: [_jsx(Text, { color: theme.primary, bold: true, children: getLiquidStateText(liquidState) }), isAtTarget && (_jsxs(Text, { color: theme.primary, bold: true, children: [" ", "*"] })), timeToTarget !== null && timeToTarget > 0 && (_jsxs(Text, { color: theme.dimText, children: [" • [~] ", _jsx(Text, { color: theme.primary, children: formatDuration(timeToTarget) }), _jsx(Text, { color: theme.dimText, children: " to target" })] }))] }) })] }));
|
|
40
13
|
}
|
package/dist/hooks/useMug.d.ts
CHANGED
package/dist/hooks/useMug.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from
|
|
2
|
-
import { getBluetoothManager } from
|
|
3
|
-
import { LiquidState, TemperatureUnit } from
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getBluetoothManager } from "../lib/bluetooth.js";
|
|
3
|
+
import { LiquidState, TemperatureUnit, } from "../lib/types.js";
|
|
4
4
|
const initialState = {
|
|
5
5
|
connected: false,
|
|
6
6
|
batteryLevel: 0,
|
|
@@ -10,11 +10,11 @@ const initialState = {
|
|
|
10
10
|
liquidState: LiquidState.Empty,
|
|
11
11
|
temperatureUnit: TemperatureUnit.Celsius,
|
|
12
12
|
color: { r: 255, g: 147, b: 41, a: 255 },
|
|
13
|
-
mugName:
|
|
13
|
+
mugName: "",
|
|
14
14
|
};
|
|
15
15
|
export function useMug() {
|
|
16
16
|
const [state, setState] = useState(initialState);
|
|
17
|
-
const [isScanning, setIsScanning] = useState(
|
|
17
|
+
const [isScanning, setIsScanning] = useState(true);
|
|
18
18
|
const [error, setError] = useState(null);
|
|
19
19
|
const [foundMugName, setFoundMugName] = useState(null);
|
|
20
20
|
useEffect(() => {
|
|
@@ -37,19 +37,19 @@ export function useMug() {
|
|
|
37
37
|
const handleDisconnected = () => {
|
|
38
38
|
setFoundMugName(null);
|
|
39
39
|
};
|
|
40
|
-
manager.on(
|
|
41
|
-
manager.on(
|
|
42
|
-
manager.on(
|
|
43
|
-
manager.on(
|
|
44
|
-
manager.on(
|
|
45
|
-
manager.on(
|
|
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
46
|
return () => {
|
|
47
|
-
manager.off(
|
|
48
|
-
manager.off(
|
|
49
|
-
manager.off(
|
|
50
|
-
manager.off(
|
|
51
|
-
manager.off(
|
|
52
|
-
manager.off(
|
|
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
53
|
};
|
|
54
54
|
}, []);
|
|
55
55
|
const startScanning = useCallback(async () => {
|
|
@@ -59,7 +59,7 @@ export function useMug() {
|
|
|
59
59
|
await manager.startScanning();
|
|
60
60
|
}
|
|
61
61
|
catch (err) {
|
|
62
|
-
setError(err instanceof Error ? err.message :
|
|
62
|
+
setError(err instanceof Error ? err.message : "Failed to start scanning");
|
|
63
63
|
}
|
|
64
64
|
}, []);
|
|
65
65
|
const setTargetTemp = useCallback(async (temp) => {
|
|
@@ -68,7 +68,7 @@ export function useMug() {
|
|
|
68
68
|
await manager.setTargetTemp(temp);
|
|
69
69
|
}
|
|
70
70
|
catch (err) {
|
|
71
|
-
setError(err instanceof Error ? err.message :
|
|
71
|
+
setError(err instanceof Error ? err.message : "Failed to set temperature");
|
|
72
72
|
}
|
|
73
73
|
}, []);
|
|
74
74
|
const setTemperatureUnit = useCallback(async (unit) => {
|
|
@@ -77,7 +77,7 @@ export function useMug() {
|
|
|
77
77
|
await manager.setTemperatureUnit(unit);
|
|
78
78
|
}
|
|
79
79
|
catch (err) {
|
|
80
|
-
setError(err instanceof Error ? err.message :
|
|
80
|
+
setError(err instanceof Error ? err.message : "Failed to set temperature unit");
|
|
81
81
|
}
|
|
82
82
|
}, []);
|
|
83
83
|
const setLedColor = useCallback(async (color) => {
|
|
@@ -86,7 +86,7 @@ export function useMug() {
|
|
|
86
86
|
await manager.setLedColor(color);
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
89
|
-
setError(err instanceof Error ? err.message :
|
|
89
|
+
setError(err instanceof Error ? err.message : "Failed to set LED color");
|
|
90
90
|
}
|
|
91
91
|
}, []);
|
|
92
92
|
const disconnect = useCallback(async () => {
|
|
@@ -95,7 +95,7 @@ export function useMug() {
|
|
|
95
95
|
await manager.disconnect();
|
|
96
96
|
}
|
|
97
97
|
catch (err) {
|
|
98
|
-
setError(err instanceof Error ? err.message :
|
|
98
|
+
setError(err instanceof Error ? err.message : "Failed to disconnect");
|
|
99
99
|
}
|
|
100
100
|
}, []);
|
|
101
101
|
return {
|
package/dist/lib/bluetooth.d.ts
CHANGED
|
@@ -40,4 +40,6 @@ export declare class BluetoothManager extends EventEmitter {
|
|
|
40
40
|
private emitState;
|
|
41
41
|
disconnect(): Promise<void>;
|
|
42
42
|
}
|
|
43
|
+
export declare function isMockMode(): boolean;
|
|
43
44
|
export declare function getBluetoothManager(): BluetoothManager;
|
|
45
|
+
export declare function setBluetoothManager(manager: BluetoothManager): void;
|
package/dist/lib/bluetooth.js
CHANGED
|
@@ -326,9 +326,17 @@ export class BluetoothManager extends EventEmitter {
|
|
|
326
326
|
}
|
|
327
327
|
// Singleton instance
|
|
328
328
|
let instance = null;
|
|
329
|
+
// Check if mock mode is enabled
|
|
330
|
+
export function isMockMode() {
|
|
331
|
+
return process.env.EMBER_MOCK === 'true' || process.env.EMBER_MOCK === '1';
|
|
332
|
+
}
|
|
329
333
|
export function getBluetoothManager() {
|
|
330
334
|
if (!instance) {
|
|
331
335
|
instance = new BluetoothManager();
|
|
332
336
|
}
|
|
333
337
|
return instance;
|
|
334
338
|
}
|
|
339
|
+
// Set a custom manager (used for mock mode)
|
|
340
|
+
export function setBluetoothManager(manager) {
|
|
341
|
+
instance = manager;
|
|
342
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { MugState, LiquidState, TemperatureUnit, RGBColor } from './types.js';
|
|
3
|
+
export interface MockBluetoothManagerEvents {
|
|
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 interface MockConfig {
|
|
12
|
+
/** Initial battery level (0-100) */
|
|
13
|
+
initialBattery?: number;
|
|
14
|
+
/** Initial current temperature in Celsius */
|
|
15
|
+
initialCurrentTemp?: number;
|
|
16
|
+
/** Initial target temperature in Celsius */
|
|
17
|
+
initialTargetTemp?: number;
|
|
18
|
+
/** Initial liquid state */
|
|
19
|
+
initialLiquidState?: LiquidState;
|
|
20
|
+
/** Whether the mug starts on the charger */
|
|
21
|
+
initiallyCharging?: boolean;
|
|
22
|
+
/** Simulated mug name */
|
|
23
|
+
mugName?: string;
|
|
24
|
+
/** Delay before "finding" the mug during scan (ms) */
|
|
25
|
+
scanDelay?: number;
|
|
26
|
+
/** Delay before "connecting" after finding (ms) */
|
|
27
|
+
connectionDelay?: number;
|
|
28
|
+
/** How fast temperature changes (degrees per second, default 0.5) */
|
|
29
|
+
tempChangeRate?: number;
|
|
30
|
+
/** How often to update simulation (ms) */
|
|
31
|
+
updateInterval?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class MockBluetoothManager extends EventEmitter {
|
|
34
|
+
private config;
|
|
35
|
+
private isConnected;
|
|
36
|
+
private simulationInterval;
|
|
37
|
+
private lastUpdateTime;
|
|
38
|
+
private state;
|
|
39
|
+
constructor(config?: MockConfig);
|
|
40
|
+
startScanning(): Promise<void>;
|
|
41
|
+
stopScanning(): Promise<void>;
|
|
42
|
+
private connect;
|
|
43
|
+
private startSimulation;
|
|
44
|
+
private updateSimulation;
|
|
45
|
+
setTargetTemp(temp: number): Promise<void>;
|
|
46
|
+
setTemperatureUnit(unit: TemperatureUnit): Promise<void>;
|
|
47
|
+
setLedColor(color: RGBColor): Promise<void>;
|
|
48
|
+
getState(): MugState;
|
|
49
|
+
private emitState;
|
|
50
|
+
disconnect(): Promise<void>;
|
|
51
|
+
/** Simulate placing the mug on the charger */
|
|
52
|
+
simulateStartCharging(): void;
|
|
53
|
+
/** Simulate removing the mug from the charger */
|
|
54
|
+
simulateStopCharging(): void;
|
|
55
|
+
/** Simulate the mug becoming empty */
|
|
56
|
+
simulateEmpty(): void;
|
|
57
|
+
/** Simulate filling the mug with liquid at a given temperature */
|
|
58
|
+
simulateFill(temperature: number): void;
|
|
59
|
+
/** Simulate a connection drop */
|
|
60
|
+
simulateDisconnect(): void;
|
|
61
|
+
/** Set battery level directly (for testing low battery scenarios) */
|
|
62
|
+
simulateBatteryLevel(level: number): void;
|
|
63
|
+
}
|
|
64
|
+
export declare function getMockBluetoothManager(config?: MockConfig): MockBluetoothManager;
|
|
65
|
+
export declare function resetMockBluetoothManager(): void;
|