Skip to content

Commit

Permalink
Merge pull request #5503 from coronasafe/smarticu/ventilator-integration
Browse files Browse the repository at this point in the history
SmartICU: Ventilator Integration (PoC)
  • Loading branch information
mathew-alex committed Jun 19, 2023
2 parents a8a8de2 + 60d88e1 commit 00aa1b0
Show file tree
Hide file tree
Showing 15 changed files with 869 additions and 139 deletions.
16 changes: 16 additions & 0 deletions src/Components/Assets/AssetConfigure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ const AssetConfigure = (props: AssetConfigureProps) => {
);
}

if (assetType === "VENTILATOR") {
return (
<Page
title={`Configure Ventilator: ${asset?.name}`}
crumbsReplacements={{
[facilityId]: { name: asset?.location_object.facility.name },
assets: { uri: `/assets?facility=${facilityId}` },
[assetId]: { name: asset?.name },
}}
backUrl={`/facility/${facilityId}/assets/${assetId}`}
>
<HL7Monitor asset={asset} assetId={assetId} facilityId={facilityId} />
</Page>
);
}

return (
<Page
title={`Configure ONVIF Camera: ${asset?.name}`}
Expand Down
16 changes: 12 additions & 4 deletions src/Components/Assets/AssetType/HL7Monitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Submit } from "../../Common/components/ButtonV2";
import CareIcon from "../../../CAREUI/icons/CareIcon";
import TextFormField from "../../Form/FormFields/TextFormField";
import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor";
import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor";

interface HL7MonitorProps {
assetId: string;
Expand Down Expand Up @@ -115,15 +116,22 @@ const HL7Monitor = (props: HL7MonitorProps) => {
</form>
</Card>
<Card>
{assetType === "HL7MONITOR" && (
{["HL7MONITOR", "VENTILATOR"].includes(assetType) && (
<MonitorConfigure asset={asset as AssetData} />
)}
</Card>
</div>

<HL7PatientVitalsMonitor
socketUrl={`wss://${middleware}/observations/${localipAddress}`}
/>
{assetType === "HL7MONITOR" && (
<HL7PatientVitalsMonitor
socketUrl={`wss://${middleware}/observations/${localipAddress}`}
/>
)}
{assetType === "VENTILATOR" && (
<VentilatorPatientVitalsMonitor
socketUrl={`wss://${middleware}/observations/${localipAddress}`}
/>
)}
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/Components/Assets/AssetTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum AssetClass {
NONE = "NONE",
ONVIF = "ONVIF",
HL7MONITOR = "HL7MONITOR",
VENTILATOR = "VENTILATOR",
}

export const assetClassProps = {
Expand Down
8 changes: 4 additions & 4 deletions src/Components/Facility/AssetCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import useAppHistory from "../../Common/hooks/useAppHistory";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { LocationSelect } from "../Common/LocationSelect";
import { FieldLabel } from "../Form/FormFields/FormField";
import { useTranslation } from "react-i18next";

const Loading = loadable(() => import("../Common/Loading"));

Expand Down Expand Up @@ -95,7 +94,6 @@ type AssetFormSection =

const AssetCreate = (props: AssetProps) => {
const { goBack } = useAppHistory();
const { t } = useTranslation();
const { facilityId, assetId } = props;

let assetTypeInitial: AssetType;
Expand Down Expand Up @@ -587,8 +585,6 @@ const AssetCreate = (props: AssetProps) => {
data-testid="asset-class-input"
>
<SelectFormField
disabled={!!(props.assetId && asset_class)}
placeholder={props.assetId ? t("none") : undefined}
name="asset_class"
label="Asset Class"
value={asset_class}
Expand All @@ -598,6 +594,10 @@ const AssetCreate = (props: AssetProps) => {
title: "HL7 Vitals Monitor",
value: AssetClass.HL7MONITOR,
},
{
title: "Ventilator",
value: AssetClass.VENTILATOR,
},
]}
optionLabel={({ title }) => title}
optionValue={({ value }) => value}
Expand Down
161 changes: 110 additions & 51 deletions src/Components/Facility/ConsultationDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import {
OptionsType,
SYMPTOM_CHOICES,
} from "../../Common/constants";
import { ConsultationModel, ICD11DiagnosisModel } from "./models";
import { getConsultation, getPatient } from "../../Redux/actions";
import {
ConsultationModel,
FacilityModel,
ICD11DiagnosisModel,
} from "./models";
import {
getConsultation,
getPatient,
getPermittedFacility,
listAssetBeds,
} from "../../Redux/actions";
import { statusType, useAbortableEffect } from "../../Common/utils";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { ABGPlots } from "./Consultations/ABGPlots";
import ButtonV2 from "../Common/components/ButtonV2";
Expand All @@ -27,7 +36,6 @@ import { NursingPlot } from "./Consultations/NursingPlot";
import { NutritionPlots } from "./Consultations/NutritionPlots";
import PatientInfoCard from "../Patient/PatientInfoCard";
import { PatientModel } from "../Patient/models";
import LegacyPatientVitalsCard from "../Patient/LegacyPatientVitalsCard";
import { PressureSoreDiagrams } from "./Consultations/PressureSoreDiagrams";
import { PrimaryParametersPlot } from "./Consultations/PrimaryParametersPlot";
import ReadMore from "../Common/components/Readmore";
Expand All @@ -41,6 +49,10 @@ import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor";
import PrescriptionsTable from "../Medicine/PrescriptionsTable";
import MedicineAdministrationsTable from "../Medicine/MedicineAdministrationsTable";
import DischargeSummaryModal from "./DischargeSummaryModal";
import VentilatorPatientVitalsMonitor from "../VitalsMonitor/VentilatorPatientVitalsMonitor";

import { AssetBedModel, AssetClass } from "../Assets/AssetTypes";
import HL7PatientVitalsMonitor from "../VitalsMonitor/HL7PatientVitalsMonitor";

const Loading = loadable(() => import("../Common/Loading"));
const PageTitle = loadable(() => import("../Common/PageTitle"));
Expand Down Expand Up @@ -431,14 +443,11 @@ export const ConsultationDetails = (props: any) => {
</div>
{tab === "UPDATES" && (
<div className="flex xl:flex-row flex-col">
<div className="xl:w-2/3 w-full">
<div className="xl:w-3/4 w-full">
<PageTitle title="Info" hideBack={true} breadcrumbs={false} />
{!consultationData.discharge_date && (
<section className="bg-white shadow-sm rounded-md flex items-stretch w-full flex-col lg:flex-row overflow-hidden">
<LegacyPatientVitalsCard
patient={patientData}
facilityId={patientData.facility}
/>
<VitalsCard consultation={consultationData} />
</section>
)}
<div className="grid lg:grid-cols-2 gap-4 mt-4">
Expand Down Expand Up @@ -903,7 +912,7 @@ export const ConsultationDetails = (props: any) => {
</div>
</div>
</div>
<div className="xl:w-1/3 w-full pl-4">
<div className="xl:w-1/4 w-full pl-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
<PageTitle title="Update Log" hideBack breadcrumbs={false} />
<div className="md:mb-[0.125rem] mb-[2rem] pl-[1.5rem]">
Expand Down Expand Up @@ -1130,51 +1139,101 @@ export const ConsultationDetails = (props: any) => {
);
};

// TODO: make this thing responsive :/
// const VitalsCard = ({ consultation }: { consultation: ConsultationModel }) => {
// const dispatch = useDispatch<any>();
// const [socketUrl, setSocketUrl] = useState<string>();
const VitalsCard = ({ consultation }: { consultation: ConsultationModel }) => {
const dispatch = useDispatch<any>();
const [loading, setLoading] = useState(true);
const [hl7SocketUrl, setHL7SocketUrl] = useState<string>();
const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState<string>();

// useEffect(() => {
// if (!consultation.facility) return;
// const fetchData = async () => {
// const [facilityRes, assetBedRes] = await Promise.all([
// dispatch(getPermittedFacility(consultation.facility as any)),
// dispatch(
// listAssetBeds({
// facility: consultation.facility as any,
// bed: consultation.current_bed?.bed_object.id,
// })
// ),
// ]);
useEffect(() => {
if (!consultation.facility) return;

// const { middleware_address } = facilityRes.data as FacilityModel;
// const assetbed = assetBedRes.data.results[0] as AssetBedModel;
const fetchData = async () => {
setLoading(true);

// if (
// !middleware_address ||
// !assetbed ||
// !assetbed.asset_object.meta?.local_ip_address
// )
// return;
const [facilityRes, assetBedRes] = await Promise.all([
dispatch(getPermittedFacility(consultation.facility as any)),
dispatch(
listAssetBeds({
facility: consultation.facility as any,
bed: consultation.current_bed?.bed_object.id,
})
),
]);

const { middleware_address } = facilityRes.data as FacilityModel;
const assetBeds = assetBedRes.data.results as AssetBedModel[];

const hl7Meta = assetBeds.find(
(i) => i.asset_object.asset_class === AssetClass.HL7MONITOR
)?.asset_object?.meta;
const hl7Middleware = hl7Meta?.middleware_hostname || middleware_address;
if (hl7Middleware && hl7Meta?.local_ip_address) {
setHL7SocketUrl(
`wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}`
);
}

// setSocketUrl(
// `wss://${middleware_address}/observations/${assetbed?.asset_object.meta?.local_ip_address}`
// );
// };
const ventilatorMeta = assetBeds.find(
(i) => i.asset_object.asset_class === AssetClass.VENTILATOR
)?.asset_object?.meta;
const ventilatorMiddleware =
ventilatorMeta?.middleware_hostname || middleware_address;
if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) {
setVentilatorSocketUrl(
`wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}`
);
}

setLoading(false);
};

fetchData();
}, [consultation]);

if (loading) {
return (
<div className="bg-black flex w-full h-full max-h-[400px] justify-center items-center text-center gap-4 rounded">
<Loading />
</div>
);
}

// fetchData();
// }, [consultation]);
if (!hl7SocketUrl && !ventilatorSocketUrl) {
return (
<span className="sr-only">
No HL7 Monitor or Ventilator configured for this patient
</span>
);
}

// if (!socketUrl) return null;
return (
<div className="flex flex-col lg:flex-row w-full bg-slate-800 gap-1 justify-between min-h-[400px] rounded">
<div className="flex-1">
{hl7SocketUrl ? (
<HL7PatientVitalsMonitor socketUrl={hl7SocketUrl} />
) : (
<VitalsDeviceNotConfigured device="HL7 Monitor" />
)}
</div>
<div className="flex-1">
{ventilatorSocketUrl ? (
<VentilatorPatientVitalsMonitor socketUrl={ventilatorSocketUrl} />
) : (
<VitalsDeviceNotConfigured device="Ventilator" />
)}
</div>
</div>
);
};

// return (
// <PatientVitalsMonitor
// socketUrl={socketUrl}
// size={{
// width: MONITOR_RATIO.w * 64,
// height: MONITOR_RATIO.h * 64,
// }}
// />
// );
// };
const VitalsDeviceNotConfigured = ({ device }: { device: string }) => {
return (
<div className="hidden lg:flex flex-col gap-4 bg-black w-full h-full items-center justify-center text-center text-gray-700">
<CareIcon className="care-l-sync-exclamation text-4xl text-gray-600" />
<span className="font-medium text-xl text-gray-700">
No {device} configured for this bed
</span>
</div>
);
};
35 changes: 29 additions & 6 deletions src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { PatientAssetBed } from "../Assets/AssetTypes";
import { Link } from "raviger";
import { GENDER_TYPES } from "../../Common/constants";
import CareIcon from "../../CAREUI/icons/CareIcon";
import WaveformLabels from "./WaveformLabels";
import { classNames } from "../../Utils/utils";

interface Props {
patientAssetBed?: PatientAssetBed;
Expand All @@ -16,7 +18,7 @@ export default function HL7PatientVitalsMonitor({
socketUrl,
size,
}: Props) {
const { connect, waveformCanvas, data } = useHL7VitalsMonitor();
const { connect, waveformCanvas, data, isOnline } = useHL7VitalsMonitor();
const { patient, bed } = patientAssetBed ?? {};

useEffect(() => {
Expand Down Expand Up @@ -63,7 +65,28 @@ export default function HL7PatientVitalsMonitor({
</div>
)}
<div className="flex flex-col md:flex-row md:justify-between divide-y divide-x-0 md:divide-y-0 md:divide-x divide-blue-600 gap-2">
<div className="relative" style={{ ...(size ?? waveformCanvas.size) }}>
<div
className={classNames(
"flex flex-col gap-1 justify-center items-center text-center p-1 text-warning-500 font-medium font-mono",
isOnline && "hidden"
)}
style={{ ...(size ?? waveformCanvas.size) }}
>
<CareIcon className="care-l-cloud-times text-4xl animate-pulse mb-2" />
<span className="font-bold">No incoming data from HL7 Monitor</span>
</div>
<div
className={classNames("relative", !isOnline && "hidden")}
style={{ ...(size ?? waveformCanvas.size) }}
>
<WaveformLabels
labels={{
ECG: "text-lime-300",
ECG_CHANNEL_2: "invisible",
Pleth: "text-yellow-300",
SpO2: "text-sky-300",
}}
/>
<canvas
className="absolute top-0 left-0"
ref={waveformCanvas.background.canvasRef}
Expand All @@ -77,17 +100,17 @@ export default function HL7PatientVitalsMonitor({
{...waveformCanvas.size}
/>
</div>
<div className="grid grid-cols-3 md:grid-cols-1 md:divide-y divide-blue-600 text-white tracking-wider">
<div className="grid grid-cols-3 md:grid-cols-1 md:divide-y divide-blue-600 text-white tracking-wider max-w-[170px]">
{/* Pulse Rate */}
<div className="flex justify-between items-center p-1">
<div className="flex flex-col h-full items-start text-sm text-primary-400 font-bold">
<span>ECG</span>
<span>{data.pulseRate?.unit ?? "--"}</span>
<span>{(data.pulseRate ?? data.heartRate)?.unit ?? "--"}</span>
</div>
<span className="text-4xl md:text-6xl font-black text-gray-300">
{data.pulseRate?.value ?? "--"}
{(data.pulseRate ?? data.heartRate)?.value ?? "--"}
</span>
{data.pulseRate?.value && (
{(data.pulseRate ?? data.heartRate)?.value && (
<span className="text-red-500 animate-pulse font-sans">❤️</span>
)}
</div>
Expand Down
Loading

0 comments on commit 00aa1b0

Please sign in to comment.