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.typeCheckingMode": "standard"
|
||||
"python.analysis.typeCheckingMode": "standard",
|
||||
"cSpell.words": [
|
||||
"timescape"
|
||||
]
|
||||
}
|
||||
@ -67,21 +67,25 @@ class AlgoModelData(BaseModel):
|
||||
_FORMAT: ClassVar[LiteralString] = "<BHBHBBHBHBBBBBBBI"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def hr_f(self) -> float:
|
||||
"""Heart rate in beats per minute"""
|
||||
return self.hr / 10.0
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def spo2_f(self) -> float:
|
||||
"""SpO2 percentage"""
|
||||
return self.spo2 / 10.0
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def r_f(self) -> float:
|
||||
"""SpO2 R value"""
|
||||
return self.r / 1000.0
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def rr_f(self) -> float:
|
||||
"""RR interval in milliseconds"""
|
||||
return self.rr / 10.0
|
||||
|
||||
129
main.py
129
main.py
@ -31,12 +31,21 @@ from app.model import AlgoReport
|
||||
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
|
||||
class AppState(TypedDict):
|
||||
worker_thread: Thread
|
||||
message_queue: MemoryObjectReceiveStream[bytes]
|
||||
task_group: TaskGroup
|
||||
history: deque[AlgoReport]
|
||||
history: AppHistory
|
||||
|
||||
|
||||
UDP_SERVER_HOST: Final[str] = "localhost"
|
||||
@ -80,40 +89,126 @@ def resource(params: Any = None):
|
||||
"worker_thread": tr,
|
||||
"message_queue": rx,
|
||||
"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
|
||||
|
||||
|
||||
def main():
|
||||
state = resource()
|
||||
logger.info("Resource created")
|
||||
history = state["history"]
|
||||
|
||||
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():
|
||||
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:
|
||||
try:
|
||||
message = state["message_queue"].receive_nowait()
|
||||
except anyio.WouldBlock:
|
||||
continue
|
||||
report = AlgoReport.unmarshal(message)
|
||||
logger.info("Report: {}", report)
|
||||
# TODO: plot
|
||||
# fig = go.Figure(scatters)
|
||||
# pannel.plotly_chart(fig)
|
||||
with placeholder.container():
|
||||
report = AlgoReport.unmarshal(message)
|
||||
history["timescape"].append(datetime.now())
|
||||
history["hr_data"].append(report.data.hr_f)
|
||||
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__":
|
||||
main()
|
||||
|
||||
# 1659A202
|
||||
|
||||
Reference in New Issue
Block a user