feat: Implement data visualization and export for MAX-BAND device
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"python.analysis.autoImportCompletions": true,
|
"python.analysis.autoImportCompletions": true,
|
||||||
"python.analysis.typeCheckingMode": "standard"
|
"python.analysis.typeCheckingMode": "standard",
|
||||||
|
"cSpell.words": [
|
||||||
|
"timescape"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@ -67,21 +67,25 @@ class AlgoModelData(BaseModel):
|
|||||||
_FORMAT: ClassVar[LiteralString] = "<BHBHBBHBHBBBBBBBI"
|
_FORMAT: ClassVar[LiteralString] = "<BHBHBBHBHBBBBBBBI"
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def hr_f(self) -> float:
|
def hr_f(self) -> float:
|
||||||
"""Heart rate in beats per minute"""
|
"""Heart rate in beats per minute"""
|
||||||
return self.hr / 10.0
|
return self.hr / 10.0
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def spo2_f(self) -> float:
|
def spo2_f(self) -> float:
|
||||||
"""SpO2 percentage"""
|
"""SpO2 percentage"""
|
||||||
return self.spo2 / 10.0
|
return self.spo2 / 10.0
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def r_f(self) -> float:
|
def r_f(self) -> float:
|
||||||
"""SpO2 R value"""
|
"""SpO2 R value"""
|
||||||
return self.r / 1000.0
|
return self.r / 1000.0
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def rr_f(self) -> float:
|
def rr_f(self) -> float:
|
||||||
"""RR interval in milliseconds"""
|
"""RR interval in milliseconds"""
|
||||||
return self.rr / 10.0
|
return self.rr / 10.0
|
||||||
|
|||||||
129
main.py
129
main.py
@ -31,12 +31,21 @@ from app.model import AlgoReport
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class AppHistory(TypedDict):
|
||||||
|
timescape: deque[datetime]
|
||||||
|
hr_data: deque[float]
|
||||||
|
hr_conf: deque[int] # in %
|
||||||
|
accel_x_data: deque[int]
|
||||||
|
accel_y_data: deque[int]
|
||||||
|
accel_z_data: deque[int]
|
||||||
|
|
||||||
|
|
||||||
# https://handmadesoftware.medium.com/streamlit-asyncio-and-mongodb-f85f77aea825
|
# https://handmadesoftware.medium.com/streamlit-asyncio-and-mongodb-f85f77aea825
|
||||||
class AppState(TypedDict):
|
class AppState(TypedDict):
|
||||||
worker_thread: Thread
|
worker_thread: Thread
|
||||||
message_queue: MemoryObjectReceiveStream[bytes]
|
message_queue: MemoryObjectReceiveStream[bytes]
|
||||||
task_group: TaskGroup
|
task_group: TaskGroup
|
||||||
history: deque[AlgoReport]
|
history: AppHistory
|
||||||
|
|
||||||
|
|
||||||
UDP_SERVER_HOST: Final[str] = "localhost"
|
UDP_SERVER_HOST: Final[str] = "localhost"
|
||||||
@ -80,40 +89,126 @@ def resource(params: Any = None):
|
|||||||
"worker_thread": tr,
|
"worker_thread": tr,
|
||||||
"message_queue": rx,
|
"message_queue": rx,
|
||||||
"task_group": unwrap(tg),
|
"task_group": unwrap(tg),
|
||||||
"history": deque(maxlen=MAX_LENGTH),
|
"history": {
|
||||||
|
"timescape": deque(maxlen=MAX_LENGTH),
|
||||||
|
"hr_data": deque(maxlen=MAX_LENGTH),
|
||||||
|
"hr_conf": deque(maxlen=MAX_LENGTH),
|
||||||
|
"accel_x_data": deque(maxlen=MAX_LENGTH),
|
||||||
|
"accel_y_data": deque(maxlen=MAX_LENGTH),
|
||||||
|
"accel_z_data": deque(maxlen=MAX_LENGTH),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
logger.info("Resource created")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
state = resource()
|
state = resource()
|
||||||
logger.info("Resource created")
|
|
||||||
history = state["history"]
|
history = state["history"]
|
||||||
|
|
||||||
def on_export():
|
def on_export():
|
||||||
raise NotImplementedError
|
file_name = f"history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.parquet"
|
||||||
|
logger.info(f"Exporting to {file_name}")
|
||||||
|
rec = ak.Record(history)
|
||||||
|
ak.to_parquet(rec, file_name)
|
||||||
|
|
||||||
def on_clear():
|
def on_clear():
|
||||||
raise NotImplementedError
|
nonlocal history
|
||||||
|
logger.info("Clearing history")
|
||||||
|
history["timescape"].clear()
|
||||||
|
history["hr_data"].clear()
|
||||||
|
history["hr_conf"].clear()
|
||||||
|
history["accel_x_data"].clear()
|
||||||
|
history["accel_y_data"].clear()
|
||||||
|
history["accel_z_data"].clear()
|
||||||
|
|
||||||
|
# https://docs.streamlit.io/develop/api-reference/layout
|
||||||
|
st.title("MAX-BAND Visualizer")
|
||||||
|
with st.container(border=True):
|
||||||
|
c1, c2 = st.columns(2)
|
||||||
|
with c1:
|
||||||
|
st.button(
|
||||||
|
"Export",
|
||||||
|
help="Export the current data to a parquet file",
|
||||||
|
on_click=on_export,
|
||||||
|
)
|
||||||
|
with c2:
|
||||||
|
st.button(
|
||||||
|
"Clear",
|
||||||
|
help="Clear the current data",
|
||||||
|
on_click=on_clear,
|
||||||
|
)
|
||||||
|
|
||||||
|
placeholder = st.empty()
|
||||||
|
|
||||||
st.button(
|
|
||||||
"Export", help="Export the current data to a parquet file", on_click=on_export
|
|
||||||
)
|
|
||||||
st.button("Clear", help="Clear the current data", on_click=on_clear)
|
|
||||||
pannel = st.empty()
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
message = state["message_queue"].receive_nowait()
|
message = state["message_queue"].receive_nowait()
|
||||||
except anyio.WouldBlock:
|
except anyio.WouldBlock:
|
||||||
continue
|
continue
|
||||||
report = AlgoReport.unmarshal(message)
|
with placeholder.container():
|
||||||
logger.info("Report: {}", report)
|
report = AlgoReport.unmarshal(message)
|
||||||
# TODO: plot
|
history["timescape"].append(datetime.now())
|
||||||
# fig = go.Figure(scatters)
|
history["hr_data"].append(report.data.hr_f)
|
||||||
# pannel.plotly_chart(fig)
|
history["hr_conf"].append(report.data.hr_conf)
|
||||||
|
history["accel_x_data"].append(report.accel_x)
|
||||||
|
history["accel_y_data"].append(report.accel_y)
|
||||||
|
history["accel_z_data"].append(report.accel_z)
|
||||||
|
|
||||||
|
# with st.container():
|
||||||
|
# c1, c2 = st.columns(2)
|
||||||
|
# with c1:
|
||||||
|
# c1.write(f"HR: {report.data.hr_f}")
|
||||||
|
# with c2:
|
||||||
|
# c2.write(f"HR Confidence: {report.data.hr_conf}")
|
||||||
|
|
||||||
|
fig_hr, fig_accel = st.tabs(["Heart Rate", "Accelerometer"])
|
||||||
|
|
||||||
|
with fig_hr:
|
||||||
|
fig_hr.plotly_chart(
|
||||||
|
go.Figure(
|
||||||
|
data=[
|
||||||
|
go.Scatter(
|
||||||
|
x=list(history["timescape"]),
|
||||||
|
y=list(history["hr_data"]),
|
||||||
|
mode="lines",
|
||||||
|
name="HR",
|
||||||
|
),
|
||||||
|
go.Scatter(
|
||||||
|
x=list(history["timescape"]),
|
||||||
|
y=list(history["hr_conf"]),
|
||||||
|
mode="lines",
|
||||||
|
name="HR Confidence",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with fig_accel:
|
||||||
|
fig_accel.plotly_chart(
|
||||||
|
go.Figure(
|
||||||
|
data=[
|
||||||
|
go.Scatter(
|
||||||
|
x=list(history["timescape"]),
|
||||||
|
y=list(history["accel_x_data"]),
|
||||||
|
mode="lines",
|
||||||
|
name="x",
|
||||||
|
),
|
||||||
|
go.Scatter(
|
||||||
|
x=list(history["timescape"]),
|
||||||
|
y=list(history["accel_y_data"]),
|
||||||
|
mode="lines",
|
||||||
|
name="y",
|
||||||
|
),
|
||||||
|
go.Scatter(
|
||||||
|
x=list(history["timescape"]),
|
||||||
|
y=list(history["accel_z_data"]),
|
||||||
|
mode="lines",
|
||||||
|
name="z",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
# 1659A202
|
|
||||||
|
|||||||
Reference in New Issue
Block a user