Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/alarm_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/alarm_priorities.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/dg_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/hd_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/msg_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/msg_ids.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/prs_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/test_config_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/common/ui_defs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/accelerometer.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/alarms.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/calibration_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/chemical_disinfect.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/chemical_disinfect_flush.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/concentrate_pumps.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/conductivity_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/constants.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/cpld.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/dg_test_configs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/dialysate_fill.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/dialysate_generator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/drain.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/drain_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/events.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/fans.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/flow_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/fluid_leak.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/flush.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/gen_idle.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/hd_proxy.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/heat_disinfect.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/heat_disinfect_active_cool.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/heaters.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/load_cells.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/pressures.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/reservoirs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/ro_permeate_sample.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/ro_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/rtc.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/samplewater.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/scheduled_runs_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/service_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/sw_configs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/switches.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/system_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/temperatures.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/thermistors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/usage_info_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/uv_reactors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/valves.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/voltages.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/dg/watchdog.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/accelerometer.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/air_bubbles.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/air_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/air_trap.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/alarms.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/battery.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/blood_flow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/blood_leak.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/buttons.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/calibration_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/constants.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/dg_proxy.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/dialysate_inlet_flow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/dialysate_outlet_flow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/fans.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/fluid_leak.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/hd_events.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/hd_test_configs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/hemodialysis_device.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/institutional_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/post_treatment.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/pressure_occlusion.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/pretreatment.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/reservoirs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/rtc.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/service_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/sw_configs.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/switches.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/syringe_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/system_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/temperatures.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/treatment.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/ui_proxy.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/usage_info_record.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/valves.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/voltages.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/hd/watchdog.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/protocols/CAN.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/protocols/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/crc.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/dg_simulator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/hd_simulator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/hd_simulator_alarms.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/messageBuilder.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/unittests.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/ui/utils.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/base.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/checks.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/conversions.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/data_logger.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/excel_ops.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/helpers.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/nv_ops_utils.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/utils/singleton.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 07db7b5f01ad17d60c190e21574e1ed0552535ff refers to a dead (removed) revision in file `dialin/version.py'. Fisheye: No comparison available. Pass `N' to diff? Index: docs/source/conf.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- docs/source/conf.py (.../conf.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ docs/source/conf.py (.../conf.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -155,7 +155,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'dialin', 'Dialin Documentation', + (master_doc, 'leahi-dialin', 'Dialin Documentation', [author], 1) ] Index: leahi-dialin/__init__.py =================================================================== diff -u --- leahi-dialin/__init__.py (revision 0) +++ leahi-dialin/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,18 @@ +from .hd import * +from .dg import * +from .ui import * +from .utils import * + +__version__ = "" + +try: + from importlib import metadata +except ImportError: # for Python < 3.8 + import importlib_metadata as metadata + +try: + __version__ = metadata.version(__name__) +except metadata.PackageNotFoundError: + # package is not installed + pass + Index: leahi-dialin/common/__init__.py =================================================================== diff -u --- leahi-dialin/common/__init__.py (revision 0) +++ leahi-dialin/common/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,7 @@ +from .alarm_defs import * +from .alarm_priorities import * +from .msg_defs import * +from .prs_defs import * +from .hd_defs import * +from .dg_defs import * +from .ui_defs import * Index: leahi-dialin/common/alarm_defs.py =================================================================== diff -u --- leahi-dialin/common/alarm_defs.py (revision 0) +++ leahi-dialin/common/alarm_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,346 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file alarm_defs.py +# +# @author (last) Vinayakam Mani +# @date (last) 10-May-2024 +# @author (original) Peter Lucia +# @date (original) 07-Aug-2020 +# +############################################################################ +from enum import unique +from ..utils.base import AlarmEnum + + +# Branch: staging +@unique +class AlarmList(AlarmEnum): + ALARM_ID_NO_ALARM = 0 + ALARM_ID_HD_STUCK_BUTTON_TEST_FAILED = 1 + ALARM_ID_HD_FPGA_POST_TEST_FAILED = 2 + ALARM_ID_DG_FPGA_POST_TEST_FAILED = 3 + ALARM_ID_HD_WATCHDOG_POST_TEST_FAILED = 4 + ALARM_ID_DG_WATCHDOG_POST_TEST_FAILED = 5 + ALARM_ID_HD_UI_COMM_POST_FAILED = 6 + ALARM_ID_HD_RTC_BATTERY_LOW = 7 + ALARM_ID_HD_ACCELEROMETER_SELF_TEST_FAILURE = 8 + ALARM_ID_DG_ACCELEROMETER_SELF_TEST_FAILURE = 9 + ALARM_ID_HD_BLOOD_LEAK_SENSOR_EMBEDDED_MODE_FAILURE = 10 + ALARM_ID_HD_DIALYSATE_TEMP_BELOW_TARGET_TEMP = 11 + ALARM_ID_HD_INTEGRITY_POST_TEST_FAILED = 12 + ALARM_ID_DG_INTEGRITY_POST_TEST_FAILED = 13 + ALARM_ID_DG_INVALID_USAGE_RECORD_CRC = 14 + ALARM_ID_HD_DIALYSATE_TEMP_ABOVE_SAFETY_TEMP = 15 + ALARM_ID_HD_ALARM_AUDIO_SELF_TEST_FAILURE = 16 + ALARM_ID_HD_UI_POST_TIMEOUT = 17 + ALARM_ID_HD_CAN_MESSAGE_NOT_ACKED_BY_DG = 18 + ALARM_ID_HD_DIALYSATE_TEMP_ABOVE_TARGET_TEMP = 19 + ALARM_ID_DG_COND_SENSORS_INVALID_CAL_RECORD = 20 + ALARM_ID_DG_DRAIN_LINE_VOLUME_INVALID_CAL_RECORD = 21 + ALARM_ID_DG_RESERVOIRS_INVALID_CAL_RECORD = 22 + ALARM_ID_DG_ACID_CONCENTRATE_INVALID_CAL_RECORD = 23 + ALARM_ID_DG_BICARB_CONCENTRATE_INVALID_CAL_RECORD = 24 + ALARM_ID_DG_ACCELEROMETERS_INVALID_CAL_RECORD = 25 + ALARM_ID_HD_ACCELEROMETERS_INVALID_CAL_RECORD = 26 + ALARM_ID_HD_BLOOD_LEAK_SENSOR_ZERO_SEQUENCE_FAILED = 27 + ALARM_ID_DG_TWO_WIRE_SENSORS_FPGA_FAULT = 28 + ALARM_ID_HD_HEPARIN_FORCE_SENSOR_INVALID_CAL_RECORD = 29 + ALARM_ID_HD_SOFTWARE_FAULT = 30 + ALARM_ID_HD_BLOOD_PUMP_MC_CURRENT_CHECK = 31 + ALARM_ID_HD_BLOOD_PUMP_OFF_CHECK = 32 + ALARM_ID_HD_BLOOD_PUMP_MC_DIRECTION_CHECK = 33 + ALARM_ID_HD_BLOOD_PUMP_ROTOR_SPEED_CHECK = 34 + ALARM_ID_HD_DIAL_IN_PUMP_MC_CURRENT_CHECK = 35 + ALARM_ID_HD_DIAL_IN_PUMP_OFF_CHECK = 36 + ALARM_ID_HD_DIAL_IN_PUMP_MC_DIRECTION_CHECK = 37 + ALARM_ID_HD_DIAL_IN_PUMP_ROTOR_SPEED_CHECK = 38 + ALARM_ID_HD_DIAL_OUT_PUMP_MC_CURRENT_CHECK = 39 + ALARM_ID_HD_DIAL_OUT_PUMP_OFF_CHECK = 40 + ALARM_ID_HD_DIAL_OUT_PUMP_MC_DIRECTION_CHECK = 41 + ALARM_ID_HD_DIAL_OUT_PUMP_ROTOR_SPEED_CHECK = 42 + ALARM_ID_DG_MAIN_PRIMARY_HEATER_FPGA_FAULT = 43 + ALARM_ID_HD_UI_COMM_TIMEOUT = 44 + ALARM_ID_HD_COMM_TOO_MANY_BAD_CRCS = 45 + ALARM_ID_HD_CAN_MESSAGE_NOT_ACKED_BY_UI = 46 + ALARM_ID_DG_MAX_RO_PUMP_PWM_EXCEEDED = 47 + ALARM_ID_HD_UF_VOLUME_ACCURACY_ERROR = 48 + ALARM_ID_HD_FPGA_COMM_TIMEOUT = 49 + ALARM_ID_DG_VALVE_CONTROL_FAILURE = 50 + ALARM_ID_HD_BLOOD_PUMP_MOTOR_SPEED_CHECK = 51 + ALARM_ID_HD_DIAL_IN_PUMP_MOTOR_SPEED_CHECK = 52 + ALARM_ID_HD_DIAL_OUT_PUMP_MOTOR_SPEED_CHECK = 53 + ALARM_ID_HD_CRITICAL_DATA_ERROR = 54 + ALARM_ID_DG_CRITICAL_DATA_ERROR = 55 + ALARM_ID_HD_ACCELEROMETER_FAILURE = 56 + ALARM_ID_DG_ACCELEROMETER_FAILURE = 57 + ALARM_ID_HD_VALVE_HOMING_FAILED = 58 + ALARM_ID_HD_VALVE_TRANSITION_TIMEOUT = 59 + ALARM_ID_HD_VALVE_NOT_FUNCTIONAL = 60 + ALARM_ID_HD_VALVE_CURRENT_OUT_OF_RANGE = 61 + ALARM_ID_HD_VALVE_POSITION_OUT_OF_RANGE = 62 + ALARM_ID_HD_SYRINGE_PUMP_PRIME_TIMEOUT = 63 + ALARM_ID_DG_BARO_PRESSURE_OUT_OF_RANGE = 64 + ALARM_ID_HD_DG_COMMAND_INVALID_PARAMETER_FAULT = 65 + ALARM_ID_HD_BLOOD_LEAK_SENSOR_SET_POINT_SET_FAILURE = 66 + ALARM_ID_HD_BP_OCCLUSION_SELF_TEST_FAILURE = 67 + ALARM_ID_HD_ACTIVE_RESERVOIR_RECIRCULATION_OUT_OF_RANGE = 68 + ALARM_ID_HD_BLOOD_LEAK_INVALID_CAL_RECORD = 69 + ALARM_ID_HD_ARTERIAL_PRESSURE_SELF_TEST_FAILURE = 70 + ALARM_ID_HD_VENOUS_PRESSURE_SELF_TEST_FAILURE = 71 + ALARM_ID_HD_NEW_LOAD_CELL_DATA_MESSAGE_NOT_RECEIVE = 72 + ALARM_ID_HD_NEW_DIALYSATE_TEMP_DATA_MESSAGE_NOT_RECEIVE = 73 + ALARM_ID_DG_INLET_UV_REACTOR_ON_WITH_NO_FLOW = 74 + ALARM_ID_HD_SYRINGE_PUMP_SELF_TEST_FAILURE = 75 + ALARM_ID_HD_VOLTAGE_OUT_OF_RANGE = 76 + ALARM_ID_DG_VOLTAGE_OUT_OF_RANGE = 77 + ALARM_ID_HD_SYRINGE_PUMP_ENCODER_DIRECTION_ERROR = 78 + ALARM_ID_HD_SYRINGE_PUMP_CONTROLLER_DIRECTION_ERROR = 79 + ALARM_ID_HD_SYRINGE_PUMP_FAULT = 80 + ALARM_ID_HD_SYRINGE_PUMP_OVER_TRAVEL_ERROR = 81 + ALARM_ID_HD_SYRINGE_PUMP_DAC_WRITE_ERROR = 82 + ALARM_ID_HD_SYRINGE_PUMP_RUNNING_WHILE_BP_OFF_ERROR = 83 + ALARM_ID_DG_SET_RTC_YEAR_INVALID = 84 + ALARM_ID_HD_PUMP_TRACK_LATCH_OPENED = 85 + ALARM_ID_HD_SET_RTC_YEAR_INVALID = 86 + ALARM_ID_DG_HEATING_INVALID_CAL_RECORD = 87 + ALARM_ID_DG_CONC_PUMP_HALL_SENSOR_OUT_OF_RANGE = 88 + ALARM_ID_DG_OUTLET_UV_REACTOR_ON_WITH_NO_FLOW = 89 + ALARM_ID_DG_LOAD_CELL_A1_B1_FPGA_FAULT = 90 + ALARM_ID_HD_DIALYSATE_FLOW_DATA_NOT_RECEIVE = 91 + ALARM_ID_DG_TEMPERATURE_SENSORS_INVALID_CAL_RECORD = 92 + ALARM_ID_DG_OUTLET_PRIMARY_CONDUCTIVITY_OUT_OF_RANGE = 93 + ALARM_ID_DG_PRESSURE_OUT_OF_RANGE = 94 + ALARM_ID_DG_WATCHDOG_EXPIRED = 95 + ALARM_ID_DG_INLET_WATER_TEMPERATURE_IN_HIGH_RANGE = 96 + ALARM_ID_DG_FILL_CONDUCTIVITY_OUT_OF_RANGE = 97 + ALARM_ID_HD_BATTERY_COMM_FAULT = 98 + ALARM_ID_HD_SYRINGE_PUMP_STALL = 99 + ALARM_ID_DG_COND_SENSORS_INVALID_TEMP_COMP_CAL_RECORD = 100 + ALARM_ID_UI_POST_HD_COMM = 101 + ALARM_ID_DG_HEAT_DISINFECT_TEMP_GRAD_OUT_OF_RANGE = 102 + ALARM_ID_HD_NVDATAMGMT_CAL_GROUP_RECORD_CRC_INVALID = 103 + ALARM_ID_HD_AIR_TRAP_ILLEGAL_LEVELS = 104 + ALARM_ID_DG_NVDATAMGMT_CAL_GROUP_RECORD_CRC_INVALID = 105 + ALARM_ID_DG_FLOW_SENSORS_INVALID_CAL_RECORD = 106 + ALARM_ID_HD_DG_RESTARTED_FAULT = 107 + ALARM_ID_HD_SYRINGE_PUMP_FPGA_ADC_FAULT = 108 + ALARM_ID_HD_SYRINGE_PUMP_VOLUME_ERROR = 109 + ALARM_ID_HD_SYRINGE_PUMP_SPEED_ERROR = 110 + ALARM_ID_HD_SYRINGE_PUMP_NOT_STOPPED_ERROR = 111 + ALARM_ID_DG_LOAD_CELL_A2_B2_FPGA_FAULT = 112 + ALARM_ID_DG_DRAIN_PUMP_CURRENT_OUT_OF_RANGE = 113 + ALARM_ID_HD_VENOUS_BUBBLE_SELF_TEST_FAILURE = 114 + ALARM_ID_DG_TEMPERATURE_SENSOR_OUT_OF_RANGE = 115 + ALARM_ID_HD_UI_SDCARD_FAILURE = 116 + ALARM_ID_DG_CAN_MESSAGE_NOT_ACKED = 117 + ALARM_ID_DG_RTC_CONFIG_ERROR = 118 + ALARM_ID_DG_RTC_BATTERY_LOW = 119 + ALARM_ID_HD_PRE_TREATMENT_WET_PRIME_TEST_FAILURE = 120 + ALARM_ID_DG_MAIN_PRIMARY_HEATER_VOLTAGE_OUT_OF_RANGE = 121 + ALARM_ID_DG_SMALL_PRIMARY_HEATER_VOLTAGE_OUT_OF_RANGE = 122 + ALARM_ID_DG_TRIMMER_HEATER_VOLTAGE_OUT_OF_RANGE = 123 + ALARM_ID_HD_END_OF_TREATMENT_HIGH = 124 + ALARM_ID_HD_TREATMENT_STOPPED_NO_RINSEBACK = 125 + ALARM_ID_HD_BLOOD_LEAK_DETECTED = 126 + ALARM_ID_HD_VENOUS_PRESSURE_LOW = 127 + ALARM_ID_HD_VENOUS_BUBBLE_DETECTED = 128 + ALARM_ID_HD_BLOOD_LEAK_RECOVERING_PLEASE_WAIT = 129 + ALARM_ID_HD_VENOUS_PRESSURE_HIGH = 130 + ALARM_ID_HD_ARTERIAL_PRESSURE_LOW = 131 + ALARM_ID_HD_ARTERIAL_PRESSURE_HIGH = 132 + ALARM_ID_DG_FLUID_LEAK_DETECTED = 133 + ALARM_ID_HD_FLUID_LEAK_DETECTED = 134 + ALARM_ID_HD_SHOCK = 135 + ALARM_ID_DG_SHOCK = 136 + ALARM_ID_HD_EXCESSIVE_TILT = 137 + ALARM_ID_DG_EXCESSIVE_TILT = 138 + ALARM_ID_HD_AC_POWER_LOST = 139 + ALARM_ID_HD_DG_COMM_TIMEOUT = 140 + ALARM_ID_HD_AIR_TRAP_FILL_DURING_TREATMENT = 141 + ALARM_ID_HD_OCCLUSION_BLOOD_PUMP = 142 + ALARM_ID_DG_DIALYSATE_TEMPERATURE_SENSORS_OUT_OF_RANGE = 143 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_COND_TOO_HIGH = 144 + ALARM_ID_DG_ACID_CONDUCTIVITY_OUT_OF_RANGE = 145 + ALARM_ID_DG_RTC_OR_TIMER_ACCURACY_FAILURE = 146 + ALARM_ID_DG_CREATING_DIALYSATE_PLEASE_WAIT = 147 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_TEMP_TOO_HIGH = 148 + ALARM_ID_HD_WATCHDOG_EXPIRED = 149 + ALARM_ID_DG_INLET_WATER_CONDUCTIVITY_IN_HIGH_RANGE = 150 + ALARM_ID_DG_INLET_WATER_CONDUCTIVITY_IN_LOW_RANGE = 151 + ALARM_ID_DG_INLET_WATER_PRESSURE_IN_LOW_RANGE = 152 + ALARM_ID_HD_PRIME_COMPLETED_HIGH = 153 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_COND_TOO_LOW = 154 + ALARM_ID_HD_NEW_RESERVOIRS_DATA_MESSAGE_NOT_RECEIVE = 155 + ALARM_ID_HD_DG_NEW_OPERATION_MODE_MESSAGE_NOT_RECEIVE = 156 + ALARM_ID_DG_CHEM_DISINFECT_PRIME_ACID_LINE_TIME_OUT = 157 + ALARM_ID_DG_INLET_WATER_TEMPERATURE_IN_LOW_RANGE = 158 + ALARM_ID_DG_CHEM_DISINFECT_FLUSH_REMOVE_ACID = 159 + ALARM_ID_HD_BLOOD_PUMP_ROTOR_SPEED_TOO_HIGH = 160 + ALARM_ID_DG_COMM_TOO_MANY_BAD_CRCS = 161 + ALARM_ID_DG_FPGA_CLOCK_SPEED_CHECK_FAILURE = 162 + ALARM_ID_HD_LOAD_CELL_ACCELERATION_RES_1_ALARM = 163 + ALARM_ID_HD_LOAD_CELL_ACCELERATION_RES_2_ALARM = 164 + ALARM_ID_HD_TREATMENT_RINSEBACK_TIMEOUT_ALARM = 165 + ALARM_ID_TEST_ALARM_NO_BLOOD_RECIRC_CLR_TOP_ONLY = 166 + ALARM_ID_HD_CARTRIDGE_DOOR_OPENED = 167 + ALARM_ID_TEST_ALARM_NO_DIAL_RECIRC_CLR_TOP_ONLY = 168 + ALARM_ID_DG_DIALYSATE_FLOW_RATE_OUT_OF_MAX_RANGE = 169 + ALARM_ID_HD_SYRINGE_PUMP_SYRINGE_EMPTY = 170 + ALARM_ID_HD_SYRINGE_PUMP_OCCLUSION = 171 + ALARM_ID_HD_SYRINGE_PUMP_NOT_ENOUGH_HEPARIN_ALARM = 172 + ALARM_ID_HD_RTC_CONFIG_ERROR = 173 + ALARM_ID_HD_RTC_OR_TIMER_ACCURACY_FAILURE = 174 + ALARM_ID_HD_PUMP_DIRECTION_STATUS_ERROR = 175 + ALARM_ID_TEST_ALARM_NO_RECIRC_CLR_TOP_ONLY = 176 + ALARM_ID_DG_SOFTWARE_FAULT = 177 + ALARM_ID_HD_COMM_TIMEOUT = 178 + ALARM_ID_DG_FPGA_COMM_TIMEOUT = 179 + ALARM_ID_DG_RO_FLOW_RATE_OUT_OF_MAX_RANGE = 180 + ALARM_ID_DG_LOAD_CELLS_TARE_WEIGHT_OUT_OF_RANGE = 181 + ALARM_ID_DG_LOAD_CELLS_INVALID_CAL_RECORD = 182 + ALARM_ID_DG_INVALID_LOAD_CELL_VALUE = 183 + ALARM_ID_DG_INLET_UV_REACTOR_NOT_HEALTHY = 184 + ALARM_ID_DG_FAN_RPM_OUT_OF_RANGE = 185 + ALARM_ID_DG_CONCENTRATE_PUMP_FAULT = 186 + ALARM_ID_DG_CP1_SPEED_CONTROL_ERROR = 187 + ALARM_ID_DG_CP2_SPEED_CONTROL_ERROR = 188 + ALARM_ID_DG_DRAIN_PUMP_RPM_OUT_OF_RANGE = 189 + ALARM_ID_DG_DRAIN_PUMP_OFF_FAULT = 190 + ALARM_ID_DG_FLOW_RATE_OUT_OF_UPPER_RANGE = 191 + ALARM_ID_DG_FLOW_RATE_OUT_OF_LOWER_RANGE = 192 + ALARM_ID_HD_PARTIAL_OCCLUSION_BLOOD_PUMP = 193 + ALARM_ID_DG_RO_PUMP_DUTY_CYCLE_OUT_OF_RANGE = 194 + ALARM_ID_DG_RO_PUMP_PRESSURE_OUT_OF_RANGE = 195 + ALARM_ID_DG_CPI_CPO_SENSORS_FPGA_FAULT = 196 + ALARM_ID_DG_CD1_CD2_SENSORS_FPGA_FAULT = 197 + ALARM_ID_DG_RO_FLOW_TOO_LOW_WHILE_PRIMARY_HEATER_IS_ON = 198 + ALARM_ID_DG_DIALYSATE_FLOW_TOO_LOW_WHILE_TRIMMER_HEATER_IS_ON = 199 + ALARM_ID_DG_THERMISTORS_TEMPERATURE_OUT_OF_RANGE = 200 + ALARM_ID_HD_PRE_TREATMENT_WET_FLOW_TEST_FAILURE = 201 + ALARM_ID_HD_PRE_TREATMENT_DRY_PRESSURE_NORMAL_TEST_FAILURE = 202 + ALARM_ID_DG_FILL_CONDUCTIVITIES_INVALID_CAL_RECORD = 203 + ALARM_ID_DG_RO_REJECTION_RATIO_OUT_OF_RANGE = 204 + ALARM_ID_DG_CONDUCTIVITY_SENSOR_FAULT = 205 + ALARM_ID_DG_DIALYSATE_FILL_OUT_OF_TIME = 206 + ALARM_ID_DG_FLOW_METER_CHECK_FAILURE = 207 + ALARM_ID_HD_VENOUS_LINE_OCCLUSION = 208 + ALARM_ID_DG_DRAIN_CIRCULATION_LINE_TIMEOUT = 209 + ALARM_ID_HD_BATTERY_PACK_ERROR_DETECTED = 210 + ALARM_ID_HD_BLOOD_SITTING_WARNING = 211 + ALARM_ID_HD_END_OF_TREATMENT_ALARM = 212 + ALARM_ID_HD_PRIME_COMPLETED_MEDIUM = 213 + ALARM_ID_AVAILABLE_30 = 214 + ALARM_ID_AVAILABLE_31 = 215 + ALARM_ID_HD_END_TREATMENT_TIMEOUT_ALARM = 216 + ALARM_ID_AVAILABLE_32 = 217 + ALARM_ID_HD_SYRINGE_DETECTED = 218 + ALARM_ID_HD_SYRINGE_PUMP_SYRINGE_REMOVED = 219 + ALARM_ID_DG_THD_SENSORS_FPGA_FAULT = 220 + ALARM_ID_HD_EMPTY_SALINE_BAG = 221 + ALARM_ID_HD_OCCLUSION_SENSOR_FPGA_FAULT = 222 + ALARM_ID_HD_ARTERIAL_SENSOR_FPGA_FAULT = 223 + ALARM_ID_HD_TREATMENT_STOPPED_BY_USER = 224 + ALARM_ID_HD_END_OF_TREATMENT_WARNING = 225 + ALARM_ID_HD_PRIME_COMPLETED_LOW_PRIORITY = 226 + ALARM_ID_AVAILABLE_15 = 227 + ALARM_ID_HD_PRIME_SALINE_PURGE_AIR_TIME_OUT = 228 + ALARM_ID_HD_PRIME_DIALYSATE_DIALYZER_TIME_OUT = 229 + ALARM_ID_HD_PRIME_DIALYSATE_BYPASS_TIME_OUT = 230 + ALARM_ID_HD_PRE_TREATMENT_DRY_PRESSURE_TEST_FAILURE = 231 + ALARM_ID_HD_PRE_TREATMENT_WET_LC_TEST_FAILURE = 232 + ALARM_ID_DG_BAROMETRIC_SENSOR_COEFFS_BAD_CRC = 233 + ALARM_ID_DG_INLET_WATER_PRESSURE_IN_HIGH_RANGE = 234 + ALARM_ID_HD_TREATMENT_STOPPED_AFTER_RINSEBACK = 235 + ALARM_ID_HD_INSTALL_NEW_CARTRIDGE = 236 + ALARM_ID_AVAILABLE_13 = 237 + ALARM_ID_HD_NO_CARTRIDGE_LOADED = 238 + ALARM_ID_HD_CARTRIDGE_REMOVAL_FAILURE = 239 + ALARM_ID_DG_BICARB_CONDUCTIVITY_OUT_OF_RANGE = 240 + ALARM_ID_DG_RESERVOIR_DRAIN_TIMEOUT = 241 + ALARM_ID_DG_RESERVOIR_FILL_TIMEOUT = 242 + ALARM_ID_DG_RESERVOIR_LEAK_TIMEOUT = 243 + ALARM_ID_DG_CLEANING_MODE_TEMP_SENSORS_DIFF_OUT_OF_RANGE = 244 + ALARM_ID_DG_HEAT_DISINFECT_TARGET_TEMP_TIMEOUT = 245 + ALARM_ID_DG_CLEANING_MODE_COND_SENSORS_OUT_OF_RANGE = 246 + ALARM_ID_AVAILABLE_7 = 247 + ALARM_ID_DG_CHEM_DISINFECT_TARGET_TEMP_OUT_OF_RANGE = 248 + ALARM_ID_DG_CHEM_DISINFECT_TARGET_COND_OUT_OF_RANGE = 249 + ALARM_ID_DG_CHEM_DISINFECT_INSERT_ACID = 250 + ALARM_ID_HD_INVALID_SYSTEM_RECORD_CRC = 251 + ALARM_ID_HD_INVALID_SERVICE_RECORD_CRC = 252 + ALARM_ID_DG_INVALID_SYSTEM_RECORD_CRC = 253 + ALARM_ID_DG_INVALID_SERVICE_RECORD_CRC = 254 + ALARM_ID_HD_UI_COMPATIBILITY_ERROR = 255 + ALARM_ID_HD_DISINFECT_CHEM_FLUSH = 256 + ALARM_ID_HD_INVALID_INSTITUTIONAL_RECORD_CRC = 257 + ALARM_ID_HD_UI_POST_FAILURE_OS_VERSION = 258 + ALARM_ID_HD_TEMPERATURES_OUT_OF_RANGE = 259 + ALARM_ID_HD_UI_POST_FAILURE_SHASUM = 260 + ALARM_ID_HD_UI_POST_FAILURE_CANBUS = 261 + ALARM_ID_HD_UI_POST_FAILURE_DISPLAY = 262 + ALARM_ID_HD_UI_POST_FAILURE_TOUCH = 263 + ALARM_ID_HD_UI_POST_FAILURE_SDCARD = 264 + ALARM_ID_HD_UI_POST_FAILURE_RTC = 265 + ALARM_ID_HD_UI_POST_FAILURE_WIFI = 266 + ALARM_ID_HD_UI_POST_FAILURE_BLUETOOTH = 267 + ALARM_ID_HD_UI_POST_FAILURE_ETHERNET = 268 + ALARM_ID_HD_UI_POST_FAILURE_SOUND = 269 + ALARM_ID_HD_SAFETY_SHUTDOWN_POST_TEST_FAILED = 270 + ALARM_ID_DG_SAFETY_SHUTDOWN_POST_TEST_FAILED = 271 + ALARM_ID_HD_FAN_RPM_OUT_OF_RANGE = 272 + ALARM_ID_AVAILABLE_10 = 273 + ALARM_ID_DG_INACTIVE_RESERVOIR_WEIGHT_OUT_OF_RANGE = 274 + ALARM_ID_HD_ARTERIAL_PRESSURE_OUT_OF_RANGE = 275 + ALARM_ID_HD_VENOUS_PRESSURE_OUT_OF_RANGE = 276 + ALARM_ID_HD_BP_OCCLUSION_OUT_OF_RANGE = 277 + ALARM_ID_HD_ACTIVE_RESERVOIR_WEIGHT_OUT_OF_RANGE = 278 + ALARM_ID_DG_DIALYSATE_DRAIN_TIME_OUT = 279 + ALARM_ID_DG_DRAIN_PUMP_DIRECTION_INVALID = 280 + ALARM_ID_DG_ACID_BOTTLE_LOW_VOLUME = 281 + ALARM_ID_DG_BICARB_BOTTLE_LOW_VOLUME = 282 + ALARM_ID_DG_LOAD_CELL_WEIGHT_OUT_OF_RANGE = 283 + ALARM_ID_DG_LOAD_CELL_PRIMARY_BACKUP_DRIFT_OUT_OF_RANGE = 284 + ALARM_ID_HD_RO_PERMEATE_SAMPLE = 285 + ALARM_ID_DG_CONCENTRATE_CAP_NOT_IN_PROPER_POSITION = 286 + ALARM_ID_HD_FPGA_CLOCK_SPEED_CHECK_FAILURE = 287 + ALARM_ID_HD_LOAD_CELL_PRIMARY_BACKUP_DRIFT_OUT_OF_RANGE = 288 + ALARM_ID_DG_DIALYSATE_CAP_NOT_IN_PROPER_POSITION = 289 + ALARM_ID_HD_VENOUS_SENSOR_FPGA_FAULT = 290 + ALARM_ID_HD_DISINFECT_FLUSH = 291 + ALARM_ID_HD_DISINFECT_HEAT = 292 + ALARM_ID_HD_DISINFECT_CHEM = 293 + ALARM_ID_HD_DISINFECT_HEAT_COOL = 294 + ALARM_ID_HD_UI_POST_FAILURE_CLOUDSYNC = 295 + ALARM_ID_DG_TDI_SENSORS_FPGA_FAULT = 296 + ALARM_ID_DG_TRO_SENSORS_FPGA_FAULT = 297 + ALARM_ID_DG_BARO_SENSOR_FPGA_FAULT = 298 + ALARM_ID_DG_INVALID_SERIAL_NUMBER = 299 + ALARM_ID_HD_INVALID_SERIAL_NUMBER = 300 + ALARM_ID_DG_RO_PERMEATE_SAMPLE_REMOVE_DIA_CAP = 301 + ALARM_ID_DG_CPU_RAM_ERROR = 302 + ALARM_ID_DG_DRAIN_PUMP_DIRECTION_FPGA_FAULT = 303 + ALARM_ID_HD_INVALID_USAGE_RECORD_CRC = 304 + ALARM_ID_HD_CPU_RAM_ERROR = 305 + ALARM_ID_AVAILABLE_50 = 306 + ALARM_ID_HD_AC_POWER_LOST_IN_TREATMENT = 307 + ALARM_ID_DG_CPI_COND_SENSOR_INVALID_CHAR = 308 + ALARM_ID_DG_CPO_COND_SENSOR_INVALID_CHAR = 309 + ALARM_ID_DG_CD1_COND_SENSOR_INVALID_CHAR = 310 + ALARM_ID_DG_CD2_COND_SENSOR_INVALID_CHAR = 311 + ALARM_ID_HD_DIAL_IN_FLOW_CHECK_FAILURE = 312 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_TEMP_TOO_LOW = 313 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_PRESSURE_TOO_HIGH = 314 + ALARM_ID_DG_CLEANING_MODE_INLET_WATER_PRESSURE_TOO_LOW = 315 + ALARM_ID_AVAILABLE_20 = 316 + ALARM_ID_AVAILABLE_21 = 317 + ALARM_ID_AVAILABLE_2 = 318 + ALARM_ID_DG_CHEM_DISINFECT_FLUSH_FLUSH_SAMPLE = 319 + ALARM_ID_DG_CHEM_DISINFECT_FLUSH_SAMPLE_TIMEOUT = 320 + ALARM_ID_DG_OUTLET_UV_REACTOR_NOT_HEALTHY = 321 + ALARM_ID_HD_UI_POST_FAILURE_INVALID_YEAR = 322 + ALARM_ID_HD_UI_POST_FAILURE_SETTINGS_BAD = 323 Index: leahi-dialin/common/alarm_priorities.py =================================================================== diff -u --- leahi-dialin/common/alarm_priorities.py (revision 0) +++ leahi-dialin/common/alarm_priorities.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,26 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file alarm_priorities.py +# +# @author (last) Quang Nguyen +# @date (last) 06-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 05-Apr-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class AlarmPriorities(DialinEnum): + ALARM_PRIORITY_NONE = 0 # Indicates not an alarm or no alarms active at this time + ALARM_PRIORITY_LOW = 1 # Low priority alarm + ALARM_PRIORITY_MEDIUM = 2 # Medium priority alarm + ALARM_PRIORITY_HIGH = 3 # High priority alarm + NUM_OF_ALARM_PRIORITIES = 4 # Total number of alarm priorities Index: leahi-dialin/common/dg_defs.py =================================================================== diff -u --- leahi-dialin/common/dg_defs.py (revision 0) +++ leahi-dialin/common/dg_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,334 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dg_defs.py +# +# @author (last) Dara Navaei +# @date (last) 08-Feb-2024 +# @author (original) Peter Lucia +# @date (original) 22-Jun-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class DGOpModes(DialinEnum): + DG_MODE_FAUL = 0 # Fault mode + DG_MODE_SERV = 1 # Service mode + DG_MODE_INIT = 2 # Initialization & POST mode + DG_MODE_STAN = 3 # Standby mode - connected to HD + DG_MODE_SOLO = 4 # Standby Solo mode - no HD connected + DG_MODE_GENE = 5 # Generation Idle mode + DG_MODE_FILL = 6 # Fill mode + DG_MODE_DRAI = 7 # Drain mode + DG_MODE_FLUS = 8 # Flush mode + DG_MODE_HEAT = 9 # Heat Disinfect mode + DG_MODE_CHEM = 10 # Chemical Disinfect mode + DG_MODE_CHFL = 11 # Chemical Disinfect Flush mode + DG_MODE_HCOL = 12 # Heat Disinfect Active Cool mode + DG_MODE_ROPS = 13 # RO Permeate Sample mode + DG_MODE_NLEG = 14 # Not legal - an illegal mode transition occurred + NUM_OF_DG_MODES = 15 # Number of DG operation modes + + +@unique +class DGInitStates(DialinEnum): + DG_POST_STATE_START = 0 + DG_POST_STATE_FW_COMPATIBILITY = 1 + DG_POST_STATE_FW_INTEGRITY = 2 + DG_POST_STATE_FPGA = 3 + DG_POST_STATE_RTC = 4 + DG_POST_STATE_NVDATAMGMT = 5 + DG_POST_STATE_TEMPERATURE_SENSORS = 6 + DG_POST_STATE_ACCELEROMETER = 7 + DG_POST_STATE_PRESSURES = 8 + DG_POST_STATE_RO_PUMP = 9 + DG_POST_STATE_DRAIN_PUMP = 10 + DG_POST_STATE_CONCENTRATE_PUMPS = 11 + DG_POST_STATE_CONDUCTIVITY_SENSORS = 12 + DG_POST_STATE_RESERVOIRS = 13 + DG_POST_STATE_UV_REACTORS = 14 + DG_POST_STATE_THERMISTORS = 15 + DG_POST_STATE_FANS = 16 + DG_POST_STATE_DIALYSATE_FLOW_SENSOR = 17 + DG_POST_STATE_WATCHDOG = 18 + DG_POST_STATE_SAFETY_SHUTDOWN = 19 + DG_POST_STATE_LOAD_CELL = 20 + DG_POST_STATE_COMPLETED = 21 + DG_POST_STATE_FAILED = 22 + NUM_OF_DG_POST_STATES = 23 + + +@unique +class DGFaultStates(DialinEnum): + DG_FAULT_STATE_START = 0 # DG fault start state + DG_FAULT_STATE_RUN_NV_POSTS = 1 # DG fault run RTC and NV data management post + DG_FAULT_STATE_COMPLETE = 2 # DG fault complete + NUM_OF_DG_FAULT_STATES = 3 # Number of fault mode states + +@unique +class DGSoloStates(DialinEnum): + DG_SOLO_STANDBY_STATE_START = 0 + DG_SOLO_IDLE_STATE = 1 + NUM_OF_DG_SOLO_STANDBY_STATES = 2 + + +@unique +class DGStandByModeStates(DialinEnum): + DG_STANDBY_MODE_STATE_IDLE = 0 # Idle standby mode state + DG_STANDBY_MODE_STATE_FLUSH_FILTER = 1 # Sample water flush filter state + DG_STANDBY_MODE_STATE_FLUSH_FILTER_IDLE = 2 # Sample water flush filter idle state + DG_STANDBY_MODE_STATE_SAMPLE_WATER = 3 # Sample water state + DG_STANDBY_MODE_STATE_PAUSE = 4 # Pause state + NUM_OF_DG_STANDBY_MODE_STATES = 5 # Number of standby mode states + + +@unique +class DGGenIdleModeStates(DialinEnum): + DG_GEN_IDLE_MODE_STATE_START = 0 + DG_GEN_IDLE_MODE_STATE_FLUSH_WATER = 1 + DG_GEN_IDLE_MODE_STATE_HANDLE_BAD_FILL = 2 + NUM_OF_DG_GEN_IDLE_MODE_STATES = 3 + + +@unique +class DGGenIdleModeBadFillSubStates(DialinEnum): + DG_HANDLE_BAD_FILL_STATE_START = 0 + DG_HANDLE_BAD_FILL_STATE_FIRST_DRAIN = 1 + DG_HANDLE_BAD_FILL_STATE_FLUSH_FILL = 2 + DG_HANDLE_BAD_FILL_STATE_SECOND_DRAIN = 3 + DG_HANDLE_BAD_FILL_STATE_REFILL = 4 + NUM_OF_DG_HANDLE_BAD_FILL_STATES = 6 + + +@unique +class DGFillModeStates(DialinEnum): + DG_FILL_MODE_STATE_TEST_INLET_WATER = 0 # Test inlet water quality state + DG_FILL_MODE_STATE_PRIME_CONCENTRATE_LINES = 1 # Prime the acid and bicarb concentrate lines + DG_FILL_MODE_STATE_FLUSH_BUBBLES = 2 # Flush the bubbles in the lines state + DG_FILL_MODE_STATE_TEST_BICARB_CONDUCTIVITY = 3 # Test the conductivity range of the bicarb concentrate state + DG_FILL_MODE_STATE_TEST_ACID_CONDUCTIVITY = 4 # Test the conductivity range of the acid concentrate state + DG_FILL_MODE_STATE_PRODUCE_DIALYSATE = 5 # Dialysate production state + DG_FILL_MODE_STATE_DELIVER_DIALYSATE = 6 # Dialysate deliver state + DG_FILL_MODE_STATE_PAUSED = 7 # Dialysate generation pause state + NUM_OF_DG_FILL_MODE_STATES = 8 # Number of fill mode states + + +@unique +class DGDrainModeStates(DialinEnum): + DG_DRAIN_STATE_START = 0 # Start drain mode state + DG_DRAIN_STATE_DRAIN = 1 # Drain drain mode state + DG_DRAIN_STATE_TARE = 2 # Tare drain mode state + DG_DRAIN_STATE_RINSE = 3 + NUM_OF_DG_DRAIN_STATES = 4 # Number of drain mode states + + +@unique +class DGFlushStates(DialinEnum): + DG_FLUSH_STATE_START = 0 + DG_FLUSH_STATE_DRAIN_R1 = 1 + DG_FLUSH_STATE_DRAIN_R2 = 2 + DG_FLUSH_STATE_FLUSH_DRAIN = 3 + DG_FLUSH_STATE_FLUSH_DIALYSATE = 4 + DG_FLUSH_STATE_FLUSH_CONCENTRATE_STRAWS = 5 + DG_FLUSH_STATE_FLUSH_R1_TO_R2 = 6 + DG_FLUSH_STATE_FLUSH_R2_AND_DRAIN_R1 = 7 + DG_FLUSH_STATE_FLUSH_CIRCULATION_DRAIN_LINE = 8 + DG_FLUSH_STATE_FLUSH_CIRCULATION = 9 + DG_FLUSH_STATE_FLUSH_WITH_FRESH_WATER = 10 + DG_FLUSH_STATE_CANCEL_BASIC_PATH = 11 + DG_FLUSH_STATE_CANCEL_WATER_PATH = 12 + DG_FLUSH_STATE_COMPLETE = 13 + NUM_OF_DG_FLUSH_STATES = 14 + + +@unique +class DGFlushUIStates(DialinEnum): + FLUSH_UI_STATE_NOT_RUNNING = 0 + FLUSH_UI_STATE_NOT_DRAIN_DEVICE = 1 + FLUSH_UI_STATE_NOT_FLUSH_RESERVOIRS = 2 + FLUSH_UI_STATE_NOT_DRAIN_RESERVOIRS = 3 + FLUSH_UI_STATE_NOT_FLUSH_RECIRCULATION_PATH = 4 + FLUSH_UI_STATE_NOT_CANCEL_FLUSH = 5 + FLUSH_UI_STATE_NOT_COMPLETE = 6 + NUM_OF_FLUSH_UI_STATES = 7 + + +@unique +class DGHeatDisinfectStates(DialinEnum): + DG_HEAT_DISINFECT_STATE_START = 0 # Heat disinfect, start mode state + DG_HEAT_DISINFECT_STATE_DRAIN_R1 = 1 # Heat disinfect, drain R1 state + DG_HEAT_DISINFECT_STATE_DRAIN_R2 = 2 # Heat disinfect, drain R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN = 3 # Heat disinfect, flush drain state + DG_HEAT_DISINFECT_STATE_FLUSH_CIRCULATION = 4 # Heat disinfect, flush circulation state + DG_HEAT_DISINFECT_STATE_FLUSH_R1_AND_R2 = 5 # Heat disinfect, flush R1 and R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_R2_AND_DRAIN_R1 = 6 # Heat disinfect, flush R2 and drain R1 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R2 = 7 # Heat disinfect, flush drain R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R1 = 8 # Heat disinfect, flush drain R1 state + DG_HEAT_DISINFECT_STATE_FILL_WITH_WATER = 9 # Heat disinfect, fill with water state + DG_HEAT_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 10 # Heat disinfect, disinfect R1 to R2 state + DG_HEAT_DISINFECT_STATE_PREPARE_FOR_HOT_WATER_TRANSITION = 11 # Heat disinfect, prepare for hot water transition + DG_HEAT_DISINFECT_STATE_FILL_R2_WITH_HOT_WATER = 12 # Heat disinfect, fill R2 with hot water state + DG_HEAT_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 13 # Heat disinfect, disinfect R2 to R1 state + DG_HEAT_DISINFECT_STATE_COOL_DOWN_HEATERS = 14 # Heat disinfect, cool down heaters state + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R1 = 15 # Heat disinfect, mix drain R1 state + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R2 = 16 # Heat disinfect, mix drain R2 state + DG_HEAT_DISINFECT_STATE_CANCEL_BASIC_PATH = 17 # Heat disinfect, cancel mode basic path state + DG_HEAT_DISINFECT_STATE_CANCEL_WATER_PATH = 18 # Heat disinfect, cancel mode water path state + DG_HEAT_DISINFECT_STATE_COMPLETE = 19 # Heat disinfect, complete state + DG_NELSON_HEAT_DISINFECT_STATE_FILL_R1_WITH_WATER = 20 # Heat disinfect, Nelson support state + NUM_OF_DG_HEAT_DISINFECT_STATES = 21 # Number of heat disinfect mode states + + +@unique +class DGHeatDisinfectUIStates(DialinEnum): + HEAT_DISINFECT_UI_STATE_NOT_RUNNING = 0 + HEAT_DISINFECT_UI_STATE_FLUSH_BEFORE_DISINFECT = 1 + HEAT_DISINFECT_UI_STATE_HEAT_UP_WATER = 2 + HEAT_DISINFECT_UI_STATE_DISINFECT_RESERVOIR_1 = 3 + HEAT_DISINFECT_UI_STATE_TRANSITION_HOT_WATER = 4 + HEAT_DISINFECT_UI_STATE_DISINFECT_RESERVOIR_2 = 5 + HEAT_DISINFECT_UI_STATE_COOL_DOWN_DEVICE = 6 + HEAT_DISINFECT_UI_STATE_FLUSH_AFTER_DISINFECT = 7 + HEAT_DISINFECT_UI_STATE_CANCEL_DISINFECT = 8 + HEAT_DISINFECT_UI_STATE_COMPLETE = 9 + NUM_OF_HEAT_DISINFECT_UI_STATES = 10 + + +@unique +class DGHeatDisinfectActiveCoolStates(DialinEnum): + DG_HEAT_DISINFECT_ACTIVE_COOL_STATE_START = 0 + DG_HEAT_DISINFECT_ACTIVE_COOL_MIX_DRAIN_R1_STATE = 1 + DG_HEAT_DISINFECT_ACTIVE_COOL_MIX_DRAIN_R2_STATE = 2 + DG_HEAT_DISINFECT_ACTIVE_COOL_FILL_R1_STATE = 3 + DG_HEAT_DISINFECT_ACTIVE_COOL_FILL_R2_STATE = 4 + DG_HEAT_DISINFECT_ACTIVE_COOL_DRAIN_R2_FILL_R1_TO_R2_STATE = 5 + DG_HEAT_DISINFECT_ACTIVE_COOL_DRAIN_R1_FILL_R2_TO_R1_STATE = 6 + DG_HEAT_DISINFECT_ACTIVE_COOL_DRAIN_R1_STATE = 7 + DG_HEAT_DISINFECT_ACTIVE_COOL_DRAIN_R2_STATE = 8 + DG_HEAT_DISINFECT_ACTIVE_COOL_CANCEL_WATER_PATH_STATE = 9 + DG_HEAT_DISINFECT_ACTIVE_COOL_CANCEL_BASIC_PATH_STATE = 10 + DG_HEAT_DISINFECT_ACTIVE_COOL_STATE_COMPLETE = 11 + + +@unique +class DGChemicalDisinfectStates(DialinEnum): + DG_CHEM_DISINFECT_STATE_START = 0 + DG_CHEM_DISINFECT_STATE_DRAIN_R1 = 1 + DG_CHEM_DISINFECT_STATE_DRAIN_R2 = 2 + DG_CHEM_DISINFECT_STATE_FLUSH_DRAIN = 3 + DG_CHEM_DISINFECT_STATE_DEPRIME_ACID_LINE = 4 + DG_CHEM_DISINFECT_STATE_FLUSH_CIRCULATION = 5 + DG_CHEM_DISINFECT_STATE_PRIME_DISINFECTANT = 6 + DG_CHEM_DISINFECT_STATE_DISINFECTANT_FLUSH = 7 + DG_CHEM_DISINFECT_STATE_FILL_WITH_DISINFECTANT = 8 + DG_CHEM_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 9 + DG_CHEM_DISINFECT_STATE_PARTIAL_DRAIN_R1_FILL_R2_TO_R1 = 10 + DG_CHEM_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 11 + DG_CHEM_DISINFECT_STATE_PARTIAL_DRAIN_R2_FILL_R1_TO_R2 = 12 + DG_CHEM_DISINFECT_STATE_CANCEL_BASIC_PATH = 13 + DG_CHEM_DISINFECT_STATE_CANCEL_WATER_PATH = 14 + DG_CHEM_DISINFECT_STATE_COMPLETE = 15 + NUM_OF_DG_CHEM_DISINFECT_STATES = 16 + + +@unique +class DGChemDisinfectFlushStates(DialinEnum): + DG_CHEM_DISINFECT_FLUSH_STATE_START = 0 + DG_CHEM_DISINFECT_FLUSH_STATE_DRAIN_R1 = 1 + DG_CHEM_DISINFECT_FLUSH_STATE_DRAIN_R2 = 2 + DG_CHEM_DISINFECT_FLUSH_STATE_FLUSH_DRAIN = 3 + DG_CHEM_DISINFECT_FLUSH_STATE_FLUSH_DISINFECTANT_LINE = 4 + DG_CHEM_DISINFECT_FLUSH_STATE_FLUSH_UF = 5 + DG_CHEM_DISINFECT_FLUSH_STATE_FLUSH_R2_TO_R1_DRAIN_R1 = 6 + DG_CHEM_DISINFECT_FLUSH_STATE_FLUSH_R1_TO_R2_DRAIN_R2 = 7 + DG_CHEM_DISINFECT_FLUSH_STATE_SAMPLE_FLUSH_R1_TO_R2_DRAIN_R2 = 8 + DG_CHEM_DISINFECT_FLUSH_STATE_CANCEL_BASIC_PATH = 9 + DG_CHEM_DISINFECT_FLUSH_STATE_CANCEL_WATER_PATH = 10 + DG_CHEM_DISINFECT_FLUSH_STATE_COMPLETE = 11 + NUM_OF_DG_CHEM_DISINFECT_FLUSH_STATES = 12 + + +@unique +class DGChemDisinfectFlushUIStates(DialinEnum): + CHEM_DISINFECT_FLUSH_UI_STATE_NOT_RUNNING = 0 + CHEM_DISINFECT_FLUSH_UI_STATE_FLUSH_AFTER_DISINFECT = 1 + CHEM_DISINFECT_FLUSH_UI_STATE_CANCEL_FLUSH = 2 + CHEM_DISINFECT_FLUSH_UI_STATE_COMPLETE = 3 + NUM_OF_CHEM_DISINFECT_FLUSH_UI_STATES = 4 + + +@unique +class DGChemDisinfectUIStates(DialinEnum): + CHEM_DISINFECT_UI_STATE_NOT_RUNNING = 0 + CHEM_DISINFECT_UI_STATE_FLUSH_BEFORE_DISINFECT = 1 + CHEM_DISINFECT_UI_STATE_MIX_WATER_AND_ACID = 2 + CHEM_DISINFECT_UI_STATE_REMOVE_ACID = 3 + CHEM_DISINFECT_UI_STATE_DISINFECT_DEVICE = 4 + CHEM_DISINFECT_UI_STATE_FLUSH_AFTER_DISINFECT = 5 + CHEM_DISINFECT_UI_STATE_CANCEL_DISINFECT = 6 + CHEM_DISINFECT_UI_STATE_COMPLETE = 7 + NUM_OF_CHEM_DISINFECT_UI_STATES = 8 + + +@unique +class DGROPermeateSampleStates(DialinEnum): + DG_RO_PERM_SAMPLE_STATE_START = 0 + DG_RO_PERM_SAMPLE_STATE_DRAIN_R1 = 1 + DG_RO_PERM_SAMPLE_STATE_DRAIN_R2 = 2 + DG_RO_PERM_SAMPLE_STATE_FLUSH_DRAIN = 3 + DG_RO_PERM_SAMPLE_STATE_FLUSH_DIALYSATE = 4 + DG_RO_PERM_SAMPLE_STATE_FLUSH_CONCENTRATE_STRAWS = 5 + DG_RO_PERM_SAMPLE_STATE_FLUSH_R2_TO_R1_AND_DRAIN_R1 = 6 + DG_RO_PERM_SAMPLE_STATE_COLLECT_SAMPLE = 7 + DG_RO_PERM_SAMPLE_STATE_CANCEL_BASIC_PATH = 8 + DG_RO_PERM_SAMPLE_STATE_CANCEL_WATER_PATH = 9 + DG_RO_PERM_SAMPLE_STATE_COMPLETE = 10 + NUM_OF_DG_RO_PERM_STATES = 11 + + +@unique +class DGEventList(DialinEnum): + DG_EVENT_STARTUP = 0 # DG startup event + DG_EVENT_OP_MODE_CHANGE = 1 # DG Op mode change event + DG_EVENT_SUB_MODE_CHANGE = 2 # DG Op sub-mode change event + DG_EVENT_CONCENTRATE_CAP_SWITCH_CHANGE = 3 # DG concentrate cap switch change + DG_EVENT_DIALYSATE_CAP_SWITCH_CHANGE = 4 # DG dialysate cap switch change + DG_EVENT_CPU_RAM_ERROR_STATUS = 5 # DG processor RAM error + DG_EVENT_CAL_RECORD_UPDATE = 6 # DG new calibration record updated + DG_EVENT_SYSTEM_RECORD_UPDATE = 7 # DG new system record has been updated + DG_EVENT_SERVICE_UPDATE = 8 # DG new service record has been updated + DG_EVENT_USAGE_INFO_UPDATE = 9 # DG new usage information has been updated + DG_EVENT_SW_CONFIG_UPDATE = 10 # DG new software configuration has been updated + DG_EVENT_SCHEDULED_RUNS_UPDATE = 11 # DG new scheduled runs information has been updated + DG_EVENT_HEATERS_INFO_UPDATE = 12 # DG new heaters information has been updated + DG_EVENT_AVG_DIALYSATE_FILL_COND_VALUES = 13 # DG average dialysate fill conductivity values + DG_EVENT_RESERVOIR_FILL_VALUES = 14 # DG reservoir fill base reservoir weight and filled volume values + DG_EVENT_OPERATION_STATUS = 15 # DG operation status event + DG_EVENT_TEMPERATURE_DRIFT = 16 # DG temperature drift event + DG_EVENT_BICARB_CHECK_RESULT = 17 # DG bicarb check result + DG_EVENT_ACID_CHECK_RESULT = 18 # DG acid check result + DG_EVENT_COND1_VS_COND2_DIFF_RESULT = 19 # DG CD1 (acid) vs. CD2 (bicarb) different result + NUM_OF_DG_EVENT_IDS = 20 # Total number of DG events + + +@unique +class DGEventDataType(DialinEnum): + EVENT_DATA_TYPE_NONE = 0 + EVENT_DATA_TYPE_U32 = 1 + EVENT_DATA_TYPE_S32 = 2 + EVENT_DATA_TYPE_F32 = 3 + EVENT_DATA_TYPE_BOOL = 4 + NUM_OF_EVENT_DATA_TYPES = 5 + + +@unique +class DGServiceModesStates(DialinEnum): + DG_SERVICE_STATE_START = 0 # Start service mode state + NUM_OF_DG_SERVICE_STATES = 1 # Number of service mode states Index: leahi-dialin/common/hd_defs.py =================================================================== diff -u --- leahi-dialin/common/hd_defs.py (revision 0) +++ leahi-dialin/common/hd_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,433 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_defs.py +# +# @author (last) Vinayakam Mani +# @date (last) 10-May-2024 +# @author (original) Peter Lucia +# @date (original) 04-Dec-2020 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class HDOpModes(DialinEnum): + MODE_FAUL = 0 # Fault mode + MODE_SERV = 1 # Service mode + MODE_INIT = 2 # Initialization & POST mode + MODE_STAN = 3 # Standby mode + MODE_TPAR = 4 # Treatment Parameters mode + MODE_PRET = 5 # Pre-Treatment mode + MODE_TREA = 6 # Treatment mode + MODE_POST = 7 # Post-Treatment mode + MODE_NLEG = 8 # Not legal - an illegal mode transition occurred + NUM_OF_MODES = 9 # Number of HD operation modes + + +@unique +class HDStandbyStates(DialinEnum): + STANDBY_START_STATE = 0 # Start standby (home actuators). + STANDBY_WAIT_FOR_TREATMENT_STATE = 1 # Wait for treatment. + STANDBY_WAIT_FOR_DISINFECT_STATE = 2 # Wait for UI to send disinfect option. + STANDBY_WAIT_FOR_DG_CLEANING_MODE_CMD_RESPONSE_STATE = 3 # Wait for DG cleaning mode command response state. + STANDBY_WAIT_FOR_DG_CLEANING_MODE_TO_START_STATE = 4 # Wait for DG cleaning mode to start state. + STANDBY_CLEANING_MODE_IN_PROGRESS_STATE = 5 # Cleaning mode in progress state. + NUM_OF_STANDBY_STATES = 6 # Number of standby states (sub-modes). + +@unique +class HDInitStates(DialinEnum): + POST_STATE_START = 0 # Start initialize & POST mode state + POST_STATE_FW_INTEGRITY = 1 # Run firmware integrity test state + POST_STATE_RTC = 2 # Run RTC test state + POST_STATE_NVDATAMGMT = 3 # Run NV Data Mgmt. test state + POST_STATE_WATCHDOG = 4 # Run watchdog test state + POST_STATE_SAFETY_SHUTDOWN = 5 # Run safety shutdown test state + POST_STATE_BLOOD_FLOW = 6 # Run blood flow test state + POST_STATE_DIALYSATE_INLET_FLOW = 7 # Run dialysate inlet flow test state + POST_STATE_DIALYSATE_OUTLET_FLOW = 8 # Run dialysate outlet flow test state + POST_STATE_BLOOD_LEAK = 9 # Run blood leak sensor test state + POST_STATE_VALVES = 10 # Run valves test state + POST_STATE_SYRINGE_PUMP = 11 # Run syringe pump test state + POST_STATE_PRES_OCCL = 12 # Run pressure occlusion state + POST_STATE_ALARM_AUDIO = 13 # Run alarm audio test state + POST_STATE_ALARM_LAMP = 14 # Run alarm lamp test state + POST_STATE_ACCELEROMETER = 15 # Run Accelerometer test state + POST_STATE_TEMPERATURES = 16 # Run temperatures POST state + POST_STATE_FANS = 17 # Run fans POST state + POST_STATE_STUCK_BUTTON = 18 # Run stuck button test state + POST_STATE_UI_POST = 19 # Check whether UI passed its POST tests + POST_STATE_FW_COMPATIBILITY = 20 # Run firmware compatibility test state + POST_STATE_FPGA = 21 # Run FPGA test state + POST_STATE_COMPLETED = 22 # POST self-tests completed state + POST_STATE_FAILED = 23 # POST self-tests failed state + NUM_OF_POST_STATES = 24 # Number of initialize & POST mode states + +@unique +class PreTreatmentSubModes(DialinEnum): + HD_PRE_TREATMENT_WATER_SAMPLE_STATE = 0 # Water sample state + HD_PRE_TREATMENT_SELF_TEST_CONSUMABLE_STATE = 1 # Consumable self-tests state + HD_PRE_TREATMENT_SELF_TEST_NO_CART_STATE = 2 # No cartridge self-tests state + HD_PRE_TREATMENT_CART_INSTALL_STATE = 3 # Consumable and cartridge installation state + HD_PRE_TREATMENT_SELF_TEST_DRY_STATE = 4 # Self-tests when the cartridge is dry state + HD_PRE_TREATMENT_PRIME_STATE = 5 # Prime blood and dialysate circuits and run wet self-tests state + HD_PRE_TREATMENT_RECIRCULATE_STATE = 6 # Re-circulate blood and dialysate circuits state + HD_PRE_TREATMENT_PATIENT_CONNECTION_STATE = 7 # Patient connection state + NUM_OF_HD_PRE_TREATMENT_STATES = 8 # Number of pre-treatment mode states + + +@unique +class PreTreatmentSampleWaterStates(DialinEnum): + SAMPLE_WATER_SETUP_STATE = 0 # Sample water setup (flush filter) state + SAMPLE_WATER_STATE = 1 # Sample water state, receiving sample water commands from the user + SAMPLE_WATER_COMPLETE_STATE = 2 # Sample water complete state + NUM_OF_SAMPLE_WATER_STATES = 3 # Number of sample water sub-mode states + + +@unique +class PreTreatmentConsumableSelfTestStates(DialinEnum): + CONSUMABLE_SELF_TESTS_INSTALL_STATE = 0 # Consumable self-tests install state + CONSUMABLE_SELF_TESTS_PRIME_STATE = 1 # Consumable self-tests prime concentrate lines state + CONSUMABLE_SELF_TESTS_BICARB_PUMP_CHECK_STATE = 2 # Consumable self-tests bicarbonate concentrate pump check state + CONSUMABLE_SELF_TESTS_ACID_PUMP_CHECK_STATE = 3 # Consumable self-tests acid concentrate pump check state + CONSUMABLE_SELF_TESTS_COMPLETE_STATE = 4 # Consumable self-tests complete state + NUM_OF_CONSUMABLE_SELF_TESTS_STATES = 5 # Number of consumable install sub-mode states + + +@unique +class PreTreatmentNoCartSelfTestStates(DialinEnum): + NO_CART_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 0 # Wait for door to be closed before running self-tests + NO_CART_SELF_TESTS_PRESSURE_CHECKS_STATE = 1 # No cartridge pressure sensors self-test state + NO_CART_SELF_TESTS_HOME_VALVES_STATE = 2 # No cartridge home valves state + NO_CART_SELF_TESTS_HOME_SYRINGE_PUMP_STATE = 3 # No cartridge home syringe pump state + NO_CART_SELF_TESTS_PUMPS_STATE = 4 # No cartridge self-test for blood pump, dialysate in pump, dialysate out pump state + NO_CART_SELF_TESTS_HOME_IDLE_STATE = 5 # Wait for valves and pumps finish homing state + NO_CART_SELF_TESTS_STOPPED_STATE = 6 # No cart self-test stopped state + NO_CART_SELF_TESTS_COMPLETE_STATE = 7 # No cartridge self-test complete state + NUM_OF_NO_CART_SELF_TESTS_STATES = 8 # Number of no cartridge self-tests states + + +@unique +class PreTreatmentWetSelfTestStates(DialinEnum): + WET_SELF_TESTS_START_STATE = 0 + WET_SELF_TESTS_BUBBLE_CHECK_SETUP_STATE = 1 + WET_SELF_TESTS_BUBBLE_CHECK_STATE = 2 + WET_SELF_TESTS_PRIME_CHECK_STATE = 3 + WET_SELF_TESTS_BLOOD_LEAK_DETECTOR_DEBUBBLE_STATE = 4 + WET_SELF_TESTS_BLOOD_LEAK_DETECTOR_STATE = 5 + WET_SELF_TESTS_FIRST_DISPLACEMENT_SETUP_STATE = 6 + WET_SELF_TESTS_FIRST_DISPLACEMENT_STATE = 7 + WET_SELF_TESTS_FIRST_DISPLACEMENT_VERIFY_STATE = 8 + WET_SELF_TESTS_SECOND_DISPLACEMENT_SETUP_STATE = 9 + WET_SELF_TESTS_SECOND_DISPLACEMENT_STATE = 10 + WET_SELF_TESTS_SECOND_DISPLACEMENT_VERIFY_STATE = 11 + WET_SELF_TESTS_STOPPED_STATE = 12 + WET_SELF_TESTS_COMPLETE_STATE = 13 + +@unique +class HDPreTreatmentReservoirMgmtStates(DialinEnum): + PRE_TREATMENT_RESERVOIR_MGMT_START_STATE = 0 # Wait for signal to start drain and fill reservoirs + PRE_TREATMENT_RESERVOIR_MGMT_DRAIN_CMD_STATE = 1 # Command DG to start draining reservoir + PRE_TREATMENT_RESERVOIR_MGMT_DRAIN_CMD_RESP_STATE = 2 # After sending drain command, process DG drain command response + PRE_TREATMENT_RESERVOIR_MGMT_START_FILL_STATE = 3 # Command DG to start filling reservoir + PRE_TREATMENT_RESERVOIR_MGMT_FILL_CMD_RESP_STATE = 4 # After sending fill command, process DG fill command response + PRE_TREATMENT_RESERVOIR_MGMT_FILL_COMPLETE_STATE = 5 # Reservoir fill has completed + PRE_TREATMENT_RESERVOIR_MGMT_REQUEST_RESERVOIR_SWITCH_STATE = 6 # Command DG to switch (toggle) reservoirs + PRE_TREATMENT_RESERVOIR_MGMT_WAIT_FOR_RESERVOIR_SWITCH_STATE = 7 # After sending switch command, process DG fill command response + PRE_TREATMENT_RESERVOIR_MGMT_COMPLETE_STATE = 8 # Pre-treatment reservoir management complete state + NUM_OF_PRE_TREATMENT_RESERVOIR_MGMT_STATES = 9 # Number of pre-treatments reservoir mgmt. states + +# Heparin states +@unique +class HeparinStates(DialinEnum): + HEPARIN_STATE_OFF = 0 # No heparin delivery is in progress + HEPARIN_STATE_STOPPED = 1 # Heparin delivery stopped by alarm or not yet started + HEPARIN_STATE_PAUSED = 2 # Heparin delivery paused + HEPARIN_STATE_INITIAL_BOLUS = 3 # Initial heparin bolus delivery in progress + HEPARIN_STATE_DISPENSING = 4 # Gradual heparin dispensing in progress + HEPARIN_STATE_COMPLETED = 5 # Heparin delivery stopped due to the set stop time before treatment end + HEPARIN_STATE_EMPTY = 6 # Heparin Syringe empty + NUM_OF_HEPARIN_STATES = 7 # Number of saline bolus states + + +# Syringe pump states +@unique +class SyringePumpStates(DialinEnum): + SYRINGE_PUMP_INIT_STATE = 0 # Syringe pump initialize state + SYRINGE_PUMP_OFF_STATE = 1 # Syringe pump off state + SYRINGE_PUMP_RETRACT_STATE = 2 # Syringe pump retract state + SYRINGE_PUMP_PRELOAD_STATE = 3 # Syringe pump preload state + SYRINGE_PUMP_SEEK_STATE = 4 # Syringe pump seek state + SYRINGE_PUMP_PRIME_STATE = 5 # Syringe pump prime state + SYRINGE_PUMP_HEP_BOLUS_STATE = 6 # Syringe pump bolus state + SYRINGE_PUMP_HEP_CONTINUOUS_STATE = 7 # Syringe pump continuous state + SYRINGE_PUMP_CONFIG_FORCE_SENSOR_STATE = 8 # Syringe pump configure force sensor state + + +# Syringe pump operations +@unique +class SyringePumpOperations(DialinEnum): + SYRINGE_PUMP_OP_STOP = 0 # Stop syringe pump + SYRINGE_PUMP_OP_RETRACT = 1 # Retract syringe pump + SYRINGE_PUMP_OP_SEEK = 2 # Seek plunger + SYRINGE_PUMP_OP_PRIME = 3 # Prime Heparin line + SYRINGE_PUMP_OP_BOLUS = 4 # Deliver Heparin bolus of set volume over 5 minutes + SYRINGE_PUMP_OP_CONTINUOUS = 5 # Continuous dispense of Heparin at set rate + + +@unique +class PostTreatmentStates(DialinEnum): + HD_POST_TREATMENT_DRAIN_RESERVOIRS_STATE = 0 # Drain reservoirs state + HD_POST_TREATMENT_PATIENT_DISCONNECTION_STATE = 1 # Patient disconnection state + HD_POST_TREATMENT_DISPOSABLE_REMOVAL_STATE = 2 # Disposable removal state + HD_POST_TREATMENT_VERIFY_STATE = 3 # Verify cartridge removed, syringe removed, and reservoirs drained state + NUM_OF_HD_POST_TREATMENT_STATES = 4 # Number of post-treatment mode states + + +@unique +class PreTreatmentCartridgeInstallStates(DialinEnum): + CARTRIDGE_INSTALL_STATE = 0 # Pre-treatment Cartridge Install state. + NUM_OF_CARTRIDGE_INSTALL_STATES = 1 # Number of pre-treatment Cartridge Install states. + + +@unique +class PreTreatmentDrySelfTestsStates(DialinEnum): + """ + The HD Pre-Treatment dry self test states + """ + DRY_SELF_TESTS_START_STATE = 0 # Dry self-tests starting state + DRY_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 1 # Wait for door to close before executing self-tests + DRY_SELF_TESTS_USED_CARTRIDGE_CHECK_STATE = 2 # Used cartridge check dry self-test state + DRY_SELF_TESTS_CARTRIDGE_LOADED_CHECK_STATE = 3 # Cartridge loaded check dry self-test state + DRY_SELF_TESTS_SYRINGE_PUMP_SEEK_STATE = 4 # Seek syringe pumps state + DRY_SELF_TESTS_PRESSURE_SENSORS_NORMAL_SETUP_STATE = 5 # Pressure sensor setup state. + DRY_SELF_TESTS_PRESSURE_SENSORS_VENOUS_SETUP_STATE = 6 # Venous pressure sensor dry self-test setup valves and pump state + DRY_SELF_TESTS_PRESSURE_SENSORS_VENOUS = 7 # Venous pressure sensor dry self-test + DRY_SELF_TESTS_PRESSURE_SENSORS_ARTERIAL_SETUP_STATE = 8 # Arterial pressure sensor dry self-test setup valves and pump state + DRY_SELF_TESTS_PRESSURE_SENSORS_ARTERIAL = 9 # Arterial pressure sensor dry self-test + DRY_SELF_TESTS_PRESSURE_SENSORS_DECAY_STATE = 10 # Pressure sensors verify pressure loss state + DRY_SELF_TESTS_PRESSURE_SENSORS_STABILITY_STATE = 11 # Pressure sensors verify pressure stability state + DRY_SELF_TESTS_PRESSURE_SENSORS_NORMAL_STATE = 12 # Pressure sensors verify normal pressure readings state + DRY_SELF_TESTS_SYRINGE_PUMP_PRIME_STATE = 13 # Prime syringe pump state + DRY_SELF_TESTS_SYRINGE_PUMP_OCCLUSION_DETECTION_STATE = 14 # Occlusion detection state + DRY_SELF_TESTS_STOPPED_STATE = 15 # Dry self-test stopped state + DRY_SELF_TESTS_COMPLETE_STATE = 16 # Dry self-test complete state + NUM_OF_DRY_SELF_TESTS_STATES = 17 # Number of dry self-tests states + + +@unique +class PreTreatmentPrimeStates(DialinEnum): + HD_PRIME_WAIT_FOR_USER_START_STATE = 0 # Wait for user to start prime state + HD_PRIME_SALINE_SETUP_STATE = 1 # Saline setup state + HD_PRIME_SALINE_PURGE_AIR_STATE = 2 # Saline purge air state + HD_PRIME_SALINE_CIRC_BLOOD_CIRCUIT_STATE = 3 # Circulate blood circuit state + HD_PRIME_RESERVOIR_ONE_FILL_COMPLETE_STATE = 4 # Wait for reservoir 1 fill complete + HD_PRIME_DIALYSATE_DIALYZER_STATE = 5 # Dialysate dialyzer fluid path state + HD_PRIME_SALINE_DIALYZER_SETUP_STATE = 6 # Saline dialyzer setup state + HD_PRIME_SALINE_DIALYZER_STATE = 7 # Saline dialyzer fluid path state + HD_PRIME_RESERVOIR_TWO_FILL_COMPLETE_STATE = 8 # Wait for reservoir 2 fill complete + HD_PRIME_DIALYSATE_BYPASS_STATE = 9 # Dialysate bypass fluid path state + HD_PRIME_WET_SELF_TESTS_STATE = 10 # Perform wet self-tests after priming complete + HD_PRIME_PAUSE = 11 # Prime pause state, waits to be resumed + HD_PRIME_COMPLETE = 12 # Prime complete state + NUM_OF_HD_PRIME_STATES = 13 # Number of prime sub-mode states + + +@unique +class PreTreatmentRecircStates(DialinEnum): + PRE_TREATMENT_RECIRC_STATE = 0 # Pre-treatment recirculate state + PRE_TREATMENT_RECIRC_STOPPED_STATE = 1 # Pre-treatment recirculate stopped state + NUM_OF_PRE_TREATMENT_RECIRC_STATES = 2 # Number of pre-treatment recirculate states + + +@unique +class TreatmentRecircStates(DialinEnum): + TREATMENT_RECIRC_DISCONNECT_PATIENT_STATE = 0 # Disconnect patient state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_RECIRC_STATE = 1 # Re-circulate Dialysate state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_STOPPED_STATE = 2 # Stopped state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_RECONNECT_PATIENT_STATE = 3 # Reconnect patient state of the treatment re-circulate sub-mode state machine + NUM_OF_TREATMENT_RECIRC_STATES = 4 # Number of treatment re-circulate sub-mode states + + +@unique +class PreTreatmentPatientConnectionStates(DialinEnum): + PRE_TREATMENT_PAT_CONN_WAIT_FOR_UF_VOL_STATE = 0 # Pre-treatment patient connect wait for UF volume setting state + PRE_TREATMENT_PAT_CONN_WAIT_FOR_DLZR_INVERT_STATE = 1 # Pre-treatment patient connect wait for dialyzer inverted state + PRE_TREATMENT_PAT_CONN_WAIT_FOR_USER_CONFIRM_STATE = 2 # Pre-treatment patient connect wait for user confirm state + PRE_TREATMENT_PAT_CONN_WAIT_FOR_TREATMENT_START_STATE = 3 # Pre-treatment patient connect wait for treatment start state + NUM_OF_PRE_TREATMENT_PAT_CONN_STATES = 4 # Number of pre-treatment patient connect states + + +@unique +class TreatmentParametersStates(DialinEnum): + HD_TREATMENT_PARAMS_MODE_STATE_WAIT_4_UI_2_SEND = 0 # Wait for UI to send treatment params mode state + HD_TREATMENT_PARAMS_MODE_STATE_WAIT_4_UI_2_CONFIRM = 1 # Wait for UI to confirm treatment params mode state + NUM_OF_HD_TREATMENT_PARAMS_MODE_STATES = 2 # Number of treatment params mode states + + +@unique +class TreatmentStates(DialinEnum): + TREATMENT_START_STATE = 0 # Start treatment - initialize treatment and go to blood prime state + TREATMENT_BLOOD_PRIME_STATE = 1 # Prime blood-side of dialyzer with gradual ramp for 1 min. while dialyzer is bypassed. No dialysis or UF taking place. No treatment time. + TREATMENT_DIALYSIS_STATE = 2 # Perform dialysis. Deliver Heparin as prescribed. Deliver UF as prescribed. Handle saline boluses as requested + TREATMENT_STOP_STATE = 3 # Treatment stopped. All pumps off. Dializer bypassed + TREATMENT_RINSEBACK_STATE = 4 # Perform rinseback with saline. Dialyzer bypassed. Dialysate recirculating + TREATMENT_RECIRC_STATE = 5 # Recirculate saline and dialysate while patient disconnected. Blood lines open and shunted. Dialyzer is bypassed + TREATMENT_END_STATE = 6 # Dialysis has ended. Blood pump slowed. Dialyzer is bypassed. Dialysate is recirculated. User can rinseback + NUM_OF_TREATMENT_STATES = 7 # Number of treatment states (sub-modes) + +@unique +class TreatmentBloodPrimeStates(DialinEnum): + BLOOD_PRIME_RAMP_STATE = 0 # Ramp state of the blood prime sub-mode state machine + NUM_OF_BLOOD_PRIME_STATES = 1 # Number of blood prime sub-mode states + +@unique +class TreatmentDialysisStates(DialinEnum): + DIALYSIS_START_STATE = 0 # Start state of dialysis sub-mode state machine + DIALYSIS_UF_STATE = 1 # Ultrafiltration state of the dialysis sub-mode state machine + DIALYSIS_SALINE_BOLUS_STATE = 2 # Saline bolus state of the dialysis sub-mode state machine + NUM_OF_DIALYSIS_STATES = 3 # Number of dialysis sub-mode states + +@unique +class TreatmentStopStates(DialinEnum): + TREATMENT_STOP_RECIRC_STATE = 0 # Dialysate and Blood re-circulation state of the treatment stop sub-mode state machine + TREATMENT_STOP_RECIRC_DIALYSATE_ONLY_STATE = 1 # Re-circulate Dialysate only state of the treatment re-circulate sub-mode state machine + TREATMENT_STOP_RECIRC_BLOOD_ONLY_STATE = 2 # Re-circulate Blood only state of the treatment re-circulate sub-mode state machine + TREATMENT_STOP_NO_RECIRC_STATE = 3 # No re-circulation state of the treatment stop sub-mode state machine + NUM_OF_TREATMENT_STOP_STATES = 4 # Number of treatment stop sub-mode states + +@unique +class TreatmentRinsebackStates(DialinEnum): + RINSEBACK_STOP_INIT_STATE = 0 # Start state (stopped) of the rinseback sub-mode state machine + RINSEBACK_RUN_STATE = 1 # Rinseback running state of the rinseback sub-mode state machine + RINSEBACK_PAUSED_STATE = 2 # Rinseback paused state of the rinseback sub-mode state machine + RINSEBACK_STOP_STATE = 3 # Rinseback stopped (done) state of the rinseback sub-mode state machine + RINSEBACK_RUN_ADDITIONAL_STATE = 4 # Additional rinseback volume (10 mL) state of the rinseback sub-mode state machine + RINSEBACK_RECONNECT_PATIENT_STATE = 5 # Rinseback reconnect patient state of the rinseback sub-mode state machine + NUM_OF_RINSEBACK_STATES = 6 # Number of rinseback sub-mode states + +@unique +class TreatmentRecircStates(DialinEnum): + TREATMENT_RECIRC_DISCONNECT_PATIENT_STATE = 0 # Disconnect patient state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_RECIRC_STATE = 1 # Re-circulate Dialysate state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_STOPPED_STATE = 2 # Stopped state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_RECONNECT_PATIENT_STATE = 3 # Reconnect patient state of the treatment re-circulate sub-mode state machine + NUM_OF_TREATMENT_RECIRC_STATES = 4 # Number of treatment re-circulate sub-mode states + +@unique +class TreatmentEndStates(DialinEnum): + TREATMENT_END_WAIT_FOR_RINSEBACK_STATE = 0 # Wait for rinseback state of the treatment end sub-mode state machine + TREATMENT_END_PAUSED_STATE = 1 # Paused state of the treatment end sub-mode state machine + NUM_OF_TREATMENT_END_STATES = 2 # Number of treatment end sub-mode states + +@unique +class HDFaultStates(DialinEnum): + HD_FAULT_STATE_START = 0 # Start fault state + HD_FAULT_STATE_RUN_NV_POSTS = 1 # HD fault run NV posts state + HD_FAULT_STATE_COMPLETE = 2 # HD fault run complete state + NUM_OF_HD_FAULT_STATES = 3 # Number of fault mode states + +@unique +class HDEventList(DialinEnum): + HD_EVENT_STARTUP = 0 # HD startup event + HD_EVENT_OP_MODE_CHANGE = 1 # HD Op mode change event + HD_EVENT_SUB_MODE_CHANGE = 2 # HD Op sub-mode change event + HD_EVENT_DRY_SELF_TEST_CARTRIDGE_RESULT = 3 # HD dry self test cartridge result + HD_EVENT_DRY_SELF_TEST_PRESSURE_RESULT = 4 # HD dry self test pressure result + HD_EVENT_WET_SELF_TEST_DISPLACEMENT_RESULT = 5 # HD wet self test displacement result + HD_EVENT_CPU_RAM_ERROR_STATUS = 6 # HD CPU RAM error status + HD_EVENT_CAL_RECORD_UPDATE = 7 # HD new calibration record updated + HD_EVENT_SYSTEM_RECORD_UPDATE = 8 # HD new system record has been updated + HD_EVENT_SERVICE_UPDATE = 9 # HD new service record has been updated + HD_EVENT_USAGE_INFO_UPDATE = 10 # HD new usage information has been updated + HD_EVENT_SW_CONFIG_UPDATE = 11 # HD new software configuration has been updated + HD_EVENT_BUTTON = 12 # HD button pressed/released + HD_EVENT_SAFETY_LINE = 13 # HD safety line pulled/released + HD_EVENT_RSRVR_1_LOAD_CELL_START_VALUES = 14 # HD reservoir 1 load cells start values + HD_EVENT_RSRVR_1_LOAD_CELL_END_VALUES = 15 # HD reservoir 2 load cells end values + HD_EVENT_RSRVR_2_LOAD_CELL_START_VALUES = 16 # HD reservoir 2 load cells start values + HD_EVENT_RSRVR_2_LOAD_CELL_END_VALUES = 17 # HD reservoir 2 load cells end values + HD_EVENT_SUB_STATE_CHANGE = 18 # HD Op sub-state change event + HD_EVENT_SYRINGE_PUMP_STATE = 19 # HD syringe pump state change event + HD_EVENT_OCCLUSION_BASELINE = 20 # HD event occlusion baseline event + HD_EVENT_RSRVR_UF_VOLUME_AND_TIME = 21 # HD ultrafiltration volume and time for a reservoir use + HD_EVENT_RSRVR_UF_RATE = 22 # HD ultrafiltration measured and expected rates + HD_EVENT_OPERATION_STATUS = 23 # HD operation status event. + HD_EVENT_AIR_TRAP_FILL = 24 # HD initiated an air trap fill (opened VBT briefly). + HD_EVENT_AIR_PUMP_ON_OFF = 25 # HD turned air pump on or off. + HD_EVENT_BLOOD_LEAK_SELF_TEST_RESULT = 26 # HD Blood leak self test result. + HD_EVENT_BLOOD_LEAK_NUM_OF_SET_POINT_CHECK_FAILURES = 27 # HD blood leak number of setpoint check failures + HD_EVENT_DRY_SELF_TEST_PRESSURE_DECAY_WAIT_PERIOD = 28 # HD dry self test pressure decay wait period + HD_EVENT_INSTIT_RECORD_UPDATE = 29 # HD new institutional record has been updated. + HD_EVENT_PARTIAL_OCCLUSION_BASELINE = 30 # HD event partial occlusion baseline event + NUM_OF_EVENT_IDS = 31 # Total number of HD events + +@unique +class HDEventDataType(DialinEnum): + EVENT_DATA_TYPE_NONE = 0 + EVENT_DATA_TYPE_U32 = 1 + EVENT_DATA_TYPE_S32 = 2 + EVENT_DATA_TYPE_F32 = 3 + EVENT_DATA_TYPE_BOOL = 4 + NUM_OF_EVENT_DATA_TYPES = 5 + +@unique +class UFStates(DialinEnum): + UF_PAUSED_STATE = 0 # Paused state of the ultrafiltration state machine + UF_RUNNING_STATE = 1 # Running state of the ultrafiltration state machine + NUM_OF_UF_STATES = 2 # Number of ultrafiltration states + +@unique +class SalineBolusStates(DialinEnum): + SALINE_BOLUS_STATE_IDLE = 0 # No saline bolus delivery is in progress + SALINE_BOLUS_STATE_WAIT_FOR_PUMPS_STOP = 1 # Wait for pumps to stop before starting bolus + SALINE_BOLUS_STATE_IN_PROGRESS = 2 # A saline bolus delivery is in progress + SALINE_BOLUS_STATE_MAX_DELIVERED = 3 # Maximum saline bolus volume reached - no more saline bolus deliveries allowed + NUM_OF_SALINE_BOLUS_STATES = 4 # Number of saline bolus states + +@unique +class TreatmentParameters(DialinEnum): + TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN = 0 + TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN = 1 + TREATMENT_PARAM_TREATMENT_DURATION_MIN = 2 + TREATMENT_PARAM_HEPARIN_PRESTOP_MIN = 3 + TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML = 4 + TREATMENT_PARAM_ACID_CONCENTRATE = 5 + TREATMENT_PARAM_BICARB_CONCENTRATE = 6 + TREATMENT_PARAM_DIALYZER_TYPE = 7 + TREATMENT_PARAM_HEPARIN_TYPE = 8 + TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN = 9 + TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN = 10 + TREATMENT_PARAM_ART_PRES_LIMIT_WINDOW = 11 + TREATMENT_PARAM_VEN_PRES_LIMIT_WINDOW = 12 + TREATMENT_PARAM_VEN_PRES_LIMIT_ASYMMETRIC = 13 + TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR = 14 + TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML = 15 + TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C = 16 + TREATMENT_PARAM_UF_VOLUME_L = 17 + NUM_OF_TREATMENT_PARAMS = 18 + +class Acid_Concentrates(DialinEnum): + ACID_CONC_TYPE_FRESENIUS_08_1251_1 = 0 + ACID_CONC_TYPE_FRESENIUS_08_2251_0 = 1 + ACID_CONC_TYPE_FRESENIUS_08_3251_9 = 2 + NUM_OF_ACID_CONC_TYPES = 3 + +class Bicarb_Concentrates(DialinEnum): + BICARB_CONC_TYPE_FRESENIUS_CENTRISOL = 0 + NUM_OF_BICARB_CONC_TYPES = 1 + +@unique +class PowerOffCommands(DialinEnum): + """ + power off commands enum + """ + PW_COMMAND_OPEN = 0 + PW_TIMEOUT_CLOSE = 1 + PW_REJECT_SHOW = 2 Index: leahi-dialin/common/msg_defs.py =================================================================== diff -u --- leahi-dialin/common/msg_defs.py (revision 0) +++ leahi-dialin/common/msg_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,231 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file msg_defs.py +# +# @author (last) Dara Navaei +# @date (last) 08-May-2024 +# @author (original) Peter Lucia +# @date (original) 07-Aug-2020 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum +from .msg_ids import MsgIds + + +# Define msg ids that are not yet added to common but are needed in leahi-dialin + +@unique +class MsgIdsDialin(DialinEnum): + + MSG_DIALIN_ID_HD_SERIAL_NUMBER_RESPONSE = 0x87 + MSG_DIALIN_ID_DG_SERIAL_NUMBER_RESPONSE = 0x88 + MSG_DIALIN_ID_UI_SYSTEM_USAGE_REQUEST = 0x89 + MSG_DIALIN_ID_HD_SYSTEM_USAGE_RESPONSE = 0x8A + MSG_DIALIN_ID_DG_SYSTEM_USAGE_RESPONSE = 0x8C + MSG_DIALIN_ID_HD_FLUID_LEAK_STATE_DETECTOR_OVERRIDE = 0x8047 + MSG_DIALIN_ID_HD_VALVES_POSITION_COUNT_OVERRIDE = 0x8058 + MSG_DIALIN_ID_HD_DISINFECT_STATE = 0x7E + MSG_DIALIN_ID_HD_VERSION_REQUEST = 0x9E + MSG_DIALIN_ID_UI_POST_REPORT_VERSION = 0x9F + + +ACK_NOT_REQUIRED = [ + MsgIds.MSG_ID_ALARM_STATUS_DATA.value, + MsgIds.MSG_ID_ALARM_TRIGGERED.value, + MsgIds.MSG_ID_ALARM_CLEARED.value, + MsgIds.MSG_ID_BLOOD_FLOW_DATA.value, + MsgIds.MSG_ID_DIALYSATE_FLOW_DATA.value, + MsgIds.MSG_ID_PRESSURE_OCCLUSION_DATA.value, + MsgIds.MSG_ID_RTC_EPOCH_DATA.value, + MsgIds.MSG_ID_DIALYSATE_OUT_FLOW_DATA.value, + MsgIds.MSG_ID_LOAD_CELL_READINGS_DATA.value, + MsgIds.MSG_ID_TREATMENT_TIME_DATA.value, + MsgIds.MSG_ID_POWER_OFF_WARNING.value, + MsgIds.MSG_ID_TREATMENT_STATE_DATA.value, + MsgIds.MSG_ID_RO_PUMP_DATA.value, + MsgIds.MSG_ID_DG_PRESSURES_DATA.value, + MsgIds.MSG_ID_DRAIN_PUMP_DATA.value, + MsgIds.MSG_ID_HD_OP_MODE_DATA.value, + MsgIds.MSG_ID_DG_OP_MODE_DATA.value, + MsgIds.MSG_ID_DG_RESERVOIRS_DATA.value, + MsgIds.MSG_ID_DG_VALVES_STATES_DATA.value, + MsgIds.MSG_ID_DG_HEATERS_DATA.value, + MsgIds.MSG_ID_DG_TEMPERATURE_DATA.value, + MsgIds.MSG_ID_SALINE_BOLUS_DATA.value, + MsgIds.MSG_ID_DG_CONDUCTIVITY_DATA.value, + MsgIds.MSG_ID_HD_ACCELEROMETER_DATA.value, + MsgIds.MSG_ID_DG_ACCELEROMETER_DATA.value, + MsgIds.MSG_ID_DG_HEAT_DISINFECT_DATA.value, + MsgIds.MSG_ID_HD_VALVES_DATA.value, + MsgIds.MSG_ID_HD_AIR_TRAP_DATA.value, + MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value, + MsgIds.MSG_ID_DG_CONCENTRATE_PUMP_DATA.value, + MsgIds.MSG_ID_HD_PRIMING_STATUS_DATA.value, + MsgIds.MSG_ID_DG_UV_REACTORS_DATA.value, + MsgIds.MSG_ID_DG_THERMISTORS_DATA.value, + MsgIds.MSG_ID_DG_FANS_DATA.value, + MsgIds.MSG_ID_HD_TREATMENT_STOP_TIMER_DATA.value, + MsgIds.MSG_ID_HD_HEPARIN_DATA.value, + MsgIds.MSG_ID_HD_RINSEBACK_PROGRESS.value, + MsgIds.MSG_ID_HD_BLOOD_PRIME_PROGRESS_DATA.value, + MsgIds.MSG_ID_HD_RECIRC_PROGRESS_DATA.value, + MsgIds.MSG_ID_PRE_TREATMENT_STATE_DATA.value, + MsgIds.MSG_ID_DG_FILTER_FLUSH_PROGRESS_DATA.value, + MsgIds.MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS_DATA.value, + MsgIds.MSG_ID_HD_DRY_SELF_TEST_PROGRESS_DATA.value, + MsgIds.MSG_ID_HD_SYRINGE_PUMP_DATA.value, + MsgIds.MSG_ID_HD_FLUID_LEAK_STATE_DATA.value, + MsgIds.MSG_ID_DG_FLUID_LEAK_STATE_DATA.value, + MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA.value, + MsgIds.MSG_ID_HD_POST_TREATMENT_STATE_DATA.value, + MsgIds.MSG_ID_DG_FLUSH_DATA.value, + MsgIds.MSG_ID_HD_VOLTAGES_DATA.value, + MsgIds.MSG_ID_HD_ALARM_INFORMATION_DATA.value, + MsgIds.MSG_ID_DG_HEAT_DISINFECT_TIME_DATA.value, + MsgIds.MSG_ID_DG_CHEM_DISINFECT_TIME_DATA.value, + MsgIds.MSG_ID_DG_VOLTAGES_DATA.value, + MsgIds.MSG_ID_DG_CHEM_DISINFECT_DATA.value, + MsgIds.MSG_ID_HD_BUBBLES_DATA.value, + MsgIds.MSG_ID_HD_TEMPERATURES_DATA.value, + MsgIds.MSG_ID_DG_SWITCHES_DATA.value, + MsgIds.MSG_ID_HD_SWITCHES_DATA.value, + MsgIds.MSG_ID_HD_FANS_DATA.value, + MsgIds.MSG_ID_DG_ALARM_INFO_DATA.value, + MsgIds.MSG_ID_HD_RESERVOIRS_DATA.value, + MsgIds.MSG_ID_DG_CPLD_STATUS_DATA.value, + MsgIds.MSG_ID_DG_DIALIN_CHECK_IN.value, + MsgIds.MSG_ID_HD_DIALIN_CHECK_IN.value +] + + +@unique +class RequestRejectReasons(DialinEnum): + REQUEST_REJECT_REASON_NONE = 0 + REQUEST_REJECT_REASON_NOT_ALLOWED_IN_CURRENT_MODE = 1 + REQUEST_REJECT_REASON_TIMEOUT_WAITING_FOR_USER_CONFIRM = 2 + REQUEST_REJECT_REASON_NOT_IN_TREATMENT_MODE = 3 + REQUEST_REJECT_REASON_INVALID_TREATMENT_STATE = 4 + REQUEST_REJECT_REASON_TREATMENT_TOO_CLOSE_TO_FINISHED = 5 + REQUEST_REJECT_REASON_TREATMENT_TIME_OUT_OF_RANGE = 6 + REQUEST_REJECT_REASON_TREATMENT_TIME_LESS_THAN_CURRENT = 7 + REQUEST_REJECT_REASON_BLOOD_FLOW_OUT_OF_RANGE = 8 + REQUEST_REJECT_REASON_DIAL_FLOW_OUT_OF_RANGE = 9 + REQUEST_REJECT_REASON_DIAL_VOLUME_OUT_OF_RANGE = 10 + REQUEST_REJECT_REASON_UF_VOLUME_OUT_OF_RANGE = 11 + REQUEST_REJECT_REASON_UF_RATE_OUT_OF_RANGE = 12 + REQUEST_REJECT_REASON_TREATMENT_TIME_LESS_THAN_MINIMUM = 13 + REQUEST_REJECT_REASON_UF_NOT_IN_PROGESS = 14 + REQUEST_REJECT_REASON_UF_NOT_PAUSED = 15 + REQUEST_REJECT_REASON_SALINE_BOLUS_IN_PROGRESS = 16 + REQUEST_REJECT_REASON_PARAM_OUT_OF_RANGE = 17 + REQUEST_REJECT_REASON_HEPARIN_PRESTOP_EXCEEDS_DURATION = 18 + REQUEST_REJECT_REASON_DG_FILTER_FLUSH_HAS_BEEN_EXPIRED = 19 + REQUEST_REJECT_REASON_AVAILABLE_2 = 20 + REQUEST_REJECT_REASON_SALINE_MAX_VOLUME_REACHED = 21 + REQUEST_REJECT_REASON_SALINE_BOLUS_NOT_IN_PROGRESS = 22 + REQUEST_REJECT_REASON_ACTION_DISABLED_IN_CURRENT_STATE = 23 + REQUEST_REJECT_REASON_ALARM_IS_ACTIVE = 24 + REQUEST_REJECT_REASON_INVALID_COMMAND = 25 + REQUEST_REJECT_REASON_TREATMENT_IS_COMPLETED = 26 + REQUEST_REJECT_REASON_ADDL_RINSEBACK_MAX_VOLUME_REACHED = 27 + REQUEST_REJECT_REASON_DIALYZER_NOT_INVERTED = 28 + REQUEST_REJECT_REASON_NO_PATIENT_CONNECTION_CONFIRM = 29 + REQUEST_REJECT_REASON_HEPARIN_PAUSE_INVALID_IN_THIS_STATE = 30 + REQUEST_REJECT_REASON_HEPARIN_NOT_PAUSED = 31 + REQUEST_REJECT_REASON_DG_COMM_LOST = 32 + REQUEST_REJECT_REASON_DRAIN_NOT_COMPLETE = 33 + REQUEST_REJECT_REASON_DG_NOT_IN_STANDBY_IDLE_STATE = 34 + REQUEST_REJECT_REASON_INVALID_REQUEST_FORMAT = 35 + REQUEST_REJECT_REASON_INVALID_DATE_OR_TIME = 36 + REQUEST_REJECT_REASON_TREATMENT_IN_PROGRESS = 37 + REQUEST_REJECT_REASON_BATTERY_IS_NOT_CHARGED = 38 + REQUEST_REJECT_REASON_RINSEBACK_NOT_COMPLETED = 39 + REQUEST_REJECT_REASON_RESERVOIR_ONE_IS_NOT_READY = 40 + REQUEST_REJECT_REASON_PUMP_TRACK_NOT_CLOSED = 41 + REQUEST_REJECT_REASON_DOOR_NOT_CLOSED = 42 + REQUEST_REJECT_REASON_SYRINGE_NOT_PRESENT = 43 + REQUEST_REJECT_REASON_DG_DIALYSATE_CAP_OPEN = 44 + REQUEST_REJECT_REASON_DG_CONCENTRATE_CAP_OPEN = 45 + REQUEST_REJECT_REASON_DG_DISINFECT_HAS_BEEN_EXPIRED = 46 + REQUEST_REJECT_REASON_DG_SERVICE_IS_DUE = 47 + REQUEST_REJECT_REASON_HD_SERVICE_IS_DUE = 48 + REQUEST_REJECT_REASON_DG_CHEM_FLUSH_NOT_COMPLETED = 49 + REQUEST_REJECT_REASON_DG_RO_FILTER_TEMPERATURE_OUT_OF_RANGE = 50 + REQUEST_REJECT_REASON_DG_INCOMPATIBLE = 51 + REQUEST_REJECT_REASON_HEPARIN_PRESTOP_WITH_NO_DISPENSE = 52 + REQUEST_REJECT_REASON_DIALYZER_REPRIME_IN_PROGRESS = 53 + REQUEST_REJECT_REASON_DG_RO_ONLY_MODE_DG_BUSY = 54 + REQUEST_REJECT_REASON_DG_RO_ONLY_MODE_INVALID_PARAMETER = 55 + REQUEST_REJECT_REASON_DG_RO_ONLY_MODE_INVALID_PAYLOAD_LENGTH = 56 + REQUEST_REJECT_REASON_TREATMENT_CANNOT_BE_RESUMED = 57 + REQUEST_REJECT_REASON_EXCEEDS_MAXIMUM_HEPARIN_VOLUME = 58 + REQUEST_REJECT_REASON_CHEM_DISINFECT_NOT_ENABLED_INST_CONFIG = 59 + NUM_OF_REQUEST_REJECT_REASONS = 60 + + +class MsgFieldPositions: + # Generic response msg field byte positions (where 32-bit data fields are used) + START_POS_FIELD_1 = 6 # Hardcoded for now to avoid cyclic import issue. See protocols.CAN.DenaliMessage class + END_POS_FIELD_1 = START_POS_FIELD_1 + 4 + START_POS_FIELD_2 = END_POS_FIELD_1 + END_POS_FIELD_2 = START_POS_FIELD_2 + 4 + START_POS_FIELD_3 = END_POS_FIELD_2 + END_POS_FIELD_3 = START_POS_FIELD_3 + 4 + START_POS_FIELD_4 = END_POS_FIELD_3 + END_POS_FIELD_4 = START_POS_FIELD_4 + 4 + START_POS_FIELD_5 = END_POS_FIELD_4 + END_POS_FIELD_5 = START_POS_FIELD_5 + 4 + START_POS_FIELD_6 = END_POS_FIELD_5 + END_POS_FIELD_6 = START_POS_FIELD_6 + 4 + START_POS_FIELD_7 = END_POS_FIELD_6 + END_POS_FIELD_7 = START_POS_FIELD_7 + 4 + START_POS_FIELD_8 = END_POS_FIELD_7 + END_POS_FIELD_8 = START_POS_FIELD_8 + 4 + START_POS_FIELD_9 = END_POS_FIELD_8 + END_POS_FIELD_9 = START_POS_FIELD_9 + 4 + START_POS_FIELD_10 = END_POS_FIELD_9 + END_POS_FIELD_10 = START_POS_FIELD_10 + 4 + START_POS_FIELD_11 = END_POS_FIELD_10 + END_POS_FIELD_11 = START_POS_FIELD_11 + 4 + START_POS_FIELD_12 = END_POS_FIELD_11 + END_POS_FIELD_12 = START_POS_FIELD_12 + 4 + START_POS_FIELD_13 = END_POS_FIELD_12 + END_POS_FIELD_13 = START_POS_FIELD_13 + 4 + START_POS_FIELD_14 = END_POS_FIELD_13 + END_POS_FIELD_14 = START_POS_FIELD_14 + 4 + START_POS_FIELD_15 = END_POS_FIELD_14 + END_POS_FIELD_15 = START_POS_FIELD_15 + 4 + START_POS_FIELD_16 = END_POS_FIELD_15 + END_POS_FIELD_16 = START_POS_FIELD_16 + 4 + START_POS_FIELD_17 = END_POS_FIELD_16 + END_POS_FIELD_17 = START_POS_FIELD_17 + 4 + START_POS_FIELD_18 = END_POS_FIELD_17 + END_POS_FIELD_18 = START_POS_FIELD_18 + 4 + START_POS_FIELD_19 = END_POS_FIELD_18 + END_POS_FIELD_19 = START_POS_FIELD_19 + 4 + START_POS_FIELD_20 = END_POS_FIELD_19 + END_POS_FIELD_20 = START_POS_FIELD_20 + 4 + START_POS_FIELD_21 = END_POS_FIELD_20 + END_POS_FIELD_21 = START_POS_FIELD_21 + 4 + START_POS_FIELD_22 = END_POS_FIELD_21 + END_POS_FIELD_22 = START_POS_FIELD_22 + 4 + START_POS_FIELD_23 = END_POS_FIELD_22 + END_POS_FIELD_23 = START_POS_FIELD_23 + 4 + START_POS_FIELD_24 = END_POS_FIELD_23 + END_POS_FIELD_24 = START_POS_FIELD_24 + 4 + START_POS_FIELD_25 = END_POS_FIELD_24 + END_POS_FIELD_25 = START_POS_FIELD_25 + 4 + START_POS_FIELD_26 = END_POS_FIELD_25 + END_POS_FIELD_26 = START_POS_FIELD_26 + 4 + START_POS_FIELD_27 = END_POS_FIELD_26 + END_POS_FIELD_27 = START_POS_FIELD_27 + 4 + START_POS_FIELD_28 = END_POS_FIELD_27 + END_POS_FIELD_28 = START_POS_FIELD_28 + 4 + START_POS_FIELD_29 = END_POS_FIELD_28 + END_POS_FIELD_29 = START_POS_FIELD_29 + 4 Index: leahi-dialin/common/msg_ids.py =================================================================== diff -u --- leahi-dialin/common/msg_ids.py (revision 0) +++ leahi-dialin/common/msg_ids.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,518 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file msg_ids.py +# +# @author (last) Dara Navaei +# @date (last) 10-May-2024 +# @author (original) Peter Lucia +# @date (original) 06-Apr-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +# Branch: staging +@unique +class MsgIds(DialinEnum): + MSG_ID_UNUSED = 0x0 + MSG_ID_OFF_BUTTON_PRESS_REQUEST = 0x1 + MSG_ID_ALARM_STATUS_DATA = 0x2 + MSG_ID_ALARM_TRIGGERED = 0x3 + MSG_ID_ALARM_CLEARED = 0x4 + MSG_ID_BLOOD_FLOW_DATA = 0x5 + MSG_ID_UI_POST_TX_NEXT_REQUEST = 0x6 + MSG_ID_UI_CHECK_IN = 0x7 + MSG_ID_DIALYSATE_FLOW_DATA = 0x8 + MSG_ID_PRESSURE_OCCLUSION_DATA = 0x9 + MSG_ID_RTC_EPOCH_DATA = 0xA + MSG_ID_DIALYSATE_OUT_FLOW_DATA = 0xB + MSG_ID_LOAD_CELL_READINGS_DATA = 0xC + MSG_ID_TREATMENT_TIME_DATA = 0xD + MSG_ID_POWER_OFF_WARNING = 0xE + MSG_ID_TREATMENT_STATE_DATA = 0xF + MSG_ID_USER_UF_PAUSE_RESUME_REQUEST = 0x10 + MSG_ID_USER_UF_SETTINGS_CHANGE_REQUEST = 0x11 + MSG_ID_USER_SALINE_BOLUS_REQUEST = 0x12 + MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE = 0x13 + MSG_ID_USER_SALINE_BOLUS_RESPONSE = 0x14 + MSG_ID_USER_CONFIRM_UF_SETTINGS_CHANGE_REQUEST = 0x15 + MSG_ID_USER_TREATMENT_TIME_CHANGE_REQUEST = 0x16 + MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_REQUEST = 0x17 + MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE = 0x18 + MSG_ID_SET_DG_DIALYSATE_TEMP_TARGETS = 0x19 + MSG_ID_TREATMENT_PARAM_CHANGE_RANGES_DATA = 0x1A + MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE = 0x1B + MSG_ID_FW_VERSIONS_REQUEST = 0x1C + MSG_ID_HD_VERSION_REPONSE = 0x1D + MSG_ID_DG_VERSION_REPONSE = 0x1E + MSG_ID_RO_PUMP_DATA = 0x1F + MSG_ID_DG_PRESSURES_DATA = 0x20 + MSG_ID_DG_SWITCH_RESERVOIR_CMD_REQUEST = 0x21 + MSG_ID_DG_FILL_CMD_REQUEST = 0x22 + MSG_ID_DG_DRAIN_CMD_REQUEST = 0x23 + MSG_ID_DRAIN_PUMP_DATA = 0x24 + MSG_ID_HD_OP_MODE_DATA = 0x25 + MSG_ID_STARTING_STOPPING_TREATMENT_CMD_REQUEST = 0x26 + MSG_ID_DG_OP_MODE_DATA = 0x27 + MSG_ID_DG_RESERVOIRS_DATA = 0x28 + MSG_ID_DG_SAMPLE_WATER_CMD_REQUEST = 0x29 + MSG_ID_DG_VALVES_STATES_DATA = 0x2A + MSG_ID_HD_START_STOP_TRIMMER_HEATER_CMD_REQUEST = 0x2B + MSG_ID_DG_HEATERS_DATA = 0x2C + MSG_ID_DG_TEMPERATURE_DATA = 0x2D + MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE = 0x2E + MSG_ID_SALINE_BOLUS_DATA = 0x2F + MSG_ID_DG_START_STOP_HEAT_DISINFECT_CMD_REQUEST = 0x30 + MSG_ID_DG_CONDUCTIVITY_DATA = 0x31 + MSG_ID_USER_ALARM_SILENCE_REQUEST = 0x32 + MSG_ID_HD_ACCELEROMETER_DATA = 0x33 + MSG_ID_DG_ACCELEROMETER_DATA = 0x34 + MSG_ID_UI_NEW_TREATMENT_PARAMS_REQUEST = 0x35 + MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE = 0x36 + MSG_ID_DG_HEAT_DISINFECT_DATA = 0x37 + MSG_ID_UI_INITIATE_TREATMENT_REQUEST = 0x38 + MSG_ID_HD_INITIATE_TREATMENT_RESPONSE = 0x39 + MSG_ID_HD_VALVES_DATA = 0x3A + MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS_REQUEST = 0x3B + MSG_ID_UI_START_PRIME_REQUEST = 0x3C + MSG_ID_HD_START_PRIME_RESPONSE = 0x3D + MSG_ID_HD_AIR_TRAP_DATA = 0x3E + MSG_ID_ALARM_CONDITION_CLEARED = 0x3F + MSG_ID_UI_ALARM_USER_ACTION_REQUEST = 0x40 + MSG_ID_USER_UF_PAUSE_RESUME_RESPONSE = 0x41 + MSG_ID_DG_CONCENTRATE_PUMP_DATA = 0x42 + MSG_ID_HD_PRIMING_STATUS_DATA = 0x43 + MSG_ID_DG_UV_REACTORS_DATA = 0x44 + MSG_ID_DG_THERMISTORS_DATA = 0x45 + MSG_ID_UI_PRESSURE_LIMITS_CHANGE_REQUEST = 0x46 + MSG_ID_HD_PRESSURE_LIMITS_CHANGE_RESPONSE = 0x47 + MSG_ID_DG_FANS_DATA = 0x48 + MSG_ID_HD_TREATMENT_STOP_TIMER_DATA = 0x49 + MSG_ID_UI_PATIENT_DISCONNECTION_CONFIRM_REQUEST = 0x4A + MSG_ID_UI_HEPARIN_PAUSE_RESUME_REQUEST = 0x4B + MSG_ID_HD_HEPARIN_PAUSE_RESUME_RESPONSE = 0x4C + MSG_ID_HD_HEPARIN_DATA = 0x4D + MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD_REQUEST = 0x4E + MSG_ID_UI_SET_UF_VOLUME_PARAMETER_REQUEST = 0x4F + MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE = 0x50 + MSG_ID_DG_COMMAND_RESPONSE = 0x51 + MSG_ID_UI_RINSEBACK_CMD_REQUEST = 0x52 + MSG_ID_HD_RINSEBACK_CMD_RESPONSE = 0x53 + MSG_ID_UI_RECIRC_CMD_REQUEST = 0x54 + MSG_ID_HD_RECIRC_CMD_RESPONSE = 0x55 + MSG_ID_HD_RINSEBACK_PROGRESS = 0x56 + MSG_ID_UI_TX_END_CMD_REQUEST = 0x57 + MSG_ID_HD_TX_END_CMD_RESPONSE = 0x58 + MSG_ID_HD_BLOOD_PRIME_PROGRESS_DATA = 0x59 + MSG_ID_HD_RECIRC_PROGRESS_DATA = 0x5A + MSG_ID_DG_CHANGE_VALVE_SETTING_CMD_REQUEST = 0x5B + MSG_ID_PRE_TREATMENT_STATE_DATA = 0x5C + MSG_ID_UI_SAMPLE_WATER_CMD_REQUEST = 0x5D + MSG_ID_HD_SAMPLE_WATER_CMD_RESPONSE = 0x5E + MSG_ID_UI_SAMPLE_WATER_RESULT = 0x5F + MSG_ID_DG_FILTER_FLUSH_PROGRESS_DATA = 0x60 + MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS_DATA = 0x61 + MSG_ID_UI_INSTALLATION_CONFIRM_REQUEST = 0x62 + MSG_ID_HD_DRY_SELF_TEST_PROGRESS_DATA = 0x63 + MSG_ID_UI_PATIENT_CONNECTION_BEGIN_REQUEST = 0x64 + MSG_ID_HD_PATIENT_CONNECTION_BEGIN_RESPONSE = 0x65 + MSG_ID_UI_PATIENT_CONNECTION_CONFIRM_REQUEST = 0x66 + MSG_ID_HD_PATIENT_CONNECTION_CONFIRM_RESPONSE = 0x67 + MSG_ID_UI_CONSUMABLE_INSTALL_CONFIRM_REQUEST = 0x68 + MSG_ID_HD_SYRINGE_PUMP_DATA = 0x69 + MSG_ID_HD_FLUID_LEAK_STATE_DATA = 0x6A + MSG_ID_DG_FLUID_LEAK_STATE_DATA = 0x6B + MSG_ID_HD_BLOOD_LEAK_DATA = 0x6C + MSG_ID_UI_HD_SET_RTC_REQUEST = 0x6D + MSG_ID_HD_UI_SET_RTC_RESPONSE = 0x6E + MSG_ID_UI_DG_SET_RTC_REQUEST = 0x6F + MSG_ID_DG_UI_SET_RTC_RESPONSE = 0x70 + MSG_ID_UI_START_TREATMENT_REQUEST = 0x71 + MSG_ID_HD_START_TREATMENT_RESPONSE = 0x72 + MSG_ID_UI_DISPOSABLE_REMOVAL_CONFIRM_REQUEST = 0x73 + MSG_ID_HD_DISPOSABLE_REMOVAL_CONFIRM_RESPONSE = 0x74 + MSG_ID_UI_TREATMENT_LOG_DATA_REQUEST = 0x75 + MSG_ID_HD_TREATMENT_LOG_DATA_RESPONSE = 0x76 + MSG_ID_HD_POST_TREATMENT_STATE_DATA = 0x77 + MSG_ID_DG_START_STOP_CHEM_DISINFECT = 0x78 + MSG_ID_DG_START_STOP_FLUSH_CMD_REQUEST = 0x79 + MSG_ID_DG_FLUSH_DATA = 0x7A + MSG_ID_HD_VOLTAGES_DATA = 0x7B + MSG_ID_HD_ALARM_AUDIO_VOLUME_SET_RESPONSE = 0x7C + MSG_ID_HD_ALARM_INFORMATION_DATA = 0x7D + MSG_ID_HD_POST_TX_NEXT_CMD_RESPONSE = 0x7E + MSG_ID_UI_DISINFECT_REQUEST = 0x7F + MSG_ID_HD_DISINFECT_RESPONSE = 0x80 + MSG_ID_DG_SEND_SERVICE_SCHEDULE_DATA_TO_HD = 0x81 + MSG_ID_HD_REQUEST_SERVICE_RECORD_FROM_HD = 0x82 + MSG_ID_DG_PARK_CONCENTRATE_PUMPS_CMD_REQUEST = 0x83 + MSG_ID_DG_HEAT_DISINFECT_TIME_DATA = 0x84 + MSG_ID_DG_CHEM_DISINFECT_TIME_DATA = 0x85 + MSG_ID_DG_VOLTAGES_DATA = 0x86 + MSG_ID_DG_CHEM_DISINFECT_DATA = 0x87 + MSG_ID_DG_SERIAL_NUMBER_RESPONSE = 0x88 + MSG_ID_UI_SERVICE_INFO_REQUEST = 0x89 + MSG_ID_HD_SERVICE_SCHEDULE_DATA = 0x8A + MSG_ID_HD_USAGE_DATA = 0x8B + MSG_ID_DG_SERVICE_SCHEDULE_DATA_TO_UI = 0x8C + MSG_ID_DG_USAGE_DATA = 0x8D + MSG_ID_HD_POST_SINGLE_TEST_RESULT = 0x8E + MSG_ID_HD_POST_FINAL_TEST_RESULT = 0x8F + MSG_ID_DG_POST_SINGLE_TEST_RESULT = 0x90 + MSG_ID_DG_POST_FINAL_TEST_RESULT = 0x91 + MSG_ID_UI_POST_FINAL_TEST_RESULT = 0x92 + MSG_ID_HD_BUBBLES_DATA = 0x93 + MSG_ID_HD_TREATMENT_LOG_PERIODIC_DATA = 0x94 + MSG_ID_HD_TREATMENT_LOG_ALARM_EVENT = 0x95 + MSG_ID_HD_TREATMENT_LOG_EVENT = 0x96 + MSG_ID_UI_ACTIVE_ALARMS_LIST_REQUEST = 0x97 + MSG_ID_HD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE = 0x98 + MSG_ID_HD_SERIAL_NUMBER_RESPONSE = 0x99 + MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_REQUEST = 0x9A + MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_RESPONSE = 0x9B + MSG_ID_HD_DG_POST_RESULT_REQUEST = 0x9C + MSG_ID_HD_TEMPERATURES_DATA = 0x9D + MSG_ID_HD_UI_VERSION_INFO_REQUEST = 0x9E + MSG_ID_UI_VERSION_INFO_RESPONSE = 0x9F + MSG_ID_HD_USAGE_INFO_REQUEST = 0xA0 + MSG_ID_DG_SWITCHES_DATA = 0xA1 + MSG_ID_HD_SWITCHES_DATA = 0xA2 + MSG_ID_HD_FANS_DATA = 0xA3 + MSG_ID_HD_EVENT = 0xA4 + MSG_ID_DG_EVENT = 0xA5 + MSG_ID_HD_SET_SERVICE_TIME_REQUEST = 0xA6 + MSG_ID_DG_ALARM_INFO_DATA = 0xA7 + MSG_ID_HD_RESERVOIRS_DATA = 0xA8 + MSG_ID_HD_DG_CONCENTRATE_MIXING_RATIOS_REQUEST = 0xA9 + MSG_ID_DG_CONCENTRATE_MIXING_RATIOS_DATA = 0xAA + MSG_ID_DG_SCHEDULED_RUNS_DATA = 0xAB + MSG_ID_DG_SET_SERVICE_TIME_REQUEST = 0xAC + MSG_ID_UI_INSTALLATION_CONFIRM_RESPONSE = 0xAD + MSG_ID_DG_FILL_MODE_DATA = 0xAE + MSG_ID_DG_GEN_IDLE_DATA = 0xAF + MSG_ID_UI_SERVICE_MODE_REQUEST = 0xB0 + MSG_ID_HD_RESPONSE_SERVICE_MODE_REQUEST = 0xB1 + MSG_ID_HD_REQUEST_UI_FINAL_POST_RESULT = 0xB2 + MSG_ID_DG_FLOW_SENSORS_DATA = 0xB3 + MSG_ID_DG_RESPONSE_SERVICE_MODE_REQUEST = 0xB4 + MSG_ID_HD_DG_ALARMS_REQUEST = 0xB5 + MSG_ID_HD_DG_SERVICE_MODE_REQUEST = 0xB6 + MSG_ID_DG_RTC_EPOCH_DATA = 0xB7 + MSG_ID_HD_DG_USAGE_INFO_REQUEST = 0xB8 + MSG_ID_UI_HD_RESET_IN_SERVICE_MODE_REQUEST = 0xB9 + MSG_ID_HD_UI_CONFIRMATION_REQUEST = 0xBA + MSG_ID_UI_CONFIRMATION_RESULT_RESPONSE = 0xBB + MSG_ID_UI_SET_DG_RO_MODE = 0xBC + MSG_ID_DG_CPLD_STATUS_DATA = 0xBD + MSG_ID_HD_BATTERY_MANAGEMENT_DATA = 0xBE + MSG_ID_HD_BATTERY_STATUS_DATA = 0xBF + MSG_ID_DG_CHEM_DISINFECT_FLUSH_DATA = 0xC0 + MSG_ID_DG_CHEM_DISINFECT_FLUSH_TIME_DATA = 0xC1 + MSG_ID_DG_START_STOP_CHEM_DISINFECT_FLUSH = 0xC2 + MSG_ID_DG_RO_ONLY_MODE_STATUS_REQUEST = 0xC3 + MSG_ID_DG_RO_ONLY_MODE_STATUS_RESPONSE = 0xC4 + MSG_ID_DG_HEAT_DISINFECT_ACTIVE_COOL_DATA = 0xC5 + MSG_ID_DG_START_STOP_HEAT_DISINFECT_ACTIVE_COOL = 0xC6 + MSG_ID_HD_AIR_PUMP_DATA = 0xC7 + MSG_ID_HD_SEND_CHEM_FLUSH_SAMPLE_PASS_FAIL_TO_DG = 0xC8 + MSG_ID_DG_START_STOP_RO_PERMEATE_SAMPLE_MODE_CMD_REQUEST = 0xC9 + MSG_ID_DG_RO_PERMEATE_SAMPLE_DATA = 0xCA + MSG_ID_HD_SEND_RO_PERMEATE_SAMPLE_DISPENSE_REQUEST_TO_DG = 0xCB + MSG_ID_DG_SEND_RO_PERMEATE_SAMPLE_DISPENSE_READY_TO_HD = 0xCD + MSG_ID_DG_DRAIN_MODE_DATA = 0xCE + MSG_ID_UI_INSTITUTIONAL_RECORD_REQUEST = 0xCF + MSG_ID_HD_INSTITUTIONAL_RECORD_RESPONSE = 0xD0 + + MSG_ID_CAN_ERROR_COUNT = 0x999 + + MSG_ID_TESTER_LOGIN_REQUEST = 0x8000 + MSG_ID_DIAL_OUT_FLOW_SET_PT_OVERRIDE = 0x8001 + MSG_ID_OFF_BUTTON_STATE_OVERRIDE = 0x8002 + MSG_ID_STOP_BUTTON_STATE_OVERRIDE = 0x8003 + MSG_ID_ALARM_LAMP_PATTERN_OVERRIDE = 0x8004 + MSG_ID_WATCHDOG_TASK_CHECKIN_OVERRIDE = 0x8005 + MSG_ID_ALARM_STATE_OVERRIDE = 0x8006 + MSG_ID_ALARM_TIME_OVERRIDE = 0x8007 + MSG_ID_BLOOD_FLOW_SET_PT_OVERRIDE = 0x8008 + MSG_ID_BLOOD_FLOW_MEAS_OVERRIDE = 0x8009 + MSG_ID_BLOOD_PUMP_MC_MEAS_SPEED_OVERRIDE = 0x800A + MSG_ID_BLOOD_PUMP_MC_MEAS_CURR_OVERRIDE = 0x800B + MSG_ID_BLOOD_FLOW_SEND_INTERVAL_OVERRIDE = 0x800C + MSG_ID_TREATMENT_TIME_REMAINING_OVERRIDE = 0x800D + MSG_ID_BLOOD_PUMP_MEAS_SPEED_OVERRIDE = 0x800E + MSG_ID_BLOOD_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0x800F + MSG_ID_DIAL_IN_FLOW_SET_PT_OVERRIDE = 0x8010 + MSG_ID_DIAL_IN_FLOW_MEAS_OVERRIDE = 0x8011 + MSG_ID_DIAL_IN_PUMP_MC_MEAS_SPEED_OVERRIDE = 0x8012 + MSG_ID_DIAL_IN_PUMP_MC_MEAS_CURR_OVERRIDE = 0x8013 + MSG_ID_DIAL_IN_FLOW_SEND_INTERVAL_OVERRIDE = 0x8014 + MSG_ID_DIAL_IN_PUMP_MEAS_SPEED_OVERRIDE = 0x8015 + MSG_ID_DIAL_IN_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0x8016 + MSG_ID_PRESSURE_ARTERIAL_OVERRIDE = 0x8017 + MSG_ID_PRESSURE_VENOUS_OVERRIDE = 0x8018 + MSG_ID_OCCLUSION_BLOOD_PUMP_OVERRIDE = 0x8019 + MSG_ID_BLOOD_ROTOR_COUNT_OVERRIDE = 0x801A + MSG_ID_HD_AIR_PUMP_SET_STATE = 0x801B + MSG_ID_PRES_OCCL_SEND_INTERVAL_OVERRIDE = 0x801C + MSG_ID_SET_RTC_DATE_TIME = 0x801D + MSG_ID_DIAL_OUT_FLOW_SEND_INTERVAL_OVERRIDE = 0x801E + MSG_ID_DIAL_OUT_UF_REF_VOLUME_OVERRIDE = 0x801F + MSG_ID_DIAL_OUT_UF_MEAS_VOLUME_OVERRIDE = 0x8020 + MSG_ID_DIAL_OUT_PUMP_MC_MEAS_SPEED_OVERRIDE = 0x8021 + MSG_ID_DIAL_OUT_PUMP_MC_MEAS_CURR_OVERRIDE = 0x8022 + MSG_ID_DIAL_OUT_PUMP_MEAS_SPEED_OVERRIDE = 0x8023 + MSG_ID_DIAL_OUT_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0x8024 + MSG_ID_DIAL_OUT_LOAD_CELL_WEIGHT_OVERRIDE = 0x8025 + MSG_ID_HD_SAFETY_SHUTDOWN_OVERRIDE = 0x8026 + MSG_ID_HD_ACCEL_OVERRIDE = 0x8027 + MSG_ID_HD_ACCEL_MAX_OVERRIDE = 0x8028 + MSG_ID_HD_ACCEL_SEND_INTERVAL_OVERRIDE = 0x8029 + MSG_ID_HD_SYRINGE_PUMP_SEND_INTERVAL_OVERRIDE = 0x802A + MSG_ID_HD_SYRINGE_PUMP_OPERATION_REQUEST = 0x802B + MSG_ID_HD_SYRINGE_PUMP_MEASURED_RATE_OVERRIDE = 0x802C + MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER = 0x802D + MSG_ID_HD_VALVES_HOME = 0x802E + MSG_ID_HD_VALVES_POSITION_OVERRIDE = 0x802F + MSG_ID_HD_VALVES_SET_AIR_TRAP_VALVE = 0x8030 + MSG_ID_HD_VALVES_SET_PWM_OVERRIDE = 0x8031 + MSG_ID_HD_AIR_TRAP_SEND_INTERVAL_OVERRIDE = 0x8032 + MSG_ID_HD_AIR_TRAP_LEVEL_SENSOR_OVERRIDE = 0x8033 + MSG_ID_HD_SOFTWARE_RESET_REQUEST = 0x8034 + MSG_ID_HD_GET_SW_CONFIG_RECORD = 0x8035 + MSG_ID_HD_SET_SW_CONFIG_RECORD = 0x8036 + MSG_ID_BLOOD_PUMP_HOME_CMD = 0x8037 + MSG_ID_DIAL_IN_PUMP_HOME_CMD = 0x8038 + MSG_ID_DIAL_OUT_PUMP_HOME_CMD = 0x8039 + MSG_ID_SUPER_CLEAR_ALARMS_CMD = 0x803A + MSG_ID_HD_SYRINGE_PUMP_MEASURED_FORCE_OVERRIDE = 0x803B + MSG_ID_HD_SYRINGE_PUMP_SYRINGE_DETECT_OVERRIDE = 0x803C + MSG_ID_HD_SET_CALIBRATION_RECORD = 0x803D + MSG_ID_HD_GET_CALIBRATION_RECORD = 0x803E + MSG_ID_HD_SEND_CALIBRATION_RECORD = 0x803F + MSG_ID_HD_SET_SYSTEM_RECORD = 0x8040 + MSG_ID_HD_GET_SYSTEM_RECORD = 0x8041 + MSG_ID_HD_SEND_SYSTEM_RECORD = 0x8042 + MSG_ID_HD_GET_SERVICE_RECORD = 0x8043 + MSG_ID_HD_SET_SERVICE_RECORD = 0x8044 + MSG_ID_HD_SEND_SERVICE_RECORD = 0x8045 + MSG_ID_HD_SET_OP_MODE_REQUEST = 0x8046 + MSG_ID_HD_FLUID_LEAK_SEND_INTERVAL_OVERRIDE = 0x8047 + MSG_ID_HD_FLUID_LEAK_STATE_OVERRIDE = 0x8048 + MSG_ID_HD_SYRINGE_PUMP_MEASURED_HOME_OVERRIDE = 0x8049 + MSG_ID_HD_SYRINGE_PUMP_MEASURED_POSITION_OVERRIDE = 0x804A + MSG_ID_HD_SYRINGE_PUMP_MEASURED_VOLUME_OVERRIDE = 0x804B + MSG_ID_HD_BLOOD_LEAK_DATA_SEND_INTERVAL_OVERRIDE = 0x804C + MSG_ID_HD_BLOOD_LEAK_STATUS_OVERRIDE = 0x804D + MSG_ID_HD_BLOOD_LEAK_ZERO_REQUEST = 0x804E + MSG_ID_HD_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE = 0x8050 + MSG_ID_HD_MONITORED_VOLTAGES_OVERRIDE = 0x8051 + MSG_ID_HD_ALARM_INFO_SEND_INTERVAL_OVERRIDE = 0x8052 + MSG_ID_HD_ALARM_AUDIO_VOLUME_LEVEL_OVERRIDE = 0x8053 + MSG_ID_HD_ALARM_AUDIO_CURRENT_HG_OVERRIDE = 0x8054 + MSG_ID_HD_ALARM_AUDIO_CURRENT_LG_OVERRIDE = 0x8055 + MSG_ID_HD_ALARM_BACKUP_AUDIO_CURRENT_OVERRIDE = 0x8056 + MSG_ID_HD_VALVES_CURRENT_OVERRIDE = 0x8057 + MSG_ID_HD_VALVES_POSITION_COUNT_OVERRIDE = 0x8058 + MSG_ID_HD_SYRINGE_PUMP_STATUS_OVERRIDE = 0x8059 + MSG_ID_HD_SYRINGE_PUMP_ENCODER_STATUS_OVERRIDE = 0x805A + MSG_ID_HD_SYRINGE_PUMP_ADC_DAC_STATUS_OVERRIDE = 0x805B + MSG_ID_HD_SYRINGE_PUMP_ADC_READ_COUNTER_OVERRIDE = 0x805C + MSG_ID_HD_BUBBLES_DATA_SEND_INTERVAL_OVERRIDE = 0x805D + MSG_ID_HD_BUBBLE_STATUS_OVERRIDE = 0x805E + MSG_ID_HD_BLOOD_PRIME_VOLUME_OVERRIDE = 0x805F + MSG_ID_HD_BUBBLE_SELF_TEST_REQUEST = 0x8060 + MSG_ID_HD_AIR_PUMP_PUBLISH_INTERVAL_OVERRIDE = 0x8061 + MSG_ID_HD_SWITCHES_STATUS_OVERRIDE = 0x8062 + MSG_ID_HD_SWITCHES_PUBLISH_INTERVAL_OVERRIDE = 0x8063 + MSG_ID_HD_BATTERY_REMAINING_CAP_MWH_OVERRIDE = 0x8064 + MSG_ID_HD_TEMPERATURES_VALUE_OVERRIDE = 0x8065 + MSG_ID_HD_TEMPERATURES_PUBLISH_INTERVAL_OVERRIDE = 0x8066 + MSG_ID_HD_FANS_PUBLISH_INTERVAL_OVERRIDE = 0x8067 + MSG_ID_HD_FANS_RPM_OVERRIDE = 0x8068 + MSG_ID_HD_RINSEBACK_VOLUME_OVERRIDE = 0x8069 + MSG_ID_HD_SEND_SW_CONFIG_RECORD = 0x806A + MSG_ID_HD_ALARM_STATUS_PUBLISH_INTERVAL_OVERRIDE = 0x806B + MSG_ID_HD_TREATMENT_TIME_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806C + MSG_ID_HD_TREATMENT_RANGES_PUBLISH_INTERVAL_OVERRIDE = 0x806D + MSG_ID_HD_TREATMENT_STOP_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806E + MSG_ID_HD_BLOOD_PRIME_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806F + MSG_ID_HD_RINSEBACK_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8070 + MSG_ID_HD_STANDBY_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8071 + MSG_ID_HD_OP_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8072 + MSG_ID_HD_PRE_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8073 + MSG_ID_HD_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8074 + MSG_ID_HD_POST_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8075 + MSG_ID_HD_BLOCK_MESSAGE_TRANSMISSION = 0x8076 + MSG_ID_HD_SYRINGE_PUMP_FORCE_SENSOR_DAC_CALIBRATE = 0x8077 + MSG_ID_HD_STOP_RTC_CLOCK = 0x8078 + MSG_ID_HD_FANS_DUTY_CYCLE_OVERRIDE = 0x8079 + MSG_ID_HD_SYRINGE_PUMP_HEPARIN_BOLUS_TARGET_RATE_OVERRIDE = 0x807A + MSG_ID_HD_REQ_CURRENT_TREATMENT_PARAMETERS = 0x807B + MSG_ID_HD_RES_CURRENT_TREATMENT_PARAMETERS = 0x807C + MSG_ID_HD_SET_FANS_RPM_ALARM_START_TIME_OFFSET = 0x807D + MSG_ID_HD_GET_USAGE_INFO_RECORD = 0x807E + MSG_ID_HD_SET_USAGE_INFO_RECORD = 0x807F + MSG_ID_HD_SEND_USAGE_INFO_RECORD = 0x8080 + MSG_ID_HD_SET_BLOOD_LEAK_2_EMB_MODE = 0x8081 + MSG_ID_HD_SET_BLOOD_LEAK_EMB_MODE_COMMAND = 0x8082 + MSG_ID_HD_SEND_BLOOD_LEAK_EMB_MODE_RESPONSE = 0x8083 + MSG_ID_HD_SEND_ALARMS_COMMAND = 0x8084 + MSG_ID_HD_BLOOD_PUMP_SET_PWM = 0x8085 + MSG_ID_HD_DIAL_IN_SET_PWM = 0x8086 + MSG_ID_HD_DIAL_OUT_SET_PWM = 0x8087 + MSG_ID_HD_DIALYSATE_INLET_PUMP_ROTOR_COUNT_OVERRIDE = 0x8088 + MSG_ID_HD_NV_RECORD_CRC_OVERRIDE = 0x8089 + MSD_ID_HD_RTC_CTL_REG1_STATUS_OVERRIDE = 0x808A + MSD_ID_HD_RTC_CTL_REG3_STATUS_OVERRIDE = 0x808B + MSG_ID_HD_BATTERY_STATUS_OVERRIDE = 0x808C + MSG_ID_HD_BATTERY_CHARGER_STATUS_OVERRIDE = 0x808D + MSG_ID_HD_BATTERY_COMM_STATUS_OVERRIDE = 0x808E + MSG_ID_HD_DIAL_IN_PUMP_HARD_STOP = 0x808F + MSG_ID_HD_DIAL_OUT_PUMP_HARD_STOP = 0x8090 + MSG_ID_HD_BLOOD_PUMP_HARD_STOP = 0x8091 + MSG_ID_HD_DIALIN_CHECK_IN = 0x8092 + MSG_ID_HD_ENABLE_VENOUS_BUBBLE_ALARM_DETECTION = 0x8093 + MSG_ID_HD_GET_TEST_CONFIGURATION = 0x8094 + MSG_ID_HD_SEND_TEST_CONFIGURATION = 0x8095 + MSG_ID_HD_RESET_ALL_TEST_CONFIGURATIONS = 0x8096 + MSG_ID_HD_SET_TEST_CONFIGURATION = 0x8097 + MSG_ID_HD_SIGNAL_RECOVER_FROM_FAULT_MODE = 0x8098 + MSG_ID_HD_RAM_STATUS_OVERRIDE = 0x8099 + MSG_ID_HD_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE = 0x809A + MSG_ID_HD_CAN_RECEIVE_ACK_MESSAGE_OVERRIDE = 0x809B + MSG_ID_HD_RECIRULATION_PCT_OVERRIDE = 0x809C + MSG_ID_HD_RAW_AIR_TRAP_LEVEL_SENSOR_OVERRIDE = 0x809D + MSG_ID_HD_GET_INSTITUTIONAL_RECORD = 0x809E + MSG_ID_HD_SET_INSTITUTIONAL_RECORD = 0x809F + MSG_ID_HD_SEND_INSTITUTIONAL_RECORD = 0x80A0 + MSG_ID_HD_PARTIAL_OCCLUSION_BLOOD_PUMP_OVERRIDE = 0x80A1 + MSG_ID_HD_PARTIAL_OCCL_BLOOD_PUMP_BASELINE_OVERRIDE = 0x80A2 + + MSG_ID_DG_TESTER_LOGIN_REQUEST = 0xA000 + MSG_ID_DG_ALARM_STATE_OVERRIDE = 0xA001 + MSG_ID_DG_WATCHDOG_TASK_CHECKIN_OVERRIDE = 0xA002 + MSG_ID_DG_SET_RTC_DATE_TIME = 0xA004 + MSG_ID_LOAD_CELL_OVERRIDE = 0xA005 + MSG_ID_PRESSURE_OVERRIDE = 0xA006 + MSG_ID_PRESSURE_SEND_INTERVAL_OVERRIDE = 0xA007 + MSG_ID_DG_HD_COMMUNICATION_STATUS_OVERRIDE = 0xA008 + MSG_ID_DG_SET_PRIMARY_AND_TRIMMER_HEATERS_TARGET_TEMP = 0xA009 + MSG_ID_RO_PUMP_SEND_INTERVAL_OVERRIDE = 0xA00A + MSG_ID_DRAIN_PUMP_SET_RPM = 0xA00B + MSG_ID_DRAIN_PUMP_SEND_INTERVAL_OVERRIDE = 0xA00C + MSG_ID_LOAD_CELL_SEND_INTERVAL_OVERRIDE = 0xA00D + MSG_ID_VALVE_STATE_OVERRIDE = 0xA00E + MSG_ID_DG_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE = 0xA00F + MSG_ID_TEMPERATURE_SENSORS_VALUE_OVERRIDE = 0xA010 + MSG_ID_START_STOP_PRIMARY_HEATER = 0xA011 + MSG_ID_TEMPERATURE_SENSORS_PUBLISH_INTERVAL_OVERRIDE = 0xA012 + MSG_ID_HEATERS_PUBLISH_INTERVAL_ORVERRIDE = 0xA013 + MSG_ID_DG_SAFETY_SHUTDOWN_OVERRIDE = 0xA014 + MSG_ID_CONDUCTIVITY_OVERRIDE = 0xA015 + MSG_ID_CONDUCTIVITY_PUBLISH_INTERVAL_OVERRIDE = 0xA016 + MSG_ID_DG_ACCEL_OVERRIDE = 0xA017 + MSG_ID_DG_ACCEL_MAX_OVERRIDE = 0xA018 + MSG_ID_DG_ACCEL_SEND_INTERVAL_OVERRIDE = 0xA019 + MSG_ID_DG_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE = 0xA01A + MSG_ID_DG_MONITORED_VOLTAGES_OVERRIDE = 0xA01B + MSG_ID_DRAIN_PUMP_TARGET_OUTLET_FLOW = 0xA01C + MSG_ID_DG_SWITCHES_STATUS_OVERRIDE = 0xA01D + MSG_ID_DG_SWITCHES_PUBLISH_INTERVAL_OVERRIDE = 0xA01E + MSG_ID_DG_OP_MODE_PUBLISH_INTERVAL_OVERRIDE = 0xA01F + MSG_ID_DG_BLOCK_MESSAGE_TRANSMISSION = 0xA020 + MSG_ID_MEASURED_FLOW_SENSORS_OVERRIDE = 0xA021 + MSG_ID_DG_SOFTWARE_RESET_REQUEST = 0xA022 + MSG_ID_DG_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE = 0xA023 + MSG_ID_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE = 0xA024 + MSG_ID_UV_REACTORS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA025 + MSG_ID_CONCENTRATE_PUMP_STATE_CHANGE_REQUEST = 0xA026 + MSG_ID_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE = 0xA027 + MSG_ID_DG_START_STOP_UV_REACTORS = 0xA028 + MSG_ID_DG_REQUEST_CALIBRATION_DATA = 0xA029 + MSG_ID_DG_FANS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA02A + MSG_ID_DG_UV_REACTORS_HEALTH_OVERRIDE = 0xA02C + MSG_ID_DG_THERMISTORS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA02D + MSG_ID_DG_THERMISTORS_VALUE_OVERRIDE = 0xA02E + MSG_ID_DG_RO_PUMP_DUTY_CYCLE_OVERRIDE = 0xA02F + MSG_ID_DG_VALVES_SENSED_STATE_OVERRIDE = 0xA030 + MSG_ID_DG_SET_RO_PUMP_TARGET_FLOW = 0xA031 + MSG_ID_DG_RO_PUMP_TARGET_PRESSURE_OVERRIDE = 0xA032 + MSG_ID_DG_SET_CALIBRATION_RECORD = 0xA033 + MSG_ID_DG_GET_CALIBRATION_RECORD = 0xA034 + MSG_ID_DG_SEND_CALIBRATION_RECORD = 0xA035 + MSG_ID_DG_SET_SYSTEM_RECORD = 0xA036 + MSG_ID_DG_GET_SYSTEM_RECORD = 0xA037 + MSG_ID_DG_SEND_SYSTEM_RECORD = 0xA038 + MSG_ID_DG_GET_SERVICE_RECORD = 0xA039 + MSG_ID_DG_SET_SERVICE_RECORD = 0xA03A + MSG_ID_DG_SEND_SERVICE_RECORD = 0xA03B + MSG_ID_DG_GET_SCHEDULED_RUNS_RECORD = 0xA03C + MSG_ID_DG_SET_SCHEDULED_RUNS_RECORD = 0xA03D + MSG_ID_DG_SEND_SCHEDULED_RUNS_RECORD = 0xA03E + MSG_ID_DG_FLUID_LEAK_SEND_INTERVAL_OVERRIDE = 0xA03F + MSG_ID_DG_FLUID_LEAK_STATE_DETECTOR_OVERRIDE = 0xA040 + MSG_ID_DG_RUN_MODE_FILL_FOR_CAL_CHECK = 0xA041 + MSG_ID_FILTER_FLUSH_TIME_PERIOD_OVERRIDE = 0xA042 + MSG_ID_DG_FANS_RPM_OVERRIDE = 0xA043 + MSG_ID_FLOW_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA044 + MSG_ID_DG_STOP_RTC_CLOCK = 0xA045 + MSG_ID_DG_DRAIN_PUMP_MEASURED_RPM_OVERRIDE = 0xA046 + MSG_ID_DG_SUPER_CLEAR_ALARMS_CMD = 0xA047 + MSG_ID_DG_ALARM_INFO_SEND_INTERVAL_OVERRIDE = 0xA048 + MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD = 0xA049 + MSG_ID_DG_GET_SW_CONFIG_RECORD = 0xA04A + MSG_ID_DG_SET_SW_CONFIG_RECORD = 0xA04B + MSG_ID_DG_SEND_SW_CONFIG_RECORD = 0xA04C + MSG_ID_DG_FANS_DUTY_CYCLE_OVERRIDE = 0xA04D + MSG_ID_DG_USED_ACID_VOLUME_ML_OVERRIDE = 0xA04E + MSG_ID_DG_SET_FANS_RPM_ALARM_START_TIME_OFFSET = 0xA04F + MSG_ID_DG_USED_BICARB_VOLUME_ML_OVERRIDE = 0xA050 + MSG_ID_FILL_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA051 + MSG_ID_DG_GEN_IDLE_PUBLISH_INTERVAL_OVERRIDE = 0xA052 + MSG_ID_DG_GET_USAGE_INFO_RECORD = 0xA053 + MSG_ID_DG_SET_USAGE_INFO_RECORD = 0xA054 + MSG_ID_DG_SEND_USAGE_INFO_RECORD = 0xA055 + MSG_ID_DG_SET_OP_MODE_REQUEST = 0xA056 + MSG_ID_DG_RESERVOIR_TARE_REQUEST = 0xA057 + MSG_ID_MSG_AVAILABLE_10 = 0xA058 + MSG_ID_DG_DRAIN_PUMP_CURRENT_OVERRIDE = 0xA059 + MSG_ID_DG_DRAIN_PUMP_DIRECTION_OVERRIDE = 0xA05A + MSG_ID_DG_RO_FEEDBACK_VOLTAGE_OVERRIDE = 0xA05B + MSG_ID_DG_DIALYSATE_FILL_INTEGRATED_VOLUME_OVERRIDE = 0xA05C + MSG_ID_DG_NV_RECORD_CRC_OVERRIDE = 0xA05D + MSG_ID_DG_CONC_PUMP_PARK_STATUS_OVERRIDE = 0xA05E + MSG_ID_DG_CONC_PUMP_PARK_FAULT_STATUS_OVERRIDE = 0xA05F + MSG_ID_DG_CONC_PUMP_PARK_COMMAND = 0xA060 + MSG_ID_DG_HEATERS_DUTY_CYCLE_OVERRIDE = 0xA061 + MSD_ID_DG_RTC_CTL_REG1_STATUS_OVERRIDE = 0xA062 + MSD_ID_DG_RTC_CTL_REG3_STATUS_OVERRIDE = 0xA063 + MSG_ID_DG_NELSON_DISINFECT_SUPPORT = 0xA064 + MSG_ID_DG_SET_DIALYSATE_MIXING_RATIOS = 0xA065 + MSG_ID_DG_SET_TEST_CONFIGURATION = 0xA066 + MSG_ID_DG_GET_TEST_CONFIGURATION = 0xA067 + MSG_ID_DG_SEND_TEST_CONFIGURATION = 0xA068 + MSG_ID_DG_RESET_ALL_TEST_CONFIGURATIONS = 0xA069 + MSG_ID_DG_DIALIN_CHECK_IN = 0xA06A + MSG_ID_DG_GET_LOAD_CELLS_TARE_VALUES = 0xA06B + MSG_ID_DG_SET_LOAD_CELLS_TARE_VALUES = 0xA06C + MSG_ID_DG_SEND_LOAD_CELLS_TARE_VALUES = 0xA06D + MSG_ID_DG_SET_COND_SENSOR_CAL_TABLE = 0xA06E + MSG_ID_DG_SIGNAL_RECOVER_FROM_FAULT_MODE = 0xA06F + MSG_ID_DG_DRAIN_MODE_BROADCAST_INTERVAL_OVERRIDE = 0xA070 + MSG_ID_DG_DIALIN_RO_ONLY_MODE_STATUS_REQUEST = 0xA071 + MSG_ID_DG_RAM_STATUS_OVERRIDE = 0xA072 + MSG_ID_DG_CAN_RECEIVE_ACK_MESSAGE_OVERRIDE = 0xA073 + MSG_ID_DG_RESERVOIR_BROADCAST_INTERVAL_OVERRIDE = 0xA074 + MSG_ID_DG_STATE_TIMER_77C_OVERRIDE = 0xA075 + MSG_ID_DG_STATE_TIMER_82C_OVERRIDE = 0xA076 + MSG_ID_DG_CHEM_DISINFECT_ACID_OVERRIDE = 0xA077 + + MSG_ID_HD_DEBUG_EVENT = 0xFFF1 + MSG_ID_DG_DEBUG_EVENT = 0xFFF2 + MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK = 0xFFFF Index: leahi-dialin/common/prs_defs.py =================================================================== diff -u --- leahi-dialin/common/prs_defs.py (revision 0) +++ leahi-dialin/common/prs_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,70 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file prs_defs.py +# +# @author (last) Quang Nguyen +# @date (last) 06-Jul-2021 +# @author (original) Behrouz NematiPour +# @date (original) 05-Nov-2020 +# +############################################################################ +class Ranges: + PRESSURE_STEPS = 10 + + ARTERIAL_PRESSURE_MINIMUM = -400 + ARTERIAL_PRESSURE_MAXIMUM = +600 + ARTERIAL_PRESSURE_LOW_MIN = -300 + ARTERIAL_PRESSURE_LOW_DEF = -300 + ARTERIAL_PRESSURE_LOW_MAX = +200 + ARTERIAL_PRESSURE_HIGH_MIN = -300 + ARTERIAL_PRESSURE_HIGH_DEF = +100 + ARTERIAL_PRESSURE_HIGH_MAX = +200 + + VENOUS_PRESSURE_MINIMUM = -100 + VENOUS_PRESSURE_MAXIMUM = +700 + VENOUS_PRESSURE_LOW_MIN = -100 + VENOUS_PRESSURE_LOW_DEF = -100 + VENOUS_PRESSURE_LOW_MAX = +600 + VENOUS_PRESSURE_HIGH_MIN = +100 + VENOUS_PRESSURE_HIGH_DEF = +400 + VENOUS_PRESSURE_HIGH_MAX = +600 + + +class AlarmPriority: + ALARM_HIGH = 3 + ALARM_MED = 2 + ALARM_LOW = 1 + ALARM_NONE = 0 + + +class AlarmFlags: + ALARM_STATE_FLAG_BIT_POS_SYSTEM_FAULT = 0 + ALARM_STATE_FLAG_BIT_POS_STOP = 1 + ALARM_STATE_FLAG_BIT_POS_NO_CLEAR = 2 + ALARM_STATE_FLAG_BIT_POS_NO_RESUME = 3 + ALARM_STATE_FLAG_BIT_POS_NO_RINSEBACK = 4 + ALARM_STATE_FLAG_BIT_POS_NO_END_TREATMENT = 5 + ALARM_STATE_FLAG_BIT_POS_NO_NEW_TREATMENT = 6 + ALARM_STATE_FLAG_BIT_POS_USER_MUST_ACK = 7 + ALARM_STATE_FLAG_BIT_POS_ALARMS_TO_ESCALATE = 8 + ALARM_STATE_FLAG_BIT_POS_ALARMS_SILENCED = 9 + ALARM_STATE_FLAG_BIT_POS_LAMP_ON = 10 + ALARM_STATE_FLAG_BIT_POS_UNUSED_1 = 11 + ALARM_STATE_FLAG_BIT_POS_UNUSED_2 = 12 + ALARM_STATE_FLAG_BIT_POS_UNUSED_3 = 13 + ALARM_STATE_FLAG_BIT_POS_UNUSED_4 = 14 + ALARM_STATE_FLAG_BIT_POS_TOP_CONDITION = 15 + + +class AlarmDataTypes: + ALARM_DATA_TYPE_NONE = 0 # No data given. + ALARM_DATA_TYPE_U32 = 1 # Alarm data is unsigned 32-bit integer type. + ALARM_DATA_TYPE_S32 = 2 # Alarm data is signed 32-bit integer type. + ALARM_DATA_TYPE_F32 = 3 # Alarm data is 32-bit floating point type. + ALARM_DATA_TYPE_BOOL = 4 # Alarm data is 32-bit boolean type. + NUM_OF_ALARM_DATA_TYPES = 5 # Total number of alarm data types. Index: leahi-dialin/common/test_config_defs.py =================================================================== diff -u --- leahi-dialin/common/test_config_defs.py (revision 0) +++ leahi-dialin/common/test_config_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,40 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file test_config_defs.py +# +# @author (last) Dara Navaei +# @date (last) 16-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 24-Apr-2023 +# +############################################################################ + +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class DGTestConfigOptions(DialinEnum): + TEST_CONFIG_MIX_WITH_WATER = 0 # Test config mix with water. + TEST_CONFIG_DISABLE_INLET_WATER_TEMP_CHECK = 1 # Test config disable inlet water temperature check + TEST_CONFIG_RECOVER_TREATMENT = 2 # Test config recover treatment + NUM_OF_TEST_CONFIGS = 3 # Number of test configuration. + + +@unique +class HDTestConfigOptions(DialinEnum): + TEST_CONFIG_USE_WET_CARTRIDGE = 0 # Test configuration use wet cartridge. + TEST_CONFIG_USE_WORN_CARTRIDGE = 1 # Test configuration use worn cartridge. + TEST_CONFIG_EXPEDITE_PRE_TREATMENT = 2 # Test configuration expedite pre-treatment. + TEST_CONFIG_SKIP_BLOOD_PRIME = 3 # Test configuration skip blood prime. + TEST_CONFIG_SKIP_DISINFECT_AND_SERVICE_TX_BLOCKERS = 4 # Test configuration skip disinfect and service treatment blockers. + TEST_CONFIG_DISABLE_WET_SELFTEST_DISPLACEMENT_CHECK = 5 # Test configuration disable wet self test displacement check. + TEST_CONFIG_RECOVER_TREATMENT = 6 # Test configuration recover treatment. + TEST_CONFIG_ENABLE_ONE_MINUTE_TREATMENT = 7 # Test configuration enable one minute treatment. + TEST_CONFIG_DISABLE_BLOOD_LEAK_ALARM = 8 # Test configuration disable blood leak alarm. + NUM_OF_TEST_CONFIGS = 9 # Number of test configurations Index: leahi-dialin/common/ui_defs.py =================================================================== diff -u --- leahi-dialin/common/ui_defs.py (revision 0) +++ leahi-dialin/common/ui_defs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,172 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file ui_defs.py +# +# @author (last) Vy +# @date (last) 10-Oct-2023 +# @author (original) Peter Lucia +# @date (original) 06-Apr-2021 +# +############################################################################ + +from . import RequestRejectReasons + + +class EResponse: + Rejected = 0 + Accepted = 1 + + +class GuiActionType: + Unknown = 0 + PowerOff = 1 + KeepAlive = 7 + BloodFlow = 5 + DialysateInletFlow = 8 + DialysateOutletFlow = 11 + TreatmentTime = 13 + PowerOffBroadcast = 14 + + AlarmStatus = 2 + AlarmTriggered = 3 + AlarmCleared = 4 + + PressureOcclusion = 9 + + DGDrainPumpData = 36 + DGHeatersData = 44 + LoadCellReadings = 12 + DGPressuresData = 32 + TemperatureSensors = 45 + + CanBUSFaultCount = 2457 + HDDebugText = 0xFFF1 + DGDebugText = 0xFFF2 + + AdjustBloodDialysateReq = 23 + AdjustBloodDialysateRsp = 24 + + AdjustDurationReq = 22 + AdjustDurationRsp = 27 + + AdjustUltrafiltrationStateReq = 16 + AdjustUltrafiltrationStateRsp = 65 + AdjustUltrafiltrationEditReq = 17 + AdjustUltrafiltrationEditRsp = 19 + AdjustUltrafiltrationConfirmReq = 21 + AdjustUltrafiltrationConfirmRsp = 46 + + AdjustPressuresLimitsReq = 70 + AdjustPressuresLimitsRsp = 71 + + TreatmentRanges = 26 + + String = 65279 + Acknow = 65535 + # Generic Acknowledgment is not a unique message ID and + # inherits its Id from the actual message. Zero is a placeholder + AcknowGeneric = 0 + + +class GuiActionTypeLength: + TREATMENT_LOG_LENGTH = 33 + + +class TXStates: + # Sub Mode + TREATMENT_START_STATE = 0 # Start treatment - initialize treatment and go to blood prime state + TREATMENT_BLOOD_PRIME_STATE = 1 # Prime blood-side of dialyzer with gradual ramp for 1 min. + TREATMENT_DIALYSIS_STATE = 2 # Perform dialysis. + TREATMENT_STOP_STATE = 3 # Treatment stopped. All pumps off. Dializer bypassed + TREATMENT_RINSEBACK_STATE = 4 # Perform rinseback with saline. Dialyzer bypassed. Dialysate recirculating + TREATMENT_RECIRC_STATE = 5 # Recirculate saline and dialysate while patient disconnected + TREATMENT_END_STATE = 6 # Dialysis has ended + + # Saline states + SALINE_BOLUS_STATE_IDLE = 0 # No saline bolus delivery is in progress + SALINE_BOLUS_STATE_WAIT_FOR_PUMPS_STOP = 1 # Wait for pumps to stop before starting bolus + SALINE_BOLUS_STATE_IN_PROGRESS = 2 # A saline bolus delivery is in progress + SALINE_BOLUS_STATE_MAX_DELIVERED = 3 # Maximum saline bolus volume reached + + # UF states + UF_PAUSED_STATE = 0 # Paused state of the ultrafiltration state machine + UF_RUNNING_STATE = 1 # Running state of the ultrafiltration state machine + UF_OFF_STATE = 3 # Completed/off state of the ultrafiltration state machine + UF_COMPLETED_STATE = 4 # Completed state of ultrafiltration state machine + + # Heparin states + HEPARIN_STATE_OFF = 0 # No heparin delivery is in progress + HEPARIN_STATE_PAUSED = 1 # Heparin delivery paused + HEPARIN_STATE_INITIAL_BOLUS = 2 # Initial heparin bolus delivery in progress + HEPARIN_STATE_DISPENSING = 3 # Gradual heparin dispensing in progress + HEPARIN_STATE_COMPLETED = 4 # Heparin delivery stopped due to the set stop time before treatment end + HEPARIN_STATE_EMPTY = 5 # Heparin Syringe empty + + # Rinseback states + RINSEBACK_STOP_INIT_STATE = 0 # Start state (stopped) of the rinseback sub-mode state machine + RINSEBACK_RUN_STATE = 1 # Rinseback running state of the rinseback sub-mode state machine + RINSEBACK_PAUSED_STATE = 2 # Rinseback paused state of the rinseback sub-mode state machine + RINSEBACK_STOP_STATE = 3 # Rinseback stopped (done) state of the rinseback sub-mode state machine + RINSEBACK_RUN_ADDITIONAL_STATE = 4 # Additional rinseback volume (10 mL) state + + # Recirculate + TREATMENT_RECIRC_RECIRC_STATE = 0 # Re-circulate state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_STOPPED_STATE = 1 # Stopped state of the treatment re-circulate sub-mode state machine + + # Blood Prime + BLOOD_PRIME_RAMP_STATE = 0 # Ramp state of the blood prime sub-mode state machine + + # Treatment End + TREATMENT_END_WAIT_FOR_RINSEBACK_STATE = 0 # Wait for rinseback state of the treatment end sub-mode state machine + TREATMENT_END_PAUSED_STATE = 1 # Paused state of the treatment end sub-mode state machine + + # Treatment Stop + TREATMENT_STOP_RECIRC_STATE = 0 # Dialysate re-circulation state + TREATMENT_STOP_NO_RECIRC_STATE = 1 # No dialysate re-circulation state + + +class TreatmentParameterRejections: + def __init__(self): + self.param_request_valid = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_blood_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialysate_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_duration = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_stop_time = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_saline_bolus = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_acid_concentrate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_bicarbonate_concentrate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialyzer_type = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_blood_pressure_measure_interval = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_rinseback_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_arterial_pressure_limit_low = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_arterial_pressure_limit_high = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_venous_pressure_limit_low = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_venous_pressure_limit_high = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_dispensing_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_bolus_volume = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialysate_temp = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + + def set_all_valid(self): + """ + Sets all parameters as valid + + @return: None + """ + for attr in dir(self): + if not callable(getattr(self, attr)) and attr.startswith("param_"): + self.__dict__[attr] = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + + def set_all_invalid(self): + """ + Sets all treatment parameters to be invalid + + @return: None + """ + for attr in dir(self): + if not callable(getattr(self, attr)) and attr.startswith("param_"): + self.__dict__[attr] = RequestRejectReasons.REQUEST_REJECT_REASON_NOT_ALLOWED_IN_CURRENT_MODE Index: leahi-dialin/dg/__init__.py =================================================================== diff -u --- leahi-dialin/dg/__init__.py (revision 0) +++ leahi-dialin/dg/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,26 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file __init__.py +# +# @author (last) Dara Navaei +# @date (last) 01-Dec-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +from .constants import RESET, NO_RESET +from .dialysate_generator import DG +from .drain_pump import DGDrainPump +from .hd_proxy import DGHDProxy +from .heaters import Heaters +from .load_cells import DGLoadCells +from .pressures import DGPressures +from .reservoirs import DGReservoirs +from .ro_pump import DGROPump +from .temperatures import TemperatureSensors +from .valves import DGValves Index: leahi-dialin/dg/accelerometer.py =================================================================== diff -u --- leahi-dialin/dg/accelerometer.py (revision 0) +++ leahi-dialin/dg/accelerometer.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,251 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file accelerometer.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Sean Nash +# @date (original) 29-Jul-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGAccelerometer(AbstractSubSystem): + """ + Hemodialysis Delivery (DG) Dialin API sub-class for accelerometer related commands. + """ + + # Vector axes + class AccelerometerVector: + def __init__(self, x=0.0, y=0.0, z=0.0): + """ + DGAccelerometer constructor + + """ + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return "{0}: ({1},{2},{3})".format(self.__class__.__name__, self.x, self.y, self.z) + + # Vector axes + VECTOR_AXIS_X = 0 + VECTOR_AXIS_Y = 1 + VECTOR_AXIS_Z = 2 + + def __init__(self, can_interface, logger: Logger): + """ + HDAccelerometer constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_ACCELEROMETER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_accelerometer_sync) + self.dg_accel_timestamp = 0.0 + self.vector = self.AccelerometerVector() + self.vector_max = self.AccelerometerVector() + self.tilts = self.AccelerometerVector() + + def get_accel_vector(self): + """ + Gets the accelerometer vector. + @return: (vector) The vector from the accelerometer + """ + return self.vector + + def get_accel_max_vector(self): + """ + Gets the accelerometer maximum vector. + @return: (vector) The max. vector from the accelerometer + """ + return self.vector_max + + def get_accel_tilts(self): + """ + Gets the tilt angles from the accelerometer. + @return: (vector) The X, Y, and Z tilt angles. + """ + return self.tilts + + @publish([ + "dg_accel_timestamp", + "vector", + "vector_max", + "tilts" + ]) + def _handler_accelerometer_sync(self, message, timestamp = 0.0): + """ + Handles published accelerometer data messages. Accelerometer data are captured + for reference. + + @param message: published accelerometer data message + @return: none + """ + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + self.vector = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + self.vector_max = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + self.tilts = self.AccelerometerVector(x[0], y[0], z[0]) + self.dg_accel_timestamp = timestamp + + def cmd_accel_vector_override(self, axis: int, mag: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer vector override command + Constraints: + Must be logged into DG. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer axis magnitude") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(mag) + " g. " + self.logger.debug("Accelerometer axis " + str(axis) + " overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_accel_max_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer maximum vector override command + Constraints: + Must be logged into DG. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_MAX_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer axis maximum magnitude") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(mag) + " g. " + self.logger.debug("Accelerometer max. axis " + str(axis) + " overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_accel_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Accelerometer broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/alarms.py =================================================================== diff -u --- leahi-dialin/dg/alarms.py (revision 0) +++ leahi-dialin/dg/alarms.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,322 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file alarms.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Quang Nguyen +# @date (original) 02-Sep-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.dg_defs import DGEventDataType +from ..utils.checks import check_broadcast_interval_override_ms +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGAlarms(AbstractSubSystem): + """ + DG interface containing alarm related commands. + """ + _ALARM_ID_MAX_ALARMS = 500 + START_POS_ALARM_ID = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ALARM_ID = START_POS_ALARM_ID + 2 + + def __init__(self, can_interface, logger: Logger): + """ + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_TRIGGERED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_triggered) + + msg_id = MsgIds.MSG_ID_ALARM_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_clear) + + msg_id = MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_condition_clear) + + msg_id = MsgIds.MSG_ID_DG_ALARM_INFO_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_information_sync) + + self.dg_alarm_triggered_timestamp = 0.0 + self.dg_alarm_clrd_timestamp = 0.0 + self.dg_alarm_condition_clrd_timestamp = 0.0 + self.dg_alarm_info_timestamp = 0.0 + # alarm states based on received DG alarm activation and alarm clear messages + self.alarm_states = [False] * self._ALARM_ID_MAX_ALARMS + # alarm condition states based on received DG alarm activation and clear condition messages + self.alarm_conditions = [False] * self._ALARM_ID_MAX_ALARMS + self.safety_shutdown_active = False + self.alarm_data_type = dict() + self.alarm_data = [0, 0] * self._ALARM_ID_MAX_ALARMS + # alarm priorities based on received DG alarm activation messages + self.alarm_priorities = [0] * self._ALARM_ID_MAX_ALARMS + # alarm ranks based on received DG alarm activation messages + self.alarm_ranks = [0] * self._ALARM_ID_MAX_ALARMS + # alarm clear top only flags based on received DG alarm activation messages + self.alarm_clear_top_only_flags = [False] * self._ALARM_ID_MAX_ALARMS + + + # Loop through the list of the event data type enum and update the dictionary + for data_type in DGEventDataType: + event_data_type = DGEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type or 'NONE' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self.alarm_data_type[event_data_type] = struct_unpack_type + + def get_alarm_states(self): + """ + Gets all states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_states + + def get_alarm_conditions(self): + """ + Gets all alarm condition states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_conditions + + def get_alarm_state(self, alarm_id): + """ + Gets alarm state for given alarm + + @return: Alarm state + """ + return self.alarm_states[alarm_id] + + def get_safety_shutdown_activated(self): + """ + Gets the state of the DG safety shutdown signal. + + @return: (bool) safety shutdown line is activated (T/F) + """ + return self.safety_shutdown_active + + def clear_dialin_alarms(self): + """ + Clears the alarms states in Dialin. + + @return: none + """ + for x in range(self._ALARM_ID_MAX_ALARMS): + self.alarm_states[x] = False + + def get_alarm_data(self, alarm_id) -> list: + """ + Gets the alarm data fields for the requested alarm id + + @return: the alarm data fields for the requested alarm id + """ + return self.alarm_data[alarm_id] + + @publish(["dg_alarm_triggered_timestamp", "alarm_states", "alarm_conditions", "alarm_data", + "alarm_priorities", "alarm_ranks", "alarm_clear_top_only_flags"]) + def _handler_alarm_triggered(self, message, timestamp = 0.0): + """ + Handles published DG alarm activation messages. + + @param message: published DG alarm activation message + @return: none + """ + + alarm_id = struct.unpack(' int: + """ + Constructs and sends the alarm state override command + Constraints: + Must be logged into DG. + Given alarm must be valid. + If inactivating alarm, given alarm must be recoverable (clearable). + + @param alarm: integer - ID of alarm to override + @param state: integer - 1 for alarm active, 0 for alarm inactive + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + alm = integer_to_bytearray(alarm) + payload = rst + sta + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ALARM_STATE_OVERRIDE.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("active" if state != 0 else "inactive") + self.logger.debug("Alarm " + str(alarm) + " " + str_res + ": " + str( + received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_clear_all_alarms(self) -> int: + """ + Constructs and sends the clear all active alarms command to the DG. + This will clear even non-recoverable alarms. + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + + key = integer_to_bytearray(-758926171) # 0xD2C3B4A5 + payload = key + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SUPER_CLEAR_ALARMS_CMD.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("All DG alarms cleared.") + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_alarm_info_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET): + """ + Constructs and sends the alarm information broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ALARM_INFO_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG alarm information broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("DG alarm information broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/calibration_record.py =================================================================== diff -u --- leahi-dialin/dg/calibration_record.py (revision 0) +++ leahi-dialin/dg/calibration_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,841 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file calibration_record.py +# +# @author (last) Dara Navaei +# @date (last) 11-Apr-2024 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsDG +from ..utils.conversions import integer_to_bytearray + + +class DGCalibrationNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for calibration commands. + """ + + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_HIGH_ORDER_GAIN_VALUE = 0 + _DEFAULT_GAIN_VALUE = 1 + _DEFAULT_OFFSET_VALUE = 0 + _DEFAULT_RATIO_VALUE = 1 + _DEFAULT_VOLUME_VALUE = 0 + _DEFAULT_EMPTY_RESERVOIR_WEIGHT_G = 1700 + _DEFAULT_ACID_MIX_RATIO = (2.35618 / 100) + _DEFAULT_ACID_BOTTLE_VOL_ML = 3430.0 + _DEFAULT_ACID_COND_US_PER_CM = 11645.05 + _DEFAULT_ACID_BOTTLE_TEMP_C = 23.5 + _DEFAULT_BICARB_MIX_RATIO = (4.06812 / 100) + _DEFAULT_BICARB_BOTTLE_VOL_ML = 3780.0 + _DEFAULT_BICARB_COND_US_PER_CM = 13734.88 + _DEFAULT_BICARB_BOTTLE_TEMP_C = 23.5 + _DEFAULT_CALIBRATION_VALUE = 1 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _DEFAULT_FLUSH_LINES_VOLUME = 0.01 + _DEFAULT_ULTRAFILTER_TAU_C_PER_MIN = -0.6 + _DEFAULT_RESERVOIR_TAU_C_PER_MIN = -0.25 + _DEFAULT_ULTRAFILTER_VOLUME_ML = 700 + _DEFAULT_COND_SENSOR_TEMP_COMP_COEFF = 0.0207 + # Fill acid/bicarb target conductivities uS/cm + _DEFAULT_FILL_1251_1_ACID_SNSR_US_PER_CM = 0.0 + _DEFAULT_FILL_1251_1_BIC_SNSR_US_PER_CM = 13616.23 + _DEFAULT_FILL_2251_0_ACID_SNSR_US_PER_CM = 0.0 + _DEFAULT_FILL_2251_0_BIC_SNSR_US_PER_CM = 13734.88 + _DEFAULT_FILL_3251_9_ACID_SNSR_US_PER_CM = 0.0 + _DEFAULT_FILL_3251_9_BIC_SNSR_US_PER_CM = 13854.49 + _DEFAULT_ACID_TEST_1251_1_US_PER_CM = 13768.28 + _DEFAULT_ACID_TEST_2251_0_US_PER_CM = 13919.05 + _DEFAULT_ACID_TEST_3251_9_US_PER_CM = 14071.04 + _DEFAULT_BIC_TEST_ACID_SNSR_US_PER_CM = 0.0 + _DEFAULT_BIC_TEST_BIC_SNSR_US_PER_CM = 3890.0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + _FIRMWARE_STACK_NAME = 'DG' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._is_getting_cal_in_progress = False + self._cal_data = 0 + self._raw_cal_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # DG Calibration_record main record + self.dg_calibration_record = self._prepare_dg_calibration_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_CALIBRATION_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_calibration_sync) + + self.dg_calibration_record_timestamp = 0.0 + + def cmd_reset_dg_calibration_record(self) -> bool: + """ + Handles resetting DG calibration record. + + @return: True if successful, False otherwise + """ + self.dg_calibration_record = self._prepare_dg_calibration_record() + self.dg_calibration_record = self._utilities.reset_fw_record(self.dg_calibration_record) + status = self.cmd_set_dg_calibration_record(self.dg_calibration_record) + + return status + + def cmd_get_dg_calibration_record_report(self, report_destination: str = None): + """ + Handles getting DG calibration_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.CAL_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + + observer = NVUtilsObserver("dg_calibration_record") + # Attach the observer to the list + self.attach(observer) + + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_dg_calibration_record() + + while not observer.received: + sleep(0.1) + + # Pass the DG calibration record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.dg_calibration_record) + + def cmd_request_dg_calibration_record(self) -> bool: + """ + Handles getting DG calibration_record record from firmware. + + @return: True if successful, False otherwise + """ + self.logger.debug("Requesting a dg calibration record...") + + if not self._is_getting_cal_in_progress: + + self._is_getting_cal_in_progress = True + self._raw_cal_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_CALIBRATION_RECORD.value) + + received_message = self.can_interface.send(message, time_out=5) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + + self.logger.debug("Timeout!!!!") + + self.logger.warning("Request cancelled: an existing request is in progress.") + return False + + def cmd_dg_calibration_record_crc_override(self, crc: int) -> bool: + """ + Handles setting DG calibration_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + dg_record = integer_to_bytearray(NVRecordsDG.NVDATAMGMT_CALIBRATION_RECORD.value) + payload = reset_byte_array + crc_value + dg_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding DG calibration record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_dg_calibration_sync(self, message, timestamp = 0.0): + """ + Handles published DG calibration_record record messages. DG calibration records are captured for + processing and updating the DG calibration_record record. + + @param message: published DG calibration_record record data message + + @return: None + """ + self.logger.debug("DG calibration sync handler...") + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = self._RECORD_START_INDEX + self._RECORD_SPECS_BYTES + length + + # Get the calibration_record data only + self._cal_data = message['message'][self._RECORD_START_INDEX:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other. + if self._current_message <= self._total_messages: + self._raw_cal_record += (message['message'][self._RECORD_START_INDEX + + self._RECORD_SPECS_BYTES:end_of_data_index]) + + if self._current_message == self._total_messages: + # Check if the requested read was just for comparing the results before writing to firmware back + self._is_getting_cal_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_calibration_record, self._raw_cal_record) + self.dg_calibration_record_timestamp = timestamp + self._handler_received_complete_dg_calibration_record() + + @publish(["dg_calibration_record_timestamp","dg_calibration_record"]) + def _handler_received_complete_dg_calibration_record(self): + """ + Publishes the received calibration record + + @return: None + """ + self.logger.debug("Received a complete dg calibration record.") + + def cmd_set_dg_calibration_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the calibration data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_dg_calibration_record() + observer = NVUtilsObserver("dg_calibration_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.dg_calibration_record, report_address, + self._utilities.CAL_RECORD_TAB_NAME) + + ret = self.cmd_set_dg_calibration_record(self.dg_calibration_record) + return ret + + def cmd_set_dg_calibration_record(self, previous_record: OrderedDict) -> bool: + """ + Handles updating the DG calibration record with the newest calibration_record data of a hardware and + sends it to FW. + + @param previous_record: (OrderedDict) the dg calibration record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + # Pass the new changes as well as the previous calibration record + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + self.logger.debug('Setting DG calibration started') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_CALIBRATION_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("DG ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending DG calibration record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending DG calibration record.") + return True + + def _prepare_dg_calibration_record(self): + """ + Handles assembling the sub dictionaries of each hardware group to make a DG calibration record. + + @return: (OrderedDict) the assembled record + """ + result = OrderedDict() + + groups_byte_size = 0 + # Call the other functions to get the dictionaries of each hardware group. All the dictionaries are + # ordered dictionaries to maintain the order in which they are inserted. The results are a tuple, the first + # element is the dictionary that was built and the second element is the byte size of the dictionary. + records_with_sizes = [self._prepare_pressure_sensors_cal_record(), self._prepare_flow_sensors_cal_record(), + self._prepare_load_cells_record(), self._prepare_temperature_sensors_record(), + self._prepare_conductivity_sensors_record(), + self._prepare_conductivity_sensors_temperature_compensation_record(), + self._prepare_pumps_record(), + self._prepare_volume_record(), self._prepare_acid_concentrates_record(), + self._prepare_bicarb_concentrates_record(), self._prepare_filters_record(), + self._prepare_fans_record(), self._prepare_accelerometer_sensor_record(), + self._prepare_heating_constants_record(), + self._prepare_acid_08_1251_1_conductivity_record(), + self._prepare_acid_08_2251_0_conductivity_record(), + self._prepare_acid_08_3251_9_conductivity_record()] + + for record, byte_size in records_with_sizes: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += byte_size + # Update the calibration record + result.update(record) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' None: + """ + Clears public class properties that are updated by the handler. + Specifically properties updated by the DG broadcast message + MSG_ID_DG_CHEM_DISINFECT_DATA. + + @returns none + """ + + self.chemical_disinfect_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.cancellation_mode = 0 + self.r1_level = 0 + self.r2_level = 0 + self.acid_average_cond_us_per_cm = 0.0 + self.chemical_disinfect_elapsed_time = 0 + + @publish(["dg_chem_disinfect_time_timestamp", "chemical_disinfect_elapsed_time", "chemical_disinfect_target_time"]) + def _handler_chemical_disinfect_to_ui_sync(self, message, timestamp=0.0): + """ + Handles published chemical disinfect message + + @param message: published chemical disinfect to UI data message + @returns none + """ + + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + disinfect_target_time, index = bytearray_to_integer(payload, index, False) + disinfect_elapsed_time, index = bytearray_to_integer(payload, index, False) + + self.chemical_disinfect_target_time = int(disinfect_target_time / 1000) + self.chemical_disinfect_elapsed_time = int(disinfect_elapsed_time / 1000) + self.dg_chem_disinfect_time_timestamp = timestamp + + @publish(["dg_chem_disinfect_data_timestamp", "chemical_disinfect_state", "overall_elapsed_time", + "state_elapsed_time", "cancellation_mode", "r1_level", "r2_level", "acid_average_cond_us_per_cm"]) + def _handler_chemical_disinfect_sync(self, message, timestamp=0.0): + """ + Handles published chemical disinfect message + + @param message: published chemical disinfect data message + @returns none + """ + + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + state, index = bytearray_to_integer(payload, index, False) + elapsed_time, index = bytearray_to_integer(payload, index, False) + state_elapsed_time, index = bytearray_to_integer(payload, index, False) + cancellation_mode, index = bytearray_to_integer(payload, index, False) + r1, index = bytearray_to_float(payload, index, False) + r2, index = bytearray_to_float(payload, index, False) + acid_cond, index = bytearray_to_float(payload, index, False) + + self.chemical_disinfect_state = state + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.cancellation_mode = cancellation_mode + self.r1_level = r1 + self.r2_level = r2 + self.acid_average_cond_us_per_cm = acid_cond + self.dg_chem_disinfect_data_timestamp = timestamp + + def get_chem_disinfect_target_time(self) -> int: + """ + Get Chemical Disinfection target time + + @return: (int) + """ + return self.chemical_disinfect_target_time + + def get_chem_disinfect_elapsed_time(self) -> int: + """ + Get Chemical Disinfection elapsed time + + @return: (int) + """ + return self.chemical_disinfect_elapsed_time + + def get_chem_disinfect_state(self) -> int: + """ + Gets Chemical Disinfection state + + @return: (int) + """ + return self.chemical_disinfect_state + + def get_chem_disinfect_overall_elapsed_time(self) -> int: + """ + Gets Chemical Disinfection overall elapsed time + + @return: (int) + """ + return self.overall_elapsed_time + + def get_chem_disinfect_state_elapsed_time(self) -> int: + """ + Gets Chemical Disinfection state elapsed time + + @return: (int) + """ + return self.state_elapsed_time + + def get_chem_disinfect_cancellation_mode(self) -> int: + """ + Gets Chemical Disinfection cancellation mode + + @return: (int) + """ + return self.cancellation_mode + + def get_chem_disinfect_r1_level(self) -> int: + """ + Gets Chemical Disinfection r1 level + + @return: (int) + """ + return self.r1_level + + def get_chem_disinfect_r2_level(self) -> int: + """ + Gets Chemical Disinfection r2 level + + @return: (int) + """ + return self.r2_level + + def cmd_acid_moving_average_override(self, conductivity: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the chem disinfect acid moving average override command + + @param conductivity: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + cond_byte_array = float_to_bytearray(conductivity) + payload = reset_byte_array + cond_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CHEM_DISINFECT_ACID_OVERRIDE.value, + payload=payload) + + self.logger.debug("override CD2 average value for sensor to" + str(conductivity)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/dg/chemical_disinfect_flush.py =================================================================== diff -u --- leahi-dialin/dg/chemical_disinfect_flush.py (revision 0) +++ leahi-dialin/dg/chemical_disinfect_flush.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,88 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file chemical_disinfect_flush.py +# +# @author (last) Micahel Garthwaite +# @date (last) 18-May-2023 +# @author (original) Steve Jarpe +# @date (original) 30-Nov-2022 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..common.dg_defs import DGChemDisinfectFlushStates, DGChemDisinfectFlushUIStates + + +class ChemicalDisinfectFlushMode(AbstractSubSystem): + """ + Chemical Disinfect Flush Mode class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.flush_state = DGChemDisinfectFlushStates.DG_CHEM_DISINFECT_FLUSH_STATE_START.value + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.rinse_count = 0 + self.flush_UI_state = DGChemDisinfectFlushUIStates.CHEM_DISINFECT_FLUSH_UI_STATE_NOT_RUNNING.value + self.dg_chem_disinfect_flush_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_CHEM_DISINFECT_FLUSH_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_chem_disinfect_flush_sync) + + def clear_flush_info(self) -> None: + """ + Clears public class properties that are updated by the handler. + Specifically properties updated by the DG broadcast message + MSG_ID_DG_CHEM_DISINFECT_FLUSH_DATA. + + @returns : none + """ + self.flush_state = DGChemDisinfectFlushStates.DG_CHEM_DISINFECT_FLUSH_STATE_START.value + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + + @publish(["dg_chem_disinfect_flush_timestamp","flush_state", "overall_elapsed_time", "state_elapsed_time", + "r1", "r2", "target_rinse_count", "rinse_count", "flush_UI_state"]) + def _handler_chem_disinfect_flush_sync(self, message: dict, timestamp = 0.0) -> None: + """ + Handles published flush message + + @param message: published flush data message + @returns none + """ + state = struct.unpack(' int: + """ + Constructs and sends the concentrate pump state change request command + + @param pump_id: unsigned int - concentrate pump ID + @param on: bool - 1 to turn on, 0 to turn off + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + payload = integer_to_bytearray(0) + integer_to_bytearray(on) + integer_to_bytearray(pump_id) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONCENTRATE_PUMP_STATE_CHANGE_REQUEST.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + if on: + self.logger.debug("Requested to turn on concentrate pump: CP" + str(pump_id)) + else: + self.logger.debug("Requested to turn off concentrate pump: CP" + str(pump_id)) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_target_speed_override(self, pump_id: int, speed: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_measured_speed_override(self, pump_id: int, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump measured speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - measured speed value to override concentrate pump with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset back to normal value for pump: " + str(pump_id)) + else: + self.logger.debug("override measured speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_parked_status_override(self, pump_id: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump parked state override command + + @param pump_id: unsigned int - concentrate pump ID + @param status: unsigned int - 1 = parked, 0 = not parked + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + status_byte_array = integer_to_bytearray(status) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + status_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CONC_PUMP_PARK_STATUS_OVERRIDE.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset parked status back to normal for pump: " + str(pump_id)) + else: + self.logger.debug("override parked status to: " + str(status) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_park_fault_state_override(self, pump_id: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump park fault state override command + + @param pump_id: unsigned int - concentrate pump ID + @param status: unsigned int - 1 = fault, 0 = no fault + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + status_byte_array = integer_to_bytearray(status) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + status_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CONC_PUMP_PARK_FAULT_STATUS_OVERRIDE.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset park fault status back to normal for pump: " + str(pump_id)) + else: + self.logger.debug("override park fault status to: " + str(status) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_park_command(self, pump_id: int) -> int: + """ + Constructs and sends the concentrate pump park command + + @param pump_id: unsigned int - concentrate pump ID + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + payload = integer_to_bytearray(pump_id) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CONC_PUMP_PARK_COMMAND.value, + payload=payload) + + self.logger.debug("park concentrate pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG concentrate pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/conductivity_sensors.py =================================================================== diff -u --- leahi-dialin/dg/conductivity_sensors.py (revision 0) +++ leahi-dialin/dg/conductivity_sensors.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,280 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file conductivity_sensors.py +# +# @author (last) Dara Navaei +# @date (last) 15-May-2023 +# @author (original) Quang Nguyen +# @date (original) 20-Jul-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ConductivitySensorsEnum(DialinEnum): + CPI = 0 + CPO = 1 + CD1 = 2 + CD2 = 3 + NUM_OF_CONDUCTIVITY_SENSORS = 4 + + +@unique +class ConductivitySensorsCalTableName(DialinEnum): + CAL_DATA_CPI_COND_SENSOR = 0 # CPi conductivity sensor. + CAL_DATA_CPO_COND_SENSOR = 1 # CPo conductivity sensor. + CAL_DATA_CD1_COND_SENSOR = 2 # CD1 conductivity sensor. + CAL_DATA_CD2_COND_SENSOR = 3 # CD2 conductivity sensor. + CAL_DATA_CD2_COND_SENSOR_CHEM_DISINFECT = 4 # CD2 chemical disinfect conductivity sensor. + CAL_DATA_CD2_COND_SENSOR_FILL_BICARB_TEST = 5 # CD2 conductivity sensor fill bicarb test. + NUM_OF_CAL_DATA_COND_SENSORS = 6 # Number of conductivity sensors. + + +class ConductivitySensors(AbstractSubSystem): + """ + ConductivitySensors + + Dialysate Generator (DG) Dialin API sub-class for conductivity sensors related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_CONDUCTIVITY_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_conductivity_sensors_sync) + + self.ro_rejection_ratio = 0.0 + self.conductivity_sensor_cpi = 0.0 + self.conductivity_sensor_cpo = 0.0 + self.conductivity_sensor_cd1 = 0.0 + self.conductivity_sensor_cd2 = 0.0 + + self.raw_conductivity_sensor_cpi = 0.0 + self.raw_conductivity_sensor_cpo = 0.0 + self.raw_conductivity_sensor_cd1 = 0.0 + self.raw_conductivity_sensor_cd2 = 0.0 + + self.cpi_sensor_status = 0 + self.cpo_sensor_status = 0 + self.cd1_sensor_status = 0 + self.cd2_sensor_status = 0 + + self.dg_conductivity_timestamp = 0.0 + + def get_conductivity_sensors(self): + """ + Gets the current conductivity value + + @return: List containing conductivity values: [ conductivity_sensor_cpi, conductivity_sensor_cpo, + conductivity_sensor_cd1, conductivity_sensor_cd2 ] + """ + return [self.conductivity_sensor_cpi, self.conductivity_sensor_cpo, + self.conductivity_sensor_cd1, self.conductivity_sensor_cd2, + self.raw_conductivity_sensor_cpi, self.raw_conductivity_sensor_cpo, + self.raw_conductivity_sensor_cd1, self.raw_conductivity_sensor_cd2] + + def get_ro_rejection_ratio(self): + """ + Gets the current RO rejection ratio value + + @return: ro_rejection_ratio + """ + return self.ro_rejection_ratio + + @publish(["dg_conductivity_timestamp","ro_rejection_ratio", "conductivity_sensor_cpi", "conductivity_sensor_cpo", + "conductivity_sensor_cd1","conductivity_sensor_cd2", "raw_conductivity_sensor_cpi", "raw_conductivity_sensor_cpo", + "raw_conductivity_sensor_cd1", "raw_conductivity_sensor_cd2", "cpi_sensor_status", "cpo_sensor_status", + "cd1_sensor_status", "cd2_sensor_status"]) + def _handler_conductivity_sensors_sync(self, message, timestamp=0.0): + """ + Handles published conductivity sensor data messages. Conductivity sensor data are captured + for reference. + + @param message: published conductivity sensor data message + @return: None + """ + + self.ro_rejection_ratio = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.conductivity_sensor_cpi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.conductivity_sensor_cpo = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.conductivity_sensor_cd1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.conductivity_sensor_cd2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + self.raw_conductivity_sensor_cpi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.raw_conductivity_sensor_cpo = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.raw_conductivity_sensor_cd1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.raw_conductivity_sensor_cd2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + + self.cpi_sensor_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.cpo_sensor_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.cd1_sensor_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.cd2_sensor_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] + + self.dg_conductivity_timestamp = timestamp + + def cmd_conductivity_sensor_override(self, sensor: int, conductivity: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param conductivity: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Conductivity sensor IDs: \n + 0 = CPI \n + 1 = CPO \n + 2 = CD1 \n + 3 = CD2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + cond_byte_array = float_to_bytearray(conductivity) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + cond_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONDUCTIVITY_OVERRIDE.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_conductivity_sensor_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity sensor data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONDUCTIVITY_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG conductivity sensor broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Conductivity sensor data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_set_conductivity_sensor_cal_table(self, sensor_id: int, cal_table_id: int): + """ + Constructs and sends the conductivity sensor calibration table command + + @param sensor_id: unsigned_int - sensor ID + @param cal_table_id: unsigned_int - calibration table value to pick + @return: 1 if successful, zero otherwise + + Conductivity sensor IDs: \n + 0 = CPI \n + 1 = CPO \n + 2 = CD1 \n + 3 = CD2 \n + + Conductivity sensor calibration table IDs: \n + 0 = CAL_DATA_CPI_COND_SENSOR \n + 1 = CAL_DATA_CPO_COND_SENSOR \n + 2 = CAL_DATA_CD1_COND_SENSOR \n + 3 = CAL_DATA_CD2_COND_SENSOR \n + 4 = CAL_DATA_CD2_COND_SENSOR_CHEM_DISINFECT \n + 5 = CAL_DATA_CD2_COND_SENSOR_FILL_BICARB_TEST \n + """ + + sensor_byte_array = integer_to_bytearray(sensor_id) + cal_table_byte_array = integer_to_bytearray(cal_table_id) + payload = sensor_byte_array + cal_table_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_COND_SENSOR_CAL_TABLE.value, + payload=payload) + + self.logger.debug("Setting Sensor {} calibration table to {}".format(ConductivitySensorsEnum(sensor_id).name, + ConductivitySensorsCalTableName(cal_table_id).name)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/constants.py =================================================================== diff -u --- leahi-dialin/dg/constants.py (revision 0) +++ leahi-dialin/dg/constants.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,18 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file constants.py +# +# @author (last) Sean +# @date (last) 14-Apr-2020 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ + +RESET = 1 +NO_RESET = 0 Index: leahi-dialin/dg/cpld.py =================================================================== diff -u --- leahi-dialin/dg/cpld.py (revision 0) +++ leahi-dialin/dg/cpld.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,116 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file cpld.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Darren Cox +# @date (original) 29-Sep-2022 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum + +@unique +class CPLD_LED_COLORS(DialinEnum): + CPLD_LED_COLOR_OFF = 0 + CPLD_LED_COLOR_ORANGE_HEAT_DISINFECT = 1 + CPLD_LED_COLOR_YELLOW_CHEM_DISINFECT = 2 + CPLD_LED_COLOR_BLUE_FLUSH_DISINFECT = 3 + +class Cpld(AbstractSubSystem): + """ + CPLD + + Dialysate Generator (DG) Dialin API sub-class for CPLD related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_CPLD_STATUS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_cpld_sync) + self.cpld_wdog = 0 + self.cpld_audio = 0 + self.cpld_fault_led = 0 + self.cpld_clean_led = 0 + + self.dg_cpld_timestamp = 0.0 + + def get_cpld_wdog(self) -> int: + """ + Gets the current CPLD watchdog value + + @return: cpld_wdog - 0 if timeout, 1 if OK + """ + return self.cpld_wdog + + def get_cpld_audio(self) -> int: + """ + Gets the current CPLD audio value + + @return: cpld_audio - 0=OFF, 1=ON + """ + return self.cpld_audio + + def get_cpld_fault_led(self) -> int: + """ + Gets the current CPLD Fault LED value + + @return: cpld_fault_led - 0=OFF, 1=ON + """ + return self.cpld_fault_led + + def get_cpld_clean_led(self) -> int: + """ + Gets the current CPLD Clean LED value + + CPLD_LED_COLOR_OFF = 0 + CPLD_LED_COLOR_ORANGE_HEAT_DISINFECT = 1 + CPLD_LED_COLOR_YELLOW_CHEM_DISINFECT = 2 + CPLD_LED_COLOR_BLUE_FLUSH_DISINFECT = 3 + + @return: cpld_clean_led - CPLD_LED_COLORS enum + """ + return self.cpld_clean_led + + @publish(["dg_cpld_timestamp","cpld_wdog", "cpld_audio", "cpld_fault_led", "cpld_clean_led"]) + def _handler_cpld_sync(self, message, timestamp=0.0): + """ + Handles published CPLD data messages. CPLD data are captured + for reference. + + @param message: published CPLD data message + @return: None + """ + + self.cpld_wdog = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.START_POS_FIELD_1 + 1]))[0] + self.cpld_audio = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1 + 1:MsgFieldPositions.START_POS_FIELD_1 + 2]))[0] + self.cpld_fault_led = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1 + 2:MsgFieldPositions.START_POS_FIELD_1 + 3]))[0] + self.cpld_clean_led = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1 + 3:MsgFieldPositions.START_POS_FIELD_1 + 4]))[0] + + self.dg_cpld_timestamp = timestamp Index: leahi-dialin/dg/dg_test_configs.py =================================================================== diff -u --- leahi-dialin/dg/dg_test_configs.py (revision 0) +++ leahi-dialin/dg/dg_test_configs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,199 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dg_test_configs.py +# +# @author (last) Dara Navaei +# @date (last) 19-May-2023 +# @author (original) Dara Navaei +# @date (original) 24-Apr-2023 +# +############################################################################ + +from logging import Logger +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.test_config_defs import DGTestConfigOptions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from .constants import NO_RESET +from ..utils.conversions import integer_to_bytearray, bytearray_to_integer +from ..utils.base import AbstractSubSystem, publish + + +class DGTestConfig(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for setting and getting the test configurations. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self.dg_test_configs = dict() + self.dg_test_configs_response_timestamp = 0.0 + + self._reset_test_configs_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_TEST_CONFIGURATION.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_test_config_sync) + + def cmd_get_test_config_status(self, config: int): + """ + Returns the status of a test config + + @param config: (int) Test config to set + @return: the status of a test config + """ + return self.dg_test_configs[DGTestConfigOptions(config).name] + + def cmd_set_recover_from_mode_fault_signal(self): + """ + Constructs and sends the DG test config the signal to recover from mode fault + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SIGNAL_RECOVER_FROM_FAULT_MODE.value) + + self.logger.debug("Setting signal to recover from mode fault") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_test_config(self, config: int, reset: int = NO_RESET): + """ + Constructs and sends the DG test config + Constraints: + Must be logged into DG. + + @param config: (int) Test config to set + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + c = integer_to_bytearray(config) + payload = reset_value + c + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_TEST_CONFIGURATION.value, + payload=payload) + + if reset == NO_RESET: + self.logger.debug("Setting {}".format(DGTestConfigOptions(config).name)) + else: + self.logger.debug("Resetting {}".format(DGTestConfigOptions(config).name)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_request_test_config_status_from_fw(self): + """ + Constructs and sends the DG test configs request + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_TEST_CONFIGURATION.value) + + self.logger.debug('Getting DG test configuration record') + # Reset the test configs regardless of whether the message has been acknowledged or not. The reset might be out + # sync and reset the test configuration while the latest data has been received. If the test configuration is + # reset in Dialin but the message was not acknowledged, the user shall send the request again + self._reset_test_configs_record() + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG test configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_reset_all_test_configs(self): + """ + Constructs and sends the DG test configs reset all + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RESET_ALL_TEST_CONFIGURATIONS.value) + + self.logger.debug("Resetting all DG test configurations") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + @publish(['dg_test_configs']) + def _handler_dg_test_config_sync(self, message, timestamp=0.0): + """ + Handles published test configuration status messages. + + @param message: published DG test configurations message + @return: None + """ + payload = message['message'] + index = MsgFieldPositions.START_POS_FIELD_1 + + for config in DGTestConfigOptions.__members__: + if 'NUM_OF_TEST_CONFIGS' not in config: + config_value, index = bytearray_to_integer(payload, index, False) + self.dg_test_configs[config] = config_value + + self.dg_test_configs_response_timestamp = timestamp + + def _reset_test_configs_record(self): + """ + Resets the test configuration dictionary + + @return: None + """ + for config in DGTestConfigOptions.__members__: + # Loop through the list of the test configuration and set the values to 0xFFFFFFFF + if 'NUM_OF_TEST_CONFIGS' not in config: + self.dg_test_configs[config] = 0xFFFFFFFF Index: leahi-dialin/dg/dialysate_fill.py =================================================================== diff -u --- leahi-dialin/dg/dialysate_fill.py (revision 0) +++ leahi-dialin/dg/dialysate_fill.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,276 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dialysate_fill.py +# +# @author (last) Dara Navaei +# @date (last) 24-Feb-2024 +# @author (original) Hung Nguyen +# @date (original) 10-Mar-2022 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DialysateFillEnum(DialinEnum): + AcidVolume = 0 + BicarbVolume = 1 + + +class DialysateFill(AbstractSubSystem): + """ + DialysateFill + + Dialysate Fill Dialin API sub-class to override the used acid and bicarb volume during fill. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + # fill mode data broadcast on CAN bus + self.avg_acid = 0.0 + self.avg_bicarb = 0.0 + self.first_fill = False + self.pctDiffConduct = 0.0 + self.used_acid = 0.0 + self.used_bicarb = 0.0 + self.total_volume = 0.0 + self.dg_fill_mode_timestamp = 0.0 + self.ro_only_mode_status = 0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FILL_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_fill_mode_monitor_sync) + + def get_fill_mode_data(self): + """ + Gets the current concentrate pump data value + + @return: List containing fill mode data values: + [ avg_acid, avg_bicarb, first_fill, + pctDiffConduct, used_acid, used_bicarb, total_volume ] + """ + return [self.avg_acid, self.avg_bicarb, self.first_fill, + self.pctDiffConduct, self.used_acid, self.used_bicarb, self.total_volume] + + @publish(["dg_fill_mode_timestamp","avg_acid", "avg_bicarb", "first_fill", "pctDiffConduct", "used_acid", + "used_bicarb", "total_volume", "ro_only_mode_status"]) + def _handler_fill_mode_monitor_sync(self, message, timestamp=0.0): + """ + Handles published dialysate fill mode data' data messages. Dialysate fill data are captured + for reference. + + @param message: published dialysate fill' data message + @return: None + """ + self.avg_acid = struct.unpack(' int: + """ + Constructs and sends the used acid volume override command + + @param volume: float - desired used acid volume to override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + reset_byte_array = integer_to_bytearray(reset) + volume_byte_array = float_to_bytearray(volume) + + payload = reset_byte_array + volume_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_USED_ACID_VOLUME_ML_OVERRIDE.value, + payload=payload) + + self.logger.debug("override the used acid volume") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_used_bicarb_volume_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the used bicarb volume override command + + @param volume: float - desired used acid volume to override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + reset_byte_array = integer_to_bytearray(reset) + volume_byte_array = float_to_bytearray(volume) + + payload = reset_byte_array + volume_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_USED_BICARB_VOLUME_ML_OVERRIDE.value, + payload=payload) + + self.logger.debug("override the used bicarb volume") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_fill_mode_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fill mode data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_FILL_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override used concentrate data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_fill_integrated_volume_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the integrated fill override command. + Constraints: + Must be logged into DG. + + @param volume: (float) volume to override with ( in mL ) + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DIALYSATE_FILL_INTEGRATED_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override total volume for dialysate fill.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "back to normal" + else: + str_res = str(volume) + self.logger.debug( + "Total Fill has been overridden " + str_res + " mL " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_mode_fill_cal_check_state(self, cal_check_state: int) -> int: + """ + Constructs and sends a message to set the mode fill calibration check status. + Constraints: + Must be logged into DG. + + @param cal_check_state: the state (0 = prime, 1 = bicarb check, 2 = acid check) + @return: 1 if successful, zero otherwise + """ + state_string = "None" + payload = integer_to_bytearray(cal_check_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RUN_MODE_FILL_FOR_CAL_CHECK.value, + payload=payload) + if cal_check_state == 0: + state_string = "Prime" + elif cal_check_state == 1: + state_string = "Bicarb Check" + elif cal_check_state == 2: + state_string = "Acid Check" + + self.logger.debug("Setting the Mode Fill calibration check to {}.".format(state_string)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Setting the Mode Fill calibration check to {}.".format(state_string)) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/dialysate_generator.py =================================================================== diff -u --- leahi-dialin/dg/dialysate_generator.py (revision 0) +++ leahi-dialin/dg/dialysate_generator.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,668 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dialysate_generator.py +# +# @author (last) Michael Garthwaite +# @date (last) 18-Oct-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +from .accelerometer import DGAccelerometer +from .alarms import DGAlarms +from .calibration_record import DGCalibrationNVRecord +from .chemical_disinfect import ChemicalDisinfect +from .chemical_disinfect_flush import ChemicalDisinfectFlushMode +from .concentrate_pumps import ConcentratePumps +from .conductivity_sensors import ConductivitySensors +from .constants import RESET, NO_RESET +from .cpld import Cpld +from .drain_pump import DGDrainPump +from .fans import Fans +from .dialysate_fill import DialysateFill +from .flow_sensors import FlowSensors +from .fluid_leak import DGFluidLeak +from .flush import FlushMode +from .gen_idle import GenIdle +from .hd_proxy import DGHDProxy +from .heat_disinfect import HeatDisinfect +from .heat_disinfect_active_cool import HeatDisinfectActiveCool +from .heaters import Heaters +from .load_cells import DGLoadCells +from .pressures import DGPressures +from .reservoirs import DGReservoirs +from .ro_pump import DGROPump +from .rtc import DGRTC +from .samplewater import DGSampleWater +from .scheduled_runs_record import DGScheduledRunsNVRecord +from .service_record import DGServiceNVRecord +from .switches import DGSwitches +from .system_record import DGSystemNVRecord +from .temperatures import TemperatureSensors +from .thermistors import Thermistors +from .uv_reactors import UVReactors +from .valves import DGValves +from .voltages import DGVoltages +from .events import DGEvents +from .sw_configs import DGSoftwareConfigs +from .usage_info_record import DGUsageNVRecord +from .dg_test_configs import DGTestConfig +from .ro_permeate_sample import ROPermeateSample +from .drain import DGDrain +from .watchdog import DGWatchdog +from ..common.msg_defs import MsgIds, MsgFieldPositions +from enum import unique +from .constants import NO_RESET +from ..protocols.CAN import DenaliCanMessenger, DenaliMessage, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, publish, LogManager, DialinEnum +from ..utils.conversions import integer_to_bytearray, unsigned_short_to_bytearray + +@unique +class DGOperationModes(DialinEnum): + # DG operation modes + DG_OP_MODE_FAULT = 0 + DG_OP_MODE_SERVICE = 1 + DG_OP_MODE_INIT_POST = 2 + DG_OP_MODE_STANDBY = 3 + DG_OP_MODE_STANDBY_SOLO = 4 + DG_OP_MODE_GEN_IDLE = 5 + DG_OP_MODE_FILL = 6 + DG_OP_MODE_DRAIN = 7 + DG_OP_MODE_FLUSH = 8 + DG_OP_MODE_DISINFECT = 9 + DG_OP_MODE_CHEMICAL_DISINFECT = 10 + + +class DG(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin object API. It provides the basic interface to communicate with + the DG firmware. + """ + + # HD login password + DG_LOGIN_PASSWORD = '123' + + SW_COMPATIBILITY_REV = 1 + + # DG version message field positions + START_POS_MAJOR = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MAJOR = START_POS_MAJOR + 1 + START_POS_MINOR = END_POS_MAJOR + END_POS_MINOR = START_POS_MINOR + 1 + START_POS_MICRO = END_POS_MINOR + END_POS_MICRO = START_POS_MICRO + 1 + START_POS_BUILD = END_POS_MICRO + END_POS_BUILD = START_POS_BUILD + 2 + + + + # FPGA + START_POS_FPGA_ID = END_POS_BUILD + END_POS_FPGA_ID = START_POS_FPGA_ID + 1 + START_POS_FPGA_MAJOR = END_POS_FPGA_ID + END_POS_FPGA_MAJOR = START_POS_FPGA_MAJOR + 1 + START_POS_FPGA_MINOR = END_POS_FPGA_MAJOR + END_POS_FPGA_MINOR = START_POS_FPGA_MINOR + 1 + START_POS_FPGA_LAB = END_POS_FPGA_MINOR + END_POS_FPGA_LAB = START_POS_FPGA_LAB + 1 + START_POS_COMPATIBILITY_REV = END_POS_FPGA_LAB + END_POS_COMPATIBILITY_REV = START_POS_COMPATIBILITY_REV + 4 + + # DG sub_modes + DG_POST_STATE_START = 0 # Start initialize & POST mode state + DG_POST_STATE_FPGA = 1 # FPGA POST test state + DG_POST_STATE_WATCHDOG = 2 # Watchdog POST test state + DG_POST_STATE_TEMPERATURE_SENSORS = 3 # Temperature Sensors POST state + DG_POST_STATE_HEATERS = 4 # Heaters POST state + DG_POST_STATE_COMPLETED = 5 # POST completed successfully state + DG_POST_STATE_FAILED = 6 # POST failed state + NUM_OF_DG_POST_STATES = 7 # Number of initialize & POST mode states + + def __init__(self, can_interface="can0", log_level=None): + """ + Initializes the DG object + + + For example: + dg_object = DG(can_interface='can0') or + dg_object = DG(can_interface="can0", log_level="DEBUG") + + Possible log levels: + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + @param can_interface: string with can bus name, e.g. "can0" + """ + super().__init__() + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + + # Create listener + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger) + self.can_interface.start() + self.callback_id = None + + # register handler for HD operation mode broadcast messages + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_op_mode_sync) + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_sync_broadcast_ch_id, + MsgIds.MSG_ID_DG_VERSION_REPONSE.value, + self._handler_dg_version) + + self.callback_id = self.can_interface.register_transmitting_interval_message(INTERVAL_10s, + self._send_dg_checkin_message) + + # initialize variables that will be populated by DG version response + self.dg_version = None + self.fpga_version = None + # create properties + self.dg_operation_mode = 0 # self.DG_OP_MODE_INIT_POST + self.dg_operation_sub_mode = 0 + self.dg_logged_in = False + self.dg_set_logged_in_status(False) + self.dg_no_transmit_msg_list = [0,0,0,0,0,0,0,0] + self.dg_op_mode_timestamp = 0.0 + self.dg_version_response_timestamp = 0.0 + + # Create command groups + self.accel = DGAccelerometer(self.can_interface, self.logger) + self.alarms = DGAlarms(self.can_interface, self.logger) + self.calibration_record = DGCalibrationNVRecord(self.can_interface, self.logger) + self.chemical_disinfect = ChemicalDisinfect(self.can_interface, self.logger) + self.chemical_disinfect_flush = ChemicalDisinfectFlushMode(self.can_interface, self.logger) + self.concentrate_pumps = ConcentratePumps(self.can_interface, self.logger) + self.conductivity_sensors = ConductivitySensors(self.can_interface, self.logger) + self.cpld = Cpld(self.can_interface, self.logger) + self.dialysate_fill = DialysateFill(self.can_interface, self.logger) + self.drain_pump = DGDrainPump(self.can_interface, self.logger) + self.fans = Fans(self.can_interface, self.logger) + self.flow_sensors = FlowSensors(self.can_interface, self.logger) + self.fluid_leak = DGFluidLeak(self.can_interface, self.logger) + self.flush = FlushMode(self.can_interface, self.logger) + self.gen_idle = GenIdle(self.can_interface, self.logger) + self.hd_proxy = DGHDProxy(self.can_interface, self.logger) + self.heat_disinfect = HeatDisinfect(self.can_interface, self.logger) + self.heat_disinfect_active_cool = HeatDisinfectActiveCool(self.can_interface, self.logger) + self.heaters = Heaters(self.can_interface, self.logger) + self.load_cells = DGLoadCells(self.can_interface, self.logger) + self.pressures = DGPressures(self.can_interface, self.logger) + self.reservoirs = DGReservoirs(self.can_interface, self.logger) + self.ro_pump = DGROPump(self.can_interface, self.logger) + self.rtc = DGRTC(self.can_interface, self.logger) + self.samplewater = DGSampleWater(self.can_interface, self.logger) + self.scheduled_runs_record = DGScheduledRunsNVRecord(self.can_interface, self.logger) + self.service_record = DGServiceNVRecord(self.can_interface, self.logger) + self.switches = DGSwitches(self.can_interface, self.logger) + self.system_record = DGSystemNVRecord(self.can_interface, self.logger) + self.temperatures = TemperatureSensors(self.can_interface, self.logger) + self.thermistors = Thermistors(self.can_interface, self.logger) + self.uv_reactors = UVReactors(self.can_interface, self.logger) + self.valves = DGValves(self.can_interface, self.logger) + self.voltages = DGVoltages(self.can_interface, self.logger) + self.events = DGEvents(self.can_interface, self.logger) + self.sw_configs = DGSoftwareConfigs(self.can_interface, self.logger) + self.usage_record = DGUsageNVRecord(self.can_interface, self.logger) + self.test_configs = DGTestConfig(self.can_interface, self.logger) + self.ro_permeate_sample = ROPermeateSample(self.can_interface, self.logger) + self.drain = DGDrain(self.can_interface, self.logger) + self.watchdog = DGWatchdog(self.can_interface, self.logger) + + def __del__(self): + self.can_interface.transmit_interval_dictionary[self.callback_id].stop() + + def get_version(self): + """ + Gets the DG version. Assumes DG version has already been requested. + + @return: The hd version string + """ + return self.dg_version + + def get_fpga_version(self): + """ + Gets the fpga version from the DG + + @return: The FPGA version + """ + return self.fpga_version + + def get_operation_mode(self): + """ + Gets the operation mode + + @return: The operation mode + """ + return self.dg_operation_mode + + def get_operation_sub_mode(self): + """ + Gets the operation sub mode + + @return: The operation sub mode + """ + return self.dg_operation_sub_mode + + def get_dg_logged_in(self): + """ + Gets the logged in status of the DG + + @return: True if DG is logged in, False if not + """ + return self.dg_logged_in + + def get_dg_blocked_msg_list(self): + """ + Gets the current list of message IDs that HD will prevent transmission of. + + @return: List of message IDs blocked from transmission + """ + return self.dg_no_transmit_msg_list + + @publish(["dg_logged_in"]) + def dg_set_logged_in_status(self, logged_in: bool = False): + """ + Callback for DG logged in status change. + @param logged_in boolean logged in status for DG + @return: none + """ + self.dg_logged_in = logged_in + + @publish(["dg_version_response_timestamp","dg_version", "fpga_version"]) + def _handler_dg_version(self, message,timestamp): + """ + Handler for response from DG regarding its version. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U08 Major \n + U08 Minor \n + U08 Micro \n + U16 Build \n + + @return: None if unsuccessful, the version string if unpacked successfully + """ + major = struct.unpack('B', bytearray(message['message'][self.START_POS_MAJOR:self.END_POS_MAJOR])) + minor = struct.unpack('B', bytearray(message['message'][self.START_POS_MINOR:self.END_POS_MINOR])) + micro = struct.unpack('B', bytearray(message['message'][self.START_POS_MICRO:self.END_POS_MICRO])) + build = struct.unpack('H', bytearray(message['message'][self.START_POS_BUILD:self.END_POS_BUILD])) + + fpga_id = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_ID:self.END_POS_FPGA_ID])) + fpga_major = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MAJOR:self.END_POS_FPGA_MAJOR])) + fpga_minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MINOR:self.END_POS_FPGA_MINOR])) + fpga_lab = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_LAB:self.END_POS_FPGA_LAB])) + compatibility = struct.unpack('I', bytearray( + message['message'][self.START_POS_COMPATIBILITY_REV:self.END_POS_COMPATIBILITY_REV])) + + if all([len(each) > 0 for each in [major, minor, micro, build]]): + self.dg_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}-{compatibility[0]}" + self.logger.debug(f"DG VERSION: {self.dg_version}") + + if all([len(each) > 0 for each in [fpga_major, fpga_minor, fpga_id, fpga_lab]]): + self.fpga_version = f"ID: {fpga_id[0]} v{fpga_major[0]}.{fpga_minor[0]}.{fpga_lab[0]}" + self.logger.debug(f"DG FPGA VERSION: {self.fpga_version}") + self.dg_version_response_timestamp = timestamp + + @publish(["dg_op_mode_timestamp","dg_operation_mode", "dg_operation_sub_mode"]) + def _handler_dg_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published DG operation mode messages. Current DG operation mode + is captured for reference. + + @param message: published DG operation mode broadcast message + @return: None + """ + + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.dg_operation_mode = mode[0] + self.dg_operation_sub_mode = smode[0] + self.dg_op_mode_timestamp = timestamp + + def cmd_log_in_to_dg(self, resend: bool = False) -> int: + """ + Constructs and sends a login command via CAN bus. Login required before \n + other commands can be sent to the DG. + + @param resend: (bool) if False (default), try to login once. Otherwise, tries to login indefinitely + @return: 1 if logged in, 0 if log in failed + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_TESTER_LOGIN_REQUEST.value, + payload=list(map(int, map(ord, self.DG_LOGIN_PASSWORD)))) + + self.logger.info("Logging in to the DG...") + + # Send message + received_message = self.can_interface.send(message, resend=resend) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.info("Successfully logged in to the DG.") + self.dg_set_logged_in_status(True) + self._send_dg_checkin_message() # Timer starts interval first + self.can_interface.transmit_interval_dictionary[self.callback_id].start() + else: + self.logger.error("Log In Failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_ui_request_dg_version(self) -> None: + """ + Constructs and sends the ui request for version message + + """ + + major = unsigned_byte_to_bytearray(0) + minor = unsigned_byte_to_bytearray(0) + micro = unsigned_byte_to_bytearray(0) + build = short_to_bytearray(0) + compatibility = integer_to_bytearray(self.SW_COMPATIBILITY_REV) + + payload = major + minor + micro + build + compatibility + + message = DenaliMessage.build_message(channel_id=DenaliChannels.ui_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_FW_VERSIONS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending Dialin request for version to DG") + + self.can_interface.send(message, 0) + + def cmd_dg_set_operation_mode(self, new_mode: int = 0) -> int: + """ + Constructs and sends a set operation mode request command via CAN bus. + Constraints: + Must be logged into DG. + Transition from current to requested op mode must be legal. + For transitioning to POST again, you can only be in Standby Mode or Solo Mode + + @param new_mode: ID of operation mode to transition to (see DGOpModes enum for options) + + @return: 1 if successful, zero otherwise + + """ + + payload = integer_to_bytearray(new_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_OP_MODE_REQUEST.value, + payload=payload) + + self.logger.debug("Requesting DG mode change to " + str(new_mode)) + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Success: Mode change accepted") + else: + self.logger.debug("Failure: Mode change rejected.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("DG mode change request Timeout!!!!") + return False + + def cmd_dg_safety_shutdown_override(self, active: bool = True, reset: int = NO_RESET) -> int: + """ + Constructs and sends an DG safety shutdown override command via CAN bus. + + @param active: boolean - True to activate safety shutdown, False to deactivate + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if active: + sft = 1 + else: + sft = 0 + rst = integer_to_bytearray(reset) + saf = integer_to_bytearray(sft) + payload = rst + saf + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SAFETY_SHUTDOWN_OVERRIDE.value, + payload=payload) + + self.logger.debug("overriding DG safety shutdown") + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Safety shutdown signal overridden") + else: + self.logger.debug("Safety shutdown signal override failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dg_software_reset_request(self) -> None: + """ + Constructs and sends an DG software reset request via CAN bus. + Constraints: + Must be logged into DG. + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SOFTWARE_RESET_REQUEST.value) + + self.logger.debug("requesting DG software reset") + + # Send message + self.can_interface.send(message, 0) + self.logger.debug("Sent request to DG to reset...") + self.dg_set_logged_in_status(False) + + def cmd_dg_op_mode_broadcast_interval_override(self, ms:int=250, reset:int=NO_RESET) -> int: + """ + Constructs and sends the DG op mode broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_OP_MODE_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG op. mode broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("DG op. mode broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_block_dg_message_transmissions(self, msg1: int = 0, msg2: int = 0, msg3: int = 0, msg4: int = 0, + msg5: int = 0, msg6: int = 0, msg7: int = 0, msg8: int = 0): + """ + Constructs and sends a block dg message transmission request + Constraints: + Must be logged into DG. + + @param msg1: integer - 1st message ID to block DG from transmitting + @param msg2: integer - 2nd message ID to block DG from transmitting + @param msg3: integer - 3rd message ID to block DG from transmitting + @param msg4: integer - 4th message ID to block DG from transmitting + @param msg5: integer - 5th message ID to block DG from transmitting + @param msg6: integer - 6th message ID to block DG from transmitting + @param msg7: integer - 7th message ID to block DG from transmitting + @param msg8: integer - 8th message ID to block DG from transmitting + @return: 1 if successful, zero otherwise + """ + # Save blocked message(s) list + self.dg_no_transmit_msg_list[0] = msg1 + self.dg_no_transmit_msg_list[1] = msg2 + self.dg_no_transmit_msg_list[2] = msg3 + self.dg_no_transmit_msg_list[3] = msg4 + self.dg_no_transmit_msg_list[4] = msg5 + self.dg_no_transmit_msg_list[5] = msg6 + self.dg_no_transmit_msg_list[6] = msg7 + self.dg_no_transmit_msg_list[7] = msg8 + # Build message payload + m1 = unsigned_short_to_bytearray(msg1) + m2 = unsigned_short_to_bytearray(msg2) + m3 = unsigned_short_to_bytearray(msg3) + m4 = unsigned_short_to_bytearray(msg4) + m5 = unsigned_short_to_bytearray(msg5) + m6 = unsigned_short_to_bytearray(msg6) + m7 = unsigned_short_to_bytearray(msg7) + m8 = unsigned_short_to_bytearray(msg8) + payload = m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_BLOCK_MESSAGE_TRANSMISSION.value, + payload=payload) + + self.logger.debug("request DG block transmission of message(s)") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Given messages blocked." + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def _send_dg_checkin_message(self) -> int: + """ + Constructs and sends an DG Dialin check in message to the DG. + + @return: none + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DIALIN_CHECK_IN.value) + self.can_interface.send(message) + return True + + def cmd_dg_ram_status_override(self, ram_reg: int = 0, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the RAM status override command + Constraints: + Must be logged into DG. + + RAM Status Bits: + SERR 0x00000001 Bit 0 - Single-bit error in TCRAM Module Error Status Register + ADDR_DEC_FAIL 0x00000004 Bit 2 - Address decode failed in TCRAM Module Error Status Register + ADDR_COMP_LOGIC_FAIL 0x00000010 Bit 4 - Address decode logic element failed in TCRAM Module Error Status Register + DERR 0x00000020 Bit 5 - Multiple bit error in TCRAM Module Error Status Register + RADDR_PAR_FAIL 0x00000100 Bit 8 - Read Address Parity Failure in TCRAM Module Error Status Register + WADDR_PAR_FAIL 0x00000200 Bit 9 - Write Address Parity Failure in TCRAM Module Error Status Register + + @param ram_reg: integer - the RAM regsiter. 0 or 1 + @param status: integer - bitmap of the status values listed aboves + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + reg = integer_to_bytearray(ram_reg) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + reg + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RAM_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug(f"Overriding RAM Status Register {reg} to {str(sts)}") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dg_pending_ack_override(self, pending_ack_channel: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends an DG pending ack override command via CAN bus. + Constraints: + Must be logged into DG. + + Will prevent receiving ACK messages from being registered. + Used to trigger ALARM_ID_DG_CAN_MESSAGE_NOT_ACKED + after retries are sent. + + Use 1 for HD CAN messages Alarm. + + + @param pending_ack_channel: integer - 1 for HD Channel ACK + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + rst = integer_to_bytearray(reset) + pack = integer_to_bytearray(pending_ack_channel) + payload = rst + pack + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CAN_RECEIVE_ACK_MESSAGE_OVERRIDE.value, + payload=payload) + + self.logger.debug("overriding DG Pending ACK") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/dg/drain.py =================================================================== diff -u --- leahi-dialin/dg/drain.py (revision 0) +++ leahi-dialin/dg/drain.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,134 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file drain.py +# +# @author (last) Micahel Garthwaite +# @date (last) 08-Aug-2023 +# @author (original) Micahel Garthwaite +# @date (original) 08-Aug-2023 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class DGDrain(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for drain op mode related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + Initialize CAN interface and logger + @param can_interface: Denali CAN Messenger object + @param: logger: (Logger) object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self.bad_fill_state = 0 + self.bad_fill_signal = 0 + self.drain_mode_state = 0 + self.dg_drain_mode_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_DRAIN_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_drain_sync) + + def get_bad_fill_sub_state(self): + """ + Gets current bad fill sub-state + + @param: none + @return: The current bad fill sub-state + """ + return self.bad_fill_state + + def get_drain_bad_fill_signal(self): + """ + Gets current gen idle sub-state + + @param: none + @return: The current gen idle sub state + """ + return self.bad_fill_signal + + def get_drain_mode_state(self): + """ + Gets current drain mode state + + @param: none + @return: The current drain mode state + """ + return self.drain_mode_state + + + @publish(["dg_drain_mode_timestamp","drain_mode_state", "bad_fill_signal", "bad_fill_state"]) + def _handler_drain_sync(self, message, timestamp=0.0): + """ + Handles published gen idle sub-states message. + + @param message: published gen idle sub-states message + @return: none + """ + + self.drain_mode_state = struct.unpack(' int: + """ + Constructs and sends the fill mode data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_MODE_BROADCAST_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override drain mode sub-states data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/drain_pump.py =================================================================== diff -u --- leahi-dialin/dg/drain_pump.py (revision 0) +++ leahi-dialin/dg/drain_pump.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,405 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file drain_pump.py +# +# @author (last) Dara Navaei +# @date (last) 27-Apr-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DrainPumpStates(DialinEnum): + DRAIN_PUMP_OFF_STATE = 0 # Drain pump off state + DRAIN_PUMP_CONTROL_TO_TARGET_STATE = 1 # Drain pump control to target state + DRAIN_PUMP_OPEN_LOOP_STATE = 2 # Drain pump open loop state + + +@unique +class DrainPumpRPMFeedBackSensors(DialinEnum): + DRAIN_PUMP_HALL_SNSR_FB = 0 # Drain pump hall sensor RPM feedback sensor + DRAIN_PUMP_MAXON_SNSR_FB = 1 # Drain pump Maxon sensor RPM feedback sensor + + +class DGDrainPump(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for drain pump related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGDrainPump constructor + + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DRAIN_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_drain_pump_sync) + + self.target_drain_pump_rpm = 0 + self.dac_value = 0 + self.drain_pump_state = 0 + self.current_drain_pump_rpm = {DrainPumpRPMFeedBackSensors.DRAIN_PUMP_HALL_SNSR_FB.name: 0, + DrainPumpRPMFeedBackSensors.DRAIN_PUMP_MAXON_SNSR_FB.name: 0} + self.target_drain_pump_outlet_flow_lpm = 0.0 + self.drain_pump_current_A = 0.0 + self.drain_pump_direction = 0 + self.dg_drain_pump_timestamp = 0.0 + + def get_target_drain_pump_rpm(self): + """ + Gets the target drain pump RPM + + @return: The target drain pump RPM + """ + return self.target_drain_pump_rpm + + def get_dac_value(self): + """ + Gets the dac value + + @return: The dac value (int) + """ + return self.dac_value + + def get_drain_pump_state(self): + """ + Gets the drain pump state + + @return: Drain pump state + """ + return self.drain_pump_state + + def get_drain_pump_current_rpm(self, sensor: int): + """ + Gets the drain pump current RPM + + @param sensor (int) the sensor to read its data + @return: Drain pump current RPM + """ + return self.current_drain_pump_rpm[DrainPumpRPMFeedBackSensors(sensor).name] + + @publish(["dg_drain_pump_timestamp", "target_drain_pump_rpm", "dac_value", "drain_pump_state", + "drain_pump_current_A", "drain_pump_direction", "target_drain_pump_outlet_flow_lpm", + "current_drain_pump_rpm"]) + def _handler_drain_pump_sync(self, message, timestamp=0.0): + """ + Handles published drain pump data messages. Drain pump data are captured + for reference. + + @param message: published drain pump data message + @return: none + """ + self.target_drain_pump_rpm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dac_value = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.drain_pump_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.current_drain_pump_rpm[DrainPumpRPMFeedBackSensors.DRAIN_PUMP_HALL_SNSR_FB.name] = \ + struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.target_drain_pump_outlet_flow_lpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.drain_pump_current_A = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.drain_pump_direction = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.current_drain_pump_rpm[DrainPumpRPMFeedBackSensors.DRAIN_PUMP_MAXON_SNSR_FB.name] = \ + struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.dg_drain_pump_timestamp = timestamp + + def cmd_drain_pump_set_outlet_target_flow_lpm(self, flow: float) -> int: + """ + Constructs and sends the drain pump target outlet flow in L/min + Constraints: + Must be logged into DG. + + @param flow: (float) target outlet flow in L/min + @return: 1 if successful, zero otherwise + """ + + flw = float_to_bytearray(flow) + payload = flw + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_TARGET_OUTLET_FLOW.value, + payload=payload) + + self.logger.debug("Setting drain pump target flow") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Drain pump outlet flow set to " + str(flow) + " L/min" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_set_rpm(self, rpm: int) -> int: + """ + Constructs and sends the drain pump speed set command + Constraints: + Must be logged into DG. + + @param rpm: (int) speed set point (in RPM) to override with + @return: 1 if successful, zero otherwise + """ + + spd = integer_to_bytearray(rpm) + payload = spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_SET_RPM.value, + payload=payload) + + self.logger.debug("Setting the drain pump RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Drain pump RPM set to " + str(rpm) + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_measured_rpm_override(self, sensor: int, rpm: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump measured RPM override command. + Constraints: + Must be logged into DG. + Given RPM must be within 300 <= RPM <= 3000 + + @param sensor: (int) the sensor (hall sensor or maxon sensor) + @param rpm: (int) rpm to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + r = integer_to_bytearray(rpm) + index = integer_to_bytearray(sensor) + payload = rst + r + index + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_PUMP_MEASURED_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Setting sensor {} to {} C".format(sensor, rpm)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(rpm) + self.logger.debug( + "Drain pump measured RPM overridden to " + str_res + " RPM " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_measured_current_override(self, current: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump measured current override command. + Constraints: + Must be logged into DG. + Given measured current must be within 0 and 2.2 Amps. + + @param current: (float) measured current to override with in Amps + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_PUMP_CURRENT_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override drain pump measured current.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(cur) + self.logger.debug( + "Drain pump measured current overridden to " + str_res + " A " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump data publication override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override drain pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Drain pump data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_measured_current_amps_override(self, current: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump measured current override command. + Constraints: + Must be logged into DG. + + @param current: (float) current to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + r = float_to_bytearray(current) + payload = rst + r + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_PUMP_CURRENT_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override drain pump measured current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(current) + self.logger.debug( + "Drain pump measured current overridden to " + str_res + " A " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain_pump_measured_direction_override(self, direction: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump measured direction override command. + Constraints: + Must be logged into DG. + 1 will be forward and 0 will be backwards + + @param direction: (int) direction to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + r = integer_to_bytearray(direction) + payload = rst + r + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_PUMP_DIRECTION_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override drain pump measured direction") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(direction) + self.logger.debug( + "Drain pump measured direction overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/events.py =================================================================== diff -u --- leahi-dialin/dg/events.py (revision 0) +++ leahi-dialin/dg/events.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,306 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file events.py +# +# @author (last) Micahel Garthwaite +# @date (last) 29-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 12-Oct-2021 +# +############################################################################ + +import struct +from logging import Logger +from ..common import * +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from datetime import datetime +from time import time + + +class DGEvents(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for events related commands. + """ + UNKNOWN_STATE = "UNKNOWN_PREVIOUS_STATE" + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_events_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_ui_ch_id + msg_id = MsgIds.MSG_ID_DG_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_events_sync) + + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_op_mode_sync) + + + # Define the dictionaries + self._dg_event_dictionary = dict() + self._dg_event_data_type = dict() + + # Dictionary of the mode as key and the sub mode states enum class as the value + self._dg_op_mode_2_sub_mode = {DGOpModes.DG_MODE_FAUL.name: DGFaultStates, + DGOpModes.DG_MODE_INIT.name: DGInitStates, + DGOpModes.DG_MODE_STAN.name: DGStandByModeStates, + DGOpModes.DG_MODE_GENE.name: DGGenIdleModeStates, + DGOpModes.DG_MODE_FILL.name: DGFillModeStates, + DGOpModes.DG_MODE_DRAI.name: DGDrainModeStates, + DGOpModes.DG_MODE_FLUS.name: DGFlushStates, + DGOpModes.DG_MODE_HEAT.name: DGHeatDisinfectStates, + DGOpModes.DG_MODE_HCOL.name: DGHeatDisinfectActiveCoolStates, + DGOpModes.DG_MODE_CHEM.name: DGChemicalDisinfectStates, + DGOpModes.DG_MODE_CHFL.name: DGChemDisinfectFlushStates, + DGOpModes.DG_MODE_ROPS.name: DGROPermeateSampleStates} + + # Loop through the list of the DG events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in DGEventList: + self._dg_event_dictionary[DGEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in DGEventDataType: + event_data_type = DGEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._dg_event_data_type[event_data_type] = struct_unpack_type + + def get_dg_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested DG event + + @param event_id the ID of the DG event types (i.e. DG_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested DG event number + """ + list_length = len(self._dg_event_dictionary[DGEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._dg_event_dictionary[DGEventList(event_id).name][list_length - 1] + else: + event = self._dg_event_dictionary[DGEventList(event_id).name][list_length - event_number - 1] + + return event + + def clear_dg_event_list(self): + """ + Clears the DG event list + + @returns none + """ + for key in self._dg_event_dictionary: + self._dg_event_dictionary[key].clear() + + def get_dg_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain DG event ID + + @param event_id the ID of the DG event types (i.e. DG_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested DG event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._dg_event_dictionary[DGEventList(event_id).name]) <= number_of_events: + list_of_events = self._dg_event_dictionary[DGEventList(event_id).name] + else: + # Get the all the events + complete_list = self._dg_event_dictionary[DGEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._dg_event_dictionary[DGEventList(event_id).name] + + return list_of_events + + @publish(["dg_events_timestamp", '_dg_event_dictionary']) + def _handler_events_sync(self, message, timestamp=0.0): + """ + Handles published events message + + @param message: published DG events data message + @returns none + """ + event_data_1 = 0 + event_data_2 = 0 + op_mode = 0 + sub_mode = 0 + sub_state = 0 + current_sub_tuple = [] + + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if event_id == DGEventList.DG_EVENT_OPERATION_STATUS.value: + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + struct_data_type = self._dg_event_data_type[HDEventDataType(event_data_type_1).name] + op_mode = struct.unpack(' current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + if previous_op_mode != DGEvents.UNKNOWN_STATE: + previous_sub_mode_enum_class = self._dg_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + # Unknown previous state. Display value instead of name. + else: + event_data_1 = str(event_data_1) + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._dg_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + event_tuple = (datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + elif event_state_name == DGEventList.DG_EVENT_OPERATION_STATUS.name: + event_tuple = (time(), op_mode, sub_mode, sub_state) + + # Update event dictionary + self._dg_event_dictionary[event_state_name].append(event_tuple) + self.dg_events_timestamp = timestamp + + @publish(["dg_event_op_mode_timestamp", "dg_event_op_mode", "dg_event_sub_mode"]) + def _handler_dg_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published DG operation mode messages. Current DG operation mode + is captured for reference. + + @param message: published DG operation mode broadcast message + @return: None + """ + + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.dg_event_op_mode = mode[0] + self.dg_event_sub_mode = smode[0] + self.dg_event_op_mode_timestamp = timestamp Index: leahi-dialin/dg/fans.py =================================================================== diff -u --- leahi-dialin/dg/fans.py (revision 0) +++ leahi-dialin/dg/fans.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,314 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file fans.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DGFansNames(DialinEnum): + FAN_INLET_1 = 0 + FAN_INLET_2 = 1 + FAN_INLET_3 = 2 + FAN_OUTLET_1 = 3 + FAN_OUTLET_2 = 4 + FAN_OUTLET_3 = 5 + + +class Fans(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for fans related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FANS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_fans_sync) + + # Publish variables + self.dg_fans_duty_cycle = 0.0 + self.dg_fans_target_rpm = 0.0 + self.inlet_1_rpm = 0.0 + self.inlet_2_rpm = 0.0 + self.inlet_3_rpm = 0.0 + self.outlet_1_rpm = 0.0 + self.outlet_2_rpm = 0.0 + self.outlet_3_rpm = 0.0 + self.rpm_alarm_time = 0 + self.dg_fans_timestamp = 0.0 + + def get_fans_target_duty_cycle(self): + """ + Gets the fans target duty cycle + + @return: Fans target duty cycle + """ + return self.dg_fans_duty_cycle + + def get_fan_inlet_1_rpm(self): + """ + Gets the inlet 1 fan RPM + + @return: Fan inlet 1 RPM + """ + return self.inlet_1_rpm + + def get_fan_inlet_2_rpm(self): + """ + Gets the inlet 2 fan RPM + + @return: Fan inlet 2 RPM + """ + return self.inlet_2_rpm + + def get_fan_inlet_3_rpm(self): + """ + Gets the inlet 3 fan RPM + + @return: Fan inlet 3 RPM + """ + return self.inlet_3_rpm + + def get_fan_outlet_1_rpm(self): + """ + Gets the outlet 1 fan RPM + + @return: Fan outlet 1 RPM + """ + return self.outlet_1_rpm + + def get_fan_outlet_2_rpm(self): + """ + Gets the outlet 2 fan RPM + + @return: Fan outlet 2 RPM + """ + return self.outlet_2_rpm + + def get_fan_outlet_3_rpm(self): + """ + Gets the outlet 3 fan RPM + + @return: Fan outlet 3 RPM + """ + return self.outlet_3_rpm + + def get_dg_fans_target_rpm(self): + """ + Gets the fans target RPM + + @return: Fans target RPM + """ + return self.dg_fans_target_rpm + + def get_fans_time_left_to_rpm_alarm(self): + """ + Gets the fans time left to RPM alarm + + @return: Fans time left to RPM alarm + """ + return self.rpm_alarm_time + + @publish(["dg_fans_timestamp",'dg_fans_duty_cycle', 'dg_fans_target_rpm', 'inlet_1_rpm', 'inlet_2_rpm', 'inlet_3_rpm', 'outlet_1_rpm', + 'outlet_2_rpm', 'outlet_3_rpm', 'rpm_alarm_time']) + def _handler_fans_sync(self, message, timestamp=0.0): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + self.dg_fans_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dg_fans_target_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.inlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.inlet_2_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.inlet_3_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.outlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.outlet_2_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.outlet_3_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.rpm_alarm_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.dg_fans_timestamp = timestamp + + def cmd_fans_rpm_override(self, fan: int, rpm: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the dg fan RPM override command + Constraints: + Must be logged into DG. + + @param fan: (int) fan ID that is status is overridden + @param rpm: (int) RPM that the fan will be overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + f = integer_to_bytearray(fan) + r = float_to_bytearray(rpm) + payload = reset_value + r + f + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fan RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Fan " + str(DGFansNames(fan).name) + " to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_rpm_alarm_start_time_offset(self, time: int) -> int: + """ + Constructs and sends the DG fan RPM alarm start time offset command + Constraints: + Must be logged into DG. + + @param time: (int) time offset in milliseconds + @return: 1 if successful, zero otherwise + """ + payload = integer_to_bytearray(time) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_FANS_RPM_ALARM_START_TIME_OFFSET.value, + payload=payload) + + self.logger.debug("Override fan RPM alarm start time offset") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("RPM alarm start time offset set to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fans data publish interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Fans data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_duty_cycle_override(self, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG fans duty cycle override command + Constraints: + Must be logged into DG. + + @param duty_cycle: (float) the duty cycle that the fans are overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + dc = float_to_bytearray(duty_cycle / 100.0) + payload = reset_value + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans duty cycle") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set fans duty cycle to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/flow_sensors.py =================================================================== diff -u --- leahi-dialin/dg/flow_sensors.py (revision 0) +++ leahi-dialin/dg/flow_sensors.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,197 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file flow_sensors.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 19-Sep-2022 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.base import DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class FlowSensorsNames(DialinEnum): + RO_FLOW_SENSOR = 0 + DIALYSATE_FLOW_SENSOR = 1 + NUM_OF_FLOW_SENSOR = 2 + + +class FlowSensors(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for dialysate flow sensor related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + Initialize CAN interface and logger + @param can_interface: Denali CAN Messenger object + @param: logger: (Logger) object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + # The flow rate of the sensors are L/min + + self.measured_ro_flow_LPM = 0.0 + self.measured_dialysate_flow_LPM = 0.0 + self.measured_ro_flow_with_cp_LPM = 0.0 + self.measured_raw_ro_flow_LPM = 0.0 + self.measured_raw_dialysate_flow_LPM = 0.0 + self.dg_flow_sensors_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_DG_FLOW_SENSORS_DATA.value, + self._handler_flow_sensors_sync) + + def get_measured_dialysate_flow_rate(self): + """ + Gets the measured flow value from DG's dialysate flow rate + @return: The dialysate flow rate L/min + """ + return self.measured_dialysate_flow_LPM + + def get_measured_ro_flow(self): + """ + Gets the measured flow value from DG's ro pump + @return: The RO flow rate L/min + """ + return self.measured_ro_flow_LPM + + def get_measured_ro_flow_with_cp(self): + """ + Gets the measured flow value from DG's ro pump with concentrate pumps + @return: The RO flow rate with concentrate pumps flow rate L/min + """ + return self.measured_ro_flow_with_cp_LPM + + def get_measured_raw_ro_flow(self): + """ + Gets the measured raw (uncalibrated) flow value from DG's ro pump (FMP) + @return: The raw RO flow rate in L/min + """ + return self.measured_raw_ro_flow_LPM + + def get_measured_raw_dialysate_flow(self): + """ + Gets the measured raw (uncalibrated) flow value from DG's dialydate flow sensor (FMD) + @return: The raw dialysate flow rate in L/min + """ + return self.measured_raw_dialysate_flow_LPM + + @publish(["dg_flow_sensors_timestamp","measured_ro_flow_LPM", "measured_dialysate_flow_LPM", "measured_ro_flow_with_cp_LPM", + "measured_raw_ro_flow_LPM", "measured_raw_dialysate_flow_LPM"]) + def _handler_flow_sensors_sync(self, message, timestamp=0.0): + """ + Handles incoming DG flow sensor broadcast messages and records latest reported flow rates. + @return: none + """ + self.measured_ro_flow_LPM = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.measured_ro_flow_with_cp_LPM = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.measured_dialysate_flow_LPM = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.measured_raw_ro_flow_LPM = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.measured_raw_dialysate_flow_LPM = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.dg_flow_sensors_timestamp = timestamp + + def cmd_measured_flow_sensor_value_lpm_override(self, sensor_id: int, rate: float, raw: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the flow sensor value override command + Constraints: + Must be logged into DG. + + @param rate: (float) the sensor flow rate to be set in L/min + @param raw: (int) 1 to override measured raw value, 0 to override measured value. + @param sensor_id: (int) the sensor id to be overriden + @param reset: (int) 1 to reset a previous override, 0 to override + @return 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(reset) + index = integer_to_bytearray(sensor_id) + value = float_to_bytearray(rate) + r = integer_to_bytearray(raw) + payload = rst + value + index + r + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_MEASURED_FLOW_SENSORS_OVERRIDE.value, + payload=payload) + self.logger.debug("Overriding " + FlowSensorsNames(sensor_id).name + " flow sensor measured value to " + + str(rate) + " L/min") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Flow sensor value override Timeout!!!") + return False + + def cmd_flow_sensor_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the flow sensor data publish interval command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG priority task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_FLOW_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding flow sensors broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Flow sensors data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/fluid_leak.py =================================================================== diff -u --- leahi-dialin/dg/fluid_leak.py (revision 0) +++ leahi-dialin/dg/fluid_leak.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,153 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file fluid_leak.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peman Montazemi +# @date (original) 11-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class DGFluidLeak(AbstractSubSystem): + """ + DGFluidLeak + + Dialysate Generator (DG) Dialin API sub-class for fluid leak related commands. + """ + + # Fluid leak detector state + FLUID_LEAK_DETECTED_STATE = 0 # Wet + NO_FLUID_LEAK_DETECTED_STATE = 1 # Dry + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_fluid_leak_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FLUID_LEAK_STATE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_fluid_leak_sync) + + self.fluid_leak_state = self.NO_FLUID_LEAK_DETECTED_STATE + + def get_fluid_leak_state(self): + """ + Gets the current fluid leak state + + @return: List containing fluid leak states: [detected, undetected] + """ + return self.fluid_leak_state + + @publish(["dg_fluid_leak_timestamp","fluid_leak_state"]) + def _handler_fluid_leak_sync(self, message: dict, timestamp) -> None: + """ + Handles published fluid leak state messages. Fluid leak state is captured + for reference. + + @param message: published fluid leak state message + @return: None + """ + + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.fluid_leak_state = state[0] + self.dg_fluid_leak_timestamp = timestamp + + def cmd_fluid_leak_detector_override(self, detected_state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak detector state override command + Constraints: + Must be logged into DG. + Given detector must be one of the detectors listed below. + + @param detected_state: unsigned int - detected (0=wet, 1=dry) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected_state) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FLUID_LEAK_STATE_DETECTOR_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fluid leak detector state value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fluid_leak_state_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak state broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG priority task interval (10 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FLUID_LEAK_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD fluid leak state broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Fluid leak state broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/flush.py =================================================================== diff -u --- leahi-dialin/dg/flush.py (revision 0) +++ leahi-dialin/dg/flush.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,87 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file flush.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 16-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..common.dg_defs import DGFlushStates, DGFlushUIStates + + +class FlushMode(AbstractSubSystem): + """ + Flush Mode class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.flush_state = DGFlushStates.DG_FLUSH_STATE_START.value + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.flush_drain_line_volume_l = 0.0 + self.flush_UI_state = DGFlushUIStates.FLUSH_UI_STATE_NOT_RUNNING.value + self.dg_flush_data_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FLUSH_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_flush_sync) + + def clear_flush_info(self) -> None: + """ + Clears public class properties that are updated by the handler. + Specifically properties updated by the DG broadcast message + MSG_ID_DG_FLUSH_DATA. + + @returns : none + """ + self.flush_state = DGFlushStates.DG_FLUSH_STATE_START.value + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + + @publish(["dg_flush_data_timestamp","flush_state", "overall_elapsed_time", + "state_elapsed_time", "flush_drain_line_volume_l","flush_UI_state"]) + def _handler_flush_sync(self, message: dict, timestamp=0.0) -> None: + """ + Handles published flush message + + @param message: published flush data message + @returns none + """ + state = struct.unpack(' int: + """ + Constructs and sends the fill mode data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GEN_IDLE_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override gen idle sub-states data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/hd_proxy.py =================================================================== diff -u --- leahi-dialin/dg/hd_proxy.py (revision 0) +++ leahi-dialin/dg/hd_proxy.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,547 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_proxy.py +# +# @author (last) Micahel Garthwaite +# @date (last) 21-Aug-2023 +# @author (original) Sean +# @date (original) 15-Apr-2020 +# +############################################################################ +from logging import Logger + +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray, float_to_bytearray, byte_to_bytearray +from ..dg import NO_RESET, RESET + + +class DGHDProxy(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for HD proxy commands. + """ + + # Reservoir IDs + RESERVOIR1 = 0 + RESERVOIR2 = 1 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + def cmd_switch_reservoirs(self, reservoir_id: int = RESERVOIR1, use_last_trimmer_dc: int = 0) -> int: + """ + Constructs and sends the switch reservoirs command. + Constraints: + DG must be in re-circulate mode. + Given reservoirID must be in the reservoir list below. + + @param reservoir_id: unsigned int - reservoir to set as active (HD will draw from this reservoir). + @return: 1 if successful, zero otherwise + + @details Reservoir IDs: \n + 0 = RESERVOIR 1 \n + 1 = RESERVOIR 2 \n + """ + + res = integer_to_bytearray(reservoir_id) + dc = integer_to_bytearray(use_last_trimmer_dc) + payload = res + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCH_RESERVOIR_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("switch reservoirs cmd sent to DG") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fill(self, volume: int = 1500, tgt_fill_flow_lpm: float = 0.8, start: int = 1) -> int: + """ + Constructs and sends the fill command. + Constraints: + DG must be in re-circulate water state of re-circulate mode. + Given fill to volume must be between 0 and 1950 mL. + + @param volume: unsigned int - volume (in mL) to fill inactive reservoir to. + @param tgt_fill_flow_lpm: float - target fill flow rate in L/min. + @param start: unsigned int - 1 = start fill, 0 = stop fill. + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(volume) + integer_to_bytearray(start) + float_to_bytearray(tgt_fill_flow_lpm) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FILL_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("fill cmd sent to DG") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_drain(self, volume: int = 0, tare_load_cell: bool = False, rinse_conc_lines: bool = False, + start: int = 1) -> int: + """ + Constructs and sends the drain command. + Constraints: + DG must be in re-circulate mode. + Given drain to volume must be between 0 and 1950 mL. + + @param volume: unsigned int - volume (in mL) to drain the inactive reservoir to. + @param tare_load_cell: bool - flag indicates to tare load cell. + @param rinse_conc_lines: bool - flag indicates to whether rinse the concentrate lines or not. + @param start: int - start/stop drain command. The default is start = 1 + @return: 1 if successful, zero otherwise + """ + + vol = integer_to_bytearray(volume) + tare = integer_to_bytearray(tare_load_cell) + rinse = integer_to_bytearray(rinse_conc_lines) + st = integer_to_bytearray(start) + payload = vol + tare + rinse + st + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("drain cmd sent to DG") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg(self, start: bool = True, acid: int = 0, bicarb: int = 0) -> int: + """ + Constructs and sends the start/stop DG command + Constraints: + DG must be in idle state of standby mode if start command given. + DG must be in re-circulate mode if stop command given. + + @param start: boolean - True = start DG, False = stop DG. + @param acid: integer - identifier for selected acid concentrate type + @param bicarb: integer - identifier for selected bicarbonate concentrate type + @return: 1 if successful, zero otherwise + """ + + if start: + cmd = 1 + cmd_str = "start " + else: + cmd = 0 + cmd_str = "stop " + start = integer_to_bytearray(cmd) + act = integer_to_bytearray(acid) + bct = integer_to_bytearray(bicarb) + payload = start + act + bct + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_STARTING_STOPPING_TREATMENT_CMD_REQUEST.value, + payload=payload) + + self.logger.debug(cmd_str + "DG cmd sent to DG") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_trimmer_heater(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG trimmer heater command + + @param start: boolean - True = start heater, False = stop heater. + @return: non-zero integer if successful, False otherwise + """ + + if start: + cmd = 1 + cmd_str = "start " + else: + cmd = 0 + cmd_str = "stop " + payload = integer_to_bytearray(cmd) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD.value, + payload=payload) + + self.logger.debug(cmd_str + "DG trimmer heater cmd sent to DG") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_stop_primary_heater(self) -> None: + """ + Constructs and sends stop heat disinfect command + + @returns none + """ + # 0 is to stop + payload = integer_to_bytearray(0) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_START_STOP_PRIMARY_HEATER.value, + payload=payload) + self.logger.debug("Stopping primary heater") + self.can_interface.send(message, 0) + + def cmd_sample_water(self, cmd: int) -> None: + """ + Constructs and sends sample water command + + @param cmd: int - 0 = stop, 1 = start, 2 = flush, 3 = end. + @returns none + """ + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SAMPLE_WATER_CMD_REQUEST.value, + payload=payload) + self.logger.debug("Sending sample water command") + self.can_interface.send(message, 0) + + def cmd_start_stop_dg_heat_disinfect(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG heat disinfect command + + @param start: (bool) True = start heat disinfect, False = stop heat disinfect. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + cmd_str = "Starting" + else: + cmd = 0 + cmd_str = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_HEAT_DISINFECT_CMD_REQUEST.value, + payload=payload) + + self.logger.debug(cmd_str + " DG heat disinfect") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg_heat_disinfect_active_cool(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG heat disinfect active cool command + + @param start: (bool) True = start heat disinfect active cool, False = stop heat disinfect active cool. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + cmd_str = "Starting" + else: + cmd = 0 + cmd_str = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_HEAT_DISINFECT_ACTIVE_COOL.value, + payload=payload) + + self.logger.debug(cmd_str + " DG heat disinfect active cool") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg_flush(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG flush command + + @param start: (bool) True = start flush, False = stop flush. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + cmd_str = "Starting" + else: + cmd = 0 + cmd_str = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_FLUSH_CMD_REQUEST.value, + payload=payload) + + self.logger.debug(cmd_str + " DG flush") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg_chemical_disinfect(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG chemical disinfect command + + @param start: (bool) True = start chemical disinfect, False = stop chemical disinfect. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + str_text = "Starting" + else: + cmd = 0 + str_text = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_CHEM_DISINFECT.value, + payload=payload) + + self.logger.debug(str_text + " DG chemical disinfect") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg_chemical_disinfect_flush(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG chemical disinfect flush command + + @param start: (bool) True = start chemical disinfect flush, False = stop chemical disinfect flush. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + str_text = "Starting" + else: + cmd = 0 + str_text = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_CHEM_DISINFECT_FLUSH.value, + payload=payload) + + self.logger.debug(str_text + " DG chemical disinfect flush") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_start_stop_dg_ro_permeate_sample(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG RO permeate sample command + + @param start: (bool) True = start RO permeate sample, False = stop RO permeate sample. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + str_text = "Starting" + else: + cmd = 0 + str_text = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_RO_PERMEATE_SAMPLE_MODE_CMD_REQUEST.value, + payload=payload) + + self.logger.debug(str_text + " DG RO permeate sample") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_resend_dg_alarms(self) -> int: + """ + Constructs and sends the re-send DG alarms command + This will allow Dialin to get caught up with DG alarms that were triggered prior to connection. + + @return: non-zero integer if successful, False otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_HD_DG_ALARMS_REQUEST.value) + + self.logger.debug("Command to re-send all active DG alarms acknowledged.") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_communication_status_override(self, status: bool, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD communication status override command. + Constraints: + Must be logged into DG. + + @param reset: (int) - 1 to reset a previous override, 0 to override + @param status: (int) - HD status communication. 0 for no comm, 1 for comm + @return: (int) 1 if successful, zero otherwise + """ + reset = integer_to_bytearray(reset) + hd_status = integer_to_bytearray(status) + payload = reset + hd_status + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_HD_COMMUNICATION_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding DG HD communication Status.") + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "Reset " + else: + str_res = "Set " + self.logger.debug("HD Communication status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_send_collect_ro_permeate_sample(self, cmd: bool = 0): + """ + Constructs and sends the HD RO permeate sample dispense request. + + @param cmd: (int) - 0 for DISPENSE_MSG_CANCEL, 1 for DISPENSE_MSG_DISPENSE + @return: (int) 1 if successful, zero otherwise + """ + sample = integer_to_bytearray(cmd) + payload = sample + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_dg_ch_id, + message_id=MsgIds.MSG_ID_HD_SEND_RO_PERMEATE_SAMPLE_DISPENSE_REQUEST_TO_DG.value, + payload=payload) + + self.logger.debug("Sending RO permeate sample dispense request") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_send_chem_flush_sample_result(self, result: bool = 0): + """ + Constructs and sends the HD chem flush sample result. + + @param result: (int) - 0 for sample fail, 1 for sample pass + @return: (int) 1 if successful, zero otherwise + """ + res = integer_to_bytearray(result) + payload = res + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_dg_ch_id, + message_id=MsgIds.MSG_ID_HD_SEND_CHEM_FLUSH_SAMPLE_PASS_FAIL_TO_DG.value, + payload=payload) + + self.logger.debug("Sending Chem flush sample result") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/heat_disinfect.py =================================================================== diff -u --- leahi-dialin/dg/heat_disinfect.py (revision 0) +++ leahi-dialin/dg/heat_disinfect.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,357 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file heat_disinfect.py +# +# @author (last) Micahel Garthwaite +# @date (last) 29-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 27-Feb-2021 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, bytearray_to_integer, bytearray_to_float + + +@unique +class HeatDisinfectStates(DialinEnum): + DG_HEAT_DISINFECT_STATE_START = 0 + DG_HEAT_DISINFECT_STATE_DRAIN_R1 = 1 + DG_HEAT_DISINFECT_STATE_DRAIN_R2 = 2 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN = 3 + DG_HEAT_DISINFECT_STATE_FLUSH_CIRCULATION = 4 + DG_HEAT_DISINFECT_STATE_FLUSH_R1_AND_R2 = 5 + DG_HEAT_DISINFECT_STATE_FLUSH_R2_AND_DRAIN_R1 = 6 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R2 = 7 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R1 = 8 + DG_HEAT_DISINFECT_STATE_FILL_WITH_WATER = 9 + DG_HEAT_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 10 + DG_HEAT_DISINFECT_STATE_FILL_R2_WITH_HOT_WATER = 11 + DG_HEAT_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 12 + DG_HEAT_DISINFECT_STATE_COOL_DOWN_HEATERS = 13 + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R1 = 14 + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R2 = 15 + DG_HEAT_DISINFECT_STATE_RINSE_R1_TO_R2 = 16 + DG_HEAT_DISINFECT_STATE_RINSE_R2_TO_R1_AND_DRAIN_R1 = 17 + DG_HEAT_DISINFECT_STATE_RINSE_CIRCULATION = 18 + DG_HEAT_DISINFECT_STATE_CANCEL_BASIC_PATH = 19 + DG_HEAT_DISINFECT_STATE_CANCEL_WATER_PATH = 20 + DG_HEAT_DISINFECT_STATE_COMPLETE = 21 + + +@unique +class HeatCancellationModes(DialinEnum): + CANCELLATION_MODE_NONE = 0 + CANCELLATION_MODE_BASIC = 1 + CANCELLATION_MODE_HOT = 2 + CANCELLATION_MODE_COLD = 3 + + +@unique +class NelsonSupportModes(DialinEnum): + NELSON_NONE = 0 + NELSON_INOCULATE = 1 + NELSON_HEAT_DISINFECT = 2 + NELSON_POS_CONTROL_HEAT_DISINFECT = 3 + NELSON_CHEM_DISINFECT = 4 + NELSON_DRAIN_SAMPLE = 5 + NUM_OF_NELSON_SUPPORT = 6 + + +class HeatDisinfect(AbstractSubSystem): + """ + Heat Disinfection class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.heat_disinfect_state = 0 + self.heat_disinfect_ui_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.heat_disinfect_target_time = 0 + self.disinfect_ro_77_time_s = 0 + self.disinfect_ro_82_time_s = 0 + self.disinfect_r_77_time_s = 0 + self.disinfect_r_82_time_s = 0 + self.cancellation_mode = 0 + self.r1_level = 0 + self.r2_level = 0 + self.dg_heat_disinfection_time_timestamp = 0.0 + self.dg_heat_disinfection_data_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_HEAT_DISINFECT_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_heat_disinfect_sync) + + channel_id = DenaliChannels.dg_to_ui_ch_id + msg_id = MsgIds.MSG_ID_DG_HEAT_DISINFECT_TIME_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_heat_disinfect_to_ui_sync) + + def clear_heat_disinfect_info(self) -> None: + """ + Clears public class properties that are updated by the handler. + Specifically properties updated by the DG broadcast message + MSG_ID_DG_HEAT_DISINFECT_DATA. + + @returns none + """ + + self.heat_disinfect_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.cancellation_mode = 0 + self.r1_level = 0 + self.r2_level = 0 + self.heat_disinfect_ui_state = 0 + + @publish(["dg_heat_disinfection_time_timestamp", "heat_disinfect_target_time", "heat_disinfect_count_down_time"]) + def _handler_heat_disinfect_to_ui_sync(self, message, timestamp=0.0): + """ + Handles published heat disinfect message + + @param message: published heat disinfect UI data message + @returns none + """ + disinfect_target_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + ro_77 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + ro_82 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + r_77 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + r_82 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + disinfect_target_time, index = bytearray_to_integer(payload, index, False) + disinfect_count_down_time, index = bytearray_to_integer(payload, index, False) + + self.heat_disinfect_target_time = int(disinfect_target_time / 1000) + self.disinfect_ro_77_time_s = ro_77 + self.disinfect_ro_82_time_s = ro_82 + self.disinfect_r_77_time_s = r_77 + self.disinfect_r_82_time_s = r_82 + self.dg_heat_disinfection_time_timestamp = timestamp + + @publish(["dg_heat_disinfection_data_timestamp","heat_disinfect_state", "overall_elapsed_time", + "state_elapsed_time", "cancellation_mode", "r1_level", "r2_level", "heat_disinfect_ui_state"]) + def _handler_heat_disinfect_sync(self, message, timestamp): + """ + Handles published heat disinfect message + + @param message: published heat disinfect data message + @returns none + """ + + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + state, index = bytearray_to_integer(payload, index, False) + elapsed_time, index = bytearray_to_integer(payload, index, False) + state_elapsed_time, index = bytearray_to_integer(payload, index, False) + cancellation_mode, index = bytearray_to_integer(payload, index, False) + r1, index = bytearray_to_float(payload, index, False) + r2, index = bytearray_to_float(payload, index, False) + ui_state, index = bytearray_to_integer(payload, index, False) + + self.heat_disinfect_state = state + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.cancellation_mode = cancellation_mode + self.r1_level = r1 + self.r2_level = r2 + self.heat_disinfect_ui_state = ui_state + self.dg_heat_disinfection_data_timestamp = timestamp + + def get_heat_disinfect_state(self) -> int: + """ + Gets Heat Disinfection State + + @return: (int) + """ + return self.heat_disinfect_state + + def get_heat_disinfect_heat_disinfect_target_time(self) -> int: + """ + Gets Heat Disinfection target time + + @return: (int) + """ + return self.heat_disinfect_target_time + + def get_heat_disinfect_heat_disinfect_count_down_time(self) -> int: + """ + Gets Heat Disinfection count down time + + @return: (int) + """ + return self.heat_disinfect_count_down_time + + def get_heat_disinfect_overall_elapsed_time(self) -> int: + """ + Gets Heat Disinfection overall elapsed time + + @return: (int) + """ + return self.overall_elapsed_time + + def get_heat_disinfect_state_elapsed_time(self) -> int: + """ + Gets Heat Disinfection state elapsed time + + @return: (int) + """ + return self.heat_disinfect_target_time + + def get_heat_disinfect_cancellation_mode(self) -> int: + """ + Gets Heat Disinfection cancellation mode + + @return: (int) + """ + return self.cancellation_mode + + def get_heat_disinfect_r1_level(self) -> int: + """ + Gets Heat Disinfection r1 level + + @return: (int) + """ + return self.r1_level + + def get_heat_disinfect_r2_level(self) -> int: + """ + Gets Heat Disinfection r2 level + + @return: (int) + """ + return self.r2_level + + def get_heat_disinfect_heat_disinfect_ui_state(self) -> int: + """ + Gets Heat Disinfection UI state + + @return: (int) + """ + return self.heat_disinfect_state + + def cmd_set_nelson_support_mode(self, support: int): + """ + Constructs and sends a command to set the firmware to a certain Nelson support + Constraints: + Must be logged into DG. This code only works on a firmware that has been built in debug mode. + + @param support: (int) the type of Nelson support + NELSON_NONE = 0 + NELSON_INOCULATE = 1 + NELSON_HEAT_DISINFECT = 2 + NELSON_POS_CONTROL_CHEM_DISINFECT = 3 + NELSON_POS_CONTROL_HEAT_DISINFECT = 4 + NELSON_CHEM_DISINFECT = 5 + NELSON_DRAIN_SAMPLE = 6 + NUM_OF_NELSON_SUPPORT = 7 + @returns none + """ + payload = integer_to_bytearray(support) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_NELSON_DISINFECT_SUPPORT.value, + payload=payload) + + self.logger.debug("Setting Nelson support to " + NelsonSupportModes(support).name) + received_message = self.can_interface.send(message, 0) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_state_timer_82c_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends timer override for heat disinfection timer at 82 C. + Note: This overrides both the RO and Reservoir timer to the specified time. + Timer is only set in transition to Heat Disinfection. + Constraints: + Must be logged into DG. + Given interval must greater than 60s + + @param ms: (int) state time in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_STATE_TIMER_82C_OVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms to 82c state timer".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_state_timer_77c_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends timer override for heat disinfection timer at 77 C. + Note: This overrides both the RO and Reservoir timer to the specified time. + Timer is only set in transition to Heat Disinfection. + Constraints: + Must be logged into DG. + Given interval must greater than 60s + + @param ms: (int) state time in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_STATE_TIMER_77C_OVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms to 77c state timer".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/heat_disinfect_active_cool.py =================================================================== diff -u --- leahi-dialin/dg/heat_disinfect_active_cool.py (revision 0) +++ leahi-dialin/dg/heat_disinfect_active_cool.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,77 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file heat_disinfect_active_cool.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 19-Dec-2022 +# +############################################################################ +import struct +from logging import Logger +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish + + +class HeatDisinfectActiveCool(AbstractSubSystem): + """ + Heat Disinfect Active Cool class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.heat_disinfect_active_cool_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.dg_heat_disinfection_active_cool_data_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_HEAT_DISINFECT_ACTIVE_COOL_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_heat_disinfect_active_cool_sync) + + def clear_heat_disinfect_active_cool_info(self) -> None: + """ + Clears public class properties that are updated by the handler. + Specifically properties updated by the DG broadcast message + MSG_ID_DG_HEAT_DISINFECT_ACTIVE_COOL_DATA. + + @returns none + """ + + self.heat_disinfect_active_cool_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + + @publish(["dg_heat_disinfection_active_cool_data_timestamp","heat_disinfect_active_cool_state", + "overall_elapsed_time", "state_elapsed_time"]) + def _handler_heat_disinfect_active_cool_sync(self, message, timestamp=0.0): + """ + Handles published heat disinfect active cool message + + @param message: published heat disinfect active cool data message + @returns none + """ + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + state_elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.heat_disinfect_active_cool_state = state + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.dg_heat_disinfection_active_cool_data_timestamp = timestamp Index: leahi-dialin/dg/heaters.py =================================================================== diff -u --- leahi-dialin/dg/heaters.py (revision 0) +++ leahi-dialin/dg/heaters.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,323 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file heaters.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 29-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class HeatersStartStop(DialinEnum): + STOP = 0 + START = 1 + + +@unique +class HeatersState(DialinEnum): + HEATER_EXEC_STATE_OFF = 0 + HEATER_EXEC_STATE_PRIMARY_RAMP_TO_TARGET = 1 + HEATER_EXEC_STATE_PRIMARY_CONTROL_TO_TARGET = 2 + HEATER_EXEC_STATE_CONTROL_TO_DISINFECT_TARGE = 3 + HEATER_EXEC_STATE_TRIMMER_RAMP_TO_TARGET = 4 + HEATER_EXEC_STATE_TRIMMER_CONTROL_TO_TARGET = 5 + NUM_OF_HEATERS_STATE = 6 + + +@unique +class HeatersNames(DialinEnum): + DG_PRIMARY_HEATER = 0 + DG_TRIMMER_HEATER = 1 + NUM_OF_DG_HEATERS = 2 + + +class Heaters(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for heaters related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.main_primary_heater_duty_cycle = 0 + self.small_primary_heater_duty_cycle = 0 + self.trimmer_heater_duty_cycle = 0 + self.primary_heaters_target_temperature = 0.0 + self.trimmer_heater_target_temperature = 0.0 + self.primary_heater_state = 0 + self.trimmer_heater_state = 0 + self.primary_efficiency = 0.0 + self.primary_calc_target_temperature = 0.0 + self.trimmer_calc_target_temperature = 0.0 + self.trimmer_use_last_duty_cycle = 0 + self.dg_heaters_timestamp = 0.0 + self.previous_flow = 0.0 + self.control_counter = 0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_HEATERS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_heaters_sync) + + def get_main_primary_heater_duty_cycle(self): + """ + Gets the main primary heater duty cycle + + @return: Main primary heater duty cycle + """ + return self.main_primary_heater_duty_cycle + + def get_small_primary_heater_duty_cycle(self): + """ + Gets the small primary heater duty cycle + + @return: Small primary heater duty cycle + """ + return self.small_primary_heater_duty_cycle + + def get_trimmer_heater_duty_cycle(self): + """ + Gets the trimmer heater duty cycle + + @return: Trimmer heater duty cycle + """ + return self.trimmer_heater_duty_cycle + + def get_primary_heater_target_temperature(self): + """ + Gets the primary heater target temperature + + @return: Primary heater target temperature + """ + return self.primary_heaters_target_temperature + + def get_trimmer_heater_target_temperature(self): + """ + Gets the trimmer heater target temperature + + @return: Trimmer heater target temperature + """ + return self.trimmer_heater_target_temperature + + @publish(["dg_heaters_timestamp","main_primary_heater_duty_cycle", "small_primary_heater_duty_cycle", "trimmer_heater_duty_cycle", + "trimmer_heater_state", "primary_heaters_target_temperature", "trimmer_heater_target_temperature", + "primary_efficiency", "primary_calc_target_temperature", "trimmer_calc_target_temperature", + "trimmer_use_last_duty_cycle", "previous_flow", "control_counter"]) + def _handler_heaters_sync(self, message, timestamp=0.0): + """ + Handles published heaters message + + @param message: published heaters data message + @returns none + """ + self.main_primary_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.small_primary_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.trimmer_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.primary_heaters_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.trimmer_heater_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.primary_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.trimmer_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.primary_efficiency = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.primary_calc_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.trimmer_calc_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.trimmer_use_last_duty_cycle = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.previous_flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.control_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] + self.dg_heaters_timestamp = timestamp + + def cmd_start_stop_primary_heater(self, state: int = HeatersStartStop.STOP.value) -> None: + """ + Constructs and sends a start/stop primary heater command + Constraints: + A target temperature for primary heater between 10 and 90 deg C must have been given to DG previously. + + @param state: (int) start/stop state of the primary heater. The default is stop. + @returns none + """ + if state == HeatersStartStop.START.value: + payload = integer_to_bytearray(1) + operation = 'Turning on ' + else: + payload = integer_to_bytearray(0) + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_START_STOP_PRIMARY_HEATER.value, + payload=payload) + + self.logger.debug(operation + " the primary heater") + self.can_interface.send(message, 0) + + def cmd_start_stop_trimmer_heater(self, state: int = HeatersStartStop.STOP.value) -> None: + """ + Constructs and sends a start/stop trimmer heater command + Constraints: + A target temperature for trimmer heater between 10 and 90 deg C must have been given to DG previously. + + @param state: (int) start/stop state of the trimmer heater. The default is stop. + @returns none + """ + if state == HeatersStartStop.START.value: + payload = integer_to_bytearray(1) + operation = 'Turning on ' + else: + payload = integer_to_bytearray(0) + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD.value, + payload=payload) + + self.logger.debug(operation + " the trimmer heater") + self.can_interface.send(message, 0) + + def cmd_hd_proxy_start_stop_trimmer_heater(self, state: int = HeatersStartStop.STOP.value) -> None: + """ + Constructs and sends start trimmer heater command + Constraints: + A target temperature for trimmer heater between 10 and 90 deg C must have been given to DG previously. + + @param state: (int) start/stop state of the trimmer heater. The default is stop. + @returns none + """ + if state == HeatersStartStop.START.value: + payload = integer_to_bytearray(1) + operation = 'Turning on ' + else: + payload = integer_to_bytearray(0) + operation = 'Turning off' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_HD_START_STOP_TRIMMER_HEATER_CMD_REQUEST.value, + payload=payload) + + self.logger.debug(operation + " the Primary heater") + self.can_interface.send(message, 0) + + def cmd_set_primary_and_trimmer_heaters_target_temperature(self, + primary_target_temp: float = 37.0, + trimmer_target_temp: float = 38.0) -> None: + """ + Constructs and sends primary and trimmer heater target temperature + + @param primary_target_temp: (float) Primary heater target temperature + @param trimmer_target_temp: (float) Trimmer heater target temperature + @returns none + """ + primary_target = float_to_bytearray(primary_target_temp) + trimmer_target = float_to_bytearray(trimmer_target_temp) + payload = primary_target + trimmer_target + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_PRIMARY_AND_TRIMMER_HEATERS_TARGET_TEMP.value, + payload=payload) + self.logger.debug("Setting Primary Heater to {} C and Trimmer Heater to {} C".format(primary_target_temp, + trimmer_target_temp)) + self.can_interface.send(message, 0) + + def cmd_heater_duty_cycle_override(self, heater: int, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends heater duty cycle override command + Must be logged into DG + There must be a minimum flow available for the heaters (FMP for primary and FMD for trimmer) + + @param heater the heater to override its value (primary, trimmer) (0-1.0 order) + @param duty_cycle the duty cycle value to override in percent + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + heater_name = integer_to_bytearray(heater) + duty = float_to_bytearray(duty_cycle/100) + payload = reset_value + duty + heater_name + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_HEATERS_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding {} heater's duty cycle to {:5.3f} %".format(HeatersNames(heater).name, duty_cycle)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_heaters_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) Publish time interval in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_HEATERS_PUBLISH_INTERVAL_ORVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the Heaters module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + Index: leahi-dialin/dg/load_cells.py =================================================================== diff -u --- leahi-dialin/dg/load_cells.py (revision 0) +++ leahi-dialin/dg/load_cells.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,270 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file load_cells.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray, bytearray_to_float + + +@unique +class DGLoadCellNames(DialinEnum): + + LOAD_CELL_RESERVOIR_1_PRIMARY = 0 # Primary load cell for reservoir 1 + LOAD_CELL_RESERVOIR_1_BACKUP = 1 # Backup load cell for reservoir 1 + LOAD_CELL_RESERVOIR_2_PRIMARY = 2 # Primary load cell for reservoir 2 + LOAD_CELL_RESERVOIR_2_BACKUP = 3 # Backup load cell for reservoir 2 + NUM_OF_LOAD_CELLS = 4 # Number of load cell sensors + + +class DGLoadCells(AbstractSubSystem): + """ + DGLoadCells + + Dialysate Generator (DG) Dialin API sub-class for load cell related commands. + """ + + # Load Cell IDs + LOAD_CELL_A1 = 0 + LOAD_CELL_A2 = 1 + LOAD_CELL_B1 = 2 + LOAD_CELL_B2 = 3 + + EMPTY_RESERVOIR_WEIGHT_GRAMS = 1600 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_load_cell_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_LOAD_CELL_READINGS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_load_cells_sync) + msg_id = MsgIds.MSG_ID_DG_SEND_LOAD_CELLS_TARE_VALUES.value + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_to_dialin_ch_id, msg_id, + self._handler_load_cells_tare_values_sync) + + self.load_cell_A1 = 0.0 + self.load_cell_A2 = 0.0 + self.load_cell_B1 = 0.0 + self.load_cell_B2 = 0.0 + self.load_cells_tare_values = dict() + self.dg_load_cells_tare_values_timestamp = 0.0 + + for cell in DGLoadCellNames.__members__: + if 'NUM_OF_LOAD_CELLS' not in cell: + self.load_cells_tare_values[cell] = 0.0 + + def get_load_cells(self): + """ + Gets the current load cell weights + + @return: List containing load cell values: [A1, A2, B1, B2] + """ + return [self.load_cell_A1, self.load_cell_A2, self.load_cell_B1, self.load_cell_B2] + + @publish(["load_cells_tare_values"]) + def _handler_load_cells_tare_values_sync(self, message, timestamp=0.0): + """ + Handles sent load cells tare value messages. + + @param message: published load cell data message + @return: None + """ + payload = message['message'] + index = MsgFieldPositions.START_POS_FIELD_1 + + for cell in DGLoadCellNames.__members__: + if 'NUM_OF_LOAD_CELLS' not in cell: + tare_value, index = bytearray_to_float(payload, index, False) + self.load_cells_tare_values[cell] = tare_value + + self.dg_load_cells_tare_values_timestamp = timestamp + + @publish(["dg_load_cell_timestamp","load_cell_A1", "load_cell_A2", "load_cell_B1", "load_cell_B2"]) + def _handler_load_cells_sync(self, message, timestamp=0.0): + """ + Handles published load cell data messages. Load cell data are captured + for reference. + + @param message: published load cell data message + @return: None + """ + + self.load_cell_A1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.load_cell_A2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.load_cell_B1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.load_cell_B2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.dg_load_cell_timestamp = timestamp + + def cmd_load_cell_override(self, sensor: int, grams: float, raw: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the load cell override command + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + @param sensor: unsigned int - sensor ID + @param grams: float - weight (in grams) to override sensor with + @param raw: integer - 1 to override calibrated weight, 0 to override filtered/tared weight + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Load Cell sensor IDs: \n + 0 = A1 \n + 1 = A2 \n + 2 = B1 \n + 3 = B2 \n + """ + + rst = integer_to_bytearray(reset) + idx = integer_to_bytearray(sensor) + grm = float_to_bytearray(grams) + cal = integer_to_bytearray(raw) + payload = rst + grm + idx + cal + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + self.logger.debug( + f"override load cell weight value for sensor {str(sensor)} to {bytearray_to_float(grm, 0)[0]} grams") + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_load_cell_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the load cell data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG load cell broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Load cell data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_load_cells_tare_values(self): + """ + Constructs and sends the load cells tare values command + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + payload = b'' + + for cell in DGLoadCellNames.__members__: + if 'NUM_OF_LOAD_CELLS' not in cell: + tare = self.load_cells_tare_values[cell] + tare_byte = float_to_bytearray(tare) + payload += tare_byte + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_LOAD_CELLS_TARE_VALUES.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + self.logger.debug("Setting load cells tare values") + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_get_load_cells_tare_values(self): + """ + Constructs and sends the load cell tare values request + Constraints: + Must be logged into DG. + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_LOAD_CELLS_TARE_VALUES.value) + # Send message + received_message = self.can_interface.send(message) + + self.logger.debug("Getting the load cells tare values") + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + Index: leahi-dialin/dg/pressures.py =================================================================== diff -u --- leahi-dialin/dg/pressures.py (revision 0) +++ leahi-dialin/dg/pressures.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,186 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file pressures.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGPressures(AbstractSubSystem): + """ + DG interface containing pressure related commands. + """ + + # Pressure Sensor IDs + PRESSURE_SENSOR_RO_PUMP_INLET = 0 + PRESSURE_SENSOR_RO_PUMP_OUTLET = 1 + PRESSURE_SENSOR_DRAIN_PUMP_INLET = 2 + PRESSURE_SENSOR_DRAIN_PUMP_OUTLET = 3 + PRESSURE_SENSOR_BAROMETRIC = 4 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: The DenaliCANMessenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_PRESSURES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_pressures_sync) + + self.ro_pump_inlet_pressure = 0.0 + self.ro_pump_outlet_pressure = 0.0 + self.drain_pump_inlet_pressure = 0.0 + self.drain_pump_outlet_pressure = 0.0 + self.barometric_pressure = 0.0 + self.dg_pressures_timestamp = 0.0 + + def get_pressures(self): + """ + Gets the pressure values + + @return: [ro pump inlet, ro pump outlet, drain pump inlet, drain pump outlet] + """ + return [self.ro_pump_inlet_pressure, + self.ro_pump_outlet_pressure, + self.drain_pump_inlet_pressure, + self.drain_pump_outlet_pressure] + + @publish([ + "dg_pressures_timestamp", + "ro_pump_inlet_pressure", + "ro_pump_outlet_pressure", + "drain_pump_inlet_pressure", + "drain_pump_outlet_pressure", + "barometric_pressure" + ]) + def _handler_pressures_sync(self, message,timestamp=0.0): + """ + Handles published pressure data messages. Pressure data are captured + for reference. + + @param message: published pressure data message + @return: none + """ + + roi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + roo = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dri = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dro = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + baro = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + self.ro_pump_inlet_pressure = roi[0] + self.ro_pump_outlet_pressure = roo[0] + self.drain_pump_inlet_pressure = dri[0] + self.drain_pump_outlet_pressure = dro[0] + self.barometric_pressure = baro[0] + self.dg_pressures_timestamp = timestamp + + def cmd_pressure_override(self, sensor: int, pressure: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + pressure sensor IDs: \n + 0 = RO Pump Inlet \n + 1 = RO Pump Outlet \n + 2 = Drain Pump Inlet \n + 3 = Drain Pump Outlet \n + 4 = Barometric pressure \n + + @param sensor: unsigned int - sensor ID + @param pressure: unsigned int - pressure (in PSI) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + prs = float_to_bytearray(pressure) + idx = integer_to_bytearray(sensor) + payload = rst + prs + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pressure sensor") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: unsigned int - broadcast interval (in ms) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + ivl = integer_to_bytearray(ms) + payload = rst + ivl + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pressure data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/reservoirs.py =================================================================== diff -u --- leahi-dialin/dg/reservoirs.py (revision 0) +++ leahi-dialin/dg/reservoirs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,243 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file reservoirs.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms + + +@unique +class DGReservoirsNames(DialinEnum): + # Reservoir IDs + RESERVOIR1 = 0 + RESERVOIR2 = 1 + + +class DGReservoirs(AbstractSubSystem): + """ + DG interface containing reservoir related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: The DenaliCANMessenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_RESERVOIRS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_reservoirs_sync) + + self.active_reservoir = 0 + self.fill_to_vol_ml = 0 + self.drain_to_vol_ml = 0 + + self.time_reservoir_cycle = 0 + self.time_reservoir_fill_2_switch = 0 + self.time_uf_decay = 0.0 + self.temp_uf_fill = 0.0 + self.temp_reservoir_use_actual = 0.0 + self.temp_reservoir_end_fill = 0.0 + self.temp_avg_fill = 0.0 + self.temp_last_fill = 0.0 + self.time_rsrvr_fill = 0.0 + self.dg_reservoirs_timestamp = 0.0 + + def get_active_reservoir(self): + """ + Gets the active reservoir + RESERVOIR1 = 0 \n + RESERVOIR2 = 1 \n + + @return: 0 or 1 + """ + return self.active_reservoir + + def get_fill_to_vol(self): + """ + Gets the fill to volume + + @return: The fill to volume (mL) + """ + return self.fill_to_vol_ml + + def get_drain_to_vol(self): + """ + Gets the drain to volume + @return: The drain to volume (mL) + """ + return self.drain_to_vol_ml + + def cmd_switch_reservoirs(self, reservoir: int, use_last_trimmer_dc: int = 0) -> bool: + """ + Sends a command to the DG to switch reservoirs + @param reservoir: (int) the new reservoir number + @param use_last_trimmer_dc: (int) use last trimmer heater duty cycle (1) or not (0) + @return: True if command sent, False if invalid reservoir provided + """ + + if reservoir not in [0, 1]: + return False + + r = integer_to_bytearray(reservoir) + dc = integer_to_bytearray(use_last_trimmer_dc) + payload = r + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCH_RESERVOIR_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending command to switch DG reservoir to {0} and use last trimmer heater" + "duty cycle to {1}".format(reservoir, use_last_trimmer_dc)) + + self.can_interface.send(message, 0) + + return True + + def cmd_reservoir_tare(self, reservoir: int) -> bool: + """ + Sends a command to the DG to tare a given reservoir + @param reservoir: (int) the ID of the reservoir to tare + @return: True if command sent, False if reservoir ID invalid + """ + + if reservoir not in [0, 1]: + return False + + payload = integer_to_bytearray(reservoir) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RESERVOIR_TARE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending command to tare DG reservoir {0}".format(reservoir)) + self.can_interface.send(message, 0) + + return True + + def cmd_set_dialysate_mixing_ratios(self, acid: float, bicarb: float) -> bool: + """ + Sends a command to the DG to set the acid and bicarb mixing ratios + @param acid (float) acid's dialysate mixing ratio + @param bicarb (float) bicarb's dialysate mixing ratio + @return: True if command sent, False if timed out + """ + + ac = float_to_bytearray(acid) + bc = float_to_bytearray(bicarb) + payload = ac + bc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_DIALYSATE_MIXING_RATIOS.value, + payload=payload) + + self.logger.debug("Setting acid mixing ratio to {:5.3f} and bicarb mixing ration to {:5.3f}".format(acid, bicarb)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + @publish(["dg_reservoirs_timestamp","active_reservoir", "fill_to_vol_ml", "drain_to_vol_ml", "time_reservoir_cycle", + "time_reservoir_fill_2_switch", "time_uf_decay", "temp_uf_fill", "temp_reservoir_use_actual", + "temp_reservoir_end_fill", "temp_avg_fill", "temp_last_fill", "time_rsrvr_fill"]) + def _handler_reservoirs_sync(self, message, timestamp=0.0): + """ + Handles published reservoir data messages. Reservoir data are captured + for reference. + + @param message: published reservoir data message + @return: none + """ + + self.active_reservoir = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.fill_to_vol_ml = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.drain_to_vol_ml = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.time_reservoir_cycle = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.time_reservoir_fill_2_switch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.time_uf_decay = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.temp_uf_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.temp_reservoir_use_actual = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.temp_reservoir_end_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.temp_avg_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.temp_last_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.time_rsrvr_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.dg_reservoirs_timestamp = timestamp + + def cmd_reservoir_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the reservoir data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RESERVOIR_BROADCAST_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override reservoir data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/ro_permeate_sample.py =================================================================== diff -u --- leahi-dialin/dg/ro_permeate_sample.py (revision 0) +++ leahi-dialin/dg/ro_permeate_sample.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,132 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file ro_permeate_sample.py +# +# @author (last) Dara Navaei +# @date (last) 12-Jun-2023 +# @author (original) Dara Navaei +# @date (original) 26-May-2023 +# +############################################################################ + + +from logging import Logger +from enum import unique +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, bytearray_to_integer, bytearray_to_float + + +@unique +class ROPermeateDispenseMsgStatus(DialinEnum): + DISPENSE_MSG_READY = 0 + DISPENSE_MSG_DISPENSING = 1 + DISPENSE_MSG_CANCEL = 2 + DISPENSE_MSG_DISPENSE = 3 + NUM_OF_DISPENSE_MSG = 4 + + +class ROPermeateSample(AbstractSubSystem): + """ + RO permeate sample class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.ro_permeate_sample_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.dispensed_volume_ml = 0.0 + self.ro_permeate_dispense_message_status = 0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_RO_PERMEATE_SAMPLE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_ro_permeate_sample_sync) + + def clear_ro_permeate_sample_info(self) -> None: + """ + Clears public class properties that are updated by the handler. + + @returns none + """ + self.ro_permeate_sample_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.dispensed_volume_ml = 0.0 + self.ro_permeate_dispense_message_status = 0 + self.ro_permeate_sample_timestamp = 0 + + def get_ro_permeate_sample_state(self) -> int: + """ + Gets RO permeate sample State + + @return: (int) + """ + return self.ro_permeate_sample_state + + def get_ro_permeate_dispensed_volume_ml(self) -> float: + """ + Gets RO permeate sample dispensed volume in milliliters + + @return: (float) + """ + return self.dispensed_volume_ml + + def cmd_send_hd_dg_is_ready_to_dispense(self): + """ + Sends HD a message on behalf of DG to inform DG is ready to dispense more fluid per request + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DG_SEND_RO_PERMEATE_SAMPLE_DISPENSE_READY_TO_HD.value) + + self.logger.debug("Sending to HD that DG is ready to dispense") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + @publish(["ro_permeate_sample_timestamp", "ro_permeate_sample_state", "overall_elapsed_time", "state_elapsed_time", + "dispensed_volume_ml", "ro_permeate_dispense_message_status"]) + def _handler_ro_permeate_sample_sync(self, message, timestamp=0.0): + """ + Handles published RO permeate sample message + + @param message: published RO permeate sample data message + @returns none + """ + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + state, index = bytearray_to_integer(payload, index, False) + elapsed_time, index = bytearray_to_integer(payload, index, False) + state_elapsed_time, index = bytearray_to_integer(payload, index, False) + dispensed_vol_ml, index = bytearray_to_float(payload, index, False) + dispense_msg, index = bytearray_to_integer(payload, index, False) + + self.ro_permeate_sample_state = state + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.dispensed_volume_ml = dispensed_vol_ml + self.ro_permeate_dispense_message_status = dispense_msg + self.ro_permeate_sample_timestamp = timestamp + Index: leahi-dialin/dg/ro_pump.py =================================================================== diff -u --- leahi-dialin/dg/ro_pump.py (revision 0) +++ leahi-dialin/dg/ro_pump.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,274 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file ro_pump.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ROPumpStates(DialinEnum): + RO_PUMP_OFF_STATE = 0 + RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE = 1 + RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE = 2 + RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE = 3 + RO_PUMP_OPEN_LOOP_STATE = 4 + + +class DGROPump(AbstractSubSystem): + """ + DGROPump + Dialysate Generator (DG) Dialin API sub-class for RO pump related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGROPump constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + + msg_id = MsgIds.MSG_ID_RO_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_ro_pump_sync) + + self.target_pressure_psi = 0.0 + self.pwm_duty_cycle_pct = 0.0 + self.ro_pump_state = 0 + self.target_flow_lpm = 0.0 + self.feedback_duty_cycle_pct = 0.0 + self.dg_ro_pump_timestamp = 0.0 + + def get_target_pressure(self): + """ + Gets the target pressure + + @return: the target pressure (PSI) + """ + return self.target_pressure_psi + + def get_target_flow_rate(self): + """ + Gets the target flow rate (lpm) + + @return: The measured flow rate (float) + """ + return self.target_flow_lpm + + def get_pwm_duty_cycle_pct(self): + """ + Gets the PWM duty cycle pct + + @return: The PWM duty cycle pct (float) + """ + return self.pwm_duty_cycle_pct + + def get_ro_pump_state(self): + """ + Gets the RO pump state + + @return: The state of the RO pump + """ + return self.ro_pump_state + + @publish(["dg_ro_pump_timestamp","target_pressure_psi", "pwm_duty_cycle_pct", + "ro_pump_state", "target_flow_lpm","feedback_duty_cycle_pct"]) + def _handler_ro_pump_sync(self, message,timestamp): + """ + Handles published ro pump data messages. RO pump data are captured + for reference. + + @param message: published RO pump data message + + @return: None + """ + self.target_pressure_psi = struct.unpack(' int: + """ + Constructs and sends the set RO pump duty cycle message + Constraints: + Must be logged into DG. + + @param duty: integer - 1 percentage for duty cycle between 0 and 100 + @return: 1 if successful, zero otherwise + """ + dc = float_to_bytearray(duty/100) + payload = dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RO_PUMP_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("RO pump duty cycle set") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug( + "RO pump duty cycle set to " + str(duty) + " %" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_ro_flow_rate(self, flow: float) -> int: + """ + Constructs and sends the RO rate set command. + Constraints: + Must be logged into DG. + + @param flow: float - flow rate (in L/min) to set + @return: 1 if successful, zero otherwise + """ + + flo = float_to_bytearray(flow) + payload = flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_RO_PUMP_TARGET_FLOW.value, + payload=payload) + + self.logger.debug("Set RO pump target flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + str_res = str(flo) + self.logger.debug( + "Target RO flow rate set to " + str_res + " L/min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_ro_pump_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the RO pump set point override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - time interval (in ms) to broadcast RO pump data + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_RO_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override RO pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "RO pump data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_ro_pump_measured_feedback_pwm_override(self, pwm: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the feedback pwm override command. + Constraints: + Must be logged into DG. + Given RPM must be within 0.0 <= PWM <= 0.99 + + @param pwm: (float) pwm to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + pwm = float_to_bytearray(pwm) + payload = rst + pwm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RO_FEEDBACK_VOLTAGE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override RO Pump measured Feedback PWM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "back to normal" + else: + str_res = str(pwm) + self.logger.debug( + "Feedback PWM has been overridden " + str_res + " % " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/dg/rtc.py =================================================================== diff -u --- leahi-dialin/dg/rtc.py (revision 0) +++ leahi-dialin/dg/rtc.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,232 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file rtc.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Quang Nguyen +# @date (original) 13-May-2021 +# +############################################################################ +import struct +from ..common.msg_defs import MsgIds, MsgFieldPositions +from logging import Logger + +from ..common.msg_defs import MsgIds +from .constants import RESET, NO_RESET +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGRTC(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for rtc commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_rtc_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_RTC_EPOCH_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_rtc_epoch) + self.rtc_epoch = 0 + + def get_rtc_epoch(self): + """ + Gets the rtc epoch + + @return: The rtc epoch + """ + return self.rtc_epoch + + @publish(["dg_rtc_timestamp","rtc_epoch"]) + def _handler_rtc_epoch(self, message,timestamp=0.0): + """ + Publishes the rtc time in epoch + + @param message: published rtc epoch message + @return: None + """ + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.rtc_epoch = epoch + self.dg_rtc_timestamp = timestamp + + def cmd_stop_rtc(self): + """ + Stops the DG RTC clock + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_STOP_RTC_CLOCK.value) + + self.logger.debug("Stopping the DG RTC") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug("RTC stop command was sent" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_rtc_time_and_date(self, second: int, minute: int, hour: int, day: int, month: int, year: int) -> int: + """ + Sets the RTC time and date with the provided date and time + + @param second: (int) Second + @param minute: (int) Minute + @param hour: (int) Hour + @param day: (int) Day + @param month: (int) Month + @param year: (int) Year + @return: 1 if Successful, False otherwise + """ + sec = bytes([second]) + mint = bytes([minute]) + hour = bytes([hour]) + day = bytes([day]) + month = bytes([month]) + year = integer_to_bytearray(year) + payload = sec + mint + hour + day + month + year + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_RTC_DATE_TIME.value, + payload=payload) + + self.logger.debug("Setting time and date to rtc") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug( + "Time and Date in rtc set to seconds: " + str(sec) + " minutes: " + str(min) + " hours: " + + str(hour) + " days: " + str(day) + " months: " + str(month) + " years: " + str(year) + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + def cmd_rtc_ctl_reg1_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG RTC control register 1 status override command + Constraints: + Must be logged into DG. + + @param status: status is a bit map containing a set of bits that represent status + from the RTC status register. Specified below + + RTC_REG_1_12_HOUR_MODE_MASK = 0x0004 ( 4 ) + RTC_REG_1_PORO = 0x0008 ( 8 ) + RTC_REG_1_CLK_STOPPED_MASK = 0x0020 ( 32 ) + RTC_REG_1_UNUSED_MASK = 0x0040 ( 64 ) + RTC_REG_1_EXT_CLK_MODE_MASK = 0x0080 ( 128 ) + + more than one status bit can be sent. + Ex: to set RTC_REG_1_PORO and RTC_REG_1_CLK_STOPPED_MASK, status should be 40 (0x0028). + + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSD_ID_DG_RTC_CTL_REG1_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG RTC Control Register 1 status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("DG RTC Control Register 1 status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_rtc_ctl_reg3_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG RTC control register 3 status override command + Constraints: + Must be logged into DG. + + @param status: status is a bit map containing a set of bits that represent status + from the RTC status register. Specified below + + RTC_REG_3_BLF_MASK = 0x0004 ( 4 ) + + more than one status bit can be sent + + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSD_ID_DG_RTC_CTL_REG3_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG RTC Control Register 3 status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("DG RTC Control Register 3 status status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/dg/samplewater.py =================================================================== diff -u --- leahi-dialin/dg/samplewater.py (revision 0) +++ leahi-dialin/dg/samplewater.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,112 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file samplewater.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Quang Nguyen +# @date (original) 02-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGSampleWater(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for sample water related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGSampleWater constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_filter_flush_timestamp=0.0 + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_sync_broadcast_ch_id, + MsgIds.MSG_ID_DG_FILTER_FLUSH_PROGRESS_DATA.value, + self._handler_filter_flush_progress_sync) + + self.filter_flush_timeout = 0 + self.filter_flush_time_countdown = 0 + + def get_filter_flush_timeout(self): + """ + Gets the filter flush timeout + + @return: The filter flush timeout + """ + return self.filter_flush_timeout + + def get_filter_flush_time_countdown(self): + """ + Gets the filter flush time countdown + + @return: The filter flush time countdown + """ + return self.filter_flush_time_countdown + + @publish([ + "dg_filter_flush_timestamp", + "filter_flush_timeout", + "filter_flush_time_countdown" + ]) + def _handler_filter_flush_progress_sync(self, message: dict,timestamp=0.0) -> None: + """ + Handles published filter flush progress data messages. Filter flush progress data are captured for reference. + + @param message: published filter flush progress data message + @return: None + """ + + self.filter_flush_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.filter_flush_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.dg_filter_flush_timestamp = timestamp + + def cmd_filter_flush_time_period_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the filter flush time periodoverride command + + @param ms: integer - time period (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(reset) + integer_to_bytearray(ms) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_FILTER_FLUSH_TIME_PERIOD_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG filter flush time period") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False Index: leahi-dialin/dg/scheduled_runs_record.py =================================================================== diff -u --- leahi-dialin/dg/scheduled_runs_record.py (revision 0) +++ leahi-dialin/dg/scheduled_runs_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,366 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file scheduled_runs_record.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +import math +from collections import OrderedDict +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils +from logging import Logger + + +class DGScheduledRunsNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for scheduled runs commands. + """ + + SCHEDULED_RUNS_RECORD_START_INDEX = 6 + SCHEDULED_RECORD_SPECS_BYTES = 12 + SCHEDULED_RECORDS_SPECS_BYTE_ARRAY = 3 + + MAX_PART_NUMBER_BYTES = 10 + MAX_SERIAL_NUMBER_BYTES = 15 + + DEFAULT_SCHEDULED_RUNS_DATE_VALUE = 0 + DEFAULT_SCHEDULED_RUNS_CRC_VALUE = 0 + DEFAULT_SCHEDULED_RUNS_PADDING_VALUE = 0 + + CURRENT_MESSAGE_NUM_INDEX = 0 + TOTAL_MESSAGES_NUM_INDEX = 4 + PAYLOAD_LENGTH_INDEX = 8 + PAYLOAD_START_INDEX = 12 + + SCHEDULED_RUNS_DATA_TYPE_INDEX = 0 + SCHEDULED_RUNS_VALUE_INDEX = 1 + + TARGET_BYTES_TO_SEND_TO_FW = 150 + MIN_PAYLOAD_BYTES_SPACE = 4 + + RTC_RAM_MAX_BYTES_TO_WRITE = 64 + + PAYLOAD_CURRENT_MSG_INDEX = 0 + PAYLOAD_TOTAL_MSG_INDEX = 1 + PAYLOAD_SCHEDULED_RUNS_BYTES_INDEX = 2 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_runs_in_progress = False + self._write_fw_data_to_excel = True + self._cal_data = 0 + self._raw_scheduled_runs_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + + # DG scheduled runs main record as an ordered dictionary + self.dg_scheduled_runs_record = self._prepare_dg_scheduled_runs_record() + self.dg_scheduled_runs_record_timestamp = 0.0 + + if self.can_interface is not None: + + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SCHEDULED_RUNS_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_scheduled_runs_record) + + def cmd_reset_dg_calibration_record(self) -> bool: + """ + Handles resetting DG scheduled services record. + + @return: True if successful, False otherwise + """ + self._prepare_dg_scheduled_runs_record() + print(self.dg_scheduled_runs_record) + self.dg_scheduled_runs_record = self._utilities.reset_fw_record(self.dg_scheduled_runs_record) + status = self.cmd_set_dg_scheduled_runs_record(self.dg_scheduled_runs_record) + + return status + + def cmd_request_dg_scheduled_runs_record(self) -> int: + """ + Handles getting DG scheduled runs record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_runs_in_progress is not True: + self._is_getting_runs_in_progress = True + # Clear the list for the next call + self._raw_scheduled_runs_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SCHEDULED_RUNS_RECORD.value) + + self.logger.debug('Getting DG scheduled runs record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_scheduled_runs_record(self, message,timestamp=0.0): + """ + Handles published DG scheduled runs record messages. DG scheduled runs records are captured for processing and + updating the DG scheduled runs record. + + @param message: published DG scheduled runs record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of scheduled runs record payload is from the start index + 12 bytes for the current message + total + # messages + the length of scheduled runs record. The rest is the CAN messaging CRC and is not + # needed. + end_of_data_index = self.SCHEDULED_RUNS_RECORD_START_INDEX + self.SCHEDULED_RECORD_SPECS_BYTES + \ + self._received_msg_length + + # Get the scheduled runs data only + self._cal_data = message['message'][self.SCHEDULED_RUNS_RECORD_START_INDEX:end_of_data_index] + + # Continue getting calibration_record records until the all the messages are received. Concatenate the + # calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_scheduled_runs_record += (message['message'][self.SCHEDULED_RUNS_RECORD_START_INDEX + + self.SCHEDULED_RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done receiving the messages + self._is_getting_runs_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._update_dg_scheduled_record_from_fw() + self.dg_scheduled_runs_record_timestamp = timestamp + self._handler_received_complete_dg_scheduled_runs_record() + + @publish(["dg_scheduled_runs_record_timestamp", "dg_scheduled_runs_record"]) + def _handler_received_complete_dg_scheduled_runs_record(self): + """ + Publishes the received scheduled runs record + + @return: None + """ + self.logger.debug("Received a complete dg scheduled runs record.") + + def _update_dg_scheduled_record_from_fw(self): + """ + Handles parsing the scheduled runs messages that were received from DG firmware. + + @return: None + """ + raw_payload_temp_start_index = 0 + # Convert the concatenated raw data into a byte array since the struct library requires byte arrays. + self._raw_scheduled_runs_record = bytearray(self._raw_scheduled_runs_record) + + # Loop through the keys for the main calibration_record dictionary + # DG_Calibration : {pressure_sensors : { ppi : { gain: [' bool: + """ + Handles updating the DG system record with the newest calibration_record data of a hardware and sends it to FW. + + @return: True upon success, False otherwise + """ + + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_scheduled_runs_record) + + self.logger.debug('Setting DG scheduled runs started') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_SCHEDULED_RUNS_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Finished sending DG scheduled runs record.") + return True Index: leahi-dialin/dg/service_record.py =================================================================== diff -u --- leahi-dialin/dg/service_record.py (revision 0) +++ leahi-dialin/dg/service_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,344 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file service_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 03-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsDG +from ..utils.conversions import integer_to_bytearray + + +@unique +class ServiceLocation(DialinEnum): + SERVICE_LOCATION_FACTORY = 0 + SERVICE_LOCATION_FIELD = 1 + + +class DGServiceNVRecord(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for service record commands. + """ + + # The default service time interval is 6 months in seconds + _DEFAULT_SERVICE_INTERVAL_S = 15768000 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_SERVICE_LOCATION = ServiceLocation.SERVICE_LOCATION_FACTORY.value + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _FIRMWARE_STACK_NAME = 'DG' + + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._service_data = 0 + self._is_getting_service_in_progress = False + self._raw_service_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.dg_service_record = self._prepare_dg_service_record() + self.dg_service_record_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SERVICE_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_service_sync) + + def cmd_reset_dg_service_record(self) -> bool: + """ + Handles resetting DG service record. + + @return: True if successful, False otherwise + """ + self.dg_service_record = self._prepare_dg_service_record() + self.dg_service_record = self._utilities.reset_fw_system_service_record(self.dg_service_record) + status = self.cmd_set_dg_service_record(self.dg_service_record) + + return status + + def cmd_request_dg_service_record(self) -> int: + """ + Handles getting DG service record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_service_in_progress is not True: + self._is_getting_service_in_progress = True + # Clear the list for the next call + self._raw_service_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SERVICE_RECORD.value) + + self.logger.debug('Getting DG service record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG service record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_dg_service_record_crc_override(self, crc: int) -> bool: + """ + Handles setting DG service_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + dg_record = integer_to_bytearray(NVRecordsDG.NVDATAMGMT_SERVICE_RECORD.value) + payload = reset_byte_array + crc_value + dg_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding DG service record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_dg_service_sync(self, message,timestamp=0.0): + """ + Handles published DG service record messages. DG service records are captured for + processing and updating the DG service record. + + @param message: published DG service record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._service_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_service_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_service_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_service_record, self._raw_service_record) + self.dg_service_record_timestamp = timestamp + self._handler_received_complete_dg_service_record() + + @publish(["dg_service_record_timestamp","dg_service_record"]) + def _handler_received_complete_dg_service_record(self): + """ + Publishes the received service record + + @return: None + """ + self.logger.debug("Received a complete dg service record.") + + def cmd_set_dg_service_record(self, dg_service_record: OrderedDict) -> bool: + """ + Handles updating the DG service record and sends it to FW. + + @param dg_service_record: (OrderedDict) the dg service record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_service_record) + + self.logger.debug('Setting DG service record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_SERVICE_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("DG ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending DG service record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending DG service record.") + return True + + def _prepare_dg_service_record(self): + """ + Handles assembling the sub dictionaries of each group to make a blank dg service record. + + @return: (OrderedDict) the assembled dg service record + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_service_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' bool: + """ + Handles setting the service record data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG service record and set and observer class to callback when the record is read back + self.cmd_request_dg_service_record() + observer = NVUtilsObserver("dg_service_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.dg_service_record, report_address, + self._utilities.SERVICE_RECORD_TAB_NAME) + + ret = self.cmd_set_dg_service_record(self.dg_service_record) + return ret + + def cmd_get_dg_service_record(self, report_address: str = None): + """ + Publicly accessible function to request the DG service record and write the record to excel. + + @param report_address: the address that the report needs to be written to. The default is None so it picks an + address and writes the excel report. + + @return: none + """ + + # Create the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.SERVICE_RECORD_TAB_NAME, + report_address, protect_sheet=True) + + # Create an object of the observer class to observe the dictionary + observer = NVUtilsObserver("dg_service_record") + # Attach the observer to the list + self.attach(observer) + + # Request the latest software configuration record from firmware + self.cmd_request_dg_service_record() + # Wait until data has been received from firmware + while not observer.received: + sleep(0.1) + # Write the updated values from excel to firmware + self._utilities.write_fw_record_to_excel(self.dg_service_record) Index: leahi-dialin/dg/sw_configs.py =================================================================== diff -u --- leahi-dialin/dg/sw_configs.py (revision 0) +++ leahi-dialin/dg/sw_configs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,324 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file sw_configs.py +# +# @author (last) Dara Navaei +# @date (last) 26-May-2023 +# @author (original) Dara Navaei +# @date (original) 01-Mar-2022 +# +############################################################################ + + +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver + + +@unique +class DGSWConfigs(DialinEnum): + # NOTE: NUM_OF enum has been removed because it should be a part of the software configuration + # structure since the members of this class is for looped to create the dictionary automatically + SW_CONFIG_DISABLE_TRIMMER_HEATER = 0 + SW_CONFIG_DISABLE_ACCELS = 1 + SW_CONFIG_DISABLE_CAL_CHECK = 2 + SW_CONFIG_DISABLE_HEATERS_MONITOR = 3 + SW_CONFIG_DISABLE_DRAIN_PUMP_MONITOR = 4 + SW_CONFIG_DISABLE_RO_PUMP_MONITOR = 5 + SW_CONFIG_DISABLE_RO_RATIO_CHECK = 6 + SW_CONFIG_DISABLE_COND_SENSOR_CHECK = 7 + SW_CONFIG_DISABLE_WATER_QUALITY_CHECK = 8 + SW_CONFIG_DISABLE_FLOW_VS_LOAD_CELL_CHECK_IN_FILL = 9 + SW_CONFIG_DISABLE_HEATERS_EFFICIENCY = 10 + SW_CONFIG_DISABLE_DISINFECT_CONDUCTIVITY_CHECK = 11 + SW_CONFIG_DISABLE_CONC_PUMPS = 12 + SW_CONFIG_DISABLE_CAPS_MONITOR = 13 + SW_CONFIG_DISABLE_UV_REACTORS = 14 + SW_CONFIG_DISABLE_VOLTAGES_MONITOR = 15 + SW_CONFIG_DISABLE_BICARB_ALARMS = 16 + SW_CONFIG_DISABLE_EMPTY_BOTTLES_ALARM = 17 + SW_CONFIG_DISABLE_BICARB_CONDUCTIVITY_TEST = 18 + SW_CONFIG_DISABLE_ACID_CONDUCTIVITY_TEST = 19 + SW_CONFIG_DISABLE_CONC_PUMPS_SPEED_ALARM = 20 + SW_CONFIG_DISABLE_MIXING_IN_FILL = 21 + SW_CONFIG_DISABLE_TEMPERATURE_SENSORS_ALARM = 22 + SW_CONFIG_DISABLE_CONCENTRATE_PUMPS_PARK = 23 + SW_CONFIG_ENABLE_USING_TPO_FOR_PRIMARY_HEATER_CONTROL = 24 + + +class DGSoftwareConfigs(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for setting and getting the software configurations. + """ + + _DEFAULT_SW_CONFIG_STATUS = 0 + _DEFAULT_CRC_VALUE = 0 + _RECORD_SPECS_BYTES = 12 + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'DG' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._sw_config_data = 0 + self._is_getting_sw_config_in_progress = False + self._raw_sw_config_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.dg_sw_config_record = self._prepare_dg_sw_configs_record() + self.dg_sw_config_record_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SW_CONFIG_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_sw_config_sync) + + def cmd_reset_dg_sw_config_record(self) -> bool: + """ + Handles resetting DG software configuration record. + + @return: True if successful, False otherwise + """ + # Get the default software configuration dictionary + self.dg_sw_config_record = self._prepare_dg_sw_configs_record() + + status = self._cmd_set_dg_sw_config_record() + + return status + + def _cmd_request_dg_sw_config_record(self) -> int: + """ + Handles getting DG software config record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_sw_config_in_progress is not True: + self._is_getting_sw_config_in_progress = True + # Clear the list for the next call + self._raw_sw_config_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SW_CONFIG_RECORD.value) + + self.logger.debug('Getting DG software configuration record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG software configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_sw_config_sync(self, message,timestamp=0.0): + """ + Handles published DG software configuration record messages. DG software configuration records are captured for + processing and updating the DG software configuration record. + + @param message: published DG software configuration record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._sw_config_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_sw_config_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_sw_config_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_sw_config_record, self._raw_sw_config_record) + self.dg_sw_config_record_timestamp = timestamp + self._handler_received_complete_dg_sw_config_record() + + @publish(["dg_sw_config_record_timestamp", "dg_sw_config_record"]) + def _handler_received_complete_dg_sw_config_record(self): + """ + Publishes the received software configuration record + + @return: None + """ + self.logger.debug("Received a complete dg software configuration record.") + + def cmd_update_dg_sw_config_record(self, excel_report_path: str): + """ + Handles preparing the DG software configuration from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the software configuration is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + status = self._utilities.get_sw_configs_from_excel(self.dg_sw_config_record, excel_report_path, + self._utilities.NON_VOLATILE_RECORD_NAME) + # The excel document was successfully read initiate a write command + if status: + self._cmd_set_dg_sw_config_record() + else: + self.logger.debug('Could not find the software configurations file') + + def _cmd_set_dg_sw_config_record(self) -> bool: + """ + Handles updating the DG software configuration record and sends it to FW. + + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(self.dg_sw_config_record) + + self.logger.debug('Setting DG sw config record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_SW_CONFIG_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Finished sending DG software configuration record.") + return True + + def _prepare_dg_sw_configs_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of each group to make a blank DG software configuration record. + + @return: (OrderedDict) the assembled dg software configuration record + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_sw_configs_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the software configuration record dictionary. + + @return: software configuration record dictionary and the byte size of this group + """ + groups_byte_size = 0 + name = 'sw_configs' + # Create an ordered dictionary + sw_configs = OrderedDict({name: {}}) + + # Loop through the members of the DGSWConfigs enum class + for config in DGSWConfigs.__members__: + # Insert the enum name into the dictionary with the default software config. Each config is one byte + sw_configs[name].update({config: [' dict: + """ + Gets the status of a switch + + @return: The status of all of the switches in a dictionary + """ + return self.dg_switches_status + + @publish(["dg_switches_timestamp", "dg_switches_status"]) + def _handler_switches_sync(self, message, timestamp=0.0): + """ + Handles published drain pump data messages. Switches data are captured + for reference. + + @param message: published drain pump data message + @return: none + """ + conc_cap = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + dialysate_cap = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.dg_switches_status[DGSwitchesName.CONCENTRATE_CAP.name] = conc_cap + self.dg_switches_status[DGSwitchesName.DIALYSATE_CAP.name] = dialysate_cap + self.dg_switches_timestamp = timestamp + + def cmd_dg_switch_status_override(self, switch: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the dg switch status override command + Constraints: + Must be logged into DG. + + @param switch: (int) switch ID that is status is overridden + @param status: (int) status that the switch will be overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + sw = integer_to_bytearray(switch) + st = integer_to_bytearray(status) + payload = reset_value + st + sw + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCHES_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override switch status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(DGSwitchesName(switch).name) + " to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dg_switches_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG switch data publication override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCHES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override DG switches data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "DG Switches data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/system_record.py =================================================================== diff -u --- leahi-dialin/dg/system_record.py (revision 0) +++ leahi-dialin/dg/system_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,356 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file system_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 03-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 10-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsDG +from ..utils.conversions import integer_to_bytearray + + +@unique +class MFGLocation(DialinEnum): + MFG_LOCATION_FACTORY = 0 + + +class DGSystemNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for system record commands. + """ + + _RECORD_SPECS_BYTES = 12 + _DEFAULT_MFG_LOCATION = MFGLocation.MFG_LOCATION_FACTORY.value + _MAX_PN_BYTES = 10 + _MAX_SN_BYTES = 20 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + _FIRMWARE_STACK_NAME = 'DG' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_sys_in_progress = False + self._sys_data = 0 + self._raw_system_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # System main record + self.dg_system_record = self._prepare_dg_system_record() + self.dg_system_record_timestamp=0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SYSTEM_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_dg_system_sync) + + def cmd_reset_dg_system_record(self) -> bool: + """ + Handles resetting DG system record. + + @return: True if successful, False otherwise + """ + self.dg_system_record = self._prepare_dg_system_record() + self.dg_system_record = self._utilities.reset_fw_system_service_record(self.dg_system_record) + status = self.cmd_set_dg_system_record(self.dg_system_record) + + return status + + def get_dg_system_record(self) -> dict: + """ + Handles getting DG system record per user's request. + NOTE: In order to get the latest system record, use cmd_request_dg_system_record first + to fetch the system record from the firmware. + + @return: DG system record dictionary + """ + return self.dg_system_record['system_record'] + + def cmd_get_dg_system_record_report(self, report_destination: str = None): + """ + Handles getting DG system_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.SYSTEM_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + + observer = NVUtilsObserver("dg_system_record") + # Attach the observer to the list + self.attach(observer) + + # Request the DG system record and set and observer class to callback when the system record is read back + self.cmd_request_dg_system_record() + + while not observer.received: + sleep(0.1) + # Pass the DG system record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.dg_system_record) + + def cmd_request_dg_system_record(self) -> int: + """ + Handles getting DG system record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_sys_in_progress is not True: + # Receiving the system record is in progress + self._is_getting_sys_in_progress = True + # Clear the list for the next call + self._raw_system_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SYSTEM_RECORD.value) + + self.logger.debug('Getting DG system record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_dg_system_record_crc_override(self, crc: int) -> bool: + """ + Handles setting DG system_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + dg_record = integer_to_bytearray(NVRecordsDG.NVDATAMGMT_SYSTEM_RECORD.value) + payload = reset_byte_array + crc_value + dg_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding DG system record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_dg_system_sync(self, message, timestamp=0.0): + """ + Handles published DG system record messages. HD system records are captured for + processing and updating the DG system record. + + @param message: published DG system record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._sys_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_system_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_sys_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_system_record, self._raw_system_record) + self.dg_system_record_timestamp = timestamp + self._handler_received_complete_dg_system_record() + + @publish(["dg_system_record_timestamp","dg_system_record"]) + def _handler_received_complete_dg_system_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete dg system record.") + + def cmd_set_dg_system_record_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the system data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_dg_system_record() + observer = NVUtilsObserver("dg_system_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.dg_system_record, report_address, + self._utilities.SYSTEM_RECORD_TAB_NAME) + + ret =self.cmd_set_dg_system_record(self.dg_system_record) + return ret + + def cmd_set_dg_system_record(self, dg_system_record: OrderedDict) -> bool: + """ + Handles updating the DG system and sends it to FW. + + @param dg_system_record: (OrderedDict) the dg system record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_system_record) + + self.logger.debug('Setting DG system record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_SYSTEM_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("DG ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending DG system record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending DG system record.") + return True + + def _prepare_dg_system_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main DG system record. + + @return: (OrderedDict) the assembled dg system record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_system_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' int: + """ + Constructs and sends broadcast time interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) Publish time interval in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message( + channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_TEMPERATURE_SENSORS_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the Temperature Sensors module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_temperatures_value_override(self, sensor_index: int, sensor_value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the value override of a temperature sensor. + Constraints: + Must be logged into DG. + Given sensor_index must be one of the sensors listed below. + + @param sensor_index : (int) Index of the sensor + @param sensor_value: (float) Value of the sensor to override + @param reset: (int) whether to reset the override value. The default is NO_RESET + @returns 1 if successful, zero otherwise + + @details temperature sensor indexes: \n + 0 = Primary Heater Inlet + 1 = Heat Disinfect + 2 = Primary Heater Outlet + 3 = Conductivity Sensor 1 + 4 = Conductivity Sensor 2 + 5 = Outlet Dialysate (Redundant) + 6 = Inlet Dialysate + 7 = Primary Heater Thermocouple + 8 = Trimmer Heater Thermocouple + 9 = Primary Heater Cold Junction + 10 = Trimmer Heater Cold Junction + 11 = Primary Heater Internal + 12 = Trimmer Heater Internal + 13 = FPGA board + 14 = Load cell A1/B1 + 15 = Load cell A2/B2 + 16 = Internal THDO RTD + 17 = Internal TDI RTD + 18 = Internal conductivity temp sensor + 19 = Barometric temperature sensor + """ + rst = integer_to_bytearray(reset) + value = float_to_bytearray(sensor_value) + index = integer_to_bytearray(sensor_index) + + payload = rst + value + index + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_TEMPERATURE_SENSORS_VALUE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Setting sensor {} to {} C".format(sensor_index, sensor_value)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/thermistors.py =================================================================== diff -u --- leahi-dialin/dg/thermistors.py (revision 0) +++ leahi-dialin/dg/thermistors.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,163 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file thermistors.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ThermistorsNames(DialinEnum): + THERMISTOR_ONBOARD_NTC = 0 + THERMISTOR_POWER_SUPPLY_1 = 1 + THERMISTOR_POWER_SUPPLY_2 = 2 + + +class Thermistors(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for thermistors and board temperature sensors related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + # Dictionary of the thermistors + self.dg_thermistors_timestamp = 0.0 + self.thermistors = {ThermistorsNames.THERMISTOR_ONBOARD_NTC.name: {}, + ThermistorsNames.THERMISTOR_POWER_SUPPLY_1.name: {}, + ThermistorsNames.THERMISTOR_POWER_SUPPLY_2.name: {}} + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_THERMISTORS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_thermistors_sync) + + def get_temperature(self, thermistor): + """ + Gets a thermistor's value + + @param: thermistor: (int) thermistor index + @return: The temperature of a thermistor + """ + return self.thermistors[ThermistorsNames(thermistor).name] + + @publish(["dg_thermistors_timestamp",'thermistors']) + def _handler_thermistors_sync(self, message, timestamp=0.0): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + onboard_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + pwr_supply_1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + pwr_supply_2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.thermistors[ThermistorsNames.THERMISTOR_ONBOARD_NTC.name] = onboard_temp + self.thermistors[ThermistorsNames.THERMISTOR_POWER_SUPPLY_1.name] = pwr_supply_1 + self.thermistors[ThermistorsNames.THERMISTOR_POWER_SUPPLY_2.name] = pwr_supply_2 + + self.dg_thermistors_timestamp = timestamp + + def cmd_thermistors_value_override(self, thermistor: int, value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the thermistors value override command + Constraints: + Must be logged into DG. + + @param value: float the temperature to be set + @param thermistor: (int) thermistor index + @param reset: (int) 1 to reset a previous override, 0 to override + @return 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlu = float_to_bytearray(value) + thr = integer_to_bytearray(thermistor) + payload = reset_value + vlu + thr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_THERMISTORS_VALUE_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Themristors value override Timeout!!!") + return False + + def cmd_thermistors_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the thermistors data publish interval command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + msg_id = MsgIds.MSG_ID_DG_THERMISTORS_DATA_PUBLISH_INTERVAL_OVERRIDE.value + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, message_id=msg_id, + payload=payload) + + self.logger.debug("Overriding themistors broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Thermistors data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/usage_info_record.py =================================================================== diff -u --- leahi-dialin/dg/usage_info_record.py (revision 0) +++ leahi-dialin/dg/usage_info_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,351 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file usage_info_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 03-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 28-Apr-2022 +# +############################################################################ + +import struct +import time +from collections import OrderedDict +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsDG +from ..utils.conversions import integer_to_bytearray + + +class DGUsageNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for setting and getting the usage information record. + """ + + _DEFAULT_USAGE_INFO_VALUE = 0.0 + _DEFAULT_CRC_VALUE = 0 + _RECORD_SPECS_BYTES = 12 + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'DG' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._usage_info_data = 0 + self._is_getting_usage_info_in_progress = False + self._raw_usage_info_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.dg_usage_info_record = self._prepare_dg_usage_info_record() + self.dg_usage_info_record_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_USAGE_INFO_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_usage_info_sync) + + def _cmd_request_dg_usage_info_record(self) -> int: + """ + Handles getting DG usage information record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_usage_info_in_progress is not True: + self._is_getting_usage_info_in_progress = True + # Clear the list for the next call + self._raw_usage_info_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_USAGE_INFO_RECORD.value) + + self.logger.debug('Getting DG usage information record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG usage information record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_dg_usage_info_crc_override(self, crc: int) -> bool: + """ + Handles setting DG usage info record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + dg_record = integer_to_bytearray(NVRecordsDG.NVDATAMGMT_USAGE_INFO_RECORD.value) + payload = reset_byte_array + crc_value + dg_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding DG usage info CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_dg_usage_info_sync(self, message, timestamp=0.0): + """ + Handles published DG usage information record messages. + + @param message: published DG usage information record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._usage_info_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_usage_info_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_usage_info_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_usage_info_record, self._raw_usage_info_record) + self.dg_usage_info_record_timestamp = timestamp + self._handler_received_complete_dg_usage_info_record() + + @publish(["dg_usage_info_record_timestamp", "dg_usage_info_record"]) + def _handler_received_complete_dg_usage_info_record(self): + """ + Publishes the received usage information record + + @return: None + """ + self.logger.debug("Received a complete dg usage information record.") + + def cmd_update_dg_usage_info_record(self, excel_report_path: str): + """ + Handles preparing the DG usage information from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the information is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + self._utilities.write_excel_record_to_fw_record(self.dg_usage_info_record, excel_report_path, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + self._cmd_set_dg_usage_info_record(self.dg_usage_info_record) + + def _cmd_set_dg_usage_info_record(self, previous_record: OrderedDict) -> bool: + """ + Handles updating the DG usage information record and sends it to FW. + + @return: True upon success, False otherwise + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + + self.logger.debug('Setting DG usage information record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_USAGE_INFO_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("DG ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending DG usage info record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending DG usage info record.") + return True + + def _prepare_dg_usage_info_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of eDG usage information. + + @return: (OrderedDict) the assembled DG usage information + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_usage_info_record()] + + for function in functions: + # Update the groups bytes size so far to be used to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the usage information record dictionary. + + @return: usage information record dictionary and the byte size of this group + """ + groups_byte_size = 0 + usage_info_records = OrderedDict( + {'usage_info_record': + {'ro_total_generated_liters': [' bool: + """ + Handles resetting DG usage info record. + + @return: True if successful, False otherwise + """ + self.dg_usage_info_record = self._prepare_dg_usage_info_record() + self.dg_usage_info_record = self._utilities.reset_fw_record(self.dg_usage_info_record) + status = self._cmd_set_dg_usage_info_record(self.dg_usage_info_record) + + return status + + def cmd_set_dg_usage_info_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the usage info data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG usage record and set and observer class to callback when the record is read back + self._cmd_request_dg_usage_info_record() + observer = NVUtilsObserver("dg_usage_info_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.dg_usage_info_record, report_address, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + + ret = self._cmd_set_dg_usage_info_record(self.dg_usage_info_record) + return ret Index: leahi-dialin/dg/uv_reactors.py =================================================================== diff -u --- leahi-dialin/dg/uv_reactors.py (revision 0) +++ leahi-dialin/dg/uv_reactors.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,268 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file uv_reactors.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +@unique +class ReactorsNames(DialinEnum): + INLET_UV_REACTOR = 0 + OUTLET_UV_REACTOR = 1 + + +@unique +class ReactorsStates(DialinEnum): + UV_REACTOR_STATE_OFF = 0 + UV_REACTOR_STATE_ON = 1 + + +@unique +class ReactorsHealthStatus(DialinEnum): + UV_REACTOR_NOT_HEALTHY = 0 + UV_REACTOR_HEALTHY = 1 + UV_REACTOR_OFF = 2 + + +class UVReactors(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for UV reactors related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.dg_uv_reactor_timestamp = 0.0 + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_UV_REACTORS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_uv_reactors_sync) + + self.inlet_uv_reactor_health = 0 + self.outlet_uv_reactor_health = 0 + self.inlet_uv_reactor_state = 0 + self.outlet_uv_reactor_state = 0 + + def get_inlet_uv_reactor_health(self) -> int: + """ + Gets the inlet UV reactor health + + @return: Inlet UV reactor health + """ + return self.inlet_uv_reactor_health + + def get_outlet_uv_reactor_health(self) -> int: + """ + Gets the outlet UV reactor health + + @return: Outlet UV reactor health + """ + return self.outlet_uv_reactor_health + + def get_inlet_uv_reactor_state(self) -> int: + """ + Gets the inlet UV reactor state + + @return: Inlet UV reactor state + """ + return self.inlet_uv_reactor_state + + def get_outlet_uv_reactor_state(self) -> int: + """ + Gets the outlet UV reactor state + + @return: Outlet UV reactor state + """ + return self.outlet_uv_reactor_state + + @publish(["dg_uv_reactor_timestamp",'inlet_uv_reactor_health', 'outlet_uv_reactor_health', 'inlet_uv_reactor_state', + 'outlet_uv_reactor_state']) + def _handler_uv_reactors_sync(self, message: dict, timestamp=0.0) -> None: + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + inlet_health = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + outlet_health = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + inlet_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + outlet_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.inlet_uv_reactor_health = inlet_health + self.outlet_uv_reactor_health = outlet_health + + # Check if the received state from firmware is in range otherwise, the state will be unknown + self.inlet_uv_reactor_state = inlet_state + self.outlet_uv_reactor_state = outlet_state + self.dg_uv_reactor_timestamp = timestamp + + def cmd_uv_reactors_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UV reactors data publish interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_UV_REACTORS_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override UV reactors data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "UV reactors data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_uv_reactors_health_override(self, reactor: int, health: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UV reactors' health override command. + Constraints: + Must be logged into DG. + + @param reactor: (int) UV reactor index + @param health: (int) 0 for unhealthy and 1 for healthy + @param reset: (int) 0 for no reset and 1 for reset + @return 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + rctr = integer_to_bytearray(reactor) + hlth = integer_to_bytearray(health) + payload = reset_value + hlth + rctr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_UV_REACTORS_HEALTH_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("UV reactors health override Timeout!!!") + return False + + def cmd_start_stop_inlet_uv_reactor(self, state: int = ReactorsStates.UV_REACTOR_STATE_OFF.value) -> int: + """ + Constructs and sends a start/stop command to the DG inlet UV reactor + + @param: state: (int) the state of the inlet UV reactor. 0 for Off (default) and 1 for On. + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(0) + inlet_uv_reactor_index = integer_to_bytearray(0) + if state == ReactorsStates.UV_REACTOR_STATE_ON.value: + payload = rst + integer_to_bytearray(1) + inlet_uv_reactor_index + operation = 'Turning on ' + else: + payload = rst + integer_to_bytearray(0) + inlet_uv_reactor_index + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_UV_REACTORS.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(operation + "inlet UV reactor") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug(operation + "inlet UV reactor timeout!!") + return False + + def cmd_start_stop_outlet_uv_reactor(self, state: int = ReactorsStates.UV_REACTOR_STATE_ON.value) -> int: + """ + Constructs and sends a start/stop command to the DG outlet UV reactor + + @param: state: (int) the state of the outlet UV reactor. 0 for Off (default) and 1 for On. + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(0) + outlet_uv_reactor_index = integer_to_bytearray(1) + if state == ReactorsStates.UV_REACTOR_STATE_ON.value: + payload = rst + integer_to_bytearray(1) + outlet_uv_reactor_index + operation = 'Turning on ' + else: + payload = rst + integer_to_bytearray(0) + outlet_uv_reactor_index + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_UV_REACTORS.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(operation + "outlet UV reactor") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug(operation + "outlet UV reactor timeout!!") + return False Index: leahi-dialin/dg/valves.py =================================================================== diff -u --- leahi-dialin/dg/valves.py (revision 0) +++ leahi-dialin/dg/valves.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,449 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file valves.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peman Montazemi +# @date (original) 19-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger +from collections import OrderedDict + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + +# Valve states +ENERGIZED = True +DEENERGIZED = False + + +@unique +class VPiVSPVBfVRD1VRD2States(DialinEnum): + VALVE_STATE_CLOSED = 0 + VALVE_STATE_OPEN = 1 + + +@unique +class VPdStates(DialinEnum): + VALVE_STATE_DRAIN_C_TO_NO = 0 + VALVE_STATE_OPEN_C_TO_NC = 1 + + +@unique +class VPoStates(DialinEnum): + VALVE_STATE_NOFILL_C_TO_NO = 0 + VALVE_STATE_FILL_C_TO_NC = 1 + + +@unique +class VDrVRcStates(DialinEnum): + VALVE_STATE_DRAIN_C_TO_NO = 0 + VALVE_STATE_RECIRC_C_TO_NC = 1 + + +@unique +class VRoVRiStates(DialinEnum): + VALVE_STATE_R1_C_TO_NO = 0 + VALVE_STATE_R2_C_TO_NC = 1 + + +@unique +class VRdVRfStates(DialinEnum): + VALVE_STATE_R2_C_TO_NO = 0 + VALVE_STATE_R1_C_TO_NC = 1 + +@unique +class DGValvesSensedStates(DialinEnum): + VALVE_STATE_OPEN = 0 # Open valve state, used only for VPi, VBf, VRD1, VRD2, and VSP + VALVE_STATE_CLOSED = 1 # Closed valve state, used only for VPi, VBf, VRD1, VRD2, and VSP + VALVE_STATE_OPEN_C_TO_NC = 2 # Open Common to Normally Closed valve state, used only for VPd + VALVE_STATE_NOFILL_C_TO_NO = 3 # No Fill Common to Normally Open valve state, used only for VPo + VALVE_STATE_FILL_C_TO_NC = 4 # Fill Common to Normally Closed valve state, used only for VPo + VALVE_STATE_DRAIN_C_TO_NO = 5 # Drain Common to Normally Open valve state, used only for VDr and VRc. It is also used for VPd in V3 + VALVE_STATE_RECIRC_C_TO_NC = 6 # Recirculate Common to Normally Closed valve state, used only for VDr and VRc + VALVE_STATE_R1_C_TO_NO = 7 # Reservoir 1 Common to Normally Open valve state, used only for VRo and VRi + VALVE_STATE_R1_C_TO_NC = 8 # Reservoir 1 Common to Normally Closed valve state, used only for VRf + VALVE_STATE_R2_C_TO_NO = 9 # Reservoir 2 Common to Normally Open valve state, used only for VRf + VALVE_STATE_R2_C_TO_NC = 10 # Reservoir 2 Common to Normally Closed valve state, used only for VRo and VRi + NUM_OF_VALVE_STATES = 11 # number of valve states + + +@unique +class DGValveNames(DialinEnum): + # NOTE: NUM_OF enum has been removed because it should be a part of the software configuration + # structure since the members of this class is for looped to create the dictionary automatically + VALVE_RESERVOIR_FILL = 0 # VRF + VALVE_RESERVOIR_INLET = 1 # VRI + RESERVED_SPACE = 2 # RESERVED SPACE + VALVE_RESERVOIR_OUTLET = 3 # VRO + VALVE_PRESSURE_OUTLET = 4 # VPO + VALVE_BYPASS_FILTER = 5 # VBF + VALVE_RECIRCULATE = 6 # VRC + VALVE_DRAIN = 7 # VDR + VALVE_PRESSURE_INLET = 8 # VPI + VALVE_SAMPLING_PORT = 9 # VSP + VALVE_RESERVOIR_DRAIN_1 = 10 # VRD1 + VALVE_RESERVOIR_DRAIN_2 = 11 # VRD2 + VALVE_PRODUCTION_DRAIN = 12 # VPD + + +class DGValves(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for valve related commands. + + """ + + # Valves states publish message field positions + START_POS_VALVES_STATES = DenaliMessage.PAYLOAD_START_INDEX + END_POS_VALVES_STATES = START_POS_VALVES_STATES + 2 # Valves States come in as a U16 value (2 bytes) + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.valves_sensed_states = OrderedDict() + self.dg_valves_states_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_VALVES_STATES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_valves_sync) + + self.valve_states_all = 0x0000 + self.valve_state_VRF = {"id": DGValveNames.VALVE_RESERVOIR_FILL.value, "state": DEENERGIZED} + self.valve_state_VRI = {"id": DGValveNames.VALVE_RESERVOIR_INLET.value, "state": DEENERGIZED} + self.valve_state_VRO = {"id": DGValveNames.VALVE_RESERVOIR_OUTLET.value, "state": DEENERGIZED} + self.valve_state_VPO = {"id": DGValveNames.VALVE_PRESSURE_OUTLET.value, "state": DEENERGIZED} + self.valve_state_VBF = {"id": DGValveNames.VALVE_BYPASS_FILTER.value, "state": DEENERGIZED} + self.valve_state_VRC = {"id": DGValveNames.VALVE_RECIRCULATE.value, "state": DEENERGIZED} + self.valve_state_VDR = {"id": DGValveNames.VALVE_DRAIN.value, "state": DEENERGIZED} + self.valve_state_VPI = {"id": DGValveNames.VALVE_PRESSURE_INLET.value, "state": DEENERGIZED} + self.valve_state_VSP = {"id": DGValveNames.VALVE_SAMPLING_PORT.value, "state": DEENERGIZED} + self.valve_state_VRD1 = {"id": DGValveNames.VALVE_RESERVOIR_DRAIN_1.value, "state": DEENERGIZED} + self.valve_state_VRD2 = {"id": DGValveNames.VALVE_RESERVOIR_DRAIN_2.value, "state": DEENERGIZED} + self.valve_state_VPD = {"id": DGValveNames.VALVE_PRODUCTION_DRAIN.value, "state": DEENERGIZED} + + # NOTE: The len function counts the enums with the same number only once. This is not the case in the DG valves + # class because each valve must have a unique ID. + self.valve_states_enum = [0 for _ in range(len(DGValveNames))] + + for valve in DGValveNames.__members__: + self.valves_sensed_states[valve] = '' + + def get_valve_states(self): + """ + Gets the valve states + + @return: All valve states: \n + [\n + Valve Reservoir Fill \n + Valve Reservoir Inlet \n + Reserved Space \n + Valve Reservoir Outlet \n + Valve Pressure Outlet \n + Valve Bypass Filter \n + Valve Recirculate \n + Valve Drain \n + Valve Pressure Inlet \n + Valve Sampling Port \n + Valve Reservoir 1 Drain \n + Valve Reservoir 2 Drain \n + Valve Production Drain \n + ]\n + """ + return [ + self.valve_state_VRF.get("state", None), + self.valve_state_VRI.get("state", None), + self.valve_state_VRO.get("state", None), + self.valve_state_VPO.get("state", None), + self.valve_state_VBF.get("state", None), + self.valve_state_VRC.get("state", None), + self.valve_state_VDR.get("state", None), + self.valve_state_VPI.get("state", None), + self.valve_state_VSP.get("state", None), + self.valve_state_VRD1.get("state", None), + self.valve_state_VRD2.get("state", None), + self.valve_state_VPD.get("state", None) + ] + + @staticmethod + def sort_by_id(observation): + """ + Converts a published dictionary of valve state information to an ordered list + of tuples. + + For example: + hd = DG() + observation = {'datetime': datetime.datetime(2020, 7, 13, 10, 43, 27, 433357), + + 'valve_state_VBF': {'id': 5, 'state': True}, + 'valve_state_VDR': {'id': 7, 'state': True}, + 'valve_state_VPD': {'id': 12, 'state': True}, + 'valve_state_VPI': {'id': 8, 'state': True}, + 'valve_state_VPO': {'id': 4, 'state': True}, + 'valve_state_VR1': {'id': 10, 'state': True}, + 'valve_state_VR2': {'id': 11, 'state': True}, + 'valve_state_VRC': {'id': 6, 'state': True}, + 'valve_state_VRF': {'id': 0, 'state': True}, + 'valve_state_VRI': {'id': 1, 'state': True}, + 'valve_state_VRO': {'id': 3, 'state': True}, + 'valve_state_VSP': {'id': 9, 'state': True}, + 'valve_states_all': 8191} + self.logger.debug(hd.valves.sort_by_id(observation)) + + ('valve_state_VRF', 0, True) + ('valve_state_VRI', 1, True) + ('valve_state_VRO', 3, True) + ('valve_state_VPO', 4, True) + ('valve_state_VBF', 5, True) + ('valve_state_VRC', 6, True) + ('valve_state_VDR', 7, True) + ('valve_state_VPI', 8, True) + ('valve_state_VSP', 9, True) + ('valve_state_VR1', 10, True) + ('valve_state_VR2', 11, True) + ('valve_state_VPD', 12, True) + + @param observation: dictionary of the observed valve states + @return: a list of tuples of the valve states + """ + + result = [] + for key, value in observation.items(): + if isinstance(value, dict): + result.append((key, value.get("id", None), value.get("state", None))) + + result = sorted(result, key=lambda each: each[1]) + return result + + @staticmethod + def _binary_to_valve_state(binary) -> bool: + """ + @param binary: binary value + @return: 1 = energized, otherwise de-energized + """ + + if binary != 0: + return ENERGIZED + else: + return DEENERGIZED + + @publish([ + "dg_valves_states_timestamp", + "valve_states_all", + "valve_state_VRF", + "valve_state_VRI", + "valve_state_VRO", + "valve_state_VPO", + "valve_state_VBF", + "valve_state_VRC", + "valve_state_VDR", + "valve_state_VPI", + "valve_state_VSP", + "valve_state_VRD1", + "valve_state_VRD2", + "valve_state_VPD", + "valve_states_enum" + ]) + def _handler_valves_sync(self, message, timestamp=0.0): + """ + Handles published valves states message. + + @param message: published valves states message + @return: none + """ + + vst = struct.unpack('H', bytearray(message['message'][self.START_POS_VALVES_STATES:self.END_POS_VALVES_STATES])) + self.valve_states_all = vst[0] + # Extract each valve state from U16 valves states using bit-masking + self.valve_state_VRF["state"] = self._binary_to_valve_state(vst[0] & 1) + self.valve_state_VRI["state"] = self._binary_to_valve_state(vst[0] & 2) + self.valve_state_VRO["state"] = self._binary_to_valve_state(vst[0] & 8) + self.valve_state_VPO["state"] = self._binary_to_valve_state(vst[0] & 16) + self.valve_state_VBF["state"] = self._binary_to_valve_state(vst[0] & 32) + self.valve_state_VRC["state"] = self._binary_to_valve_state(vst[0] & 64) + self.valve_state_VDR["state"] = self._binary_to_valve_state(vst[0] & 128) + self.valve_state_VPI["state"] = self._binary_to_valve_state(vst[0] & 256) + self.valve_state_VSP["state"] = self._binary_to_valve_state(vst[0] & 512) + self.valve_state_VRD1["state"] = self._binary_to_valve_state(vst[0] & 1024) + self.valve_state_VRD2["state"] = self._binary_to_valve_state(vst[0] & 2048) + self.valve_state_VPD["state"] = self._binary_to_valve_state(vst[0] & 4096) + + self.valve_states_enum[DGValveNames.VALVE_RESERVOIR_FILL.value] = VRdVRfStates(self._binary_to_valve_state(vst[0] & 1)).name # VRF + self.valve_states_enum[DGValveNames.VALVE_RESERVOIR_INLET.value] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 2)).name # VRI + self.valve_states_enum[DGValveNames.VALVE_RESERVOIR_OUTLET.value] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 8)).name # VRO + self.valve_states_enum[DGValveNames.VALVE_PRESSURE_OUTLET.value] = VPoStates(self._binary_to_valve_state(vst[0] & 16)).name # VPO + self.valve_states_enum[DGValveNames.VALVE_BYPASS_FILTER.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 32)).name # VBF + self.valve_states_enum[DGValveNames.VALVE_RECIRCULATE.value] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 64)).name # VRC + self.valve_states_enum[DGValveNames.VALVE_DRAIN.value] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 128)).name # VDR + self.valve_states_enum[DGValveNames.VALVE_PRESSURE_INLET.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 256)).name # VPI + self.valve_states_enum[DGValveNames.VALVE_SAMPLING_PORT.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 512)).name # VSP + self.valve_states_enum[DGValveNames.VALVE_RESERVOIR_DRAIN_1.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 1024)).name # VRD1 + self.valve_states_enum[DGValveNames.VALVE_RESERVOIR_DRAIN_2.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 2048)).name # VRD2 + self.valve_states_enum[DGValveNames.VALVE_PRODUCTION_DRAIN.value] = VPdStates(self._binary_to_valve_state(vst[0] & 4096)).name # VPD + + start = self.END_POS_VALVES_STATES + end = start + 1 + for valve_id in self.valves_sensed_states: + valve_state_number = struct.unpack('B', bytearray(message['message'][start:end]))[0] + self.valves_sensed_states[valve_id] = DGValvesSensedStates(valve_state_number).name + start = end + end += 1 + + self.dg_valves_states_timestamp = timestamp + + def cmd_valve_sensed_state_override(self, valve: int, state: bool, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve sensed state override command. + Constraints: + Must be logged into DG. + Given valve ID must be one of the valve IDs listed below. + + @param valve: unsigned int - valve ID + @param state: bool - valve state + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + valve IDs: \n + 0 = Valve Reservoir Fill \n + 1 = Valve Reservoir Inlet \n + 2 = Reserved Space \n + 3 = Valve Reservoir Outlet \n + 4 = Valve Pressure Outlet \n + 5 = Valve Bypass Filter \n + 6 = Valve Recirculate \n + 7 = Valve Drain \n + 8 = Valve Pressure Inlet \n + 9 = Valve Sampling Port \n + 10 = Valve Reservoir 1 Drain \n + 11 = Valve Reservoir 2 Drain \n + 12 = Valve Production Drain \n + """ + + rst = integer_to_bytearray(reset) + ste = integer_to_bytearray(int(state)) + vlv = integer_to_bytearray(valve) + payload = rst + ste + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_VALVES_SENSED_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override valve sensed state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_valve_override(self, valve: int, state: bool, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve state override command. + Constraints: + Must be logged into DG. + Given valve ID must be one of the valve IDs listed below. + + @param valve: unsigned int - valve ID + @param state: bool - valve state + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + valve IDs: \n + 0 = Valve Reservoir Fill \n + 1 = Valve Reservoir Inlet \n + 2 = Reserved Space \n + 3 = Valve Reservoir Outlet \n + 4 = Valve Pressure Outlet \n + 5 = Valve Bypass Filter \n + 6 = Valve Recirculate \n + 7 = Valve Drain \n + 8 = Valve Pressure Inlet \n + 9 = Valve Sampling Port \n + 10 = Valve Reservoir 1 Drain \n + 11 = Valve Reservoir 2 Drain \n + 12 = Valve Production Drain \n + """ + + rst = integer_to_bytearray(reset) + ste = integer_to_bytearray(int(state)) + vlv = integer_to_bytearray(valve) + payload = rst + ste + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_VALVE_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override valve state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_valve_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve state override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: unsigned int - broadcast interval (in ms) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + ivl = integer_to_bytearray(ms) + payload = rst + ivl + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override valves states publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/voltages.py =================================================================== diff -u --- leahi-dialin/dg/voltages.py (revision 0) +++ leahi-dialin/dg/voltages.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,250 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file voltages.py +# +# @author (last) Dara Navaei +# @date (last) 17-Apr-2023 +# @author (original) Sean Nash +# @date (original) 22-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.base import DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +# Monitored voltages +@unique +class DGMonitoredVoltages(DialinEnum): + MONITORED_LINE_24V_MAIN = 0 # Main voltage (24V) + MONITORED_LINE_1_8V_FPGA = 1 # FPGA logic voltage (1.8V) + MONITORED_LINE_1V_FPGA = 2 # FPGA reference voltage (1V) + MONITORED_LINE_3_3V_SENSORS = 3 # Sensors voltage (3.3V) + MONITORED_LINE_1_8V_PROC = 4 # Processor voltage (1.8V) + MONITORED_LINE_5V_SENSORS = 5 # Sensors voltage (5V) + MONITORED_LINE_5V_LOGIC = 6 # Logic voltage (5V) + MONITORED_LINE_3_3V = 7 # Logic voltage (3.3V) + MONITORED_LINE_1_2V_PROC = 8 # Processor voltage (1.2V) + MONITORED_LINE_V_REF = 9 # Reference voltage (3V) + MONITORED_LINE_EXT_ADC_1_REF_V = 10 # External ADC 1 reference voltage (3V) + MONITORED_LINE_EXT_ADC_2_REF_V = 11 # External ADC 2 reference voltage (3V) + MONITORED_LINE_FPGA_VCC_V = 12 # FPGA VCC voltage (3V) + MONITORED_LINE_FPGA_AUX_VCC_V = 13 # FPGA auxiliary voltage (3V) + MONITORED_LINE_FPGA_VPVN_V = 14 # FPGA VPVN voltage (1V) + MONITORED_LINE_PS_GATE_DRIVER_V = 15 # P/S gate driver (5V) + MONITORED_LINE_NON_ISOLATED_24_V_MAIN = 16 # 24 V non-isolate voltage + # NOTE: This enum has been commented out because two enums cannot have the same value but the firmware has + # MONITORED_LINE_PS_GATE_DRIVER_V and MONITORED_LINE_LAST_RANGE_CHECKED_LINE with the same number so for clarity it + # is kept here + # MONITORED_LINE_LAST_RANGE_CHECKED_LINE = MONITORED_LINE_PS_GATE_DRIVER_V # Monitored last range + MONITORED_LINE_24V_POWER_PRIM_HTR_V = 17 # Power primary heater voltage (24V) + MONITORED_LINE_24V_GND_MAIN_PRIM_HTR_V = 18 # Main primary heater ground voltage + MONITORED_LINE_24V_GND_SMALL_PRIM_HTR_V = 19 # Small primary heater voltage (24V) + MONITORED_LINE_24V_GND_TRIM_HTR_V = 20 # Trimmer heater voltage (24V) + NUM_OF_MONITORED_LINES = 21 # Number of monitored voltages + + +class DGVoltages(AbstractSubSystem): + """ + Hemodialysis Delivery (DG) Dialin API sub-class for voltage monitor related commands and data. + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGVoltages constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.dg_voltages_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_VOLTAGES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_monitored_voltages_sync) + self.monitored_voltages = [0.0] * DGMonitoredVoltages.NUM_OF_MONITORED_LINES.value + + def get_monitored_voltages(self): + """ + Gets all DG monitored voltages + + @return: List of voltages of size NUM_OF_MONITORED_VOLTAGE_LINES + """ + return self.monitored_voltages + + @publish([ + "dg_voltages_timestamp", + "monitored_voltages" + ]) + def _handler_monitored_voltages_sync(self,message,timestamp=0.0): + """ + Handles published DG monitored voltages data messages. Voltage data are captured + for reference. + + @param message: published monitored voltages data message + @return: none + """ + + v1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + v12 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + v18p = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + v18f = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + v3r = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + ve1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + ve2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + v33 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + v33s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + v5l = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + v5s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + v5g = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + v24p = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + v24pp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + v24pg = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + v24sg = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16])) + v24t = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17])) + int_vcc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18])) + aux_vcc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_19:MsgFieldPositions.END_POS_FIELD_19])) + vpvn = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_20:MsgFieldPositions.END_POS_FIELD_20])) + v24_non_iso = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_21:MsgFieldPositions.END_POS_FIELD_21])) + + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1V_FPGA.value] = v1[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_2V_PROC.value] = v12[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_8V_PROC.value] = v18p[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_8V_FPGA.value] = v18f[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_V_REF.value] = v3r[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_EXT_ADC_1_REF_V.value] = ve1[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_EXT_ADC_2_REF_V.value] = ve2[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_3_3V.value] = v33[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_3_3V_SENSORS.value] = v33s[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_5V_LOGIC.value] = v5l[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_5V_SENSORS.value] = v5s[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_PS_GATE_DRIVER_V.value] = v5g[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_MAIN.value] = v24p[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_POWER_PRIM_HTR_V.value] = v24pp[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_GND_MAIN_PRIM_HTR_V.value] = v24pg[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_GND_SMALL_PRIM_HTR_V.value] = v24sg[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_GND_TRIM_HTR_V.value] = v24t[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_FPGA_VCC_V.value] = int_vcc[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_FPGA_AUX_VCC_V.value] = aux_vcc[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_FPGA_VPVN_V.value] = vpvn[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_NON_ISOLATED_24_V_MAIN.value] = v24_non_iso[0] + self.dg_voltages_timestamp = timestamp + + def cmd_monitored_voltage_override(self, signal: int = 0, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG monitored voltage override command + Constraints: + Must be logged into DG. + Given signal must be valid member of DGMonitoredVoltages enum + + @param signal: integer - ID of signal to override + @param volts: float - value (in volts) to override signal with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vlt = float_to_bytearray(volts) + idx = integer_to_bytearray(signal) + payload = rst + vlt + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_MONITORED_VOLTAGES_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored DG voltage for signal " + str(signal)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(volts) + " V. " + self.logger.debug("Monitored DG voltage overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_monitored_voltages_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the monitored DG voltages broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored DG voltages broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("DG monitored voltages broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/dg/watchdog.py =================================================================== diff -u --- leahi-dialin/dg/watchdog.py (revision 0) +++ leahi-dialin/dg/watchdog.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,81 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file watchdog.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Micahel Garthwaite +# @date (original) 17-Aug-2023 +# +############################################################################ +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray + + +class DGWatchdog(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for watchdog related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + def cmd_watchdog_task_check_in_override(self, state: int, task: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the watchdog task check-in override command + Constraints: + Must be logged into DG. + Given task must be valid. + Given state must be a 0 or 1. + + @param state: integer - 1 for task checked in, 0 for task not checked in + @param task: integer - ID of task to override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + tsk = integer_to_bytearray(task) + payload = rst + sta + tsk + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_WATCHDOG_TASK_CHECKIN_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG watchdog task check-in state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("checked in" if state != 0 else "not checked in") + self.logger.debug("watchdog task check-in overridden to " + str_res + ":" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/__init__.py =================================================================== diff -u --- leahi-dialin/hd/__init__.py (revision 0) +++ leahi-dialin/hd/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,12 @@ +from .alarms import HDAlarms +from .blood_flow import HDBloodFlow +from .buttons import HDButtons +from .constants import RESET, NO_RESET, BUTTON_PRESSED, BUTTON_RELEASED +from .dialysate_inlet_flow import HDDialysateInletFlow +from .dialysate_outlet_flow import HDDialysateOutletFlow +from .hemodialysis_device import HD +from .pressure_occlusion import HDPressureOcclusion +from .rtc import HDRTC +from .treatment import HDTreatment +from .ui_proxy import HDUIProxy +from .watchdog import HDWatchdog Index: leahi-dialin/hd/accelerometer.py =================================================================== diff -u --- leahi-dialin/hd/accelerometer.py (revision 0) +++ leahi-dialin/hd/accelerometer.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,252 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file accelerometer.py +# +# @author (last) Micahel Garthwaite +# @date (last) 03-Mar-2023 +# @author (original) Sean Nash +# @date (original) 29-Jul-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAccelerometer(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for accelerometer related commands. + """ + + # Vector axes + class AccelerometerVector: + def __init__(self, x=0.0, y=0.0, z=0.0): + """ + HDAccelerometer constructor + + """ + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return "{0}: ({1},{2},{3})".format(self.__class__.__name__, self.x, self.y, self.z) + + # Vector axes + VECTOR_AXIS_X = 0 + VECTOR_AXIS_Y = 1 + VECTOR_AXIS_Z = 2 + + def __init__(self, can_interface, logger: Logger): + """ + HDAccelerometer constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_ACCELEROMETER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_accelerometer_sync) + + self.hd_accel_timestamp = 0.0 + self.vector = self.AccelerometerVector() + self.vector_max = self.AccelerometerVector() + self.tilts = self.AccelerometerVector() + + def get_accel_vector(self): + """ + Gets the accelerometer vector. + @return: (vector) The vector from the accelerometer + """ + return self.vector + + def get_accel_max_vector(self): + """ + Gets the accelerometer maximum vector. + @return: (vector) The max. vector from the accelerometer + """ + return self.vector_max + + def get_accel_tilts(self): + """ + Gets the tilt angles from the accelerometer. + @return: (vector) The X, Y, and Z tilt angles. + """ + return self.tilts + + @publish([ + "hd_accel_timestamp", + "vector", + "vector_max", + "tilts" + ]) + def _handler_accelerometer_sync(self, message,timestamp=0.0): + """ + Handles published accelerometer data messages. Accelerometer data are captured + for reference. + + @param message: published accelerometer data message + @return: none + """ + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + self.vector = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + self.vector_max = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + self.tilts = self.AccelerometerVector(x[0], y[0], z[0]) + self.hd_accel_timestamp = timestamp + + def cmd_accel_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer vector override command + Constraints: + Must be logged into HD. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer axis magnitude") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(mag) + " g. " + self.logger.debug("Accelerometer axis " + str(axis) + " overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_accel_max_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer maximum vector override command + Constraints: + Must be logged into HD. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_MAX_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer axis maximum magnitude") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(mag) + " g. " + self.logger.debug("Accelerometer max. axis " + str(axis) + " overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_accel_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Accelerometer broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/air_bubbles.py =================================================================== diff -u --- leahi-dialin/hd/air_bubbles.py (revision 0) +++ leahi-dialin/hd/air_bubbles.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,254 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file air_bubbles.py +# +# @author (last) Sean Nash +# @date (last) 04-May-2023 +# @author (original) Peman Montazemi +# @date (original) 18-May-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class HDAirBubbles(AbstractSubSystem): + """ + HDAirBubbles + + Hemodialysis Delivery (HD) Dialin API sub-class for air bubbles related commands. + ADA: Air bubble Detector Arterial + ADV: Air bubble Detector Venous + """ + + # Air bubble detectors + ADV = 0 # Air bubble Detector Venous + + # Air bubble detectors status + BUBBLE_DETECTED_STATUS = 0 # Air bubble detected + FLUID_DETECTED_STATUS = 1 # Fluid (no air bubble) detected + + # Air bubble detectors state machine states + AIR_BUBBLE_NORMAL_STATE = 0 # Normal state + AIR_BUBBLE_SELF_TEST_STATE = 1 # Self-test state + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_BUBBLES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_air_bubbles_data_sync) + + self.hd_air_bubbles_timestamp = 0.0 + # Initialize status of ADV air bubble detectors to fluid (no air bubble) detected + self.air_bubbles_status = [self.FLUID_DETECTED_STATUS] + + # Initialize state of ADV air bubble detectors state machine to normal + self.air_bubbles_state = [self.AIR_BUBBLE_NORMAL_STATE] + + def get_air_bubble_status(self, index: int) -> int: + """ + Gets the given air bubble status using index 0 for ADV. + + @param index: integer - 0 for getting ADV detector status + @return: Air bubble status (air bubble or fluid) for given detector + """ + + return self.air_bubbles_status[index] + + def get_air_bubble_state(self, index: int) -> int: + """ + Gets the given air bubble state using index 0 for ADV. + + @param index: integer - 0 for getting ADV detector state + @return: integer - air bubble state (0: init, 1: self-test, 2: normal) for given detector + """ + + return self.air_bubbles_state[index] + + @publish(["hd_air_bubbles_timestamp", "air_bubbles_status", "air_bubbles_state"]) + def _handler_air_bubbles_data_sync(self, message, timestamp=0.0): + """ + Handles published air bubbles data messages. Air bubble status and state are captured + for ADV detector. + + @param message: published air bubbles data message as: ADV status, ADV state + @return: None + """ + + adv_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + adv_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.air_bubbles_status = [adv_status[0]] + self.air_bubbles_state = [adv_state[0]] + self.hd_air_bubbles_timestamp = timestamp + + def cmd_air_bubble_status_override(self, status: int, index: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air bubble detector status override command + Constraints: + Must be logged into HD. + Given detector must be one of the detectors listed below. + + @param status: unsigned int - status (0=air bubble, 1=fluid) to override detector with + @param index: integer - 0 for ADV status override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + i = integer_to_bytearray(index) + stat = integer_to_bytearray(status) + payload = rst + stat + i + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLE_STATUS_OVERRIDE.value, + payload=payload) + + if index == self.ADV: + self.logger.debug("Override air bubble detector ADV status value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_air_bubble_self_test_request(self, index: int) -> int: + """ + Request air bubble self-test for a given detector (ADV) + Constraints: + Must be logged into HD. + + @param index: integer - 0 for ADV status override + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(index) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLE_SELF_TEST_REQUEST.value, + payload=payload) + + if index == self.ADV: + self.logger.debug("Request air bubble self-test for detector ADV") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_air_bubbles_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air bubbles data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be positive non-zero and a multiple of the HD priority task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if ms > 0 and ms % 50 == 0: + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLES_DATA_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD air bubbles data broadcast interval to"+str(ms)+"ms") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Air bubbles data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + elif ms <= 0: + self.logger.debug("ms must be positive non-zero.") + return False + elif ms % 50 != 0: + self.logger.debug("ms must be a multiple of 50.") + return False + else: + self.logger.debug("ms must be an integer.") + return False + + def cmd_venous_bubble_alarm_enable(self, enabled: bool = True) -> int: + """ + Constructs and sends the venous bubble alarm detection enable/disable command + Constraints: + Must be logged into HD. + + @param enabled: bool - True enables alarm detection, False disables alarm detection + @return: 1 if successful, zero otherwise + """ + + ena = 1 if enabled else 0 + + payload = integer_to_bytearray(ena) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ENABLE_VENOUS_BUBBLE_ALARM_DETECTION.value, + payload=payload) + + if enabled: + self.logger.debug("Enable venous bubble alarm detection") + else: + self.logger.debug("Disable venous bubble alarm detection") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/air_pump.py =================================================================== diff -u --- leahi-dialin/hd/air_pump.py (revision 0) +++ leahi-dialin/hd/air_pump.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,151 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file air_pump.py +# +# @author (last) Micahel Garthwaite +# @date (last) 10-Mar-2023 +# @author (original) Micahel Garthwaite +# @date (original) 21-Nov-2022 +# +############################################################################ + +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class HDAirPump(AbstractSubSystem): + """ + HDAirPump + + Hemodialysis Delivery (HD) Dialin API sub-class for air pump related commands. + """ + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_AIR_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_air_pump_sync) + + self.air_pump_state = 0 + self.hd_air_pump_timestamp = 0.0 + + def get_air_pump_state(self): + """ + Gets the air pump state + AIR_PUMP_STATE_INIT = 0, ///< Air Pump Initialize state + AIR_PUMP_STATE_OFF, ///< Air Pump Off state + AIR_PUMP_STATE_ON, ///< Air Pump On state + NUM_OF_AIR_PUMP_STATES, ///< Number of air pump states + + @return: the current air pump state. + """ + return self.air_pump_state + + @publish(["hd_air_pump_timestamp", "air_pump_state"]) + def _handler_air_pump_sync(self, message, timestamp=0.0): + """ + Handles published air pump data messages. + + @param message: published air pump data message as: air pump state + @return: None + """ + aps = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.air_pump_state = aps[0] + self.hd_air_pump_timestamp = timestamp + + def cmd_air_pump_set_state(self, state: int) -> int: + """ + Constructs and sends the air pump set state command. + AIR_PUMP_STATE_INIT = 0, ///< Air Pump Initialize state + AIR_PUMP_STATE_OFF, ///< Air Pump Off state + AIR_PUMP_STATE_ON, ///< Air Pump On state + NUM_OF_AIR_PUMP_STATES, ///< Number of air pump states + + Constraints: + Must be logged into HD. + + """ + + idx = integer_to_bytearray(state) + payload = idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_PUMP_SET_STATE.value, + payload=payload) + + self.logger.debug("setting air pump state to" + str(state)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_air_pump_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air pump data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_PUMP_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD air pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Air pump data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/air_trap.py =================================================================== diff -u --- leahi-dialin/hd/air_trap.py (revision 0) +++ leahi-dialin/hd/air_trap.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,246 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file air_trap.py +# +# @author (last) Dara Navaei +# @date (last) 12-Mar-2024 +# @author (original) Sean Nash +# @date (original) 21-Sep-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAirTrap(AbstractSubSystem): + """ + HDAirTrap + + Hemodialysis Delivery (HD) Dialin API sub-class for air trap related commands. + """ + + # Air trap level sensor IDs + LOWER_LEVEL_SENSOR = 0 + UPPER_LEVEL_SENSOR = 1 + + # Air trap level sensor levels + AIR_DETECTED_AT_LEVEL = 0 + FLUID_DETECTED_AT_LEVEL = 1 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_AIR_TRAP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_air_trap_sync) + + self.lower_level = self.AIR_DETECTED_AT_LEVEL + self.upper_level = self.AIR_DETECTED_AT_LEVEL + self.lower_level_raw = self.AIR_DETECTED_AT_LEVEL + self.upper_level_raw = self.AIR_DETECTED_AT_LEVEL + self.hd_air_trap_timestamp = 0.0 + + def get_air_trap_levels(self): + """ + Gets the current air trap levels + + @return: List containing air trap levels: [Lower, Upper, RawLower, RawUpper] + """ + return [self.lower_level, self.upper_level, self.lower_level_raw,self.upper_level_raw] + + def get_air_trap_lower_level(self): + """ + Gets the current air trap lower level reading + + @return: 0 for air, 1 for fluid at lower level + """ + return self.lower_level + + def get_air_trap_upper_level(self): + """ + Gets the current air trap upper level reading + + @return: 0 for air, 1 for fluid at upper level + """ + return self.upper_level + + def get_raw_air_trap_lower_level(self): + """ + Gets the current air trap raw lower level reading + + @return: 0 for air, 1 for fluid at lower level + """ + return self.lower_level_raw + + def get_raw_air_trap_upper_level(self): + """ + Gets the current air trap raw upper level reading + + @return: 0 for air, 1 for fluid at upper level + """ + return self.upper_level_raw + + @publish(["hd_air_trap_timestamp", "lower_level", "upper_level", "raw_lower_level", "raw_upper_level"]) + def _handler_air_trap_sync(self, message, timestamp=0.0): + """ + Handles published air trap data messages. Air trap data are captured + for reference. + + @param message: published air trap data message + @return: None + """ + + lower = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + upper = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + raw_lower = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + raw_upper = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + self.lower_level = lower[0] + self.upper_level = upper[0] + self.lower_level_raw = raw_lower[0] + self.upper_level_raw = raw_upper[0] + self.hd_air_trap_timestamp = timestamp + + def cmd_air_trap_level_sensor_override(self, sensor: int, detected: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air trap level sensor override command + Constraints: + Must be logged into HD. + Given sensor must be one of the sensors listed below. + + @param sensor: unsigned int - sensor ID + @param detected: unsigned int - detected (0=air, 1=fluid) to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Air trap sensor IDs: \n + 0 = Lower level \n + 1 = Upper level \n + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected) + idx = integer_to_bytearray(sensor) + payload = rst + det + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_TRAP_LEVEL_SENSOR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override air trap level sensor detection value for sensor " + str(sensor) + " ,Level: " + str(detected)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_raw_air_trap_level_sensor_override(self, sensor: int, detected: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the raw air trap level sensor override command + Constraints: + Must be logged into HD. + Given sensor must be one of the sensors listed below. + + @param sensor: unsigned int - sensor ID + @param detected: unsigned int - detected (0=air, 1=fluid) to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Air trap sensor IDs: \n + 0 = Lower level \n + 1 = Upper level \n + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected) + idx = integer_to_bytearray(sensor) + payload = rst + det + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RAW_AIR_TRAP_LEVEL_SENSOR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override raw air trap level sensor detection value for sensor " + str(sensor) + " ,Level: " + str(detected)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_air_trap_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air trap data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_TRAP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD air trap data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Air trap data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/alarms.py =================================================================== diff -u --- leahi-dialin/hd/alarms.py (revision 0) +++ leahi-dialin/hd/alarms.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,1020 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file alarms.py +# +# @author (last) Michael Garthwaite +# @date (last) 04-Oct-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique +from ..utils.base import DialinEnum + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.hd_defs import HDEventDataType +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAlarms(AbstractSubSystem): + """ + HD interface containing alarm related commands. + """ + + # Alarm lamp patterns + HD_ALARM_LAMP_PATTERN_OFF = 0 + HD_ALARM_LAMP_PATTERN_OK = 1 + HD_ALARM_LAMP_PATTERN_FAULT = 2 + HD_ALARM_LAMP_PATTERN_HIGH = 3 + HD_ALARM_LAMP_PATTERN_MEDIUM = 4 + HD_ALARM_LAMP_PATTERN_LOW = 5 + HD_ALARM_LAMP_PATTERN_MANUAL = 6 + + # Alarm priority states + HD_ALARM_STATE_NONE = 0 + HD_ALARM_STATE_LOW = 1 + HD_ALARM_STATE_MEDIUM = 2 + HD_ALARM_STATE_HIGH = 3 + + # Alarm response buttons + @unique + class AlarmResponseButtons(DialinEnum): + HD_ALARM_RESPONSE_BUTTON_RESUME = 0 + HD_ALARM_RESPONSE_BUTTON_RINSEBACK = 1 + HD_ALARM_RESPONSE_BUTTON_END_TREATMENT = 2 + NUM_OF_HD_ALARM_RESPONSE_BUTTONS = 3 + + # Alarm status message field positions + START_POS_ALARM_STATE = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ALARM_STATE = START_POS_ALARM_STATE + 4 + START_POS_ALARM_TOP = END_POS_ALARM_STATE + END_POS_ALARM_TOP = START_POS_ALARM_TOP + 4 + START_POS_ALARM_ESCALATES_IN = END_POS_ALARM_TOP + END_POS_ALARM_ESCALATES_IN = START_POS_ALARM_ESCALATES_IN + 4 + START_POS_ALARM_SILENCE_EXPIRES_IN = END_POS_ALARM_ESCALATES_IN + END_POS_ALARM_SILENCE_EXPIRES_IN = START_POS_ALARM_SILENCE_EXPIRES_IN + 4 + START_POS_ALARMS_FLAGS = END_POS_ALARM_SILENCE_EXPIRES_IN + END_POS_ALARMS_FLAGS = START_POS_ALARMS_FLAGS + 2 + + def __init__(self, can_interface, logger: Logger): + """ + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_STATUS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarms_status_sync) + + channel_id = DenaliChannels.hd_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_TRIGGERED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_trigger) + msg_id = MsgIds.MSG_ID_ALARM_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_clear) + msg_id = MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_condition_clear) + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_ALARM_INFORMATION_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_information_sync) + + self.hd_alarm_status_timestamp = 0.0 + self.hd_alarm_triggered_timestamp = 0.0 + self.hd_alarm_cleared_timestamp = 0.0 + self.hd_alarm_clr_condition_timestamp = 0.0 + self.hd_alarm_information_timestamp = 0.0 + # composite alarm status based on latest HD alarm status broadcast message + self.alarms_priority_state = 0 + self.alarm_top = 0 + self.alarms_silence_expires_in = 0 + self.alarms_escalates_in = 0 + self.alarms_flags = 0 + + # alarm states based on received HD alarm activation and alarm clear messages + self.alarm_states = [False] * 500 + # alarm condition states based on received HD alarm activation and clear condition messages + self.alarm_conditions = [False] * 500 + # alarm priorities based on received HD alarm activation messages + self.alarm_priorities = [0] * 500 + # alarm ranks based on received HD alarm activation messages + self.alarm_ranks = [0] * 500 + # alarm clear top only flags based on received HD alarm activation messages + self.alarm_clear_top_only_flags = [False] * 500 + # alarm debug data on alarm triggered message + self.alarm_data = [0, 0] * 500 + + # alarm information + self.alarm_data_type = dict() + self.alarm_volume = 0 + self.alarm_audio_curr_hg = 0.0 + self.alarm_audio_curr_lg = 0.0 + self.alarm_backup_audio_curr = 0.0 + self.safety_shutdown_active = False + self.ac_power_lost = False + self.alarm_table_button_blockers = [False] * self.AlarmResponseButtons.NUM_OF_HD_ALARM_RESPONSE_BUTTONS.value + self.alarm_state_button_blockers = [False] * self.AlarmResponseButtons.NUM_OF_HD_ALARM_RESPONSE_BUTTONS.value + + # Loop through the list of the event data type enum and update the dictionary + for data_type in HDEventDataType: + event_data_type = HDEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type or 'NONE' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self.alarm_data_type[event_data_type] = struct_unpack_type + + def get_current_alarms_state(self): + """ + Gets the current alarms state. + + @return: (int) the current alarms state. + """ + return self.alarms_priority_state + + def get_alarm_states(self): + """ + Gets all states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_states + + def get_alarm_response_button_table_blocker_states(self): + """ + Gets the states (T/F) of the alarm response buttons blocked by alarm table properties. + + @return: List of booleans of size 3 (for resume, rinseback and end treatment buttons) + """ + return self.alarm_table_button_blockers + + def get_alarm_response_button_state_blocker_states(self): + """ + Gets the states (T/F) of the alarm response buttons blocked by state properties. + + @return: List of booleans of size 3 (for resume, rinseback and end treatment buttons) + """ + return self.alarm_state_button_blockers + + def get_alarm_conditions(self): + """ + Gets all alarm condition states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_conditions + + def get_alarm_state(self, alarm_id): + """ + Gets alarm state for given alarm + + @return: Alarm state + """ + return self.alarm_states[alarm_id] + + def get_alarm_priority(self, alarm_id): + """ + Gets alarm priority for given alarm. + 0=None + 1=Low + 2=Medium + 3=High + + @return: Alarm priority + """ + return self.alarm_priorities[alarm_id] + + def get_alarm_rank(self, alarm_id): + """ + Gets alarm rank for given alarm + + @return: Alarm rank + """ + return self.alarm_ranks[alarm_id] + + def get_alarm_clear_top_only(self, alarm_id): + """ + Gets clear top only property for given alarm + + @return: Clear top only property (T/F) + """ + return self.alarm_clear_top_only_flags[alarm_id] + + def get_alarms_top(self): + """ + Gets the top alarm + + @return: (int) the top alarm + """ + return self.alarm_top + + def get_alarms_silence_expires_in(self): + """ + Gets the remaining time the alarms will be silenced (s) + + @return: (int) how long until the alarm silence expires + """ + return self.alarms_silence_expires_in + + def get_alarms_escalates_in(self): + """ + Gets the alarms escalates in time (s) + + @return: (int) how long until the alarm escalates + """ + return self.alarms_escalates_in + + def get_alarms_flags(self): + """ + Gets the alarms flags + + Extract each flag from the flags int using bit-masking. E.g. + + System Fault = result & 1 + Stop = result & 2 + No Clear = result & 4 + No Resume = result & 8 + No Rinseback = result & 16 + No End Treatment = result & 32 + No New Treatment = result & 64 + User Must ACK = result & 128 + Alarms to Escalate = result & 256 + Alarms Silenced = result & 512 + Alarm Lamp On = result & 1024 + TBD = result & 2048 + No Blood Recirc = result & 4096 + No Dialysate Recirc = result & 8192 + No Minimize = result & 16384 + Condition Detected = result & 32768 + + @return: (int) The alarms flags value + """ + return self.alarms_flags + + def get_alarm_volume(self): + """ + Gets the alarm audio volume level. + + @return: (int) current alarm audio volume (1..5) + """ + return self.alarm_volume + + def get_alarm_audio_current_hg(self): + """ + Gets the alarm audio current - high gain. + + @return: (float) alarm audio current - high gain (in mA) + """ + return self.alarm_audio_curr_hg + + def get_alarm_audio_current_lg(self): + """ + Gets the alarm audio current - low gain. + + @return: (float) alarm audio current - low gain (in mA) + """ + return self.alarm_audio_curr_lg + + def get_alarm_backup_audio_current(self): + """ + Gets the alarm backup audio current. + + @return: (float) alarm backup audio current (in mA) + """ + return self.alarm_backup_audio_curr + + def get_safety_shutdown_activated(self): + """ + Gets the state of the HD safety shutdown signal. + + @return: (bool) safety shutdown line is activated (T/F) + """ + return self.safety_shutdown_active + + def get_ac_power_lost(self): + """ + Gets the state of the HD a/c power loss signal. + + @return: (bool) a/c power is lost (T/F) + """ + return self.ac_power_lost + + def get_alarm_flag_system_fault(self) -> bool: + """ + Gets the alarm flag system fault. + + @return: (bool) Alarm flag system fault (T/F) + """ + return (self.alarms_flags & 1) > 0 + + def get_alarm_flag_stop(self) -> bool: + """ + Gets the alarm flag no clear. + + @return: (bool) Alarm flag no clear (T/F) + """ + return (self.alarms_flags & 2) > 0 + + def get_alarm_flag_no_clear(self) -> bool: + """ + Gets the alarm flag no clear. + + @return: (bool) Alarm flag no clear (T/F) + """ + return (self.alarms_flags & 4) > 0 + + def get_alarm_flag_no_resume(self) -> bool: + """ + Gets the alarm flag no resume. + + @return: (bool) Alarm flag no resume (T/F) + """ + return (self.alarms_flags & 8) > 0 + + def get_alarm_flag_no_rinseback(self) -> bool: + """ + Gets the alarm flag no rinseback. + + @return: (bool) Alarm flag no rinseback (T/F) + """ + return (self.alarms_flags & 16) > 0 + + def get_alarm_flag_no_end_treatment(self) -> bool: + """ + Gets the alarm flag no end treatment. + + @return: (bool) Alarm flag no end treatment (T/F) + """ + return (self.alarms_flags & 32) > 0 + + def get_alarm_flag_no_new_treatment(self) -> bool: + """ + Gets the alarm flag no new treatment. + + @return: (bool) Alarm flag no new treatment (T/F) + """ + return (self.alarms_flags & 64) > 0 + + def get_alarm_flag_okay_button_only(self) -> bool: + """ + Gets the alarm flag Ok button only. + + @return: (bool) Alarm flag Ok button only (T/F) + """ + return (self.alarms_flags & 128) > 0 + + def get_alarm_flag_alarm_to_escalate(self) -> bool: + """ + Gets the alarm flag to escalate. + + @return: (bool) Alarm flag indicating an active alarm is due to escalate (T/F) + """ + return (self.alarms_flags & 256) > 0 + + def get_alarm_flag_is_alarm_silenced(self) -> bool: + """ + Gets the alarm flag alarm silence + + @return: (bool) Alarm flag indicating alarms are currently silenced (T/F) + """ + return (self.alarms_flags & 512) > 0 + + def get_alarm_flag_lamp_on(self) -> bool: + """ + Gets the alarm flag lamp on. + + @return: (bool) Alarm lamp on (T/F) + """ + return (self.alarms_flags & 1024) > 0 + + def get_alarm_flag_no_blood_recirculation(self) -> bool: + """ + Gets the alarm flag no blood recirculation. + + @return: (bool) Alarm lamp on (T/F) + """ + return (self.alarms_flags & 4096) > 0 + + def get_alarm_flag_no_dialysate_recirculation(self) -> bool: + """ + Gets the alarm flag no dialysate recirculation. + + @return: (bool) Alarm lamp on (T/F) + """ + return (self.alarms_flags & 8192) > 0 + + def get_alarm_flag_no_minimize(self) -> bool: + """ + Gets the alarm flag no minimize. + + @return: (bool) Alarm cannot be minimized (T/F) + """ + return (self.alarms_flags & 16384) > 0 + + def get_alarm_flag_top_condition(self) -> bool: + """ + Gets the alarm flag top condition. + + @return: (bool) Top Alarm's condition is still being detected + """ + + return (self.alarms_flags & 32768) > 0 + + def clear_dialin_alarms(self): + """ + Clears the alarms states in Dialin. + + @return: none + """ + for x in range(500): + self.alarm_states[x] = False + + def get_alarm_data(self, alarm_id) -> list: + """ + Gets the alarm data fields for the requested alarm id + + @return: the alarm data fields for the requested alarm id + """ + return self.alarm_data[alarm_id] + + @publish(["hd_alarm_status_timestamp", "alarms_priority_state", "alarm_top", "alarms_silence_expires_in", "alarms_escalates_in", "alarms_flags"]) + def _handler_alarms_status_sync(self, message, timestamp=0.0): + """ + Handles published alarms status messages. alarms status data are captured + for reference. + + @param message: published alarm status data message + @return: none + """ + self.alarms_priority_state = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.alarm_top = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.alarms_escalates_in = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.alarms_silence_expires_in = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.alarms_flags = struct.unpack('H', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.START_POS_FIELD_5+2]))[0] + + self.hd_alarm_status_timestamp = timestamp + + @publish(["hd_alarm_triggered_timestamp", "alarm_states", "alarm_conditions", "alarm_data", + "alarm_priorities", "alarm_ranks", "alarm_clear_top_only_flags"]) + def _handler_alarm_trigger(self, message, timestamp=0.0): + """ + Handles published HD alarm activation messages. + + @param message: published HD alarm activation message + @return: none + """ + self.logger.debug("Alarm activated!") + alarm_id = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + data_typ_1 = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + # Get the corresponding structure format + struct_data_type_1 = self.alarm_data_type[HDEventDataType(data_typ_1[0]).name] + # Get the data value by unpacking the data type + data_1 = struct.unpack(struct_data_type_1, bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + data_typ_2 = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + # Get the corresponding structure format + struct_data_type_2 = self.alarm_data_type[HDEventDataType(data_typ_2[0]).name] + # Get the data value by unpacking the data type + data_2 = struct.unpack(struct_data_type_2, bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + priority = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + rank = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + clr_top_only = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + + self.logger.debug("Alarm ID: %d %d %d" % (alarm_id[0], data_1[0], data_2[0])) + self.alarm_states[alarm_id[0]] = True + self.alarm_conditions[alarm_id[0]] = True + self.alarm_priorities[alarm_id[0]] = priority[0] + self.alarm_ranks[alarm_id[0]] = rank[0] + self.alarm_clear_top_only_flags[alarm_id[0]] = clr_top_only[0] + self.alarm_data[alarm_id[0]] = [data_1[0], data_2[0]] + self.hd_alarm_triggered_timestamp = timestamp + + @publish(["hd_alarm_cleared_timestamp", "alarm_states", "alarm_conditions"]) + def _handler_alarm_clear(self, message, timestamp=0.0): + """ + Handles published HD alarm clear messages. + + @param message: published HD alarm clear message + @return: none + """ + alarm_id = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + self.alarm_states[alarm_id[0]] = False + self.alarm_conditions[alarm_id[0]] = False + self.hd_alarm_cleared_timestamp = timestamp + + @publish(["hd_alarm_clr_condition_timestamp", "alarm_conditions", "alarm_conditions"]) + def _handler_alarm_condition_clear(self, message, timestamp=0.0): + """ + Handles published HD alarm clear alarm condition messages. + + @param message: published HD alarm clear alarm condition message + @return: none + """ + alarm_id = struct.unpack('i', bytearray(message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + self.alarm_conditions[alarm_id[0]] = False + self.hd_alarm_clr_condition_timestamp = timestamp + + @publish(["hd_alarm_information_timestamp", "alarm_volume", "alarm_audio_curr_hg", "alarm_audio_curr_lg", "alarm_backup_audio_curr", + "safety_shutdown_active", "ac_power_lost", "alarm_table_button_blockers", "alarm_state_button_blockers"]) + def _handler_alarm_information_sync(self, message, timestamp=0.0): + """ + Handles published HD alarm information broadcast messages. + + @param message: published HD alarm information message + @return: none + """ + + vol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ach = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + acl = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + bac = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + saf = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + acp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + trs = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.START_POS_FIELD_7+1])) + trb = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7+1:MsgFieldPositions.START_POS_FIELD_7+2])) + tet = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7+2:MsgFieldPositions.START_POS_FIELD_7+3])) + srs = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7+3:MsgFieldPositions.START_POS_FIELD_7+4])) + srb = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7+4:MsgFieldPositions.START_POS_FIELD_7+5])) + set = struct.unpack('B', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7+5:MsgFieldPositions.START_POS_FIELD_7+6])) + + self.alarm_volume = vol[0] + self.alarm_audio_curr_hg = ach[0] + self.alarm_audio_curr_lg = acl[0] + self.alarm_backup_audio_curr = bac[0] + self.safety_shutdown_active = True if saf[0] == 1 else False + self.ac_power_lost = True if acp[0] == 1 else False + self.alarm_table_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_RESUME.value] = True if trs[0] == 1 else False + self.alarm_table_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_RINSEBACK.value] = True if trb[0] else False + self.alarm_table_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_END_TREATMENT.value] = True if tet[0] else False + self.alarm_state_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_RESUME.value] = True if srs[0] else False + self.alarm_state_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_RINSEBACK.value] = True if srb[0] else False + self.alarm_state_button_blockers[self.AlarmResponseButtons.HD_ALARM_RESPONSE_BUTTON_END_TREATMENT.value] = True if set[0] else False + self.hd_alarm_information_timestamp = timestamp + + def cmd_clear_all_alarms(self) -> int: + """ + Constructs and sends the clear all active alarms command. + This will clear even non-recoverable alarms. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + key = integer_to_bytearray(-758926171) # 0xD2C3B4A5 + payload = key + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_SUPER_CLEAR_ALARMS_CMD.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("All alarms cleared.") + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_resend_all_alarms(self) -> int: + """ + Constructs and sends the re-send all active alarms command. + This will allow Dialin to get caught up with HD alarms that were triggered prior to connection. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SEND_ALARMS_COMMAND.value) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Command to re-send all active HD alarms acknowledged.") + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_alarm_state_override(self, alarm: int, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm state override command + Constraints: + Must be logged into HD. + Given alarm must be valid. + If inactivating alarm, given alarm must be recoverable (clearable). + + @param alarm: integer - ID of alarm to override + @param state: integer - 1 for alarm active, 0 for alarm inactive + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + alm = integer_to_bytearray(alarm) + payload = rst + sta + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATE_OVERRIDE.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("active" if state != 0 else "inactive") + self.logger.debug("Alarm " + str(alarm) + " " + str_res + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_time_override(self, alarm: int, time_ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm time override command + Constraints: + Must be logged into HD. + Given alarm must be valid. + + @param alarm: integer - ID of alarm to override + @param time_ms: integer - time (in ms) since alarm was activated + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + ms = integer_to_bytearray(time_ms) + alm = integer_to_bytearray(alarm) + payload = rst + ms + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_TIME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm time since activated") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(time_ms) + self.logger.debug("Alarm time since activated overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_lamp_pattern_override(self, pattern: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm lamp pattern override command. + Constraints: + Must be logged into HD. + Given pattern must be one of the patterns listed below. + + @param pattern: integer - ID of alarm lamp pattern to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Patterns: \n + 0 = off \n + 1 = ok \n + 2 = fault \n + 3 = high \n + 4 = medium \n + 5 = low \n + 6 = manual + """ + rst = integer_to_bytearray(reset) + pat = integer_to_bytearray(pattern) + payload = rst + pat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_LAMP_PATTERN_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override Alarm Lamp Pattern") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(received_message) + if reset == RESET: + str_pat = "reset back to normal" + elif pattern == self.HD_ALARM_LAMP_PATTERN_OFF: + str_pat = "off" + elif pattern == self.HD_ALARM_LAMP_PATTERN_OK: + str_pat = "ok" + elif pattern == self.HD_ALARM_LAMP_PATTERN_FAULT: + str_pat = "fault" + elif pattern == self.HD_ALARM_LAMP_PATTERN_HIGH: + str_pat = "high" + elif pattern == self.HD_ALARM_LAMP_PATTERN_MEDIUM: + str_pat = "medium" + elif pattern == self.HD_ALARM_LAMP_PATTERN_LOW: + str_pat = "low" + else: + str_pat = "manual" + self.logger.debug("Alarm lamp pattern overridden to " + str_pat + ":" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_info_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET): + """ + Constructs and sends the alarm information broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_INFO_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm information broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Alarm information broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_status_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the alarm status broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_STATUS_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm status broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Alarm status broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_volume_override(self, volume: int = 5, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio volume override command + Constraints: + Must be logged into HD. + Given volume must be an integer between 1 and 5. + + @param volume: integer - alarm audio volume level (1..5) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = integer_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_VOLUME_LEVEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio volume level") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(volume) + ": " + self.logger.debug("Alarm audio volume level overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_current_hg_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio current (high gain) override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for high gain alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_CURRENT_HG_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio high gain current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(current) + " mA: " + self.logger.debug("Alarm audio high gain current overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_current_lg_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio current (low gain) override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for low gain alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_CURRENT_LG_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio high gain current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(current) + " mA: " + self.logger.debug("Alarm audio low gain current overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_alarm_backup_audio_current_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the backup alarm audio current override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for backup alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_BACKUP_AUDIO_CURRENT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm backup audio current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(current) + " mA: " + self.logger.debug("Alarm backup audio current overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/battery.py =================================================================== diff -u --- leahi-dialin/hd/battery.py (revision 0) +++ leahi-dialin/hd/battery.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,358 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file battery.py +# +# @author (last) Michael Garthwaite +# @date (last) 04-Oct-2023 +# @author (original) Micahel Garthwaite +# @date (original) 28-Mar-2022 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDBattery(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for battery subsystem. + """ + + _BATTERY_MAH_2_MWH_FACTOR = 14.7 # < Conversion factor for mAh to mWh. + + def __init__(self, can_interface, logger: Logger): + """ + HDBattery constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_BATTERY_MANAGEMENT_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_battery_manager_sync) + msg_id = MsgIds.MSG_ID_HD_BATTERY_STATUS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_battery_status_sync) + + self.hd_battery_management_timestamp = 0.0 + self.hd_battery_status_timestamp = 0.0 + # values from battery status message + self.RemainingCapacity = 0 + self.BatteryStatus = 0 + self.BatteryChargerStatus = 0 + self.BatteryCommStatus = 0 + + # values from battery manager message + self.RemainingCapacityAlarm = 0 + self.RemainingTimeAlarm = 0 + self.BatteryMode = 0 + self.AtRate = 0 + self.AtRateTimeToFull = 0 + self.AtRateTimeToEmpty = 0 + self.AtRateOK = 0 + self.Temperature = 0 + self.Voltage = 0 + self.Current = 0 + self.AverageCurrent = 0 + self.MaxError = 0 + self.RelativeStateOfCharge = 0 + self.AbsoluteStateOfCharge = 0 + self.FullChargeCapacity = 0 + self.RunTimeToEmpty = 0 + self.AverageTimeToEmpty = 0 + self.AverageTimeToFull = 0 + self.ChargingCurrent = 0 + self.ChargingVoltage = 0 + self.CycleCount = 0 + self.DesignCapacity = 0 + self.DesignVoltage = 0 + self.SpecificationInfo = 0 + self.ManufactureDate = 0 + self.SerialNumber = 0 + self.ManufacturerName = "" + self.DeviceName = "" + self.DeviceChemistry = "" + + def get_battery_remaining_capacity(self): + """ + Gets the remaining capacity + + @return: battery remaining capacity + """ + return self.RemainingCapacity + + @publish(["hd_battery_status_timestamp", 'RemainingCapacity', 'BatteryStatus', 'BatteryChargerStatus', + 'BatteryCommStatus']) + def _handler_battery_status_sync(self, message, timestamp=0.0): + """ + Handles published battery data messages. Battery data are captured + for reference. + + @param message: published battery data message + @return: none + """ + self.RemainingCapacity = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.BatteryStatus = struct.unpack('HH', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.BatteryChargerStatus = struct.unpack('HH', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.BatteryCommStatus = struct.unpack('HH', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.hd_battery_status_timestamp = timestamp + + @publish(['hd_battery_management_timestamp', + 'RemainingCapacityAlarm', 'RemainingTimeAlarm', 'BatteryMode', 'AtRate', 'AtRateTimeToFull', + 'AtRateTimeToEmpty', 'AtRateOK', 'Temperature', 'Voltage', 'Current', 'AverageCurrent', 'MaxError', + 'RelativeStateOfCharge', 'AbsoluteStateOfCharge', 'FullChargeCapacity', 'RunTimeToEmpty', + 'AverageTimeToEmpty', 'AverageTimeToFull', 'ChargingCurrent', 'ChargingVoltage', 'CycleCount', + 'DesignCapacity', 'DesignVoltage', 'SpecificationInfo', 'ManufactureDate', 'SerialNumber', + 'ManufacturerName', 'DeviceName', 'DeviceChemistry']) + def _handler_battery_manager_sync(self, message, timestamp=0.0): + """ + Handles published battery data messages. Battery data are captured + for reference. + + @param message: published battery data message + @return: none + """ + + self.RemainingCapacityAlarm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.RemainingTimeAlarm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.BatteryMode = struct.unpack('HH', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.AtRate = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.AtRateTimeToFull = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.AtRateTimeToEmpty = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.AtRateOK = struct.unpack('HH', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.Temperature = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.Voltage = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.Current = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.AverageCurrent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.MaxError = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.RelativeStateOfCharge = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] + self.AbsoluteStateOfCharge = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14]))[0] + self.FullChargeCapacity = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15]))[0] + self.RunTimeToEmpty = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16]))[0] + self.AverageTimeToEmpty = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17]))[0] + self.AverageTimeToFull = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18]))[0] + self.ChargingCurrent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_19:MsgFieldPositions.END_POS_FIELD_19]))[0] + self.ChargingVoltage = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_20:MsgFieldPositions.END_POS_FIELD_20]))[0] + self.CycleCount = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_21:MsgFieldPositions.END_POS_FIELD_21]))[0] + self.DesignCapacity = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_22:MsgFieldPositions.END_POS_FIELD_22]))[0] + self.DesignVoltage = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_23:MsgFieldPositions.END_POS_FIELD_23]))[0] + self.SpecificationInfo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_24:MsgFieldPositions.END_POS_FIELD_24]))[0] + self.ManufactureDate = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_25:MsgFieldPositions.END_POS_FIELD_25]))[0] + self.SerialNumber = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_26:MsgFieldPositions.END_POS_FIELD_26]))[0] + self.ManufacturerName = struct.unpack('s', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_27:MsgFieldPositions.END_POS_FIELD_27]))[0] + self.DeviceName = struct.unpack('s', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_28:MsgFieldPositions.END_POS_FIELD_28]))[0] + self.DeviceChemistry = struct.unpack('s', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_29:MsgFieldPositions.END_POS_FIELD_29]))[0] + self.hd_battery_management_timestamp = timestamp + + def cmd_battery_remaining_capacity_override(self, mWh: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the battery remaining capacity (in mWh) override \n + command. + Constraints: + Must be logged into HD. + + @param mWh: float - remaining mWh of power in the HD battery to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + pwr = float_to_bytearray(mWh) + payload = rst + pwr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BATTERY_REMAINING_CAP_MWH_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD battery remaining capacity") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mWh) + self.logger.debug("HD battery remaining capacity overridden to " + str_res + " mWh: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_battery_status_override(self, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the battery remaining capacity (in mWh) override \n + command. + BATTERY_PACK_ERROR_BITS = 0x000F + Constraints: + Must be logged into HD. + + @param status: int - the battery comm status as a bit map. + + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BATTERY_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD battery status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(sts) + self.logger.debug("HD battery status overridden to " + str_res + " " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_battery_charger_status_override(self, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the battery charger status override command. + Setting BATTERY_CHARGER_STATUS_AC_PRESENT_MASK to 0 causes the HD to assume battery power. + Constraints: + Must be logged into HD. + + @param status: int - the bit map for the charger status register. + BATTERY_CHARGER_STATUS_AC_PRESENT_MASK = 0x8000 + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BATTERY_CHARGER_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD battery charger status.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(sts) + self.logger.debug("HD battery charger status overridden to " + str_res + " " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_battery_comm_status_override(self, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the battery comm status override command. \n + This overrides the I2C status register. \n + Constraints: + Must be logged into HD. + + @param status: int - the bit map for the i2c status register. + I2C_AL = 0x0001, /* arbitration lost */ + I2C_NACK = 0x0002, /* no acknowledgement */ + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BATTERY_COMM_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD battery comm status.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(sts) + self.logger.debug("HD battery comm status overridden to " + str_res + " " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/blood_flow.py =================================================================== diff -u --- leahi-dialin/hd/blood_flow.py (revision 0) +++ leahi-dialin/hd/blood_flow.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,595 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file blood_flow.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import PUMP_CONTROL_MODE_CLOSED_LOOP, PUMP_CONTROL_MODE_OPEN_LOOP +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDBloodFlow(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for blood-flow related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HD_BloodFlow constructor + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_BLOOD_FLOW_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_flow_sync) + self.hd_blood_flow_timestamp = 0.0 + self.target_blood_flow_rate = 0 + self.measured_blood_flow_rate = 0.0 + self.measured_blood_pump_rotor_speed = 0.0 + self.measured_blood_pump_speed = 0.0 + self.measured_blood_pump_mc_speed = 0.0 + self.measured_blood_pump_mc_current = 0.0 + self.pwm_duty_cycle_pct = 0.0 + self.rotor_count = 0 + self.pres_blood_flow_rate = 0 + self.rotor_hall_state = 0 + + def get_target_blood_flow_rate(self): + """ + Gets the target blood flow rate + + @return: The target blood flow rate + """ + return self.target_blood_flow_rate + + def get_measured_blood_flow_rate(self): + """ + Gets the measured blood flow rate + + @return: the measured blood flow rate + """ + return self.measured_blood_flow_rate + + def get_measured_blood_pump_rotor_speed(self): + """ + Gets the measured blood pump rotor speed + + @return: The measured blood pump rotor speed + """ + return self.measured_blood_pump_rotor_speed + + def get_measured_blood_pump_speed(self): + """ + Gets the measured blood pump speed + + @return: the measured blood pump speed + """ + return self.measured_blood_pump_speed + + def get_measured_blood_pump_motor_controller_speed(self): + """ + Gets the measured blood pump motor controller speed + + @return: The measured blood pump motor controller speed + """ + return self.measured_blood_pump_mc_speed + + def get_measured_blood_pump_motor_controller_current(self): + """ + Gets the measured blood pump motor controller current + + @return: the measured blood pump motor controller current + """ + return self.measured_blood_pump_mc_current + + def get_pwm_duty_cycle_pct(self): + """ + Gets the pwm duty cycle pct + + @return: the pwm duty cycle pct (0..100) + """ + return self.pwm_duty_cycle_pct + + def get_rotor_count(self): + """ + Gets the blood pump rotor count (since cartridge installed) + + @return: the blood pump rotor count + """ + return self.rotor_count + + def get_pres_blood_flow_rate(self): + """ + Gets the prescribed blood flow rate + + @return: the prescribed blood flow rate + """ + return self.pres_blood_flow_rate + + def get_blood_pump_rotor_hall_state(self): + """ + Gets the blood pump rotor hall sensor state. + + @return: the current blood pump rotor hall sensor state (1=home, 0=not home) + """ + return self.rotor_hall_state + + @publish(["hd_blood_flow_timestamp", "target_blood_flow_rate", "measured_blood_flow_rate", "measured_blood_pump_rotor_speed", + "measured_blood_pump_speed", "measured_blood_pump_mc_speed", "measured_blood_pump_mc_current", + "pwm_duty_cycle_pct", "rotor_count", "pres_blood_flow_rate", "rotor_hall_state"]) + def _handler_blood_flow_sync(self, message, timestamp=0.0): + """ + Handles published blood flow data messages. Blood flow data are captured + for reference. + + @param message: published blood flow data message + @return: none + """ + + tgt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rotor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mcspeed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + mccurr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + pwm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + rot = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + pres = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + hal = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + + self.target_blood_flow_rate = tgt[0] + self.measured_blood_flow_rate = flow[0] + self.measured_blood_pump_rotor_speed = rotor[0] + self.measured_blood_pump_speed = speed[0] + self.measured_blood_pump_mc_speed = mcspeed[0] + self.measured_blood_pump_mc_current = mccurr[0] + self.pwm_duty_cycle_pct = pwm[0] + self.rotor_count = rot[0] + self.pres_blood_flow_rate = pres[0] + self.rotor_hall_state = hal[0] + self.hd_blood_flow_timestamp = timestamp + + def cmd_blood_flow_set_point_override(self, flow: int, mode: int = PUMP_CONTROL_MODE_CLOSED_LOOP, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood flow set point override command + Constraints: + Must be logged into HD. + Given flow rate must be valid (<= +/-500 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with (negative for reverse direction) + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood flow set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug( + "Blood flow set point overridden to " + str_res + " mL/min" + str_mode + + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_flow_measured_override(self, flow: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood flow override command + Constraints: + Must be logged into HD. + + @param flow: integer - measured flow (in mL/min) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = float_to_bytearray(flow) + payload = rst + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_MEAS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood flow") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + self.logger.debug("Blood flow (measured)) overridden to " + str_res + " mL/min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_motor_controller_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor controller speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump motor controller speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump MC speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_motor_controller_current_override(self, curr: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor controller current override command + Constraints: + Must be logged into HD. + + @param curr: float - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + self.logger.debug("Blood pump MC current (measured) overridden to " + str_res + " mA: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_motor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_rotor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump rotor speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump rotor speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood flow broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Blood flow broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_stop_blood_pump(self) -> int: + """ + Constructs and sends a blood pump stop request message to the HD. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PUMP_HARD_STOP.value) + + self.logger.debug("Stopping blood pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Stopped blood pump") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_home_blood_pump(self) -> int: + """ + Constructs and sends a blood pump home request message to the HD. + Constraints: + Must be logged into HD. + Blood pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_HOME_CMD.value) + + self.logger.debug("Homing blood pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Blood pump homed : " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_rotor_count_override(self, rot_count: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood pump rotor count override command + Constraints: + Must be logged into HD. + Given count must be zero or positive integer. + + @param rot_count: integer - rotor count to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cnt = integer_to_bytearray(rot_count) + payload = rst + cnt + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_ROTOR_COUNT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood pump rotor count") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(rot_count) + ": " + self.logger.debug("Blood pump rotor count overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_flow_pump_set_pwm(self, pwm_pct: float) -> int: + """ + Constructs and sends a blood flow pump set pwm message to the HD. + Constraints: + Must be logged into HD. + Valid parameters are between 0.10 (stop) and 0.89 (full speed) + + @return: 1 if successful, zero otherwise + """ + pwm = float_to_bytearray(pwm_pct) + payload = pwm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PUMP_SET_PWM.value, + payload=payload) + + self.logger.debug("Setting blood pump PWM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Blood pump set pwm to " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/hd/blood_leak.py =================================================================== diff -u --- leahi-dialin/hd/blood_leak.py (revision 0) +++ leahi-dialin/hd/blood_leak.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,329 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file blood_leak.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Aug-2023 +# @author (original) Peman Montazemi +# @date (original) 15-Apr-2021 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, bytearray_to_byte, bytearray_to_integer, \ + unsigned_short_to_bytearray, byte_to_bytearray + + +@unique +class EmbModeCommands(DialinEnum): + NU = 0 # NULL command + CS = 1 # Control S command (this is handled automatically by using the embedded mode command) + SP = 2 # Set set point command + T = 3 # Self test command + G = 4 # Get self-test drive + I = 5 # Display intensity + V = 6 # Display blood detection level + Z = 7 # Zero command + Q = 8 # Zero confirm command + D = 9 # Display set point + C = 10 # Calibration command + + +@unique +class BloodLeakStates(DialinEnum): + """ + HD blood leak embedded mode state machine states + """ + BLOOD_LEAK_WAIT_FOR_POST_STATE = 0 + BLOOD_LEAK_CHECK_SET_POINT_STATE = 1 + BLOOD_LEAK_INIT_STATE = 2 + BLOOD_LEAK_CHECK_ZERO_AND_SELF_TEST_STATE = 3 + BLOOD_LEAK_NORMAL_STATE = 4 + BLOOD_LEAK_RECOVER_BLOOD_DETECT_STATE = 5 + NUM_OF_BLOOD_LEAK_STATES = 6 + + +class HDBloodLeak(AbstractSubSystem): + """ + HDBloodLeak + + Hemodialysis Delivery (HD) Dialin API sub-class for blood leak related commands. + """ + + # Blood leak detector status + BLOOD_LEAK_DETECTED = 0 # Blood detected + NO_BLOOD_LEAK_DETECTED = 1 # No blood detected + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_leak_sync) + + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_BLOOD_LEAK_EMB_MODE_RESPONSE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_leak_emb_mode_cmd_resp) + + self.hd_blood_leak_status_timestamp = 0.0 + self.hd_blood_leak_emb_mode_response_timestamp = 0.0 + self.blood_leak_status = self.NO_BLOOD_LEAK_DETECTED + self.blood_leak_state = BloodLeakStates.BLOOD_LEAK_INIT_STATE.value + self.blood_leak_emb_mode_cmds = dict() + self.blood_leak_error_persistent_ctr = 0 + self.blood_leak_serial_comm_state = 0 + + for cmd in EmbModeCommands.__members__: + # Initialize all the embedded mode commands + self.blood_leak_emb_mode_cmds[cmd] = '' + + def get_blood_leak_status(self): + """ + Gets the current blood leak status + + @return: List containing blood leak status: [detected, undetected] + """ + return self.blood_leak_status + + def get_blood_leak_state(self): + """ + Gets the current blood leak state + (0: wait for POST, 1: check set point, 2: init, + 3: zero and self test, 4: normal, 5: recover blood detect state ) + + @return: integer - blood leak state + """ + return self.blood_leak_state + + def get_blood_leak_emb_mode_command_response(self, emb_mod_cmd: int) -> str: + """ + Gets the most recent embedded mode command response for a given command + + @param emb_mod_cmd the command to get its response + + @return: string - embedded mode command response + """ + if emb_mod_cmd < len(EmbModeCommands): + return self.blood_leak_emb_mode_cmds[EmbModeCommands(emb_mod_cmd).name] + else: + self.logger.debug("Invalid command!") + + @publish(['hd_blood_leak_status_timestamp', 'blood_leak_status', 'blood_leak_state', + 'blood_leak_error_persistent_ctr', 'blood_leak_serial_comm_state']) + def _handler_blood_leak_sync(self, message, timestamp=0.0): + """ + Handles published blood leak status messages. Blood leak status is captured + for reference. + + @param message: published blood leak status message + @return: None + """ + self.blood_leak_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.blood_leak_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.blood_leak_error_persistent_ctr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.blood_leak_serial_comm_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.hd_blood_leak_status_timestamp = timestamp + + def cmd_blood_leak_detector_override(self, detected: int, reset=NO_RESET): + """ + Constructs and sends the blood leak detector state override command + Constraints: + Must be logged into HD. + + @param detected: unsigned int - detected (0=detected, 1=undetected) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override blood leak detector state value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_leak_zero_request(self): + """ + Request blood leak zeroing + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_ZERO_REQUEST.value) + + self.logger.debug("Request blood leak zeroing") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_leak_data_broadcast_interval_override(self, ms, reset=NO_RESET): + """ + Constructs and sends the blood leak data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD blood leak data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Blood leak data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_leak_set_to_embedded_mode(self): + """ + Constructs and sends switching to embedded mode command + Constraints: + Must be logged into HD. + + @return: non-zero integer if successful, False otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_BLOOD_LEAK_2_EMB_MODE.value) + + self.logger.debug("Setting the blood leak to embedded mode") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_leak_set_embedded_mode_command(self, command: int, msg_payload: int = None): + """ + Constructs and sends switching to embedded mode command + Constraints: + Must be logged into HD. + It is recommended to have delays in between sending each command to make sure the firmware's queue is not + overflown. + The sensor must be in embedded mode. The firmware sets the sensor into the embedded mode by default. + + @return: non-zero integer if successful, False otherwise + """ + if command < len(EmbModeCommands): + command_bytes = byte_to_bytearray(command) + self.blood_leak_emb_mode_cmds[EmbModeCommands(command).name] = '' + data = 0 + + if msg_payload is not None: + data = msg_payload + + data = unsigned_short_to_bytearray(data) + payload = command_bytes + data + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_BLOOD_LEAK_EMB_MODE_COMMAND.value, + payload=payload) + + self.logger.debug("Sending " + str(EmbModeCommands(command).name) + " to the blood leak sensor") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + else: + self.logger.debug("Invalid command!") + + @publish(['hd_blood_leak_emb_mode_response_timestamp', 'blood_leak_emb_mode_cmd_response']) + def _handler_blood_leak_emb_mode_cmd_resp(self, message, timestamp=0.0): + """ + Handles published blood leak status messages. Blood leak status is captured + for reference. + + @param message: published blood leak status message + @return: None + """ + # Clear the variable for the next read + blood_leak_emb_mode_cmd_response = '' + payload = message['message'] + index = MsgFieldPositions.START_POS_FIELD_1 + cmd, index = bytearray_to_byte(payload, index, False) + length, index = bytearray_to_integer(payload, index, False) + + for i in range(0, length): + # Loop through the length and get the + char, char_index = bytearray_to_byte(payload, index + i, False) + blood_leak_emb_mode_cmd_response += chr(char) + + self.blood_leak_emb_mode_cmds[EmbModeCommands(cmd).name] = blood_leak_emb_mode_cmd_response + self.hd_blood_leak_emb_mode_response_timestamp = timestamp Index: leahi-dialin/hd/buttons.py =================================================================== diff -u --- leahi-dialin/hd/buttons.py (revision 0) +++ leahi-dialin/hd/buttons.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,187 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file buttons.py +# +# @author (last) Micahel Garthwaite +# @date (last) 18-Aug-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray, byte_to_bytearray + + +class HDButtons(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for button related commands. + """ + + def __init__(self, can_interface: DenaliCanMessenger, logger: Logger): + """ + HD_Buttons constructor + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.poweroff_timeout_expired = False + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, + MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + self._handler_poweroff_timeout_occurred) + self.hd_power_off_timestamp = 0.0 + + def get_power_timeout_expired(self): + """ + Gets the poweroff timeout expired status + + @return: True if expired, False otherwise + """ + + return self.poweroff_timeout_expired + + def reset_poweroff_timeout_expired(self): + """ + Resets the leahi-dialin poweroff timeout flag to False + + @return: None + """ + + self.poweroff_timeout_expired = False + + @publish(['hd_power_off_timestamp', "poweroff_timeout_expired"]) + def _handler_poweroff_timeout_occurred(self, message, timestamp=0.0): + """ + Poweroff timeout occurred handler + + @param message: The poweroff timeout occurred message string + @return: None + """ + if len(message["message"]) < 16: + self.logger.debug("Poweroff message id detected, but was the wrong length.") + return + + mode = struct.unpack('h', bytearray( + message["message"][MsgFieldPositions.START_POS_FIELD_1: MsgFieldPositions.START_POS_FIELD_1 + 2])) + + if len(mode) > 0: + self.poweroff_timeout_expired = bool(mode[0]) + self.hd_power_off_timestamp = timestamp + + def cmd_off_button_override(self, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the Off button override command + See HD/App/Controllers/Buttons.c + Constraints: + Must be logged into HD. + Given state must be a 0 or 1. + + @param state: integer - 1 to override off button to "pressed", 0 to "released" + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + payload = rst + sta + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override off button") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("pressed" if state != 0 else "released") + + self.logger.debug("Off button overridden to " + str_res + ":" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_stop_button_override(self, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the Stop button override command + See HD/App/Controllers/Buttons.c + Constraints: + Must be logged into HD. + Given state must be a 0 or 1. + + @param state: integer - 1 to override stop button to "pressed", 0 to "released" + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + payload = rst + sta + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_STOP_BUTTON_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override stop button") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("pressed" if state != 0 else "released") + self.logger.debug("Stop button overridden to " + str_res + ":" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + + # response payload is OK or not OK + return received_message["message"][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_send_hd_power_off_response(self, usr_response: int ) -> None: + """ + Constructs and sends HD user response to power off request to the UI. + + OFF_BUTTON_RSP_USER_REQUESTS_POWER_OFF = 0, ///< User requests power off response + OFF_BUTTON_RSP_USER_CONFIRMS_POWER_OFF = 1, ///< User confirms power off response + OFF_BUTTON_RSP_USER_REJECTS_POWER_OFF = 2, ///< User rejects power off response + NUM_OF_OFF_BUTTON_RSPS ///< Number of off button responses from UI + + @param usr_response: (int) - user response defined in enum + @return: none + """ + payload = byte_to_bytearray(usr_response) + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + payload=payload) + # No ACK required + self.can_interface.send(message, 0) + return True + Index: leahi-dialin/hd/calibration_record.py =================================================================== diff -u --- leahi-dialin/hd/calibration_record.py (revision 0) +++ leahi-dialin/hd/calibration_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,523 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file calibration_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 03-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsHD +from ..utils.conversions import integer_to_bytearray + + +class HDCalibrationNVRecord(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for calibration_record commands. + """ + + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_HIGH_ORDER_GAIN_VALUE = 0 + _DEFAULT_GAIN_VALUE = 1 + _DEFAULT_OFFSET_VALUE = 0 + _DEFAULT_RATIO_VALUE = 1 + _DEFAULT_VOLUME_VALUE = 0 + _DEFAULT_CONCENTRATE_VALUE = 1 + _DEFAULT_CALIBRATION_VALUE = 1 + _DEFAULT_BLOOD_LEAK_SET_POINT = 20 + _DEFAULT_HEPARIN_PUMP_VOLTAGE = 0.15 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + _FIRMWARE_STACK_NAME = 'HD' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_cal_in_progress = False + self._cal_data = 0 + self._raw_cal_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # HD Calibration main record + self.hd_calibration_record = self._prepare_hd_calibration_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_CALIBRATION_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_calibration_sync) + + def cmd_reset_hd_calibration_record(self) -> bool: + """ + Handles resetting HD calibration record. + + @return: True if successful, False otherwise + """ + self.hd_calibration_record = self._prepare_hd_calibration_record() + self.hd_calibration_record = self._utilities.reset_fw_record(self.hd_calibration_record) + status = self.cmd_set_hd_calibration_record(self.hd_calibration_record) + + return status + + def cmd_get_hd_calibration_record_report(self, report_destination: str = None): + """ + Handles getting HD calibration_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.CAL_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + + observer = NVUtilsObserver("hd_calibration_record") + # Attach the observer to the list + self.attach(observer) + # Request the HD calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_hd_calibration_record() + + while not observer.received: + sleep(0.1) + # Pass the HD calibration record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.hd_calibration_record) + + def cmd_request_hd_calibration_record(self) -> int: + """ + Handles getting HD calibration_record record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_cal_in_progress is not True: + self._is_getting_cal_in_progress = True + # Clear the list for the next call + self._raw_cal_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_CALIBRATION_RECORD.value) + + self.logger.debug('Getting HD calibration record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_hd_calibration_record_crc_override(self, crc: int) -> bool: + """ + Handles setting HD calibration_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + hd_record = integer_to_bytearray(NVRecordsHD.NVDATAMGMT_CALIBRATION_RECORD.value) + payload = reset_byte_array + crc_value + hd_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding HD calibration record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_hd_calibration_sync(self, message, timestamp=0.0): + """ + Handles published HD calibration_record record messages. DG calibration_record records are captured for + processing and updating the HD calibration_record record. + + @param message: published HD calibration_record record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = self._RECORD_START_INDEX + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the calibration_record data only + self._cal_data = message['message'][self._RECORD_START_INDEX:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_cal_record += (message['message'][self._RECORD_START_INDEX + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_cal_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_calibration_record, self._raw_cal_record) + self._handler_received_complete_hd_calibration_record() + + self.hd_cal_record_timestamp = timestamp + + @publish(["hd_calibration_record"]) + def _handler_received_complete_hd_calibration_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete hd calibration record.") + + def cmd_set_hd_calibration_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the calibration data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written to excel + + @return: none + """ + + observer = NVUtilsObserver("hd_calibration_record") + # Attach the observer to the list + self.attach(observer) + # Request the HD calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_hd_calibration_record() + + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_calibration_record, report_address, + self._utilities.CAL_RECORD_TAB_NAME) + + ret = self.cmd_set_hd_calibration_record(self.hd_calibration_record) + return ret + + def cmd_set_hd_calibration_record(self, hd_calibration_record: OrderedDict) -> bool: + """ + Handles updating the HD calibration_record record with the newest calibration_record data of a hardware and + sends it to FW. + + @param hd_calibration_record: (OrderedDict) the hd calibration record to be sent + @return: none + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_calibration_record) + + self.logger.debug("Setting HD calibration record") + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_CALIBRATION_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("HD ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending HD calibration record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending HD calibration record.") + return True + + def _prepare_hd_calibration_record(self): + """ + Handles assembling the sub dictionaries of each hardware group to make the main HD calibration_record record. + + @return: (OrderedDict) the assembled hd calibration record + """ + result = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_pumps_record(), self._prepare_valves_record(), + self._prepare_occlusion_sensors_record(), self._prepare_pressure_sensors_record(), + self._prepare_temperature_sensors_record(), self._prepare_heparin_force_sensor_record(), + self._prepare_accelerometer_sensor_record(), self._prepare_blood_leak_sensor_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' int: + """ + Constructs and sends the dialysate flow set point override command + Constraints: + Must be logged into HD. + Given flow must be valid (<= +/-600 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate flow set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug( + "Dialysate flow set point overridden to " + str_res + " mL/min" + str_mode + + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_flow_measured_override(self, flow: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate flow override command + Constraints: + Must be logged into HD. + + @param flow: integer - measured flow (in mL/min) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = float_to_bytearray(flow) + payload = rst + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_MEAS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate flow") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + self.logger.debug("Dialysate flow (measured)) overridden to " + str_res + " mL/min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_measured_motor_controller_speed_override(self, speed: int, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor controller speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump motor controller speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate pump MC speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_measured_motor_controller_current_override(self, curr: int, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor current override command + Constraints: + Must be logged into HD. + + @param curr: integer - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + self.logger.debug("Dialysate inlet pump MC current (measured) overridden to " + str_res + " mA: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_measured_motor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate inlet pump speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_measured_rotor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump rotor speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate inlet pump rotor speed (measured) overridden to " + str_res + " RPM: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate inlet flow broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Dialysate inlet flow broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_stop_dialysate_inlet_pump(self) -> int: + """ + Constructs and sends a dialysate inlet pump stop request message to the HD. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIAL_IN_PUMP_HARD_STOP.value) + + self.logger.debug("Stopping dialysate inlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Stopped dialysate inlet pump") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_home_dialysate_inlet_pump(self) -> int: + """ + Constructs and sends a dialysate inlet pump home request message to the HD. + Constraints: + Must be logged into HD. + Dialysate inlet pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_HOME_CMD.value) + + self.logger.debug("Homing dialysate inlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate inlet pump homed : " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_rotor_count_override(self, rot_count: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the dialysate inlet pump rotor count override command + Constraints: + Must be logged into HD. + Given count must be zero or positive integer. + + @param rot_count: integer - rotor count to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cnt = integer_to_bytearray(rot_count) + payload = rst + cnt + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIALYSATE_INLET_PUMP_ROTOR_COUNT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate inlet pump rotor count") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(rot_count) + ": " + self.logger.debug("Dialysate inlet pump rotor count overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_set_pwm(self, pwm_pct: float) -> int: + """ + Constructs and sends a dialysate inlet pump set pwm message to the HD. + Constraints: + Must be logged into HD. + Valid parameters are between 0.10 (stop) and 0.89 (full speed) + + @return: 1 if successful, zero otherwise + """ + pwm = float_to_bytearray(pwm_pct) + payload = pwm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIAL_IN_SET_PWM.value, + payload=payload) + + self.logger.debug("Setting dialysate inlet pump PWM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate inlet pump set pwm to " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/hd/dialysate_outlet_flow.py =================================================================== diff -u --- leahi-dialin/hd/dialysate_outlet_flow.py (revision 0) +++ leahi-dialin/hd/dialysate_outlet_flow.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,703 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dialysate_outlet_flow.py +# +# @author (last) Vinayakam Mani +# @date (last) 14-Dec-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import PUMP_CONTROL_MODE_CLOSED_LOOP, PUMP_CONTROL_MODE_OPEN_LOOP +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDDialysateOutletFlow(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for dialysate outlet pump related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDDialysateFlow constructor + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DIALYSATE_OUT_FLOW_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dialysate_outlet_flow_sync) + + self.reference_dialysate_outlet_uf_volume = 0.0 + self.measured_dialysate_outlet_uf_volume = 0.0 + self.measured_dialysate_outlet_pump_rotor_speed = 0.0 + self.measured_dialysate_outlet_pump_speed = 0.0 + self.measured_dialysate_outlet_pump_mc_speed = 0.0 + self.measured_dialysate_outlet_pump_mc_current = 0.0 + self.pwm_duty_cycle_pct = 0.0 + self.dialysate_outlet_pump_corr_offset = 0.0 + self.dialysate_outlet_pump_calc_rate = 0.0 + self.uf_calculated_rate = 0.0 + self.rotor_hall_state = 0 + self.uf_set_rate = 0.0 + self.dialysate_outlet_pump_state = 0 + self.hd_dial_outlet_flow_timestamp = 0.0 + + + def get_reference_dialysate_outlet_uf_volume(self): + """ + Gets the reference dialysate outlet uf volume + + @return: the reference dialysate outlet uf volume + """ + return self.reference_dialysate_outlet_uf_volume + + def get_measured_dialysate_outlet_uf_volume(self): + """ + Gets the measured dialysate outlet uf volume + + @return: the measured dialysate outlet uf volume + """ + return self.measured_dialysate_outlet_uf_volume + + def get_measured_dialysate_outlet_pump_rotor_speed(self): + """ + Gets the measured dialysate outlet pump rotor speed + @return: the measured dialysate outlet pump rotor speed + """ + return self.measured_dialysate_outlet_pump_rotor_speed + + def get_measured_dialysate_outlet_pump_speed(self): + """ + Gets the measured dialysate outlet pump speed + @return: the measured dialysate outlet pump speed + """ + return self.measured_dialysate_outlet_pump_speed + + def get_measured_dialysate_outlet_pump_motor_controller_speed(self): + """ + Gets the measured dialysate outlet pump motor controller speed + + @return: the measured dialysate outlet pump motor controller speed + """ + return self.measured_dialysate_outlet_pump_mc_speed + + def get_measured_dialysate_outlet_pump_motor_controller_current(self): + """ + Gets the measured dialysate outlet pump motor controller current + + @return: Gets the measured dialysate outlet pump motor controller current + """ + return self.measured_dialysate_outlet_pump_mc_current + + def get_pwm_duty_cycle_pct(self): + """ + Gets the pwm duty cycle percent + + @return: the pwm duty cycle percent + """ + return self.pwm_duty_cycle_pct + + def get_dial_outlet_pump_rotor_hall_state(self): + """ + Gets the dialysate outlet pump rotor hall sensor state. + + @return: the current dialysate outlet pump rotor hall sensor state (1=home, 0=not home) + """ + return self.rotor_hall_state + + def get_uf_set_rate(self): + """ + Gets the prescribed UF set rate. + + @return: the prescribed UF set rate + """ + return self.uf_set_rate + + def get_dial_outlet_pump_state(self): + """ + Gets the dialysate outlet pump state. + + @return: the current dialysate outlet pump state (0 = Pump Off, 1 = Ramp up, 2 = Ramp down, 3 = Control to Target) + """ + return self.dialysate_outlet_pump_state + + + @publish([ + "hd_dial_outlet_flow_timestamp", + "reference_dialysate_outlet_uf_volume", + "measured_dialysate_outlet_uf_volume", + "measured_dialysate_outlet_pump_rotor_speed", + "measured_dialysate_outlet_pump_speed", + "measured_dialysate_outlet_pump_mc_speed", + "measured_dialysate_outlet_pump_mc_current", + "dialysate_outlet_pump_corr_offset", + "dialysate_outlet_pump_calc_rate", + "uf_calculated_rate", + "pwm_duty_cycle_pct", + "rotor_hall_state", + "uf_set_rate", + "dialysate_outlet_pump_state" + ]) + def _handler_dialysate_outlet_flow_sync(self, message, timestamp=0.0): + """ + Handles published dialysate outlet flow data messages. Dialysate flow data are captured + for reference. + + @param message: published dialysate outlet flow data message + @return: none + """ + + refvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + measvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rotor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mcspeed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + mccurr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + pwm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + dopcoffset = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + dopcrate = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + ufcrate = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + hal = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + ufsrate = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + dopstate = struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + + self.reference_dialysate_outlet_uf_volume = refvol[0] + self.measured_dialysate_outlet_uf_volume = measvol[0] + self.measured_dialysate_outlet_pump_rotor_speed = rotor[0] + self.measured_dialysate_outlet_pump_speed = speed[0] + self.measured_dialysate_outlet_pump_mc_speed = mcspeed[0] + self.measured_dialysate_outlet_pump_mc_current = mccurr[0] + self.pwm_duty_cycle_pct = pwm[0] + self.dialysate_outlet_pump_corr_offset = dopcoffset[0] + self.dialysate_outlet_pump_calc_rate = dopcrate[0] + self.uf_calculated_rate = ufcrate[0] + self.rotor_hall_state = hal[0] + self.uf_set_rate = ufsrate[0] + self.dialysate_outlet_pump_state = dopstate[0] + self.hd_dial_outlet_flow_timestamp = timestamp + + def cmd_dialysate_outlet_flow_set_point_override(self, + flow: int, + mode: int = PUMP_CONTROL_MODE_CLOSED_LOOP, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the dialysate outlet pump set point override command + Constraints: + Must be logged into HD. + Given flow must be valid (<= +/-600 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate outlet pump set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == RESET: + str_res = "reset back to stopped" + else: + str_res = str(flow) + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug("Dialysate outlet pump set point overridden to " + + str_res + + " mL/min" + + str_mode + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_reference_uf_volume_override(self, refvol: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UF reference volume override command + Constraints: + Must be logged into HD. + + @param refvol: float - reference UF volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(refvol) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_UF_REF_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override UF reference volume with " + str(refvol) + "mL.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(refvol) + " mL. " + self.logger.debug( + "UF reference volume " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_measured_uf_volume_override(self, measvol: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured UF volume override command + Constraints: + Must be logged into HD. + + @param measvol: float - measured UF volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(measvol) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_UF_MEAS_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured UF volume with " + str(measvol) + " mL.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(measvol) + " mL. " + self.logger.debug("UF measured volume " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_controller_speed_override(self, speed: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor controller measured speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump motor controller speed to " + str(spd) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump MC speed (measured) " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_controller_current_override(self, curr: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor current override command + Constraints: + Must be logged into HD. + + @param curr: float - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(curr) + " mA. " + self.logger.debug("Dialysate outlet pump MC current (measured) " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_speed_override(self, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump speed to " + str(speed) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump speed (measured) " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_rotor_speed_override(self, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump rotor speed to " + str(speed) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump rotor speed (measured) " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate outlet flow broadcast interval to " + str(ms) + " ms.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(ms) + " ms. " + self.logger.debug("Dialysate outlet flow broadcast interval " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_load_cell_weight_override(self, sensor: int, weight: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured load cell weight override command. + Constraints: + Must be logged into HD. + Given sensor must be one of the sensors listed below. + + @param sensor: integer - ID of load cell to override + @param weight: float - weight (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Load Cells: \n + 0 = reservoir 1 primary \n + 1 = reservoir 1 backup \n + 2 = reservoir 2 primary \n + 3 = reservoir 2 backup \n + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(weight) + sen = integer_to_bytearray(sensor) + payload = rst + spd + sen + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_LOAD_CELL_WEIGHT_OVERRIDE.value, + payload=payload) + + self.logger.debug( + "override measured load cell weight to " + str(weight) + " grams for load cell # " + str(sensor)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(weight) + " grams. " + self.logger.debug("Load cell # " + str(sensor) + " " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_load_cell_weights(self, r1p: float, r1b: float, r2p: float, r2b: float) -> int: + """ + Constructs and sends the set load cell weights command. + + @param r1p: float - weight (in g) for reservoir 1 primary load cell + @param r1b: float - weight (in g) for reservoir 1 backup load cell + @param r2p: float - weight (in g) for reservoir 2 primary load cell + @param r2b: float - weight (in g) for reservoir 2 backup load cell + @return: 0 - no response will come from HD for this message + """ + + r1pb = float_to_bytearray(r1p) + r1bb = float_to_bytearray(r1b) + r2pb = float_to_bytearray(r2p) + r2bb = float_to_bytearray(r2b) + payload = r1pb + r1bb + r2pb + r2bb + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_READINGS_DATA.value, + payload=payload) + + self.logger.debug("measured load cell weights set.") + + # Send message + self.can_interface.send(message, 0) + + return 0 + + def cmd_stop_dialysate_outlet_pump(self) -> int: + """ + Constructs and sends a dialysate outlet pump stop request message to the HD. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIAL_OUT_PUMP_HARD_STOP.value) + + self.logger.debug("Stopping dialysate outlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Stopped dialysate outlet pump") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_home_dialysate_outlet_pump(self) -> int: + """ + Constructs and sends a dialysate outlet pump home request message to the HD. + Constraints: + Must be logged into HD. + Dialysate outlet pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_HOME_CMD.value) + + self.logger.debug("Homing dialysate outlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate outlet pump homed : " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_set_pwm(self, pwm_pct: float) -> int: + """ + Constructs and sends a dialysate outlet pump set pwm message to the HD. + Constraints: + Must be logged into HD. + Valid parameters are between 0.10 (stop) and 0.89 (full speed) + + @return: 1 if successful, zero otherwise + """ + pwm = float_to_bytearray(pwm_pct) + payload = pwm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIAL_OUT_SET_PWM.value, + payload=payload) + + self.logger.debug("Setting dialysate outlet pump PWM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate outlet pump set pwm to " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/hd/fans.py =================================================================== diff -u --- leahi-dialin/hd/fans.py (revision 0) +++ leahi-dialin/hd/fans.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,257 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file fans.py +# +# @author (last) Michael Garthwaite +# @date (last) 20-Sep-2023 +# @author (original) Dara Navaei +# @date (original) 04-Aug-2021 +# +############################################################################ + + +import struct +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import (DenaliMessage, DenaliChannels) +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from logging import Logger +from enum import unique + + +@unique +class HDFansNames(DialinEnum): + + FAN_INLET_1 = 0 + + +class HDFans(AbstractSubSystem): + """ + @brief Hemodialysis Device (HD) Dialin API sub-class for fans related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_FANS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_fans_sync) + + # Publish variables + self.duty_cycle = 0.0 + self.target_rpm = 0.0 + self.inlet_1_rpm = 0.0 + self.rpm_alarm_time = 0 + self.hd_fans_data_timestamp = 0.0 + + self.remove = 0 + + def get_fans_target_duty_cycle(self): + """ + Gets the fans target duty cycle + + @return: Fans target duty cycle + """ + return self.duty_cycle + + def get_fan_inlet_1_rpm(self): + """ + Gets the inlet 1 fan RPM + + @return: Fan inlet 1 RPM + """ + return self.inlet_1_rpm + + def get_fan_target_rpm(self): + """ + Gets the fans target RPM + + @return: target RPM + """ + return self.target_rpm + + def get_fans_time_left_to_rpm_alarm(self): + """ + Gets the fans time left to RPM alarm + + @return: Fans time left to RPM alarm + """ + return self.rpm_alarm_time + + @publish(["hd_fans_data_timestamp", 'duty_cycle', 'target_rpm', 'inlet_1_rpm', 'rpm_alarm_time']) + def _handler_fans_sync(self, message, timestamp=0.0): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + self.duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.target_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.inlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.rpm_alarm_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.hd_fans_data_timestamp = timestamp + + def cmd_fans_rpm_override(self, fan: int, rpm: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD fan RPM override command + Constraints: + Must be logged into HD. + + @param fan: (int) fan ID that is status is overridden + @param rpm: (int) RPM that the fan will be overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + f = integer_to_bytearray(fan) + r = float_to_bytearray(rpm) + payload = reset_value + r + f + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fan RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Fan " + str(HDFansNames(fan).name) + " to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_rpm_alarm_start_time_offset(self, time: int) -> int: + """ + Constructs and sends the HD fan RPM alarm start time offset command + Constraints: + Must be logged into HD. + + @param time: (int) time offset in milliseconds + @return: 1 if successful, zero otherwise + """ + payload = integer_to_bytearray(time) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_FANS_RPM_ALARM_START_TIME_OFFSET.value, + payload=payload) + + self.logger.debug("Override fan RPM alarm start time offset") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug("RPM alarm start time offset set to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fans data publish interval. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Fans data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fans_duty_cycle_override(self, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD fans duty cycle override command + Constraints: + Must be logged into HD. + + @param duty_cycle: (float) the duty cycle that the fans are overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + dc = float_to_bytearray(duty_cycle / 100.0) + payload = reset_value + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans duty cycle") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set fans duty cycle to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/fluid_leak.py =================================================================== diff -u --- leahi-dialin/hd/fluid_leak.py (revision 0) +++ leahi-dialin/hd/fluid_leak.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,153 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file fluid_leak.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peman Montazemi +# @date (original) 10-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class HDFluidLeak(AbstractSubSystem): + """ + HDFluidLeak + + Hemodialysis Delivery (HD) Dialin API sub-class for fluid leak related commands. + """ + + # Fluid leak detector state + FLUID_LEAK_DETECTED_STATE = 0 # Wet + NO_FLUID_LEAK_DETECTED_STATE = 1 # Dry + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_FLUID_LEAK_STATE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_fluid_leak_sync) + + self.fluid_leak_state = self.NO_FLUID_LEAK_DETECTED_STATE + self.hd_fluid_leak_timestamp = 0.0 + + def get_fluid_leak_state(self): + """ + Gets the current fluid leak state + + @return: List containing fluid leak states: [detected, undetected] + """ + return self.fluid_leak_state + + @publish(["hd_fluid_leak_timestamp", "fluid_leak_state"]) + def _handler_fluid_leak_sync(self, message, timestamp=0.0): + """ + Handles published fluid leak state messages. Fluid leak state is captured + for reference. + + @param message: published fluid leak state message + @return: None + """ + + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.fluid_leak_state = state[0] + self.hd_fluid_leak_timestamp = timestamp + + def cmd_fluid_leak_detector_override(self, detected_state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak detector state override command + Constraints: + Must be logged into HD. + Given detector must be one of the detectors listed below. + + @param detected_state: unsigned int - detected (0=wet, 1=dry) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected_state) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FLUID_LEAK_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fluid leak detector state value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_fluid_leak_state_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak state broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD priority task interval (10 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FLUID_LEAK_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD fluid leak state broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Fluid leak state broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/hd_events.py =================================================================== diff -u --- leahi-dialin/hd/hd_events.py (revision 0) +++ leahi-dialin/hd/hd_events.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,304 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_events.py +# +# @author (last) Micahel Garthwaite +# @date (last) 29-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 12-Nov-2021 +# +############################################################################ + +import struct +from logging import Logger +from ..common import * +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from datetime import datetime +from time import time + + +class HDEvents(AbstractSubSystem): + """ + Hemodialysis (HD) Dialin API sub-class for events related commands. + """ + UNKNOWN_STATE = "UNKNOWN_PREVIOUS_STATE" + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_ui_ch_id + msg_id = MsgIds.MSG_ID_HD_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_events_sync) + + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_op_mode_sync) + + # Define the dictionaries + self._hd_event_dictionary = dict() + self._hd_event_data_type = dict() + self.hd_event_timestamp = 0.0 + self.hd_event_op_mode_timestamp = 0.0 + + # Dictionary of the mode as key and the sub mode states enum class as the value + self._hd_op_mode_2_sub_mode = {HDOpModes.MODE_FAUL.name: HDFaultStates, + HDOpModes.MODE_INIT.name: HDInitStates, + HDOpModes.MODE_STAN.name: HDStandbyStates, + HDOpModes.MODE_TPAR.name: TreatmentParametersStates, + HDOpModes.MODE_PRET.name: PreTreatmentSubModes, + HDOpModes.MODE_TREA.name: TreatmentStates, + HDOpModes.MODE_POST.name: PostTreatmentStates} + + # Loop through the list of the HD events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in HDEventList: + self._hd_event_dictionary[HDEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in HDEventDataType: + event_data_type = HDEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._hd_event_data_type[event_data_type] = struct_unpack_type + + def get_hd_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested HD event + + @param event_id the ID of the HD event types (i.e. HD_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested HD event number + """ + list_length = len(self._hd_event_dictionary[HDEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._hd_event_dictionary[HDEventList(event_id).name][list_length - 1] + else: + event = self._hd_event_dictionary[HDEventList(event_id).name][list_length - event_number - 1] + + return event + + def clear_hd_event_list(self): + """ + Clears the HD event list + + @returns none + """ + for key in self._hd_event_dictionary: + self._hd_event_dictionary[key].clear() + + def get_hd_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain HD event ID + + @param event_id the ID of the HD event types (i.e. HD_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested HD event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._hd_event_dictionary[HDEventList(event_id).name]) <= number_of_events: + list_of_events = self._hd_event_dictionary[HDEventList(event_id).name] + else: + # Get the all the events + complete_list = self._hd_event_dictionary[HDEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._hd_event_dictionary[HDEventList(event_id).name] + + return list_of_events + + @publish(["hd_event_timestamp", '_hd_event_dictionary']) + def _handler_events_sync(self, message, timestamp=0.0): + """ + Handles published events message + + @param message: published HD events data message + @returns none + """ + event_data_1 = 0 + event_data_2 = 0 + op_mode = 0 + sub_mode = 0 + sub_state = 0 + fourth_state = 0 + current_sub_tuple = [] + + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if event_id == HDEventList.HD_EVENT_OPERATION_STATUS.value: + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + struct_data_type = self._hd_event_data_type[HDEventDataType(event_data_type_1).name] + op_mode = struct.unpack(' current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + if previous_op_mode != HDEvents.UNKNOWN_STATE: + previous_sub_mode_enum_class = self._hd_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + # Unknown previous state. Display value instead of name. + else: + event_data_1 = str(event_data_1) + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._hd_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + event_tuple = (datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + elif event_state_name == HDEventList.HD_EVENT_OPERATION_STATUS.name: + event_tuple = (time(), op_mode, sub_mode, sub_state, fourth_state) + + # Update event dictionary + self._hd_event_dictionary[event_state_name].append(event_tuple) + self.hd_event_timestamp = timestamp + + @publish(["hd_event_op_mode_timestamp", "hd_event_op_mode", "hd_event_sub_mode"]) + def _handler_hd_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published HD operation mode messages. Current HD operation mode + is captured for reference. + + @param message: published HD operation mode broadcast message + @return: None + """ + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.hd_event_op_mode = mode[0] + self.hd_event_sub_mode = smode[0] + self.hd_event_op_mode_timestamp = timestamp Index: leahi-dialin/hd/hd_test_configs.py =================================================================== diff -u --- leahi-dialin/hd/hd_test_configs.py (revision 0) +++ leahi-dialin/hd/hd_test_configs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,197 @@ +########################################################################### +# +# Copyright (c) 2023-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_test_configs.py +# +# @author (last) Dara Navaei +# @date (last) 26-May-2023 +# @author (original) Dara Navaei +# @date (original) 02-May-2023 +# +############################################################################ + + +from logging import Logger +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.test_config_defs import HDTestConfigOptions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from .constants import NO_RESET +from ..utils.conversions import integer_to_bytearray, bytearray_to_integer +from ..utils.base import AbstractSubSystem, publish + + +class HDTestConfig(AbstractSubSystem): + """ + + Hemodialysis (HD) Dialin API sub-class for setting and getting the test configurations. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self.hd_test_configs = dict() + self.hd_test_configs_response_timestamp = 0.0 + + self._reset_test_configs_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_TEST_CONFIGURATION.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_test_config_sync) + + def cmd_set_recover_from_mode_fault_signal(self): + """ + Constructs and sends the HD test config the signal to recover from mode fault + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SIGNAL_RECOVER_FROM_FAULT_MODE.value) + + self.logger.debug("Setting signal to recover from mode fault") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_get_test_config_status(self, config: int): + """ + Returns the status of a test config + + @param config: (int) Test config to set + @return: the status of a test config + """ + return self.hd_test_configs[HDTestConfigOptions(config).name] + + def cmd_set_test_config(self, config: int, reset: int = NO_RESET): + """ + Constructs and sends the HD test config + Constraints: + Must be logged into HD. + + @param config: (int) Test config to set + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + c = integer_to_bytearray(config) + payload = reset_value + c + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_TEST_CONFIGURATION.value, + payload=payload) + + if reset == NO_RESET: + self.logger.debug("Setting {}".format(HDTestConfigOptions(config).name)) + else: + self.logger.debug("Resetting {}".format(HDTestConfigOptions(config).name)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_request_test_config_status_from_fw(self): + """ + Constructs and sends the HD test configs request + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_TEST_CONFIGURATION.value) + + self.logger.debug('Getting HD test configuration record') + self._reset_test_configs_record() + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting HD test configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_reset_all_test_configs(self): + """ + Constructs and sends the HD test configs reset all + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RESET_ALL_TEST_CONFIGURATIONS.value) + + self.logger.debug("Resetting all HD test configurations") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + @publish(['hd_test_configs']) + def _handler_test_config_sync(self, message, timestamp=0.0): + """ + Handles published test configuration status messages. + + @param message: published HD test configurations message + @return: None + """ + payload = message['message'] + index = MsgFieldPositions.START_POS_FIELD_1 + + for config in HDTestConfigOptions.__members__: + if 'NUM_OF_TEST_CONFIGS' not in config: + config_value, index = bytearray_to_integer(payload, index, False) + self.hd_test_configs[config] = config_value + + self.hd_test_configs_response_timestamp = timestamp + + def _reset_test_configs_record(self): + """ + Resets the test configuration dictionary + + @return: None + """ + for config in HDTestConfigOptions.__members__: + # Loop through the list of the test configuration and set the values to 0xFFFFFFFF + if 'NUM_OF_TEST_CONFIGS' not in config: + self.hd_test_configs[config] = 0xFFFFFFFF Index: leahi-dialin/hd/hemodialysis_device.py =================================================================== diff -u --- leahi-dialin/hd/hemodialysis_device.py (revision 0) +++ leahi-dialin/hd/hemodialysis_device.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,836 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hemodialysis_device.py +# +# @author (last) Dara Navaei +# @date (last) 26-Feb-2024 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct + +from .accelerometer import HDAccelerometer +from .air_bubbles import HDAirBubbles +from .air_pump import HDAirPump +from .air_trap import HDAirTrap +from .alarms import HDAlarms +from .battery import HDBattery +from .blood_flow import HDBloodFlow +from .blood_leak import HDBloodLeak +from .buttons import HDButtons +from .calibration_record import HDCalibrationNVRecord +from .dialysate_inlet_flow import HDDialysateInletFlow +from .dialysate_outlet_flow import HDDialysateOutletFlow +from .dg_proxy import HDDGProxy +from .fluid_leak import HDFluidLeak +from .pressure_occlusion import HDPressureOcclusion +from .pretreatment import HDPreTreatment +from .rtc import HDRTC +from .service_record import HDServiceNVRecords +from .switches import HDSwitches +from .temperatures import HDTemperatures +from .fans import HDFans +from .constants import NO_RESET, RESET +from .syringe_pump import HDSyringePump +from .system_record import HDSystemNVRecords +from .treatment import HDTreatment +from .ui_proxy import HDUIProxy +from .valves import HDValves +from .voltages import HDVoltages +from .watchdog import HDWatchdog +from ..common.hd_defs import HDOpModes +from .hd_events import HDEvents +from .reservoirs import HDReservoirs +from .post_treatment import HDPostTreatment +from .sw_configs import HDSoftwareConfigs +from .battery import HDBattery +from .usage_info_record import HDUsageNVRecord +from .hd_test_configs import HDTestConfig +from .institutional_record import HDInstitutionalNVRecords +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, LogManager, INTERVAL_10s +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, unsigned_short_to_bytearray, bytearray_to_integer, \ + bytearray_to_byte + + +class HD(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin object API. + It provides the basic interface to communicate with the HD firmware. + """ + # HD debug event max count + _HD_DEBUG_EVENT_LIST_COUNT = 10 + _HD_DEBUG_EVENT_MSG_LEN_INDEX = 5 + + # HD login password + HD_LOGIN_PASSWORD = '123' + + # UI version message field positions + START_POS_MAJOR = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MAJOR = START_POS_MAJOR + 1 + START_POS_MINOR = END_POS_MAJOR + END_POS_MINOR = START_POS_MINOR + 1 + START_POS_MICRO = END_POS_MINOR + END_POS_MICRO = START_POS_MICRO + 1 + START_POS_BUILD = END_POS_MICRO + END_POS_BUILD = START_POS_BUILD + 2 + START_POS_COMPATIBILITY_REV = END_POS_BUILD + END_POS_COMPATIBILITY_REV = START_POS_COMPATIBILITY_REV + 4 + + def __init__(self, can_interface="can0", log_level=None): + """ + HD object provides test/service commands for the HD sub-system. + + >> hd_object = HD('can0') + >> hd_object = HD(can_interface='can0', log_level="DEBUG") + + Possible log levels: + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + @param can_interface: (str) CANBus interface name, e.g. "can0" + @param log_level: (str) Logging level, defaults to None + """ + + super().__init__() + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + + # Create listener + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger) + self.can_interface.start() + self.callback_id = None + # register handler for HD operation mode broadcast messages + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_op_mode_sync) + + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_HD_DEBUG_EVENT.value, + self._handler_hd_debug_event_sync) + + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_UI_VERSION_INFO_RESPONSE.value, + self._handler_ui_version_response_sync) + + self.callback_id = self.can_interface.register_transmitting_interval_message(INTERVAL_10s, + self._send_hd_checkin_message) + + # create properties + self.hd_op_mode_timestamp = 0.0 + self.hd_debug_events_timestamp = 0.0 + self.ui_version_info_response_timestamp = 0.0 + self.hd_operation_mode = HDOpModes.MODE_INIT.value + self.hd_operation_sub_mode = 0 + self.hd_logged_in = False + self.hd_set_logged_in_status(False) + self.hd_no_transmit_msg_list = [0, 0, 0, 0, 0, 0, 0, 0] + self.ui_version = None + self.hd_debug_events = [''] * self._HD_DEBUG_EVENT_LIST_COUNT + self.hd_debug_event_index = 0 + self.hd_last_debug_event = '' + + # Create command groups + self.accel = HDAccelerometer(self.can_interface, self.logger) + self.air_bubbles = HDAirBubbles(self.can_interface, self.logger) + self.air_pump = HDAirPump(self.can_interface, self.logger) + self.air_trap = HDAirTrap(self.can_interface, self.logger) + self.alarms = HDAlarms(self.can_interface, self.logger) + self.battery = HDBattery(self.can_interface, self.logger) + self.blood_leak = HDBloodLeak(self.can_interface, self.logger) + self.bloodflow = HDBloodFlow(self.can_interface, self.logger) + self.buttons = HDButtons(self.can_interface, self.logger) + self.calibration_record = HDCalibrationNVRecord(self.can_interface, self.logger) + self.dialysate_inlet_flow = HDDialysateInletFlow(self.can_interface, self.logger) + self.dialysate_outlet_flow = HDDialysateOutletFlow(self.can_interface, self.logger) + self.dg_proxy = HDDGProxy(self.can_interface, self.logger) + self.fluid_leak = HDFluidLeak(self.can_interface, self.logger) + self.pressure_occlusion = HDPressureOcclusion(self.can_interface, self.logger) + self.pretreatment = HDPreTreatment(self.can_interface, self.logger) + self.rtc = HDRTC(self.can_interface, self.logger) + self.service_record = HDServiceNVRecords(self.can_interface, self.logger) + self.switches = HDSwitches(self.can_interface, self.logger) + self.syringe_pump = HDSyringePump(self.can_interface, self.logger) + self.system_record = HDSystemNVRecords(self.can_interface, self.logger) + self.treatment = HDTreatment(self.can_interface, self.logger) + self.ui = HDUIProxy(self.can_interface, self.logger) + self.valves = HDValves(self.can_interface, self.logger) + self.voltages = HDVoltages(self.can_interface, self.logger) + self.calibration_record = HDCalibrationNVRecord(self.can_interface, self.logger) + self.system_record = HDSystemNVRecords(self.can_interface, self.logger) + self.service_record = HDServiceNVRecords(self.can_interface, self.logger) + self.switches = HDSwitches(self.can_interface, self.logger) + self.temperatures = HDTemperatures(self.can_interface, self.logger) + self.fans = HDFans(self.can_interface, self.logger) + self.watchdog = HDWatchdog(self.can_interface, self.logger) + self.events = HDEvents(self.can_interface, self.logger) + self.hd_reservoirs = HDReservoirs(self.can_interface, self.logger) + self.sw_configs = HDSoftwareConfigs(self.can_interface, self.logger) + self.post_treatment = HDPostTreatment(self.can_interface, self.logger) + self.battery = HDBattery(self.can_interface, self.logger) + self.usage_record = HDUsageNVRecord(self.can_interface, self.logger) + self.test_configs = HDTestConfig(self.can_interface, self.logger) + self.institutional_record = HDInstitutionalNVRecords(self.can_interface, self.logger) + + def __del__(self): + self.can_interface.transmit_interval_dictionary[self.callback_id].stop() + + def get_operation_mode(self): + """ + Gets the HD operation mode + + @return: The hd operation mode + """ + return self.hd_operation_mode + + def get_hd_logged_in(self): + """ + Gets the logged in status of the HD + + @return: True if HD is logged in, False if not + """ + return self.hd_logged_in + + def get_hd_blocked_msg_list(self): + """ + Gets the current list of message IDs that HD will prevent transmission of. + + @return: List of message IDs blocked from transmission + """ + return self.hd_no_transmit_msg_list + + def get_ui_version(self): + """ + Gets the last recieved ui_version from the HD + + @return: ui_version in a string. + """ + return self.ui_version + + @publish(["hd_debug_events_timestamp","hd_debug_events"]) + def _handler_hd_debug_event_sync(self, message, timestamp = 0.0): + + payload = message['message'] + message_length = payload[self._HD_DEBUG_EVENT_MSG_LEN_INDEX] + temp_message = '' + + index = MsgFieldPositions.START_POS_FIELD_1 + + for i in range(0, message_length): + # Loop through the length and get the + char, char_index = bytearray_to_byte(payload, index + i, False) + temp_message += chr(char) + + self.hd_debug_events_timestamp = timestamp + self.hd_debug_events.insert(self.hd_debug_event_index, temp_message) + self.hd_last_debug_event = temp_message + + self.hd_debug_event_index += 1 + if self.hd_debug_event_index == self._HD_DEBUG_EVENT_LIST_COUNT: + self.hd_debug_event_index = 0 + + @publish(["hd_logged_in"]) + def hd_set_logged_in_status(self, logged_in: bool = False): + """ + Callback for HD logged in status change. + @param logged_in boolean logged in status for HD + @return: none + """ + self.hd_logged_in = logged_in + + @publish(["hd_op_mode_timestamp","hd_operation_mode", "hd_operation_sub_mode"]) + def _handler_hd_op_mode_sync(self, message, timestamp = 0.0): + """ + Handles published HD operation mode messages. Current HD operation mode + is captured for reference. + + @param message: published HD operation mode broadcast message + @return: None + """ + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.hd_operation_mode = mode[0] + self.hd_operation_sub_mode = smode[0] + self.hd_op_mode_timestamp = timestamp + + + def _handler_ui_version_response_sync(self,message, timestamp = 0.0): + """ + Handler for response from HD regarding its version. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U08 Major \n + U08 Minor \n + U08 Micro \n + U16 Build \n + U32 Compatibility revision + + @return: None if not successful, the version string if unpacked successfully + """ + major = struct.unpack(' 0 for each in [major, minor, micro, build, compatibility]]): + self.ui_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}.{compatibility[0]}" + self.logger.debug(f"UI VERSION: {self.ui_version}") + + else: + self.ui_version = None + self.logger.debug("Failed to retrieve UI Version.") + + def cmd_log_in_to_hd(self, resend: bool = False) -> int: + """ + Constructs and sends a login command via CAN bus. Login required before \n + other commands can be sent to the HD. + + @param resend: (bool) if False (default), try to login once. Otherwise, tries to login indefinitely + @return: 1 if logged in, 0 if log in failed + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_TESTER_LOGIN_REQUEST.value, + payload=list(map(int, map(ord, self.HD_LOGIN_PASSWORD)))) + + self.logger.debug("Logging in...") + + # Send message + received_message = self.can_interface.send(message, resend=resend) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Success: Logged In") + self.hd_set_logged_in_status(True) + self._send_hd_checkin_message() # Timer starts interval first + self.can_interface.transmit_interval_dictionary[self.callback_id].start() + else: + self.logger.debug("Failure: Log In Failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Login Timeout!!!!") + return False + + def cmd_hd_request_calibration_data(self) -> int: + """ + Constructs and sends an HD calibration data request command via CAN bus. + Constraints: + Must be logged into HD. + + \returns response message if received, False if no response received + + @return: 1 if successful, zero otherwise + + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_CALIBRATION_RECORD.value) + + self.logger.debug("requesting HD calibration data.") + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("HD calibration data request accepted.") + else: + self.logger.debug("HD calibration data request failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_set_operation_mode(self, new_mode: int = 0) -> int: + """ + Constructs and sends a set operation mode request command via CAN bus. + Constraints: + Must be logged into HD. + Transition from current to requested op mode must be legal. + NOTE: for POST the HD device shall be in Standby Mode + + @param new_mode: ID of operation mode to transition to + HD_OP_MODE_FAULT = 0 + HD_OP_MODE_SERVICE = 1 + HD_OP_MODE_INIT_POST = 2 + HD_OP_MODE_STANDBY = 3 + HD_OP_MODE_TREATMENT_PARAMS = 4 + HD_OP_MODE_PRE_TREATMENT = 5 + HD_OP_MODE_TREATMENT = 6 + HD_OP_MODE_POST_TREATMENT = 7 + + @return: 1 if successful, zero otherwise + + """ + + payload = integer_to_bytearray(new_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_OP_MODE_REQUEST.value, + payload=payload) + + self.logger.debug("Requesting HD mode change to " + str(new_mode)) + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Success: Mode change accepted") + else: + self.logger.debug("Failure: Mode change rejected.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD mode change request Timeout!!!!") + return False + + def cmd_hd_safety_shutdown_override(self, active: bool = True, reset: int = NO_RESET) -> int: + """ + Constructs and sends an HD safety shutdown override command via CAN bus. + Constraints: + Must be logged into HD. + + \returns response message if received, False if no response received + + @param active: boolean - True to activate safety shutdown, False to deactivate + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if active: + sft = 1 + else: + sft = 0 + rst = integer_to_bytearray(reset) + saf = integer_to_bytearray(sft) + payload = rst + saf + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SAFETY_SHUTDOWN_OVERRIDE.value, + payload=payload) + + self.logger.debug("overriding HD safety shutdown") + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Safety shutdown signal overridden") + else: + self.logger.debug("Safety shutdown signal override failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_software_reset_request(self) -> None: + """ + Constructs and sends an HD software reset request via CAN bus. + Constraints: + Must be logged into HD. + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SOFTWARE_RESET_REQUEST.value) + + self.logger.debug("requesting HD software reset") + + # Send message + self.can_interface.send(message, 0) + self.logger.debug("Sent request to HD to reset...") + self.hd_set_logged_in_status(False) + + def cmd_op_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the HD operation mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override operation mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Operation mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_standby_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the standby mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_STANDBY_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override standby mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Standby mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pre_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the pre-treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_PRE_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pre-treatment mode broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Pre-treatment mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_post_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the post-treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_POST_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override post-treatment mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Post-treatment mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_block_hd_message_transmissions(self, msg1: int = 0, msg2: int = 0, msg3: int = 0, msg4: int = 0, + msg5: int = 0, msg6: int = 0, msg7: int = 0, msg8: int = 0): + """ + Constructs and sends a block hd message transmission request + Constraints: + Must be logged into HD. + + @param msg1: integer - 1st message ID to block HD from transmitting + @param msg2: integer - 2nd message ID to block HD from transmitting + @param msg3: integer - 3rd message ID to block HD from transmitting + @param msg4: integer - 4th message ID to block HD from transmitting + @param msg5: integer - 5th message ID to block HD from transmitting + @param msg6: integer - 6th message ID to block HD from transmitting + @param msg7: integer - 7th message ID to block HD from transmitting + @param msg8: integer - 8th message ID to block HD from transmitting + @return: 1 if successful, zero otherwise + """ + # Save blocked message(s) list + self.hd_no_transmit_msg_list[0] = msg1 + self.hd_no_transmit_msg_list[1] = msg2 + self.hd_no_transmit_msg_list[2] = msg3 + self.hd_no_transmit_msg_list[3] = msg4 + self.hd_no_transmit_msg_list[4] = msg5 + self.hd_no_transmit_msg_list[5] = msg6 + self.hd_no_transmit_msg_list[6] = msg7 + self.hd_no_transmit_msg_list[7] = msg8 + # Build message payload + m1 = unsigned_short_to_bytearray(msg1) + m2 = unsigned_short_to_bytearray(msg2) + m3 = unsigned_short_to_bytearray(msg3) + m4 = unsigned_short_to_bytearray(msg4) + m5 = unsigned_short_to_bytearray(msg5) + m6 = unsigned_short_to_bytearray(msg6) + m7 = unsigned_short_to_bytearray(msg7) + m8 = unsigned_short_to_bytearray(msg8) + payload = m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOCK_MESSAGE_TRANSMISSION.value, + payload=payload) + + self.logger.debug("request HD block transmission of message(s)") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Given messages blocked." + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_request_ui_version(self) -> None: + """ + Constructs and sends a ui version request to the HD. + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_UI_VERSION_INFO_REQUEST.value) + + self.logger.debug("Sending an UI version request to the HD.") + self.can_interface.send(message, 0) + + def _send_hd_checkin_message(self) -> int: + """ + Constructs and sends an HD Dialin check in message to the HD. + + @return: none + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_DIALIN_CHECK_IN.value) + self.can_interface.send(message) + return True + + def cmd_hd_ram_status_override(self, ram_reg: int = 0, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the RAM status override command + Constraints: + Must be logged into HD. + + RAM Status Bits: + SERR 0x00000001 Bit 0 - Single-bit error in TCRAM Module Error Status Register + ADDR_DEC_FAIL 0x00000004 Bit 2 - Address decode failed in TCRAM Module Error Status Register + ADDR_COMP_LOGIC_FAIL 0x00000010 Bit 4 - Address decode logic element failed in TCRAM Module Error Status Register + DERR 0x00000020 Bit 5 - Multiple bit error in TCRAM Module Error Status Register + RADDR_PAR_FAIL 0x00000100 Bit 8 - Read Address Parity Failure in TCRAM Module Error Status Register + WADDR_PAR_FAIL 0x00000200 Bit 9 - Write Address Parity Failure in TCRAM Module Error Status Register + + @param ram_reg: integer - the RAM regsiter. 0 or 1 + @param status: integer - bitmap of the status values listed aboves + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + reg = integer_to_bytearray(ram_reg) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + reg + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RAM_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug(f"Overriding RAM Status Register {reg} to {str(sts)}") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_pending_ack_override(self, pending_ack_channel: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends an HD pending ack override command via CAN bus. + Constraints: + Must be logged into HD. + + Will prevent receiving ACK messages from being registered. + Used to trigger ALARM_ID_HD_CAN_MESSAGE_NOT_ACKED_BY_UI and ALARM_ID_HD_CAN_MESSAGE_NOT_ACKED_BY_DG + after retries are sent. + + Use 1 for UI CAN messages Alarm. + 2 for DG CAN messages Alarm. + + @param pending_ack_channel: integer - 1 for UI Channel ACK, 2 for DG Channel ACK + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + rst = integer_to_bytearray(reset) + pack = integer_to_bytearray(pending_ack_channel) + payload = rst + pack + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_CAN_RECEIVE_ACK_MESSAGE_OVERRIDE.value, + payload=payload) + + self.logger.debug("overriding HD Pending ACK") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/hd/institutional_record.py =================================================================== diff -u --- leahi-dialin/hd/institutional_record.py (revision 0) +++ leahi-dialin/hd/institutional_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,390 @@ +########################################################################### +# +# Copyright (c) 2024-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file institutional_record.py +# +# @author (last) Dara Navaei +# @date (last) 06-May-2024 +# @author (original) Dara Navaei +# @date (original) 26-Feb-2024 +# +############################################################################ + +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsHD +from ..utils.conversions import integer_to_bytearray + + +class HDInstitutionalNVRecords(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for institutional record commands. + """ + _DEFAULT_MIN_BLOOD_FLOW_MLPM = 100 + _DEFAULT_MAX_BLOOD_FLOW_MLPM = 500 + _DEFAULT_MIN_DIALYSATE_FLOW_MLPM = 100 + _DEFAULT_MAX_DIALYSATE_FLOW_MLPM = 600 + _DEFAULT_MIN_TX_DURATION_MIN = 60 + _DEFAULT_MAX_TX_DURATION_MIN = 480 + _DEFAULT_MIN_STOP_HEP_DISP_BEFORE_TX_END_MIN = 0 + _DEFAULT_MAX_STOP_HEP_DISP_BEFORE_TX_END_MIN = 480 + _DEFAULT_MIN_SALINE_BOLUS_VOLUME_ML = 100 + _DEFAULT_MAX_SALINE_BOLUS_VOLUME_ML = 300 + _DEFAULT_MIN_DIALYSATE_TEMPERATURE_C = 35.0 + _DEFAULT_MAX_DIALYSATE_TEMPERATURE_C = 37.0 + _DEFAULT_MIN_ART_PRESS_LIMIT_WINDOW_MMHG = 120 + _DEFAULT_MAX_ART_PRESS_LIMIT_WINDOW_MMHG = 200 + _DEFAULT_MIN_VEN_PRESS_LIMIT_WINDOW_MMHG = 100 + _DEFAULT_MAX_VEN_PRESS_LIMIT_WINDOW_MMHG = 200 + _DEFAULT_MIN_VEN_ASYM_PRESS_LIMIT_WINDOW_MMHG = 20 + _DEFAULT_MAX_VEN_ASYM_PRESS_LIMIT_WINDOW_MMHG = 35 + _DEFAULT_MIN_UF_VOLUME_L = 0.0 + _DEFAULT_MAX_UF_VOLUME_L = 8.0 + _DEFAULT_MIN_HEPARIN_DISP_RATE_MLPHR = 0.0 + _DEFAULT_MAX_HEPARIN_DISP_RATE_MLPHR = 1.0 + _DEFAULT_MIN_HEPARIN_BOLUS_VOLUME_ML = 0.0 + _DEFAULT_MAX_HEPARIN_BOLUS_VOLUME_ML = 2.0 + _DEFAULT_ENABLE_CHEM_DISINFECT = 0 + + _RECORD_SPECS_BYTES = 12 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _FIRMWARE_STACK_NAME = 'HD' + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_institutional_in_progress = False + self._write_fw_data_to_excel = True + self._institutional_data = 0 + self._raw_institutional_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.hd_institutional_record = self._prepare_hd_institutional_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_INSTITUTIONAL_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_institutional_sync) + self.hd_institutional_record_timestamp = 0.0 + + def cmd_reset_hd_institutional_record(self) -> bool: + """ + Handles resetting HD institutional record. + + @return: True if successful, False otherwise + """ + self.hd_institutional_record = self._prepare_hd_institutional_record() + self.hd_institutional_record = self._utilities.reset_fw_system_service_record(self.hd_institutional_record) + status = self.cmd_set_hd_institutional_record(self.hd_institutional_record) + + return status + + def cmd_request_hd_institutional_record(self) -> int: + """ + Handles getting HD institutional record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_institutional_in_progress is not True: + self._is_getting_institutional_in_progress = True + # Clear the list for the next call + self._raw_institutional_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_INSTITUTIONAL_RECORD.value) + + self.logger.debug('Getting HD institutional record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_hd_institutional_record_crc_override(self, crc: int) -> bool: + """ + Handles setting HD institutional CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + hd_record = integer_to_bytearray(NVRecordsHD.NVDATAMGMT_INSTITUTIONAL_RECORD.value) + payload = reset_byte_array + crc_value + hd_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding HD institutional record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_hd_institutional_sync(self, message, timestamp=0.0): + """ + Handles published HD system record messages. HD institutional records are captured for + processing and updating the HD institutional record. + + @param message: published HD institutional record data message + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of institutional_record record payload is from the start index + 12 bytes for the current message+total + # messages + the length of institutional_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._institutional_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting institutional_record records until the all the calibration_record messages are received. + # Concatenate the institutional_record records to each other + if self._current_message <= self._total_messages: + self._raw_institutional_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_institutional_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_institutional_record, self._raw_institutional_record) + self.hd_institutional_record_timestamp = timestamp + self._handler_received_complete_hd_institutional_record() + + + @publish(["hd_institutional_record_timestamp", "hd_institutional_record"]) + def _handler_received_complete_hd_institutional_record(self): + """ + Publishes the received institutional record + + @return: None + """ + self.logger.debug("Received a complete hd institutional record.") + + def cmd_set_hd_institutional_record(self, hd_institutional_record: OrderedDict) -> bool: + """ + Handles updating the HD institutional and sends it to FW. + + @param hd_institutional_record: (OrderedDict) the hd institutional record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + self.logger.debug('Setting HD institutional record') + + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_institutional_record) + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_INSTITUTIONAL_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("HD ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending HD institutional record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("HD institutional record was set successfully!") + return True + else: + self.logger.debug("HD institutional record was not set. Please make sure the ranges are Ok.") + return False + + def _prepare_hd_institutional_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main HD institutional record. + + @return: (OrderedDict) an assembled hd institutional record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_institutional_record()] + + for function in functions: + # Update the groups bytes size so far to be used to padding later + groups_byte_size += function[1] + # Update the institutional record + result.update(function[0]) + + # Build the CRC of the main institutional_record record + record_crc = OrderedDict({'crc': [' bool: + """ + Handles setting the institutional record data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG institutional record and set and observer class to callback when the record is read back + self.cmd_request_hd_institutional_record() + observer = NVUtilsObserver("hd_institutional_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_institutional_record, report_address, + self._utilities.INSTITUTIONAL_RECORD_TAB_NAME) + + ret = self.cmd_set_hd_institutional_record(self.hd_institutional_record) + return ret + + def cmd_get_hd_institutional_record(self, report_address: str = None): + """ + Publicly accessible function to request the HD institutional record and write the record to excel. + + @param report_address: the address that the report needs to be written to. The default is None so it picks an + address and writes the excel report. + + @return: none + """ + + # Create the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.INSTITUTIONAL_RECORD_TAB_NAME, + report_address, protect_sheet=True) + + # Create an object of the observer class to observe the dictionary + observer = NVUtilsObserver("hd_institutional_record") + # Attach the observer to the list + self.attach(observer) + + # Request the latest software configuration record from firmware + self.cmd_request_hd_institutional_record() + # Wait until data has been received from firmware + while not observer.received: + sleep(0.1) + # Write the updated values from excel to firmware + self._utilities.write_fw_record_to_excel(self.hd_institutional_record) Index: leahi-dialin/hd/post_treatment.py =================================================================== diff -u --- leahi-dialin/hd/post_treatment.py (revision 0) +++ leahi-dialin/hd/post_treatment.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,79 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file post_treatment.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Micahel Garthwaite +# @date (original) 22-Feb-2022 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import bytearray_to_integer + + +@unique +class HDPostTreatmentDrainStates(DialinEnum): + + DRAIN_RESERVOIR_SWITCH_STATE = 0 + DRAIN_RESERVOIR_START_DRAIN_STATE = 1 + DRAIN_RESERVOIR_DRAIN_STATE = 2 + DRAIN_RESERVOIR_COMPLETE_STATE = 3 + NUM_OF_DRAIN_STATES = 4 + + +class HDPostTreatment(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for post treatment related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDPostTreatment constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_POST_TREATMENT_STATE_DATA.value, + self._handler_post_treatment_sub_mode_sync) + self.hd_post_treatment_state_timestamp = 0.0 + self.post_treatment_sub_mode = 0 + self.post_treatment_drain_state = 0 + + def get_post_treatment_sub_mode(self) -> int: + """ + @return: (int) post treatment submode + """ + return self.post_treatment_sub_mode + + @publish(["hd_post_treatment_state_timestamp","post_treatment_sub_mode", "post_treatment_drain_state"]) + def _handler_post_treatment_sub_mode_sync(self, message, timestamp=0.0): + """ + Handles published post treatment sub mode data messages. Sub mode data is also captured for reference. + + @param message: published post treatment sub mode broadcast message + U32 sub-mode + + @return: none + """ + payload = message['message'] + index = MsgFieldPositions.START_POS_FIELD_1 + self.post_treatment_sub_mode, index = bytearray_to_integer(payload, index, False) + self.post_treatment_drain_state, index = bytearray_to_integer(payload, index, False) + self.hd_post_treatment_state_timestamp = timestamp + Index: leahi-dialin/hd/pressure_occlusion.py =================================================================== diff -u --- leahi-dialin/hd/pressure_occlusion.py (revision 0) +++ leahi-dialin/hd/pressure_occlusion.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,465 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file pressure_occlusion.py +# +# @author (last) Vinayakam Mani +# @date (last) 02-May-2024 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDPressureOcclusion(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for pressure related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDPressureOcclusion constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_PRESSURE_OCCLUSION_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_pressure_occlusion_sync) + + self.hd_pressure_occlusion_timestamp = 0.0 + self.arterial_pressure = 0.0 + self.venous_pressure = 0.0 + self.arterial_long_filtered_pressure = 0.0 + self.venous_long_filtered_pressure = 0.0 + self.blood_pump_occlusion = 0 + self.pressure_limits_state = 0 + self.arterial_pressure_limit_min = 0 + self.arterial_pressure_limit_max = 0 + self.venous_pressure_limit_min = 0 + self.venous_pressure_limit_max = 0 + self.blood_pump_occlusion_long_filter = 0.0 + self.partial_occlusion_baseline = 0.0 + + def get_arterial_pressure(self): + """ + Gets the arterial pressure. + @return: (float) The arterial pressure + """ + return self.arterial_pressure + + def get_long_filtered_arterial_pressure(self): + """ + Gets the long filtered arterial pressure. + @return: (float) The long filtered arterial pressure + """ + return self.arterial_long_filtered_pressure + + def get_venous_pressure(self): + """ + Gets the venous pressure + + @return: (float) The venous pressure + """ + return self.venous_pressure + + def get_long_filtered_venous_pressure(self): + """ + Gets the long filtered venous pressure + + @return: (float) The long filtered venous pressure + """ + return self.venous_long_filtered_pressure + + def get_blood_pump_occlusion(self): + """ + Gets the blood pump occlusion + + @return: (int) The blood pump occlusion + """ + return self.blood_pump_occlusion + + def get_pressure_limits_state(self): + """ + Gets the current pressure limits state. + Pressure states: + PRESSURE_LIMITS_STATE_OFF = 0 Off - not pressure low/high alarms will be detected) + PRESSURE_LIMITS_STATE_IDLE = 1 Idle - in Treatment mode state where BP is stopped - no pressure low/high alarms + PRESSURE_LIMITS_STATE_WIDE = 2 Wide - in Treatment mode state where BP is running but wide limits apply + PRESSURE_LIMITS_STATE_STABILIZATION = 3 Stabilization - in Treatment mode state where BP is running (dialysis + or stop), but need to stabilize first + PRESSURE_LIMITS_STATE_STABILIZATION_2 = 4 Second stage stabilization - re adjust the pressure that has been + drifted due to UF control etc and limit + windows apply + PRESSURE_LIMITS_STATE_STABLE = 5 Stable - in Treatment mode state where BP is running (dialysis or stop) and + limit windows apply + + + @return: (int) The pressure limits state + """ + return self.pressure_limits_state + + def get_arterial_pressure_limit_min(self): + """ + Gets the current arterial pressure limit (minimum). + + @return: (int) The arterial pressure limit (minimum) + """ + return self.arterial_pressure_limit_min + + def get_arterial_pressure_limit_max(self): + """ + Gets the current arterial pressure limit (maximum). + + @return: (int) The arterial pressure limit (maximum) + """ + return self.arterial_pressure_limit_max + + def get_venous_pressure_limit_min(self): + """ + Gets the current venous pressure limit (minimum). + + @return: (int) The venous pressure limit (minimum) + """ + return self.venous_pressure_limit_min + + def get_venous_pressure_limit_max(self): + """ + Gets the current venous pressure limit (maximum). + + @return: (int) The venous pressure limit (maximum) + """ + return self.venous_pressure_limit_max + + def get_filtered_blood_pump_occlusion(self): + """ + Gets the filtered blood pump occlusion + + @return: (float) The filtered blood pump occlusion + """ + return self.blood_pump_occlusion_long_filter + + def get_partial_blood_pump_occlusion_baseline(self): + """ + Gets the partial blood pump occlusion baseline + + @return: (float) The partial blood pump occlusion baseline + """ + return self.partial_occlusion_baseline + + @publish([ + "hd_pressure_occlusion_timestamp", + "arterial_pressure", + "venous_pressure", + "blood_pump_occlusion", + "pressure_limits_state", + "arterial_pressure_limit_min", + "arterial_pressure_limit_max", + "venous_pressure_limit_min", + "venous_pressure_limit_max", + "arterial_long_filtered_pressure", + "venous_long_filtered_pressure", + "blood_pump_occlusion_long_filter", + "partial_occlusion_blood_pump_baseline" + ]) + def _handler_pressure_occlusion_sync(self, message, timestamp=0.0): + """ + Handles published pressure & occlusion data messages. Pressure data are captured + for reference. + + @param message: published pressure & occlusion data message + @return: none + """ + + art = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ven = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + pls = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + apl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + apu = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + vpl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + vpu = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + lfa = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + lfv = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + fbp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + pob = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + + self.arterial_pressure = art[0] + self.venous_pressure = ven[0] + self.blood_pump_occlusion = bp[0] + self.pressure_limits_state = pls[0] + self.arterial_pressure_limit_min = apl[0] + self.arterial_pressure_limit_max = apu[0] + self.venous_pressure_limit_min = vpl[0] + self.venous_pressure_limit_max = vpu[0] + self.arterial_long_filtered_pressure = lfa[0] + self.venous_long_filtered_pressure = lfv[0] + self.blood_pump_occlusion_long_filter = fbp[0] + self.partial_occlusion_baseline = pob[0] + self.hd_pressure_occlusion_timestamp = timestamp + + def cmd_arterial_pressure_measured_override(self, pres: float, filtered: bool = False, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured arterial pressure override command + Constraints: + Must be logged into HD. + + @param pres: float - measured arterial pressure (in mmHg) to override with + @param filtered: bool - switch between filtered override and raw override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flt = integer_to_bytearray(filtered) + prs = float_to_bytearray(pres) + payload = rst + prs + flt + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_ARTERIAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured arterial pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(pres) + " mmHg. " + self.logger.debug("Arterial pressure (measured)) overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_venous_pressure_measured_override(self, pres: float, filtered: bool = False, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured venous pressure \n + override command. + Constraints: + Must be logged into HD. + + @param pres: float - venous pressure (in mmHg) to override with + @param filtered: bool - switch between filtered override and raw override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flt = integer_to_bytearray(filtered) + prs = float_to_bytearray(pres) + payload = rst + prs + flt + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_VENOUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured venous pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(pres) + " mmHg. " + self.logger.debug("Venous pressure (measured) overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_occlusion_override(self, occl: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump occlusion pressure override command + Constraints: + Must be logged into HD. + + @param occl: integer - pressure (in counts) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = integer_to_bytearray(occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_OCCLUSION_BLOOD_PUMP_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump occlusion pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(occl) + " mmHg. " + self.logger.debug("Blood pump occlusion pressure (measured) overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_filtered_blood_pump_measured_occlusion_override(self, filtered_occl: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured filtered blood pump occlusion pressure override command + Constraints: + Must be logged into HD. + + @param filtered_occl: float - pressure (in counts) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = float_to_bytearray(filtered_occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_PARTIAL_OCCLUSION_BLOOD_PUMP_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured filtered blood pump occlusion pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(filtered_occl) + self.logger.debug("Filtered Blood pump occlusion pressure (measured) overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_partial_occlusion_baseline_override(self, partial_occl_baseline: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the partial blood pump occlusion pressure baseline override command + Constraints: + Must be logged into HD. + + @param partial_occl_baseline: float - partial blood pump occlusion baseline override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ_bl = float_to_bytearray(partial_occl_baseline) + payload = rst + occ_bl + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_PARTIAL_OCCL_BLOOD_PUMP_BASELINE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override partial blood pump occlusion baseline pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(partial_occl_baseline) + self.logger.debug(" Partial Blood pump occlusion baseline pressure overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_occlusion_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure/occlusion broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRES_OCCL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pressure/occlusion broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Pressure/occlusion broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/pretreatment.py =================================================================== diff -u --- leahi-dialin/hd/pretreatment.py (revision 0) +++ leahi-dialin/hd/pretreatment.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,333 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file pretreatment.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Quang Nguyen +# @date (original) 02-Mar-2021 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from ..common import (PreTreatmentSubModes, + PreTreatmentSampleWaterStates, + PreTreatmentConsumableSelfTestStates, + PreTreatmentRecircStates, + PreTreatmentNoCartSelfTestStates, + PreTreatmentDrySelfTestsStates, + PreTreatmentPrimeStates) +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum + + +@unique +class PreTreatmentRsrvrState(DialinEnum): + PRE_TREATMENT_RESERVOIR_MGMT_START_STATE = 0 + PRE_TREATMENT_RESERVOIR_MGMT_DRAIN_CMD_STATE = 1 + PRE_TREATMENT_RESERVOIR_MGMT_DRAIN_CMD_RESP_STATE = 2 + PRE_TREATMENT_RESERVOIR_MGMT_START_FILL_STATE = 3 + PRE_TREATMENT_RESERVOIR_MGMT_FILL_CMD_RESP_STATE = 4 + PRE_TREATMENT_RESERVOIR_MGMT_FILL_COMPLETE_STATE = 5 + PRE_TREATMENT_RESERVOIR_MGMT_WAIT_FOR_RESERVOIR_SWITCH_STATE = 6 + PRE_TREATMENT_RESERVOIR_MGMT_COMPLETE_STATE = 7 + NUM_OF_PRE_TREATMENT_RESERVOIR_MGMT_STATES = 8 + + +class HDPreTreatment(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for pretreatment related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDPreTreatment constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_PRE_TREATMENT_STATE_DATA.value, + self._handler_pre_treatment_state_sync) + + msg_id = MsgIds.MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS_DATA.value + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, msg_id, + self._handler_no_cart_self_test_progress_sync) + + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_DRY_SELF_TEST_PROGRESS_DATA.value, + self._handler_dry_self_test_progress_sync) + + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_PRIMING_STATUS_DATA.value, + self._handler_prime_progress_sync) + + self.hd_pre_treatment_state_timestamp = 0.0 + self.hd_no_cart_st_timestamp = 0.0 + self.hd_dry_st_timestamp = 0.0 + self.hd_priming_timestamp = 0.0 + self.pre_treatment_submode = 0 + self.pre_treatment_sample_water_state = 0 + self.pre_treatment_consumable_self_test_state = 0 + self.pre_treatment_no_cart_self_test_state = 0 + self.pre_treatment_installation_state = 0 + self.pre_treatment_dry_self_test_state = 0 + self.pre_treatment_prime_state = 0 + self.pre_treatment_recirc_state = 0 + self.pre_treatment_patient_connection_state = 0 + self.pre_treatment_wet_self_test_state = 0 + self.pre_treatment_reservoir_state = 0 + + self.no_cart_self_test_timeout = 0 + self.no_cart_self_test_time_countdown = 0 + + self.dry_self_test_timeout = 0 + self.dry_self_test_time_countdown = 0 + + self.prime_timeout = 0 + self.prime_time_countdown = 0 + + def get_pre_treatment_submode(self): + """ + Gets the pre-treatment state + + @return: The pre-treatment state + """ + return self.pre_treatment_submode + + def get_pre_treatment_sample_water_state(self): + """ + Gets the pre-treatment sample water state + + @return: The pre-treatment sample water state + """ + return self.pre_treatment_sample_water_state + + def get_pre_treatment_consumable_self_test_state(self): + """ + Gets the pre-treatment consumable self-test state + + @return: The pre-treatment consumable self-test state + """ + return self.pre_treatment_consumable_self_test_state + + def get_pre_treatment_no_cart_self_test_state(self): + """ + Gets the pre-treatment no cartridge self-test state + + @return: The pre-treatment no cartridge self-test state + """ + return self.pre_treatment_no_cart_self_test_state + + def get_pre_treatment_installation_state(self): + """ + Gets the pre-treatment installation state + + @return: The pre-treatment installation state + """ + return self.pre_treatment_installation_state + + def get_pre_treatment_dry_self_test_state(self): + """ + Gets the pre-treatment dry self-test state + + @return: The pre-treatment dry self-test state + """ + return self.pre_treatment_dry_self_test_state + + def get_pre_treatment_prime_state(self): + """ + Gets the pre-treatment primt state + + @return: The pre-treatment prime state + """ + return self.pre_treatment_prime_state + + def get_pre_treatment_recirc_state(self): + """ + Gets the pre-treatment re-circulate state + + @return: The pre-treatment re-circulate state + """ + return self.pre_treatment_recirc_state + + def get_pre_treatment_patient_connection_state(self): + """ + Gets the pre-treatment patient connection state + + @return: The pre-treatment patient connection state + """ + return self.pre_treatment_patient_connection_state + + def get_no_cart_self_test_timeout(self): + """ + Gets the pre-treatment no cartridge self-test timeout + + @return: The pre-treatment no cartridge self-test timeout + """ + return self.no_cart_self_test_timeout + + def get_no_cart_self_test_time_countdown(self): + """ + Gets the pre-treatment no cartridge self-test time countdown + + @return: The pre-treatment no cartridge self-test time countdown + """ + return self.no_cart_self_test_time_countdown + + def get_dry_self_test_timeout(self): + """ + Gets the pre-treatment dry self-test timeout + + @return: The pre-treatment dry self-test timeout + """ + return self.dry_self_test_timeout + + def get_dry_self_test_time_countdown(self): + """ + Gets the pre-treatment dry self-test time countdown + + @return: The pre-treatment dry self-test time countdown + """ + return self.dry_self_test_time_countdown + + def get_prime_timeout(self): + """ + Gets the pre-treatment prime timeout + + @return: The pre-treatment prime timeout + """ + return self.prime_timeout + + def get_prime_time_countdown(self): + """ + Gets the pre-treatment prime time countdown + + @return: The pre-treatment prime time countdown + """ + return self.prime_time_countdown + + def get_wet_self_test_state(self): + """ + Gets the pre-treatment wet self test state + + @return: The pre-treatment wet self test state + """ + return self.pre_treatment_wet_self_test_state + + @publish([ + "hd_pre_treatment_state_timestamp", + "pre_treatment_submode", + "pre_treatment_sample_water_state", + "pre_treatment_consumable_self_test_state", + "pre_treatment_no_cart_self_test_state", + "pre_treatment_installation_state", + "pre_treatment_dry_self_test_state", + "pre_treatment_prime_state", + "pre_treatment_recirc_state", + "pre_treatment_patient_connection_state", + "pre_treatment_wet_self_test_state", + "pre_treatment_reservoir_state" + ]) + def _handler_pre_treatment_state_sync(self, message, timestamp=0.0): + """ + Handles published pre-treatment state data messages. + + @param message: published pre-treatment state data message + @return: none + """ + + self.pre_treatment_submode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.pre_treatment_sample_water_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.pre_treatment_consumable_self_test_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.pre_treatment_no_cart_self_test_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.pre_treatment_installation_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.pre_treatment_dry_self_test_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.pre_treatment_prime_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.pre_treatment_recirc_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.pre_treatment_patient_connection_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.pre_treatment_wet_self_test_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + # Yes, I know we should do the new way, but we we need a task to cover all these in one shot! + self.pre_treatment_reservoir_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.hd_pre_treatment_state_timestamp = timestamp + + + @publish([ + "hd_no_cart_st_timestamp", + "no_cart_self_test_timeout", + "no_cart_self_test_time_countdown" + ]) + def _handler_no_cart_self_test_progress_sync(self, message, timestamp=0.0): + """ + Handles published no cartridge self-test progress data messages. + + @param message: published no cartridge self-test progress data message + @return: None + """ + self.no_cart_self_test_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.no_cart_self_test_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.hd_no_cart_st_timestamp = timestamp + + @publish([ + "hd_dry_st_timestamp", + "dry_self_test_timeout", + "dry_self_test_time_countdown" + ]) + def _handler_dry_self_test_progress_sync(self, message, timestamp=0.0): + """ + Handles published dry self-test progress data messages. + + @param message: published dry self-test progress data message + @return: None + """ + + self.dry_self_test_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dry_self_test_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.hd_dry_st_timestamp = timestamp + + @publish([ + "hd_priming_timestamp", + "prime_timeout", + "prime_time_countdown" + ]) + def _handler_prime_progress_sync(self, message, timestamp=0.0): + """ + Handles published prime progress data messages. + + @param message: published prime progress data message + @return: None + """ + + self.prime_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.prime_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.hd_priming_timestamp = timestamp \ No newline at end of file Index: leahi-dialin/hd/reservoirs.py =================================================================== diff -u --- leahi-dialin/hd/reservoirs.py (revision 0) +++ leahi-dialin/hd/reservoirs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,129 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file reservoirs.py +# +# @author (last) Michael Garthwaite +# @date (last) 04-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 23-Nov-2021 +# +############################################################################ + + +import struct +from logging import Logger +from enum import unique + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class HDReservoirStates(DialinEnum): + + TREATMENT_RESERVOIR_MGMT_START_STATE = 0 + TREATMENT_RESERVOIR_MGMT_DRAIN_RESERVOIR_STATE = 1 + TREATMENT_RESERVOIR_MGMT_WAIT_TO_FILL_STATE = 2 + TREATMENT_RESERVOIR_MGMT_FILL_RESERVOIR_STATE = 3 + TREATMENT_RESERVOIR_MGMT_WAIT_FOR_FILL_SETTLE_STATE = 4 + TREATMENT_RESERVOIR_MGMT_WAIT_FOR_SWITCH_SETTLE_STATE = 5 + NUM_OF_TREATMENT_RESERVOIR_MGMT_STATES = 6 + + +class HDReservoirs(AbstractSubSystem): + """ + HD interface containing reservoir related commands. + """ + + def __init__(self, can_interface, logger: Logger): + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_RESERVOIRS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_reservoirs_sync) + + self.hd_reservoirs_timestamp = 0.0 + self.reservoir_state = 0 + self.active_reservoir_uf_ml = 0.0 + self.active_reservoir_spent_vol_ml = 0.0 + self.dilution_level_pct = 0.0 + self.recirculation_level_pct = 0.0 + self.time_depletion = 0 + self.time_wait_to_fill = 0 + + self.temp_remove_fill_flow = 0.0 + + @publish(['hd_reservoirs_timestamp', 'reservoir_state', 'active_reservoir_uf_ml', 'active_reservoir_spent_vol_ml', 'dilution_level_pct', + 'recirculation_level_pct', 'time_depletion', 'time_wait_to_fill', 'temp_remove_fill_flow']) + def _handler_reservoirs_sync(self, message, timestamp=0.0): + """ + Handles published reservoir data messages. Reservoir data are captured + for reference. + + @param message: published reservoir data message + @return: none + """ + + self.reservoir_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.active_reservoir_uf_ml = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.active_reservoir_spent_vol_ml = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.dilution_level_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.recirculation_level_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.time_depletion = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.time_wait_to_fill = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.temp_remove_fill_flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + self.hd_reservoirs_timestamp = timestamp + + def cmd_recirulation_pct_override(self, recirulation_pct: float, reset: int) -> int: + """ + Constructs and sends a recirulation percentage override message to the HD. + Constraints: + Must be logged into HD. + + @param recirulation_pct: (float) recirulation percentage value + @param reset: (int) - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(reset) + pct = float_to_bytearray(recirulation_pct) + payload = rst + pct + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RECIRULATION_PCT_OVERRIDE.value, + payload=payload) + + self.logger.debug("Setting recirulation percentage.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Recirulation percentage set to " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/rtc.py =================================================================== diff -u --- leahi-dialin/hd/rtc.py (revision 0) +++ leahi-dialin/hd/rtc.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,235 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file rtc.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +import struct +from ..common.msg_defs import MsgIds, MsgFieldPositions +from logging import Logger +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class HDRTC(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for rtc commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_RTC_EPOCH_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_rtc_epoch) + + self.hd_rtc_timestamp = 0.0 + self.rtc_epoch = 0 + + def get_rtc_epoch(self): + """ + Gets the rtc epoch + + @return: The rtc epoch + """ + return self.rtc_epoch + + @publish(["hd_rtc_timestamp","rtc_epoch"]) + def _handler_rtc_epoch(self, message, timestamp=0.0): + """ + Publishes the rtc time in epoch + + @param message: published rtc epoch message + @return: None + """ + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.rtc_epoch = epoch + self.hd_rtc_timestamp = timestamp + + def cmd_stop_rtc(self): + """ + Stops the HD RTC clock + + @return: 1 if Successful, False otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_STOP_RTC_CLOCK.value) + + self.logger.debug("Stopping the HD RTC") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug("RTC stop command was sent" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_rtc_time_and_date(self, second: int, minute: int, hour: int, day: int, month: int, year: int) -> int: + """ + Sets the RTC time and date from the provided + + @param second: (int) Second + @param minute: (int) Minute + @param hour: (int) Hour + @param day: (int) Day + @param month: (int) Month + @param year: (int) Year + @return: 1 if Successful, False otherwise + """ + sec = bytes([second]) + mint = bytes([minute]) + hour = bytes([hour]) + day = bytes([day]) + month = bytes([month]) + year = integer_to_bytearray(year) + payload = sec + mint + hour + day + month + year + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_SET_RTC_DATE_TIME.value, + payload=payload) + + self.logger.debug("Setting time and date to rtc") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + # str_res = str(flow) + self.logger.debug( + "Time and Date in rtc set to seconds: " + str(sec) + " minutes: " + str(min) + " hours: " + + str(hour) + " days: " + str(day) + " months: " + str(month) + " years: " + str(year) + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_rtc_ctl_reg1_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD RTC control register 1 status override command + Constraints: + Must be logged into HD. + + @param status: status is a bit map containing a set of bits that represent status + from the RTC status register. Specified below + + RTC_REG_1_12_HOUR_MODE_MASK = 0x0004 ( 4 ) + RTC_REG_1_PORO = 0x0008 ( 8 ) + RTC_REG_1_CLK_STOPPED_MASK = 0x0020 ( 32 ) + RTC_REG_1_UNUSED_MASK = 0x0040 ( 64 ) + RTC_REG_1_EXT_CLK_MODE_MASK = 0x0080 ( 128 ) + + more than one status bit can be sent. + Ex: to set RTC_REG_1_PORO and RTC_REG_1_CLK_STOPPED_MASK, status should be 40 (0x0028). + + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSD_ID_HD_RTC_CTL_REG1_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD RTC Control Register 1 status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("HD RTC Control Register 1 status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_rtc_ctl_reg3_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD RTC control register 3 status override command + Constraints: + Must be logged into HD. + + @param status: status is a bit map containing a set of bits that represent status + from the RTC status register. Specified below + + RTC_REG_3_BLF_MASK = 0x0004 ( 4 ) + + more than one status bit can be sent + + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x0000FFFF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSD_ID_HD_RTC_CTL_REG3_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD RTC Control Register 3 status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("HD RTC Control Register 3 status status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi-dialin/hd/service_record.py =================================================================== diff -u --- leahi-dialin/hd/service_record.py (revision 0) +++ leahi-dialin/hd/service_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,348 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file service_record.py +# +# @author (last) Dara Navaei +# @date (last) 26-Feb-2024 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsHD +from ..utils.conversions import integer_to_bytearray + + +@unique +class ServiceLocation(DialinEnum): + SERVICE_LOCATION_FACTORY = 0 + SERVICE_LOCATION_FIELD = 1 + + +class HDServiceNVRecords(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for service record commands. + """ + + # The default service time interval is 6 months in seconds + _DEFAULT_SERVICE_INTERVAL_S = 15768000 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_SERVICE_LOCATION = ServiceLocation.SERVICE_LOCATION_FACTORY.value + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _FIRMWARE_STACK_NAME = 'HD' + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_service_in_progress = False + self._write_fw_data_to_excel = True + self._service_data = 0 + self._raw_service_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.hd_service_record = self._prepare_hd_service_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_SERVICE_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_service_sync) + self.hd_service_record_timestamp = 0.0 + + def cmd_reset_hd_service_record(self) -> bool: + """ + Handles resetting HD service record. + + @return: True if successful, False otherwise + """ + self.hd_service_record = self._prepare_hd_service_record() + self.hd_service_record = self._utilities.reset_fw_system_service_record(self.hd_service_record) + status = self.cmd_set_hd_service_record(self.hd_service_record) + + return status + + def cmd_request_hd_service_record(self) -> int: + """ + Handles getting HD service record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_service_in_progress is not True: + self._is_getting_service_in_progress = True + # Clear the list for the next call + self._raw_service_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_SERVICE_RECORD.value) + + self.logger.debug('Getting HD service record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_hd_service_record_crc_override(self, crc: int) -> bool: + """ + Handles setting HD service_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + hd_record = integer_to_bytearray(NVRecordsHD.NVDATAMGMT_SERVICE_RECORD.value) + payload = reset_byte_array + crc_value + hd_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding HD service record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_hd_service_sync(self, message, timestamp=0.0): + """ + Handles published HD system record messages. HD system records are captured for + processing and updating the HD system record. + + @param message: published HD system record data message + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._service_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_service_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_service_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_service_record, self._raw_service_record) + self.hd_service_record_timestamp = timestamp + self._handler_received_complete_hd_service_record() + + + @publish(["hd_service_record_timestamp","hd_service_record"]) + def _handler_received_complete_hd_service_record(self): + """ + Publishes the received service record + + @return: None + """ + self.logger.debug("Received a complete hd service record.") + + def cmd_set_hd_service_record(self, hd_service_record: OrderedDict) -> bool: + """ + Handles updating the HD system and sends it to FW. + + @param hd_service_record: (OrderedDict) the hd service record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + self.logger.debug('Setting HD service record') + + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_service_record) + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_SERVICE_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("HD ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending HD service record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending HD service record.") + return True + + def _prepare_hd_service_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main HD service record. + + @return: (OrderedDict) an assembled hd service record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_service_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' bool: + """ + Handles setting the service record data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG service record and set and observer class to callback when the record is read back + self.cmd_request_hd_service_record() + observer = NVUtilsObserver("hd_service_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_service_record, report_address, + self._utilities.SERVICE_RECORD_TAB_NAME) + + ret = self.cmd_set_hd_service_record(self.hd_service_record) + return ret + + def cmd_get_hd_service_record(self, report_address: str = None): + """ + Publicly accessible function to request the HD service record and write the record to excel. + + @param report_address: the address that the report needs to be written to. The default is None so it picks an + address and writes the excel report. + + @return: none + """ + + # Create the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.SERVICE_RECORD_TAB_NAME, + report_address, protect_sheet=True) + + # Create an object of the observer class to observe the dictionary + observer = NVUtilsObserver("hd_service_record") + # Attach the observer to the list + self.attach(observer) + + # Request the latest software configuration record from firmware + self.cmd_request_hd_service_record() + # Wait until data has been received from firmware + while not observer.received: + sleep(0.1) + # Write the updated values from excel to firmware + self._utilities.write_fw_record_to_excel(self.hd_service_record) Index: leahi-dialin/hd/sw_configs.py =================================================================== diff -u --- leahi-dialin/hd/sw_configs.py (revision 0) +++ leahi-dialin/hd/sw_configs.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,341 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file sw_configs.py +# +# @author (last) Dara Navaei +# @date (last) 25-Apr-2023 +# @author (original) Dara Navaei +# @date (original) 01-Mar-2022 +# +############################################################################ + + +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver + + +@unique +class HDSWConfigs(DialinEnum): + # NOTE: NUM_OF enum has been removed because it should be a part of the software configuration + # structure since the members of this class is for looped to create the dictionary automatically + SW_CONFIG_DISABLE_ALARM_AUDIO = 0 + SW_CONFIG_DISABLE_AIR_TRAP_LEVELING_ALARM = 1 + SW_CONFIG_DISABLE_ACK_ERRORS = 2 + SW_CONFIG_ENABLE_WORN_OUT_CARTRIDGE = 3 + SW_CONFIG_DISABLE_MOTOR_CURRNT_CHECKS = 4 + SW_CONFIG_DISABLE_PUMP_FLOW_CHECKS = 5 + SW_CONFIG_DISABLE_PUMP_DIRECTION_CHECKS = 6 + SW_CONFIG_DISABLE_PUMP_SPEED_CHECKS = 7 + SW_CONFIG_DISABLE_SYRINGE_PUMP = 8 + SW_CONFIG_ENABLE_SYRINGE_PUMP_CMDS = 9 + SW_CONFIG_DISABLE_SYRINGE_PUMP_ALARMS = 10 + SW_CONFIG_DISABLE_PRESSURE_CHECKS = 11 + SW_CONFIG_DISABLE_ARTERIAL_PRESSURE_CHECK = 12 + SW_CONFIG_DISABLE_VENOUS_PRESSURE_CHECK = 13 + SW_CONFIG_DISABLE_DIALYSATE_TEMP_CHECK = 14 + SW_CONFIG_DISABLE_CAL_CHECK = 15 + SW_CONFIG_ENABLE_ALARM_VOLUME_DEFAULT_LOW = 16 + SW_CONFIG_DISABLE_ILLEGAL_AIR_TRAP_ALARM = 17 + SW_CONFIG_DISABLE_SELF_TESTS_AIR_BUBBLE_CHECK = 18 + SW_CONFIG_DISABLE_OCCLUSION_SELF_TEST = 19 + SW_CONFIG_DISABLE_BLOOD_LEAK_SELF_TEST = 20 + SW_CONFIG_DISABLE_BLOOD_LEAK_ALARM = 21 + SW_CONFIG_DISABLE_UI_INTERACTION = 22 + SW_CONFIG_DISABLE_SAMPLE_WATER = 23 + SW_CONFIG_DISABLE_CONSUMABLES_TESTS = 24 + SW_CONFIG_DISABLE_DRY_SELF_TESTS = 25 + SW_CONFIG_DISABLE_PRIMING = 26 + SW_CONFIG_DISABLE_WET_SELF_TEST = 27 + SW_CONFIG_ENABLE_WET_SELF_TEST_WIDER_VOLUME_TOL = 28 + SW_CONFIG_DISABLE_ULTRAFILTRATION_ALARMS = 29 + SW_CONFIG_DISABLE_BUBBLE_ALARMS = 30 + SW_CONFIG_DISABLE_ACCELEROMETERS = 31 + SW_CONFIG_DISABLE_RESERVOIRS_ALARMS = 32 + SW_CONFIG_DISABLE_CARTRIDGE_REMOVAL_STEP = 33 + SW_CONFIG_DISABLE_PUMPS_FLOW_LIMITS = 34 + SW_CONFIG_DISABLE_UI_COMM_ALARMS = 35 + SW_CONFIG_DISABLE_VOLTAGES_ALARMS = 36 + SW_CONFIG_ENABLE_1_MIN_TREATMENT = 37 + SW_CONFIG_ENABLE_BLOOD_PUMP_OPEN_LOOP = 38 + SW_CONFIG_ENABLE_DIALYSATE_INLET_PUMP_OPEN_LOOP = 39 + SW_CONFIG_DISABLE_SWITCHES_MONITOR = 40 + SW_CONFIG_ENABLE_VBA_SPECIAL_POSITION_C = 41 + SW_CONFIG_DISABLE_SERVICE_AND_DISINFECT_CHECK = 42 + SW_CONFIG_DISABLE_AIR_PUMP = 43 + + +class HDSoftwareConfigs(AbstractSubSystem): + """ + @brief Hemodialysis Device (HD) Dialin API sub-class for HD software configurations related commands. + """ + + _DEFAULT_SW_CONFIG_STATUS = 0 + _DEFAULT_CRC_VALUE = 0 + _RECORD_SPECS_BYTES = 12 + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'HD' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._sw_config_data = 0 + self._is_getting_sw_config_in_progress = False + self._raw_sw_config_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.hd_sw_config_record = self._prepare_hd_sw_configs_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_SW_CONFIG_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_sw_config_sync) + self.hd_sw_config_record_timestamp = 0.0 + + def cmd_reset_hd_sw_config_record(self) -> bool: + """ + Handles resetting HD software configuration record. + + @return: True if successful, False otherwise + """ + # Get the default software configuration dictionary + self.hd_sw_config_record = self._prepare_hd_sw_configs_record() + # Calculate the CRC for reset software configuration record + status = self._cmd_set_hd_sw_config_record() + + return status + + def _cmd_request_hd_sw_config_record(self) -> int: + """ + Handles getting HD software config record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_sw_config_in_progress is not True: + self._is_getting_sw_config_in_progress = True + # Clear the list for the next call + self._raw_sw_config_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_SW_CONFIG_RECORD.value) + + self.logger.debug('Getting HD software configuration record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG software configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_hd_sw_config_sync(self, message, timestamp=0.0): + """ + Handles published HD software configuration record messages. HD software configuration records are captured for + processing and updating the HD software configuration record. + + @param message: published HD software configuration record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._sw_config_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_sw_config_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_sw_config_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_sw_config_record, self._raw_sw_config_record) + self.hd_sw_config_record_timestamp = timestamp + self._handler_received_complete_hd_sw_config_record() + + @publish(["hd_sw_config_record_timestamp","hd_sw_config_record"]) + def _handler_received_complete_hd_sw_config_record(self): + """ + Publishes the received software configuration record + + @return: None + """ + self.logger.debug("Received a complete HD software configuration record.") + + def cmd_update_hd_sw_config_record(self, excel_report_path: str): + """ + Handles preparing the HD software configuration from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the software configuration is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + status = self._utilities.get_sw_configs_from_excel(self.hd_sw_config_record, excel_report_path, + self._utilities.NON_VOLATILE_RECORD_NAME) + # The excel document was successfully read initiate a write command + if status: + self._cmd_set_hd_sw_config_record() + else: + self.logger.debug('Could not find the software configurations file') + + def _cmd_set_hd_sw_config_record(self) -> bool: + """ + Handles updating the HD software configuration record and sends it to FW. + + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(self.hd_sw_config_record) + + self.logger.debug('Setting HD sw config record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_SW_CONFIG_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Finished sending HD software configuration record.") + return True + + def _prepare_hd_sw_configs_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of each group to make a blank HD software configuration record. + + @return: (OrderedDict) the assembled dg software configuration record + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_sw_configs_record()] + + for function in functions: + # Update the groups bytes size so far to be used for padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the software configuration record dictionary. + + @return: software configuration record dictionary and the byte size of this group + """ + groups_byte_size = 0 + name = 'sw_configs' + # Create an ordered dictionary + sw_configs = OrderedDict({name: {}}) + + # Loop through the members of the HDSWConfigs enum class + for config in HDSWConfigs.__members__: + # Insert the enum name into the dictionary with the default software config. Each config is one byte + sw_configs[name].update({config: [' dict: + """ + Gets the status of a switch + + @return: The status of all of the switches in a dictionary + """ + return self.hd_switches_status + + @publish(["hd_switches_timestamp", "hd_switches_status"]) + def _handler_switches_sync(self, message, timestamp=0.0): + """ + Handles published HD switches data messages. Switches data are captured for reference. + + @param message: published switches data message + @return: none + """ + front_door = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + pump_track = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.hd_switches_status[HDSwitchesNames.FRONT_DOOR.name] = HDSwitchStatus(front_door).value + self.hd_switches_status[HDSwitchesNames.PUMP_TRACK_SWITCH.name] = HDSwitchStatus(pump_track).value + self.hd_switches_timestamp = timestamp + + def cmd_hd_switch_status_override(self, switch: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD switch status override command + Constraints: + Must be logged into HD. + + @param switch: (int) switch ID that is status is overridden + @param status: (int) status that the switch will be overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + sw = integer_to_bytearray(switch) + st = integer_to_bytearray(status) + payload = reset_value + st + sw + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SWITCHES_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override switch status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(HDSwitchesNames(switch).name) + " to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_switches_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD switch data publication override command. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SWITCHES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD switches data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "HD Switches data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/syringe_pump.py =================================================================== diff -u --- leahi-dialin/hd/syringe_pump.py (revision 0) +++ leahi-dialin/hd/syringe_pump.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,780 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file syringe_pump.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Sean Nash +# @date (original) 12-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.hd_defs import HeparinStates, SyringePumpStates, SyringePumpOperations +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDSyringePump(AbstractSubSystem): + """ + HDSyringePump + + Hemodialysis Delivery (HD) Dialin API sub-class for syringe pump related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_SYRINGE_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_syringe_pump_data) + + self.syringe_pump_state = SyringePumpStates.SYRINGE_PUMP_INIT_STATE.value + self.heparin_state = HeparinStates.HEPARIN_STATE_OFF.value + self.syringe_pump_set_rate_ml_hr = 0.0 + self.syringe_pump_meas_rate_ml_hr = 0.0 + self.syringe_pump_position = 0 + self.syringe_pump_volume_ml = 0.0 + self.syringe_pump_safety_volume_ml = 0.0 + self.syringe_pump_home_v = 0.0 + self.syringe_pump_switch_v = 0.0 + self.syringe_pump_force_v = 0.0 + self.syringe_pump_status = 0 + self.syringe_pump_encoder_status = 0 + self.syringe_pump_adc_dac_status = 0 + self.syringe_pump_adc_read_counter = 0 + self.hd_syringe_pump_timestamp = 0.0 + + def get_syringe_pump_state(self): + """ + Gets the current syringe pump state. + + @return: latest published syringe pump state. + SYRINGE_PUMP_INIT_STATE = 0 + SYRINGE_PUMP_OFF_STATE = 1 + SYRINGE_PUMP_RETRACT_STATE = 2 + SYRINGE_PUMP_SEEK_STATE = 3 + SYRINGE_PUMP_PRIME_STATE = 4 + SYRINGE_PUMP_HEP_BOLUS_STATE = 5 + SYRINGE_PUMP_HEP_CONTINUOUS_STATE = 6 + SYRINGE_PUMP_CONFIG_FORCE_SENSOR_STATE = 7 + """ + return self.syringe_pump_state + + def get_heparin_state(self): + """ + Gets the current Heparin state. + + @return: latest published Heparin state. + HEPARIN_STATE_OFF = 0 + HEPARIN_STATE_STOPPED = 1 + HEPARIN_STATE_PAUSED = 2 + HEPARIN_STATE_INITIAL_BOLUS = 3 + HEPARIN_STATE_DISPENSING = 4 + HEPARIN_STATE_COMPLETED = 5 + HEPARIN_STATE_EMPTY = 6 + """ + return self.heparin_state + + def get_syringe_pump_set_rate(self): + """ + Gets the current set syringe pump rate. + + @return: latest published syringe pump set rate (in mL/hr). + """ + return self.syringe_pump_set_rate_ml_hr + + def get_syringe_pump_meas_rate(self): + """ + Gets the current measured syringe pump rate. + + @return: latest published syringe pump measured rate (in mL/hr). + """ + return self.syringe_pump_meas_rate_ml_hr + + def get_syringe_pump_position(self): + """ + Gets the current syringe pump position. + + @return: latest published syringe pump position (in encoder counts). + """ + return self.syringe_pump_position + + def get_syringe_pump_volume_delivered_ml(self): + """ + Gets the current syringe pump volume delivered. + + @return: latest published syringe pump volume delivered (in mL). + """ + return self.syringe_pump_volume_ml + + def get_syringe_pump_home_v(self): + """ + Gets the current syringe pump home voltage reading + + @return: latest published voltage read from the home optical sensor + """ + return self.syringe_pump_home_v + + def get_syringe_pump_switch_v(self): + """ + Gets the current syringe pump switch voltage reading + + @return: latest published voltage read from the syringe detection switch + """ + return self.syringe_pump_switch_v + + def get_syringe_pump_force_v(self): + """ + Gets the current syringe pump force voltage reading + + @return: latest published voltage read from the force sensor + """ + return self.syringe_pump_force_v + + def get_syringe_pump_safety_volume(self): + """ + Gets the current syringe pump safety volume reading + + @return: latest published safety volume calculated by HD firmware + """ + return self.syringe_pump_safety_volume_ml + + def get_syringe_pump_status(self): + """ + Gets the current syringe pump status + + @return: latest published syringe pump status by HD firmware + """ + return self.syringe_pump_status + + def get_syringe_pump_encoder_status(self): + """ + Gets the current syringe pump encoder status + + @return: latest published syringe pump encoder status by HD firmware + """ + return self.syringe_pump_encoder_status + + def get_syringe_pump_adc_dac_status(self): + """ + Gets the current syringe pump ADC & DAC status + + @return: latest published syringe pump ADC & DAC status by HD firmware + """ + return self.syringe_pump_adc_dac_status + + def get_syringe_pump_adc_read_counter(self): + """ + Gets the current syringe pump ADC read counter + + @return: latest published ADC read counter by HD firmware + """ + return self.syringe_pump_adc_read_counter + + @publish(["hd_syringe_pump_timestamp", + "syringe_pump_state", "syringe_pump_set_rate_ml_hr", + "syringe_pump_meas_rate_ml_hr", "syringe_pump_position", + "syringe_pump_volume_ml", "syringe_pump_home_v", + "syringe_pump_switch_v", "syringe_pump_force_v", + "heparin_state", "syringe_pump_safety_volume_ml", + "syringe_pump_status", "syringe_pump_encoder_status", + "syringe_pump_adc_dac_status", "syringe_pump_adc_read_counter"]) + def _handler_syringe_pump_data(self, message, timestamp=0.0): + """ + Handles published syringe pump data messages. Syringe pump data are captured + for reference. + + @param message: published syringe pump data message + @return: None + """ + + sta = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + hep = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + srt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + mrt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + pos = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + hom = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + det = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + frc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + saf = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + sts = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + + self.syringe_pump_state = sta[0] + self.heparin_state = hep[0] + self.syringe_pump_set_rate_ml_hr = srt[0] + self.syringe_pump_meas_rate_ml_hr = mrt[0] + self.syringe_pump_position = pos[0] + self.syringe_pump_volume_ml = vol[0] + self.syringe_pump_home_v = hom[0] + self.syringe_pump_switch_v = det[0] + self.syringe_pump_force_v = frc[0] + self.syringe_pump_safety_volume_ml = saf[0] + self.syringe_pump_status = (sts[0] & 0xFF000000) >> 24 + self.syringe_pump_encoder_status = (sts[0] & 0x00FF0000) >> 16 + self.syringe_pump_adc_dac_status = (sts[0] & 0x0000FF00) >> 8 + self.syringe_pump_adc_read_counter = (sts[0] & 0x000000FF) + self.hd_syringe_pump_timestamp = timestamp + + def cmd_syringe_pump_operation(self, operation: int = SyringePumpOperations.SYRINGE_PUMP_OP_STOP.value, + rate: float = 0.0, + volume: float = 0.0) -> int: + """ + Constructs and sends the syringe pump operation command + Constraints: + Must be logged into HD. + Syringe pump must be in appropriate state for the given operation. + Given rate/volume (when applicable) must be within valid range for the given operation. + The following Treatment parameters should be set before executing operations 3,4 & 5: + - Treatment Duration + - Heparin Pre-Stop Time + - Rate if volume is being sent + - Volume if rate is being sent + Failure to set these treatment parameters may result in faulting the HD device. + + @param operation: unsigned int - ID of operation being requested + @param rate: float - target rate for given operation (if applicable) + @param volume: float - target volume for given operation (if applicable) + @return: 1 if successful, zero otherwise + + Syringe pump operation IDs: + SYRINGE_PUMP_OP_STOP = 0 + SYRINGE_PUMP_OP_RETRACT = 1 + SYRINGE_PUMP_OP_SEEK = 2 + SYRINGE_PUMP_OP_PRIME = 3 + SYRINGE_PUMP_OP_BOLUS = 4 + SYRINGE_PUMP_OP_CONTINUOUS = 5 + """ + + op = integer_to_bytearray(operation) + rat = float_to_bytearray(rate) + vol = float_to_bytearray(volume) + payload = op + rat + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_OPERATION_REQUEST.value, + payload=payload) + + self.logger.debug("requesting syringe pump operation " + str(operation) + + ", rate=" + str(rate) + + ", volume=" + str(volume)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_data_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Syringe pump data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_position_override(self, position: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured position override command + Constraints: + Must be logged into HD. + + @param position: integer - position (in encoder counts) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + pos = integer_to_bytearray(position) + payload = rst + pos + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_POSITION_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured position") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(position) + " encoder counts: " + self.logger.debug("Syringe pump measured position overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_rate_override(self, rate: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured rate override command + Constraints: + Must be logged into HD. + + @param rate: float - rate (in mL/hr) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + rat = float_to_bytearray(rate) + payload = rst + rat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_RATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(rate) + " mL/hr: " + self.logger.debug("Syringe pump measured rate overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_force_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured force override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_FORCE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured force") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(volts) + " volts: " + self.logger.debug("Syringe pump measured force overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_detect_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured syringe detect override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_SYRINGE_DETECT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured syringe detection") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(volts) + " volts: " + self.logger.debug("Syringe pump measured syringe detection overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_home_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured home override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_HOME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured home") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(volts) + " volts: " + self.logger.debug("Syringe pump measured home overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_volume_override(self, volume: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured volume override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured volume delivered") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(volume) + " mL: " + self.logger.debug("Syringe pump measured volume overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump status override command + Constraints: + Must be logged into HD. + + @param status: integer - status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("Syringe pump status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_encoder_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump encoder status override command + Constraints: + Must be logged into HD. + + @param status: integer - encoder status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ENCODER_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump encoder status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("Syringe pump encoder status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_adc_dac_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump ADC & DAC status override command + Constraints: + Must be logged into HD. + + @param status: integer - ADC and DAC status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ADC_DAC_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump ADC & DAC status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(status) + self.logger.debug("Syringe pump ADC & DAC status overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_adc_read_counter_override(self, counter: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump ADC read counter override command + Constraints: + Must be logged into HD. + + @param counter: integer - status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + ctr = integer_to_bytearray(counter & 0x000000FF) + payload = rst + ctr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ADC_READ_COUNTER_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump ADC read counter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(counter) + self.logger.debug("Syringe pump ADC read counter overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_syringe_pump_dac_ref_voltage(self) -> int: + """ + Constructs and sends the set syringe pump DAC vRef. The value the DAC is set to is within HD Calibration Record. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_FORCE_SENSOR_DAC_CALIBRATE.value, + ) + + self.logger.debug("Set HD syringe pump DAC reference voltage") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Syringe pump DAC reference voltage set.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_heprin_target_rate_override(self, rate: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the heprin bolus target rate value override command + Constraints: + Must be logged into HD. + + @param rate: (float) the heparin bolus target rate to be set in mL/hour + @param reset: (int) 1 to reset a previous override, 0 to override + @return 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlu = float_to_bytearray(rate) # HD expects the rate in mL/hour + payload = reset_value + vlu + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_HEPARIN_BOLUS_TARGET_RATE_OVERRIDE.value, + payload=payload) + self.logger.debug("Overriding heprin bolus target rate value override") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Heprin bolus target value override Timeout!!!") + return False Index: leahi-dialin/hd/system_record.py =================================================================== diff -u --- leahi-dialin/hd/system_record.py (revision 0) +++ leahi-dialin/hd/system_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,357 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file system_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 03-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger +from time import sleep + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsHD +from ..utils.conversions import integer_to_bytearray + + +@unique +class MFGLocation(DialinEnum): + MFG_LOCATION_FACTORY = 0 + + +class HDSystemNVRecords(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for system record commands. + """ + + _RECORD_SPECS_BYTES = 12 + _DEFAULT_MFG_LOCATION = MFGLocation.MFG_LOCATION_FACTORY.value + _MAX_PN_BYTES = 10 + _MAX_SN_BYTES = 20 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + _FIRMWARE_STACK_NAME = 'HD' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._is_getting_sys_in_progress = False + self._sys_data = 0 + self._raw_system_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # System main record + self.hd_system_record = self._prepare_hd_system_record() + self.hd_system_record_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_SYSTEM_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_hd_system_sync) + + def cmd_reset_hd_system_record(self) -> bool: + """ + Handles resetting HD system record. + + @return: True if successful, False otherwise + """ + self.hd_system_record = self._prepare_hd_system_record() + self.hd_system_record = self._utilities.reset_fw_system_service_record(self.hd_system_record) + status = self.cmd_set_hd_system_record(self.hd_system_record) + + return status + + def get_hd_system_record(self) -> dict: + """ + Handles getting HD system record per user's request. + NOTE: In order to get the latest system record, use cmd_request_hd_system_record first + to fetch the system record from the firmware. + + @return: HD system record dictionary + """ + return self.hd_system_record['system_record'] + + def cmd_get_hd_system_record_report(self, report_destination: str = None): + """ + Handles getting HD system_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.SYSTEM_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + + observer = NVUtilsObserver("hd_system_record") + # Attach the observer to the list + self.attach(observer) + + # Request the HD system record and set and observer class to callback when the system record is read back + self.cmd_request_hd_system_record() + + while not observer.received: + sleep(0.1) + # Pass the HD system record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.hd_system_record) + + def cmd_request_hd_system_record(self) -> int: + """ + Handles getting HD calibration_record data from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_sys_in_progress is not True: + # Receiving the system record is in progress + self._is_getting_sys_in_progress = True + # Clear the list for the next call + self._raw_system_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_SYSTEM_RECORD.value) + + self.logger.debug('Getting HD system record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_hd_system_record_crc_override(self, crc: int) -> bool: + """ + Handles setting HD system_record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + hd_record = integer_to_bytearray(NVRecordsHD.NVDATAMGMT_SYSTEM_RECORD.value) + payload = reset_byte_array + crc_value + hd_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding HD system record CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_hd_system_sync(self, message, timestamp=0.0): + """ + Handles published HD system record messages. HD system records are captured for + processing and updating the HD system record. + + @param message: published HD system record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._sys_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_system_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_sys_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_system_record, self._raw_system_record) + self.hd_system_record_timestamp = timestamp + self._handler_received_complete_hd_system_record() + + @publish(["hd_system_record_timestamp","hd_system_record"]) + def _handler_received_complete_hd_system_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete hd system record.") + + def cmd_set_hd_system_record_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the system data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_hd_system_record() + observer = NVUtilsObserver("hd_system_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_system_record, report_address, + self._utilities.SYSTEM_RECORD_TAB_NAME) + + ret = self.cmd_set_hd_system_record(self.hd_system_record) + return ret + + def cmd_set_hd_system_record(self, hd_system_record: OrderedDict) -> bool: + """ + Handles updating the HD system and sends it to FW. + + @param hd_system_record: (OrderedDict) the hd system record to be sent + @return: True upon success, False otherwise + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_system_record) + + self.logger.debug('Setting HD system record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_SYSTEM_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("HD ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending HD system record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending HD system record.") + return True + + def _prepare_hd_system_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main HD system record. + + @return: (OrderedDict) the assembled hd system record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_hd_system_group()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' dict: + """ + Gets the status of HD temperature sensors + + @return: The HD temperatures values in a dictionary + """ + return self.hd_temperatures + + @publish(["hd_temperatures_timestamp","hd_temperatures"]) + def _handler_temperatures_sync(self, message,timestamp=0.0): + """ + Handles published HD temperatures data messages. Temperatures data are captured for reference. + + @param message: published temperatures data message + @return: none + """ + onboard_thermistor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + power_supply_thermistor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + fpga_board_sensor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + venous_pressure_sensor_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + arterial_sensor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + self.hd_temperatures[HDTemperaturesNames.THERMISTOR_ONBOARD_NTC.name] = onboard_thermistor + self.hd_temperatures[HDTemperaturesNames.THERMISTOR_POWER_SUPPLY_1.name] = power_supply_thermistor + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_FPGA_BOARD.name] = fpga_board_sensor + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_VENOUS_PRESS_TEMP.name] = venous_pressure_sensor_temp + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_ARTERIAL_PRESS_TEMP.name] = arterial_sensor + self.hd_temperatures_timestamp = timestamp + + def cmd_temperatures_value_override(self, sensor: int, value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD temperatures value override command + Constraints: + Must be logged into HD. + + @param sensor: (int) sensor ID that is status is overridden + @param value: (int) value that the temperature sensor will be overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + snsr = integer_to_bytearray(sensor) + vl = float_to_bytearray(value) + payload = reset_value + vl + snsr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TEMPERATURES_VALUE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override temperature sensor value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(HDTemperaturesNames(sensor).name) + " to: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_temperatures_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD temperatures data publication override command. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TEMPERATURES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD temperatures data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "HD temperatures data broadcast interval overridden to " + str_res + " ms: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/treatment.py =================================================================== diff -u --- leahi-dialin/hd/treatment.py (revision 0) +++ leahi-dialin/hd/treatment.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,1727 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file treatment.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from .constants import RESET, NO_RESET + + +class HDTreatment(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for treatment related commands. + + """ + + # Treatment Parameter IDs + HD_TREATMENT_PARAMETER_BLOOD_FLOW = 0 + HD_TREATMENT_PARAMETER_DIALYSATE_FLOW = 1 + HD_TREATMENT_PARAMETER_TREATMENT_DURATION = 2 + HD_TREATMENT_PARAMETER_HEPARIN_PRE_STOP_TIME = 3 + HD_TREATMENT_PARAMETER_SALINE_BOLUS_VOLUME = 4 + HD_TREATMENT_PARAMETER_ACID_CONCENTRATE = 5 + HD_TREATMENT_PARAMETER_BICARB_CONCENTRATE = 6 + HD_TREATMENT_PARAMETER_DIALYZER_TYPE = 7 + HD_TREATMENT_PARAMETER_HEPARIN_TYPE = 8 + HD_TREATMENT_PARAMETER_BP_MEAS_INTERVAL = 9 + HD_TREATMENT_PARAMETER_RINSEBACK_FLOW_RATE = 10 + HD_TREATMENT_PARAMETER_ART_PRESSURE_LIMIT_WINDOW = 11 + HD_TREATMENT_PARAMETER_VEN_PRESSURE_LIMIT_WINDOW = 12 + HD_TREATMENT_PARAMETER_VEN_PRESSURE_LIMIT_ASYMMETRIC = 13 + HD_TREATMENT_PARAMETER_HEPARIN_DISPENSE_RATE = 14 + HD_TREATMENT_PARAMETER_HEPARIN_BOLUS_VOLUME = 15 + HD_TREATMENT_PARAMETER_DIALYSATE_TEMPERATURE = 16 + HD_TREATMENT_PARAMETER_UF_VOLUME = 17 + + # Dialyzer Type IDs + DIALYZER_TYPE_BBRAUN_PRO_13H = 0 # BBraun Diacap Pro 13H + DIALYZER_TYPE_BBRAUN_PRO_16H = 1 # BBraun Diacap Pro 16H + DIALYZER_TYPE_BBRAUN_PRO_19H = 2 # BBraun Diacap Pro 19H + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F160NRE = 3 # Fresenius Optiflux F160NRe + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F180NRE = 4 # Fresenius Optiflux F180NRe + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F200NRE = 5 # Fresenius Optiflux F200NRe + + + # Acid Concentrate IDs + ACID_CONC_TYPE_FRESENIUS_08_1251_1 = 0 + ACID_CONC_TYPE_FRESENIUS_08_2251_0 = 1 + ACID_CONC_TYPE_FRESENIUS_08_3251_9 = 2 + + # Bicarbonate Concentrate IDs + BICARB_CONC_TYPE_FRESENIUS_CENTRISOL = 0 + + # UF states + UF_START_STATE = 0 # Start state of the ultrafiltration state machine + UF_PAUSED_STATE = 1 # Paused state of the ultrafiltration state machine + UF_RUNNING_STATE = 2 # Running state of the ultrafiltration state machine + + # Saline bolus states + SALINE_BOLUS_STATE_IDLE = 0 # No saline bolus delivery is in progress + SALINE_BOLUS_STATE_WAIT_FOR_PUMPS_STOP = 1 # Wait for pumps to stop before starting bolus + SALINE_BOLUS_STATE_IN_PROGRESS = 2 # A saline bolus delivery is in progress + SALINE_BOLUS_STATE_MAX_DELIVERED = 3 # Maximum saline bolus volume reached + + # Dialyzer Re-Prime states + DIALYZER_REPRIME_STATE_DIALYSATE_PUMPS_OFF = 0 # Turn off dialysate pumps and wait for them to stop + DIALYZER_REPRIME_STATE_PURGE_PRIOR_1 = 1 # Purge air from an interrupted reprime + DIALYZER_REPRIME_STATE_PURGE_PRIOR_2 = 2 # Purge air from an interrupted reprime + DIALYZER_REPRIME_STATE_REPRIME = 3 # Remove air from dialyzer + DIALYZER_REPRIME_STATE_PURGE_LINES = 4 # Purge air from lines after repriming dialyzer + + def __init__(self, can_interface, logger: Logger): + """ + HDTreatment constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_TREATMENT_TIME_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_time_sync) + msg_id = MsgIds.MSG_ID_TREATMENT_STATE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_state_sync) + msg_id = MsgIds.MSG_ID_SALINE_BOLUS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_saline_bolus_data_sync) + msg_id = MsgIds.MSG_ID_HD_RINSEBACK_PROGRESS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_rinseback_data_sync) + msg_id = MsgIds.MSG_ID_HD_BLOOD_PRIME_PROGRESS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_prime_data_sync) + msg_id = MsgIds.MSG_ID_HD_RECIRC_PROGRESS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_recirculate_data_sync) + msg_id = MsgIds.MSG_ID_HD_TREATMENT_STOP_TIMER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_stop_timer_data_sync) + + msg_id = MsgIds.MSG_ID_HD_RES_CURRENT_TREATMENT_PARAMETERS.value + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_dialin_ch_id, msg_id, + self._handler_treatment_current_parameters) + + self.hd_treatment_time_timestamp = 0.0 + self.hd_treatment_state_timestamp = 0.0 + self.hd_saline_bolus_timestamp = 0.0 + self.hd_rinseback_progress_timestamp = 0.0 + self.hd_blood_prime_progress_timestamp = 0.0 + self.hd_recirc_progress_timestamp = 0.0 + self.hd_treatment_stop_timer_data_timestamp = 0.0 + self.hd_res_current_treatment_parameters_timestamp = 0.0 + # treatment duration data + self.treatment_time_prescribed = 0 + self.treatment_time_elapsed = 0 + self.treatment_time_remaining = 0 + # treatment state data + self.treatment_state = 0 + self.treatment_uf_state = 0 + self.saline_bolus_state = 0 + self.heparin_state = 0 + self.rinseback_state = 0 + self.treatment_recirculate_state = 0 + self.blood_prime_state = 0 + self.treatment_end_state = 0 + self.treatment_stop_state = 0 + self.dialysis_state = 0 + self.dialyzer_reprime_state = 0 + # saline bolus status + self.saline_bolus_max_vol = 0 + self.saline_bolus_cum_vol = 0.0 + self.saline_bolus_bol_vol = 0.0 + # blood prime status + self.blood_prime_tgt_vol = 0.0 + self.blood_prime_cum_vol = 0.0 + # rinseback status + self.rinseback_tgt_vol = 0.0 + self.rinseback_cum_vol = 0.0 + self.rinseback_cur_rate = 0 + self.rinseback_timeout_secs = 0 + self.rinseback_countdown_secs = 0 + # re-circulation status + self.recirc_timeout_secs = 0 + self.recirc_countdown_secs = 0 + # treatment stop status + self.treatment_stop_timeout_secs = 0 + self.treatment_stop_timeout_countdown_secs = 0 + + self.current_treatment_param_dict = {} + self.current_blood_flow = 0 + self.current_dialysate_flow = 0 + self.current_treatment_duration = 0 + self.current_heparin_pre_stop = 0 + self.current_saline_bolus_volume = 0 + self.current_acid_concentrate = 0 + self.current_bicarb_concentrate = 0 + self.current_dialyzer_type = 0 + self.current_heparin_type = 0 + self.current_blood_pressure_measurement_interval = 0 + self.current_rinseback_flow_rate = 0 + self.current_arterial_pressure_window = 0 + self.current_venous_pressure_window = 0 + self.current_venous_pressure_asymmetric = 0 + self.current_heparin_bolus = 0 + self.current_heparin_dispense = 0 + self.current_dialysate_temp = 0 + self.current_uf_volume = 0 + + def reset(self) -> None: + """ + Reset all treatment variables + + @return: None + """ + # treatment duration data + self.treatment_time_prescribed = 0 + self.treatment_time_elapsed = 0 + self.treatment_time_remaining = 0 + # treatment state data + self.treatment_state = 0 + self.treatment_uf_state = 0 + self.saline_bolus_state = 0 + self.heparin_state = 0 + self.rinseback_state = 0 + self.treatment_recirculate_state = 0 + self.blood_prime_state = 0 + self.treatment_end_state = 0 + self.treatment_stop_state = 0 + self.dialysis_state = 0 + # saline bolus status + self.saline_bolus_max_vol = 0 + self.saline_bolus_cum_vol = 0.0 + self.saline_bolus_bol_vol = 0.0 + # blood prime status + self.blood_prime_tgt_vol = 0.0 + self.blood_prime_cum_vol = 0.0 + # rinseback status + self.rinseback_tgt_vol = 0.0 + self.rinseback_cum_vol = 0.0 + self.rinseback_cur_rate = 0 + self.rinseback_timeout_secs = 0 + self.rinseback_countdown_secs = 0 + # re-circulation status + self.recirc_timeout_secs = 0 + self.recirc_countdown_secs = 0 + # treatment stop status + self.treatment_stop_timeout_secs = 0 + self.treatment_stop_timeout_coundown_secs = 0 + + + def get_treatment_time_prescribed(self) -> int: + """ + Gets the prescribed treatment time + + @return: The prescribed treatment time + """ + return self.treatment_time_prescribed + + def get_treatment_time_elapsed(self) -> int: + """ + Gets the elapsed treatment time + + @return: The elapsed treatment time + """ + return self.treatment_time_elapsed + + def get_treatment_time_remaining(self) -> int: + """ + Gets the remaining treatment time + + @return: The remaining treatment time + """ + return self.treatment_time_remaining + + def get_treatment_state(self) -> int: + """ + Gets the current treatment state + + @return: The current treatment state ID + """ + return self.treatment_state + + def get_treatment_UF_state(self) -> int: + """ + Gets the current treatment ultrafiltration state + + @return: The current treatment ultrafiltration state ID + """ + return self.treatment_uf_state + + def get_treatment_saline_bolus_state(self) -> int: + """ + Gets the current treatment saline bolus state + + @return: The current treatment saline bolus state ID + """ + return self.saline_bolus_state + + def get_treatment_heparin_state(self) -> int: + """ + Gets the current treatment Heparin state + + @return: The current treatment Heparin state ID + """ + return self.heparin_state + + def get_treatment_rinseback_state(self) -> int: + """ + Gets the current treatment rinseback state + + @return: The current treatment rinseback state ID + """ + return self.rinseback_state + + def get_treatment_recirculate_state(self) -> int: + """ + Gets the current treatment recirculate state + + @return: The current treatment recirculate state ID + """ + return self.treatment_recirculate_state + + def get_treatment_blood_prime_state(self) -> int: + """ + Gets the current treatment blood prime state + + @return: The current treatment blood prime state ID + """ + return self.blood_prime_state + + def get_treatment_end_state(self) -> int: + """ + Gets the current treatment end state + + @return: The current treatment end state ID + """ + return self.treatment_end_state + + def get_treatment_stop_state(self) -> int: + """ + Gets the current treatment stop state + + @return: The current treatment stop state ID + """ + return self.treatment_stop_state + + def get_dialysis_state(self) -> int: + """ + Gets the current treatment dialysis state + + @return: The current treatment dialysis state ID + """ + return self.dialysis_state + + def get_dialyzer_reprime_state(self) -> int: + """ + Gets the current treatment dialyzer re-prime state + + @return: The current treatment dialyzer re-prime state ID + """ + return self.dialyzer_reprime_state + + def get_saline_bolus_max_volume(self) -> int: + """ + Returns maximum volume (in mL) saline that can be delivered to a patient + + @return: The maximum saline bolus volume + """ + return self.saline_bolus_max_vol + + def get_saline_bolus_cumulative_volume_delivered(self) -> float: + """ + Returns cumulative volume (in mL) of saline delivered + + @return: The cumulative saline volume delivered + """ + return self.saline_bolus_cum_vol + + def get_saline_bolus_volume_delivered(self) -> float: + """ + Returns bolus volume (in mL) of saline delivered + + @return: The bolus saline volume delivered + """ + return self.saline_bolus_bol_vol + + def get_blood_prime_target_volume(self) -> float: + """ + Returns blood prime target volume (in mL) + + @return: The blood prime target volume + """ + return self.blood_prime_tgt_vol + + def get_blood_prime_volume_delivered(self) -> float: + """ + Returns blood prime volume (in mL) delivered + + @return: The blood prime volume delivered + """ + return self.blood_prime_cum_vol + + def get_rinseback_target_volume(self) -> float: + """ + Returns rinseback target volume (in mL) + + @return: The rinseback target volume + """ + return self.rinseback_tgt_vol + + def get_rinseback_volume_delivered(self) -> float: + """ + Returns rinseback volume (in mL) delivered + + @return: The rinseback volume delivered + """ + return self.rinseback_cum_vol + + def get_rinseback_current_rate(self) -> int: + """ + Returns rinseback current rate (in mL/min) + + @return: The rinseback current rate + """ + return self.rinseback_cur_rate + + def get_rinseback_timeout(self) -> int: + """ + Returns rinseback timeout period (in seconds) + + @return: The rinseback timeout period + """ + return self.rinseback_timeout_secs + + def get_rinseback_timeout_countdown(self) -> int: + """ + Returns rinseback timeout countdown (in seconds) + + @return: The rinseback timeout countdown + """ + return self.rinseback_countdown_secs + + def get_recirc_timeout(self) -> int: + """ + Returns recirc timeout (in seconds) + + @return: The recirc timeout + """ + return self.recirc_timeout_secs + + def get_recirc_timeout_countdown(self) -> int: + """ + Returns recirc timeout countdown (in seconds) + + @return: The recirc timeout countdown + """ + return self.recirc_countdown_secs + + def get_treatment_stop_timeout(self) -> int: + """ + Returns treatment stop timeout (in seconds) + + @return: The treatment stop timeout + """ + return self.treatment_stop_timeout_secs + + def get_treatment_stop_timeout_countdown(self) -> int: + """ + Returns treatment stop timeout countdown (in seconds) + + @return: The treatment stop timeout countdown + """ + return self.treatment_stop_timeout_countdown_secs + + + def get_current_treatment_parameters(self) -> dict: + """ + Returns current treatment parameters in a dictionary + + @return: self.current_treatment_param_dict + """ + self.current_treatment_param_dict = { + "blood_flow": self.current_blood_flow, + "dialysate_flow" : self.current_dialysate_flow, + "treatment_duration" : self.current_treatment_duration, + "heparin_pre_stop" : self.current_heparin_pre_stop, + "saline_bolus" : self.current_saline_bolus_volume, + "acid_concentrate" : self.current_acid_concentrate, + "bicarb_concetrate" : self.current_bicarb_concentrate, + "dialyzer_type" : self.current_dialyzer_type, + "heparin_type" : self.current_heparin_type, + "blood_pressure_interval" : self.current_blood_pressure_measurement_interval, + "rinseback_flow_rate" : self.current_rinseback_flow_rate, + "arterial_pressure_window" : self.current_arterial_pressure_window, + "venous_pressure_window" : self.current_venous_pressure_window, + "venous_pressure_asymmetric" : self.current_venous_pressure_asymmetric, + "heparin_bolus" : self.current_heparin_bolus, + "heparin_dispense" : self.current_heparin_dispense, + "dialysate_temp" : self.current_dialysate_temp, + "uf_volume" : self.current_uf_volume + } + + return self.current_treatment_param_dict + + + @publish([ + "hd_treatment_time_timestamp", + "treatment_time_prescribed", + "treatment_time_elapsed", + "treatment_time_remaining" + ]) + def _handler_treatment_time_sync(self, message, timestamp=0.0): + """ + Handles published treatment time data messages. treatment time data are captured + for reference. + + @param message: published treatment time data message + @return: None + """ + + tot = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ela = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rem = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + self.treatment_time_prescribed = tot[0] + self.treatment_time_elapsed = ela[0] + self.treatment_time_remaining = rem[0] + self.hd_treatment_time_timestamp = timestamp + + @publish([ + "hd_treatment_state_timestamp", + "treatment_state", + "treatment_uf_state", + "saline_bolus_state", + "heparin_state", + "rinseback_state", + "treatment_recirculate_state", + "blood_prime_state", + "treatment_end_state", + "treatment_stop_state", + "dialysis_state", + "dialyzer_reprime_state" + ]) + def _handler_treatment_state_sync(self, message, timestamp=0.0): + """ + Handles published treatment state data messages. treatment state data are captured + for reference. + + @param message: published treatment state data message + @return: none + """ + + tst = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ufs = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + hep = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + rin = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + rec = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + bpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + txe = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + txs = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + + self.treatment_state = tst[0] + self.treatment_uf_state = ufs[0] + self.saline_bolus_state = bol[0] + self.heparin_state = hep[0] + self.rinseback_state = rin[0] + self.treatment_recirculate_state = rec[0] + self.blood_prime_state = bpr[0] + self.treatment_end_state = txe[0] + self.treatment_stop_state = txs[0] + self.dialysis_state = dia[0] + self.hd_treatment_state_timestamp = timestamp + + @publish([ + "hd_saline_bolus_timestamp", + "saline_bolus_max_vol", + "saline_bolus_cum_vol", + "saline_bolus_bol_vol" + ]) + def _handler_saline_bolus_data_sync(self, message, timestamp=0.0): + """ + Handles published saline bolus data messages. Saline bolus data are captured + for reference. + + @param message: published saline bolus data message + @return: none + """ + + mxm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cum = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + self.saline_bolus_max_vol = mxm[0] + self.saline_bolus_cum_vol = cum[0] + self.saline_bolus_bol_vol = bol[0] + self.hd_saline_bolus_timestamp = timestamp + + @publish([ + "hd_rinseback_progress_timestamp", + "rinseback_tgt_vol", + "rinseback_cum_vol", + "rinseback_cur_rate", + "rinseback_timeout_secs", + "rinseback_countdown_secs" + ]) + def _handler_rinseback_data_sync(self, message, timestamp=0.0): + """ + Handles published rinseback data (progress) messages. Rinseback data are captured + for reference. + + @param message: published rinseback data message + @return: none + """ + + tgt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cum = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rat = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + cdn = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + self.rinseback_tgt_vol = tgt[0] + self.rinseback_cum_vol = cum[0] + self.rinseback_cur_rate = rat[0] + self.rinseback_timeout_secs = tmo[0] + self.rinseback_countdown_secs = cdn[0] + self.hd_rinseback_progress_timestamp = timestamp + + @publish([ + "hd_blood_prime_progress_timestamp", + "blood_prime_tgt_vol", + "blood_prime_cum_vol" + ]) + def _handler_blood_prime_data_sync(self, message, timestamp=0.0): + """ + Handles published blood prime data (progress) messages. Blood prime data are captured + for reference. + + @param message: published blood prime data message + @return: none + """ + + tgt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cum = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.blood_prime_tgt_vol = tgt[0] + self.blood_prime_cum_vol = cum[0] + self.hd_blood_prime_progress_timestamp = timestamp + + @publish([ + "hd_recirc_progress_timestamp", + "recirc_timeout_secs", + "recirc_countdown_secs" + ]) + def _handler_recirculate_data_sync(self, message, timestamp=0.0): + """ + Handles published recirculate data (progress) messages. Recirculate data are captured + for reference. + + @param message: published recirculate data message + @return: none + """ + + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cdn = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.recirc_timeout_secs = tmo[0] + self.recirc_countdown_secs = cdn[0] + self.hd_recirc_progress_timestamp = timestamp + + @publish(["hd_treatment_stop_timer_data_timestamp","treatment_stop_timeout_secs", "treatment_stop_timeout_countdown_secs"]) + def _handler_treatment_stop_timer_data_sync(self, message, timestamp=0.0) -> None: + """ + Handles published treatment stop progress data messages. + + @param message: published treatment stop progress data message + @return: None + """ + + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cnd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.treatment_stop_timeout_secs = tmo[0] + self.treatment_stop_timeout_countdown_secs = cnd[0] + self.hd_treatment_stop_timer_data_timestamp = timestamp + + @publish(["hd_res_current_treatment_parameters_timestamp", "accepted", "current_blood_flow", "current_dialysate_flow", + "current_treatment_duration", "current_heparin_pre_stop", "current_saline_bolus_volume", + "current_acid_concentrate", "current_bicarb_concentrate", "current_dialyzer_type", + "current_heparin_type", "current_blood_pressure_measurement_interval", "current_rinseback_flow_rate", + "current_arterial_pressure_window", "current_venous_pressure_window", "current_venous_pressure_asymmetric", + "current_heparin_bolus", "current_heparin_dispense", "current_dialysate_temp", "current_uf_volume"]) + def _handler_treatment_current_parameters(self, message, timestamp=0.0) -> None: + """ + Handles published current treatment parameters messages. + + @param message: published current treatment parameters data message. + @return: None + """ + accepted = struct.unpack(' int: + """ + Constructs and sends the set blood flow rate treatment parameter command. + This will only set the treatment parameter setting. It will not immediately + set the blood pump on with this set point. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 100 and 500 mL/min + + @param flow: integer - set blood flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BLOOD_FLOW) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting blood flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + str_flo = str(flow) + self.logger.debug("Blood flow rate parameter set to " + str_flo + " mL/min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_treatment_param_dialysate_flow_rate(self, flow: int) -> int: + """ + Constructs and sends the set dialysate flow rate treatment parameter command. + This will only set the treatment parameter setting. It will not immediately + set the dialysate inlet pump on with this set point. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 100 and 600 mL/min + + @param flow: integer - set blood flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYSATE_FLOW) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialysate flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_flo = str(flow) + self.logger.debug("Dialysate flow rate parameter set to " + str_flo + " mL/min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_treatment_param_duration(self, duration: int) -> int: + """ + Constructs and sends the set treatment duration parameter command. + Constraints: + Must be logged into HD. + Duration must be positive integer and should be between 60 and 480 min + + @param duration: integer - set treatment duration (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_TREATMENT_DURATION) + dur = integer_to_bytearray(duration) + payload = par + dur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting treatment duration") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_dur = str(duration) + self.logger.debug("Treatment duration parameter set to " + str_dur + " min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_heparin_pre_stop_time(self, pre_stop: int) -> int: + """ + Constructs and sends the set Heparin pre-stop time parameter command. + Constraints: + Must be logged into HD. + Pre-stop time for Heparin must be positive integer and should be between 0 and 120 min + + @param pre_stop: integer - set Heparin pre-stop time (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_PRE_STOP_TIME) + sto = integer_to_bytearray(pre_stop) + payload = par + sto + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin pre-stop time") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_sto = str(pre_stop) + self.logger.debug("Heparin pre-stop time parameter set to " + str_sto + " min: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_saline_bolus_volume(self, volume: int) -> int: + """ + Constructs and sends the set saline bolus volume parameter command. + Constraints: + Must be logged into HD. + Volume must be positive integer and should be between 0 and 300 mL + + @param volume: integer - set saline bolus volume (in mL) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_SALINE_BOLUS_VOLUME) + vol = integer_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting saline bolus volume") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Saline bolus volume parameter set to " + str_vol + " mL: " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_acid_concentrate(self, acid: int) -> int: + """ + Constructs and sends the set acid concentrate parameter command. + Constraints: + Must be logged into HD. + Acid ID must be positive integer and should be between 0 and 2 (see below) + ACID_CONC_TYPE_FRESENIUS_08_1251_1 = 0 + ACID_CONC_TYPE_FRESENIUS_08_2251_0 = 1 + ACID_CONC_TYPE_FRESENIUS_08_3251_9 = 2 + + @param acid: integer - set acid concentrate type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_ACID_CONCENTRATE) + acd = integer_to_bytearray(acid) + payload = par + acd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting acid concentrate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_acd = str(acid) + self.logger.debug("Acid concentrate parameter set to " + str_acd + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_bicarb_concentrate(self, bicarb: int) -> int: + """ + Constructs and sends the set bicarbonate concentrate parameter command. + Constraints: + Must be logged into HD. + Bicarb ID must be positive integer and should be between 0 and 0 (see below) + BICARB_CONC_TYPE_FRESENIUS_CENTRISOL = 0 + + @param bicarb: integer - set bicarbonate concentrate type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BICARB_CONCENTRATE) + bic = integer_to_bytearray(bicarb) + payload = par + bic + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting bicarbonate concentrate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_bic = str(bicarb) + self.logger.debug("Bicarbonate concentrate parameter set to " + str_bic + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_dialyzer_type(self, dialyzer: int) -> int: + """ + Constructs and sends the set dialyzer type parameter command. + Constraints: + Must be logged into HD. + Dialyzer ID must be positive integer and should be between 0 and 4 (see below) + DIALYZER_TYPE_BBRAUN_PRO_13H = 0 + DIALYZER_TYPE_BBRAUN_PRO_16H = 1 + DIALYZER_TYPE_BBRAUN_PRO_19H = 2 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F160NRE = 3 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F180NRE = 4 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F200NRE = 5 + + + @param dialyzer: integer - set dialyzer type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYZER_TYPE) + dia = integer_to_bytearray(dialyzer) + payload = par + dia + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialyzer type parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_dia = str(dialyzer) + self.logger.debug("Dialyzer type parameter set to " + str_dia + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_bp_measurement_interval(self, intvl: int) -> int: + """ + Constructs and sends the set blood pressure measurement interval parameter command. + Constraints: + Must be logged into HD. + Interval must be positive integer and should be between 0 and 60 min + + @param intvl: integer - set blood pressure measurement interval (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BP_MEAS_INTERVAL) + bpi = integer_to_bytearray(intvl) + payload = par + bpi + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting BP measurement interval parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_bpi = str(intvl) + self.logger.debug("BP measurement interval parameter set to " + str_bpi + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_rinseback_flow_rate(self, flow: int) -> int: + """ + Constructs and sends the set rinseback flow rate parameter command. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 50 and 175 mL/min + + @param flow: integer - set rinseback flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_RINSEBACK_FLOW_RATE) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting rinseback flow rate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_flo = str(flow) + self.logger.debug("Rinseback flow rate parameter set to " + str_flo + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_arterial_pressure_alarm_limit_window(self, window: int) -> int: + """ + Constructs and sends the set arterial pressure alarm limit window parameter command. + Constraints: + Must be logged into HD. + Window must be integer and should be between 100 and 200 mmHg + + @param window: integer - set arterial pressure alarm limit window (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_ART_PRESSURE_LIMIT_WINDOW) + win = integer_to_bytearray(window) + payload = par + win + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting arterial pressure alarm limit window parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_win = str(window) + self.logger.debug("Arterial pressure alarm limit window parameter set to " + str_win + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_venous_pressure_alarm_limit_window(self, window: int) -> int: + """ + Constructs and sends the set venous pressure alarm limit window parameter command. + Constraints: + Must be logged into HD. + Window must be integer and should be between +100 and +200 mmHg + + @param window: integer - set venous pressure alarm limit window (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_VEN_PRESSURE_LIMIT_WINDOW) + win = integer_to_bytearray(window) + payload = par + win + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting venous pressure alarm limit window parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_win = str(window) + self.logger.debug("Venous pressure alarm limit window parameter set to " + str_win + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_venous_pressure_alarm_limit_asymmetric(self, asym: int) -> int: + """ + Constructs and sends the set venous pressure alarm limit asymmetric parameter command. + Constraints: + Must be logged into HD. + Asymmetric must be integer and should be between 20 and 30 mmHg + + @param asym: integer - set venous pressure alarm limit asymmetric (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_VEN_PRESSURE_LIMIT_ASYMMETRIC) + asy = integer_to_bytearray(asym) + payload = par + asy + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting venous pressure alarm limit asymmetric parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_asy = str(asym) + self.logger.debug("Venous pressure upper alarm limit parameter set to " + str_asy + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_heparin_dispense_rate(self, rate: float) -> int: + """ + Constructs and sends the set Heparin dispense rate parameter command. + Constraints: + Must be logged into HD. + Rate must be floating point value and should be between 0 and 1.0 mL/hr + + @param rate: float - set Heparin dispense rate (in mL/hr) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_DISPENSE_RATE) + rat = float_to_bytearray(rate) + payload = par + rat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin dispense rate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_rat = str(rate) + self.logger.debug("Heparin dispense rate parameter set to " + str_rat + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_heparin_bolus_volume(self, volume: float) -> int: + """ + Constructs and sends the set Heparin bolus volume parameter command. + Constraints: + Must be logged into HD. + Volume must be floating point value and should be between 0 and 2.0 mL + + @param volume: float - set Heparin bolus volume (in mL) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_BOLUS_VOLUME) + vol = float_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin bolus volume parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Heparin bolus volume parameter set to " + str_vol + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_dialysate_temperature(self, temp: float) -> int: + """ + Constructs and sends the set dialysate temperature parameter command. + Constraints: + Must be logged into HD. + Temperature must be floating point value and should be between 35.0 and 38.0 deg C + + @param temp: float - set dialysate temperature (in deg C) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYSATE_TEMPERATURE) + tmp = float_to_bytearray(temp) + payload = par + tmp + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialysate temperature parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_tmp = str(temp) + self.logger.debug("Dialysate temperature parameter set to " + str_tmp + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_ultrafiltration_volume(self, volume: float) -> int: + """ + Constructs and sends the set ultrafiltration volume parameter command. + Constraints: + Must be logged into HD. + Volume must be floating point value and should be between 0.0 and 8.0L + + @param volume: float - set ultrafiltration volume (in L) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_UF_VOLUME) + vol = float_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting ultrafiltration volume parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Ultrafiltration volume parameter set to " + str_vol + ": " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_treatment_time_remaining_override(self, secs_remaining: int) -> int: + """ + Constructs and sends the treatment time remaining override command + Constraints: + Must be logged into HD. + + @param secs_remaining: integer - number of seconds remaining (must be positive) + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(secs_remaining) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_TIME_REMAINING_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD treatment time remaining (in seconds).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Treatment time remaining overridden to " + str(secs_remaining) + " seconds. " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_prime_volume_delivered_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood prime volume delivered override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) of blood delivered during blood prime (must be positive) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD blood prime volume delivered (in mL).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Blood prime volume delivered overridden to " + str(volume) + " mL. " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_rinseback_volume_delivered_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the rinseback volume delivered override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) of blood returned during rinseback (must be positive) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD rinseback volume delivered (in mL).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Rinseback volume delivered overridden to " + str(volume) + " mL. " + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_blood_prime_submode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment blood prime sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment blood prime sub-mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment blood prime sub-mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_treatment_stop_submode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment stop sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_STOP_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment stop sub-mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment stop sub-mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_rinseback_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment rinseback sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment rinseback sub-mode data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment rinseback sub-mode data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_treatment_param_ranges_broadcast_interval_override(self, ms: int = 60000, reset: int = NO_RESET): + """ + Constructs and sends the treatment parameter ranges broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_RANGES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment parameter ranges data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment parameter ranges data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_treatment_time_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment time data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_TIME_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment time data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Treatment time data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_request_current_treatment_parameters(self): + """ + Constructs and sends the current treatment parameters request message + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_REQ_CURRENT_TREATMENT_PARAMETERS.value) + + self.logger.debug("Requesting current Treatment Parameters from HD.") + + # Send message + received_message = self.can_interface.send(message) + # If there is content... + if received_message is not None: + self.logger.debug("Current Treatment Parameter Request recieved.") + # response payload is OK or not OK + return True + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/ui_proxy.py =================================================================== diff -u --- leahi-dialin/hd/ui_proxy.py (revision 0) +++ leahi-dialin/hd/ui_proxy.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,1926 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file ui_proxy.py +# +# @author (last) Michael Garthwaite +# @date (last) 17-Oct-2023 +# @author (original) Sean +# @date (original) 15-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from collections import OrderedDict +from logging import Logger + +from ..common.msg_defs import MsgIds, RequestRejectReasons, MsgFieldPositions +from ..common.hd_defs import TreatmentParameters +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, float_to_bytearray, unsigned_byte_to_bytearray, short_to_bytearray, byte_to_bytearray + + +class HDUIProxy(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for ui commands. + """ + SW_COMPATIBILITY_REV = 1 + + # misc definitions + LITER_TO_ML_CONVERSION_FACTOR = 1000.0 + MAX_ALARM_VOLUME_LEVEL = 7 + + # UF pause/resume command IDs + UF_CMD_PAUSE = 0 + UF_CMD_RESUME = 1 + + # UF change option IDs + UF_CMD_CHANGE_TIME_TO_ADJUST = 0 + UF_CMD_CHANGE_RATE_TO_ADJUST = 1 + + # in-treatment change response codes + RESPONSE_REJECTED = 0 + RESPONSE_ACCEPTED = 1 + + # HD version message field positions + START_POS_MAJOR = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MAJOR = START_POS_MAJOR + 1 + START_POS_MINOR = END_POS_MAJOR + END_POS_MINOR = START_POS_MINOR + 1 + START_POS_MICRO = END_POS_MINOR + END_POS_MICRO = START_POS_MICRO + 1 + START_POS_BUILD = END_POS_MICRO + END_POS_BUILD = START_POS_BUILD + 2 + + + # FPGA + START_POS_FPGA_ID = END_POS_BUILD + END_POS_FPGA_ID = START_POS_FPGA_ID + 1 + START_POS_FPGA_MAJOR = END_POS_FPGA_ID + END_POS_FPGA_MAJOR = START_POS_FPGA_MAJOR + 1 + START_POS_FPGA_MINOR = END_POS_FPGA_MAJOR + END_POS_FPGA_MINOR = START_POS_FPGA_MINOR + 1 + START_POS_FPGA_LAB = END_POS_FPGA_MINOR + END_POS_FPGA_LAB = START_POS_FPGA_LAB + 1 + + START_POS_COMP = END_POS_FPGA_LAB + END_POS_COMP = START_POS_COMP + 4 + + @unique + class AlarmUserOptions(DialinEnum): + + ALARM_USER_ACTION_RESUME = 0 + ALARM_USER_ACTION_RINSEBACK = 1 + ALARM_USER_ACTION_END_TREATMENT = 2 + ALARM_USER_ACTION_ACK = 3 + NUMBER_OF_ALARM_USER_ACTIONS = 4 + + @unique + class RinsebackUserActions(DialinEnum): + + REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_START = 0 + REQUESTED_USER_ACTION_RINSEBACK_INCREASE_RATE = 1 + REQUESTED_USER_ACTION_RINSEBACK_DECREASE_RATE = 2 + REQUESTED_USER_ACTION_RINSEBACK_PAUSE = 3 + REQUESTED_USER_ACTION_RINSEBACK_RESUME = 4 + REQUESTED_USER_ACTION_RINSEBACK_END = 5 + REQUESTED_USER_ACTION_RINSEBACK_ADDITIONAL = 6 + REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_DISCONNECT = 7 + REQUESTED_USER_ACTION_RINSEBACK_END_TREATMENT = 8 + REQUESTED_USER_ACTION_RINSEBACK_BACK_TO_TREATMENT = 9 + NUM_OF_REQUESTED_RINSEBACK_USER_ACTIONS = 10 + + @unique + class RecircUserActions(DialinEnum): + + REQUESTED_USER_ACTION_TX_RECIRC_RECONNECT = 0 + REQUESTED_USER_ACTION_TX_RECIRC_CONFIRM_RECONNECT = 1 + REQUESTED_USER_ACTION_TX_RECIRC_RESUME_RC = 2 + REQUESTED_USER_ACTION_TX_RECIRC_END_TREATMENT = 3 + NUM_OF_REQUESTED_TX_RECIRC_USER_ACTIONS = 4 + + @unique + class TreatmentEndUserActions(DialinEnum): + REQUESTED_USER_ACTION_TX_END_RINSEBACK_START = 0 + NUM_OF_REQUESTED_TX_END_USER_ACTIONS = 1 + + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + # register function to handle HD response to UF change requests + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_ui_ch_id + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE.value, + self._handler_uf_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE.value, + self._handler_uf_change_confirm_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE.value, + self._handler_treatment_duration_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE.value, + self._handler_blood_and_dialysate_change_response) + self.can_interface.register_receiving_publication_function(channel_id, MsgIds.MSG_ID_TREATMENT_PARAM_CHANGE_RANGES_DATA.value, + self._handler_treatment_param_ranges) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_VERSION_REPONSE.value, + self._handler_hd_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS_REQUEST.value, + self._handler_treatment_param_settings) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + self._handler_treatment_param_settings_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_USER_SALINE_BOLUS_RESPONSE.value, + self._handler_saline_bolus_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER_REQUEST.value, + self._handler_uf_volume_setting_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_RINSEBACK_CMD_RESPONSE.value, + self._handler_rinseback_cmd_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_RECIRC_CMD_RESPONSE.value, + self._handler_recirc_cmd_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + self._handler_treatment_end_cmd_response) + self.can_interface.\ + register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, + MsgIds.MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_RESPONSE.value, + self._handler_treatment_end_cmd_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE.value, + self._handler_active_alarm_list_response) + + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_to_ui_ch_id, + MsgIds.MSG_ID_DG_RO_ONLY_MODE_STATUS_RESPONSE.value, + self._handler_ro_mode_status) + + self.hd_uf_settings_change_res_timestamp = 0.0 + self.hd_uf_settings_change_confirm_res_timestamp = 0.0 + self.hd_treatment_time_change_res_timestamp = 0.0 + self.hd_blood_dial_rate_change_res_timestamp = 0.0 + self.hd_treatment_param_change_ranges_res_timestamp = 0.0 + self.hd_version_res_timestamp = 0.0 + self.hd_new_treatment_params_timestamp = 0.0 + self.hd_new_treatment_params_res_timestamp = 0.0 + self.hd_saline_bolus_res_timestamp = 0.0 + self.hd_set_uf_volume_parameter_res_timestamp = 0.0 + self.hd_rinseback_cmd_res_timestamp = 0.0 + self.hd_recirc_res_timestamp = 0.0 + self.hd_tx_end_cmd_res_timestamp = 0.0 + self.hd_disinfection_standby_timestamp = 0.0 + self.hd_active_alarm_list_timestamp = 0.0 + self.ro_mode_timestamp = 0 + # initialize variables that will be populated by HD version response + self.hd_version = None + self.fpga_version = None + + # initialize treatment parameters that are seen from UI or that Dialin user sets + self.treatment_parameters = [0.0] * TreatmentParameters.NUM_OF_TREATMENT_PARAMS.value + # initialize variables that will be populated by treatment parameters response message + self.treatment_parameters_valid = False + self.treatment_parameters_reject_reasons = [0] * TreatmentParameters.NUM_OF_TREATMENT_PARAMS.value + # initialize variables that will be populated by treatment parameter ranges message + self.min_treatment_duration_min = 0 + self.max_treatment_duration_min = 0 + self.min_uf_volume_ml = 0.0 + self.max_uf_volume_ml = 0.0 + self.min_dialysate_flow_rate_ml_min = 0 + self.max_dialysate_flow_rate_ml_min = 0 + # initialize variables that will be populated by response from HD to treatment duration change request + self.duration_change_succeeded = False + self.duration_change_reject_reason = 0 + self.duration_change_time_min = 0 + self.duration_change_uf_vol_ml = 0 + # initialize variables that will be populated by response from HD to UF change request + self.uf_change_succeeded = False + self.uf_change_reject_reason = 0 + self.uf_change_volume_ml = 0.0 + self.uf_change_time_min = 0 + self.uf_change_rate_ml_min = 0.0 + self.uf_change_time_diff = 0 + self.uf_change_rate_diff = 0.0 + self.uf_old_rate_ml_min = 0.0 + # initialize variables that will be populated by response from HD to blood & dialysate flow rate change request + self.blood_and_dialysate_flow_rate_change_succeeded = False + self.blood_and_dialysate_flow_rate_change_reject_reason = 0 + self.target_blood_flow_rate = 0 + self.target_dialysate_flow_rate = 0 + # initialize variables that will be populated by response to saline bolus request + self.saline_bolus_request_succeeded = False + self.saline_bolus_request_reject_reason = 0 + self.saline_bolus_request_bolus_volume = 0 + # initialize variables that will be populated by response to rinseback command + self.rinseback_cmd_succeeded = False + self.rinseback_cmd_reject_reason = 0 + # initialize variables that will be populated by response to recirculate command + self.recirc_cmd_succeeded = False + self.recirc_cmd_reject_reason = 0 + # initialize variables that will be populated by response to treatment end command + self.treatment_end_cmd_succeeded = False + self.treatment_end_cmd_reject_reason = 0 + # initialize variables that will be populated by response to active alarm list cmd + self.active_alarm_list_succeeded = False + self.active_alarm_list_reject_reason = 0 + self.alarm_list_response = () + self.ro_mode_status = 0 + self.ro_mode_rejection_status = 0 + + self.reject_reasons = OrderedDict() + for attr in RequestRejectReasons: + self.reject_reasons[RequestRejectReasons(attr).name] = RequestRejectReasons(attr).value + self.reject_reasons = OrderedDict(sorted(self.reject_reasons.items(), key=lambda key: key[1])) + + def get_hd_version(self): + """ + Gets the hd version + + @return: The hd version + """ + return self.hd_version + + def get_fpga_version(self): + """ + Gets the fpga version + + @return: the fpga version + """ + return self.fpga_version + + def get_reject_reasons(self): + """ + Gets all possible reject reasons + + @return: OrderedDict(), {"": ... } + """ + return self.reject_reasons + + def get_treatment_parameters(self): + """ + Gets the array of treatment parameters set by user or Dialin + + @return: the array of treatment parameters + """ + return self.treatment_parameters + + def get_treatment_parameters_reject_reasons(self): + """ + Gets the array of reject reasons for treatment parameters by HD firmware + + @return: the array of reject reasons + """ + return self.treatment_parameters_reject_reasons + + def get_treatment_parameters_valid(self): + """ + Gets the T/F flag for whether treatment parameters + are considered valid by HD. + + @return: True if treatment parameters are valid, False if not + """ + return self.treatment_parameters_valid + + def get_min_treatment_duration_min(self): + """ + Gets the min treatment duration + + @return: the min treatment duration (minutes) + """ + return self.min_treatment_duration_min + + def get_max_treatment_duration_min(self): + """ + Gets the max treatment duration + + @return: The max treatment duration (minutes) + """ + return self.max_treatment_duration_min + + def get_min_uf_volume_ml(self): + """ + Gets the min uf volume + + @return: the min uf volume (mL) + """ + return self.min_uf_volume_ml + + def get_max_uf_volume_ml(self): + """ + Gets the max uf volume + + @return: The max uf volume (mL) + """ + return self.max_uf_volume_ml + + def get_min_dialysate_flow_rate_ml_min(self): + """ + Gets the min dialysate flow rate + + @return: The min dialysate flow rate (mL/min) + """ + return self.min_dialysate_flow_rate_ml_min + + def get_max_dialysate_flow_rate_ml_min(self): + """ + Gets the max dialysate flow rate + + @return: The max dialysate flow rate (mL/min) + """ + return self.max_dialysate_flow_rate_ml_min + + def get_duration_change_succeeded(self): + """ + Gets the duration change succeeded status + + @return: (bool) The duration change succeeded status + """ + return self.duration_change_succeeded + + def get_duration_change_reject_reason(self): + """ + Gets the duration change reject reason + + @return: (int) The duration change reject reason + """ + return self.duration_change_reject_reason + + def get_duration_change_time_min(self): + """ + Gets the duration change time + + @return: the duration change time (min) + """ + return self.duration_change_time_min + + def get_duration_change_uf_vol_ml(self): + """ + Gets the duration change uf vol + + @return: the duration change uf vol (mL) + """ + return self.duration_change_uf_vol_ml + + def get_uf_change_succeeded(self): + """ + Gets the uf change succeeded status + + @return: True if succeeded, False otherwise + """ + return self.uf_change_succeeded + + def get_uf_change_reject_reason(self): + """ + Gets the uf change reject reason + @return: (int) The uf change reject reason + """ + return self.uf_change_reject_reason + + def get_uf_change_volume_ml(self): + """ + Gets the uf change volume + @return: The uf change volume (mL) + """ + return self.uf_change_volume_ml + + def get_uf_change_time_min(self): + """ + Gets the uf change time + + @return: The uf change time (min) + """ + return self.uf_change_time_min + + def get_uf_change_rate_ml_min(self): + """ + Gets the uf change rate + + @return: The uf change rate (mL/min) + """ + return self.uf_change_rate_ml_min + + def get_uf_change_time_diff(self): + """ + Gets the uf change time diff + + @return: The uf change time diff + """ + return self.uf_change_time_diff + + def get_uf_change_rate_diff(self): + """ + Gets the uf change rate diff + + @return: The uf change rate diff + """ + return self.uf_change_rate_diff + + def get_blood_and_dialysate_flow_rate_change_succeeded(self): + """ + Gets the blood and dialysate flow rate change succeeded status + + @return: True if successful, False otherwise + """ + return self.blood_and_dialysate_flow_rate_change_succeeded + + def get_blood_and_dialysate_flow_rate_change_reject_reason(self): + """ + Gets the blood and dialysate flow rate change reject reason + + @return: (int) The reason for the rejection + """ + return self.blood_and_dialysate_flow_rate_change_reject_reason + + def get_target_blood_flow_rate(self): + """ + Gets the target blood flow rate + + @return: The target blood flow rate + """ + return self.target_blood_flow_rate + + def get_target_dialysate_flow_rate(self): + """ + Gets the target dialysate flow rate + + @return: The target dialysate flow rate + """ + return self.target_dialysate_flow_rate + + def get_saline_bolus_reject_reason(self): + """ + Gets the reject reason code for the saline bolus request + + @return: The reject reason code for saline bolus request + """ + return self.saline_bolus_request_reject_reason + + def get_saline_bolus_volume(self): + """ + Gets the HD f/w saline bolus volume (in mL) + + @return: The saline bolus volume (in mL) + """ + return self.saline_bolus_request_bolus_volume + + def get_active_alarm_list(self): + """ + Gets the HD alarm list. + + @return : the alarm list as a tuple + """ + return self.alarm_list_response + + @publish([ + "hd_version_res_timestamp" + "hd_version", + "fpga_version" + ]) + def _handler_hd_version(self, message, timestamp=0.0): + """ + Handler for response from HD regarding its version. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U08 Major \n + U08 Minor \n + U08 Micro \n + U16 Build \n + + @return: None if not successful, the version string if unpacked successfully + """ + major = struct.unpack('B', bytearray( + message['message'][self.START_POS_MAJOR:self.END_POS_MAJOR])) + minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_MINOR:self.END_POS_MINOR])) + micro = struct.unpack('B', bytearray( + message['message'][self.START_POS_MICRO:self.END_POS_MICRO])) + build = struct.unpack('H', bytearray( + message['message'][self.START_POS_BUILD:self.END_POS_BUILD])) + + fpga_id = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_ID:self.END_POS_FPGA_ID])) + fpga_major = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MAJOR:self.END_POS_FPGA_MAJOR])) + fpga_minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MINOR:self.END_POS_FPGA_MINOR])) + fpga_lab = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_LAB:self.END_POS_FPGA_LAB])) + compatibility = struct.unpack('i', bytearray( + message['message'][self.START_POS_COMP:self.END_POS_COMP])) + + if all([len(each) > 0 for each in [major, minor, micro, build]]): + self.hd_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}-{compatibility[0]}" + self.logger.debug(f"HD VERSION: {self.hd_version}") + + if all([len(each) > 0 for each in [fpga_major, fpga_minor, fpga_id, fpga_lab]]): + self.fpga_version = f"ID: {fpga_id[0]} v{fpga_major[0]}.{fpga_minor[0]}.{fpga_lab[0]}" + self.logger.debug(f"HD FPGA VERSION: {self.fpga_version}") + self.hd_version_res_timestamp = timestamp + + @publish([ + "hd_new_treatment_params_timestamp", + "treatment_parameters" + ]) + def _handler_treatment_param_settings(self, message, timestamp=0.0): + """ + Handler for UI msg containing user set treatment parameters. + + @param message: message from UI to HD containing user selected treatment parameters.\n + U32 blood flow rate. \n + U32 dialysate flow rate. \n + U32 treatment duration. \n + U32 Heparin pre-stop time. \n + U32 saline bolus volume. \n + U32 acid concentrate. \n + U32 bicarb concentrate. \n + U32 dialyzer type. \n + U32 heparin type. \n + U32 BP measurement interval. \n + U32 rinseback flow rate. \n + S32 arterial pressure alarm limit window. \n + S32 venous pressure alarm limit window. \n + S32 venous pressure alarm limit asymmetric. \n + F32 Heparin dispense rate. \n + F32 Heparin bolus volume. \n + F32 dialysate temperature. \n + + @return: None + """ + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dur = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + sto = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + sal = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + acd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + bic = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + dlz = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + hpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + bpi = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + rbf = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + apw = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + vpw = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + vpa = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + hdr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + hbv = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16])) + tmp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17])) + + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = dur[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = sto[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acd[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bic[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dlz[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = hpr[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bpi[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rbf[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_ART_PRES_LIMIT_WINDOW.value] = apw[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_WINDOW.value] = vpw[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_ASYMMETRIC.value] = vpa[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hdr[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hbv[0] + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = tmp[0] + self.hd_new_treatment_params_timestamp = timestamp + + @publish([ + "hd_set_uf_volume_parameter_res_timestamp" + "treatment_parameters" + ]) + def _handler_uf_volume_setting_response(self, message, timestamp=0.0): + """ + Handler for UI msg containing user set ultrafiltration volume parameter. + + @param message: message from UI to HD containing user selected ultrafiltration volume.\n + F32 ultrafiltration volume (in mL). + + @return: None + """ + tmp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_UF_VOLUME_L.value] = tmp[0] / 1000.0 + self.hd_set_uf_volume_parameter_res_timestamp = timestamp + + @publish([ + "hd_new_treatment_params_res_timestamp" + "treatment_parameters_valid", + "treatment_parameters_reject_reasons" + ]) + def _handler_treatment_param_settings_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding validation of treatment parameters. + + @param message: response message from HD regarding validity of provided treatment parameters.\n + U32 0=Treatment parameters are valid, 1=Treatment parameters are invalid. \n + U32 Reject reason code for blood flow rate (0=valid). \n + U32 Reject reason code for dialysate flow rate (0=valid). \n + U32 Reject reason code for treatment duration (0=valid). \n + U32 Reject reason code for Heparin pre-stop time (0=valid). \n + U32 Reject reason code for saline bolus volume (0=valid). \n + U32 Reject reason code for acid concentrate (0=valid). \n + U32 Reject reason code for bicarb concentrate (0=valid). \n + U32 Reject reason code for dialyzer type (0=valid). \n + U32 Reject reason code for heparin type (0=valid). \n + U32 Reject reason code for BP measurement interval (0=valid). \n + U32 Reject reason code for rinseback flow rate (0=valid). \n + U32 Reject reason code for arterial pressure alarm limit window (0=valid). \n + U32 Reject reason code for venous pressure alarm limit window (0=valid). \n + U32 Reject reason code for venous pressure alarm limit asymmetric (0=valid). \n + U32 Reject reason code for Heparin dispense rate (0=valid). \n + U32 Reject reason code for Heparin bolus volume (0=valid). \n + U32 Reject reason code for dialysate temperature (0=valid). \n + + @return: None + """ + val = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dur = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + sto = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + sal = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + acd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + bic = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + dlz = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + hpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + bpi = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + rbf = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + apw = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + vpw = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + vpa = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + hdr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16])) + hbv = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17])) + tmp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18])) + + if val[0] == 1: + self.treatment_parameters_valid = True + else: + self.treatment_parameters_valid = False + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = dur[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = sto[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acd[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bic[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dlz[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = hpr[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bpi[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rbf[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_ART_PRES_LIMIT_WINDOW.value] = apw[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_WINDOW.value] = vpw[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_ASYMMETRIC.value] = vpa[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hdr[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hbv[0] + self.treatment_parameters_reject_reasons[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = tmp[0] + self.hd_new_treatment_params_res_timestamp = timestamp + + @publish([ + "hd_treatment_param_change_ranges_res_timestamp", + "min_treatment_duration_min", + "max_treatment_duration_min", + "min_uf_volume_ml", + "max_uf_volume_ml", + "min_dialysate_flow_rate_ml_min", + "max_dialysate_flow_rate_ml_min" + ]) + def _handler_treatment_param_ranges(self, message, timestamp=0.0): + """ + Handler for response from HD regarding valid treatment parameter ranges. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U32 Minimum treatment duration setting (in min.). \n + U32 Maximum treatment duration setting (in min.). \n + F32 Minimum ultrafiltration volume (in mL). \n + F32 Maximum ultrafiltration volume (in mL). \n + U32 Minimum dialysate flow rate (in mL/min). \n + U32 Maximum dialysate flow rate (in mL/min). + + @return: None + """ + mintime = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + maxtime = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + minufvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + maxufvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mindialrt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + maxdialrt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + + self.min_treatment_duration_min = mintime[0] + self.max_treatment_duration_min = maxtime[0] + self.min_uf_volume_ml = minufvol[0] + self.max_uf_volume_ml = maxufvol[0] + self.min_dialysate_flow_rate_ml_min = mindialrt[0] + self.max_dialysate_flow_rate_ml_min = maxdialrt[0] + self.hd_treatment_param_change_ranges_res_timestamp = timestamp + + @publish([ + "hd_saline_bolus_res_timestamp", + "saline_bolus_request_succeeded", + "saline_bolus_request_reject_reason", + "saline_bolus_request_bolus_volume" + ]) + def _handler_saline_bolus_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding saline bolus request. + + @param message: response message from HD regarding saline bolus request.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 Saline bolus volume (mL) + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + if rsp[0] == 1: + self.saline_bolus_request_succeeded = True + else: + self.saline_bolus_request_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.saline_bolus_request_reject_reason = RequestRejectReasons(rea[0]) + self.saline_bolus_request_bolus_volume = vol[0] + self.hd_saline_bolus_res_timestamp = timestamp + + @publish([ + "hd_treatment_time_change_res_timestamp", + "duration_change_succeeded", + "duration_change_reject_reason", + "duration_change_time_min", + "duration_change_uf_vol_ml" + ]) + def _handler_treatment_duration_change_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding treatment duration change request. + + @param message: response message from HD regarding treatment duration change.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 treatment duration (min) \n + F32 UF volue (mL) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + self.duration_change_succeeded = rsp[0] + if RequestRejectReasons.has_value(rea[0]): + self.duration_change_reject_reason = RequestRejectReasons(rea[0]) + self.duration_change_time_min = tim[0] + self.duration_change_uf_vol_ml = vol[0] + self.hd_treatment_time_change_res_timestamp = timestamp + + @publish([ + "hd_blood_dial_rate_change_res_timestamp", + "blood_and_dialysate_flow_rate_change_succeeded", + "blood_and_dialysate_flow_rate_change_reject_reason", + "target_blood_flow_rate", + "target_dialysate_flow_rate" + ]) + def _handler_blood_and_dialysate_change_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding blood & dialysate flow rate change request. + + @param message: response message from HD regarding requested blood & dialysate flow rate settings change.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 Blood flow rate (mL/min) \n + U32 Dialysate flow rate (mL/min) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dil = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.blood_and_dialysate_flow_rate_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.blood_and_dialysate_flow_rate_change_reject_reason = RequestRejectReasons(rea[0]) + self.target_blood_flow_rate = bld[0] + self.target_dialysate_flow_rate = dil[0] + self.hd_blood_dial_rate_change_res_timestamp = timestamp + + @publish([ + "hd_uf_settings_change_res_timestamp", + "uf_change_succeeded", + "uf_change_reject_reason", + "uf_change_volume_ml", + "uf_change_time_min", + "uf_change_time_diff", + "uf_change_rate_ml_min", + "uf_change_rate_diff" + ]) + def _handler_uf_change_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding UF change request. + + @param message: response message from HD regarding requested ultrafiltration settings change.\n + BOOL Accepted \n + U32 RejectReason (if not accepted) + F32 UF Volume (mL) - converted to Liters \n + U32 treatment Time (min) \n + S32 treatment Time Change (min) \n + F32 UF Rate (mL/min) \n + F32 UF Rate Change (mL/min) + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + tmd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + rat = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + rtd = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + ort = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.uf_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.uf_change_reject_reason = RequestRejectReasons(rea[0]) + + self.uf_change_volume_ml = vol[0] # / self.LITER_TO_ML_CONVERSION_FACTOR + + self.uf_change_time_min = tim[0] + self.uf_change_time_diff = tmd[0] + self.uf_change_rate_ml_min = rat[0] + self.uf_change_rate_diff = rtd[0] + + self.uf_old_rate_ml_min = ort[0] + self.hd_uf_settings_change_res_timestamp = timestamp + + @publish(["hd_uf_settings_change_confirm_res_timestamp", + "uf_change_succeeded", + "uf_change_reject_reason", + "uf_change_volume_ml", + "uf_change_time_min", + "uf_change_rate_ml_min"]) + def _handler_uf_change_confirm_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding UF change confirmation. + + @param message: response message from HD regarding confirmed ultrafiltration settings change.\n + BOOL Accepted \n + U32 RejectReason (if not accepted) + F32 UF Volume (mL) - converted to Liters \n + U32 treatment Time (min) \n + F32 UF Rate (mL/min) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + rat = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.uf_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.uf_change_reject_reason = RequestRejectReasons(rea[0]) + self.uf_change_volume_ml = vol[0] / self.LITER_TO_ML_CONVERSION_FACTOR + self.uf_change_time_min = tim[0] + self.uf_change_rate_ml_min = rat[0] + self.hd_uf_settings_change_confirm_res_timestamp = timestamp + + @publish([ + "hd_rinseback_cmd_res_timestamp", + "rinseback_cmd_succeeded", + "rinseback_cmd_reject_reason", + ]) + def _handler_rinseback_cmd_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding rinseback user command. + + @param message: response message from HD regarding rinseback user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.rinseback_cmd_succeeded = True + else: + self.rinseback_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.rinseback_cmd_reject_reason = RequestRejectReasons(rea[0]) + self.hd_rinseback_cmd_res_timestamp = timestamp + + @publish([ + "hd_recirc_res_timestamp", + "recirc_cmd_succeeded", + "recirc_cmd_reject_reason", + ]) + def _handler_recirc_cmd_response(self, message, timestamp=0.0): + """ + Handler for response from HD regarding recirculate user command. + + @param message: response message from HD regarding recirculate user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.recirc_cmd_succeeded = True + else: + self.recirc_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.recirc_cmd_reject_reason = RequestRejectReasons(rea[0]) + self.hd_recirc_res_timestamp = timestamp + + @publish([ + "hd_tx_end_cmd_res_timestamp", + "treatment_end_cmd_succeeded", + "treatment_end_cmd_reject_reason", + ]) + def _handler_treatment_end_cmd_response(self, message: dict, timestamp=0.0) -> None: + """ + Handler for response from HD regarding treatment end user command. + + @param message: response message from HD regarding treatment end user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.treatment_end_cmd_succeeded = True + else: + self.treatment_end_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.treatment_end_cmd_reject_reason = RequestRejectReasons(rea[0]) + self.hd_tx_end_cmd_res_timestamp = timestamp + + @publish([ + "hd_active_alarm_list_timestamp", + "active_alarm_list_succeeded", + "active_alarm_list_reject_reason", + "alarm_list_response", + ]) + def _handler_active_alarm_list_response(self, message: dict, timestamp=0.0) -> None: + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + al1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + al2 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + al3 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + al4 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + al5 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + al6 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + al7 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + al8 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + al9 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + al10 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + + self.active_alarm_list_succeeded = rsp + self.active_alarm_list_reject_reason = rea + self.alarm_list_response = (al1, al2, al3, al4, al5, al6, al7, al8, al9, al10) + self.hd_active_alarm_list_timestamp = timestamp + + @publish(["ro_mode_timestamp", "ro_mode_status", "ro_mode_rejection_status"]) + def _handler_ro_mode_status(self, message: dict, timestamp=0.0) -> None: + """ + Handles published ro only message response from the UI + + @param message: published RO only mode response + + @return: None + """ + romode = struct.unpack(' None: + """ + Constructs and sends the ui check-in message + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_CHECK_IN.value) + + self.logger.debug("Sending ui checkin w/ HD") + + self.can_interface.send(message, 0) + + def cmd_ui_request_hd_version(self) -> None: + """ + Constructs and sends the ui request for version message + + """ + + major = unsigned_byte_to_bytearray(0) + minor = unsigned_byte_to_bytearray(0) + micro = unsigned_byte_to_bytearray(0) + build = short_to_bytearray(0) + compatibility = integer_to_bytearray(self.SW_COMPATIBILITY_REV) + + payload = major + minor + micro + build + compatibility + + message = DenaliMessage.build_message(channel_id=DenaliChannels.ui_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_FW_VERSIONS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending ui request for version to HD") + + self.can_interface.send(message, 0) + + def cmd_ui_set_alarm_audio_volume_level(self, volume: int = MAX_ALARM_VOLUME_LEVEL) -> None: + """ + Constructs and sends the ui set alarm volume level message + + @param volume: alarm volume level (1..5) + + """ + + payload = integer_to_bytearray(volume) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending ui request to set alarm audio volume to level " + str(volume) + " to HD") + self.can_interface.send(message, 0) + + def cmd_ui_uf_volume_set(self, uf_volume: float) -> None: + """ + Constructs and sends the ui set ultrafiltration volume parameter message + + @param uf_volume: ultrafiltration volume (in mL) + + """ + + payload = float_to_bytearray(uf_volume) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER_REQUEST.value, + payload=payload) + + self.logger.debug("Sending ui request to set ultrafiltration volume parameter of " + str(uf_volume) + " to HD") + self.can_interface.send(message, 0) + + def cmd_ui_uf_pause_resume(self, cmd: int = UF_CMD_PAUSE) -> None: + """ + Constructs and sends a ui UF command message + + @param cmd: 0 for pause, 1 for resume + + @return: None + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_PAUSE_RESUME_REQUEST.value, + payload=payload) + + if cmd == self.UF_CMD_PAUSE: + str_cmd = "pause" + else: + str_cmd = "resume" + self.logger.debug("Sending UF " + str_cmd + " command.") + + self.can_interface.send(message, 0) + + def cmd_ui_uf_settings_change_request(self, vol: float = 0.0) -> None: + """ + Constructs and sends a ui UF change settings command message + + @param vol: (float) new ultrafiltration volume setting (in L) + + @return: None + """ + + # reset response to this command so we can tell when response is received + # self.UFChangeResponse = None + # build command message + volume = float_to_bytearray(vol * self.LITER_TO_ML_CONVERSION_FACTOR) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_REQUEST.value, + payload=volume) + + self.logger.debug("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_uf_settings_change_confirm(self, vol: float = 0.0, adj: int = UF_CMD_CHANGE_TIME_TO_ADJUST) -> None: + """ + Constructs and sends a ui UF change settings command message + + @param vol: (float) new ultrafiltration volume setting (in L) + @param adj: (int) 0 for adjust time, 1 for adjust rate + + @return: None + """ + + # reset response to this command so we can tell when response is received + # self.UFChangeResponse = None + + # build command message + volume = float_to_bytearray(vol * self.LITER_TO_ML_CONVERSION_FACTOR) + adjust = integer_to_bytearray(adj) + payload = volume + adjust + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_CONFIRM_UF_SETTINGS_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_treatment_duration_setting_change_request(self, time_min: int = 0) -> None: + """ + Constructs and sends a ui UF change settings confirmed by user message + + @param time_min: (int) treatment time (in min). + @return: None + """ + + payload = integer_to_bytearray(time_min) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending treatment duration setting change request.") + + # Send message + self.can_interface.send(message, 0) + + def cmd_ui_blood_and_dialysate_flow_settings_change_request(self, blood_flow: int, dial_flow: int) -> None: + + """ + Constructs and sends a ui blood & dialysate flow settings change request by user message + + @param blood_flow: (int) blood flow rate set point (in mL/min). + @param dial_flow: (int) dialysate flow rate set point (in mL/min). + + @return: None + """ + + bld = integer_to_bytearray(blood_flow) + dial = integer_to_bytearray(dial_flow) + payload = bld + dial + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending blood & dialysate flow rate settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_initiate_treatment_request(self, cmnd: int = 1) -> None: + """ + Constructs and sends a ui initiate treatment command + + @param cmnd: (int) start treatment command code (1 = initiate, 0 = cancel) + @return: None + """ + + cmd = integer_to_bytearray(cmnd) + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_INITIATE_TREATMENT_REQUEST.value, + payload=payload) + + self.logger.debug("Sending start treatment command request.") + + self.can_interface.send(message, 0) + + def set_a_treatment_parameter(self, param_idx: int, value: int) -> None: + """ + Constructs and sends a set treatment parameter message + Constraints: + Must be logged into HD. + + @param param_idx: (int) index/enum of parameter to set (see TreatmentParameters enum) + @param value: (int or float - depends on param_idx) value to set for given treatment parameter + + @return: None + """ + if TreatmentParameters.has_value(param_idx): + self.treatment_parameters[param_idx] = value + idx = integer_to_bytearray(param_idx) + if param_idx == TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value: + val = float_to_bytearray(value) + elif param_idx == TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value: + val = float_to_bytearray(value) + elif param_idx == TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value: + val = float_to_bytearray(value) + elif param_idx == TreatmentParameters.TREATMENT_PARAM_UF_VOLUME_L.value: + val = float_to_bytearray(value) + else: + val = integer_to_bytearray(value) + payload = idx + val + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + self.logger.debug("Setting treatment parameter " + str(param_idx) + " to " + str(value) + ".") + self.can_interface.send(message, 0) + else: + self.logger.debug("Invalid param_idx given.") + + def cmd_set_treatment_parameters(self, bld_flow: int = 350, + dia_flow: int = 300, + duration: int = 60, + hep_rate: float = 0.0, + hep_bol: float = 0.0, + hep_stop: int = 0, + sal_bol: int = 100, + acid: int = 0, + bicarb: int = 0, + dialyzer: int = 0, + hep_type: int = 0, + dia_temp: float = 37.0, + art_win: int = 120, + ven_win: int = 100, + ven_asy: int = 20, + bp_intvl: int = 30, + rb_flow: int = 100) -> None: + """ + Constructs and sends a ui set treatment parameters message + Constraints: + HD must be in treatment parameters mode + + @param bld_flow: (int) blood flow rate (in mL/min) + @param dia_flow: (int) dialysate flow rate (in mL/min) + @param duration: (int) treatment duration (in min) + @param hep_rate: (float) Heparin dispense rate (in mL/hr) + @param hep_bol: (float) Heparin bolus volume (in mL) + @param hep_stop: (int) Heparin pre-stop time (in min) + @param sal_bol: (int) Saline bolus volume (in mL) + @param acid: (int) acid concentrate type + @param bicarb: (int) bicarbonate concentrate type + @param dialyzer: (int) dialyzer type + @param hep_type: (int) heparin type + @param dia_temp: (float) dialysate temperature (in deg C) + @param art_win: (int) arterial pressure alarm limit window (in mmHg) + @param ven_win: (int) venous pressure alarm limit window (in mmHg) + @param ven_asy: (int) venous pressure alarm limit asymmetric (in mmHg) + @param bp_intvl: (int) blood pressure measurement interval (in min) + @param rb_flow: (int) rinseback flow rate (in mL/min) + + @return: None + """ + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld_flow + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia_flow + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = duration + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = hep_stop + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal_bol + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acid + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bicarb + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dialyzer + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = hep_type + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bp_intvl + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rb_flow + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_ART_PRES_LIMIT_WINDOW.value] = art_win + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_WINDOW.value] = ven_win + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_VEN_PRES_LIMIT_ASYMMETRIC.value] = ven_asy + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hep_rate + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hep_bol + self.treatment_parameters[TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = dia_temp + + bld = integer_to_bytearray(bld_flow) + dia = integer_to_bytearray(dia_flow) + dur = integer_to_bytearray(duration) + hps = integer_to_bytearray(hep_stop) + sal = integer_to_bytearray(sal_bol) + acc = integer_to_bytearray(acid) + bic = integer_to_bytearray(bicarb) + dzr = integer_to_bytearray(dialyzer) + hpr = integer_to_bytearray(hep_type) + bpi = integer_to_bytearray(bp_intvl) + rbf = integer_to_bytearray(rb_flow) + apw = integer_to_bytearray(art_win) + vpw = integer_to_bytearray(ven_win) + vpa = integer_to_bytearray(ven_asy) + hdr = float_to_bytearray(hep_rate) + hbv = float_to_bytearray(hep_bol) + tmp = float_to_bytearray(dia_temp) + + payload = bld + dia + dur + hps + sal + acc + bic + dzr + hpr + bpi + rbf + apw + vpw + vpa + hdr + hbv + tmp + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending treatment parameters to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_confirm_treatment_parameters(self, cmd: int = 0) -> None: + """ + Constructs and sends a ui confirm treatment parameters message + Constraints: + Command must be one of the following: + REJECT = 0 (user rejects treatment parameters) + CONFIRM = 1 (uesr confirms treatment parameters) + + @param cmd: (int) confirm treatment parameters command code + + @return: None + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending confirm treatment parameters message.") + + self.can_interface.send(message, 0) + + def cmd_ui_request_saline_bolus(self, start: bool = False) -> None: + """ + Constructs and sends a ui request for a saline bolus message + Constraints: + HD must be in treatment mode, dialysis sub-mode. + Will not succeed if saline bolus already in progress. + Will not succeed if max. saline volume has already been reached. + + @param start: (bool) True if we're requesting bolus start, False if bolus abort + + @return: none + """ + + if start: + sta = integer_to_bytearray(1) + request = "start" + else: + sta = integer_to_bytearray(0) + request = "abort" + + payload = sta + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_SALINE_BOLUS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending request to " + request + " a saline bolus.") + + self.can_interface.send(message, 0) + + def cmd_ui_rinseback_user_action( + self, action: int = RinsebackUserActions.REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_START.value) -> None: + """ + Constructs and sends a UI rinseback user action message + Constraints: + HD must be in treatment mode, rinseback sub-mode. + + @param action: (enumerated int) User action + CONFIRM_START = 0 (start rinseback) + INCREASE_RATE = 1 (increase rinseback flow rate by 25 mL/min) + DECREASE_RATE = 2 (decrease rinseback flow rate by 25 mL/min) + PAUSE = 3 (pause rinseback) + RESUME = 4 (resume paused rinseback) + RINSEBACK_END = 5 (end rinseback now) + ADDITIONAL = 6 (10 mL more) + CONFIRM_DISCONNECT = 7 (go to re-circulate sub-mode) + END_TREATMENT = 8 (go to post-treatment mode) + BACK_TO_TREATMENT = 9 (ready to resume treatment) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_RINSEBACK_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending rinseback command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_recirculate_user_action( + self, action: int = RecircUserActions.REQUESTED_USER_ACTION_TX_RECIRC_RECONNECT.value) -> None: + """ + Constructs and sends a UI recirculate user action message + Constraints: + HD must be in treatment mode, recirculate sub-mode. + + @param action: (enumerated int) User action + RECONNECT = 0 (ready to reconnect patient) + CONFIRM_RECONNECT = 1 (confirm patient is reconnected - ready to resume treatment) + RESUME_RC = 2 (resume recirculation - not ready to reconnect patient) + END_TREATMENT = 3 (go to post-treatment mode) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_RECIRC_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending recirculate command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_treatment_end_user_action( + self, action: int = TreatmentEndUserActions.REQUESTED_USER_ACTION_TX_END_RINSEBACK_START.value) -> None: + """ + Constructs and sends a UI treatment end user action message + Constraints: + HD must be in treatment mode, treatment end sub-mode. + + @param action: (enumerated int) User action + RINSEBACK_START = 0 (start final rinseback) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_TX_END_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending treatment end command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_user_alarm_response(self, option: int = AlarmUserOptions.ALARM_USER_ACTION_ACK.value) -> None: + """ + Constructs and sends a ui alarm response message. + Constraints: + An alarm must be active. + The selected user action must be enabled for the "top" active alarm and current op. mode. + + @param option: (enum - AlarmUserOptions) ID of user alarm response option \n + 0-RESUME \n + 1-RINSEBACK \n + 2-END TREATMENT \n + 3-ACK + + @return: none + """ + + opt = integer_to_bytearray(option) + payload = opt + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_ALARM_USER_ACTION_REQUEST.value, + payload=payload) + + self.logger.debug("Sending user alarm response option " + str(option) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_silence_alarm(self, toggle: int = 1) -> None: + """ + Constructs and sends a ui alarm response message + + @param toggle: (U32) alarm silence cmd \n + 0-Cancel alarm silence \n + 1-Silence alarms + + @return: none + """ + + cmd = integer_to_bytearray(toggle) + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_ALARM_SILENCE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending user alarm response option " + str(toggle) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_sample_water(self, cmd: int = 0) -> None: + """ + Constructs and sends a ui water sample request message + + @param cmd: (U32) sample water cmd \n + 0-Stop sample water \n + 1-Start sample water + + @return: none + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SAMPLE_WATER_CMD_REQUEST.value, + payload=payload) + + self.logger.debug("Sending user sample water command " + str(cmd) + " to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_send_sample_water_result(self, result: int = 1) -> None: + """ + Constructs and sends a ui water sample result message + + @param result: (U32) sample water result \n + 0-Fail \n + 1-Pass + + @return: none + """ + + payload = integer_to_bytearray(result) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SAMPLE_WATER_RESULT.value, + payload=payload) + + self.logger.debug("Sending user sample water result " + str(result) + " to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_consumable_installation_confirm(self) -> None: + """ + Constructs and sends a ui consumable installation confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_CONSUMABLE_INSTALL_CONFIRM_REQUEST.value) + + self.logger.debug("Sending user consumable installation confirm to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_disposable_installation_confirm(self) -> None: + """ + Constructs and sends a ui disposable installation confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_INSTALLATION_CONFIRM_REQUEST.value) + + self.logger.debug("Sending user disposable installation confirm to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_start_prime_request(self) -> None: + """ + Constructs and sends a ui start prime request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_START_PRIME_REQUEST.value) + + self.logger.debug("Sending user start prime request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_continue_to_treatment_request(self) -> None: + """ + Constructs and sends a ui continue to treatment request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_CONNECTION_BEGIN_REQUEST.value) + + self.logger.debug("Sending user continue to treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_patient_connection_confirm(self) -> None: + """ + Constructs and sends a ui patient connection confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_CONNECTION_CONFIRM_REQUEST.value) + + self.logger.debug("Sending user continue to treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_start_treatment_request(self) -> None: + """ + Constructs and sends a ui start treatment request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_START_TREATMENT_REQUEST.value) + + self.logger.debug("Sending user start treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_patient_disconnection_confirm(self) -> None: + """ + Constructs and sends a ui patient disconnection confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_DISCONNECTION_CONFIRM_REQUEST.value) + + self.logger.debug("Sending user patient disconnection confirm msg to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_disposable_removal_confirm(self) -> None: + """ + Constructs and sends a ui disposable removal confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_DISPOSABLE_REMOVAL_CONFIRM_REQUEST.value) + + self.logger.debug("Sending user disposable removal confirm msg to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_set_standby_submode_to_disinfect(self) -> None: + """ + Constructs and sends a ui set standby submode to wait for disinfect + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_REQUEST.value) + + self.logger.debug("Sending setting standby submode to wait for disinfect to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_set_hd_service_time(self) -> None: + """ + Constructs and sends a ui message to set the HD service time + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_SERVICE_TIME_REQUEST.value) + + self.logger.debug("Setting HD service time.") + self.can_interface.send(message, 0) + + def cmd_ui_set_dg_service_time(self) -> None: + """ + Constructs and sends a ui message to set the DG service time + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_SERVICE_TIME_REQUEST.value) + + self.logger.debug("Setting DG service time.") + self.can_interface.send(message, 0) + + def cmd_set_ro_only_mode_status(self, status: int) -> int: + """ + Constructs and sends a message to DG to set the status of the RO only mode: + 1 = Enable RO only mode + 0 = Disable RO only mode and be normal mode + + @param status: (int) RO only mode status (1=enable, 0=disable) + @return: 1 if successful, zero otherwise + """ + st = integer_to_bytearray(status) + payload = st + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_UI_SET_DG_RO_MODE.value, + payload=payload) + + self.logger.debug("Setting RO only mode to: {}".format(status)) + + self.can_interface.send(message, 0) + return True + + def cmd_ui_send_alarm_list_request(self) -> None: + """ + Constructs and sends a ui message to request the HD active alarm list. + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.ui_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_ACTIVE_ALARMS_LIST_REQUEST.value) + + self.logger.debug("Sending active alarm list request msg to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_off_button_response(self, response: int) -> int: + """ + Constructs and sends a message to HD to proxy the response from the off button message request + 0 = OFF_BUTTON_RSP_USER_REQUESTS_POWER_OFF # User requests power off response + 1 = OFF_BUTTON_RSP_USER_CONFIRMS_POWER_OFF # User confirms power off response + 2 = OFF_BUTTON_RSP_USER_REJECTS_POWER_OFF # User rejects power off response + + @param response: (int) off button response to HD + @return: 1 if successful, zero otherwise + """ + res = byte_to_bytearray(response) + payload = res + + message = DenaliMessage.build_message(channel_id=DenaliChannels.ui_to_hd_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending Power off message request with value {}".format(res)) + # No ACK required + self.can_interface.send(message, 0) + return True + + def cmd_get_ro_only_mode_status(self) -> int: + """ + Constructs and sends a message to DG to set the status of the RO only mode: + 1 = Enable RO only mode + 0 = Disable RO only mode and be normal mode + + @return: 1 if RO only mode is enabled, zero if not + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DIALIN_RO_ONLY_MODE_STATUS_REQUEST.value) + + self.logger.debug("Requesting RO only mode") + + self.can_interface.send(message) + return True + + def cmd_ui_send_alarm_triggered_message(self, alarm_id: int, data_field1: int = 0, data_field2: int = 0, + priority: int = 0, rank: int = 0, top: int = 0) -> int: + """ + Constructs and sends an alarm triggered message to the HD. Used for UI POST failures. + Data fields are restricted to unsigned integer type. Defaulted to 0. + + @param alarm_id: (int) the alarm id from the ui to trigger + @param data_field1: (int) data value for data field 1 + @param data_field2: (int) data value for data field 2 + @param priority: (int) Alarm priority + @param rank: (int) Alarm rank + @param top: (int) Alarm Clear Top + @return: 1 if successful, zero otherwise + """ + + field_type_uint = 1 + + alarm = integer_to_bytearray(alarm_id) + data_type1 = integer_to_bytearray(field_type_uint) + dat1 = integer_to_bytearray(data_field1) + data_type2 = integer_to_bytearray(field_type_uint) + dat2 = integer_to_bytearray(data_field2) + priority = integer_to_bytearray(priority) + rank = integer_to_bytearray(rank) + top = integer_to_bytearray(top) + + payload = alarm + data_type1 + dat1 + data_type2 + dat2 + priority + rank + top + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_TRIGGERED.value, + payload=payload) + + self.logger.debug("Sending UI alarm triggered message") + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_ui_send_pressure_limits_change_request(self, arterial: int, venous: int, asymmetric_venous: int) -> None: + """ + Constructs and sends a pressure limits request message to the HD. + + @param arterial: (int) date value for arterial pressure limit. + @param venous: (int) data value for venous pressure limit. + @param asymmetric_venous: (int) data value for venous asymmetric limit. + @return: 1 if successful, zero otherwise + """ + art = integer_to_bytearray(arterial) + ven = integer_to_bytearray(venous) + vena = integer_to_bytearray(asymmetric_venous) + + payload = art + ven + vena + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PRESSURE_LIMITS_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending UI pressure limits message") + + received_message = self.can_interface.send(message) \ No newline at end of file Index: leahi-dialin/hd/usage_info_record.py =================================================================== diff -u --- leahi-dialin/hd/usage_info_record.py (revision 0) +++ leahi-dialin/hd/usage_info_record.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,345 @@ +########################################################################### +# +# Copyright (c) 2022-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file usage_info_record.py +# +# @author (last) Michael Garthwaite +# @date (last) 17-Oct-2023 +# @author (original) Dara Navaei +# @date (original) 28-Apr-2022 +# +############################################################################ + +import struct +import time +from collections import OrderedDict +from logging import Logger +from time import sleep + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver, NVRecordsHD +from ..utils.conversions import integer_to_bytearray + + +class HDUsageNVRecord(AbstractSubSystem): + """ + @brief Hemodialysis Device (HD) Dialin API sub-class for setting and getting the usage information record. + """ + + _DEFAULT_USAGE_INFO_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _RECORD_SPECS_BYTES = 12 + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'HD' + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + self._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._usage_info_data = 0 + self._is_getting_usage_info_in_progress = False + self._raw_usage_info_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.hd_usage_info_record = self._prepare_hd_usage_info_record() + self.hd_usage_info_record_timestamp = 0.0 # For timestamp + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_USAGE_INFO_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_usage_info_sync) + + def _cmd_request_hd_usage_info_record(self) -> int: + """ + Handles getting HD usage information record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_usage_info_in_progress is not True: + self._is_getting_usage_info_in_progress = True + # Clear the list for the next call + self._raw_usage_info_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_USAGE_INFO_RECORD.value) + + self.logger.debug('Getting HD usage information record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting HD usage information record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def cmd_hd_usage_info_crc_override(self, crc: int) -> bool: + """ + Handles setting HD usage info record CRC override. + + @param crc: (int) the CRC override value + + @return: True if successful, False otherwise + """ + # This command does not have a reset but since the corresponding payload structure in firmware requires a reset + # so the payload length is the same when it is received in the firmware. + reset_byte_array = integer_to_bytearray(0) + crc_value = integer_to_bytearray(crc) + hd_record = integer_to_bytearray(NVRecordsHD.NVDATAMGMT_USAGE_INFO_RECORD.value) + payload = reset_byte_array + crc_value + hd_record + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_NV_RECORD_CRC_OVERRIDE.value, + payload=payload) + + self.logger.debug("Overriding HD usage info CRC to: " + str(crc)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def _handler_hd_usage_info_sync(self, message, timestamp=0.0): + """ + Handles published HD usage information record messages. + + @param message: published HD usage information record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._usage_info_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_usage_info_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_usage_info_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_usage_info_record, self._raw_usage_info_record) + self.hd_usage_info_record_timestamp = timestamp + self._handler_received_complete_hd_usage_info_record() + + @publish(["hd_usage_info_record_timestamp", "hd_usage_info_record"]) + def _handler_received_complete_hd_usage_info_record(self): + """ + Publishes the received usage information record + + @return: None + """ + self.logger.debug("Received a complete HD usage information record.") + + def cmd_update_hd_usage_info_record(self, excel_report_path: str): + """ + Handles preparing the HD usage information from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the information is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + self._utilities.write_excel_record_to_fw_record(self.hd_usage_info_record, excel_report_path, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + self._cmd_set_hd_usage_info_record(self.hd_usage_info_record) + + def _cmd_set_hd_usage_info_record(self, previous_record: OrderedDict) -> bool: + """ + Handles updating the HD usage information record and sends it to FW. + + @return: True upon success, False otherwise + """ + transfer_status = 1 + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + + self.logger.debug('Setting HD usage information record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_USAGE_INFO_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.warning("HD ACK not received!") + continue + elif transfer_status == 0: + self.logger.debug("Sending HD usage info record failed") + return False + + transfer_status = received_message['message'][6] + + if transfer_status == 1: + self.logger.debug("Finished sending HD usage info record.") + return True + + def _prepare_hd_usage_info_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of HD usage information. + + @return: (OrderedDict) the assembled HD usage information + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_usage_info_record()] + + for function in functions: + # Update the groups bytes size so far to be used to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the usage information record dictionary. + + @return: usage information record dictionary and the byte size of this group + """ + groups_byte_size = 0 + usage_info_records = OrderedDict( + {'usage_info_record': + {'tx_total_time_hours': [' bool: + """ + Handles resetting HD usage info record. + + @return: True if successful, False otherwise + """ + self.hd_usage_info_record = self._prepare_hd_usage_info_record() + self.hd_usage_info_record = self._utilities.reset_fw_record(self.hd_usage_info_record) + status = self._cmd_set_hd_usage_info_record(self.hd_usage_info_record) + + return status + + def cmd_set_dg_usage_info_excel_to_fw(self, report_address: str) -> bool: + """ + Handles setting the usage info data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written from excel + + @return: none + """ + + # Request the HD usage record and set and observer class to callback when the record is read back + self._cmd_request_hd_usage_info_record() + observer = NVUtilsObserver("hd_usage_info_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_usage_info_record, report_address, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + + ret = self._cmd_set_hd_usage_info_record(self.hd_usage_info_record) + return ret + Index: leahi-dialin/hd/valves.py =================================================================== diff -u --- leahi-dialin/hd/valves.py (revision 0) +++ leahi-dialin/hd/valves.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,412 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file valves.py +# +# @author (last) James Walter Taylor +# @date (last) 03-Aug-2023 +# @author (original) Dara Navaei +# @date (original) 19-Aug-2020 +# +############################################################################ + + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from ..common import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ValvesEnum(DialinEnum): + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + + +@unique +class ValvesPositions(DialinEnum): + VALVE_POSITION_NOT_IN_POSITION = 0 + VALVE_POSITION_A_INSERT_EJECT = 1 + VALVE_POSITION_B_OPEN = 2 + VALVE_POSITION_C_CLOSE = 3 + + +@unique +class ValvesStates(DialinEnum): + VALVE_STATE_WAIT_FOR_POST = 0 + VALVE_STATE_HOMING_NOT_STARTED = 1 + VALVE_STATE_HOMING_FIND_ENERGIZED_EDGE = 2 + VALVE_STATE_HOMING_FIND_DEENERGIZED_EDGE = 3 + VALVE_STATE_IDLE = 4 + VALVE_STATE_IN_TRANSITION = 5 + VALVE_STATE_IN_BYPASS_MODE = 6 + + +@unique +class AirTrapState(DialinEnum): + STATE_CLOSED = 0 + STATE_OPEN = 1 + + +class HDValves(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for valves related commands. + """ + # Valves states publish message field positions + # Note the MsgFieldPosition was not used since some of the published data are S16 + START_POS_VALVES_ID = DenaliMessage.PAYLOAD_START_INDEX + END_POS_VALVES_ID = START_POS_VALVES_ID + 4 + + START_VALVES_STATE = END_POS_VALVES_ID + END_VALVES_STATE = START_VALVES_STATE + 4 + + START_POS_VALVES_CURR_POS = END_VALVES_STATE + END_POS_VALVES_CURR_POS = START_POS_VALVES_CURR_POS + 4 + + START_POS_VALVES_CURR_POS_CNT = END_POS_VALVES_CURR_POS + END_POS_VALVES_CURR_POS_CNT = START_POS_VALVES_CURR_POS_CNT + 2 + + START_POS_VALVES_NEXT_POS_CNT = END_POS_VALVES_CURR_POS_CNT + END_POS_VALVES_NEXT_POS_CNT = START_POS_VALVES_NEXT_POS_CNT + 2 + + START_POS_VALVES_CURRENT = END_POS_VALVES_NEXT_POS_CNT + END_POS_VALVES_CURRENT = START_POS_VALVES_CURRENT + 4 + + START_VALVES_POS_C = END_POS_VALVES_CURRENT + END_VALVES_POS_C = START_VALVES_POS_C + 2 + + START_VALVES_POS_A = END_VALVES_POS_C + END_VALVES_POS_A = START_VALVES_POS_A + 2 + + START_VALVES_POS_B = END_VALVES_POS_A + END_VALVES_POS_B = START_VALVES_POS_B + 2 + + START_VALVES_PWM = END_VALVES_POS_B + END_VALVES_PWM = START_VALVES_PWM + 4 + + START_AIR_TRAP_VALVE_STATUS = END_VALVES_PWM + END_AIR_TRAP_VALVE_STATUS = START_AIR_TRAP_VALVE_STATUS + 4 + + def __init__(self, can_interface, logger: Logger): + """ + DGDrainPump constructor + + @param can_interface: (DenaliCanMessenger) - Denali CAN messenger object. + @param logger: (Logger) - Dialin logger + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_VALVES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_valves_sync) + + self.hd_valves_timestamp = 0.0 + # A dictionary of the valves with the status + self.valves_status = {ValvesEnum.VDI.name: {}, ValvesEnum.VDO.name: {}, ValvesEnum.VBA.name: {}, + ValvesEnum.VBV.name: {}} + + self.hd_air_trap_status = 0 + + def get_hd_air_trap_status(self): + """ + Returns the hd air trap status + @return: (str) the HD air trap status + """ + return self.hd_air_trap_status + + def get_hd_valves_status(self): + """ + Gets the hd valves status + + @return: (dict) the hd valves status + """ + return self.valves_status + + def cmd_hd_valves_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: Publish time interval in ms + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the HD valves module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_set_hd_valve_position(self, valve: int, position: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param position: integer - Position number: + VALVE_POSITION_A_INSERT_EJECT = 1 + VALVE_POSITION_B_OPEN = 2 + VALVE_POSITION_C_CLOSE = 3 + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pos = integer_to_bytearray(position) + payload = reset_value + pos + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_POSITION_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD cmd_valve_override Timeout!!!") + return False + + def cmd_set_hd_valve_current_override(self, valve: int, current: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param current: float value to override current + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + cur = float_to_bytearray(current) + payload = reset_value + cur + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_CURRENT_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug("Setting {} current to {:5.3f} A".format(str(ValvesEnum(valve).name), current)) + + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD current override Timeout!!!") + return False + + def cmd_set_hd_valve_position_count_override(self, valve: int, position_count: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param position_count: integer value + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pos = integer_to_bytearray(position_count) + payload = reset_value + pos + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_POSITION_COUNT_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug("Setting {} position to {} ".format(str(ValvesEnum(valve).name), position_count)) + + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD current override Timeout!!!") + return False + + def cmd_set_hd_valve_pwm(self, valve: int, pwm: int, direction: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves PWM command + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param pwm: integer - sets the pwm value + @param direction: integer - Direction number: + 0 = Clockwise + 1 = Counter clockwise + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pwm = integer_to_bytearray(pwm) + dir_value = integer_to_bytearray(direction) + payload = reset_value + vlv + pwm + dir_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_SET_PWM_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD cmd_valve_override Timeout!!!") + return False + + def cmd_home_hd_valve(self, valve: int) -> int: + """ + Constructs and sends the HD valves home command + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @returns 1 if successful, zero otherwise + """ + payload = integer_to_bytearray(valve) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_HOME.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD Homing Valve Timeout!!!") + return False + + def cmd_set_hd_air_trap_valve(self, valve_state: int = AirTrapState.STATE_CLOSED.name) -> int: + """ + Constructs and sends an open/close command to the HD air trap valve + + @param valve_state: air trap valve state (open or close) + @returns 1 if successful, zero otherwise + """ + + if valve_state == AirTrapState.STATE_OPEN.value: + payload = integer_to_bytearray(1) + else: + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_SET_AIR_TRAP_VALVE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Opening air trap valve") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Opening air trap valve timeout!!") + return False + + @publish(["hd_valves_timestamp","valves_status", "hd_air_trap_status"]) + def _handler_hd_valves_sync(self, message: dict, timestamp=0.0) -> None: + """ + Handles published HD valves data messages. HD valves data are captured + for reference. + + @param message: published HD valves data message + @returns none + """ + vlv_id = struct.unpack('i', bytearray( + message['message'][self.START_POS_VALVES_ID:self.END_POS_VALVES_ID]))[0] + state_id = struct.unpack('i', bytearray( + message['message'][self.START_VALVES_STATE:self.END_VALVES_STATE]))[0] + pos_id = struct.unpack('i', bytearray( + message['message'][self.START_POS_VALVES_CURR_POS:self.END_POS_VALVES_CURR_POS]))[0] + pos_cnt = struct.unpack('h', bytearray( + message['message'][self.START_POS_VALVES_CURR_POS_CNT:self.END_POS_VALVES_CURR_POS_CNT]))[0] + next_pos = struct.unpack('h', bytearray( + message['message'][self.START_POS_VALVES_NEXT_POS_CNT:self.END_POS_VALVES_NEXT_POS_CNT]))[0] + current = struct.unpack('f', bytearray( + message['message'][self.START_POS_VALVES_CURRENT:self.END_POS_VALVES_CURRENT]))[0] + pos_c = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_C:self.END_VALVES_POS_C]))[0] + pos_a = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_A:self.END_VALVES_POS_A]))[0] + pos_b = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_B:self.END_VALVES_POS_B]))[0] + pwm = struct.unpack('i', bytearray( + message['message'][self.START_VALVES_PWM:self.END_VALVES_PWM]))[0] + air_trap = struct.unpack('i', bytearray( + message['message'][self.START_AIR_TRAP_VALVE_STATUS:self.END_AIR_TRAP_VALVE_STATUS]))[0] + + # To make sure values of the enums are not out of range + if ValvesEnum.has_value(vlv_id) and ValvesPositions.has_value(pos_id) and ValvesStates.has_value(pos_id): + vlv_name = ValvesEnum(vlv_id).name + # Update the valves dictionary + self.valves_status[vlv_name] = {'Valve': vlv_name, 'PosID': ValvesPositions(pos_id).name, 'PosCnt': pos_cnt, + 'Cmd': next_pos, 'State': ValvesStates(state_id).name, 'Current': current, + 'PosA': pos_a, 'PosB': pos_b, 'PosC': pos_c, 'PWM': pwm} + # Update the air trap valve's status + self.hd_air_trap_status = air_trap + self.hd_valves_timestamp = timestamp \ No newline at end of file Index: leahi-dialin/hd/voltages.py =================================================================== diff -u --- leahi-dialin/hd/voltages.py (revision 0) +++ leahi-dialin/hd/voltages.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,201 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file voltages.py +# +# @author (last) Micahel Garthwaite +# @date (last) 03-Mar-2023 +# @author (original) Sean Nash +# @date (original) 15-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from ..common.msg_defs import MsgIds, MsgFieldPositions + + +# Monitored voltages +@unique +class HDMonitoredVoltages(DialinEnum): + MONITORED_LINE_1_2V = 0 # Processor voltage (1.2V) + MONITORED_LINE_3_3V = 1 # Logic voltage (3.3V) + MONITORED_LINE_5V_LOGIC = 2 # Logic voltage (5V) + MONITORED_LINE_5V_SENSORS = 3 # Sensors voltage (5V) + MONITORED_LINE_24V = 4 # Actuators voltage (24V) + MONITORED_LINE_24V_REGEN = 5 # Actuators regen voltage (24V) + MONITORED_LINE_FPGA_REF_V = 6 # FPGA ADC reference voltage (1V) + MONITORED_LINE_PBA_REF_V = 7 # PBA ADC reference voltage (3V) + MONITORED_LINE_FPGA_VCC_V = 8 # FPGA Vcc (3V) + MONITORED_LINE_FPGA_AUX_V = 9 # FPGA Vaux (3V) + MONITORED_LINE_FPGA_PVN_V = 10 # FPGA Vpvn (1V) + NUM_OF_MONITORED_VOLTAGE_LINES = 11 # Number of HD operation modes + +class HDVoltages(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for voltage monitor related commands and data. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDVoltages constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_VOLTAGES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_monitored_voltages_sync) + self.monitored_voltages = [0.0] * HDMonitoredVoltages.NUM_OF_MONITORED_VOLTAGE_LINES.value + self.hd_voltages_timestamp = 0.0 + + def get_monitored_voltages(self): + """ + Gets all HD monitored voltages + + @return: List of voltages of size NUM_OF_MONITORED_VOLTAGE_LINES + """ + return self.monitored_voltages + + @publish(["hd_voltages_timestamp","monitored_voltages"]) + def _handler_monitored_voltages_sync(self, message, timestamp=0.0): + """ + Handles published HD monitored voltages data messages. Voltage data are captured + for reference. + + @param message: published monitored voltages data message + @return: none + """ + + v12 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + v33 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + v5l = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + v5s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + v24 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + v24g = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + vfr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + vpr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + vfc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + vfa = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + vfp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_1_2V.value] = v12[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_3_3V.value] = v33[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_5V_LOGIC.value] = v5l[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_5V_SENSORS.value] = v5s[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_24V.value] = v24[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_24V_REGEN.value] = v24g[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_REF_V.value] = vfr[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_PBA_REF_V.value] = vpr[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_VCC_V.value] = vfc[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_AUX_V.value] = vfa[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_PVN_V.value] = vfp[0] + self.hd_voltages_timestamp = timestamp + + def cmd_monitored_voltage_override(self, signal: int = 0, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD monitored voltage override command + Constraints: + Must be logged into HD. + Given signal must be valid member of HDMonitoredVoltages enum + + @param signal: integer - ID of signal to override + @param volts: float - value (in volts) to override signal with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vlt = float_to_bytearray(volts) + idx = integer_to_bytearray(signal) + payload = rst + vlt + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_MONITORED_VOLTAGES_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored HD voltage for signal " + str(signal)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = str(volts) + " V. " + self.logger.debug("Monitored HD voltage overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_monitored_voltages_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the monitored HD voltages broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored HD voltages broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("HD monitored voltages broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/hd/watchdog.py =================================================================== diff -u --- leahi-dialin/hd/watchdog.py (revision 0) +++ leahi-dialin/hd/watchdog.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,81 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file watchdog.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray + + +class HDWatchdog(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for watchdog related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + def cmd_watchdog_task_check_in_override(self, state: int, task: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the watchdog task check-in override command + Constraints: + Must be logged into HD. + Given task must be valid. + Given state must be a 0 or 1. + + @param state: integer - 1 for task checked in, 0 for task not checked in + @param task: integer - ID of task to override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + tsk = integer_to_bytearray(task) + payload = rst + sta + tsk + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_WATCHDOG_TASK_CHECKIN_OVERRIDE.value, + payload=payload) + + self.logger.debug("override watchdog task check-in state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("checked in" if state != 0 else "not checked in") + self.logger.debug("watchdog task check-in overridden to " + str_res + ":" + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi-dialin/protocols/CAN.py =================================================================== diff -u --- leahi-dialin/protocols/CAN.py (revision 0) +++ leahi-dialin/protocols/CAN.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,905 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file CAN.py +# +# @author (last) Micahel Garthwaite +# @date (last) 30-Jun-2023 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +import threading +import time +from collections import deque +import asyncio +from typing import Callable + +import can +from can.interfaces import socketcan +import math +from time import sleep +from datetime import datetime +import sys +from logging import Logger +import struct +from .. import common +from ..common import MsgIds +from ..utils import SingletonMeta, IntervalTimer +from concurrent.futures import ThreadPoolExecutor + +import os + +class DenaliMessage: + BYTE_ORDER = 'little' + START_BYTE = 0xA5 + START_INDEX = 0 + MSG_SEQ_INDEX = 1 + MSG_ID_INDEX = 3 + PAYLOAD_LENGTH_INDEX = 5 + PAYLOAD_START_INDEX = 6 + PAYLOAD_LENGTH_FIRST_PACKET = 1 + HEADER_LENGTH = 6 + PACKET_LENGTH = 8 + CRC_LENGTH = 1 + MAX_MSG_ID_NUMBER = 65535 + MAX_NUMBER_OF_PAYLOAD_BYTES = 254 + CRC_LIST = [ + 0, 49, 98, 83, 196, 245, 166, 151, 185, 136, 219, 234, 125, 76, 31, 46, + 67, 114, 33, 16, 135, 182, 229, 212, 250, 203, 152, 169, 62, 15, 92, 109, + 134, 183, 228, 213, 66, 115, 32, 17, 63, 14, 93, 108, 251, 202, 153, 168, + 197, 244, 167, 150, 1, 48, 99, 82, 124, 77, 30, 47, 184, 137, 218, 235, + 61, 12, 95, 110, 249, 200, 155, 170, 132, 181, 230, 215, 64, 113, 34, 19, + 126, 79, 28, 45, 186, 139, 216, 233, 199, 246, 165, 148, 3, 50, 97, 80, + 187, 138, 217, 232, 127, 78, 29, 44, 2, 51, 96, 81, 198, 247, 164, 149, + 248, 201, 154, 171, 60, 13, 94, 111, 65, 112, 35, 18, 133, 180, 231, 214, + 122, 75, 24, 41, 190, 143, 220, 237, 195, 242, 161, 144, 7, 54, 101, 84, + 57, 8, 91, 106, 253, 204, 159, 174, 128, 177, 226, 211, 68, 117, 38, 23, + 252, 205, 158, 175, 56, 9, 90, 107, 69, 116, 39, 22, 129, 176, 227, 210, + 191, 142, 221, 236, 123, 74, 25, 40, 6, 55, 100, 85, 194, 243, 160, 145, + 71, 118, 37, 20, 131, 178, 225, 208, 254, 207, 156, 173, 58, 11, 88, 105, + 4, 53, 102, 87, 192, 241, 162, 147, 189, 140, 223, 238, 121, 72, 27, 42, + 193, 240, 163, 146, 5, 52, 103, 86, 120, 73, 26, 43, 188, 141, 222, 239, + 130, 179, 224, 209, 70, 119, 36, 21, 59, 10, 89, 104, 255, 206, 157, 172 + ] + + _seq_num = 1 + + @staticmethod + def build_basic_message(channel_id=0, message=None): + """ + Builds a basic message dictionary containing the channel id and message + + @param channel_id: (int) indicates the channel + @param message: (list) integers forming the message + @return:: dictionary with channel_id and message keys + """ + if message is None: + message = [] + return {'channel_id': channel_id, 'message': message} + + @classmethod + def build_message(cls, channel_id=0, message_id=0, payload=None, seq=None): + """ + Builds a Denali message + + @param channel_id: (int) indicates the channel + @param message_id: (int) indicating the request type + @param payload: (list) contains the payload + @param seq: (int) Overrides current sequence number if set + @return:: dictionary with channel_id and 8-byte padded message + """ + + if payload is None: + payload = [] + + message_list = [DenaliCanMessenger.START_BYTE] + + if 0 <= message_id <= DenaliMessage.MAX_MSG_ID_NUMBER: + if seq is None: + # Wrap sequence number if it hits a max int16 + if cls._seq_num >= 32767: + cls._seq_num = 1 + + seq = cls._seq_num + + if message_id not in common.msg_defs.ACK_NOT_REQUIRED: + seq *= -1 + + message_seq_in_bytes = seq.to_bytes(2, byteorder=DenaliMessage.BYTE_ORDER, signed=True) + + message_list += [message_seq_in_bytes[0]] + message_list += [message_seq_in_bytes[1]] + + # Add message ID as unsigned 16-bit # + message_id_in_bytes = message_id.to_bytes(2, byteorder=DenaliMessage.BYTE_ORDER) + + message_list += [message_id_in_bytes[0]] + message_list += [message_id_in_bytes[1]] + + cls._seq_num += 1 + + else: + + return [] + + # Check payload length + payload_length = len(payload) + + # if payload is larger than 255 return nothing + if payload_length <= DenaliMessage.MAX_NUMBER_OF_PAYLOAD_BYTES: + # payload has to be a list + message_list += [payload_length] + else: + return [] + + message_list += payload + + # Because CRC does not include first byte, then we pass a list with out it + message_list += [DenaliMessage.crc8(message_list[1:])] + + message_list = DenaliMessage.pad_message_with_zeros(message_list) + + return DenaliMessage.build_basic_message(channel_id=channel_id, message=message_list) + + @staticmethod + def crc8(message_list): + """ + Returns the calculated crc from a message list + + @param message_list: is a list of integer numbers containing the message + @return:: integer containing a unsigned byte + """ + crc = 0 + for byte in message_list: + unsigned_byte = byte ^ crc + crc = DenaliMessage.CRC_LIST[unsigned_byte] + + return crc + + @staticmethod + def pad_message_with_zeros(message): + """ + Returns a packet padded with zeros that guarantees that the packet is a multiple of 8 bytes. + + @param message: packet that may or may not be multiple of 8 bytes + @return:: packet that is 8-byte multiple + + """ + message_length = len(message) + + # message must be multiple of 8 + if message_length % DenaliMessage.PACKET_LENGTH != 0: + # We need to pad the message with trailing zeros + add_these_many_zeros = math.ceil(message_length / DenaliMessage.PACKET_LENGTH) * \ + DenaliMessage.PACKET_LENGTH - message_length + + message += [0] * add_these_many_zeros + + return message + + @staticmethod + def get_crc(message): + """ + Gets the CRC in message + + @param message: Dialin complete message with CRC + + @return:: CRC in message + """ + + crc_index = DenaliMessage.PAYLOAD_START_INDEX + DenaliMessage.get_payload_length(message) + + return message['message'][crc_index] + + @staticmethod + def verify_crc(message): + """ + Verifies message CRC equals calculated message CRC + + @return:: TRUE if CRC matches, FALSE otherwise + """ + + if message is None: + return False + else: + message_list = message['message'] + + message_length = DenaliMessage.PAYLOAD_START_INDEX + DenaliMessage.get_payload_length(message) + calculated_crc = DenaliMessage.crc8(message_list[1:message_length]) + actual_crc = DenaliMessage.get_crc(message) + + return calculated_crc == actual_crc + + @staticmethod + def get_channel_id(message: dict) -> int: + """ + Returns request ID from message + + @param message: dictionary with channel_id and message keys + @return:: integer with channel id + + """ + + return message['channel_id'] + + @staticmethod + def get_sequence_number(message: dict) -> int: + """ + Returns sequence number from the message + + @param message: dictionary containing the message + @return:: (int) the sequence number + """ + seq = message['message'][DenaliMessage.MSG_SEQ_INDEX:DenaliMessage.MSG_ID_INDEX] + return int.from_bytes(seq, byteorder=DenaliMessage.BYTE_ORDER, signed=True) + + @staticmethod + def create_ack_message(message: dict, passive_mode: bool = True): + """ + Negates the sequence number and replaces the original message's sequence number with the + negated sequence number to create the ACK message. + @param message: (dict) a complete leahi-dialin message + @param passive_mode: (dict) true if in passive mode, false otherwise + @return: (dict) ACK message for the input message + """ + message = message.copy() + + seq = struct.unpack('h', bytearray( + message['message'][DenaliMessage.MSG_SEQ_INDEX:DenaliMessage.MSG_ID_INDEX]))[0] + + # send back empty payload since this is an ACK + payload = bytearray() + + channel_id_rx = DenaliMessage.get_channel_id(message) + + if passive_mode: + channel_rx_tx_pairs = { + DenaliChannels.hd_alarm_broadcast_ch_id: None, + DenaliChannels.dg_alarm_broadcast_ch_id: None, + DenaliChannels.ui_alarm_broadcast_ch_id: None, + DenaliChannels.hd_to_dg_ch_id: None, + DenaliChannels.dg_to_hd_ch_id: None, + DenaliChannels.hd_to_ui_ch_id: None, + DenaliChannels.hd_sync_broadcast_ch_id: None, + DenaliChannels.dg_to_ui_ch_id: None, + DenaliChannels.dg_sync_broadcast_ch_id: None, + DenaliChannels.ui_to_hd_ch_id: None, + DenaliChannels.ui_sync_broadcast_ch_id: None, + DenaliChannels.hd_to_dialin_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_dialin_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.ui_to_dialin_ch_id: DenaliChannels.dialin_to_ui_ch_id + } + else: + channel_rx_tx_pairs = { + DenaliChannels.hd_alarm_broadcast_ch_id: None, + DenaliChannels.dg_alarm_broadcast_ch_id: None, + DenaliChannels.ui_alarm_broadcast_ch_id: None, + DenaliChannels.hd_to_dg_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_hd_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.hd_to_ui_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.hd_sync_broadcast_ch_id: None, + DenaliChannels.dg_to_ui_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.dg_sync_broadcast_ch_id: None, + DenaliChannels.ui_to_hd_ch_id: DenaliChannels.dialin_to_ui_ch_id, + DenaliChannels.ui_sync_broadcast_ch_id: DenaliChannels.dialin_to_ui_ch_id, + DenaliChannels.hd_to_dialin_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_dialin_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.ui_to_dialin_ch_id: DenaliChannels.dialin_to_ui_ch_id + } + + channel_id_tx = channel_rx_tx_pairs.get(channel_id_rx, None) + + if channel_id_tx is None: + return None + + message = DenaliMessage.build_message( + channel_id=channel_id_tx, + message_id=common.msg_defs.MsgIds.MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK.value, + payload=payload, + seq=-seq) + + return message + + @staticmethod + def get_message_id(message): + """ + Returns request ID from packet + @param message: complete Diality Packet + @return:: integer with request ID + + """ + msg_id_array = message['message'][DenaliMessage.MSG_ID_INDEX: + DenaliMessage.PAYLOAD_LENGTH_INDEX] + return int.from_bytes(msg_id_array, DenaliMessage.BYTE_ORDER) + + @staticmethod + def get_message_id_xstr(message): + """ + Returns request ID from packet in hex string + @param message: complete Diality Packet + @return:: integer with request ID + """ + msg_id = "" + for index in range(DenaliMessage.MSG_ID_INDEX, DenaliMessage.PAYLOAD_LENGTH_INDEX): + msg_id += "{0:02X}" .format(message['message'][index]) + return msg_id + + @staticmethod + def get_payload_length(message): + """ + Returns payload length from message + + @param message: dictionary with channel_id and message keys + @return:: a unsigned payload length + """ + return message['message'][DenaliMessage.PAYLOAD_LENGTH_INDEX] + + @staticmethod + def get_payload(message): + """ + Returns payload array from message + + @param message: dictionary with channel_id and message keys + @return:: a payload array if exist + """ + + payload_length = DenaliMessage.get_payload_length(message) + + if payload_length == 0: + return None + else: + return message['message'][DenaliMessage.PAYLOAD_START_INDEX:] + + @staticmethod + def get_total_packets(message, is_array=True): + """ + Returns the number of packets needed to transmit Denali Message + + @param message: dictionary with channel_id and message keys or raw message + @param is_array: True if message is an array and not a dictionary + @return:: number of packets + """ + the_message = message if is_array else message['message'] + + return math.ceil((the_message[DenaliMessage.PAYLOAD_LENGTH_INDEX] + + DenaliMessage.HEADER_LENGTH + DenaliMessage.CRC_LENGTH) / + DenaliMessage.PACKET_LENGTH) + + +class DenaliChannels: + """ + Convenience class listing all the possible CAN channels used in the Denali system. + Updates made to the "Message List.xlsx" document found in the Diality Software Team SharePoint, + specifically in the CAN Channels sheet, should also be applied here. + """ + + hd_alarm_broadcast_ch_id = 0x001 + dg_alarm_broadcast_ch_id = 0x002 + ui_alarm_broadcast_ch_id = 0x004 + hd_to_dg_ch_id = 0x008 + dg_to_hd_ch_id = 0x010 + hd_to_ui_ch_id = 0x020 + hd_sync_broadcast_ch_id = 0x040 + dg_to_ui_ch_id = 0x070 + dg_sync_broadcast_ch_id = 0x080 + ui_to_hd_ch_id = 0x100 + ui_to_dg_ch_id = 0x110 + ui_sync_broadcast_ch_id = 0x200 + dialin_to_hd_ch_id = 0x400 + hd_to_dialin_ch_id = 0x401 + dialin_to_dg_ch_id = 0x402 + dg_to_dialin_ch_id = 0x403 + dialin_to_ui_ch_id = 0x404 + ui_to_dialin_ch_id = 0x405 + + +class LongDenaliMessageBuilder: + + def __init__(self, message: can.Message): + """ + LongDialityMessageBuilder is a utility object that helps construct a Denali message + that is longer than 8 bytes. It is only called when we don't yet have a long message + builder for the current channel. Basic principle is to construct an object with the + first 8 byte message which contains the length of the message, and the later push the + remaining messages. e.g., let's imagine a 3 message packet. + + obj = LongDialityMessageBuilder(msg1) + + message = obj.push(msg2), returns None, message is not complete + + message = obj.push(msg3), return the packet which is the concatenation of msg1, msg2 and msg3 + + @param message: a CAN message + """ + self.message_data = [b for b in message.data] + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(self.message_data) + self.number_of_can_packets_up_to_now = 1 + + def push(self, message: can.Message, first_packet=False): + """ + push appends the CAN message to the current list of messages + + @param message: 8-byte message + + @param first_packet: True if it is the first packet received + + @return:: None if the packet is not completed, otherwise returns the complete packet + """ + message_data = [b for b in message.data] + if first_packet: + self.message_data = message_data + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(message_data) + self.number_of_can_packets_up_to_now = 1 + + else: + self.message_data += message_data + self.number_of_can_packets_up_to_now += 1 + + if self.number_of_can_packets_up_to_now == self.number_of_can_packets_needed: + return_message = self.message_data + self.message_data = None + return return_message + + else: + return None + + +class DenaliCanMessenger(metaclass=SingletonMeta): + START_BYTE = DenaliMessage.START_BYTE + DIALIN_MSG_RESP_TO = 0.5 # number of seconds to wait for a response to a send command + + def __init__(self, can_interface: str, + logger: Logger, + passive_mode=True, + console_out=False): + """ + DenaliCanMessenger constructor + + @param can_interface - string containing the can interface, e.g., 'can0" + @return: DialityCanMessenger object + + """ + self.message_queue_mutex = threading.Lock() + self.response_dictionary_mutex = threading.Lock() + self.transmitting_mutex = threading.Lock() + self.logger = logger + self.message_queue = deque() + self.callback_listener_complete_messages = None + self.callback_listener_invalid_messages = None + self.thread_pool_executor = ThreadPoolExecutor(max_workers=1) + # TODO for debugging purposes + if os.path.exists('Listener_can_dump.log'): os.remove('Listener_can_dump.log') + self.temp_logger = open('Listener_can_dump.log', 'w') + if os.path.exists('Send_can_dump.log'): os.remove('Send_can_dump.log') + self.temp_send_logger = open('Send_can_dump.log', 'w') + if os.path.exists('dialin_processed_msg.log'): os.remove('dialin_processed_msg.log') + self.temp_dialin_processed_logger = open('dialin_processed_msg.log', 'w') + # TODO for debugging purposes + # try to setup can bus and exit if the can bus has not ben setup to use. + try: + self.bus = socketcan.SocketcanBus(channel=can_interface) + self.loop = asyncio.get_event_loop() + if self.bus is not None: + self.thread_canbus = threading.Thread(target=self.listener, daemon=True) + self.thread_message_queue = threading.Thread(target=self.handle_messages, daemon=True) + else: + self.thread_canbus = None + s = "Can connection is not valid" + self.logger.debug(s) + sys.exit(s) + self.listener_buffer = can.AsyncBufferedReader() + self.notifier = can.Notifier(bus=self.bus, listeners=[self.listener_buffer], loop=self.loop) + except Exception as e: + s = str(e) + self.logger.error(s) + print(s) + sys.exit(19) + + self.passive_mode = passive_mode + self.console_out = console_out + self.send_event = threading.Event() + self.long_message_builders = {} + self.long_msg_channel_id_set = set() + self.messages = None + self.command_response_message = None + self.response_channel_id = -1 + self.run = False + self.sync_response_dictionary = {} + self.ui_received_function_ptr = None + self.pending_requests = {} + self.transmit_interval_dictionary = {} + + def start(self): + """ + starts listening to the can interface. + + """ + + if self.bus is None: + self.logger.error("Cannot start can listener.") + return + else: + self.run = True + if self.thread_message_queue is not None and self.thread_canbus is not None: + if not self.thread_canbus.is_alive(): + self.thread_canbus.start() + self.logger.info("Canbus thread has started.") + if not self.thread_message_queue.is_alive(): + self.thread_message_queue.start() + self.logger.info("Message queue thread has started.") + else: + self.logger.error("Cannot start listener...") + + def stop(self): + """ + Stop listening the can interface + + """ + self.run = False + self.logger.debug("\nCan listener has stopped.") + + def listener(self): + """ + Listens for diality message on the can interface passed during construction. + """ + async def _listener(): + while True: + message = await self.listener_buffer.get_message() + + if message is not None: + data = str(time.time()) + ',' + str(message) + '\r' + self.temp_logger.write(data) + self.message_queue.append(message) + + else: # no new packets in receive buffer + # Careful here, making this any shorter will start limiting CPU time for other threads + sleep(0.01) + + if not self.loop.is_running(): + self.loop.run_until_complete(_listener()) + else: + self.loop.create_task(_listener()) + + def handle_messages(self): + """ + Handles messages added to the leahi-dialin canbus message queue + @return: None + """ + + while True: + + if not self.message_queue: + # Careful here, making this any shorter will start limiting CPU time for other threads + sleep(0.01) + else: + self.message_queue_mutex.acquire() + message: can.Message = self.message_queue.popleft() + + if message.dlc == DenaliMessage.PACKET_LENGTH: + # We have received a legit CAN message of 8 bytes + can_data = [b for b in message.data] + channel_id = message.arbitration_id + if not DenaliMessage.PAYLOAD_LENGTH_INDEX < len(can_data): + self.logger.error("Invalid Denali message received: {0}".format(message)) + self.messages = None # Can't process this message, get the next one + self.message_queue_mutex.release() + continue + else: + message_length = can_data[DenaliMessage.PAYLOAD_LENGTH_INDEX] + + # if we are building a long message, then proceed to push it to the channel dictionary + if channel_id in self.long_msg_channel_id_set: + self.messages = self.long_message_builders[channel_id].push(message) + + elif can_data[0] == DenaliMessage.START_BYTE and \ + message_length <= DenaliMessage.PAYLOAD_LENGTH_FIRST_PACKET: # This is a short packet + # This is the first time that we are building a message + self.messages = can_data # deliver the packet + + elif can_data[0] == self.START_BYTE and \ + message_length > DenaliMessage.PAYLOAD_LENGTH_FIRST_PACKET: # Long packet start + # We are starting to build a long message, include it in the lonMsgChannelIDSet + self.long_msg_channel_id_set.add(channel_id) + + # if we don't have a long Denali message builder yet, create it + if channel_id not in self.long_message_builders.keys(): + self.long_message_builders[channel_id] = LongDenaliMessageBuilder(message) + self.messages = None + + else: # if we do have a builder. This is the first time + self.long_message_builders[channel_id].push(message, first_packet=True) + self.messages = None + + # Do we have a complete (long or short) Denali Message? + if self.messages is not None: + message_valid = True # assume true for now, set to false if CRC check fails below + complete_dialin_message = DenaliMessage.build_basic_message(channel_id=channel_id, + message=self.messages) + dialin_msg_id = DenaliMessage.get_message_id(complete_dialin_message) + dialin_ch_id = DenaliMessage.get_channel_id(complete_dialin_message) + self.temp_dialin_processed_logger.write(str(complete_dialin_message)) + if dialin_ch_id in self.long_msg_channel_id_set: + # We need to remove channel ID from the long message set + self.long_msg_channel_id_set.remove(dialin_ch_id) + + # Need to verify CRC at this point + if not DenaliMessage.verify_crc(complete_dialin_message): + # if verify is False, let's drop (ignore) this message + message_valid = False + dialin_ch_id = None + dialin_msg_id = None + self.logger.critical( + "Incorrect CRC, received message: {}, crc: {}, calculated crc: {}\n".format( + self.messages, DenaliMessage.get_crc(complete_dialin_message), + DenaliMessage.crc8(self.messages))) + + if message_valid: + if self.console_out: + self.do_console_out(complete_dialin_message) + # Send an ack if required + if DenaliMessage.get_sequence_number(complete_dialin_message) < 0: + # ACK required. Send back the received message with the sequence sign bit flipped + msg = DenaliMessage.create_ack_message(message=complete_dialin_message, + passive_mode=False) + if msg is not None: + self.send(msg, 0, is_ack=True) + + # We first check if this is a response to a send request that is pending + if self.pending_requests and dialin_msg_id in self.pending_requests: + + self.pending_requests[dialin_msg_id] = complete_dialin_message + self.send_event.set() + + # If it is not, this is a publication message and we need to call it's register function + else: + self.response_dictionary_mutex.acquire() + + if DenaliCanMessenger.is_ui_received_channel(dialin_ch_id): # check if the channel is in ui channels + if self.ui_received_function_ptr is not None: + self.thread_pool_executor.submit( + self.ui_received_function_ptr, + complete_dialin_message, message.timestamp + ) + + if dialin_ch_id in self.sync_response_dictionary.keys() and \ + dialin_msg_id in self.sync_response_dictionary[channel_id].keys(): + for function_id in self.sync_response_dictionary[dialin_ch_id][dialin_msg_id]: + self.thread_pool_executor.submit( + self.sync_response_dictionary[dialin_ch_id][dialin_msg_id][function_id], + complete_dialin_message, message.timestamp) + + self.response_dictionary_mutex.release() + else: + self.logger.critical("Invalid message: {}\n".format(self.messages)) + + # Done with this message, let's get the next one + self.messages = None + self.message_queue_mutex.release() + + @staticmethod + def is_ui_received_channel(channel_id: int) -> bool: + """ + checks if the channel id, channel_id is ui channel. + @param channel_id: the channel id to check + @return: true, if the channel is the ui channel. + """ + if channel_id in { + DenaliChannels.ui_to_hd_ch_id, + DenaliChannels.ui_to_dg_ch_id, + DenaliChannels.ui_to_dialin_ch_id, + DenaliChannels.ui_sync_broadcast_ch_id, + DenaliChannels.ui_alarm_broadcast_ch_id + }: # check if the channel is in ui channels + return True + else: + return False + + def register_receiving_publication_function(self, channel_id, message_id, function): + """ + Assign a function with packet parameter to an sync request id, e.g., + def function(packet). + + @param channel_id: can channel number where messages are received + @param message_id: Diality request ID in message + @param function: function reference + """ + # function_id is a UID for each callback per channel,msg pair. + function_id = id(function) + # if the channel_id exist, we update the dictionary for the channel_id + self.response_dictionary_mutex.acquire() + if channel_id in self.sync_response_dictionary.keys(): + if message_id in self.sync_response_dictionary[channel_id].keys(): + self.sync_response_dictionary[channel_id][message_id].update({function_id: function}) + else: + self.sync_response_dictionary[channel_id].update( {message_id: {function_id: function}}) + + # otherwise, we need to create the dictionary for the channel_id, msg_id pair + else: + self.sync_response_dictionary[channel_id] = {message_id: {function_id: function}} + self.response_dictionary_mutex.release() + + def register_received_all_ui_publication_function(self, function_ptr: Callable): + """ + Assign a function with packet parameter to an sync request id, e.g., + def function(packet). + + @param function_ptr: function reference + """ + + self.ui_received_function_ptr = function_ptr + + def send(self, + built_message: dict, + time_out: float = DIALIN_MSG_RESP_TO, + resend: bool = False, + is_ack: bool = False): + """ + Sends a Denali message + + @param built_message: (dict) message built using DialinMessage class + @param time_out: (float) time it will wait for a response in seconds + @param resend: (bool) Allow resending the message when no response is received. Disabled by default + @param is_ack: (bool) If we're sending an ACK, False by default + @return: (dict) The Denali packet. If a timeout occurs returns None + """ + data = str(time.time()) + ',' + str(built_message) + '\r' + self.temp_send_logger.write(data) + msg_sent = False + msg_id = -1 + + # keep trying to send message until we get a response + while not msg_sent: + + channel_id = DenaliMessage.get_channel_id(built_message) + + padded_can_message_array = built_message['message'] + + msg_id = DenaliMessage.get_message_id(built_message) + if not is_ack and msg_id not in self.pending_requests: + self.pending_requests[msg_id] = None + + # A message can be longer than 8 bytes, so we need to split it + # into 8 bytes packets. + + number_of_packets = DenaliMessage.get_total_packets(padded_can_message_array) + + # We are sending one message at a time on CAN + self.transmitting_mutex.acquire() + + for n in range(number_of_packets): + packet = padded_can_message_array[n * DenaliMessage.PACKET_LENGTH: + (n + 1) * DenaliMessage.PACKET_LENGTH] + + # Sending one packet at a time + packet = can.Message(arbitration_id=channel_id, + data=packet, + is_extended_id=False) + + self.bus.send(packet, 0) # 0.1) + + self.transmitting_mutex.release() + + # After all messages have been sent, we clear a flag + self.send_event.clear() + + # Block until the timeout completes or until the threading event's flag is set in self.listener, + # indicating a response has been received + self.send_event.wait(time_out) + + # if we're sending an ack, nothing left to do + if is_ack: + return None + + # only resend the message if resend is enabled and we haven't received a response yet + if resend and self.pending_requests.get(msg_id, None) is None: + msg_sent = False + self.logger.debug("No response. Re-sending message.") + else: + msg_sent = True + + response = self.pending_requests.get(msg_id, None) + if response is not None: + del self.pending_requests[msg_id] + return response + + @staticmethod + def _format_message_candump_style(message: can.Message, channel: str, send: bool = True) -> str: + """ + Formats a packet + @param message: (can.Message) The packet to log + @param channel: (str) The channel send or received on + @param send: (bool) Whether we're sending or receiving this packet + @return: The styled message + """ + + tmp = str(message) + + data = tmp[-41:-18].upper() + if send: + data = tmp[-23:].upper() + + return " {0} {1} [{2}] {3}\n".format(channel, str(hex(message.arbitration_id)[2:]).zfill(3), message.dlc, data) + + def do_log_can(self, packet: can.Message, style="candump", channel="can0", send=True): + """ + Logs all packets sent or received by leahi-dialin in candump, or non-candump style format + + @param packet: (can.Message) The packet to log + @param style: (str) The style to log in (candump, non-candump) + @param channel: (str) The channel send or received on + @param send: (bool) Whether we're sending or receiving this packet + @return: None + """ + filename = "Dialin_CAN_Send.log" + if not send: + filename = "Dialin_CAN_Receive.log" + if style == "candump": + with open(filename, 'a') as f: + styled_message = self._format_message_candump_style(message=packet, channel=channel, send=send) + f.write(styled_message) + else: + with open(filename, 'a') as f: + f.write("{0}\n".format(packet)) + + @staticmethod + def convert_message_to_string(complete_dialin_message: dict) -> str: + """ + Converts the DenaliMessage to hex string (data len is not hex) + @param complete_dialin_message: the complete can message in dictionary + @return: + """ + channel = "{0:03X}" .format(DenaliMessage.get_channel_id(complete_dialin_message)) + msg_id = DenaliMessage.get_message_id_xstr(complete_dialin_message) + data_len = DenaliMessage.get_payload_length(complete_dialin_message) + length = "{0:02X}" .format(data_len) + data = "" + pram_len = 0 + if data_len != 0: + pram_len = int(data_len / 4) + for i in range(pram_len): + data += "{0:02X}".format(complete_dialin_message['message'][DenaliMessage.PAYLOAD_START_INDEX + i ]) + data += "{0:02X}".format(complete_dialin_message['message'][DenaliMessage.PAYLOAD_START_INDEX + i + 1]) + data += "{0:02X}".format(complete_dialin_message['message'][DenaliMessage.PAYLOAD_START_INDEX + i + 2]) + data += "{0:02X}".format(complete_dialin_message['message'][DenaliMessage.PAYLOAD_START_INDEX + i + 3]) + data += " " + message = "{} {} {} {}".format(channel, msg_id, data_len, data) + return message + + @staticmethod + def do_console_out(complete_dialin_message: dict) -> None: + """ + prints out the message in hex format similar to the candump + @return: None + """ + exception_msg_id = { + MsgIds.MSG_ID_UI_CHECK_IN.value, + MsgIds.MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK + } + msg_id = DenaliMessage.get_message_id(complete_dialin_message) + if msg_id in exception_msg_id: + return + + message = "# " + DenaliCanMessenger.convert_message_to_string(complete_dialin_message) + print(message) + + def register_transmitting_interval_message(self, interval: float, function) ->None: + """ + registers a callback function with a specified time interval to a dictionary + @return: None + """ + function_id = id(function) + + if function_id in self.transmit_interval_dictionary.keys(): + self.logger.error("ERROR: Attempting to assign more than one timed interval per given method.") + self.transmit_interval_dictionary[function_id].stop() + self.transmit_interval_dictionary[function_id].start() + else: + self.transmit_interval_dictionary[function_id] = IntervalTimer(interval, function) + + return function_id Index: leahi-dialin/protocols/__init__.py =================================================================== diff -u --- leahi-dialin/protocols/__init__.py (revision 0) +++ leahi-dialin/protocols/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,4 @@ +from .CAN import DenaliCanMessenger +from .CAN import DenaliChannels +from .CAN import LongDenaliMessageBuilder +from .CAN import DenaliMessage Index: leahi-dialin/ui/__init__.py =================================================================== diff -u --- leahi-dialin/ui/__init__.py (revision 0) +++ leahi-dialin/ui/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,2 @@ +from .hd_simulator import HDSimulator +from .dg_simulator import DGSimulator Index: leahi-dialin/ui/crc.py =================================================================== diff -u --- leahi-dialin/ui/crc.py (revision 0) +++ leahi-dialin/ui/crc.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,63 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file crc.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +crc8_table = ( + 0, 49, 98, 83, 196, 245, 166, 151, 185, 136, 219, 234, 125, 76, 31, 46, + 67, 114, 33, 16, 135, 182, 229, 212, 250, 203, 152, 169, 62, 15, 92, 109, + 134, 183, 228, 213, 66, 115, 32, 17, 63, 14, 93, 108, 251, 202, 153, 168, + 197, 244, 167, 150, 1, 48, 99, 82, 124, 77, 30, 47, 184, 137, 218, 235, + 61, 12, 95, 110, 249, 200, 155, 170, 132, 181, 230, 215, 64, 113, 34, 19, + 126, 79, 28, 45, 186, 139, 216, 233, 199, 246, 165, 148, 3, 50, 97, 80, + 187, 138, 217, 232, 127, 78, 29, 44, 2, 51, 96, 81, 198, 247, 164, 149, + 248, 201, 154, 171, 60, 13, 94, 111, 65, 112, 35, 18, 133, 180, 231, 214, + 122, 75, 24, 41, 190, 143, 220, 237, 195, 242, 161, 144, 7, 54, 101, 84, + 57, 8, 91, 106, 253, 204, 159, 174, 128, 177, 226, 211, 68, 117, 38, 23, + 252, 205, 158, 175, 56, 9, 90, 107, 69, 116, 39, 22, 129, 176, 227, 210, + 191, 142, 221, 236, 123, 74, 25, 40, 6, 55, 100, 85, 194, 243, 160, 145, + 71, 118, 37, 20, 131, 178, 225, 208, 254, 207, 156, 173, 58, 11, 88, 105, + 4, 53, 102, 87, 192, 241, 162, 147, 189, 140, 223, 238, 121, 72, 27, 42, + 193, 240, 163, 146, 5, 52, 103, 86, 120, 73, 26, 43, 188, 141, 222, 239, + 130, 179, 224, 209, 70, 119, 36, 21, 59, 10, 89, 104, 255, 206, 157, 172 +) + + +def crc8(data): + """ + generates crc8 for the data vData + @param data: byte of data + @return: (int) the crc code + """ + crc = 0 + length = len(data) + i = 0 + while length > 0: + crc = crc8_table[data[i] ^ crc] + length = length - 1 + i = i + 1 + return crc + + +def calc_crc8(vstring, vdelimiter='.'): + """ + calculates crc8 for each character in string vString + @param vstring: (str) the bytes of data + @param vdelimiter: (character) the string delimiter + @return: the hex formatted crc of the given string + """ + new_str = vstring.replace(vdelimiter, '') + ba = bytearray.fromhex(new_str) + x = '{:02X}'.format(crc8(ba), 'x') + return x Index: leahi-dialin/ui/dg_simulator.py =================================================================== diff -u --- leahi-dialin/ui/dg_simulator.py (revision 0) +++ leahi-dialin/ui/dg_simulator.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,700 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file dg_simulator.py +# +# @author (last) Vy +# @date (last) 10-Oct-2023 +# @author (original) Peter Lucia +# @date (original) 16-Mar-2021 +# +############################################################################ +from ..common import * +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, LogManager +from . import messageBuilder + + +class DGSimulator(AbstractSubSystem): + instance_count = 0 + + def __init__(self, can_interface: str = "can0", + log_level: str = None, + console_out: bool = False, + passive_mode: bool = False, + auto_response: bool = False): + + super().__init__() + DGSimulator.instance_count = DGSimulator.instance_count + 1 + + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + self.console_out = console_out + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger, + console_out=console_out, + passive_mode=passive_mode) + self.can_interface.start() + + self.ui_dg_set_rtc_req_timestamp = 0.0 + self.ui_dg_fw_version_req_timestamp = 0.0 + self.ui_service_info_req_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_dg_ch_id + if auto_response: + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_DG_SET_RTC_REQUEST.value, + self._handler_set_rtc_request) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_FW_VERSIONS_REQUEST.value, + self._handler_request_dg_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_UI_SERVICE_INFO_REQUEST.value, + self._handler_system_usage_response) + + def _handler_system_usage_response(self,message,timestamp=0.0) -> None: + """ + Handles a request for system usage + + @return: None + """ + self.logger.debug("Handling request for system usage.") + self.ui_service_info_req_timestamp = timestamp + + payload = integer_to_bytearray(1619628663) + payload += integer_to_bytearray(1619887863) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERVICE_SCHEDULE_DATA_TO_UI.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_set_rtc_request(self, message: dict,timestamp=0.0) -> None: + """ + Handles a request to set the DG RTC + @param message: (dict) the message content + @return: None + """ + + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.ui_dg_set_rtc_req_timestamp = timestamp + self.logger.debug("DG: Request to set the DG epoch to {0}".format(epoch)) + + self.cmd_send_set_rtc_response(YES, 0) + + def cmd_send_set_rtc_response(self, response: int, reason: int) -> None: + """ + Sends a set RTC response message + + @param response: integer - 0=NO, 1=YES + @param reason: integer - the rejection reason + @return: None + """ + self.logger.debug("DG: Sending response {0} reason {1}".format(response, reason)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_UI_SET_RTC_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_checkin_dg(self) -> None: + """ + check-in (keep alive) message from DG + @return: none + """ + + payload = ["A5", "01", "00", "06", "00", "00", "76", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.dg_to_hd_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_set_dg_ro_pump_data(self, set_pt_pressure: int, flow_rate: float, pwm: float) -> None: + """ + the DG RO Pump Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x1F00| 0x080 | 8 | 1 Hz | N | DG | All | DG RO Pump Data | \ref Data::mPressure | \ref Data::mFlowRate | \ref Data::mPWM | @param vSetPtPressure: + + @param set_pt_pressure: (int) set Point Pressure + @param flow_rate: float - Flow Rate + @param pwm: float - PWM + @return: none + """ + + payload = integer_to_bytearray(set_pt_pressure) + payload += float_to_bytearray(flow_rate) + payload += float_to_bytearray(pwm) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_RO_PUMP_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_pressures_data(self, ro_inlet_pressure: float, ro_outlet_pressure: float, + drain_inlet_pressure: float, drain_outlet_pressure: float, + barometric_pressure : float) -> None: + """ + the DG Pressures Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x2000| 0x080 | 8 | 1 Hz | N | DG | All | DG Pressures Data | \ref Data::mROInletPSI | \ref Data::mROOutletPSI | \ref Data::mDrainInletPSI | \ref Data::mDrainOutletPSI | + + @param ro_inlet_pressure: float - RO Inlet PSI + @param ro_outlet_pressure: float - RO Outlet PSI + @param drain_inlet_pressure: float - Drain Inlet PSI + @param drain_outlet_pressure: float - Drain Outlet PSI + @return: none + """ + + payload = float_to_bytearray(ro_inlet_pressure) + payload += float_to_bytearray(ro_outlet_pressure) + payload += float_to_bytearray(drain_inlet_pressure) + payload += float_to_bytearray(drain_outlet_pressure) + payload += float_to_bytearray(barometric_pressure) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_PRESSURES_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_drain_pump_data(self, set_pt_pwm: int, dac_value: int) -> None: + """ + the DG Drain Pump Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x2400| 0x080 | 8 | 1 Hz | N | DG | All | DG Drain Pump Data | \ref Data::mRPM | \ref Data::mDAC | + + @param set_pt_pwm: integer - Set Point RPM + @param dac_value: integer - DAC Value + @return: none + """ + + payload = integer_to_bytearray(set_pt_pwm) + payload += integer_to_bytearray(dac_value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_operation_mode(self, dg_op_mode: int) -> None: + """ + the DG Operation Mode Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2700| 0x080 | 8 | 1 Hz | N | DG | All | DG Operation Mode Data | \ref Data::mOpMode | + + @param dg_op_mode: integer - DG Operation Mode + @return: none + """ + + payload = integer_to_bytearray(dg_op_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_OP_MODE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_reservoir_data(self, active_reservoir: int, fill_to_vol_ml: int, drain_to_vol_ml: int) -> None: + """ + the DG Reservoir Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2800| 0x080 | 8 | 1 Hz | N | DG | All | DG Reservoir Data | \ref Data::mActiveReservoir | \ref Data::mFillToVol | \ref Data::mDrainToVol | + + @param active_reservoir: integer - Active Reservoir + @param fill_to_vol_ml: integer - Fill To Volume ML + @param drain_to_vol_ml: integer - Drain To Vol ML + @return: none + """ + + payload = integer_to_bytearray(active_reservoir) + payload += integer_to_bytearray(fill_to_vol_ml) + payload += integer_to_bytearray(drain_to_vol_ml) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_RESERVOIRS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_valves_states(self, valves_states): + """ + the DG Valves States Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U16) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2A00| 0x080 | 8 | 2 Hz | N | DG | All | DG Valves States Data | \ref Data::mStates | + + @param valves_states: integer - Valves states + @return: none + """ + + payload = integer_to_bytearray(valves_states) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_VALVES_STATES_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_heaters_data(self, main_primary_dc: int, small_primary_dc: int, trimmer_dc: int, + primary_target_temp : float, trimmer_target_temp : float, + primary_heater_state: float, trimmer_heater_state: float, + primary_efficiency : float, primary_calc_target_temp:float, + trimmer_calc_current_temp: float, trimmer_use_last_dc:float) -> None: + """ + the DG Heaters Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2C00| 0x080 | 8 | 2 Hz | N | DG | All | DG Heaters Data | \ref Data::mMainPrimaryDC | \ref Data::mSmallPrimaryDC | \ref Data::mTrimmerDC | + + @param main_primary_dc: integer - Main PriMary DC + @param small_primary_dc: integer - Small Primary DC + @param trimmer_dc: integer - Trimmer DC + @return: none + """ + + payload = integer_to_bytearray(main_primary_dc) + payload += integer_to_bytearray(small_primary_dc) + payload += integer_to_bytearray(trimmer_dc) + payload += integer_to_bytearray(primary_target_temp) + payload += integer_to_bytearray(trimmer_target_temp) + payload += integer_to_bytearray(primary_heater_state) + payload += integer_to_bytearray(trimmer_heater_state) + payload += integer_to_bytearray(primary_efficiency) + payload += integer_to_bytearray(primary_calc_target_temp) + payload += integer_to_bytearray(trimmer_calc_current_temp) + payload += integer_to_bytearray(trimmer_use_last_dc) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_HEATERS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_load_cell_readings_data(self, reservoir1_primary: float, reservoir1_backup: float, + reservoir2_primary: float, reservoir2_backup: float) -> None: + """ + The DG Load Cell Readings Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x0C00| 0x080 | 8 | 10 Hz | N | DG | All | DG Load Cell Readings Data | \ref Data::mReservoir1Prim | \ref Data::mReservoir1Bkup | \ref Data::mReservoir2Prim | \ref Data::mReservoir2Bkup | + @param reservoir1_primary: float - Reservoir 1 Primary + @param reservoir1_backup: float - Reservoir 1 Backup + @param reservoir2_primary: float - Reservoir 2 Primary + @param reservoir2_backup: float - Reservoir 2 Backup + @return: none + """ + + payload = float_to_bytearray(reservoir1_primary) + payload += float_to_bytearray(reservoir1_backup) + payload += float_to_bytearray(reservoir2_primary) + payload += float_to_bytearray(reservoir2_backup) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_READINGS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_temperatures_data(self, inlet_primary_heater: float, outlet_primary_heater: float, + conductivity_sensor1: float, conductivity_sensor2: float, + outlet_redundancy: float, inlet_dialysate: float, + primary_heater_thermocouple: float, trimmer_heater_thermocouple: float, + primary_heater_cold_junction: float, trimmer_heater_cold_junction: float, + primary_heater_internal_temp: float, trimmer_heater_internal_temp: float) -> None: + """ + the DG Temperatures Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2D00| 0x080 | 8 | 2 Hz | N | DG | All | DG Temperatures Data | \ref Data::mInletPrimaryHeater | \ref Data::mOutletPrimaryHeater | \ref Data::mConductivitySensor1 | + + | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | #8:(F32) | + |:--: |:--: |:--: |:--: |:--: | + | \ref Data::mConductivitySensor2 | \ref Data::mOutletRedundancy | \ref Data::mInletDialysate | \ref Data::mPrimaryHeaterThermoCouple | \ref Data::mTrimmerHeaterThermoCouple | + + | #9:(F32) | #10:(F32) | #11:(F32) | #12:(F32) | + | :--: |:--: |:--: |:--: | + | \ref Data::mPrimaryHeaterColdJunction | \ref Data::mTrimmerHeaterColdJunction | \ref Data::mPrimaryHeaterInternal | \ref Data::mTrimmerHeaterInternal | + @param inlet_primary_heater: (float) Inlet Primary Heater + @param outlet_primary_heater: (float) Outlet Primary Heater + @param conductivity_sensor1: (float) Conductivity Sensor 1 + @param conductivity_sensor2: (float) Conductivity Sensor 2 + @param outlet_redundancy: (float) Outlet Redundancy + @param inlet_dialysate: (float) Inlet Dialysate + @param primary_heater_thermocouple: (float) Primary Heater Thermocouple + @param trimmer_heater_thermocouple: (float) Trimmer Heater Thermocouple + @param primary_heater_cold_junction: (float) Primary Heater ColdJunction + @param trimmer_heater_cold_junction: (float) Trimmer Heater ColdJunction + @param primary_heater_internal_temp: (float) Primary Heater Internal Temperature + @param trimmer_heater_internal_temp: (float) Trimmer HeaterInternal Temperature + @return: none + """ + + payload = float_to_bytearray(inlet_primary_heater) + payload += float_to_bytearray(outlet_primary_heater) + payload += float_to_bytearray(conductivity_sensor1) + payload += float_to_bytearray(conductivity_sensor2) + payload += float_to_bytearray(outlet_redundancy) + payload += float_to_bytearray(inlet_dialysate) + payload += float_to_bytearray(primary_heater_thermocouple) + payload += float_to_bytearray(trimmer_heater_thermocouple) + payload += float_to_bytearray(primary_heater_cold_junction) + payload += float_to_bytearray(trimmer_heater_cold_junction) + payload += float_to_bytearray(primary_heater_internal_temp) + payload += float_to_bytearray(trimmer_heater_internal_temp) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_TEMPERATURE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_unknown_dg(self) -> None: + """ + the unknown message from DG setter/sender method + @return: none + """ + + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_UNUSED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_accelerometer_dg_data(self, x: float, y: float, z: float, + x_max: float, y_max: float, z_max: float, + x_tilt: float, y_tilt: float, z_tilt: float) -> None: + """ + the accelerometer hd data message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x3400| 0x080 | 8 | 1Hz | N | HD | UI | DG Accelerometer data | + + | #1:(F32) | #2:(F32) | #3:(U32) | + |:--: |:--: |:--: | + | \ref Data::mX | \ref Data::mY | \ref Data::mX | + + | #4:(F32) | #5:(F32) | #6:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXMax | \ref Data::mYMax | \ref Data::mXMax | + + | #7:(F32) | #8:(F32) | #9:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXTilt | \ref Data::mYTilt | \ref Data::mXTilt | + + @param x: float - x axis + @param y: float - y axis + @param z: float - z axis + @param x_max: float - x axis max + @param y_max: float - y axis max + @param z_max: float - z axis max + @param x_tilt: float - x axis tilt + @param y_tilt: float - y axis tilt + @param z_tilt: float - z axis tilt + @return: None + """ + + payload = float_to_bytearray(x) + payload += float_to_bytearray(y) + payload += float_to_bytearray(z) + payload += float_to_bytearray(x_max) + payload += float_to_bytearray(y_max) + payload += float_to_bytearray(z_max) + payload += float_to_bytearray(x_tilt) + payload += float_to_bytearray(y_tilt) + payload += float_to_bytearray(z_tilt) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCELEROMETER_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_dg_version(self, message,timestamp=0.0) -> None: + """ + Handles a request for the HD version + + @return: None + """ + self.logger.debug("Handling request for dg version.") + self.ui_dg_fw_version_req_timestamp = timestamp + self.cmd_send_version_dg_data(9, 9, 9, 9, 9, 9, 9, 9) + self.cmd_send_dg_serial_number() + + def cmd_send_dg_serial_number(self) -> None: + """ + Sends the dg serial number + @return: None + """ + payload = b'0123456789\0' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERIAL_NUMBER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_version_dg_data(self, major: int, minor: int, micro: int, build: int, + fpga_id: int, fpga_major: int, fpga_minor: int, fpga_lab: int, + compatibility_rev: int) -> None: + """ + [0x1E00] # 30 0x080 Rsp Y DG All + DG f/w version + U08=Major + U08=Minor + U08=Micro + U16=Build + U08=FPGA ID + U08=FPGA Major + U08=FPGA Minor + U08=FPGA Lab + U32=compatibility rev + --- + the dg version response message method + @param major: (uint) - Major version number + @param minor: (uint) - Minor version number + @param micro: (uint) - Micro version number + @param build: (uint) - Build version number + @param fpga_id: (int) - FPGA id version number + @param fpga_major: (int) - FPGA Major version number + @param fpga_minor: (int) - FPGA Minor version number + @param fpga_lab: (int) - FPGA Lab version number + @param compatibility_rev: (uint) - The FWs/UI compatibility revision + @return: None + """ + + payload = unsigned_byte_to_bytearray(major) + payload += unsigned_byte_to_bytearray(minor) + payload += unsigned_byte_to_bytearray(micro) + payload += unsigned_short_to_bytearray(build) + payload += unsigned_byte_to_bytearray(fpga_id) + payload += unsigned_byte_to_bytearray(fpga_major) + payload += unsigned_byte_to_bytearray(fpga_minor) + payload += unsigned_byte_to_bytearray(fpga_lab) + payload += unsigned_integer_to_bytearray(compatibility_rev) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_VERSION_REPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_serial_dg_data(self, serial: str): + """ + the dg version serial response message method + @param serial: serial number + @return: None + """ + + payload = bytes(serial, 'ascii') + b'\x00' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERIAL_NUMBER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + @staticmethod + def build_dg_debug_text(text: str) -> list: + """ + the debug text message from DG builder method + @param text: string - the debug text + @return: none + """ + message_length = 40 + txt = messageBuilder.textToByte(text, message_length) # + 1 null term + msg = messageBuilder.buildMessage(GuiActionType.DGDebugText, 1 * (message_length + 1), False, txt) + return messageBuilder.toFrames(msg) + + def cmd_send_dg_pre_treatment_filter_flush_progress_data(self, total, countdown) -> None: + """ + send the pretreatment filter flush progress data + @param total: (U32) Total time in second + @param countdown: (U32) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_FILTER_FLUSH_PROGRESS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_disinfect_progress_time_heat(self, total: int, countdown: int) -> None: + """ + the broadcast progress heat disinfect time + @param total: the total time + @param countdown: the gradual countdown time + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_HEAT_DISINFECT_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_post(self, item: int, passed: bool, done: bool = False) -> None: + """ + send hd post message the single(item) or the final(done) + @param item: the post state/item index + @param passed: the post result single or final + @param done: if this is the final post message this should be true + @return: None + """ + payload = integer_to_bytearray(passed) + if not done: + payload += integer_to_bytearray(item) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_POST_FINAL_TEST_RESULT.value if done + else MsgIds.MSG_ID_DG_POST_SINGLE_TEST_RESULT.value, + payload=payload) + + self.can_interface.send(message, 0) + + # ------------------------------------------------ GENERAL MESSAGES ------------------------------------------------ + + def cmd_send_dg_disinfect_progress_time_checmical(self, total: int, countdown: int) -> None: + """ + the broadcast progress chemical disinfect time + @param total: the total time + @param countdown: the gradual countdown time + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_CHEM_DISINFECT_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_general_response(self, message_id: int, accepted: int, reason: int, + is_pure_data: bool = False, + has_parameters: bool = False, + parameters_payload: any = 0x00, + channel_id=DenaliChannels.dg_to_ui_ch_id) -> None: + """ + a general method to send any standard response message, by it's id and list of parameters. + @param message_id: the id of the message + @param accepted: the standard accepted parameter of any response message + @param reason: the standard rejection reason parameter of any response message + @param is_pure_data: The message only has data + @param has_parameters: if the message has parameter this needs to be true. + @param parameters_payload: the list of parameters pre-converted and ready to be concatenated to the payload. + @param channel_id: (int) indicates the channel + @return: None + """ + payload = "" + + if not is_pure_data: + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + if has_parameters: + payload = parameters_payload + + message = DenaliMessage.build_message(channel_id=channel_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_general_progress_data(self, message_id: int, total: int, countdown: int) -> None: + """ + a general method t send any standard progress data message, by id + @param message_id: the id of the message + @param total: the total value of the progress data + @param countdown: the remaining or countdown value + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_ack(self, seq: int) -> None: + """ + sending dg ack message by the sequence seq + @param seq: the message sequence number + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=GuiActionType.Acknow, + seq=seq) + + self.can_interface.send(message, 0) + + def cmd_send_dg_conductivity_data(self, ro_rejection_ratio: float, cpi_conductivity: float, + cpo_conductivity: float, cd1_conductivity: float, + cd2_conductivity: float, CPi_raw : float, CPo_raw:float, + CD1_raw: float, CD2_raw:float, CPi_sensor_status:int, + CPo_sensor_status : int, CD1_sensor_status:int, + CD2_sensor_status: int) -> None: + """ + A simulated DG broadcast message of conductivity data. + @param ro_rejection_ratio: (float) RO Pump rejection ratio + @param cpi_conductivity: (float) CPi conductivity + @param cpo_conductivity: (float) CPo conductivity + @param cd1_conductivity: (float) CD1 conductivity + @param cd2_conductivity: (float) CD2 conductivity + @return: None + """ + payload = float_to_bytearray(ro_rejection_ratio) + payload += float_to_bytearray(cpi_conductivity) + payload += float_to_bytearray(cpo_conductivity) + payload += float_to_bytearray(cd1_conductivity) + payload += float_to_bytearray(cd2_conductivity) + payload += float_to_bytearray(CPi_raw) + payload += float_to_bytearray(CPo_raw) + payload += float_to_bytearray(CD1_raw) + payload += float_to_bytearray(CD2_raw) + payload += integer_to_bytearray(CPi_sensor_status) + payload += integer_to_bytearray(CPo_sensor_status) + payload += integer_to_bytearray(CD1_sensor_status) + payload += integer_to_bytearray(CD2_sensor_status) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_CONDUCTIVITY_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + Index: leahi-dialin/ui/hd_simulator.py =================================================================== diff -u --- leahi-dialin/ui/hd_simulator.py (revision 0) +++ leahi-dialin/ui/hd_simulator.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,2317 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_simulator.py +# +# @author (last) Michael Garthwaite +# @date (last) 22-Apr-2024 +# @author (original) Peter Lucia +# @date (original) 06-Aug-2020 +# +############################################################################ +import enum +from time import sleep +from typing import Callable +from inspect import signature +from . import messageBuilder +from ..common import * +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, LogManager + + +class HDSimulator(AbstractSubSystem): + NUM_TREATMENT_PARAMETERS = 19 + instance_count = 0 + + # UI version message field positions + START_POS_MAJOR = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MAJOR = START_POS_MAJOR + 1 + START_POS_MINOR = END_POS_MAJOR + END_POS_MINOR = START_POS_MINOR + 1 + START_POS_MICRO = END_POS_MINOR + END_POS_MICRO = START_POS_MICRO + 1 + START_POS_BUILD = END_POS_MICRO + END_POS_BUILD = START_POS_BUILD + 2 + START_POS_COMPAT = END_POS_BUILD + END_POS_COMPAT = START_POS_COMPAT + 4 + + def __init__(self, can_interface: str = "can0", + log_level: bool = None, + console_out: bool = False, + passive_mode: bool = True, + auto_response: bool = False): + """ + The HDSimulator constructor + + @param can_interface: (str) the can interface name + @param log_level: (str) or (None) if not set, contains the logging level + @param console_out: (bool) If True, write each leahi-dialin message to the console. + """ + super().__init__() + HDSimulator.instance_count = HDSimulator.instance_count + 1 + + self.auto_response = auto_response + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + self.console_out = console_out + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger, + console_out=console_out, + passive_mode=passive_mode) + self.can_interface.start() + + self.ui_initiate_treatment_req_timestamp = 0.0 + self.ui_set_uf_volume_parameter_timestamp = 0.0 + self.ui_new_treatment_parameters_timestamp = 0.0 + self.ui_user_confirm_treatment_parameters_timestamp = 0.0 + self.ui_tx_end_cmd_timestamp = 0.0 + self.ui_hd_set_rtc_req_timestamp = 0.0 + self.ui_fw_versions_req_timestamp = 0.0 + self.ui_service_info_req_timestamp = 0.0 + self.ui_version_info_response_timestamp = 0.0 + self.ui_version_info_compatibility_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_hd_ch_id + if auto_response: + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_INITIATE_TREATMENT_REQUEST.value, + self._handler_ui_initiate_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER_REQUEST.value, + self._handler_ui_pre_treatment_uf_request) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS_REQUEST.value, + self._handler_ui_validate_parameters) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS_REQUEST.value, + self._handler_ui_confirm_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_TX_END_CMD_REQUEST.value, + self._handler_ui_end_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_HD_SET_RTC_REQUEST.value, + self._handler_set_rtc_request) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_FW_VERSIONS_REQUEST.value, + self._handler_request_hd_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_UI_SERVICE_INFO_REQUEST.value, + self._handler_system_usage_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_UI_VERSION_INFO_RESPONSE.value, + self._handler_ui_post_ui_version_compatibility) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_UI_VERSION_INFO_RESPONSE.value, + self._handler_ui_version) + + self.treatment_parameter_rejections = TreatmentParameterRejections() + + # initialize variables that will be populated by UI version response + self.ui_version = None + + def add_publication(self, channel_id: DenaliChannels, message_id: MsgIds, function_ptr: Callable) -> None: + """ + Allows later addition of publication to the HDSimulator + @param channel_id: (DenaliChannels) the channel id of the message + @param message_id: (MsgIds) the message id + @param function_ptr: (Callable) the pointer to the message handler function + @return: None + """ + if self.auto_response: + if channel_id > 0 and message_id != MsgIds.MSG_ID_UNUSED and function_ptr is not None: + self.can_interface.register_receiving_publication_function(channel_id, + message_id, + function_ptr) + else: + self.logger.debug("rejected publication registration {0}, {1}, {2}".format(channel_id, + message_id, + function_ptr)) + + def set_ui_all_publication(self, function_ptr: Callable) -> None: + """ + Allows later addition of publication to the HDSimulator + This function needs improvements, it has been implemented to quickly being used by Development Testing team. + @param function_ptr: (Callable) the pointer to the message handler function + @return: None + """ + function_signature_exp = "(message:dict)->None" # TODO: update later to get param name and type. + if not callable(function_ptr): + print("ui all publication rejected (not a function)") + self.logger.debug("ui all publication rejected (not a function)") + else: + function_signature_act = str(signature(function_ptr)).replace(" ", "") + if function_signature_act == function_signature_exp: + self.can_interface.register_received_all_ui_publication_function(function_ptr) + else: + print("ui all publication rejected {0},{1}" + .format(function_signature_exp, + function_signature_act)) + self.logger.debug("rejected ui all messages publication registration, expected function signature {0}, got {1} " + .format(function_signature_exp, + function_signature_act)) + + def get_ui_version(self): + """ + Gets the ui version + + @return: The ui version + """ + return self.ui_version + + def _handler_system_usage_response(self, message, timestamp=0.0) -> None: + """ + Handles a request for system usage + + @return: None + """ + self.ui_service_info_req_timestamp = timestamp + payload = integer_to_bytearray(1619628663) + payload += integer_to_bytearray(1619887863) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERVICE_SCHEDULE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_set_rtc_request(self, message: dict, timestamp=0.0) -> None: + """ + Handles a UI request to set the HD RTC + @param message: (dict) the message containing the request + @return: None + """ + + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.ui_hd_set_rtc_req_timestamp = timestamp + + self.logger.debug("Request to set the HD epoch to {0}".format(epoch)) + + self.cmd_send_set_rtc_response(YES, 0) + + def cmd_send_set_rtc_response(self, response, reason): + """ + Sends a set RTC response message + + @param response: (int) 0=NO, 1=YES + @param reason: the rejection reason + @return: None + """ + self.logger.debug("HD: Sending response {0} reason {1}".format(response, reason)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_UI_SET_RTC_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_parameter_validation_response(self, rejections: List[RequestRejectReasons]): + """ + Sends a treatment parameter validation response + + @param rejections: A list of rejection code enums + @return: True if successful, False otherwise + """ + if len(rejections) != self.NUM_TREATMENT_PARAMETERS: + self.logger.error("Invalid number of treatment parameter enums provided.") + return False + + if not all([isinstance(each, enum.Enum) for each in rejections]): + self.logger.error("Not all rejections are enums.") + return False + + payload = integer_to_bytearray(0) + + for rejection in rejections: + payload += integer_to_bytearray(rejection.value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + payload=payload) + + # Send message + self.can_interface.send(message, 0) + + return True + + def cmd_send_treatment_parameter_manual_validation_response(self, rejections: List[int]): + """ + Sends a manually built treatment parameter validation response + + @param rejections: A list of rejection code enums + @return: True if successful, False otherwise + """ + if len(rejections) != self.NUM_TREATMENT_PARAMETERS: + self.logger.error("Invalid number of treatment parameter enums provided.") + return False + + if not all([isinstance(each, int) for each in rejections]): + self.logger.error("Not all rejections are the correct type.") + return False + + payload = bytearray() + + for rejection in rejections: + payload += integer_to_bytearray(rejection) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + payload=payload) + + # Send message + self.can_interface.send(message, 0) + + return True + + def cmd_send_poweroff_command(self): + """ + Broadcast that the poweroff command + @return: None + """ + payload = byte_to_bytearray(PowerOffCommands.PW_COMMAND_OPEN.value) + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_poweroff_timeout(self): + """ + Sends a poweroff timeout + @return: None + """ + payload = byte_to_bytearray(PowerOffCommands.PW_TIMEOUT_CLOSE.value) + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_poweroff_reject(self): + """ + Sends a poweroff reject + @return: None + """ + payload = byte_to_bytearray(PowerOffCommands.PW_REJECT_SHOW.value) + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS_REQUEST.value, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_poweroff_imminent(self): + """ + Broadcast that the system will shut down + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_POWER_OFF_WARNING.value, + payload=None) + self.can_interface.send(message, 0) + + def _handler_ui_confirm_treatment(self, message, timestamp=0.0): + """ + Handler function to detect when a treatment is confirmed + + @param message: the confirm treatment message + @return: None + """ + + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.ui_user_confirm_treatment_parameters_timestamp = timestamp + if request == 0: + self.logger.debug("Received UI cancel confirmation of Treatment Parameters. ") + return + + self.logger.debug("Received UI confirmation of Treatment Parameters. ") + + self.logger.debug("Priming ...") + state = 0 + total_seconds = 100 + for seconds_remaining in range(total_seconds, -1, -1): + if seconds_remaining % (total_seconds // 3) == 0: + state += 1 + self.cmd_send_priming_time_remaining(state, seconds_remaining, total_seconds) + sleep(0.05) + self.logger.debug("Finished priming.") + + def _handler_ui_pre_treatment_uf_request(self, message, timestamp=0.0): + """ + Handles the ui pre treatment uf request and sends a response + @param message: The ui pretreatment uf request message + @return: None + """ + + uf_volume = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("Received UF Volume: {0} mL".format(uf_volume)) + self.ui_set_uf_volume_parameter_timestamp = timestamp + self.cmd_send_uf_treatment_response(1, 0, uf_volume) + + def _handler_ui_initiate_treatment(self, message,timestamp=0.0): + """ + Handler function to start a treatment + + @param message: the start treatment message + @return: None + """ + + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if request == 0: + self.logger.debug("Selecting treatment parameters") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_PRET.value) + elif request == 1: + self.logger.debug("Canceling treatment") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_STAN.value) + elif request == 2: + self.logger.debug("Starting treatment") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_TREA.value) + + self.ui_initiate_treatment_req_timestamp = timestamp + self.cmd_initiate_treatment_response(YES, 0) + + def cmd_initiate_treatment_response(self, response: int, reason: int): + """ + Sends a start treatment response message + + @param response: 0=NO, 1=YES + @param reason: the rejection reason + @return: None + """ + self.logger.debug("Sending: {0}".format(response)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_INITIATE_TREATMENT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_operation_mode(self, op_mode: int, sub_mode: int = 0): + """ + Broadcasts the current HD operation mode + @param op_mode: hd operation mode + @param sub_mode: hd operation sub-mode + @return: None + """ + + if not isinstance(op_mode, int): + raise ValueError("Provided mode is not of type 'int'") + if not isinstance(sub_mode, int): + raise ValueError("Provided mode is not of type 'int'") + + payload = integer_to_bytearray(op_mode) + payload += integer_to_bytearray(sub_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_uf_treatment_response(self, accepted, reason, volume): + """ + Sends the uf volume adjustment response message in pre-treatment + @param accepted: (uint) the acceptance, 1 = accepted, 0 = rejected + @param reason: (uint) the reason for rejection + @param volume: (float) the acceptable/accepted ultrafiltration volume + @return: none + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_end_treatment_response(self): + """ + Sends an end treatment response + @return: None + """ + + payload = integer_to_bytearray(YES) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_ui_validate_parameters(self, message, timestamp=0.0) -> None: + """ + handler for UI parameters validation + + @return: None + """ + rejections = [ + self.treatment_parameter_rejections.param_request_valid, + self.treatment_parameter_rejections.param_blood_flow_rate, + self.treatment_parameter_rejections.param_dialysate_flow_rate, + self.treatment_parameter_rejections.param_duration, + self.treatment_parameter_rejections.param_heparin_stop_time, + self.treatment_parameter_rejections.param_saline_bolus, + self.treatment_parameter_rejections.param_acid_concentrate, + self.treatment_parameter_rejections.param_bicarbonate_concentrate, + self.treatment_parameter_rejections.param_dialyzer_type, + self.treatment_parameter_rejections.param_blood_pressure_measure_interval, + self.treatment_parameter_rejections.param_rinseback_flow_rate, + self.treatment_parameter_rejections.param_arterial_pressure_limit_low, + self.treatment_parameter_rejections.param_arterial_pressure_limit_high, + self.treatment_parameter_rejections.param_venous_pressure_limit_low, + self.treatment_parameter_rejections.param_venous_pressure_limit_high, + self.treatment_parameter_rejections.param_heparin_dispensing_rate, + self.treatment_parameter_rejections.param_heparin_bolus_volume, + self.treatment_parameter_rejections.param_dialysate_temp + ] + self.ui_new_treatment_parameters_timestamp = timestamp + self.cmd_send_treatment_parameter_validation_response(rejections) + + def test_started(self, test_name: str): + """ + Logs that a test was started + + @param test_name: The name of the test + @return: None + """ + self.logger.info("Test Started: {0}".format(test_name)) + + def test_completed(self): + """ + Logs that a test was completed + + @return: None + """ + self.logger.info("Test Completed") + + def cmd_send_acknowledge_hd(self): + """ + the acknowledge from HD + @return: none + """ + + payload = ["A5", "01", "00", "FF", "FF", "00", "19", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_send_acknowledge_ui(self): + """ + the acknowledge from UI + @return: none + """ + + payload = ["A5", "01", "00", "FF", "FF", "00", "19", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.ui_to_hd_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_dialog(self): + """ + the message from HD to UI to show the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "00", "38"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_hide_poweroff_dialog(self): + """ + the message from HD to UI to hide the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "01", "09"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_notification_dialog(self): + """ + the message from HD to UI to show the shutting down notification box + @return: none + """ + + payload = ["A5", "01", "00", "0E", "00", "00", "24", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_sync_broadcast_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_rejection_dialog(self): + """ + the message from HD to UI to show the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "02", "5A"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + @staticmethod + def wait_for_message_to_be_sent(delay=0.050): + """ + After each multi-frame message put a 50ms sleep, time.sleep(0.1) + it seems it's needed otherwise the test will check a value which has not been received yet. + :@param delay: the number of seconds to wait + @return: none + """ + time.sleep(delay) + + @staticmethod + def build_hd_debug_text(vtext): + """ + the debug text message from HD builder method + @param vtext: (str) the debug text + @return: none + """ + message_length = 40 + txt = messageBuilder.textToByte(vtext, message_length) # + 1 null term + msg = messageBuilder.buildMessage(GuiActionType.HDDebugText, 1 * (message_length + 1), False, txt) + return messageBuilder.toFrames(msg) + + @staticmethod + def cmd_set_hd_debug_text(debug_text): + """ + the debug text message from HD setter/sender method + @param debug_text: (str) the debug text + @return: none + """ + + frames = HDSimulator.build_hd_debug_text(debug_text) + frames = messageBuilder.toCandumpFormat(frames) + for frame in frames: + subprocess.call(['cansend', 'can0', '020#{}'.format(frame)]) + HDSimulator.wait_for_message_to_be_sent() + + @staticmethod + def cmd_set_dg_debug_text(debug_text): + """ + the debug text message from DG setter/sender method + @param debug_text: (str) the debug text + @return: none + """ + frames = HDSimulator.build_dg_debug_text(debug_text) + frames = messageBuilder.toCandumpFormat(frames) + for frame in frames: + subprocess.call(['cansend', 'can0', '070#{}'.format(frame)]) + HDSimulator.wait_for_message_to_be_sent() + + def cmd_set_treatment_parameter_ranges(self, min_treatment_duration: int, max_treatment_duration: int, + min_uf_volume: float, max_uf_volume: float, + min_dialysate_flow_rate: int, max_dialysate_flow_rate: int) -> None: + """ + The Treatment adjustment parameter ranges data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) | #4:(F32) | #5:(U32) | #6:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x1A00| 0x020 | 6 | 1/60 Hz| Y | HD | UI | Treatment adjustment param ranges Data | \ref Data::mDuration_Min | \ref Data::mDuration_Max | \ref Data::mUltrafiltration_Volume_Min | \ref Data::mUltrafiltration_Volume_Max | \ref Data::mDialysate_Flow_Min | \ref Data::mDialysate_Flow_Max | + + @param min_treatment_duration: (int) Min Treatment Duration + @param max_treatment_duration: (int) Max Treatment Duration + @param min_uf_volume: (float) Min UF Volume + @param max_uf_volume: (float) Max UF Volume + @param min_dialysate_flow_rate: (int) Min Dialysate Flow Rate + @param max_dialysate_flow_rate: (int) Max Dialysate Flow Rate + @return: None + """ + + payload = integer_to_bytearray(min_treatment_duration) + payload += integer_to_bytearray(max_treatment_duration) + payload += float_to_bytearray(min_uf_volume) + payload += float_to_bytearray(max_uf_volume) + payload += integer_to_bytearray(min_dialysate_flow_rate) + payload += integer_to_bytearray(max_dialysate_flow_rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_PARAM_CHANGE_RANGES_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_blood_flow_rate(self, flow_set_pt: int, measured_flow: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float, rotor_count: int, + pres_flow: int, rotor_hall: int) -> None: + """ + The Blood Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(S32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | #8:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0500| 0x040 | 7 | 1 Hz | N | HD | All | Blood Flow Data | \ref Data::mFlowSetPoint | \ref Data::mMeasuredFlow | \ref Data::mRotorSpeed | \ref Data::mMotorSpeed | \ref Data::mMotorCtlSpeed | \ref Data::mMotorCtlCurrent | \ref Data::mPWMDutyCycle | \ref Data::mRotorCount | + + @param flow_set_pt: (int) Flow Set Point + @param measured_flow: (float) Measured Flow + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @param rotor_count: (int) Rotor Count + @param pres_flow: (int) Prescribed flow rate + @param rotor_hall: (int) Rotor hall sensor + @return: None + """ + + payload = integer_to_bytearray(flow_set_pt) + payload += float_to_bytearray(measured_flow) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + payload += integer_to_bytearray(rotor_count) + payload += integer_to_bytearray(pres_flow) + payload += integer_to_bytearray(rotor_hall) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_dialysate_flow_rate(self, flow_set_pt: int, measured_flow: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float, rotor_count: int, + pres_flow: int, rotor_hall: int) -> None: + """ + The Dialysate Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(S32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0800| 0x040 | 7 | 1 Hz | N | HD | All | Dialysate Flow Data | mFlowSetPoint | mMeasuredFlow | mRotorSpeed | mMotorSpeed | mMotorCtlSpeed | mMotorCtlCurrent | mPWMDutyCycle | + + @param flow_set_pt: (signed int) Flow Set Point + @param measured_flow: (float) Measured Flow + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @param rotor_count: (int) Rotor Count + @param pres_flow: (int) Prescribed flow rate + @param rotor_hall: (int) Rotor hall sensor + @return: None + """ + + payload = integer_to_bytearray(flow_set_pt) + payload += float_to_bytearray(measured_flow) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + payload += integer_to_bytearray(rotor_count) + payload += integer_to_bytearray(pres_flow) + payload += integer_to_bytearray(rotor_hall) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DIALYSATE_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_blood_dialysate_response(self, accepted: int, reason: int, + blood_rate: int, dialysate_flow_rate: int) -> None: + """ + The Blood/dialysate rate change Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | #4:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x1800| 0x020 | 6 | Rsp | Y | HD | UI | Blood/dialysate rate change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mBloodRate | \ref Data::mDialysateRate | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param blood_rate: (int) Blood Flow Rate + @param dialysate_flow_rate: (int) Dialysate Flow Rate + @return: None + """ + + if not isinstance(accepted, int): + accepted = int(accepted) + if not isinstance(reason, int): + reason = int(reason) + if not isinstance(blood_rate, int): + blood_rate = int(blood_rate) + if not isinstance(dialysate_flow_rate, int): + dialysate_flow_rate = int(dialysate_flow_rate) + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(blood_rate) + payload += integer_to_bytearray(dialysate_flow_rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_duration_response(self, accepted: int, reason: int, + duration: int, ultrafiltration: float) -> None: + """ + The Treatment Duration change Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | #5:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x1B00| 0x020 | 6 | Rsp | Y | HD | UI | Treatment Duration change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mDuration | \ref Data::mUFVolume | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param duration: (int) Treatment Duration + @param ultrafiltration: (float) Ultrafiltration Volume + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(duration) + payload += float_to_bytearray(ultrafiltration) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_state_response(self, accepted: int, reason: int, state: int) -> None: + """ + the Treatment ultrafiltration adjustment response message setter/sender method + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param state: (int) Ultrafiltration State + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_PAUSE_RESUME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_accepted(self, state: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_state_response which sends accept true + @param state: (int) Ultrafiltration State + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_state_response(EResponse.Accepted, 0, state) + + def cmd_set_treatment_adjust_ultrafiltration_rejected(self, reason: int, state: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_state_response which sends accept false + @param reason: (int) rejection reason + @param state: (int) Ultrafiltration State + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_state_response(EResponse.Rejected, reason, state) + + def cmd_set_treatment_adjust_ultrafiltration_init_response(self, accepted: int, reason: int, volume: float) -> None: + """ + the ultrafiltration volume change response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: + |0x5000| 0x020 | 6 | Rsp | Y | HD | UI | Pre UF Volume Adjustment Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_edit_response(self, accepted: int, reason: int, volume: float, + duration: int, duration_diff: int, + rate: float, rate_diff: float, rate_old: float) -> None: + """ + the ultrafiltration volume change response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #1:(U32) | #2:(U32) | #3:(F32) | #4:(U32) | #5:(F32) | #6:(U32) | #7:(U32) | #8:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x1300| 0x020 | 6 | Rsp | Y | HD | UI | UF Vol. Change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume | \ref Data::mDuration | \ref Data::mRate | \ref Data::mDurationDiff | \ref Data::mRateDiff | \ref Data::mRateOld | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @param duration: (int) Treatment Duration + @param duration_diff: (int) Duration Difference + @param rate: (float) Ultrafiltration Rate + @param rate_diff: (float) Ultrafiltration Rate Difference + @param rate_old: (float) Ultrafiltration Rate Old + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + payload += integer_to_bytearray(duration) + payload += integer_to_bytearray(duration_diff) + payload += float_to_bytearray(rate) + payload += float_to_bytearray(rate_diff) + payload += float_to_bytearray(rate_old) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_edit_rejected(self, reason: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_edit_response which only sends a rejection reason + and sends other values all as zero + @param reason: (int) rejection reason + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_edit_response(0, reason, 0, 0, 0, 0, 0, 0) + + def cmd_set_treatment_adjust_ultrafiltration_confirm_response(self, accepted: int, reason: int, volume: float, + duration: int, rate: float) -> None: + """ + the ultrafiltration volume Change Confirmation Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) | #4:(U32) | #5:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: | + |0x2E00| 0x020 | 6 | Rsp | Y | HD | UI | UF Vol. Change Confirmation Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume | \ref Data::mDuration | \ref Data::mRate | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @param duration: (int) Treatment Duration + @param rate: (float) Ultrafiltration Rate + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + payload += integer_to_bytearray(duration) + payload += float_to_bytearray(rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_confirm_rejected(self, reason: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_confirm_response which only sends a rejection reason + and sends other values all as zero + + @param reason: (int) rejection reason + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_confirm_response(0, reason, 0, 0, 0) + + def cmd_set_treatment_time(self, sec_total: int, sec_elapsed: int, sec_remain: int = 0) -> None: + """ + the Treatment Time Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x0D00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Time Data | \ref Data::mTotal | \ref Data::mElapsed | \ref Data::mRemaining | + + @param sec_total: (int) Treatment Total Duration in Seconds + @param sec_elapsed: (int) Treatment Total Elapsed Time in Seconds + @param sec_remain: (int) Treatment Remaining Time in Seconds + @return: none + """ + if sec_remain is None: + sec_remain = sec_total - sec_elapsed + + payload = integer_to_bytearray(sec_total) + payload += integer_to_bytearray(sec_elapsed) + payload += integer_to_bytearray(sec_remain) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_ultrafiltration_outlet_flow_data(self, ref_uf_vol: float, measured_uf_vol: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float, dop_corr_offset: float, + dop_calc_rate: float, uf_calc_rate: float, rotor_hall: int, + current_set_uf_rate: float) -> None: + """ + the Outlet Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0B00| 0x040 | 7 | 1 Hz | N | HD | All | Outlet Flow Data | \ref Data::mRefUFVol | \ref Data::mMeasUFVol | \ref Data::mRotorSpeed | \ref Data::mMotorSpeed | \ref Data::mMotorCtlSpeed | \ref Data::mMotorCtlCurrent | \ref Data::mPWMDtCycle | + + @param ref_uf_vol: (float) Ref UF Volume + @param measured_uf_vol: (float) Measured UF Volume + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @param dop_corr_offset: (float) correction offset + @param dop_calc_rate: (float) calculated dop flow rate + @param uf_calc_rate: (float) calculated uf flow rate + @param rotor_hall: (int) rotor hall sensor + @param current_set_uf_rate: (float) set UF rate + @return: none + """ + + payload = float_to_bytearray(ref_uf_vol) + payload += float_to_bytearray(measured_uf_vol) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + payload += float_to_bytearray(dop_corr_offset) + payload += float_to_bytearray(dop_calc_rate) + payload += float_to_bytearray(uf_calc_rate) + payload += integer_to_bytearray(rotor_hall) + payload += float_to_bytearray(current_set_uf_rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DIALYSATE_OUT_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_pressure_occlusion_data(self, arterial_prs: float, venous_prs: float, blood_pump_occlusion: int, + pressure_state: int, art_min_limit: int, art_max_limit: int, + ven_min_limit: int, ven_max_limit: int + , filtered_arterial_prs: float, + filtered_venous_prs : float + ) -> None: + """ + the Pressure/Occlusion Data messages setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(U32) | #4:(U32) | #5:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: | + |0x0900| 0x040 | 7 | 1 Hz | N | HD | All | PressureOcclusion Data | \ref Data::mArterialPressure | \ref Data::mVenousPressure | \ref Data::mBloodPumpOcclusion | \ref Data::mDialysateInletPumpOcclusion | \ref Data::mDialysateOutletPumpOcclusion | + + @param arterial_prs: (float) Arterial Pressure + @param venous_prs: (float) Venous Pressure + @param blood_pump_occlusion: (uint) Blood Pump Occlusion + @param pressure_state: (uint) Pressure state + @param art_min_limit: (int) arterial minimum limit + @param art_max_limit: (int) arterial maximum limit + @param ven_min_limit: (int) venous minimum limit + @param ven_max_limit: (int) venous maximum limit + # @param filtered_arterial_prs: (float) filtered arterial pressure + # @param filtered_venous_prs: (float) filtered venous pressure + @return: none + """ + + payload = float_to_bytearray(arterial_prs) + payload += float_to_bytearray(venous_prs) + payload += integer_to_bytearray(blood_pump_occlusion) + payload += integer_to_bytearray(pressure_state) + payload += integer_to_bytearray(art_min_limit) + payload += integer_to_bytearray(art_max_limit) + payload += integer_to_bytearray(ven_min_limit) + payload += integer_to_bytearray(ven_max_limit) + payload += float_to_bytearray(filtered_arterial_prs) + payload += float_to_bytearray(filtered_venous_prs) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_OCCLUSION_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_states_data(self, sub_mode: int, uf_state: int, saline_state: int, heparin_state: int, + rinseback_state: int, recirculate_state: int, blood_prime_state: int, + treatment_end_state: int, treatment_stop_state: int, dialysis_state: int) -> None: + """ + the Treatment States Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x0F00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment States Data | + + | #1:(U32) | #2:(U32) | #3:(U32) || + |:--: |:--: |:--: || + | \ref Data::mSubMode | \ref Data::mUFState | \ref Data::mSalineState || + |||| + | #4:(U32) | #5:(U32) | #6:(U32) || + |:--: |:--: |:--: || + | \ref Data::mHeparinState | \ref Data::mRinsebackState | \ref Data::mRecirculateState || + |||| + | #7:(U32) | #8:(U32) | #9:(U32) | + |:--: |:--: |:--: | + | \ref Data::vBloodPrimeState | \ref Data::mTreatmentEndState | \ref Data::mTreammentStopState | + | #9:(U32) || + |:--: || + | \ref Data::mDialysisState || + + @param sub_mode: (int) Sub-Mode + @param uf_state: (int) UF State + @param saline_state: (int) Saline Bolus State + @param heparin_state: (int) Saline Bolus State + @param rinseback_state: (int) Rinseback State + @param recirculate_state: (int) Recirculate State + @param blood_prime_state: (int) Blood Prime State + @param treatment_end_state: (int) Treatment End State + @param treatment_stop_state: (int) Treatment Stop State + @param dialysis_state: (int) Dialysis State + @return: none + """ + + payload = integer_to_bytearray(sub_mode) + payload += integer_to_bytearray(uf_state) + payload += integer_to_bytearray(saline_state) + payload += integer_to_bytearray(heparin_state) + payload += integer_to_bytearray(rinseback_state) + payload += integer_to_bytearray(recirculate_state) + payload += integer_to_bytearray(blood_prime_state) + payload += integer_to_bytearray(treatment_end_state) + payload += integer_to_bytearray(treatment_stop_state) + payload += integer_to_bytearray(dialysis_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_STATE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_start_state(self) -> None: + """ + starting the treatment for user convenience since Tx is not by default running + @return: none + """ + self.cmd_set_treatment_states_data(TXStates.TREATMENT_DIALYSIS_STATE, + TXStates.UF_OFF_STATE, + TXStates.SALINE_BOLUS_STATE_IDLE, + TXStates.HEPARIN_STATE_OFF, + TXStates.RINSEBACK_STOP_INIT_STATE, + TXStates.TREATMENT_RECIRC_RECIRC_STATE, + TXStates.BLOOD_PRIME_RAMP_STATE, + TXStates.TREATMENT_END_WAIT_FOR_RINSEBACK_STATE, + TXStates.TREATMENT_STOP_RECIRC_STATE) + + def cmd_set_hd_operation_mode_data(self, operation_mode: int, operation_sub_mode: int) -> None: + """ + The HD Operation Mode Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2500| 0x040 | 7 | 1 Hz | N | HD | All | HD Operation Mode Data | \ref Data::mOpMode | + @param operation_mode: (int) Operation Mode + @param operation_sub_mode: (int) Operation Sub-Mode + @return: None + """ + + payload = integer_to_bytearray(operation_mode) + payload += integer_to_bytearray(operation_sub_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_saline_bolus_data(self, target: int, cumulative: float, delivered: float) -> None: + """ + the Treatment Saline Bolus Data message sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2F00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Saline Bolus Data | \ref Data::mTarget | \ref Data::mCumulative | \ref Data::mDelivered | + + @param target: (int) Saline Bolus Target Volume + @param cumulative: (float) Saline Bolus Cumulative Volume + @param delivered: (float) Saline Bolus Delivered Volume + @return: none + """ + + payload = integer_to_bytearray(target) + payload += float_to_bytearray(cumulative) + payload += float_to_bytearray(delivered) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_SALINE_BOLUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_saline_bolus_response(self, accepted: int, reason: int, target: int) -> None: + """ + the Saline Bolus Response message sender method + | MSG | CAN ID | M.Box | Type | Ack | Src | Dest | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:---:|:------:|:-----:|:----:|:---:|:---:|:----:|:---------------------:|:--------------------:|:-------------------:|:-------------------:| + | 20 | 0x020 | 6 | Rsp | Y | HD | UI | Saline Bolus Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mTarget | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param target: (int) Saline Bolus Target Volume + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(target) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_SALINE_BOLUS_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_canbus_fault_count(self, count: int) -> None: + """ + the CANBus fault count message setter/sender method + @param count: (int) Fault Count + @return: none + """ + + payload = integer_to_bytearray(count) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_CAN_ERROR_COUNT.value, + payload=payload) + + self.can_interface.send(message, 0) + HDSimulator.wait_for_message_to_be_sent() + + def cmd_send_unknown_hd(self) -> None: + """ + the unknown message from HD setter/sender method + @return: none + """ + + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_UNUSED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_heparin_data(self, cumulative: float, required: float) -> None: + """ + the Treatment Heparin Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #1:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x4D00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Heparin Data | \ref Data::mCumulative | \ref Data::mRequired | + + @param cumulative: (float) Heparin Cumulative Volume + @param required: (float) Heparin Volume required for treatment + @return: none + """ + payload = float_to_bytearray(cumulative) + payload += float_to_bytearray(required) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPARIN_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_heparin_pause_resume_response(self, accepted: int, reason: int) -> None: + """ + the Heparin Response message setter/sender method + | MSG | CAN ID | M.Box | Type | Ack | Src | Dest | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:-----:|:----:|:---:|:---:|:----:|:----------------:|:--------------------:|:-------------------:| + |0x4C00| 0x020 | 6 | Rsp | Y | HD | UI | Heparin Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @return: none + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPARIN_PAUSE_RESUME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_pressures_limit_response(self, accepted: int, reason: int, + art_pressure_limit_window: int, ven_pressure_limit_window: int, + ven_pressure_asym_limit_window: int) -> None: + """ + the Blood/dialysate rate change Response message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(S32) | #4:(S32) | #3:(S32) | #4:(S32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x4700| 0x020 | 6 | Rsp | Y | HD | UI | A/V BP Limit Change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mArterialLow | \ref Data::mArterialHigh | \ref Data::mVenousLow | \ref Data::mVenousHigh | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param art_pressure_limit_window: (int) Arterial Pressure Limit window (mmHg) + @param ven_pressure_limit_window: (int) Venous Pressure Limit window (mmHg) + @param ven_pressure_asym_limit_window: (int) Venous Pressure asymmetric limit window (mmHg) + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(art_pressure_limit_window) + payload += integer_to_bytearray(ven_pressure_limit_window) + payload += integer_to_bytearray(ven_pressure_asym_limit_window) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_PRESSURE_LIMITS_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_rinseback_response(self, accepted: int, reason: int) -> None: + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5300| 0x020 | 6 | Rsp | Y | HD | UI | Rinseback State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason : (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_rinseback_data(self, target_vol: float, current_vol: float, flow_rate: int, timeout: int, + timeout_countdown: int, is_completed: bool) -> None: + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(U32) | #4:(U32) | #5:(U32) | #6:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x5600| 0x020 | 6 | 1Hz | N | HD | UI | Rinseback progress data | \ref Data::mTarget | \ref Data::mCurrent | \ref Data::mRate | \ref Data::mTimeoutTotal | \ref Data::mTimeoutCountDown | \ref Data::is_completed | + + @param target_vol : (float) the target volume in mL + @param current_vol : (float) the current volume in mL + @param flow_rate : (uint ) the current flow rate in mL/min + @param timeout : (uint ) Total Timeout + @param timeout_countdown : (uint ) Current Timeout count down + @param is_completed : (bool ) Is Rinseback completed. + @return: None + """ + + payload = float_to_bytearray(target_vol) + payload += float_to_bytearray(current_vol) + payload += integer_to_bytearray(flow_rate) + payload += integer_to_bytearray(timeout) + payload += integer_to_bytearray(timeout_countdown) + payload += integer_to_bytearray(is_completed) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_recirculate_data(self, timeout_total: int, timeout_count_down: int) -> None: + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5A00| 0x020 | 6 | 1Hz | N | HD | UI | Rinseback progress data | \ref Data::mTimeoutTotal | \ref Data::mTimeoutCountDown | + + @param timeout_total: (int) Total Timeout + @param timeout_count_down: (int) Current Timeout count down + @return: None + """ + + payload = integer_to_bytearray(timeout_total) + payload += integer_to_bytearray(timeout_count_down) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RECIRC_PROGRESS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_blood_prime_data(self, target: float, current: float): + """ + the bloodprime state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x5900| 0x020 | 6 | 1Hz | N | HD | UI | bloodprime progress data | \ref Data::mTarget | \ref Data::mCurrent | \ref Data::mRate | + + @param target : (float) the target volume in mL + @param current : (float) the current volume in mL + @return: None + """ + + payload = float_to_bytearray(target) + payload += float_to_bytearray(current) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_PROGRESS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_recirculate_response(self, accepted: int, reason: int) -> None: + """ + the recirculate state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5500| 0x020 | 6 | Rsp | Y | HD | UI | Recirculate State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason : (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RECIRC_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_ui_end_treatment(self, message: dict, timestamp=0.0) -> None: + """ + Handler function when received a request to end a treatment + + @param message: (dict) the end treatment request + @return: None + """ + self.logger.debug("End treatment requested") + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.ui_tx_end_cmd_timestamp = timestamp + if request == 0: + self.logger.debug("Request to start rinseback") + self.cmd_send_treatment_adjust_end_response(accepted=YES, reason=0) + else: + self.logger.debug("End treatment unknown request") + + def cmd_send_treatment_adjust_end_response(self, accepted: int, reason: int): + """ + the treatment end state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5800| 0x020 | 6 | Rsp | Y | HD | UI | Treatment End State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_accelerometer_hd_data(self, x: float, y: float, z: float, + x_max: float, y_max: float, z_max: float, + x_tilt: float, y_tilt: float, z_tilt: float) -> None: + """ + the accelerometer hd data message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x3300| 0x040 | 7 | 1Hz | N | HD | UI | HD Accelerometer data | + + | #1:(F32) | #2:(F32) | #3:(U32) | + |:--: |:--: |:--: | + | \ref Data::mX | \ref Data::mY | \ref Data::mX | + + | #4:(F32) | #5:(F32) | #6:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXMax | \ref Data::mYMax | \ref Data::mXMax | + + | #7:(F32) | #8:(F32) | #9:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXTilt | \ref Data::mYTilt | \ref Data::mXTilt | + + @param x: float - x axis + @param y: float - y axis + @param z: float - z axis + @param x_max: float - x axis max + @param y_max: float - y axis max + @param z_max: float - z axis max + @param x_tilt: float - x axis tilt + @param y_tilt: float - y axis tilt + @param z_tilt: float - z axis tilt + @return: None + """ + + payload = float_to_bytearray(x) + payload += float_to_bytearray(y) + payload += float_to_bytearray(z) + payload += float_to_bytearray(x_max) + payload += float_to_bytearray(y_max) + payload += float_to_bytearray(z_max) + payload += float_to_bytearray(x_tilt) + payload += float_to_bytearray(y_tilt) + payload += float_to_bytearray(z_tilt) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCELEROMETER_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_hd_version(self,message,timestamp=0.0) -> None: + """ + Handles a request for the HD version + + @return: None + """ + self.ui_fw_versions_req_timestamp = timestamp + self.logger.debug("Handling request for hd version.") + self.cmd_send_version_hd_data(9, 9, 9, 9, 9, 9, 9, 9) + self.cmd_send_hd_serial_number() + + def cmd_send_hd_serial_number(self) -> None: + """ + Sends the hd serial number response + + @return: None + """ + self.logger.debug("Sending hd serial number...") + + payload = bytearray('0123456789\0', encoding="utf-8") + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERIAL_NUMBER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_version_hd_data(self, major: int, minor: int, micro: int, build: int, + fpga_id: int, fpga_major: int, fpga_minor: int, fpga_lab: int, + compatibility_rev: int) -> None: + """ + [0x1D00] # 29 0x040 Rsp Y HD All + HD f/w version + U08=Major + U08=Minor + U08=Micro + U16=Build + U08=FPGA ID + U08=FPGA Major + U08=FPGA Minor + U08=FPGA Lab + U32=compatibility rev + --- + the dg version response message method + @param major: (uint) - Major version number + @param minor: (uint) - Minor version number + @param micro: (uint) - Micro version number + @param build: (uint) - Build version number + @param fpga_id: (int) - FPGA id version number + @param fpga_major: (int) - FPGA Major version number + @param fpga_minor: (int) - FPGA Minor version number + @param fpga_lab: (int) - FPGA Lab version number + @param compatibility_rev: (uint) - The FWs/UI compatibility revision + @return: None + """ + + payload = unsigned_byte_to_bytearray(major) + payload += unsigned_byte_to_bytearray(minor) + payload += unsigned_byte_to_bytearray(micro) + payload += unsigned_short_to_bytearray(build) + payload += unsigned_byte_to_bytearray(fpga_id) + payload += unsigned_byte_to_bytearray(fpga_major) + payload += unsigned_byte_to_bytearray(fpga_minor) + payload += unsigned_byte_to_bytearray(fpga_lab) + payload += unsigned_integer_to_bytearray(compatibility_rev) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_VERSION_REPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_serial_hd_data(self, serial: str): + """ + the hd version serial response message method + @param serial: serial number + @return: None + """ + + payload = bytes(serial, 'ascii') + b'\x00' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERIAL_NUMBER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_state_data(self, + sub_mode: int, + water_sample_state: int, + consumables_self_test_state: int, + no_cartridge_self_test_state: int, + installation_state: int, + dry_self_test_state: int, + prime_state: int, + recirculate_state: int, + patient_connection_state: int, + wet_selftests_state: int, + pretreatment_rsrvr_state: int + ) -> None: + """ + sends the broadcast message of the pre-treatment states + @param sub_mode : (int) the main pre treatment state + @param water_sample_state : (int) water sample state + @param consumables_self_test_state : (int) consumables self test state + @param no_cartridge_self_test_state : (int) no cartridge self-test state + @param installation_state : (int) installation state + @param dry_self_test_state : (int) dry self-test state + @param prime_state : (int) prime state + @param recirculate_state : (int) recirculate state + @param patient_connection_state : (int) patient connection state + @param wet_selftests_state : (int) wet selftest state + @param pretreatment_rsrvr_state : (int) reservoir state + @return: + """ + payload = integer_to_bytearray(sub_mode) + payload += integer_to_bytearray(water_sample_state) + payload += integer_to_bytearray(consumables_self_test_state) + payload += integer_to_bytearray(no_cartridge_self_test_state) + payload += integer_to_bytearray(installation_state) + payload += integer_to_bytearray(dry_self_test_state) + payload += integer_to_bytearray(prime_state) + payload += integer_to_bytearray(recirculate_state) + payload += integer_to_bytearray(patient_connection_state) + payload += integer_to_bytearray(wet_selftests_state) + payload += integer_to_bytearray(pretreatment_rsrvr_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_PRE_TREATMENT_STATE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_water_sample_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment water sample response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SAMPLE_WATER_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_self_test_no_cartridge_progress_data(self, total: int, countdown: int) -> None: + """ + send the pretreatment no cartridge self-tests progress data + @param total: (int) Total time in second + @param countdown: (int) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_self_test_dry_progress_data(self, total: int, countdown: int) -> None: + """ + send the pretreatment dry self-tests progress data + @param total: (int) Total time in second + @param countdown: (int) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DRY_SELF_TEST_PROGRESS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_disposables_prime_progress_data(self, timeout: int, countdown: int) -> None: + """ + Broadcasts the progress time data of pre-treatment disposables priming to the UI + @param timeout : (int) the total progress time timeout in seconds + @param countdown: (int) the remaining time in seconds + @return: None + """ + payload = bytearray() + payload += integer_to_bytearray(timeout) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_PRIMING_STATUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_prime_start_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment prime start response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_START_PRIME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_continue_to_treament_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment continue to treatment response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_START_TREATMENT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_patient_connection_confirm_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment patient connection confirm response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_PATIENT_CONNECTION_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_post_treatment_disposable_removal_confirm_response(self, accepted: int, reason: int) -> None: + """ + send post treatment disposable removal confirm response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DISPOSABLE_REMOVAL_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_post_treatment_log_response(self, accepted: bool, reason: int, + bood_flow_rate: int, + dialysate_flow_rate: int, + treatment_duration: int, + actual_treatment_duration: int, + acid_concentrate_type: int, + bicarbonate_concentrate_type: int, + potassium_concentration: int, + calcium_concentration: int, + bicarbonate_concentration: int, + sodium_concentration: int, + dialysate_temperature: float, + dialyzer_type: int, + treatment_start_date_time: int, + treatment_end_date_time: int, + average_blood_flow: float, + average_dialysate_flow: float, + dialysate_volume_used: float, + average_dialysate_temp: float, + origin_uf_volume: float, + target_uf_volume: float, + actual_uf_volume: float, + origin_uf_rate: float, + target_uf_rate: float, + actual_uf_rate: float, + saline_bolus_volume: int, + heparin_bolus_volume: float, + heparin_dispense_rate: float, + heparin_pre_stop: int, + heparin_delivered_volume: float, + heparin_type: int, + average_arterial_pressure: float, + average_venous_pressure: float, + device_id: int, + water_sample_test_result: int + ) -> None: + """ + send post treatment log response + @param accepted: true if accpeted + @param reason: the rejection reason + @param bood_flow_rate: bood flow rate + @param dialysate_flow_rate: dialysate flow rate + @param treatment_duration: treatment duration + @param actual_treatment_duration: actual treatment duration + @param acid_concentrate_type: acid concentrate type + @param bicarbonate_concentrate_type: bicarbonate concentrate type + @param potassium_concentration: potassium concentration + @param calcium_concentration: calcium concentration + @param bicarbonate_concentration: bicarbonate concentration + @param sodium_concentration: sodium concentration + @param dialysate_temperature: dialysate temperature + @param dialyzer_type: dialyzer type + @param treatment_start_date_time: treatment start date time + @param treatment_end_date_time: treatment end date time + @param average_blood_flow: average blood flow + @param average_dialysate_flow: average dialysate flow + @param dialysate_volume_used: dialysate volume used + @param average_dialysate_temp: average dialysate temp + @param origin_uf_volume: origin uf volume + @param target_uf_volume: target uf volume + @param actual_uf_volume: actual uf volume + @param origin_uf_rate: origin uf rate + @param target_uf_rate: target uf rate + @param actual_uf_rate: actual uf rate + @param saline_bolus_volume: saline bolus volume + @param heparin_bolus_volume: heparin bolus volume + @param heparin_dispense_rate: heparin dispense rate + @param heparin_pre_stop: heparin pre stop + @param heparin_delivered_volume: heparin delivered volume + @param heparin_type: heparin type + @param average_arterial_pressure: average arterial pressure + @param average_venous_pressure: average venous pressure + @param device_id: device id + @param water_sample_test_result: water sample test result + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += unsigned_integer_to_bytearray(int(bood_flow_rate)) + payload += unsigned_integer_to_bytearray(int(dialysate_flow_rate)) + payload += unsigned_integer_to_bytearray(int(treatment_duration)) + payload += unsigned_integer_to_bytearray(int(actual_treatment_duration)) + payload += unsigned_integer_to_bytearray(int(acid_concentrate_type)) + payload += unsigned_integer_to_bytearray(int(bicarbonate_concentrate_type)) + payload += unsigned_integer_to_bytearray(int(potassium_concentration)) + payload += unsigned_integer_to_bytearray(int(calcium_concentration)) + payload += unsigned_integer_to_bytearray(int(bicarbonate_concentration)) + payload += unsigned_integer_to_bytearray(int(sodium_concentration)) + payload += float_to_bytearray(float(dialysate_temperature)) + payload += unsigned_integer_to_bytearray(int(dialyzer_type)) + payload += unsigned_integer_to_bytearray(int(treatment_start_date_time)) + payload += unsigned_integer_to_bytearray(int(treatment_end_date_time)) + payload += float_to_bytearray(float(average_blood_flow)) + payload += float_to_bytearray(float(average_dialysate_flow)) + payload += float_to_bytearray(float(dialysate_volume_used)) + payload += float_to_bytearray(float(average_dialysate_temp)) + payload += float_to_bytearray(float(origin_uf_volume)) + payload += float_to_bytearray(float(target_uf_volume)) + payload += float_to_bytearray(float(actual_uf_volume)) + payload += float_to_bytearray(float(origin_uf_rate)) + payload += float_to_bytearray(float(target_uf_rate)) + payload += float_to_bytearray(float(actual_uf_rate)) + payload += unsigned_integer_to_bytearray(int(saline_bolus_volume)) + payload += float_to_bytearray(float(heparin_bolus_volume)) + payload += float_to_bytearray(float(heparin_dispense_rate)) + payload += unsigned_integer_to_bytearray(int(heparin_pre_stop)) + payload += float_to_bytearray(float(heparin_delivered_volume)) + payload += unsigned_integer_to_bytearray(int(heparin_type)) + payload += float_to_bytearray(float(average_arterial_pressure)) + payload += float_to_bytearray(float(average_venous_pressure)) + payload += unsigned_integer_to_bytearray(int(device_id)) + payload += unsigned_integer_to_bytearray(int(water_sample_test_result)) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_DATA_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_data(self, blood_flow_rate: float, dialysate_flow_rate: float, uf_rate: float, + arterial_pressure: float, venous_pressure: float) -> None: + """ + send the treatment log data + @param blood_flow_rate: (float) blood flow rate + @param dialysate_flow_rate: (float) Dialysate Flow Rate + @param uf_rate: (float) UF Rate + @param arterial_pressure: (float) Arterial Pressure + @param venous_pressure: (float) Venous Pressure + @return: None + """ + payload = float_to_bytearray(blood_flow_rate) + payload += float_to_bytearray(dialysate_flow_rate) + payload += float_to_bytearray(uf_rate) + payload += float_to_bytearray(arterial_pressure) + payload += float_to_bytearray(venous_pressure) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_PERIODIC_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_alarm(self, alarm_id: int, parameter1: float, parameter2: float) -> None: + """ + send the treatment log data + @param alarm_id: (U32) alarm ID + @param parameter1: (F32) paramter 1 (it's not clear yet how many paramters with what type is required and this is only plceholder) + @param parameter2: (F32) paramter 2 (it's not clear yet how many paramters with what type is required and this is only plceholder) + @return: None + """ + payload = integer_to_bytearray(alarm_id) + payload += float_to_bytearray(parameter1) + payload += float_to_bytearray(parameter2) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_ALARM_EVENT.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_event(self, event_id: int, old_value: float, new_value: float) -> None: + """ + send the treatment log data + @param event_id: (U32) alarm ID + @param old_value: (F32) the old value + @param new_value: (F32) the new value + @return: none + """ + payload = integer_to_bytearray(event_id) + payload += float_to_bytearray(old_value) + payload += float_to_bytearray(new_value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_EVENT.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_disinfect_response(self, accepted: bool, reason: int) -> None: + """ + the HD response to the request from UI to initiate a disinfection/flush + @param accepted: boolean accepted or rejected + @param reason: the rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DISINFECT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + @publish(["ui_version"]) + def _handler_ui_version(self, message, timestamp=0.0) -> None: + """ + Handles the ui version response + @param message: The ui version response message + @return: None + """ + payload = message['message'] + index = DenaliMessage.PAYLOAD_START_INDEX + major, index = bytearray_to_byte(payload, index, False) + minor, index = bytearray_to_byte(payload, index, False) + micro, index = bytearray_to_byte(payload, index, False) + build, index = bytearray_to_short(payload, index, False) + compt, index = bytearray_to_integer(payload, index, False) + self.ui_version_info_response_timestamp = timestamp + self.ui_version = f"v{major}.{minor}.{micro}.{build}.{compt}" + self.logger.debug(f"UI VERSION: {self.ui_version}") + + if self.console_out: + print("Version:", self.ui_version) + + def cmd_send_hd_request_ui_version(self) -> None: + """ + send HD request for UI version + + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_UI_VERSION_INFO_REQUEST.value, + payload=None) + self.can_interface.send(message, 0) + + def cmd_send_hd_post(self, item: int, passed: bool, done: bool = False) -> None: + """ + send HD post message the single(item) or the final(done) + @param item: the post state/item index + @param passed: the post result single or final + @param done: if this is the final post message this should be true + @return: None + """ + payload = integer_to_bytearray(passed) + if not done: + payload += integer_to_bytearray(item) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_POST_FINAL_TEST_RESULT.value if done + else MsgIds.MSG_ID_HD_POST_SINGLE_TEST_RESULT.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_confirm_post_tx_next(self, accepted: int, reason: int): + """ + the HD response to the request from UI to continue after a post treatment review + @param accepted: boolean accepted or rejected + @param reason: the rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_POST_TX_NEXT_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_heparin_response(self, syringePumpVolumeDelivered: float, syringePumpVolumeRequired: float): + """ + the HD response to the request from UI to continue after a post treatment review + @param accepted: boolean accepted or rejected + @param reason: the rejection reason + @return: None + """ + payload = float_to_bytearray(syringePumpVolumeDelivered) + payload += float_to_bytearray(syringePumpVolumeRequired) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPARIN_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + # ------------------------------------------------ GENERAL MESSAGES ------------------------------------------------ + + def cmd_send_hd_general_response(self, message_id: int, accepted: int, reason: int, + is_pure_data: bool = False, + has_parameters: bool = False, + parameters_payload: any = 0x00, + channel_id=DenaliChannels.hd_to_ui_ch_id) -> None: + """ + a general method to send any standard response message, by it's id and list of parameters. + @param message_id: the id of the message + @param accepted: the standard accepted parameter of any response message + @param reason: the standard rejection reason parameter of any response message + @param is_pure_data: The message only has data + @param has_parameters: if the message has parameter this needs to be true. + @param parameters_payload: the list of parameters pre-converted and ready to be concatenated to the payload. + @param channel_id: (int) indicates the channel + @return: None + """ + + payload = "" + if not is_pure_data: + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + if has_parameters: + payload = parameters_payload + + message = DenaliMessage.build_message(channel_id=channel_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_general_progress_data(self, message_id: int, total: int, countdown: int) -> None: + """ + a general method t send any standard progress data message, by it's id + @param message_id: the id of the message + @param total: the total value of the progress data + @param countdown: the remaining or countdown value + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=message_id, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_hd_ack(self, seq: int) -> None: + """ + sending hd ack message by the sequence seq + @param seq: the message sequence number + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=GuiActionType.Acknow, + seq=seq) + + self.can_interface.send(message, 0) + + def cmd_send_power_on_self_test_version_request(self) -> None: + """ + Sends the power on self test version request + + @return: None + """ + + payload = bytearray() + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIdsDialin.MSG_DIALIN_ID_HD_VERSION_REQUEST.value, + payload=payload) + self.can_interface.send(message, 0) + + def _handler_ui_post_ui_version_compatibility(self, message: dict, timestamp=0.0) -> None: + """ + Handles the UI's reporting of its version during the power on self tests + + @param message: The message data + @return: None + """ + + ui_major = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + ui_minor = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + ui_micro = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + ui_build = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + ui_compatibility = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.ui_version_info_compatibility_timestamp = timestamp + self.logger.debug("UI version power on self test received: " + "Major: {0} " + "Minor: {1} " + "Micro: {2} " + "Build: {3} " + "Compat: {4} " + .format(ui_major, ui_minor, ui_micro, ui_build, ui_compatibility)) + + def cmd_send_hd_blood_leak_data(self, blood_leak_status: int, blood_leak_state: int, + blood_leak_zero_status_counter: int, blood_leak_serial_state: int) -> None: + """ + A simulated HD broadcast message of blood leak data. + @param blood_leak_status: (int) Blood leak status + @param blood_leak_state: (int) Blood leak state + @param blood_leak_zero_status_counter: (int) Blood leak zero status counter + @param blood_leak_serial_state: (int) Blood leak serial comm state + + @return: None + """ + + payload = unsigned_integer_to_bytearray(blood_leak_status) + payload += unsigned_integer_to_bytearray(blood_leak_state) + payload += unsigned_integer_to_bytearray(blood_leak_zero_status_counter) + payload += unsigned_integer_to_bytearray(blood_leak_serial_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_air_trap_data(self, lower_level: int, upper_level: int) -> None: + """ + A simulated HD broadcast message of air trap data. + @param lower_level: (int) Air trap lower level + @param upper_level: (int) Air trap upper level + @return: None + """ + + payload = unsigned_integer_to_bytearray(lower_level) + payload += unsigned_integer_to_bytearray(upper_level) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_TRAP_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_air_bubble_data(self, venous_air_bubble_status: int, venous_air_bubble_state: int) -> None: + """ + A simulated HD broadcast message of air bubble data. + @param venous_air_bubble_status: (int) Venous air bubble status + @param venous_air_bubble_state: (int) Venous air bubble state + @return: None + """ + + payload = unsigned_integer_to_bytearray(venous_air_bubble_status) + payload += unsigned_integer_to_bytearray(venous_air_bubble_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLES_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_service_mode_response(self, accepted: int, reason: int) -> None: + """ + A simulated HD response message of service mode request + @param accepted: (int) acceptance. 1 if accepted + @param reason: (int) rejection reason + @return: None + """ + payload = unsigned_integer_to_bytearray(accepted) + payload += unsigned_integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RESPONSE_SERVICE_MODE_REQUEST.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_institutional_record_response(self, minBloodFlowMLPM: int, maxBloodFlowMLPM: int, minDialysateFlowMLPM: int, maxDialysateFlowMLPM: int, + minTxDurationMIN: int, maxTxDurationMIN: int, minStopHeparinDispBeforeTxEndMIN: int, maxStopHeparinDispBeforeTxEndMIN: int, + minSalineBolusVolumeML: int, maxSalineBolusVolumeML: int, minDialysateTempC: float, maxDialysateTempC: float, + minArtPressLimitWindowMMHG: int, maxArtPressLimitWindowMMHG: int, minVenPressLimitWindowMMHG: int, maxVenPressLimitWindowMMHG: int, + minVenAsymPressLimitMMHG: int, maxVenAsymPressLimitMMHG: int, minUFVolumeL: float, maxUFVolumeL: float, + minHeparinDispRateMLPHR: float, maxHeparinDispRateMLPHR: float, minHeparinBolusVolumeML: float, maxHeparinBolusVolumeML: float, + enableChemicalDisinfect: int) -> None: + + """ + A simulated HD response message of hd institutational record response. + Default values: + _DEFAULT_MIN_BLOOD_FLOW_MLPM = 100 + _DEFAULT_MAX_BLOOD_FLOW_MLPM = 500 + _DEFAULT_MIN_DIALYSATE_FLOW_MLPM = 100 + _DEFAULT_MAX_DIALYSATE_FLOW_MLPM = 600 + _DEFAULT_MIN_TX_DURATION_MIN = 60 + _DEFAULT_MAX_TX_DURATION_MIN = 480 + _DEFAULT_MIN_STOP_HEP_DISP_BEFORE_TX_END_MIN = 0 + _DEFAULT_MAX_STOP_HEP_DISP_BEFORE_TX_END_MIN = 480 + _DEFAULT_MIN_SALINE_BOLUS_VOLUME_ML = 100 + _DEFAULT_MAX_SALINE_BOLUS_VOLUME_ML = 300 + _DEFAULT_MIN_DIALYSATE_TEMPERATURE_C = 35.0 + _DEFAULT_MAX_DIALYSATE_TEMPERATURE_C = 37.0 + _DEFAULT_MIN_ART_PRESS_LIMIT_WINDOW_MMHG = 120 + _DEFAULT_MAX_ART_PRESS_LIMIT_WINDOW_MMHG = 200 + _DEFAULT_MIN_VEN_PRESS_LIMIT_WINDOW_MMHG = 100 + _DEFAULT_MAX_VEN_PRESS_LIMIT_WINDOW_MMHG = 200 + _DEFAULT_MIN_VEN_ASYM_PRESS_LIMIT_WINDOW_MMHG = 20 + _DEFAULT_MAX_VEN_ASYM_PRESS_LIMIT_WINDOW_MMHG = 35 + _DEFAULT_MIN_UF_VOLUME_L = 0.0 + _DEFAULT_MAX_UF_VOLUME_L = 8.0 + _DEFAULT_MIN_HEPARIN_DISP_RATE_MLPHR = 0.0 + _DEFAULT_MAX_HEPARIN_DISP_RATE_MLPHR = 1.0 + _DEFAULT_MIN_HEPARIN_BOLUS_VOLUME_ML = 0.0 + _DEFAULT_MAX_HEPARIN_BOLUS_VOLUME_ML = 2.0 + _DEFAULT_ENABLE_CHEM_DISINFECT = 1 + + @param minBloodFlowMLPM: (int) Min blood flow in mL/min. + @param maxBloodFlowMLPM: (int) Max blood flow in mL/min. + @param minDialysateFlowMLPM: (int) Min dialysate flow in mL/min. + @param maxDialysateFlowMLPM: (int) Max dialysate flow in mL/min. + @param minTxDurationMIN: (int) Min treatment duration in minutes. + @param maxTxDurationMIN: (int) Max treatment duration in minutes. + @param minStopHeparinDispBeforeTxEndMIN: (int) Min stop heparin dispense before treatment end in minutes. + @param maxStopHeparinDispBeforeTxEndMIN: (int) Max stop heparin dispense before treatment end in minutes. + @param minSalineBolusVolumeML: (int) Min saline bolus volume in milliliters. + @param maxSalineBolusVolumeML: (int) Max saline bolus volume in milliliters. + @param minDialysateTempC: (float) Min dialysate temperature in C. + @param maxDialysateTempC: (float) Max dialysate temperature in C. + @param minArtPressLimitWindowMMHG: (int) Min arterial pressure limit window in mmHg. + @param maxArtPressLimitWindowMMHG: (int) Max arterial pressure limit window in mmHg. + @param minVenPressLimitWindowMMHG: (int) Min venous pressure limit window in mmHg. + @param maxVenPressLimitWindowMMHG: (int) Max venous pressure limit window in mmHg. + @param minVenAsymPressLimitMMHG: (int) Min venous asymmetric pressure limit in mmHg. + @param maxVenAsymPressLimitMMHG: (int) Max venous asymmetric pressure limit in mmHg. + @param minUFVolumeL: (float) Min ultrafiltration volume in mL. + @param maxUFVolumeL: (float) Max ultrafiltration volume in mL. + @param minHeparinDispRateMLPHR : (float) Min heparin dispense rate in mL/hr. + @param maxHeparinDispRateMLPHR: (float) Max heparin dispense rate in mL/hr. + @param minHeparinBolusVolumeML: (float) Min heparin bolus volume in mL. + @param maxHeparinBolusVolumeML: (float) Max heparin bolus volume in mL. + @param enableChemicalDisinfect: (int) Enable/disable chemical disinfect. + @return: None + """ + + payload = unsigned_integer_to_bytearray(minBloodFlowMLPM) + payload += unsigned_integer_to_bytearray(maxBloodFlowMLPM) + payload += unsigned_integer_to_bytearray(minDialysateFlowMLPM) + payload += unsigned_integer_to_bytearray(maxDialysateFlowMLPM) + payload += unsigned_integer_to_bytearray(minTxDurationMIN) + payload += unsigned_integer_to_bytearray(maxTxDurationMIN) + payload += unsigned_integer_to_bytearray(minStopHeparinDispBeforeTxEndMIN) + payload += unsigned_integer_to_bytearray(maxStopHeparinDispBeforeTxEndMIN) + payload += unsigned_integer_to_bytearray(minSalineBolusVolumeML) + payload += unsigned_integer_to_bytearray(maxSalineBolusVolumeML) + payload += float_to_bytearray(minDialysateTempC) + payload += float_to_bytearray(maxDialysateTempC) + payload += unsigned_integer_to_bytearray(minArtPressLimitWindowMMHG) + payload += unsigned_integer_to_bytearray(maxArtPressLimitWindowMMHG) + payload += unsigned_integer_to_bytearray(minVenPressLimitWindowMMHG) + payload += unsigned_integer_to_bytearray(maxVenPressLimitWindowMMHG) + payload += unsigned_integer_to_bytearray(minVenAsymPressLimitMMHG) + payload += unsigned_integer_to_bytearray(maxVenAsymPressLimitMMHG) + payload += float_to_bytearray(minUFVolumeL) + payload += float_to_bytearray(maxUFVolumeL) + payload += float_to_bytearray(minHeparinDispRateMLPHR) + payload += float_to_bytearray(maxHeparinDispRateMLPHR) + payload += float_to_bytearray(minHeparinBolusVolumeML) + payload += float_to_bytearray(maxHeparinBolusVolumeML) + payload += unsigned_integer_to_bytearray(enableChemicalDisinfect) + + + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_INSTITUTIONAL_RECORD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) \ No newline at end of file Index: leahi-dialin/ui/hd_simulator_alarms.py =================================================================== diff -u --- leahi-dialin/ui/hd_simulator_alarms.py (revision 0) +++ leahi-dialin/ui/hd_simulator_alarms.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,545 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file hd_simulator_alarms.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peter Lucia +# @date (original) 28-Sep-2020 +# +############################################################################ +import struct +from time import sleep, time + +from ..utils import YES, NO +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from logging import Logger +from ..utils.base import AbstractSubSystem +from ..utils.conversions import * +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.alarm_defs import AlarmList +from dialin.common.prs_defs import AlarmDataTypes + +HIGH = 3 +MED = 2 +LOW = 1 +NONE = 0 + +HIGH_PRIORITY_COLOR = "#c53b33" +MED_LOW_PRIORITY_COLOR = "#f5a623" + + +class Alarms: + # TODO: this should be generated from FW + # ALARM_ID = (priority, alarmID, escalates in, silent_espires_in, flags) + ALARM_ID_NO_ALARM = (NONE, 0, 0, 0, 0) + ALARM_ID_SOFTWARE_FAULT = (HIGH, 1, 0, 0, 0) + ALARM_ID_STUCK_BUTTON_TEST_FAILED = (HIGH, 2, 0, 0, 0) + ALARM_ID_FPGA_POST_TEST_FAILED = (HIGH, 3, 0, 0, 0) + ALARM_ID_WATCHDOG_POST_TEST_FAILED = (HIGH, 4, 0, 0, 0) + ALARM_ID_UI_COMM_POST_FAILED = (HIGH, 5, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MC_CURRENT_CHECK = (MED, 6, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_OFF_CHECK = (MED, 7, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MC_DIRECTION_CHECK = (MED, 8, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_CHECK = (HIGH, 9, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MC_CURRENT_CHECK = (MED, 10, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_OFF_CHECK = (MED, 11, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MC_DIRECTION_CHECK = (MED, 12, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_ROTOR_SPEED_CHECK = (HIGH, 13, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MC_CURRENT_CHECK = (MED, 14, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_OFF_CHECK = (MED, 15, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MC_DIRECTION_CHECK = (MED, 16, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_ROTOR_SPEED_CHECK = (HIGH, 17, 0, 0, 0) + ALARM_ID_WATCHDOG_EXPIRED = (HIGH, 18, 0, 0, 0) + ALARM_ID_RTC_COMM_ERROR = (HIGH, 19, 0, 0, 0) + ALARM_ID_RTC_CONFIG_ERROR = (HIGH, 20, 0, 0, 0) + ALARM_ID_DG_COMM_TIMEOUT = (HIGH, 21, 0, 0, 0) + ALARM_ID_UI_COMM_TIMEOUT = (HIGH, 22, 0, 0, 0) + ALARM_ID_COMM_TOO_MANY_BAD_CRCS = (HIGH, 23, 0, 0, 0) + ALARM_ID_TREATMENT_STOPPED_BY_USER = (LOW, 24, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_WARNING = (MED, 25, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_TOO_LONG_NO_RESUME = (MED, 26, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_TOO_LONG_NO_RINSEBACK = (HIGH, 27, 0, 0, 0) + ALARM_ID_CAN_MESSAGE_NOT_ACKED = (HIGH, 28, 0, 0, 0) + ALARM_ID_OCCLUSION_BLOOD_PUMP = (HIGH, 29, 0, 0, 0) + ALARM_ID_OCCLUSION_DIAL_IN_PUMP = (HIGH, 30, 0, 0, 0) + ALARM_ID_OCCLUSION_DIAL_OUT_PUMP = (HIGH, 31, 0, 0, 0) + ALARM_ID_ARTERIAL_PRESSURE_LOW = (HIGH, 32, 0, 0, 0) + ALARM_ID_ARTERIAL_PRESSURE_HIGH = (HIGH, 33, 0, 0, 0) + ALARM_ID_VENOUS_PRESSURE_LOW = (HIGH, 34, 0, 0, 0) + ALARM_ID_VENOUS_PRESSURE_HIGH = (HIGH, 35, 0, 0, 0) + ALARM_ID_UF_RATE_TOO_HIGH_ERROR = (HIGH, 36, 0, 0, 0) + ALARM_ID_UF_VOLUME_ACCURACY_ERROR = (HIGH, 37, 0, 0, 0) + ALARM_ID_RTC_BATTERY_LOW = (HIGH, 38, 0, 0, 0) + ALARM_ID_RTC_OR_TIMER_ACCURACY_FAILURE = (HIGH, 39, 0, 0, 0) + ALARM_ID_RTC_RAM_OPS_ERROR = (HIGH, 40, 0, 0, 0) + ALARM_ID_NVDATA_EEPROM_OPS_FAILURE = (HIGH, 41, 0, 0, 0) + ALARM_ID_NVDATA_MFG_RECORD_CRC_ERROR = (HIGH, 42, 0, 0, 0) + ALARM_ID_NVDATA_SRVC_RECORD_CRC_ERROR = (HIGH, 43, 0, 0, 0) + ALARM_ID_NVDATA_CAL_RECORD_CRC_ERROR = (HIGH, 44, 0, 0, 0) + ALARM_ID_NVDATA_HW_USAGE_DATA_CRC_ERROR = (HIGH, 45, 0, 0, 0) + ALARM_ID_NVDATA_DISINFECTION_DATE_CRC_ERROR = (HIGH, 46, 0, 0, 0) + ALARM_ID_RO_PUMP_OUT_PRESSURE_OUT_OF_RANGE = (HIGH, 47, 0, 0, 0) + ALARM_ID_TEMPERATURE_SENSORS_OUT_OF_RANGE = (HIGH, 48, 0, 0, 0) + ALARM_ID_TEMPERATURE_SENSORS_INCONSISTENT = (HIGH, 49, 0, 0, 0) + ALARM_ID_HD_COMM_TIMEOUT = (HIGH, 50, 0, 0, 0) + ALARM_ID_VALVE_CONTROL_FAILURE = (HIGH, 51, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 52, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 53, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 54, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MOTOR_SPEED_CHECK = (HIGH, 55, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MOTOR_SPEED_CHECK = (HIGH, 56, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MOTOR_SPEED_CHECK = (HIGH, 57, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_TOO_HIGH = (HIGH, 58, 0, 0, 0) + ALARM_ID_INLET_WATER_TEMPERATURE_OUT_OF_RANGE = (HIGH, 59, 0, 0, 0) + ALARM_ID_DOES_NOT_EXIST = (HIGH, 99, 0, 0, 0) + + +class HDAlarmsSimulator(AbstractSubSystem): + instance_count = 0 + + def __init__(self, can_interface: DenaliCanMessenger, logger: Logger): + """ + @param can_interface: Denali Can Messenger object + """ + super().__init__() + HDAlarmsSimulator.instance_count = HDAlarmsSimulator.instance_count + 1 + + self.can_interface = can_interface + self.logger = logger + self.flags = 0 + self.clear_after_user_action = False + self.current_alarm_volume = 5 + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_hd_ch_id + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_ALARM_SILENCE_REQUEST.value, + self._handler_alarm_silence) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_ALARM_USER_ACTION_REQUEST.value, + self._handler_user_action) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD_REQUEST.value, + self._handler_request_override_alarm_volume) + + def _send_alarm_volume_broadcast(self): + """ + Sends the alarm volume broadcast message + @return: None + """ + + payload = integer_to_bytearray(5) # alarm volume + payload += float_to_bytearray(1.0) # alarm audio current high gain (mA) + payload += float_to_bytearray(1.0) # alarm audio current low gain (mA) + payload += float_to_bytearray(1.0) # alarm backup audio current (mA) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_INFORMATION_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_override_alarm_volume(self, message: dict) -> None: + """ + Handler for a UI request to override the alarm volume level + @param message: + @return: + """ + vol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.START_POS_FIELD_1 + 4]))[0] + + self.logger.debug("Received request to override alarm volume to {0}".format(vol)) + + if 1 <= vol <= 5: + self.current_alarm_volume = vol + payload = integer_to_bytearray(YES) + payload += integer_to_bytearray(0) + else: + payload = integer_to_bytearray(NO) + payload += integer_to_bytearray(1) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_VOLUME_SET_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_activate_alarm_id(self, state: int = HIGH, + alarm: int = 0, + escalates_in: int = 0, + silence_expires: int = 0, + flags: int = 0): + """ + Activates the specified alarm + + @param state: (int) Alarm priority + @param alarm: (int) the alarm id + @param escalates_in: (int) how long until the alarm escalates + @param silence_expires: (int) seconds until silence expires + @param flags: (int) See 'cmd_make_alarm_flags' + @return: None + """ + + state = integer_to_bytearray(state) + top = integer_to_bytearray(alarm) + escalates_in = integer_to_bytearray(escalates_in) + silence_expires = integer_to_bytearray(silence_expires) + flags = integer_to_bytearray(flags) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_activate_alarm(self, alarm: AlarmList, + state: int = HIGH, + escalates_in: int = 0, + silence_expires: int = 0, + flags: int = 0): + """ + Activates the specified alarm + + + @param alarm: the alarm enum + @param state: Alarm priority + @param escalates_in: how long until the alarm escalates + @param silence_expires: seconds until silence expires + @param flags: additional alarm flags + Alarm flags: + eFlag_systemFault = 0 + eFlag_stop = 1 + eFlag_noClear = 2 + eFlag_noResume = 3 + eFlag_noRinseback = 4 + eFlag_noEndTreatment = 5 + eFlag_noNewTreatment = 6 + eFlag_bypassDialyzer = 7 + eFlag_alarmsToEscalate = 8 + eFlag_alarmsSilenced = 9 + eFlag_userAcknowledged = 10 + ... unused = 11 - 16 + @return: None + """ + + state = integer_to_bytearray(state) + top = integer_to_bytearray(alarm.value) + escalates_in = integer_to_bytearray(escalates_in) + silence_expires = integer_to_bytearray(silence_expires) + flags = integer_to_bytearray(flags) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_make_alarm_flags(self, + system_fault=0, + stop=0, + no_clear=0, + no_resume=0, + no_rinseback=0, + no_end_treatment=0, + no_new_treatment=0, + user_must_ack=0, + alarms_to_escalate=0, + alarms_silenced=0, + lamp_on=0, + unused_1=0, + unused_2=0, + unused_3=0, + no_minimize=0, + top_condition=0 + ): + """ + Helper function to construct the flags + + @param system_fault: One or more system faults has been triggered + @param stop: Alarm(s) have stopped treatment/activity and placed system in a safe state + @param no_clear: One or more active alarms is not recoverable + @param no_resume: The "resume" user recovery option is disabled + @param no_rinseback: The "rinseback" user recovery option is disabled + @param no_end_treatment: The "end treatment" user recovery option is disabled + @param no_new_treatment: A new treatment may not be started without cycling power to system + @param user_must_ack: The "ok" user recovery option is enabled + @param alarms_to_escalate: One or more active alarms will escalate in time + @param alarms_silenced: Alarms have been temporarily silenced by user + @param lamp_on: Alarm lamp is currently on (for syncing to UI) + @param unused_1: unused + @param unused_2: unused + @param unused_3: unused + @param no_minimize: Prevent user from minimizing alarm window + @param top_condition: The top alarm's condition is still being detected + @return: (int) containing all the flags + """ + flags = 0 + flags ^= system_fault * 2 ** 0 \ + | stop * 2 ** 1 \ + | no_clear * 2 ** 2 \ + | no_resume * 2 ** 3 \ + | no_rinseback * 2 ** 4 \ + | no_end_treatment * 2 ** 5 \ + | no_new_treatment * 2 ** 6 \ + | user_must_ack * 2 ** 7 \ + | alarms_to_escalate * 2 ** 8 \ + | alarms_silenced * 2 ** 9 \ + | lamp_on * 2 ** 10 \ + | unused_1 * 2 ** 11 \ + | unused_2 * 2 ** 12 \ + | unused_3 * 2 ** 13 \ + | no_minimize * 2 ** 14 \ + | top_condition * 2 ** 15 + return flags + + def cmd_send_clear_alarms(self): + """ + Broadcasts a clear alarms message + + @return: None + """ + + state = integer_to_bytearray(0) + top = integer_to_bytearray(0) + escalates_in = integer_to_bytearray(0) + silence_expires = integer_to_bytearray(0) + flags = integer_to_bytearray(0) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_cleared(self, alarm_id: int = 0): + """ + Broadcasts to clear a specific alarm ID + + the Alarm Cleared message builder method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x0400| 0x001 | 1 | Event | Y | HD | All | Alarm Cleared | \ref Data::mAlarmID | + + @return: None + """ + + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_cleared_condition(self, alarm_id: int = 0): + """ + Broadcasts that the alarm condition has been cleared + + the Alarm Cleared message builder method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:---------:|:---:|:------:|:---:|:---:|:---:|:---------------------: |:--: | + |0x3F00| 0x001,2,4 | 1 | Event | Y | HD | All | Alarm Condition Cleared | \ref Data::mAlarmID | + + @return: None + """ + + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_triggered(self, alarm_id, field_descriptor_1: int, data_field_1: str, + field_descriptor_2: int, data_field_2: str, + priority: int, rank: int, clear_top: int) -> None: + """ + Triggers an alarm. + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x0300| 0x001 | 1 | Event | Y | HD | All | Alarm Triggered | \ref Data::mAlarmID | + + @param alarm_id: (int) the alarm id to trigger + @param field_descriptor_1: (int) alarm data 1 type + @param data_field_1: (str) alarm data 1 + @param field_descriptor_2: (int) alarm data 2 type + @param data_field_2: (str) alarm data 2 + @param priority: (int) alarm priority + @param rank: (int) alarm rank + @param clear_top: (int) clear top only + + @return: None + """ + zero = integer_to_bytearray(0) + payload = integer_to_bytearray(alarm_id) + if field_descriptor_1 == AlarmDataTypes.ALARM_DATA_TYPE_NONE: + payload += zero + payload += zero + payload += zero + payload += zero + else: + if field_descriptor_1 == AlarmDataTypes.ALARM_DATA_TYPE_F32: + payload += integer_to_bytearray(field_descriptor_1) + payload += float_to_bytearray(float(data_field_1)) + else: # BOOL, S32, U32 + payload += integer_to_bytearray(field_descriptor_1) + payload += integer_to_bytearray(int(data_field_1)) + if field_descriptor_2 == AlarmDataTypes.ALARM_DATA_TYPE_NONE: + payload += zero + payload += zero + else: + if field_descriptor_2 == AlarmDataTypes.ALARM_DATA_TYPE_F32: + payload += integer_to_bytearray(field_descriptor_2) + payload += float_to_bytearray(float(data_field_2)) + else: # BOOL, S32, U32 + payload += integer_to_bytearray(field_descriptor_2) + payload += integer_to_bytearray(int(data_field_2)) + + payload += unsigned_integer_to_bytearray(priority) + payload += unsigned_integer_to_bytearray(rank) + payload += unsigned_integer_to_bytearray(clear_top) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_TRIGGERED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_repeat_broadcast_alarm(self, freq: int = 4, timeout: float = float('inf'), **kwargs): + """ + Broadcast the specified alarm message at particular frequency + + + @param freq: cycles / s of the broadcast + @param timeout: How long to broadcast the alarm for + @param kwargs: arguments to pass to cmd_activate_alarm + @return: None + """ + start = time() + current = time() + counter = 0 + silence_expires = 60 + + while current - start < timeout: + self.cmd_activate_alarm(flags=self.flags, silence_expires=silence_expires, **kwargs) + sleep(1.0 / freq) + current = time() + counter += 1 + if silence_expires > 0: + silence_expires -= 1 + else: + self.flags = self.cmd_make_alarm_flags(alarms_silenced=0) + + def set_flags(self, flags): + """ + Sets the alarm flags + + @param flags: + @return: None + """ + self.flags = flags + + def _handler_alarm_acknowledge(self) -> None: + """ + TODO: Remove + Handles the alarm acknowledge message + + @return: None + """ + + self.logger.debug("Alarm acknowledged") + self.flags = self.flags | 2 ** 10 + + def _handler_alarm_silence(self, message): + """ + Handles the alarm silence message + + @param message: the message with 0 = cancel, 1 = silence + @return: None + """ + + request = message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1][0] + if request: + self.logger.debug("Alarm Silence Request Start {0}".format(request)) + self.flags = self.flags | 2 ** 9 + else: + self.logger.debug("Alarm Silence Request Cancel {0}".format(request)) + self.flags = self.flags & ~2 ** 9 + + def cmd_alarm_condition_cleared(self, alarm_id: int): + """ + Sends the alarm condition cleared message + @param alarm_id: (int) The alarm ID + @return: None + """ + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_user_action(self, message): + """ + Called when the user responds to an alarm + @return: None + """ + response = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("User response to alarm: {0}".format(response)) + if self.clear_after_user_action: + self.cmd_send_clear_alarms() + + def cmd_send_active_list_response(self, accept: bool, reason: int = 0, + a0: int = 0, a1: int = 0, a2: int = 0, a3: int = 0, a4: int = 0, + a5: int = 0, a6: int = 0, a7: int = 0, a8: int = 0, a9: int = 0) -> None: + """ + send the list of active alarms + @param accept: boolean value true if the request accepted + @param reason: the rejection reason + @param a0: alarm id 0 in the list - First + @param a1: alarm id 1 in the list + @param a2: alarm id 2 in the list + @param a3: alarm id 3 in the list + @param a4: alarm id 4 in the list + @param a5: alarm id 5 in the list + @param a6: alarm id 6 in the list + @param a7: alarm id 7 in the list + @param a8: alarm id 8 in the list + @param a9: alarm id 9 in the list - Last + @return: None + """ + payload = integer_to_bytearray(accept) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(a0) + payload += integer_to_bytearray(a1) + payload += integer_to_bytearray(a2) + payload += integer_to_bytearray(a3) + payload += integer_to_bytearray(a4) + payload += integer_to_bytearray(a5) + payload += integer_to_bytearray(a6) + payload += integer_to_bytearray(a7) + payload += integer_to_bytearray(a8) + payload += integer_to_bytearray(a9) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) Index: leahi-dialin/ui/messageBuilder.py =================================================================== diff -u --- leahi-dialin/ui/messageBuilder.py (revision 0) +++ leahi-dialin/ui/messageBuilder.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,97 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file messageBuilder.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +from dialin.ui import utils, crc + +syncByte = 'A5' + + +def toCandumpFormat(msg: any) -> str: + """ + the converter method which converts the message vMsg to the string that candump tool understands. + @param msg: the message + @return: converted string to candump tool format + """ + if type(msg) == list: + for index, value in enumerate(msg): + msg[index] = ".".join(utils.partition(value, 2, False)) + else: + msg = ".".join(utils.partition(msg, 2, False)) + return msg + + +def toFrames(msg: any) -> list: + """ + converts the message vMsg to frames + @param msg: adds the crc8 checksum at the end of the message vMsg + @return: the frames + """ + mlen = 16 + padded = utils.padding(msg, mlen) + frames = utils.partition(padded, mlen, False) + return frames + + +def addCRC8(string: str, delimiter: str = "") -> str: + """ + adds the crc8 checksum at the end of the string vString + @param string: (str) the string to be used + @param delimiter: (str) the string delimiter + @return: the string with crc8 + """ + return string + delimiter + crc.calc_crc8(string, delimiter) + + +def textToByte(text: str, length) -> str: + """ + converts the string text to bytes by the given length + @param text: (str) given string + @param length: (int) given length + @return: converted text + """ + new_text = "" + text_len = len(text) + for i in range(length): + if i < text_len: + new_text += utils.toI08(ord(text[i])) + else: + new_text += utils.toI08(0) + new_text += utils.toI08(0) # null /0 + return new_text + + +def buildMessage(msg_id: int, length: int, ack: bool, *args) -> str: + """ + builds message from the parameter givven + @param msg_id: (int) the message ID + @param length: (int) length of the message payload in bytes + @param ack: (bool) if true the message requires acknowledge back + @param args: payload arguments. + @return: built message + """ + msg = "" + if ack: + seq = -1 + else: + seq = 1 + + msg += utils.toI16(seq) # always used seq# (-)1 (for now) + msg += utils.toI16(msg_id) + msg += utils.toI08(length) + for arg in args: + msg += arg + msg += crc.calc_crc8(msg) + return syncByte + msg Index: leahi-dialin/ui/unittests.py =================================================================== diff -u --- leahi-dialin/ui/unittests.py (revision 0) +++ leahi-dialin/ui/unittests.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,75 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file unittests.py +# +# @author (last) Michael Garthwaite +# @date (last) 20-Sep-2023 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ +import test +import sys + +from dialin.ui import crc +from dialin.ui.utils import check_can0 + +MICRO = [8, 9, 10] + + +def test_python_version(): + """ + tests the current python version compatibility + @return: None + """ + test.compare(sys.version_info.major, 3) + test.compare(sys.version_info.minor, 8) + + test.compare(sys.version_info.micro in MICRO, True) + + +def test_crc8(): + """ + test case for crc8 method + @return: None + """ + str_byte1 = ("4B 43 09 00 14 00 00" + "00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 00 00" + "00 00" # 9D + ) + + str_byte2 = ("4C 43 02 00 12 03 00" + "00 00 14 00 00 00 00 00" + "00 00 00 00 00 00 7F 00" # 55 + ) + + str_byte3 = ("4A 43 05 00 1C 00 00" + "00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 6A B6" + "99 43 D5 68 6F 44 00 00" + "00 00" # 4F + ) + + str_byte4 = ("FB 18 07 00 04 00 00" + "00 00" # 7F + ) + + test.compare(crc.calc_crc8(str_byte1, ' '), '9D') + test.compare(crc.calc_crc8(str_byte2, ' '), '55') + test.compare(crc.calc_crc8(str_byte3, ' '), '4F') + test.compare(crc.calc_crc8(str_byte4, ' '), '7F') + + +def test_can0(): + """ + tests if the can0 bus driver presents + @return: None + """ + ok, msg = check_can0() + test.compare(ok, True, msg) Index: leahi-dialin/ui/utils.py =================================================================== diff -u --- leahi-dialin/ui/utils.py (revision 0) +++ leahi-dialin/ui/utils.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,199 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file utils.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +import time +import struct +from subprocess import check_output + + +def srsui(vstr=""): + """ + for user convenience if wanted to put a note for the SRSUI + it makes sure all the numbers are 3 digit aligned to easily read on test results. + """ + return "SRSUI " + "{}".format(vstr).rjust(3, '0') + + +def isVisible(object, vobject): + """ + checks if the object is visible. + Note: in SquishQt it's a little different since usual check is wait + Note : object is the squish's builtin variable which should be available within the test + I don't thing importing would be a good idea here since it might be a runtime assigned value + and it may have different/undefined value when imported out of test + """ + if object.exists(vobject): + o_object = findObject(vobject) + if o_object is None: + return False + else: + return o_object.visible + else: + return False + + +def waitForGUI(delay_s: int = 2): + """ + a global 2 seconds default wait method which is used to wait for GUI animations mostly + to make sure the object is available. + @param delay_s: integer - amount of second for delay + @return: none + """ + time.sleep(delay_s) + + +def toUXX(value: int, byte_count: int, delimiter: str) -> str: + """ + converts the value to hex + @param value: (int) the value + @param byte_count: (int) number of bytes the value will take eg. U32=4, ... + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the value + """ + x = '{0:0{1}X}'.format(int(value) & (2 ** (4 * byte_count) - 1), byte_count, 'x') + byte_arr = partition(x, 2) + return delimiter.join(byte_arr) + + +def toI32(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value to integer 4 bytes + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 4 bytes + """ + return toUXX(value, 8, delimiter) + + +def toI16(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value to integer 2 bytes + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 2 bytes + """ + return toUXX(value, 4, delimiter) + + +def toI08(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value vValue to integer 1 byte + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 1 bytes + """ + return toUXX(value, 2, delimiter) + + +def toF32(value: float) -> str: + """ + converts value to floating point 4 bytes. + @param value: (float) the value + @return: hex formated conversion of the vValue to float 4 bytes + """ + return '{:08X}'.format(struct.unpack('f', value))[0], 'X') + + +def partition(string: str, part: int, right_rirection: bool = True) -> list: + """ + splits the given string into sections of vPart long + and puts the sections from left to right if right_rirection is False + and puts the sections from right to left if right_rirection is True + after the split is done + @param string: (str) the given string + @param part: (int) length of the section + @param right_rirection: (bool) order of sections in the output list + @return: (list) the section of the string in list + """ + return [string[i: i + part] for i in range(0, len(string), part)][::-1 if right_rirection else 1] + + +def padding(string: str, length: int) -> any: + """ + added zero at the right side of the string to be of length of vLen + @param string: (str) the string to add trailing zero to + @param length: (int) the entire length of the string + @return: (str) padded string + """ + str_len = len(string) + pad_len = int(str_len / length) * length + (length * (1 if str_len % length else 0)) + return string.ljust(pad_len, "0") + + +def tstStart(test_name) -> None: + """ + test case start print out with time + @param test_name: (str) name of the test case + @return: none - prints out on the console + """ + print(time.strftime("%H:%M:%S Start", time.localtime()) + " - " + test_name) + + +def tstDone() -> None: + """ + test case end print out with time + @return: none - prints out on the console + """ + print(time.strftime("%H:%M:%S Done ", time.localtime())) + + +def l2ml(value) -> int: + """ + converts liter to milliliter + @param (int) value: the value in liter. + @return: (int) value converted to milliliter. + """ + return int(round(value, 3) * 1000) + + +def ml2l(value): + """ + converts milliliter to liter + @param (int) value: the value in milliliter. + @return: (int) value converted to liter. + """ + return value / 1000 + + +def dict_update(obj: dict, key: str, value): + """ + Adds a key and value to a dictionary object without updating the original + dictionary + + @param obj: (dict) the object to update + @param key: (str) the key name + @param value: the new value of the field + @return: (dict) the updated dictionary object + """ + obj = obj.copy() + obj[key] = value + return obj + + +def check_can0(): + """ + check if the can0 bus driver presents + @return: (list) false if can0 not exists, msg as a message + """ + canid = "can0" + ipa = "ip a" + ipa = check_output(ipa, shell=True) + loc = str(ipa).find(canid) + if loc >= 0: + msg = "can device '{}' found".format(canid) + else: + msg = "No can device registered as '{}'".format(canid) + return loc >= 0, msg Index: leahi-dialin/utils/__init__.py =================================================================== diff -u --- leahi-dialin/utils/__init__.py (revision 0) +++ leahi-dialin/utils/__init__.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,10 @@ +from .base import * +from .checks import * +from .helpers import * +from .conversions import * +from .excel_ops import * +from .nv_ops_utils import * +from .singleton import * +from .data_logger import DataLogger +YES = 1 +NO = 0 Index: leahi-dialin/utils/base.py =================================================================== diff -u --- leahi-dialin/utils/base.py (revision 0) +++ leahi-dialin/utils/base.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,323 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file base.py +# +# @author (last) Micahel Garthwaite +# @date (last) 16-May-2023 +# @author (original) Peter Lucia +# @date (original) 22-Jun-2020 +# +############################################################################ +import logging +import os +from abc import ABC, abstractmethod +from datetime import datetime +from enum import Enum +from threading import Timer + +INTERVAL_1s = 1 +INTERVAL_5s = 5 +INTERVAL_10s = 10 +INTERVAL_60s = 60 + +class AbstractObserver(ABC): + """ + Publicly accessible parent class for all observers. + + The update method will receive data when data is made available + """ + + @abstractmethod + def update(self): + """ + Attach an observer + """ + pass + + +class _FauxLogger: + + def __init__(self, printing_enabled=False): + self.printing_enabled = printing_enabled + + def debug(self, msg): + if self.printing_enabled: + print("DEBUG: {0}".format(msg)) + + def info(self, msg): + if self.printing_enabled: + print("INFO: {0}".format(msg)) + + def warn(self, msg): + if self.printing_enabled: + print("WARN: {0}".format(msg)) + + def warning(self, msg): + if self.printing_enabled: + print("WARNING: {0}".format(msg)) + + def error(self, msg): + if self.printing_enabled: + print("ERROR: {0}".format(msg)) + + def critical(self, msg): + if self.printing_enabled: + print("CRITICAL: {0}".format(msg)) + + +class LogManager: + LOG_FMT = '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s' + LOG_FMT_NO_METADATA = '%(message)s' + LOG_DT_FMT = '%m-%d-%Y:%H:%M:%S' + + def __init__(self, log_level=None, log_filepath="Dialin.log"): + """ + + @param log_level: (str) or (None) if not set, contains the logging level + @param log_filepath: (str) the log filepath + """ + + self.log_level = log_level + self.logging_enabled = self.log_level_enables_logging(log_level) + self.log_filepath = self.get_available_log_path(log_filepath) + self.logger = None + self.configure_logging(self.log_filepath) + + def log_level_enables_logging(self, log_level: str): + """ + Check if the log level string is a valid logging level + + @param log_level: (str) the logging level + @return: True if the log level is valid, False otherwise + """ + return log_level is not None and log_level.upper() in self.get_logging_levels() + + @staticmethod + def get_logging_levels(): + """ + Gets all possible logging levels + + @return: All possible logging levels + """ + return ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + def request_new_log_path(self, path: str): + """ + Clears the logger, gets a new log filepath + and configures the logger to use it. + + @param path: the requested log filepath + @return: The new log filepath + """ + self.clear_logger() + self.log_filepath = self.get_available_log_path(path) + self.configure_logging(self.log_filepath) + return self.log_filepath + + def clear_logger(self): + """ + If the logger has been created, clear its handlers + + @return: True if successful, False otherwise + """ + + # remove existing handlers + if self.logger is not None: + for handler in self.logger.handlers: + self.logger.removeHandler(handler) + return True + return False + + def configure_logging(self, log_path): + """ + Sets up the logger to use the provided log path + + @param log_path: Path to the log file + @return: True if success, False otherwise + """ + if self.logger is not None: + print("Logger already configured. Please clear the logger first.") + return False + + # configure the logging + if self.logging_enabled and self.log_level not in ["PRINT_ONLY", "CAN_ONLY"]: + numeric_level = getattr(logging, self.log_level.upper(), logging.ERROR) + self.logger = logging.getLogger("Dialin") + self.logger.setLevel(numeric_level) + + fh = logging.FileHandler(log_path) + fh.setLevel(numeric_level) + ch = logging.StreamHandler() + ch.setLevel(numeric_level) + formatter = logging.Formatter(fmt=self.LOG_FMT, + datefmt=self.LOG_DT_FMT) + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + self.logger.addHandler(fh) + self.logger.addHandler(ch) + else: + self.logger = _FauxLogger(printing_enabled=self.log_level == "PRINT_ONLY") + + return True + + def print_and_log(self, message, log_level=logging.DEBUG): + """ + Prints a message if its severity is >= the current log level. + Also logs the message. + + @param message: The message to print and log + @param log_level: The logging level, indicates the severity of the message + + @return:: None + """ + if not self.logging_enabled: + return + + if log_level == logging.DEBUG: + self.logger.debug(message) + elif log_level == logging.INFO: + self.logger.info(message) + elif log_level == logging.WARNING: + self.logger.warning(message) + elif log_level == logging.ERROR: + self.logger.error(message) + elif log_level == logging.CRITICAL: + self.logger.critical(message) + + @staticmethod + def get_available_log_path(filepath: str): + """ + Gets an available log path from filepath + appends integer to the end if file already exists. + + @param filepath: The full path to the file + @return: (str) The available log filepath + """ + + if not os.path.exists(filepath): + return filepath + + path, ext = os.path.splitext(filepath) + i = 0 + while os.path.exists("{0}{1}{2}".format(path, i, ext)): + i += 1 + return "{0}{1}{2}".format(path, i, ext) + + +class AbstractSubSystem: + + @abstractmethod + def __init__(self): + """ + Initialization function for the sub system + # The abstract base class requires all abstract methods are overridden by children classes + + """ + self._observers = [] + self._datetime_fmt = "%m.%d.%Y_%I.%M.%S.%f" + pass + + def attach(self, observer: AbstractObserver): + """ + Attach an observer so it is updated upon published events + """ + self._observers.append(observer) + + def detach(self, observer: AbstractObserver): + """ + Detach an observer + """ + self._observers.remove(observer) + + +def publish(keys): + """ + Decorator that accepts a list of variable names to publish + To be used in any AbstractSubSystem + + @param keys: The variable names to publish + @return: A function that will take a function and return another function + """ + + def _decorator(func): + """ + + @param func: The handler function + @return: The function to wrap around _publish + """ + + def _wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + result = {} + + if not self._observers: + return None + + result["datetime"] = datetime.now() + result["subsystem"] = self.__class__.__name__ + + for key in keys: + result[key] = getattr(self, key) + + for observer in self._observers: + observer.update(result) + + return _wrapper + + return _decorator + + +class DialinEnum(Enum): + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + +class AlarmEnum(Enum): + def __init__(self, *args): + cls = self.__class__ + if any(self.value == member.value for member in cls): + raise ValueError("aliases not allowed: %r --> %r" % (self.name, cls(self.value).name)) + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + +class IntervalTimer(object): + """ + A class object that is used to execute a function on a timed interval. + Timed interval auto starts on object creation. + Uses to send CAN messages at a specified interval. + + """ + def __init__(self, interval, function, *args, **kwargs): + self._timer = None + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.is_running = False + + def _run(self): + self.is_running = False + self.start() + self.function(*self.args, **self.kwargs) + + def start(self): + if not self.is_running: + self._timer = Timer(self.interval, self._run) + self._timer.daemon = True + self._timer.start() + self.is_running = True + + def stop(self): + self._timer.cancel() + self.is_running = False Index: leahi-dialin/utils/checks.py =================================================================== diff -u --- leahi-dialin/utils/checks.py (revision 0) +++ leahi-dialin/utils/checks.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,27 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file checks.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean Nash +# @date (original) 27-May-2021 +# +############################################################################ + +def check_broadcast_interval_override_ms(ms: int): + """ + Checks whether a given broadcast interval override (in ms) is valid. + + @param ms: (int) number of ms being used to override a broadcast interval + @return: True if valid, False if not + """ + result = False + if ms > 0 and ms % 50 == 0: + result = True + return result Index: leahi-dialin/utils/conversions.py =================================================================== diff -u --- leahi-dialin/utils/conversions.py (revision 0) +++ leahi-dialin/utils/conversions.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,208 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file conversions.py +# +# @author (last) Dara Navaei +# @date (last) 20-Jun-2022 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from typing import List, Union + +# https://docs.python.org/3/library/struct.html#format-characters + + +def byte_to_bytearray(val: int) -> bytes: + """ + Converts a byte value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts a byte value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts a short integer (2 bytes) value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an unsigned short integer (2 bytes) value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int or val < 0: + raise ValueError("Expected unsigned integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an integer value into a byte array (little endian) + + @param val: integer to convert to byte array, bool is accepted + @return: byte array + """ + if type(val) != bool and type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an integer value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int or val < 0: + raise ValueError("Expected unsigned integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts a float value into a byte array (little endian) + + @param val: float to convert to byte array, int is accepted + @return:: byte array + """ + if type(val) != float and type(val) != int: + raise ValueError("Expected float but received {0} with type {1}".format(val, type(val))) + return struct.pack(' List[int]: + """ + Converts an integer to a bit array. For direct conversion to binary please use bin(val), + as this function has a separate purpose + @param val: (int) the number to convert + @param num_bits: (int) the number of bits needed + @return: (List[int]) The number represented in binary form as a list of 1's and 0's + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return [(val >> bit) & 1 for bit in range(num_bits - 1, -1, -1)] + + +def bytearray_to_value(fmt: str, buf: bytes) -> int: + """ + Converts the buffer to the type of value specified in the frm as format + + @param fmt: string format to convert the buffer to. + @param buf: the buffer of bytes to be converted to the value + + @return: the converted value of buffer to the type of fmt if successful or None if not. + """ + value = None + try: + value = struct.unpack(fmt, bytearray(buf)) + except BaseException as err: + print("err: ", err) + + if value is not None and len(value) > 0: + value = value[0] + + return value + + +def bytearray_to_byte(buffer: bytes, index: int, signed: bool = True) -> [int, int]: + """ + Converts the buffer of bytes to a value of one(1) byte integer + + @param buffer: (bytes) the source buffer + @param index: (int) the start index of reading the source buffer + @param signed: (bool) convert to signed or unsigned value + @return: pair of the [value and incremented index by the length. + """ + length = 1 # for a byte + value = bytearray_to_value("b" if signed else "B", buffer[index:index + length]) + return value, index + length + + +def bytearray_to_short(buffer: bytes, index: int, signed: bool = True) -> [int, int]: + """ + Converts the buffer of bytes to a value of two(2) byte integer + @param buffer: (bytes) the source buffer + @param index: (int) the start index of reading the source buffer + @param signed: (bool) convert to signed or unsigned value + @return: pair of the [value and incremented index by the length. + """ + length = 2 # for a short + value = bytearray_to_value("h" if signed else "H", buffer[index:index + length]) + return value, index + length + + +def bytearray_to_integer(buffer: bytes, index: int, signed: bool = True) -> [int, int]: + """ + Converts the buffer of bytes to a value of four(4) byte integer + + @param buffer: (bytes) the source buffer + @param index: (int) the start index of reading the source buffer + @param signed: (bool) convert to signed or unsigned value + @return: pair of the [value and incremented index by the length. + """ + length = 4 # for an integer + value = bytearray_to_value("i" if signed else "I", buffer[index:index + length]) + return value, index + length + + +def bytearray_to_long(buffer: bytes, index: int, signed: bool = True) -> [int, int]: + """ + Converts the buffer of bytes to a value of eight(8) byte integer + + @param buffer: (bytes) the source buffer + @param index: (int) the start index of reading the source buffer + @param signed: (bool) convert to signed or unsigned value + @return: pair of the [value and incremented index by the length. + """ + length = 8 # for a long + value = bytearray_to_value("q" if signed else "Q", buffer[index:index + length]) + return value, index + length + + +def bytearray_to_float(buffer: bytes, index: int, is_double: bool = False) -> [float, int]: + """ + Converts the buffer of bytes to a value of floating point. + + @param buffer: (bytes) the source buffer + @param index: (int) the start index of reading the source buffer + @param is_double: (bool) convert the value to eight(8) byte floating point if is_double is True, + or four(4) bytes otherwise. + @return: pair of the [value and incremented index by the length. + """ + length = 8 if is_double else 4 + value = bytearray_to_value("d" if is_double else "f", buffer[index:index + length]) + return value, index + length + Index: leahi-dialin/utils/data_logger.py =================================================================== diff -u --- leahi-dialin/utils/data_logger.py (revision 0) +++ leahi-dialin/utils/data_logger.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,161 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file data_logger.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 30-Apr-2021 +# +############################################################################ +import glob +import os +import shutil +import threading +from collections import deque +from logging import Logger +from time import sleep +from typing import Tuple +from multiprocessing import Process +from pathlib import Path +import pandas as pd + +from dialin.utils.base import _FauxLogger + + +class DataLogger: + + MAX_CHUNK_SIZE = 20 + + def __init__(self, + folder: str = "/tmp/", # must be the full path + logger: Logger = _FauxLogger()): + """ + Logs data to xlsx file + @param folder: (str) the destination filepath + + """ + super().__init__() + self.queue = deque() # Thread safe + self.disable_logging = False + self.thread = threading.Thread(target=self.logging_scheduler, daemon=True) + self.base_folder = folder + self.csv_writers = {} + self.logger = logger + self.thread.start() + self.path_disable_logging = "/tmp/DIALIN_DISABLE_LOGGING" + + def logging_scheduler(self) -> None: + """ + Called on each timer event. Calls the data logger method + + @return: None + """ + while True: + self.do_data_logging() + sleep(0.05) + + def add_data(self, data: Tuple): + """ + + @param data: (Tuple) the data to add to the queue + @return: None + """ + # ("module", "timestamp", "header", "value") + if not os.path.exists(self.path_disable_logging): + self.queue.append(data) + # TODO: Remove later - keep for debugging + # else: + # self.logger.debug("Ignoring {0}".format(data)) + + def do_data_logging(self) -> None: + """ + Called on teach timer event. Logs the data currently in deque + @return: None + """ + + i = 0 + while self.queue: + module, timestamp, data_name, data_value = self.queue.pop() + filename = module + folder = os.path.join(self.base_folder, filename + "Log") + if not os.path.isdir(folder): + os.mkdir(folder) + filepath = os.path.join(folder, data_name + ".csv") + + if not os.path.exists(filepath): + header = ["timestamp", data_name] + with open(filepath, 'w') as f: + f.write(",".join(header) + "\n") + else: + with open(filepath, 'a') as f: + f.write(timestamp + ", " + data_value + "\n") + + i += 1 + if i >= self.MAX_CHUNK_SIZE: + break + + def export_to_xlsx(self, output_path: str) -> None: + """ + Called when the user wishes to export all captured logs to a xlsx file + + @param output_path: (str) the destination output path + @return: None + """ + process = Process(target=self._do_export_to_xlsx, args=(output_path,)) + process.start() + + def _do_export_to_xlsx(self, output_path: str) -> None: + """ + Performs the actual export to xlsx + + @param output_path: (str) the destination output path + @return: None + """ + + Path(self.path_disable_logging).touch() + self.logger.debug("Starting data export to {0}".format(output_path)) + log_path = os.path.join(self.base_folder, "*Log") + folders = glob.glob(log_path) + if len(folders) == 0: + self.logger.debug("No folder with data to export") + os.remove(self.path_disable_logging) + return + writer = pd.ExcelWriter(output_path) + for folder in folders: + files = os.path.join(folder, "*.csv") + csv_files = glob.glob(files) + module_name = os.path.basename(folder) + df = pd.DataFrame() + for csv_file in csv_files: + df_data = pd.read_csv(csv_file) + df_data = df_data.set_index("timestamp") + df = df.join(df_data, how="outer") + try: + df.to_excel(writer, sheet_name=module_name) + self.logger.debug("Added {0} to {1}".format(module_name, output_path)) + except ValueError as e: + self.logger.error("Error during write to excel: {0}".format(e)) + + writer.save() + self.logger.debug("Finished data export to {0}".format(output_path)) + os.remove(self.path_disable_logging) + + def clear_logs(self): + """ + Called when the user clears the logs + @return: None + """ + Path(self.path_disable_logging).touch() + log_path = os.path.join(self.base_folder, "*Log") + folders = glob.glob(log_path) + for folder in folders: + self.logger.debug("Removing {0}".format(folder)) + shutil.rmtree(folder) + os.remove(self.path_disable_logging) + Index: leahi-dialin/utils/excel_ops.py =================================================================== diff -u --- leahi-dialin/utils/excel_ops.py (revision 0) +++ leahi-dialin/utils/excel_ops.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,228 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file excel_ops.py +# +# @author (last) Dara Navaei +# @date (last) 22-Feb-2022 +# @author (original) Dara Navaei +# @date (original) 21-Feb-2021 +# +############################################################################ + +import os +import math +import datetime +from openpyxl.styles import PatternFill, Font, Alignment, Protection +from openpyxl.utils import get_column_letter +from openpyxl import Workbook, load_workbook + +COLUMN_WIDTH_TOLERANCE = 3.0 +COLUMN_MAX_WIDTH = 50.0 +ROW_DEFAULT_HEIGHT = 15.0 +GREEN = PatternFill(start_color='00FF00', end_color='00FF00', fill_type='solid') +YELLOW = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid') +BLUE = PatternFill(start_color='00CCFF', end_color='00CCFF', fill_type='solid') +RED = PatternFill(start_color='FF0000', end_color='FF0000', fill_type='solid') +NO_COLOR = PatternFill(fill_type='none') + + +def get_an_excel_workbook(): + """ + This function returns an excel workbook object + + @return excel workbook object + """ + return Workbook() + + +def setup_excel_worksheet(workbook_obj, title, index=None, protection=False): + """ + Creates the worksheets in the created excel file with the name of each software_project_name as the name of + the worksheet. + + @param workbook_obj: Excel workbook object + @param title: Title of the created worksheet + @param index: Index of the worksheet. If a sheet needs to be at a certain place (default none) + @param protection: Flag to indicate whether the worksheet is write protected or not (default False) + + @return none + """ + # Assume the new sheet does not exist + is_sheet_name_available = False + + # Loop through the list of the sheet names in the excel + for sheet_name in workbook_obj.sheetnames: + # If the requested title exists in the excel document, set as True + # and break out of the loop. + if title == sheet_name: + is_sheet_name_available = True + break + # If the requested sheet does not exist, create it + if is_sheet_name_available is False: + # Create a tab and name the tab with the name of the projects dictionary + workbook_obj.create_sheet(title=title, index=index) + + # Check if the created worksheet must be write protected and if yes, protect it + if protection: + workbook_obj[title].protection.sheet = True + + # If the first tab is Sheet or Sheet1, remove it + # The other tabs must be created first before removing the default tab + if workbook_obj.sheetnames[0] == 'Sheet' or workbook_obj.sheetnames[0] == 'Sheet1': + sheet = workbook_obj.get_sheet_by_name(workbook_obj.sheetnames[0]) + workbook_obj.remove_sheet(sheet) + + +def write_to_excel(workbook_obj, project, row, column, data, name='Calibri', font=11, bold=False, color=None, + merge=None, horizontal='left', freeze=False, max_col_len=None, protect_cell=False): + """ + This function writes data at the specified row and column in an excel file (object). + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param row: Current row number + @param column: Current column number + @param data: Data to be written at the cell + @param name: Font type (default Calibri) + @param font: Font size (default 11) + @param bold: Bold or un-bold (default un-bold) + @param color: Color of the cell (default no color) + @param merge: Merge cells. Cells must be provided with A1:A4 format (default none) + @param horizontal: Horizontal alignment (default left) + @param freeze: Freeze top row (default false) + @param max_col_len: maximum length of a column (default none, means it is not restricted) + @param protect_cell: flag to indicate whether to write protect cell or not (default False) + + @return: None + """ + row_height = 0 + # Get the number of an alphabetic column (i.e A -> 1) + column_letter = get_column_letter(column) + cell_name = column_letter + str(row) + + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + # Set the provided data into the specified row and column and set the bold, color and horizontal alignment + active_sheet.cell(row=row, column=column).value = data + active_sheet.cell(row=row, column=column).font = Font(size=font, bold=bold, name=name) + # Wrapping text is not needed unless the length of the data is more than maximum column + # length + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=False) + # Get the width of the current column + column_width = active_sheet.column_dimensions[column_letter].width + # When the column width is on the default, openpyxl reports None. If the width is reported as None, + # it will be set to 0 for math comparison + column_width = 0 if column_width is None else column_width + # If the length is not provided, use the default maximum length + max_len = COLUMN_MAX_WIDTH if max_col_len is None else max_col_len + # Remove all the end of the line artifacts + length_of_data = len(str(data).rstrip()) + + # If the length of data was greater than the maximum length, calculate the number of + # rows is needed with the + # default height + if length_of_data > max_len: + # Since the length is greater than maximum, enable wrap text + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=True) + # Calculate what the row height should be when the cell is extended + row_height = math.ceil(length_of_data / max_len) * ROW_DEFAULT_HEIGHT + active_sheet.column_dimensions[column_letter].width = max_len + # If the length of the data provided is already less than the length of cell, + # do nothing + elif column_width < length_of_data: + active_sheet.column_dimensions[column_letter].width = length_of_data + COLUMN_WIDTH_TOLERANCE + row_height = ROW_DEFAULT_HEIGHT + + # If the current row height is not defined or the row height is less than the calculated new height, set it + if active_sheet.row_dimensions[row].height is None or active_sheet.row_dimensions[row].height < row_height: + active_sheet.row_dimensions[row].height = row_height + + # If color has been defined, set the color of the cell + if color is not None: + active_sheet[cell_name].fill = color + + # If merge has been requested + if merge is not None: + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge)) + + if freeze: + # To freeze row 1, make the cell is not row 1, that's why A2 was chosen + active_sheet.freeze_panes = 'A2' + + # Enforce the cell protection whether it is a False or True + active_sheet.cell(row, column).protection = Protection(locked=protect_cell) + + +def load_excel_report(path): + """ + This function returns an object of a currently existing excel workbook + + @return loaded excel workbook object + """ + return load_workbook(path) + + +def get_cell_value(workbook_obj, project, row, col): + """ + This function returns the value of a written excel cell + + @return excel workbook object + """ + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + # Get the cell object + cell_obj = active_sheet.cell(row=row, column=col) + # Convert it to the actual value + return cell_obj.value + + +def merge_cells(workbook_obj, project, start_row, start_col, end_row, end_col): + """ + This function merges the specified cells. + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param start_row: Row number at the beginning of merge + @param start_col: Column number at the beginning of merge + @param end_row: Row number at the end of merge + @param end_col: Column number at the end of merge + + @return: None + """ + # Go to the define sheet + active_sheet = workbook_obj[project] + # Convert the cell numbers to text and number (i.e row=1 and col=1 -> A1) for the start and end cells + merge_start_cell = get_column_letter(start_col) + str(start_row) + merge_end_cell = get_column_letter(end_col) + str(end_row) + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge_start_cell) + ':' + str(merge_end_cell)) + + +def save_report(excel_workbook, save_dir, record_name, stack_name=None): + """ + This function overrides the save function in the Base class. The function saves the excel file. + + @param excel_workbook: Excel workbook object + @param save_dir: Saving directory + @param record_name: Type of record being saved (i.e. calibration, software configuration) + @param stack_name: Name of the software stack name (i.e. HD) default none + + @returns none + """ + # Get the current date + current_date = str(datetime.datetime.now().date()) + # Some of the records might want to add the name of the stack. For instance, software configurations might want to + # mention that this is an HD or DG software configuration report. + if stack_name is not None: + address = current_date + '-' + str(stack_name) + '-' + str(record_name).upper() + '-Record.xlsx' + else: + address = current_date + '-' + str(record_name).upper() + '-Record.xlsx' + # Create the save path by using the path, date and current code review excel count out of total number of them + path = os.path.join(save_dir, address) + excel_workbook.save(filename=path) Index: leahi-dialin/utils/helpers.py =================================================================== diff -u --- leahi-dialin/utils/helpers.py (revision 0) +++ leahi-dialin/utils/helpers.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,238 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file helpers.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 29-Apr-2021 +# +############################################################################ +import logging +import subprocess +from datetime import datetime +from .base import LogManager + + +def setup_virtual_can_interface(): + """ + Convenience function to setup a virtual can interface using the most common settings + See the setup_can function to setup a can interface with custom settings. + + @return: True if successful, false otherwise + """ + bring_interface_down(interface="can0", delete=True) + is_present = is_interface_present("can0") + if not is_present: + setup_can(interface="can0", + is_virtual=True) + result = is_interface_up("can0") + if result: + print("Success") + return True + + print("Failure") + return False + + +def setup_real_can_interface(): + """ + Convenience function to setup a real can interface using the most common settings + See the setup_can function to setup a can interface with custom settings. + + @return: True if successful, False otherwise + """ + delete_virtual = input("Delete virtual can interface? (y,n)") + if delete_virtual.upper() in ["Y", "YES"]: + bring_interface_down(interface="can0", delete=True) + if is_interface_present("can0"): + print("Failure") + return False + else: + bring_interface_down(interface="can0", delete=False) + if not is_interface_present("can0") or is_interface_up("can0"): + print("Failure") + return False + input("Press 'Enter' after plugging in the PCAN-USB.") + setup_can(interface="can0", + is_virtual=False, + bitrate=250000, + restart_ms=100, + txqueue=10000) + + if is_interface_present("can0") and is_interface_up("can0"): + print("Success") + return True + else: + print("Failure") + return False + + +def bring_interface_down(interface: str = "can0", delete=False): + """ + Brings the specified can interface down + + @param interface: (str) the interface name + @param delete: (bool) whether to delete the interface after bringing it down + @return: None + """ + cmd_iface_down = "sudo ifconfig {0} down > /dev/null 2>&1".format(interface) + cmd_iface_delete = "sudo ip link delete dev {0} > /dev/null 2>&1".format(interface) + + success, info = run_cmd(cmd_iface_down) + if not success: + print("Warning: Could not bring interface down: {0}".format(info)) + if delete: + run_cmd(cmd_iface_delete) + if not success: + print("Warning: Could not delete the interface: {0}".format(info)) + + +def run_cmd(cmd: str): + """ + Runs the provided command, returns if it was successful and output or error information + + @param cmd: (str) the command to run + @return: tuple(bool, info) Whether the command was successful. If true info is the output, otherwise + info contains the error information + """ + try: + result = subprocess \ + .check_output(cmd, + shell=True).decode("utf-8").strip() + return True, result + except subprocess.CalledProcessError as e: + return False, str(e) + + +def is_interface_up(interface: str = "can0"): + """ + Checks if the specified interface is listed and up + + @param interface: + @return: (bool) True if up, False if not or if we can't tell + """ + cmd_check_interface = "ifconfig {0}".format(interface) + + success, info = run_cmd(cmd_check_interface) + if success: + if interface in info and "UP,RUNNING" in info: + return True + return False + + +def is_interface_present(interface: str = "can0"): + """ + Checks if the specified interface is listed + + @param interface: + @return: (bool) True if present, False if not or if we can't tell + """ + cmd_check_interface = "ip a" + + success, info = run_cmd(cmd_check_interface) + if success: + if interface in info: + return True + return False + + +def setup_can(interface: str = "can0", is_virtual=False, bitrate=250000, restart_ms=100, txqueue=10000): + """ + Convenience function to setup a can interface + + @param interface: (str) The interface name + @param is_virtual: (bool) If the interface is virtual or not + @param bitrate: (int) The desired bitrate (applies to non-virtual interfaces only) + @param restart_ms: (int) The desired restart_ms (applies to non-virtual interfaces only) + @param txqueue: (int) The desired txqueue length + @return: + """ + cmd_iface_up = "sudo ip link set {0} up type can bitrate {1} restart-ms {2}" \ + .format(interface, bitrate, restart_ms) + cmd_txqueue = "sudo ifconfig {0} txqueuelen {1}".format(interface, txqueue) + + cmd_iface_type_virtual = "sudo ip link add dev {0} type vcan".format(interface) + cmd_iface_up_virtual = "sudo ip link set {0} up".format(interface) + + if is_virtual: + success, info = run_cmd(cmd_iface_type_virtual) + if not success: + print("Warning: Could not set iface type to virtual: {0}".format(info)) + success, info = run_cmd(cmd_iface_up_virtual) + if not success: + print("Warning: Could not bring up virtual interface: {0}".format(info)) + else: + success, info = run_cmd(cmd_iface_up) + if not success: + print("Warning: Could not bring interface up: {0}".format(info)) + + success, info = run_cmd(cmd_txqueue) + if not success: + print("Warning: Could not set txtqueue length: {0}".format(cmd_txqueue)) + + +def create_logger(log_path: str = "/tmp/DialinScript.log", + level: str = "ERROR", + enable_metadata=True, + clear_before_write=False): + """ + Convenience function to create a logger for external Dialin scripts + + @param log_path: (str) The full path to the output log file. + @param level: (str) The logging level (e.g. INFO, WARN, DEBUG, ERROR, CRITICAL) + @param enable_metadata: (bool) if True, include metadata, otherwise, log only the message + @param clear_before_write: (bool) if True, clear log file before write to it + @return: (logging.Logger) The logger object + """ + + numeric_level = getattr(logging, level, logging.ERROR) + + current_time = datetime.now() + + # create a new unique logger using current date and time + logger = logging.getLogger("DialinScript{0}{1}{2}".format(current_time.hour, + current_time.minute, + current_time.second)) + logger.setLevel(numeric_level) + + if clear_before_write: + fh = logging.FileHandler(log_path, mode='w') + else: + fh = logging.FileHandler(log_path) + fh.setLevel(numeric_level) + ch = logging.StreamHandler() + ch.setLevel(numeric_level) + if enable_metadata: + formatter = logging.Formatter(fmt=LogManager.LOG_FMT, + datefmt=LogManager.LOG_DT_FMT) + else: + formatter = logging.Formatter(fmt=LogManager.LOG_FMT_NO_METADATA) + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + logger.addHandler(fh) + logger.addHandler(ch) + return logger + + +def find_variables_in_object(obj, value, starts_with: str = ""): + """ + Returns a list of variable names that in the object that match the searched value + + @param obj: (object) The object to search through + @param value: (object) The value to lookup + @param starts_with: (str) The + @return: (List[str]) A list of variable names matching the searched value + """ + result = [] + for attr in dir(obj): + if not callable(getattr(obj, attr)) and attr.startswith(starts_with): + if value == getattr(obj, attr): + result.append(attr) + return result Index: leahi-dialin/utils/nv_ops_utils.py =================================================================== diff -u --- leahi-dialin/utils/nv_ops_utils.py (revision 0) +++ leahi-dialin/utils/nv_ops_utils.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,1038 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file nv_ops_utils.py +# +# @author (last) Dara Navaei +# @date (last) 05-Mar-2024 +# @author (original) Dara Navaei +# @date (original) 21-Feb-2021 +# +############################################################################ +import os.path +import struct +import time +from enum import unique +from logging import Logger +from typing import List +from collections import OrderedDict +from .excel_ops import * +from dialin.utils.base import AbstractObserver, DialinEnum + + +@unique +class NVRecordsDG(DialinEnum): + NVDATAMGMT_CALIBRATION_RECORD = 0 + NVDATAMGMT_SYSTEM_RECORD = 1 + NVDATAMGMT_SERVICE_RECORD = 2 + NVDATAMGMT_SCHEDULED_RUNS_RECORD = 3 + NVDATAMGMT_HEATERS_INFO_RECORD = 4 + NVDATAMGMT_USAGE_INFO_RECORD = 5 + NVDATAMGMT_SW_CONFIG_RECORD = 6 + NUM_OF_NVDATMGMT_RECORDS_JOBS = 7 + +class NVRecordsHD(DialinEnum): + NVDATAMGMT_CALIBRATION_RECORD = 0 + NVDATAMGMT_SYSTEM_RECORD = 1 + NVDATAMGMT_SERVICE_RECORD = 2 + NVDATAMGMT_INSTITUTIONAL_RECORD = 3 + NVDATAMGMT_USAGE_INFO_RECORD = 4 + NVDATAMGMT_SW_CONFIG_RECORD = 5 + NUM_OF_NVDATMGMT_RECORDS_JOBS = 6 + + +class NVUtilsObserver(AbstractObserver): + """ + + Observation class + """ + def __init__(self, prop): + self.received = False + self.prop = prop + + def update(self, message): + """ + Publicly accessible function to provide an update of the object that is being observed + + @param message: (str) the message to update its status + + @return none + """ + self.received = message.get(self.prop, False) + + +class NVOpsUtils: + """ + + Processes the calibration_record records, service records, system records, and the scheduled runs records. + The records are prepared to be sent to firmware or to be received from firmware. + """ + CRC_16_TABLE = ( + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 + ) + + # Public defines + DEFAULT_CHAR_VALUE = ' ' + CAL_RECORD_TAB_NAME = 'Calibration_Record' + NON_VOLATILE_RECORD_NAME = 'SW_Config_Report' + USAGE_INFO_RECORD_TAB_NAME = 'Usage_Info_Record' + SYSTEM_RECORD_TAB_NAME = 'System_Record' + SERVICE_RECORD_TAB_NAME = 'Service_Record' + INSTITUTIONAL_RECORD_TAB_NAME = 'Institutional_Record' + DEFAULT_EPOCH_VALUE = 0 + + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _RECORD_SPECS_BYTE_ARRAY = 3 + + _CURRENT_MESSAGE_NUM_INDEX = 0 + + _DATA_TYPE_INDEX = 0 + _DATA_VALUE_INDEX = 1 + _CHAR_LENGTH_INDEX = 2 + _DICT_VALUE_LIST_LEN = 2 + + _TARGET_BYTES_TO_SEND_TO_FW = 150 + _MIN_PAYLOAD_BYTES_SPACE = 4 + + _PAYLOAD_CURRENT_MSG_INDEX = 0 + _PAYLOAD_TOTAL_MSG_INDEX = 1 + _PAYLOAD_TOTAL_BYTES_INDEX = 2 + + _SW_CONFIGS_TITLE_COL = 'SW Configurations' + _SW_CONFIGS_VALUE_COL = 'Status' + _SW_CONFIGS_REPORT_NAME = 'SW-CONFIGS' + _CAL_TIME_NAME = 'cal_time' + _NEW_CAL_TIME_SIGNAL = 'new' + _CRC_NAME = 'crc' + _PADDING_GROUP_NAME = 'padding' + + def __init__(self, logger: Logger): + """ + Constructor for the NVOptsUtils class + + @param logger: (Logger) the logger + """ + + self.logger = logger + self._workspace_dir = '' + self._excel_workbook = '' + self._record_name = '' + self._firmware_stack = '' + self._is_writing_to_excel_done = False + self._is_read_done = False + + # This list contains different data packet lists + # i.e. [[message 1, payload], [message 2, payload], ...] + self._record_packets_to_send_to_fw = [] + + # Buffer that is used to keep the groups data except the crc to convert them to bytes and calculate the crc + # of the group. + self._temp_groups_data_to_calculate_crc = [] + + def _create_workspace(self, dir_name): + """ + Publicly accessible function to get create a workspace for the script that is running. + + @param dir_name: Name of the workspace directory + + @return none + """ + # Get the root directory of the current script + scripts_root_dir = os.path.dirname(os.path.dirname(__file__)) + # Get the root directory of the entire scripts folder. The workspace that holds the + # code review reports and clones other scripts and repositories must be outside of the scripts + root_dir = os.path.dirname(os.path.dirname(scripts_root_dir)) + # Create the address of the workspace + self._workspace_dir = os.path.join(root_dir, dir_name) + # If the path does not exist, make it, otherwise, change to that directory + if not os.path.isdir(self._workspace_dir): + # Create the directory and go to it + os.mkdir(self._workspace_dir) + os.chdir(self._workspace_dir) + + def prepare_excel_report(self, firmware_stack: str, record_name: str, directory: str, protect_sheet: bool = False): + """ + Publicly accessible function to prepare the excel report + + @param firmware_stack: (str) firmware stack name (e.g. "HD" or "DG") + @param record_name: (str) record type to check such as calibration, system, ... + @param directory: (str) the directory in which to write the excel document. + @param protect_sheet: (bool) flag to indicate whether to write protect the sheet or not (default False) + @return none + """ + path = '' + is_report_found = False + + # If a directory is provided and there is not a folder in that address, create the + # directory. Set the workspace directory to the provided directory. If a directory was not + # provided, create a workspace in the default position + default_nv_directory = firmware_stack + '_NV_Records' + + if directory is not None: + directory = os.path.join(directory, default_nv_directory) + if not os.path.isdir(directory): + # Create the directory and go to it + os.mkdir(directory) + + self._workspace_dir = directory + os.chdir(self._workspace_dir) + else: + self._create_workspace(default_nv_directory) + + self._record_name = record_name + self._firmware_stack = firmware_stack + + # List all the files in the workspace directory + for file in os.listdir(self._workspace_dir): + # If the file has an extension of .xlsx + if file.endswith('.xlsx'): + # Check if the firmware stack (i.e. DG) is in the file and name of the file + # does not have lock in it. When the file is open, there is a hidden lock file + # in there and it is ignored + file = str(file) + if self._firmware_stack in file and 'lock' not in file: + if str(datetime.datetime.now().date()) in file: + if self._SW_CONFIGS_REPORT_NAME in file and self.NON_VOLATILE_RECORD_NAME == record_name: + # Create the file path and exit the loop + path = os.path.join(self._workspace_dir, file) + is_report_found = True + break + elif self._SW_CONFIGS_REPORT_NAME not in file and self.NON_VOLATILE_RECORD_NAME not in \ + record_name: + # Create the file path and exit the loop + path = os.path.join(self._workspace_dir, file) + is_report_found = True + break + + if is_report_found: + # Load the excel workbook + self._excel_workbook = load_excel_report(path) + else: + # Get an excel workbook object + self._excel_workbook = get_an_excel_workbook() + + # Setup worksheet and create the current tab + setup_excel_worksheet(self._excel_workbook, self._record_name, protection=protect_sheet) + + def write_fw_record_to_excel(self, calibration_record: OrderedDict): + """ + Writes a calibration record to excel + @param calibration_record: (dict) the record to write to excel + @return: None + """ + try: + row = 1 + # Let's say the calibration record is: + # Get the keys of the calibration group {'pressure_sensors': 'ppi', {'fourth_order': [' bytearray: + """ + Gets the list of the characters and makes sure their length is to the define length + + @param record: (list) the list that contains the characters, data type and the target character length + @return characters (bytearray) that are converted to bytearrays + """ + data_type = record[0] + char = record[1] + char_len = record[2] + temp = bytearray() + + if len(char) > char_len: + char = char[:char_len] + elif len(char) < char_len: + for i in range(len(char), char_len): + char += NVOpsUtils.DEFAULT_CHAR_VALUE + + for ch in char: + temp += struct.pack(data_type, ch.encode('ascii')) + + return temp + + @staticmethod + def reset_fw_record(record: OrderedDict) -> OrderedDict: + """ + Gets a record and updated the calibration date and crc + + @param record: (dict) the record to calculate the calibration time and crc + @return record (OrderedDict) the record with updated calibration time and crc + """ + for key, value in record.items(): + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + if sub_key == 'cal_time': + sub_value[sub_key][1] = NVOpsUtils.get_current_time_in_epoch() + crc = NVOpsUtils.get_group_record_crc(sub_value) + + sub_value['crc'][1] = crc + return record + + @staticmethod + def reset_fw_system_service_record(record: OrderedDict) -> OrderedDict: + """ + Gets a record and updated the calibration date and crc + + @param record: (dict) the record to calculate the calibration time and crc + @return record (OrderedDict) the record with updated calibration time and crc + """ + for key, value in record.items(): + # Check if there is a CRC in the inner dictionary since some of the structures might not have it. + # For instance, the software configuration record does not have an inner CRC and it only has a global CRC + # with the padding + if isinstance(value, dict): + crc = NVOpsUtils.get_group_record_crc(value) + value['crc'][1] = crc + return record + + @staticmethod + def get_group_record_crc(group_record: dict) -> int: + """ + Gets a group record and calculates the crc for the group + + @param group_record: (dict) the record to calculate the crc + @return crc (int) the calculated crc + """ + value_in_bytes = b'' + temp = [] + for key, value in group_record.items(): + if key is not 'crc': + data_type = value[0] + if data_type == ' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + # Insert a 4-byte 0 to the index of the total messages. This is a place holder and it will + # be updated with the right value later. + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._DICT_VALUE_LIST_LEN: + byte_size += current_byte_size * group[key][self._CHAR_LENGTH_INDEX] + else: + byte_size += current_byte_size + + return byte_size + + @classmethod + def crc_16(cls, data): + """ + generates crc16 for the provided data + @param data: byte of data + @return: (int) the crc code + """ + crc = 0xFFFF + length = len(data) + i = 0 + + while length > 0: + # Make the sure variables are 16-bit integers + left = (crc << 8) & 0x0000FFFF + right = (crc >> 8) & 0x0000FFFF + crc = left ^ cls.CRC_16_TABLE[data[i] ^ (right & 0x00FF)] + length -= 1 + i += 1 + + return crc + + @staticmethod + def get_data_type_bytes(data): + """ + Handles converting the string representation of the bytes of the data types in a struct to numbers. + This is a static method. + + @param data: the data to be converted to bytes in number + @return: calculated byte size + """ + number_of_bytes = struct.calcsize(data) + + return number_of_bytes + + @staticmethod + def calculate_padding_byte_size(total_byte_size, max_buffer_size): + """ + Handles calculating the padding length based on the provided buffer sizes. This is a static method. + + @param total_byte_size: total byte size of a record dictionary + @param max_buffer_size: max buffer size that is allowed to be used in the dictionary + + @return: padding size in bytes + """ + + # Calculate the padding size: + # If bytes in the dictionary % max write bytes to RTC RAM (64) or EEPROM (16) is 0, not padding is needed + # Else padding = (ceil(dictionary bytes/max write) * max write)-dictionary bytes + if (total_byte_size % max_buffer_size) == 0: + padding_size = 0 + else: + padding_size = (math.ceil(total_byte_size / max_buffer_size) * max_buffer_size) - total_byte_size + + return padding_size + + def write_sw_config_to_excel(self, sw_configs: OrderedDict, stack_name: str): + """ + Publicly accessible function to write the software configurations into an excel report + + @param sw_configs: (ordered dictionary) the configurations record to write to excel + @param stack_name: (str) the name of the software stack (i.e. HD) + + @return none + """ + row = 1 + names_col_number = 2 + write_to_excel(self._excel_workbook, self._record_name, row, names_col_number, self._SW_CONFIGS_TITLE_COL, + bold=True, freeze=True, protect_cell=True) + + values_col_number = 3 + write_to_excel(self._excel_workbook, self._record_name, row, values_col_number, self._SW_CONFIGS_VALUE_COL, + bold=True, protect_cell=True) + + # Prepare for writing the values + row += 1 + + for key, values in sw_configs['sw_configs'].items(): + + write_to_excel(self._excel_workbook, self._record_name, row, names_col_number, key, bold=True, + protect_cell=True) + + # Get the configuration value + config_value = values[1] + # If the config value is not 0, color the cell as green otherwise, leave it none colored + color = GREEN if config_value != 0 else NO_COLOR + write_to_excel(self._excel_workbook, self._record_name, row, values_col_number, config_value, color=color) + row += 1 + + save_report(self._excel_workbook, self._workspace_dir, self._SW_CONFIGS_REPORT_NAME, stack_name=stack_name) + + def get_sw_configs_from_excel(self, sw_configs_dict: OrderedDict, excel_path: str, sw_config_excel_tab: str): + """ + Publicly accessible function to get the software configurations from excel + + @param sw_configs_dict: (ordered dictionary) the configurations record to write to excel + @param excel_path: (str): the path to the excel report that its data is read + @param sw_config_excel_tab: (str): the name of the tab in the excel report that the values are located at + + @return status of the operations + """ + row = 1 + col = 1 + title_col = None + value_col = None + max_col_to_go = 50 + status = False + # Load the excel report + self._excel_workbook = load_excel_report(excel_path) + active_sheet = self._excel_workbook[sw_config_excel_tab] + + while True: + # Loop through the cells in the title row + value = active_sheet.cell(row=row, column=col).value + # If the col number exceeded the maximum column number, break out of the loop + if col >= max_col_to_go: + break + + # Check if the value of the read cell is not none + if value is not None: + # If the value of the title row is name of the sw configs title column name, update the title col number + if value.strip() == self._SW_CONFIGS_TITLE_COL: + title_col = col + # If the value of the title row is the name of the values of the sw configs, update the value col number + if value.strip() == self._SW_CONFIGS_VALUE_COL: + value_col = col + # If the title col and value col numbers are both found and they are not none, then exit the while loop + # since the values have been found, otherwise, increment the column number and keep looking + if title_col is not None and value_col is not None: + break + else: + col += 1 + + if title_col is not None and value_col is not None: + # Get the last non-empty row number of the current active sheet + last_non_empty_row = active_sheet.max_row + # Get the dictionary of the provided sw configurations dictionary, this can be either HD or DG + fw_sw_configs = sw_configs_dict['sw_configs'] + # Loop through the excel from row 2 since row 1 is the titles row until the last non-empty row + 1 since the + # range method does not include that last element so the last non-empty row will not be covered if there is + # not a +1. + for row in range(2, last_non_empty_row + 1): + config = active_sheet.cell(row=row, column=title_col).value + + if config is not None: + # Check if the software configuration that has been read from excel exists in dictionary that has + # been prepared in Dialin + if config.strip() in fw_sw_configs: + excel_config_value = active_sheet.cell(row=row, column=value_col).value + # Check if the value is an integer and it is a 1 or a 0 + # The only acceptable values are 1 for enable and 0 for disable + if isinstance(excel_config_value, int) and excel_config_value == 1 or excel_config_value == 0: + fw_sw_configs[config.strip()][1] = excel_config_value + else: + # If the value is not acceptable, set the default value that is sent down to firmware to 0 + # and write incorrect into the report so the user will notice that they had and empty cell + # or a cell with a non-acceptable value (i.e. 123). Color the cell as red. + fw_sw_configs[config.strip()][1] = 0 + write_to_excel(self._excel_workbook, sw_config_excel_tab, row, value_col, 'Incorrect Value', + color=RED) + # Save back the excel workbook with the changes + # This save function is the openpyxl save and not the internal save function that creates + # the save path. The save path already exists + self._excel_workbook.save(filename=excel_path) + status = True + + return status Index: leahi-dialin/utils/singleton.py =================================================================== diff -u --- leahi-dialin/utils/singleton.py (revision 0) +++ leahi-dialin/utils/singleton.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,45 @@ +########################################################################### +# +# Copyright (c) 2021-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file singleton.py +# +# @author (last) Quang Nguyen +# @date (last) 19-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 29-Apr-2021 +# +############################################################################ +from threading import Lock + + +class SingletonMeta(type): + """ + Thread-safe implementation of Singleton. + """ + + _instances = {} + + _lock: Lock = Lock() + _creation_attempts = 0 + """ + Synchronizes threads during first access to the Singleton. + """ + + def __call__(cls, *args, **kwargs): + """ + Possible changes to the value of the `__init__` argument do not affect + the returned instance. + """ + cls._creation_attempts += 1 + # TODO: Remove later - keep for debugging + # print("CAN interface constructor call counter: {0}".format(cls._creation_attempts)) + # First thread acquires the lock + with cls._lock: + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] Index: leahi-dialin/version.py =================================================================== diff -u --- leahi-dialin/version.py (revision 0) +++ leahi-dialin/version.py (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -0,0 +1,69 @@ +########################################################################### +# +# Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file version.py +# +# @author (last) Dara Navaei +# @date (last) 18-Jan-2023 +# @author (original) Peter Lucia +# @date (original) 18-Jun-2020 +# +############################################################################ +import subprocess + +VERSION = "0.9.0" + + +def get_branch(): + """ + Gets the current branch name in the current git repository + + @return: The current branch name, None if it can't be determined + """ + + try: + return subprocess.check_output("git rev-parse --abbrev-ref HEAD", shell=True).decode("utf-8").strip() + except subprocess.CalledProcessError: + return None + + +def get_last_commit(): + """ + Gets the latest commit in the current git repository + + @return: (str) the latest commit in the current git repository, None if it can't be determined + """ + try: + return subprocess.check_output("git rev-parse --short=7 HEAD", shell=True).decode("utf-8").strip() + except subprocess.CalledProcessError: + return None + + +def check_if_git_repo(): + """ + Checks if we're in a git repo or not to know if we can get the git branch and commit + + @return: True if in a git repo, False otherwise + """ + + return subprocess.call(["git", "branch"], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0 + + +branch = None +commit = None + +DEV_VERSION = VERSION + +if check_if_git_repo(): + branch = get_branch() + commit = get_last_commit() + DEV_VERSION += ".{0}".format(branch) + DEV_VERSION += ".{0}".format(commit) + + +if __name__ == '__main__': + print(VERSION) Index: setup.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- setup.py (.../setup.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ setup.py (.../setup.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -68,7 +68,7 @@ if __name__ == '__main__': check_if_git_repo() setuptools.setup( - name="dialin", + name="leahi-dialin", author="Peter Lucia", author_email="plucia@diality.com", description="The Diality Dialin API", Index: tests/dg_nvm_scripts.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/dg_nvm_scripts.py (.../dg_nvm_scripts.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/dg_nvm_scripts.py (.../dg_nvm_scripts.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -25,7 +25,7 @@ # Use cmd_get_dg_sw_config_record() to get the software configurations record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #dg.sw_configs.cmd_get_dg_sw_config_record() @@ -47,7 +47,7 @@ # Use cmd_get_dg_calibration_record_report() to get the calibration record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in you computer. #dg.calibration_record.cmd_get_dg_calibration_record_report() @@ -66,7 +66,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. dg.system_record.cmd_get_dg_system_record_report() @@ -82,7 +82,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. dg.usage_record.cmd_get_dg_usage_info_record() @@ -97,7 +97,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #dg.service_record.cmd_get_dg_service_record() Index: tests/dg_tests.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/dg_tests.py (.../dg_tests.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/dg_tests.py (.../dg_tests.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -636,7 +636,7 @@ def run_flush_mode(): complete_counter = 1 - f = open("/home/fw/projects/dialin/tests/flush_mode.log", "w") + f = open("/home/fw/projects/leahi-dialin/tests/flush_mode.log", "w") dg.hd_proxy.cmd_start_stop_dg_flush() #dg.cmd_dg_software_reset_request() Index: tests/hd_blood_leak_data.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/hd_blood_leak_data.py (.../hd_blood_leak_data.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/hd_blood_leak_data.py (.../hd_blood_leak_data.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -119,7 +119,7 @@ print(hd.blood_leak.get_blood_leak_emb_mode_command_response()) """ """ - f = open("/home/fw/projects/dialin/tests/blood_leak.log", "w") + f = open("/home/fw/projects/leahi-dialin/tests/blood_leak.log", "w") try: while True: Index: tests/hd_nvm_scripts.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/hd_nvm_scripts.py (.../hd_nvm_scripts.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/hd_nvm_scripts.py (.../hd_nvm_scripts.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -26,7 +26,7 @@ # Use cmd_get_hd_sw_config_record() to get the software configurations record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called HD_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #hd.sw_configs.cmd_get_hd_sw_config_record() @@ -48,7 +48,7 @@ # Use cmd_get_dg_calibration_record_report() to get the calibration record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called HD_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. hd.calibration_record.cmd_get_hd_calibration_record_report() @@ -67,7 +67,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #hd.system_record.cmd_get_hd_system_record_report() @@ -83,7 +83,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #hd.usage_record.cmd_get_hd_usage_info_record() @@ -98,7 +98,7 @@ # Use cmd_get_dg_system_record_report() to get the system record in an excel # This function gets an address to locate the report there (i.e. /home/fw/projects/) # It creates a folder called DG_NV_Records in the destination that is called - # If no address is provided, the default location is one folder above the dialin folder wherever it is installed + # If no address is provided, the default location is one folder above the leahi-dialin folder wherever it is installed # in your computer. #hd.service_record.cmd_get_hd_service_record() Index: tests/peter/test_dg_records.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/peter/test_dg_records.py (.../test_dg_records.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/peter/test_dg_records.py (.../test_dg_records.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -266,11 +266,11 @@ #print(dg.calibration_record.dg_calibration_record) #record_old_formatted = pprint.pformat(dg.calibration_record.dg_calibration_record, indent=4) - #with open("/home/fw/projects/dialin/tests/peter/dg_cal_record.log", 'w') as f: + #with open("/home/fw/projects/leahi-dialin/tests/peter/dg_cal_record.log", 'w') as f: # f.write(record_old_formatted) - #subprocess.call("meld /home/fw/projects/dialin/tests/peter/dialin_test_record_old.log" - # " /home/fw/projects/dialin/tests/peter/dialin_test_record_new.log", shell=True) + #subprocess.call("meld /home/fw/projects/leahi-dialin/tests/peter/dialin_test_record_old.log" + # " /home/fw/projects/leahi-dialin/tests/peter/dialin_test_record_new.log", shell=True) def test_dg_reset_record(): Index: tests/peter/test_gen_requirements.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/peter/test_gen_requirements.py (.../test_gen_requirements.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/peter/test_gen_requirements.py (.../test_gen_requirements.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -50,7 +50,7 @@ def gen_cmd_requirements(module) -> dict: """ - Generates requirements from dialin commands + Generates requirements from leahi-dialin commands @param module: the module to generate command requirements for @return: """ Index: tests/peter/test_hd_records.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/peter/test_hd_records.py (.../test_hd_records.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/peter/test_hd_records.py (.../test_hd_records.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -52,7 +52,7 @@ """ # store the old record after reading it from fw record_old_formatted = pprint.pformat(hd.calibration_record.hd_calibration_record, indent=4) - with open("/home/fw/projects/dialin/tests/peter/hd_cal_record.log", 'w') as f: + with open("/home/fw/projects/leahi-dialin/tests/peter/hd_cal_record.log", 'w') as f: f.write(record_old_formatted) """ @@ -120,7 +120,7 @@ sleep(0.2) record_old_formatted = pprint.pformat(hd.system_record.hd_system_record, indent=4) - with open("/home/fw/projects/dialin/tests/peter/hd_sys_record_old.log", 'w') as f: + with open("/home/fw/projects/leahi-dialin/tests/peter/hd_sys_record_old.log", 'w') as f: f.write(record_old_formatted) hd.system_record.hd_system_record['system_record']['top_level_pn'][1] = 'ASD-S123' Index: tests/peter/test_logging.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/peter/test_logging.py (.../test_logging.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/peter/test_logging.py (.../test_logging.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -59,8 +59,8 @@ hd.vvlogger.debug("VVLOGGER: TEST 1 (BEFORE DIALIN logger.debug() calls)") hd.test_debug() hd.vvlogger.debug("VVLOGGER: TEST 2 (AFTER DIALIN logger.debug() calls)") - # observe that no dialin logger.debug() messages appear in vvlogger.log - # and no dialin logger.debug() messages appear in the pycharm console (same behavior when running from terminal). + # observe that no leahi-dialin logger.debug() messages appear in vvlogger.log + # and no leahi-dialin logger.debug() messages appear in the pycharm console (same behavior when running from terminal). if __name__ == '__main__': test_create_logger() Index: tests/test_flush.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/test_flush.py (.../test_flush.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/test_flush.py (.../test_flush.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -108,7 +108,7 @@ def run_flush_mode(): complete_counter = 1 - f = open("/home/fw/projects/dialin/tests/flush_mode.log", "w") + f = open("/home/fw/projects/leahi-dialin/tests/flush_mode.log", "w") dg.hd_proxy.cmd_start_stop_dg_flush() #dg.cmd_dg_software_reset_request() Index: tests/test_hd_dg_fans.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/test_hd_dg_fans.py (.../test_hd_dg_fans.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/test_hd_dg_fans.py (.../test_hd_dg_fans.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -16,7 +16,7 @@ from dialin import HD, DG from dialin.hd.temperatures import HDTemperaturesNames -#from dialin.hd.fans import FansNames +#from leahi-dialin.hd.fans import FansNames from dialin.hd.switches import HDSwitchesNames from time import sleep Index: tests/test_voltages.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/test_voltages.py (.../test_voltages.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/test_voltages.py (.../test_voltages.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -47,7 +47,7 @@ x = 0 # create log file - with open("/home/fw/projects/dialin/tests/v_test.log", "w") as f: + with open("/home/fw/projects/leahi-dialin/tests/v_test.log", "w") as f: # write column header labels to log file header = "HD1.2V, HD3.3V, HD5V Logic, HD5V Sensors, HD24V, HD24V Regen, HDFPGA RefV, HDPBA RefV, HDAlmCurrHg, HDAlmCurrLg, HDAlmBckpCurr," + \ "DG1V FPGA, DG1.2V, DG1.8V Proc, DG1.8V FPGA, DGVRef, DGRef1, DGRef2, DG3.3V, DG3.3V Sensors, DG5V Logic, DG5V Sensors, DG5V P/S Gate Drvr, DG24V, DG24V Htr, DG24V Trim\n" Index: tests/unit_tests/test_msg_ids.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tests/unit_tests/test_msg_ids.py (.../test_msg_ids.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tests/unit_tests/test_msg_ids.py (.../test_msg_ids.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -25,7 +25,7 @@ def test_msg_ids(self): os.chdir("../../") - for root, dirs, files in os.walk("dialin/"): + for root, dirs, files in os.walk("leahi-dialin/"): for file in files: if file.endswith(".py"): path = os.path.join(root, file) Index: tools/build_common_defs.py =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tools/build_common_defs.py (.../build_common_defs.py) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tools/build_common_defs.py (.../build_common_defs.py) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -163,7 +163,7 @@ build_common_defs(dst_alarms_txt="/tmp/AlarmIds.txt", dst_common="/tmp/common", common_branch=common_branch, - dst_python=os.path.join(os.path.abspath("../"), "dialin/common/alarm_defs.py"), + dst_python=os.path.join(os.path.abspath("../"), "leahi-dialin/common/alarm_defs.py"), author="Peter Lucia", prefix_match="ALARM_ID_", cpp_header="AlarmDefs.h", @@ -185,7 +185,7 @@ build_common_defs(dst_alarms_txt="/tmp/MsgDefs.txt", dst_common="/tmp/common", common_branch=common_branch, - dst_python=os.path.join(os.path.abspath("../"), "dialin/common/msg_ids.py"), + dst_python=os.path.join(os.path.abspath("../"), "leahi-dialin/common/msg_ids.py"), author="Peter Lucia", prefix_match="MSG_ID_", cpp_header="MsgDefs.h", Index: tools/document.sh =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tools/document.sh (.../document.sh) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tools/document.sh (.../document.sh) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -35,7 +35,7 @@ cd $CURR_DIR fi cd ../ -sphinx-apidoc -o docs/source dialin +sphinx-apidoc -o docs/source leahi-dialin cd docs/ if [[ "$@" == "*--clean*" ]]; then make clean Index: tools/install_to_venv.sh =================================================================== diff -u -rb93016ee67204223658152cb2952e8ab16af1b94 -r07db7b5f01ad17d60c190e21574e1ed0552535ff --- tools/install_to_venv.sh (.../install_to_venv.sh) (revision b93016ee67204223658152cb2952e8ab16af1b94) +++ tools/install_to_venv.sh (.../install_to_venv.sh) (revision 07db7b5f01ad17d60c190e21574e1ed0552535ff) @@ -27,5 +27,5 @@ read -p "Enter the path to the repository's virtualenv directory: " -e -i "$venv" venv if [ ! -d "$venv" ]; then echo "Could not find $venv" ; exit 1; fi source venv/bin/activate -pip3 uninstall dialin +pip3 uninstall leahi-dialin pip3 install $whl_file