Skip to content

Commit

Permalink
Fix FrequencyRangeList
Browse files Browse the repository at this point in the history
  • Loading branch information
toni-neurosc committed Sep 25, 2024
1 parent 1b066de commit 3cc373c
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 50 deletions.
109 changes: 80 additions & 29 deletions gui_dev/src/pages/Settings/FrequencyRange.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import {
TextField,
Button,
Box,
Typography,
IconButton,
Stack,
} from "@mui/material";
import { useState } from "react";
import { TextField, Button, IconButton, Stack } from "@mui/material";
import { Add, Close } from "@mui/icons-material";
import { debounce } from "@/utils";

const NumberField = ({ ...props }) => (
<TextField
Expand All @@ -25,31 +20,63 @@ const NumberField = ({ ...props }) => (
/>
);

export const FrequencyRange = ({ name, range, onChange, onRemove }) => {
const handleChange = (field, value) => {
onChange(name, { ...range, [field]: value });
export const FrequencyRange = ({
id,
name,
range,
onChangeName,
onChangeRange,
onRemove,
}) => {
const [localName, setLocalName] = useState(name);

const debouncedChangeName = debounce((newName) => {
onChangeName(newName, name);
}, 1000);

const handleNameChange = (e) => {
const newName = e.target.value;
setLocalName(newName);
debouncedChangeName(newName);
};

const handleNameBlur = () => {
onChangeName(localName, name);
};

const handleKeyPress = (e) => {
if (e.key === "Enter") {
console.log(e.target.value, name);
onChangeName(localName, name);
}
};

const handleRangeChange = (field, value) => {
onChangeRange(id, { ...range, [field]: value });
};

return (
<Stack direction="row" alignItems="center" gap={1}>
<TextField
size="small"
value={name}
value={localName}
fullWidth
onChange={(e) => onChange(e.target.value, range, name)}
onChange={handleNameChange}
onBlur={handleNameBlur}
onKeyPress={handleKeyPress}
/>
<NumberField
size="small"
type="number"
value={range.frequency_low_hz}
onChange={(e) => handleChange("frequency_low_hz", e.target.value)}
onChange={(e) => handleRangeChange("frequency_low_hz", e.target.value)}
label="Low Hz"
/>
<NumberField
size="small"
type="number"
value={range.frequency_high_hz}
onChange={(e) => handleChange("frequency_high_hz", e.target.value)}
onChange={(e) => handleRangeChange("frequency_high_hz", e.target.value)}
label="High Hz"
/>
<IconButton
Expand All @@ -64,47 +91,71 @@ export const FrequencyRange = ({ name, range, onChange, onRemove }) => {
);
};

export const FrequencyRangeList = ({ ranges, onChange }) => {
const handleChange = (newName, newRange, oldName = newName) => {
export const FrequencyRangeList = ({
ranges,
rangeOrder,
onChange,
onOrderChange,
}) => {
const handleChangeRange = (name, newRange) => {
const updatedRanges = { ...ranges };
if (newName !== oldName) {
delete updatedRanges[oldName];
updatedRanges[name] = newRange;
onChange(["frequency_ranges_hz"], updatedRanges);
};

const handleChangeName = (newName, oldName) => {
if (oldName === newName) {
return;
}
updatedRanges[newName] = newRange;

const updatedRanges = { ...ranges, [newName]: ranges[oldName] };
delete updatedRanges[oldName];
onChange(["frequency_ranges_hz"], updatedRanges);

const updatedOrder = rangeOrder.map((name) =>
name === oldName ? newName : name
);
onOrderChange(updatedOrder);
};

const handleRemove = (name) => {
const updatedRanges = { ...ranges };
delete updatedRanges[name];
onChange(["frequency_ranges_hz"], updatedRanges);

const updatedOrder = rangeOrder.filter((item) => item !== name);
onOrderChange(updatedOrder);
};

const addRange = () => {
let newName = "NewRange";
let counter = 0;
while (ranges.hasOwnProperty(newName)) {
// Find first available name
while (Object.hasOwn(ranges, newName)) {
counter++;
newName = `NewRange${counter}`;
}

const updatedRanges = {
...ranges,
[newName]: { frequency_low_hz: "", frequency_high_hz: "" },
[newName]: { frequency_low_hz: 1, frequency_high_hz: 500 },
};
onChange(["frequency_ranges_hz"], updatedRanges);

const updatedOrder = [...rangeOrder, newName];
onOrderChange(updatedOrder);
};

return (
<Stack>
<Typography variant="h6" gutterBottom>
Frequency Ranges
</Typography>
{Object.entries(ranges).map(([name, range]) => (
{rangeOrder.map((name, index) => (
<FrequencyRange
key={name}
key={index}
id={index}
name={name}
range={range}
onChange={handleChange}
range={ranges[name]}
onChangeName={handleChangeName}
onChangeRange={handleChangeRange}
onRemove={handleRemove}
/>
))}
Expand Down
56 changes: 42 additions & 14 deletions gui_dev/src/pages/Settings/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react";
import {
Box,
Button,
ButtonGroup,
InputAdornment,
Stack,
Switch,
Expand Down Expand Up @@ -96,18 +97,24 @@ const SettingsSection = ({
const boxTitle = title ? title : formatKey(path[path.length - 1]);

// If we receive a primitive value, we need to render a component

if (typeof settings !== "object") {
const Component = componentRegistry[typeof settings];
if (!Component) {
console.error(`Invalid component type: ${typeof settings}`);
return null;
}
return (
<Stack direction="row">
<Typography variant="body2">{boxTitle}</Typography>
<Component label={title} value={settings} onChange={onChange} />
</Stack>
<SettingsField
Component={Component}
label={boxTitle}
value={settings}
onChange={onChange}
depth={depth + 1}
/>
// <Stack direction="row">
// <Typography variant="body2">{boxTitle}</Typography>
// <Component label={title} value={settings} onChange={onChange} />
// </Stack>
);
}

Expand Down Expand Up @@ -158,6 +165,12 @@ const SettingsContent = () => {
const [selectedFeature, setSelectedFeature] = useState("");
const settings = useSettingsStore((state) => state.settings);
const updateSettings = useSettingsStore((state) => state.updateSettings);
const frequencyRangeOrder = useSettingsStore(
(state) => state.frequencyRangeOrder
);
const updateFrequencyRangeOrder = useSettingsStore(
(state) => state.updateFrequencyRangeOrder
);

if (!settings) {
return <div>Loading settings...</div>;
Expand Down Expand Up @@ -209,7 +222,7 @@ const SettingsContent = () => {
gap={2}
p={2}
>
<Stack>
<Stack sx={{ minWidth: "33%" }}>
<TitledBox title="General Settings" depth={0}>
{generalSettingsKeys.map((key) => (
<SettingsSection
Expand All @@ -225,6 +238,8 @@ const SettingsContent = () => {
<TitledBox title="Frequency Ranges" depth={0}>
<FrequencyRangeList
ranges={settings.frequency_ranges_hz}
rangeOrder={frequencyRangeOrder}
onOrderChange={updateFrequencyRangeOrder}
onChange={handleChange}
/>
</TitledBox>
Expand Down Expand Up @@ -302,15 +317,28 @@ export const Settings = () => {
return (
<Stack justifyContent="center" pb={2}>
<SettingsContent />
<Button
variant="contained"
component={Link}
color="primary"
to="/decoding"
sx={{ mt: 2 }}
<Stack
direction="row"
width="fit-content"
sx={{ position: "absolute", bottom: "2.5rem", right: "1rem", gap: 1 }}
backgroundColor="background.level3"
borderRadius={2}
border="1px solid"
borderColor={"divider"}
p={1}
>
Run Stream
</Button>
<Button variant="contained" color="primary" to="/decoding">
Reset Settings
</Button>
<Button
variant="contained"
component={Link}
color="primary"
to="/decoding"
>
Run Stream
</Button>
</Stack>
</Stack>
);
};
21 changes: 18 additions & 3 deletions gui_dev/src/stores/settingsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ const uploadSettingsToServer = async (settings) => {

export const useSettingsStore = createStore("settings", (set, get) => ({
settings: null,
frequencyRangeOrder: [],
isLoading: false,
error: null,
retryCount: 0,

setSettings: (settings) => set({ settings }),
setFrequencyRangeOrder: (order) => set({ frequencyRangeOrder: order }),

fetchSettingsWithDelay: () => {
set({ isLoading: true, error: null });
Expand All @@ -46,7 +48,11 @@ export const useSettingsStore = createStore("settings", (set, get) => ({
throw new Error("Failed to fetch settings");
}
const data = await response.json();
set({ settings: data, retryCount: 0 });
set({
settings: data,
frequencyRangeOrder: Object.keys(data.frequency_ranges_hz || {}),
retryCount: 0,
});
} catch (error) {
console.log("Error fetching settings:", error);
set((state) => ({
Expand All @@ -65,8 +71,13 @@ export const useSettingsStore = createStore("settings", (set, get) => ({

resetRetryCount: () => set({ retryCount: 0 }),

updateFrequencyRangeOrder: (newOrder) => {
set({ frequencyRangeOrder: newOrder });
},

updateSettings: async (updater) => {
const currentSettings = get().settings;
const currentOrder = get().frequencyRangeOrder;

// Apply the update optimistically
set((state) => {
Expand All @@ -75,18 +86,22 @@ export const useSettingsStore = createStore("settings", (set, get) => ({

const newSettings = get().settings;

// Update the frequency range order
const newOrder = Object.keys(newSettings.frequency_ranges_hz || {});
set({ frequencyRangeOrder: newOrder });

try {
const result = await uploadSettingsToServer(newSettings);

if (!result.success) {
// Revert the local state if the server update failed
set({ settings: currentSettings });
set({ settings: currentSettings, frequencyRangeOrder: currentOrder });
}

return result;
} catch (error) {
// Revert the local state if there was an error
set({ settings: currentSettings });
set({ settings: currentSettings, frequencyRangeOrder: currentOrder });
throw error;
}
},
Expand Down
1 change: 1 addition & 0 deletions gui_dev/src/utils/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function debounce(func, wait) {
timeout = setTimeout(later, wait);
};
}

export const flattenDictionary = (dict, parentKey = "", result = {}) => {
for (let key in dict) {
const newKey = parentKey ? `${parentKey}.${key}` : key;
Expand Down
10 changes: 6 additions & 4 deletions py_neuromodulation/gui/backend/app_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def __init__(

def push_features_to_frontend(self, feature_queue: Queue) -> None:
while True:
time.sleep(0.002) # NOTE: should be adapted depending on feature sampling rate
time.sleep(
0.002
) # NOTE: should be adapted depending on feature sampling rate
if feature_queue.empty() is False:
self.logger.info("data in feature queue")
features = feature_queue.get()
Expand All @@ -98,11 +100,13 @@ async def get_settings():

@self.post("/api/settings")
async def update_settings(data: dict):
print(data)
try:
self.pynm_state.settings = NMSettings.model_validate(data)
self.logger.debug(self.pynm_state.settings.features)
return self.pynm_state.settings.model_dump()
except ValueError as e:
self.logger.error(f"Error updating settings: {e}")
raise HTTPException(
status_code=422,
detail={"error": "Validation failed", "details": str(e)},
Expand All @@ -122,7 +126,7 @@ async def handle_stream_control(data: dict):
experiment_name=data["experiment_name"],
websocket_manager_features=self.websocket_manager_features,
)

# this also fails due to pickling error
# self.push_features_process = Process(
# target=self.push_features_to_frontend,
Expand Down Expand Up @@ -388,5 +392,3 @@ async def websocket_endpoint(websocket: WebSocket):
# # Serve the index.html for any path that doesn't match an API route
# print(Path.cwd())
# return FileResponse("frontend/index.html")


0 comments on commit 3cc373c

Please sign in to comment.