feat: Implement data visualization and export for MAX-BAND device

This commit is contained in:
2025-02-11 15:22:37 +08:00
parent af26793eef
commit e5bb316873
3 changed files with 120 additions and 18 deletions

View File

@ -1,4 +1,7 @@
{ {
"python.analysis.autoImportCompletions": true, "python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "standard" "python.analysis.typeCheckingMode": "standard",
"cSpell.words": [
"timescape"
]
} }

View File

@ -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
View File

@ -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