/* file: record.c G. Moody 13 October 1993 Last revised: 1 February 1996 record 6.0 Record data received over serial lines from HP Component Monitoring System Copyright (C) 1993, 1994, 1995, 1996 by George B. Moody. Permission is granted to reproduce and distribute copies of this program freely provided that this copyright notice and permission statement are attached to all copies. About this program ================== This program determines what signals and measurements are available from a Hewlett-Packard Component Monitoring System, and then logs all of the available data to disk continuously until it is stopped by the user. The monitor contains up to eight plug-in modules for monitoring physiologic signals (ECG, respiration, pressures, temperature, gases, etc.), and can be reconfigured by the user by inserting or removing modules at any time without restarting the monitor. This program, however, determines only which signals and measurements are available at the time it is started, and does not attempt to keep track of modules added later. Signals and measurements are considered available only if the associated module and the transducer are connected and the parameter is turned on; note that a signal or measurement need not be displayed on the monitor screen to be available (in fact, this program cannot determine what subset of available parameters is being displayed). Compiling this program ====================== To compile this program using Borland or Turbo `make', copy the appropriate versions of the external libraries `mecif.lib', `gfcl.lib', and `dbl.lib' (see below) into this directory, and type make See `makefile' in this directory for details on compilation. This program has been successfully compiled using Turbo C/C++ 3.0 and Borland C/C++ 4.0. Earlier versions of these compilers may work as well, but will require recompiling the external libraries as well as the program source. It should be fairly easy to port this program to Microsoft C (all of the libraries it uses can be compiled by Microsoft C/C++ compilers without changes; this program uses Borland/Turbo-specific functions for screen output, for far heap memory allocation, and for checking the available disk space, but close equivalents are provided with Microsoft C). This program makes use of three external libraries: 1. The MECIF library (mecif.lib), version A.01.10, available as Part No. M1046-9220C (Dec. 1992) from Hewlett-Packard. This library contains functions that support serial communication between the PC and the HP CMS monitor. The package includes an MS-DOS diskette (containing the library itself in source and binary forms, various header (.h) files, a demonstration program, and documentation files), and a manual titled "HP Component Monitoring System RS-232 Computer Interface Programming Guide (Option #J13)", referred to below as the HP Guide. The versions of the MECIF library on this diskette include a compiled copy of "rs232g.c" (also on this diskette), and are the preferred alternative to the version on the Hewlett-Packard diskette. All versions of the MECIF library are compiled using the large memory model; this diskette includes versions compiled for use with Turbo C/C++ 3.0 and Borland C/C++ 4.0. 2. The Greenleaf CommLib library (gfcl.lib), version 4.0, available from Greenleaf Software Inc., 16479 Dallas Parkway, Suite 570, Dallas, TX 75248 (telephone: +1 214 248 2561). This library contains functions invoked by `rs232g.c' (see above) that support low-level serial communications using many different types of PC serial ports, including "smart" serial cards; this program does not call any CommLib functions directly. The package includes MS-DOS diskettes (containing the library in source form, various header files, and example programs), and a manual titled "Greenleaf CommLib 4.0 Reference", referred to below as the CommLib manual. The sources can be compiled by many popular C compilers, including those from Borland and Microsoft; specify the primary compiler to be used when ordering CommLib, and the package will also include precompiled versions of the library for use with that compiler and various memory models. Be sure to use `gfcl.lib' (the large memory model version) with this program. CommLib is required in order to use the version of the revised MECIF library on this diskette; it is not required in order to use the Hewlett-Packard version. Although executable programs built using CommLib may be distributed without restriction, `gfcl.lib' may not be; this diskette therefore does not include a copy of `gfcl.lib'. 3. The DB library (dbl.lib), version 9.0 or later, provided on this diskette in binary form for Turbo C/C++ 3.0 and Borland C/C++ 4.0, and available in binary form for several other popular MS-DOS C compilers by anonymous FTP from penelope.mit.edu. This library includes functions that this program uses to create files of received data in a compact, portable, binary format. The DB Software Package, available from the author (George B. Moody, MIT Room E25-505, Cambridge, MA 02139; telephone: +1 617 253 7424), includes the DB library in C source form for MS-DOS, UNIX, and the Macintosh, and is accompanied by a large set of applications for physiologic signal processing, viewing, printing, and editing recordings such as those produced by this program, as well as a manual titled "ECG Database Programmer's Guide", referred to below as the DB Guide. Be sure to use `dbl.lib' (the large memory model version) with this program. Executable programs built using the DB library may be distributed provided that the following notice is incorporated in all copies: Portions of this software are Copyright (C) Massachusetts Institute of Technology 1992, and are incorporated by permission. It is possible to compile this program using the version of mecif.lib provided on the HP diskette and without using CommLib, and to use it with the standard PC serial ports (COM1 and COM2). The use of the revised MECIF library provided on this diskette, the CommLib library, and a "smart" serial card is strongly recommended as an alternative, permitting reliable operation at top speed while logging data to local drives. Without the use of CommLib and a smart serial card, this program is limited to logging data to network drives. (Characters arrive at 260 microsecond intervals on each of the 38400 baud serial lines. Serial interrupts are locked out for several milliseconds during local disk writes. Without a smart card, characters that arrive while writing to the disk are lost. Although the MECIF protocol incorporates limited error detection, there is no provision for error correction or retransmission of lost messages. Lacking a smart serial card, it is possible to log data to a network drive, since the network interface accepts data at PC bus speeds, avoiding the local disk's interrupt latency.) This operating mode has been tested using a 3Com Etherlink III Ethernet interface card, with Sun PC-NFS networking software used to access the network drives. The versions of the revised MECIF library provided on this diskette have been compiled for use with the DigiBoard PC/4e (or PC/8i) board, available from DigiBoard, Inc., 6400 Flying Cloud Drive, Eden Prairie, MN 55344 (telephone: +1 612 943 9020). Other serial devices supported by CommLib (hence the revised MECIF library) include smart cards from Arnet and Star Gate; a one-line change in `rs232g.c' is required in order to recompile a version of `mecif.lib' for use with any of these cards. The revised MECIF library also incorporates more robust error-recovery that avoids the need to reboot the monitor in the event of interrupted transmissions, a problem that occurs regularly when using the version of mecif.lib from the HP diskette. Running this program ==================== The file `readme.doc' in this directory is a user's manual for this program. Refer to that file for details omitted in the brief description below. To use this program: 1. Connect the HP monitor to the PC using standard serial printer cables (see the wiring diagram on page 2-2 of the HP Guide). Up to four lines may be used. 2. Turn on both the monitor and the PC. 3. Run the program by typing record or record FILE (where FILE is the name of a configuration file such as `record.ini'). This program checks the available serial lines and sets the speeds of the PC's ports to match those of the monitor. The best results will be obtained using the highest available speeds, permitting the maximum amount of data to be retrieved. If the total available bandwidth is insufficient to permit the monitor to transmit all signals and measurements, this program attempts to obtain as many signals as possible, and then as many measurements as possible using the remaining bandwidth. If this program is used routinely in a setting in which the bandwidth is insufficient, it may be worth modifying the code to permit the user to select which signals and measurements are to be recorded. The monitor may have one or two RS232 serial cards installed in it (two are necessary in order to provide suffficient bandwidth to acquire all signals in most cases). Each RS232 card provides two ports. The lower ports are designated ports 1 and 3, and support communications at 9600, 19200, and 38400 baud; the upper ports (2 and 4) are restricted to 9600 baud (or 19200 baud if the lower port on the same card is not running at 38400 baud). The TX/RX order should be Low/High for all connected ports. These settings cannot be changed from the PC; refer to the HP Guide (pp. 2-4 to 2-9) for information on changing them using the monitor's controls. The following baud rate settings are recommended (choose the first set that works from the list): Port 1 Port 2 Port 3 Port 4 38400 9600 38400 9600 * maximum bandwidth 38400 38400 ** maximum bandwidth with only two lines 19200 19200 19200 19200 * use with modems or long cables if 38400 baud can't be used 38400 9600 maximum bandwidth with only one RS232 card 38400 maximum bandwidth with only one line * Note that the revised MECIF library must be used in order to make use of three or four ports (the standard library supports only two). In some monitor configurations, the use of three or four ports fails for unknown reasons (apparently not related to this program or to the PC hardware). For this reason, the default configuration (**) uses only ports 1 and 3. Any of the first three choices provides sufficient bandwidth in most cases. If you are using a version of this program compiled for use with a DigiBoard PC/4e or PC/8i serial card, connections should be made using ports P1, P2, P3, and P4 (which should be configured as COM5, COM6, COM7, and COM8) of the DigiBoard interface, and the DigiBoard driver (currently `xidos5.sys') should be loaded. Otherwise, use the PC serial ports COM1 and COM2 (depending on your hardware, COM3 and COM4 may or may not be usable as well). It makes no difference which PC port is connected to which monitor port. For details on setting options in this program, see the file `record.ini' in this directory, which is a sample configuration file. This program sets its options using the first available source from the following: 1. a file named on the command line (for example, `record myconfig.ini') 2. a file named in the environment variable RCONFIG (for example, by the command `set RCONFIG=c:\record\myconfig.ini') 3. a file named `record.ini' found in the same directory as `record.exe' 4. the compiled-in defaults shown below Thus a file named on the command line takes precedence over a file named in RCONFIG, and so on. About the output ================ Each time it runs, this program reads and updates an `index file'. The index file may be named in the configuration file (by default, it is `record.idx' in the same directory as `record.exe'). The index file is a text file. Each line in it contains data for one run of `record'. The first field of each line is the run index number. The run index number is the name of the directory in which the log files for this run are collected (see below). By default, this directory is placed in c:\, but any other location may be specified using the configuration file. If space on the drive containing the log directory becomes nearly exhausted, and a "spillover" directory (presumably on a different drive) has been specified in the configuration file, `record' switches to that directory. Only one "spillover" directory may be specified; if none is specified, `record' does not use a "spillover" directory. When almost all of the available space has been used, `record' exits (it avoids writing on the last few blocks). The log files contain the data retrieved from the monitor. At regular intervals (10 minutes by default), this program closes the current set of log files and opens a new set; this strategy limits the amount of data lost if there is an interruption in power or other system failure. Each set of log files is identified by a five-digit sequence number contained within the names of the files. The first three characters of the file name are the index number (modulo 1000), and the next five are the sequence number (beginning with 00001). (In the unlikely event that more than 99,999 sets of log files are created, the index number is incremented and the files continue to be generated in numerical order.) The extension (`.dat', `.al', or `.nu') indicates what type of data will be found in the file (sampled signals, annotations of alarms and signal connections/disconnections, or numerics). An ASCII header (`.hea') file accompanies each set, and contains the date and time (from the system clock) at which the files in the set were created, along with information necessary to read the `.dat' and `.al' files using DB application programs (such as `view', included in this directory). Generally, log files of different types will have different lengths, but all log files of any given type should have approximately the same length, with the following exceptions: 1. The last file of each type may be shorter than the others. 2. If alarms or `inop' conditions occur, the corresponding `.al' and `.txt' files will be longer than the others. 3. If a module was removed from the monitor during the run, the log files obtained during that period may be shorter than the others. 4. Log files open when messages were lost may be shorter than the others. Except for the first case, careful attention should be paid to any log files that differ significantly in length from others of the same type. This program also logs its run-time errors in an error log (by default, `record.log', in the same directory as `record.exe'). The error log should be consulted after each run. */ #include #include #include #include "meciflib.h" #include "database.h" #undef DIRECTORY /* to avoid conflict with incompatible definition in dir.h */ #include /* ctrlbrk, getdfree */ #include /* cprintf, text colors, etc. */ #include /* setdisk, chdir */ #include /* farmalloc */ #include "db.h" /* DB library data structures and prototypes */ #include "ecgcodes.h" /* Annotation types for DB library putann() function */ #define VERSION "6.0" /* version number of this program */ #define min(A,B) (((A) < (B)) ? (A) : (B)) /* Prototypes for functions defined below. */ void init(int argc, char **argv); void getdata(void); void cleanup(int exit_code); void addcon(i_16 port, int com); int findcon(i_16 port); int try_com(int com, unsigned speed); int cap(unsigned speed); void assign_com(int index, int cost); void deassign_com(int index, int credit); int wave_priority(int index); void make_timestamp(void); void show_fixup_message(void); void openlogerr(char *file_name); void flush_buf(void); int openlog(void); void resynch(void); void main(int argc, char **argv) { init(argc, argv); /* read config, open communications with monitor */ getdata(); /* retrieve and record data from monitor */ cleanup(0); /* close files, reset hardware */ } /* The `getconf' macro is used by init() (below) to check a line of input (already in `buf', defined within init) for the string named by getconf's first argument. If the string is found, the value following the string (and an optional `:' or `=') is converted using sscanf and the format specifier supplied as getconf's second argument, and stored in the variable named by the first argument. Finally, if debug is non-zero, getconf prints the name of the variable and its new value on the standard error output. */ #define getconf(a, fmt) if (p = strstr(buf, #a)) { \ sscanf(p, #a "%*[=: ]" fmt, &a); \ if (debug) fprintf(stderr, #a " = " fmt "\n", a); \ } /* Configurable parameters. */ int debug = 0; /* if non-zero, print debugging messages */ int error_show = 1; /* if non-zero, display run-time errors */ unsigned file_duration = 600;/* length of log files in seconds */ int test_mode = 0; /* if non-zero, skip interactive setup */ unsigned com_speed[5] = { 0U /* unused */, 38400U, 38400U, 0U, 0U }; /* baud rates for serial lines */ unsigned com_errors[5]; /* error counts for serial lines */ char error_log[80]; /* name of error log */ char index_file[80]; /* name of index file */ char logdir[80]; /* the primary log file directory */ char log2dir[80]; /* the secondary log file directory */ char notice[5][80]; /* a message of up to 5 lines to be posted on-screen while this program is running */ /* Files used by this program (excluding those created by the DB library). */ FILE *debugfp = stderr; /* destination for debugging messages */ FILE *errorlogfp; /* run-time error log */ FILE *indexfp; /* file containing index numbers and patient data */ FILE *nufp; /* numeric data log file */ /* Other global variables. */ char *pname; /* the name of this program, extracted from argv[0] */ int indexno; /* index (identification) number for current run */ long seqno; /* file sequence number + 10^5 * (indexno mod 1000) */ int porta; /* com number of fastest port (1, 2, 3, or 4) */ int portb; /* com number of 2nd-fastest port (0 if only 1 port active) */ int portc; /* com number of 3rd-fastest port (0 if <3 ports active) */ int portd; /* com number of slowest port (0 if <4 ports active) */ int bwtoohigh; /* non-zero if bandwidth requirements of available data exceed the capacity of available connections */ int initializing; /* 1 while executing in init, 0 afterwards */ int nresp = 0; /* number of responses to tune requests received */ int nwaves = 0; /* number of cooked wave message types in tune lists */ long ncw, nnu, nws, nal;/* numbers of cooked wave, numeric, wave support, and alarm/inop messages logged */ int nmeas = 0; /* number of numeric measurement types to be logged */ char numap[48][8]; /* labels of numeric measurements to be logged */ int drive, altdrive; /* drive numbers for drives containing logdir and log2dir (2 = c:, 3 = d:, etc.) */ int error_flag; /* if non-zero, an error occurred since last openlog() */ double clusters_per_hour; /* estimated rate of consumption of disk space */ /* The date and time strings below can be set by invoking make_timestamp() whenever it's necessary to use one of them. */ char date_string[12]; char time_string[10]; /* The following arrays are indexed by logical com numbers (1, 2, 3, or 4). If using the revised MECIF library, these are mapped to PC COM numbers in "rs232g.c"; otherwise, they correspond to the physical ports COM1, COM2, COM3, and COM4. The first element in each array (with subscript 0) is unused. */ int bandwidth[5]; /* expected rate of data received (bytes/sec) */ int capacity[5]; /* capacity of connections (bytes/sec) */ int connected[5]; /* non-zero for active connections */ i_16 cid[5]; /* MECIF con ids */ i_16 pid[5] = { -1, -1, -1, -1, -1 }; /* MECIF port ids */ LibTuneList tune_list[5]; /* see below */ /* About the tune lists ==================== A LibTuneList consists of a pointer to an array of MsgIdTyps, and a counter indicating the length of the array; it specifies what parameters are to be retrieved over a given com port. tune_list (declared above) is an array of LibTuneLists, one for each com port (as noted above, tune_list[0] is unused). The tune_list_msg_id array (below) is an array of MsgIdTyps, but the members of tune_list address only subarrays of it. The entire tune_list_msg_id array is a list of all of the parameters that are supported by the monitor, obtained using the par_list_req function of the MECIF library. For any given monitor configuration, only a subset of the supported parameters is actually available. The init function below determines which parameters are available and then rearranges tune_list_msg_id so that all parameters to be retrieved using a given com port occupy contiguous subarrays within the larger array; it then sets the tune_list[] pointers and counters to address these subarrays. The three arrays that follow tune_list_msg_id are not part of the tune lists themselves, but they are organized in parallel with tune_list_msg_id and contain additional information about the parameters. */ #define NMID 128 MsgIdTyp tune_list_msg_id[NMID];/* list of parameters supplied by monitor */ char tune_list_ascii_id[NMID][12]; /* parameter names (e.g. "PRESS 1") */ char tune_list_label[NMID][12]; /* assigned names (e.g. "ABP") */ char tune_list_active[NMID]; /* status (0: not available; 1 or 2: to be retrieved via com1 or com2; -1: available but not being retrieved) */ /* Wave priorities. These are used to determine which signals are to be recorded if there is insufficient bandwidth to record all of them. A signal with a numerically high priority will be recorded in preference to one of lower priority. */ int ecg_priority[4] = { 0 /* unused */, 10, 10, 10 }; int press_priority[7] = {0 /* unused */, 5, 5, 5, 3, 3, 3 }; int resp_priority = 7; int pleth_priority = 4; int co2_priority = 6; int other_priority = 2; /* DB library-related variables. */ #define BSIZE 1024 int *vbuf; /* circular buffer of samples to be recorded */ char recname[9]; /* name of current DB record (seqno, as 8-digit ASCII) */ int nsig; /* number of signals being recorded */ int tspf; /* total number of samples per frame */ int smap[MAXSIG]; /* frame offset of first sample in each signal */ long record_length; /* length of generated DB records in samples */ MsgIdTyp fsmap[MAXSIG]; /* fsmap[i] is the MsgId of cooked wave and wave support messages related to signal i, used by findsig() to get the value of i given a message. */ DB_Siginfo si[MAXSIG]; /* si[i] contains information for signal i (filled in by init() and used by openlog() when creating the `.dat' and `.hea' files) */ /* About the patient data ====================== This information is acquired from the user by init, logged in the index file together with the index number, and displayed on-screen for the duration of the recording. To make its use by nursing staff as painless as possible, this program does not enforce the use of any particular formats for these strings; they are needed only to reduce the possibility of losing track of the source of the recording, and they should be verified and corrected if necessary by comparison with the patient's chart at the time data from the chart are entered. At that point, it is appropriate to enforce the use of standard formats. */ char patient_name[80]; /* as on the patient's chart */ char patient_mrn[20]; /* medical record number */ char start_date[20]; /* date at beginning of recording */ char start_time[20]; /* time at beginning of recording (may not match system time -- needed later to synchronize nurse's notes, etc., with data gathered by this program) */ /* Function `init' reads the configuration file, opens the communications ports, determines what information is to be retrieved from the monitor, acquires the patient data, and initializes the reading loop. */ void init(argc, argv) int argc; char *argv[]; { char buf[256], *cfname = NULL, ini_file[80], *p, *getenv(); FILE *config; int a, c, i, j, k, npar; LibRet *status; MsgIdTyp tm; ParList *plp; initializing = 1; /* Extract the name of this program from argv[0]. */ pname = argv[0] + strlen(argv[0]); while (pname >= argv[0] && *pname != '\\' && *pname != ':') { if (*pname == '.') *pname = '\0'; /* strip off extension */ pname--; } pname++; vbuf = (int *)farmalloc((long)sizeof(int)*(long)BSIZE*(long)16); if (vbuf == NULL) { fprintf(stderr, "%s: insufficient memory\n", pname); exit(1); } /* Show the opening screen. */ clrscr(); textbackground(GREEN); textcolor(YELLOW); for (i = 1; i < 26; i++) { gotoxy(1, i); cprintf(" " " "); } gotoxy(5, 9); cprintf("PC interface to the Hewlett-Packard Component Monitoring System"); gotoxy(30, 11); cprintf("%s %s (%s)", pname, VERSION, __DATE__); textcolor(WHITE); gotoxy(7, 13); cprintf("Author: George B. Moody"); gotoxy(15, 14); cprintf("Harvard-MIT Division of Health Sciences and Technology"); gotoxy(15, 15); cprintf("MIT Room E25-505A"); gotoxy(15, 16); cprintf("Cambridge, MA 02139 USA"); gotoxy(15, 18); cprintf("Telephone: +1 617 253 7424"); gotoxy(15, 19); cprintf("Internet: george@mit.edu"); gotoxy(5, 21); cprintf("Portions of this software are Copyright (C) Massachusetts" " Institute of"); gotoxy(5, 22); cprintf("Technology 1992, and are incorporated by permission."); gotoxy(1, 24); cprintf("Press a key to begin: "); if (getch() == '\033') { clrscr(); textbackground(BLACK); textcolor(WHITE); for (i = 1; i < 26; i++) { gotoxy(1, i); cprintf(" " " "); } exit(0); } clrscr(); /* Generate the default names of the index file, the error log, and the configuration file from argv[0]. Note that the `.exe' has been stripped off already (above). */ sprintf(index_file, "%s.idx", argv[0]); sprintf(error_log, "%s.log", argv[0]); sprintf(ini_file, "%s.ini", argv[0]); /* Set the default initial part of the log file directory name. The index number will be appended to this string. */ strcpy(logdir, "c:\\"); /* Set the default initial part of the alternate ("spillover") directory name. On a system with 2 disk drives, this might be "d:\\"; it's best to set this in record.ini, however, so that the configuration is more readily visible to the user. */ strcpy(log2dir, ""); /* Get the name of the configuration file, from the command line ... */ if (argc > 1) cfname = argv[1]; /* ... or from the environment. */ else if ((cfname = getenv("RCONFIG")) == NULL) cfname = ini_file; /* Unless its name is missing or empty, open the configuration file. */ if (cfname && *cfname) { char *p; if ((config = fopen(cfname, "rt")) == NULL) { gotoxy(2, 12); textcolor(YELLOW); cprintf("%s: can't read configuration file %s\n", pname, cfname); gotoxy(1, 25); textcolor(WHITE); exit(1); } /* Read the configuration file a line at a time. */ while (fgets(buf, sizeof(buf), config)) { /* Skip comments (empty lines and lines beginning with `#'). */ if (buf[0] == '#' || buf[0] == '\n') continue; /* Set parameters. Each `getconf' below is executed once for each non-comment line in the configuration file. */ getconf(debug, "%d"); getconf(com_speed[1], "%u"); getconf(com_speed[2], "%u"); getconf(com_speed[3], "%u"); getconf(com_speed[4], "%u"); getconf(file_duration, "%u"); getconf(test_mode, "%d"); getconf(error_log, "%s"); getconf(error_show, "%d"); getconf(index_file, "%s"); getconf(logdir, "%s"); getconf(log2dir, "%s"); getconf(notice[0], "%s"); getconf(notice[1], "%s"); getconf(notice[2], "%s"); getconf(notice[3], "%s"); getconf(notice[4], "%s"); getconf(ecg_priority[1], "%d"); getconf(ecg_priority[2], "%d"); getconf(ecg_priority[3], "%d"); getconf(press_priority[1], "%d"); getconf(press_priority[2], "%d"); getconf(press_priority[3], "%d"); getconf(press_priority[4], "%d"); getconf(press_priority[5], "%d"); getconf(press_priority[6], "%d"); getconf(resp_priority, "%d"); getconf(pleth_priority, "%d"); getconf(co2_priority, "%d"); getconf(other_priority, "%d"); } } fclose(config); /* Open the error log if specified. */ if (*error_log && (errorlogfp = fopen(error_log, "at")) == NULL) { gotoxy(2, 12); textcolor(YELLOW); cprintf("%s: can't open error log `%s'\n", pname, error_log); textcolor(WHITE); gotoxy(1, 25); exit(1); } /* Save debugging messages in error log if specified. */ if (debug && errorlogfp) debugfp = errorlogfp; /* Initialize the communications port(s). */ for (i = 1; i < 5; i++) { while (com_speed[i] && try_com(i, com_speed[i]) == 0 && try_com(i, com_speed[i] = 38400U) == 0 && try_com(i, com_speed[i] = 19200U) == 0 && try_com(i, com_speed[i] = 9600U) == 0) { gotoxy(2, 10); textcolor(YELLOW); cprintf("Please check the com%d cable.", i); textcolor(WHITE); gotoxy(2, 12); cprintf("Press to try com%d again,", i); gotoxy(2, 13); cprintf(" or to continue without using com%d: ", i); if (getch() == '\033') com_speed[i] = 0; else { clrscr(); gotoxy(2, 8); cprintf("Testing com%d connection (please wait) ...", i); } } clrscr(); } if (com_speed[1] == 0 && com_speed[2] == 0 && com_speed[3] == 0 && com_speed[4] == 0) { cleanup(0); gotoxy(2, 12); textcolor(YELLOW); cprintf("This program cannot run, " "because serial communications cannot be established."); textcolor(WHITE); gotoxy(1, 25); exit(1); } capacity[0] = porta = portb = portc = portd = 0; for (i = 1; i < 5; i++) capacity[i] = cap(com_speed[i]); for (i = 1; i < 5; i++) if (capacity[i] > capacity[porta]) porta = i; for (i = 1; i < 5; i++) if (capacity[i] > capacity[portb] && i != porta) portb = i; for (i = 1; i < 5; i++) if (capacity[i] > capacity[porta] && i!=porta && i!=portb) portc = i; for (i = 1; i < 5; i++) if (capacity[i]>capacity[porta]&&i!=porta&&i!=portb&&i!=portc)portd=i; cid[porta] = connect_req(pid[porta], SRC_ParServer, 60, 0, DISABLE); addcon(cid[porta], porta); if (portb) { cid[portb] = connect_req(pid[portb] ,SRC_ParServer, 60, 0, DISABLE); addcon(cid[portb], portb); } if (portc) { cid[portc] = connect_req(pid[portc], SRC_ParServer, 60, 0, DISABLE); addcon(cid[portc], portc); } if (portd) { cid[portd] = connect_req(pid[portd], SRC_ParServer, 60, 0, DISABLE); addcon(cid[portd], portd); } /* Once we begin making requests, rx_mecif must be called frequently to receive the monitor's responses (at least when they are expected), or communications will fail at some point. Originally, I tried to use detune_all_req and disconnect_req after gathering the data needed to construct the tune lists, so that the patient data could be gathered interactively without having to call rx_mecif during that time. I didn't succeed, apparently because using these functions doesn't reset the MECIF library to its original state, and subsequent attempts to reopen communications fail as a result. I finally tried disabling mirror requests from the monitor to the PC entirely (as specified by the penultimate parameter in the connect_req argument lists above), and using a long interval between PC-initiated mirror requests. This allows me not to call rx_mecif during interactive input. Why not simply do the interactive input first? I think this would be a poor design from the user's perspective, since (1) the user has to wait to see if the recording process starts properly in any case, and (2) if it doesn't start properly, this program must be restarted (since, as noted above, the MECIF library doesn't seem to provide a way of returning to its initial state) and the data entered by the user would have to be re-entered. */ gotoxy(2, 10); if (par_list_req(cid[porta]) != SUCCESS) { /* This seems to happen only if the response of the monitor to a previous par_list_req was not completely received, as might happen if this program was run previously and interrupted (see below). There doesn't seem to be anything we can do about this problem from the PC end. This problem has not been observed when using the the revised MECIF library (see above). */ cprintf("This computer cannot obtain data from the" " monitor until the monitor has been"); gotoxy(2, 11); cprintf("restarted. Please restart the monitor" " and run this program again."); cleanup(0); exit(1); } cprintf("Identification of the signals and measurements" " available from the HP"); gotoxy(2, 11); cprintf("monitor usually takes about 30 seconds." " If this computer pauses for a"); gotoxy(2, 12); cprintf("longer period, press any key to interrupt it and try again."); gotoxy(2, 14); cprintf("Identifying available parameters (please wait) ..."); while (1) { status = rx_mecif(); /* read a message */ if (status->cmd == PAR_LIST_RSP) { cprintf("."); if ((status->ret).pret == NULL) continue; /* wait until entire list has been received */ plp = (ParList *)((status->ret).pret); break; } /* It seems like a really bad idea to interrupt this loop, since doing so while the monitor is answering a PAR_LIST_REQ may result in leaving the monitor unwilling to communicate further until it's restarted (see error message above). But if we can't interrupt this loop, we have to restart the PC *and* the monitor when the monitor doesn't answer, and it takes longer to restart the PC than the monitor. So we trust the user not to interrupt the loop except when the monitor doesn't answer. */ if (kbhit()) { show_fixup_message(); cleanup(0); exit(1); } } npar = plp->num_of_par; for (i = 0; i < npar; i++) { tune_list_msg_id[i] = plp->ident[i].msg_id; if (nwaves == 0 && i > 0 && tune_list_msg_id[i].MsgType != tune_list_msg_id[0].MsgType) nwaves = i; strncpy(tune_list_ascii_id[i], plp->ident[i].ascii_id, 8); } if (debug) { fprintf(debugfp, "Available parameters:"); for (i = 0; i < npar; i++) fprintf(debugfp, "%u %12s", plp->ident[i].msg_id.SourceNo, plp->ident[i].ascii_id); } /* Determine what signals and measurements are active on the monitor. Begin by checking the wave support messages. */ for (i = 0; i < nwaves; i += 10) { tune_list[porta].num_tune = min(nwaves-i, 10); tune_list[porta].msg_id_ptr = &tune_list_msg_id[i]; nresp = 0; if (tune_lib_req(cid[porta], &tune_list[porta], SINGLE) != SUCCESS) cleanup(1); do { status = rx_mecif(); /* read a message */ if (kbhit()) { show_fixup_message(); cleanup(0); exit(1); } } while (status->cmd != SINGLE_TUNE_RSP || status->ret.pret == NULL); /* The following message loop is here so that the responses to the single requests can be received before requesting the next set. It's not obvious how to be sure that all of the responses have been received, particularly for parameters other than wave support and numerics. The rx_lib_* functions below update nresp, but it appears that only rx_lib_ws and rx_lib_nu actually get invoked as a result of messages received here. According to the HP Guide (page 5-17), the MECIF library is "sensitive" to the frequency of rx_mecif calls. It's not clear what this means, but the 50 msec delay in this loop seems to keep the library happy. A longer delay risks losing messages, and no delay at all seems to have a similar effect for reasons unknown. */ for (j = 0; j < 40 && nresp < tune_list[porta].num_tune; j++) { status = rx_mecif(); delay(50); } if (j == 40) textcolor(YELLOW); cprintf("."); if (j == 40) textcolor(WHITE); } /* Skip the cooked wave messages for now (the wave support messages tell us what we need to know about these). Continue by checking the remaining parameters available from the monitor. */ for (i = 2*nwaves; i < npar; i += 10) { tune_list[porta].num_tune = min(npar-i, 10); tune_list[porta].msg_id_ptr = &tune_list_msg_id[i]; nresp = 0; if (tune_lib_req(cid[porta], &tune_list[porta], SINGLE) != SUCCESS) cleanup(1); do { status = rx_mecif(); /* read a message */ if (kbhit()) { show_fixup_message(); cleanup(0); exit(1); } } while (status->cmd != SINGLE_TUNE_RSP || status->ret.pret == NULL); for (j = 0; j < 40 && nresp < tune_list[porta].num_tune; j++) { status = rx_mecif(); delay(50); } if (j == 40) textcolor(YELLOW); cprintf("."); if (j == 40) textcolor(WHITE); } clrscr(); if (debug) { gotoxy(1,1); cprintf("nwaves = %d\r\n", nwaves); cprintf(" Press any key to continue:"); getch(); clrscr(); } gotoxy(41, k = 1); k++; cprintf("Measurements:"); for (i = 2*nwaves; i < 127; i++) { int cost; if (tune_list_msg_id[i].MsgType == tune_list_msg_id[2*nwaves].MsgType){ if (strstr(tune_list_ascii_id[i], "PRESS") || strstr(tune_list_ascii_id[i], "NBP")) cost = 55; else cost = 39; if (tune_list_active[i]) assign_com(i, cost); /* may modify tune_list_active[i] */ if (tune_list_active[i] > 0) strcpy(numap[nmeas++], tune_list_label[i]); if (k >= 12) { gotoxy(1, 24); cprintf("Press any key to continue: "); getch(); gotoxy(1, 24); clreol(); for (k = 2; k < 12; k++) { gotoxy(41, k); clreol(); } k = 2; } if (debug || tune_list_active[i]) { gotoxy(41, k++); cprintf("%8s -> %12s (%d)", tune_list_ascii_id[i], *tune_list_label[i]? tune_list_label[i]: "*** ??? ***", tune_list_active[i]); } } else if ((tune_list_msg_id[i].SourceId == SRC_AlMgr) && (tune_list_msg_id[i].ChannelId == CHA_AlText)) { assign_com(i, 61); /* cost is zero except when an alarm occurs */ gotoxy(2, 10); cprintf("Alarms (%d)", tune_list_active[i]); } else if ((tune_list_msg_id[i].SourceId == SRC_AlMgr) && (tune_list_msg_id[i].ChannelId == CHA_InText)) { assign_com(i, 63); /* cost is zero except when an "inop" occurs */ gotoxy(20, 10); cprintf("Inops (%d)", tune_list_active[i]); } #if 0 else if ((tune_list_msg_id[i].SourceId == SRC_AlMgr) && (tune_list_msg_id[i].ChannelId == CHA_AlStat)) assign_com(i, 13); #endif } gotoxy(2, 1); cprintf("Signals:"); /* This code is not ideal, since it relies on the (observed) sequence of parameters: 0 - nwaves-1 wave support nwaves - 2*nwaves-1 cooked waves 2*nwaves - 127 numerics, other A better solution would be to check the last three characters in the ascii_id strings to determine the parameter type and to match the wave support and cooked wave parameters, rather than relying on the observed order. */ for (i = 0, k = 2; i < nwaves; i++) { int cost; if (strstr(tune_list_ascii_id[i], "ECG")) cost = 1409; /* 33 (wave support) + 43*32 (cooked wave) */ else cost = 641; /* 33 (wave support) + 19*32 (cooked wave) */ if (tune_list_active[i]) { assign_com(i, cost); /* i = index of the wave support message */ while (tune_list_active[i] < 0) { /* Not enough bandwidth available -- check if a lower priority waveform can be "bumped" from the list. */ int l, m, p, pmin; for (l = 0, m = i, pmin = wave_priority(i); l < i; l++) { if (tune_list_active[l] > 0 && (p = wave_priority(l)) < pmin) { pmin = p; m = l; } } if (m < i) { int credit; if (strstr(tune_list_ascii_id[m], "ECG")) credit = 1409; else credit = 641; fprintf(errorlogfp, "deassigning %s\n", tune_list_ascii_id[m]); deassign_com(m, credit); assign_com(i, cost); } else break; } } } for (i = 0, k = 2; i < nwaves; i++) { /* The next statement assigns the cooked wave to the same state as its associated wave support. */ tune_list_active[i+nwaves] = tune_list_active[i]; if (k >= 12) { gotoxy(1, 24); cprintf("Press any key to continue: "); getch(); gotoxy(1, 24); clreol(); for (k = 2; k < 12; k++) { gotoxy(2, k); clreol(); } k = 2; } if (debug || tune_list_active[i]) { gotoxy(2, k++); cprintf("%8s -> %12s (%d)", tune_list_ascii_id[i], *tune_list_label[i] ? tune_list_label[i] : "*** ??? ***", tune_list_active[i]); } } record_length = file_duration * 125L; /* Warn if the communications capacity is exceeded. *** Code to allow the user to choose which parameters to retrieve should go here *** */ if (bwtoohigh) { textcolor(YELLOW); gotoxy(2, 15); cprintf("Bandwidth insufficient to receive signals or measurements" " marked with (-1)"); textcolor(WHITE); } /* Set up the DB library signal information structures for the signals to be retrieved. These are already in sorted order in the tune_list_* arrays. */ for (i = nsig = 0; i < nwaves && nsig < MAXSIG; i++) { char *p; if (tune_list_active[i] <= 0) continue; fsmap[nsig] = tune_list_msg_id[i]; for (p = &tune_list_label[i][SPI_LABEL_LENGTH-2]; *p == ' '; p--) ; si[nsig].desc = tune_list_label[i]; *(p+1) = '\0'; /* discard trailing spaces */ if (strstr(tune_list_ascii_id[i], "ECG")) si[nsig].spf = 4; /* 4 samples per frame per ECG signal */ else si[nsig].spf = 1; /* 1 sample per frame per other signal */ smap[nsig] = tspf; tspf += si[nsig].spf; si[nsig].fmt = 212; /* we'll pack 2 12-bit samples into 3 bytes */ /* The lines below are a crude attempt to set rough calibrations for some common signals, based on observed values while running in `demo' mode. I haven't figured out how to get accurate calibration data from the monitor yet. */ if (strcmp(si[nsig].desc, "ABP") == 0) { si[nsig].gain = 20; si[nsig].baseline = -1500; si[nsig].units = "mmHg"; } else if (strcmp(si[nsig].desc, "PAP") == 0) { si[nsig].gain = 75; si[nsig].baseline = -1500; si[nsig].units = "mmHg"; } else si[nsig].gain = 2000; si[nsig++].adcres = 12; /* The `fname' member of the structure will be set by openlog(). */ } /* Construct the final tune lists. First we sort the tune_list_* arrays by the port number. This sort must be non-destructive (i.e., the relative order of elements with the same port numbers must not be changed). */ for (i = 0; i < NMID; i++) { int swaps; for (j = 1, swaps = 0; j < NMID-i; j++) if (tune_list_active[j-1] > tune_list_active[j]) { k = tune_list_active[j-1]; tune_list_active[j-1] = tune_list_active[j]; tune_list_active[j] = k; tm = tune_list_msg_id[j-1]; tune_list_msg_id[j-1] = tune_list_msg_id[j]; tune_list_msg_id[j] = tm; swaps++; } if (swaps == 0) break; /* sorting is completed when no more swaps */ } /* Now we scan the sorted arrays to determine the first element belonging to each list, and how many elements belong to each list. */ for (i = j = k = 0; i < NMID; i++) if (tune_list_active[i] > j) { tune_list[j].num_tune = i - k; /* set length of previous list */ j = tune_list_active[i]; tune_list[j].msg_id_ptr = &tune_list_msg_id[k = i]; /* ... and the pointer to the beginning of this list */ } tune_list[j].num_tune = i - k; for (i = 1; i < 5; i++) { gotoxy(1, i+10); if (com_speed[i]) cprintf("COM%d: %u baud, capacity %d bytes/second" " (%d used for %d item%s, %d free)", i, com_speed[i], capacity[i], bandwidth[i], tune_list[i].num_tune, tune_list[i].num_tune == 1 ? "" : "s", capacity[i] - bandwidth[i]); else cprintf("COM%d: not available", i); } /* Open the index file and determine the next index number. */ if (indexfp = fopen(index_file, "rt")) { while (fgets(buf, sizeof(buf), indexfp)) { if (buf[0] == '#') continue; sscanf(buf, "%d", &indexno); } indexno++; fclose(indexfp); } /* Create the log file directory (or directories). */ p = logdir + strlen(logdir); if (*(p-1) != '\\') *p++ = '\\'; /* make sure the prefix ends in `\' */ sprintf(p, "%03d", indexno); if (mkdir(logdir) < 0) { /* can't create it, try incrementing indexno */ sprintf(p, "%03d", ++indexno); if (mkdir(logdir) < 0) { gotoxy(1, 24); textcolor(YELLOW); cprintf("%s: can't create log directory `%s'", pname, logdir); textcolor(WHITE); cleanup(2); } } if (log2dir[0]) { p = log2dir + strlen(log2dir); if (*(p-1) != '\\') *p++ = '\\'; sprintf(p, "%03d", indexno); if (mkdir(log2dir) < 0) { gotoxy(1, 24); textcolor(YELLOW); cprintf("%s: can't create log directory `%s'", pname, log2dir); textcolor(WHITE); cleanup(3); } } /* Change to the drive and directory specified by logdir. */ if (logdir[1] == ':') { /* logdir includes drive letter */ if ('a' <= logdir[0] && logdir[0] <= 'z') drive = logdir[0] - 'a'; else if ('A' <= logdir[0] && logdir[0] <= 'Z') drive = logdir[0]-'A'; setdisk(drive); } else drive = getdisk(); chdir(logdir); if (log2dir[0] != '\0' && log2dir[1] == ':') { /* log2dir includes drive letter */ if ('a' <= log2dir[0] && log2dir[0] <= 'z') altdrive = log2dir[0]-'a'; else if ('A'<=log2dir[0]&& log2dir[0]<='Z') altdrive = log2dir[0]-'A'; } else altdrive = drive; /* Create the first set of log files. */ seqno = (long)(indexno % 1000) * 100000; error_flag = 0; /* Prepare to update the index file. */ if ((indexfp = fopen(index_file, "at")) == NULL) { gotoxy(1, 24); textcolor(YELLOW); cprintf("%s: can't open index file `%s'\n", pname, index_file); textcolor(WHITE); cleanup(5); } if (debug) { gotoxy(1, 24); cprintf("Press any key to continue: "); getch(); clrscr(); } if (test_mode) { /* non-interactive, do not gather patient data */ make_timestamp(); strcpy(start_date, date_string); strncpy(start_time, time_string, 5); strcpy(patient_name, "Test mode"); strcpy(patient_mrn, "11 22 33 44"); clrscr(); } else { gotoxy(2, 16); cprintf("Please enter the patient's name and" " medical record number as they appear on"); gotoxy(2, 17); cprintf("the patient's chart, today's date, and the" " current time as shown by the wall"); gotoxy(2, 18); cprintf("clock. Press after each entry."); gotoxy(2, 20); cprintf("Patient's name: "); gotoxy(2, 21); cprintf("Medical record #: "); gotoxy(2, 22); cprintf("Today's date: "); gotoxy(2, 23); cprintf("Current time: "); textcolor(YELLOW); gotoxy(20, 22); cprintf("%s", start_date); gotoxy(20, 23); cprintf("%s", start_time); textcolor(WHITE); _wscroll = 0; do { char ch; textcolor(YELLOW); buf[0] = sizeof(patient_name); do { gotoxy(20, 20); /* Let the user bail out here (by pressing the key) without making a recording. This is useful if (for example) the desired signals are not available from the monitor, and the monitor needs to be reconfigured before making the recording. */ ch = getch(); if (ch == '\033') { cleanup(0); chdir("\\"); rmdir(logdir); if (log2dir[0]) rmdir(log2dir); exit(1); } else ungetch(ch); cgets(buf); } while (buf[1] == '\0' && patient_name[0] == '\0'); if (buf[1]) { strcpy(patient_name, buf+2); gotoxy(20+strlen(patient_name), 20); clreol(); } buf[0] = sizeof(patient_mrn); do { gotoxy(20, 21); cgets(buf); } while (buf[1] == '\0' & patient_mrn[0] == '\0'); if (buf[1]) { strcpy(patient_mrn, buf+2); gotoxy(20+strlen(patient_mrn), 21); clreol(); } buf[0] = sizeof(start_date); gotoxy(20, 22); clreol(); make_timestamp(); strcpy(start_date, date_string); cprintf("%s", date_string); gotoxy(20, 22); cgets(buf); if (buf[1]) strcpy(start_date, buf+2); buf[0] = sizeof(start_time); gotoxy(20, 23); clreol(); make_timestamp(); strcpy(start_time, time_string); cprintf("%s", time_string); gotoxy(20, 23); cgets(buf); if (buf[1]) strcpy(start_time, cgets(buf)); gotoxy(2, 25); textcolor(WHITE); cprintf("Is this information correct? " "(press Y or N, and ): "); textcolor(YELLOW); do { /* Offer a last chance to bail out here (by pressing the key) without making a recording. */ ch = getch(); if (ch == '\033') { cleanup(0); chdir("\\"); rmdir(logdir); if (log2dir[0]) rmdir(log2dir); exit(1); } else ungetch(ch); buf[0] = 2; cgets(buf); } while (buf[2] != 'y' && buf[2] != 'Y' && buf[2] != 'n' && buf[2] != 'N'); gotoxy(2, 25); clreol(); } while (buf[2] != 'y' && buf[2] != 'Y'); textcolor(WHITE); clrscr(); } if (errorlogfp) fprintf(errorlogfp, "%s %s Started test %03d\n", date_string, time_string, indexno); /* Update and close the index file. */ fprintf(indexfp, "%03d\t%s\t%s\t%s\t%s\n", indexno, patient_mrn, start_date, start_time, patient_name); fclose(indexfp); setbasetime(NULL); if (openlog() < 1) { gotoxy(1, 24); textcolor(YELLOW); cprintf("%s: can't create log files", pname); textcolor(WHITE); cleanup(4); } if (tune_lib_req(cid[porta],&tune_list[porta],CONTINUOUS) != SUCCESS) cleanup(6); do { status = rx_mecif(); if (kbhit()) cleanup(7); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); if (portb && tune_list[portb].num_tune) { if (tune_lib_req(cid[portb], &tune_list[portb], CONTINUOUS) != SUCCESS) cleanup(8); do { status = rx_mecif(); if (kbhit()) cleanup(9); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } if (portc && tune_list[portc].num_tune) { if (tune_lib_req(cid[portc], &tune_list[portc], CONTINUOUS) != SUCCESS) cleanup(10); do { status = rx_mecif(); if (kbhit()) cleanup(11); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } if (portd && tune_list[portd].num_tune) { if (tune_lib_req(cid[portd], &tune_list[portd], CONTINUOUS) != SUCCESS) cleanup(12); do { status = rx_mecif(); if (kbhit()) cleanup(13); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } gotoxy(1, 1); cprintf("Patient's name: %s", patient_name); gotoxy(1, 2); cprintf("Medical record #: %s ID: %03d", patient_mrn, indexno); gotoxy(1, 3); cprintf("Recording since: %s on %s", start_time, start_date); gotoxy(1, 5); cprintf("Signals and measurements are now being recorded" " by this computer. To end"); gotoxy(1, 6); cprintf("the recording at any time, hold down the `Ctrl'" " key and type `D'. Doing so"); gotoxy(1, 7); cprintf("has no effect on the patient's bedside monitor."); /* Post the user message, if any. */ for (i = 0; i < 5; i++) if (notice[i][0]) { for (j = 0; notice[i][j]; j++) if (notice[i][j] == '_') notice[i][j] = ' '; gotoxy(1, 9+i); cprintf("%s", notice[i]); } gotoxy(1, 14); cprintf("Messages"); gotoxy(1, 15); cprintf("WS: "); gotoxy(1, 16); cprintf("CW: "); gotoxy(1, 17); cprintf("NU: "); gotoxy(1, 18); cprintf("AL/IN: "); if (nsig > 0) { gotoxy(19, 14); cprintf("Signals"); } if (nmeas > 0) { gotoxy(40, 14); cprintf("Measurements"); } gotoxy(1, 25); cprintf("%s %s", pname, VERSION); initializing = 0; } int c_break(void) { return (1); } /* Function `getdata' contains the reading loop (the code that retrieves data from the monitor and records it in disk files). */ void getdata() { ctrlbrk(c_break); /* ignore keyboard interrupts */ while (1) { rx_mecif(); /* read a message */ if (kbhit()) { if (getch() == '\04') { /* control-D */ gotoxy(20, 25); cprintf("Interrupt -- exiting."); gotoxy(1, 24); break; } } } } /* Function `cleanup' closes files and resets the hardware just before this program exits. */ void cleanup(int exit_code) { int i, j; LibRet *status; for (j = 1; j < 5; j++) if (cid[j] && tune_list[j].num_tune) { detune_all_req(cid[j], 0); for (i = 0; i < 25; i++) { status = rx_mecif(); if (status->cmd == DETUNE_ALL_REQ) break; } } for (j = 1; j < 5; j++) if (cid[j]) disconnect_req(cid[j]); for (j = 1; j < 5; j++) if (pid[j] != -1) close_port(pid[j]); /* Close the log files, if any. */ if (*recname) { flush_buf(); newheader(recname); dbquit(); } if (nufp) fclose(nufp); if (exit_code && errorlogfp) { make_timestamp(); fprintf(errorlogfp, "%s %s Fatal error %d\n", date_string, time_string, exit_code); } else if (!initializing && errorlogfp) { make_timestamp(); fprintf(errorlogfp, "%s %s Ended test %03d (WS: %ld, CW: %ld, NU: %ld, AL/IN: %ld)\n", date_string, time_string, indexno, nws, ncw, nnu, nal); } if (errorlogfp) fclose(errorlogfp); if (exit_code) { clrscr(); gotoxy(2, 10); cprintf("This program cannot run without corrective action."); gotoxy(2, 11); cprintf("Please report error code %d%c.", exit_code, 'A'+ exit_code); gotoxy(1, 25); exit(exit_code); } gotoxy(1, 25); } /* Check the physical connection between a com line and the monitor. mirror_req sends a message to the monitor and returns immediately. If the connection is functional, rx_mecif will receive a MIRROR_RSP; if not, the MECIF library calls rx_error with code NO_RSP_MIR_REQ after two seconds have elapsed, and rx_error clears connected[]. */ int try_com(int com, unsigned speed) { LibRet *status; int debug_save = debug; i_16 temp_cid; if ((pid[com] = init_rs232port(com-1, speed)) == BAUDRATE_WRONG || (pid[com] == PORT_WRONG) || (pid[com] == NOT_ENOUGH_MEMORY)) { pid[com] = -1; return (connected[com] = 0); /* failure */ } debug = 0; connected[com] = 1; addcon(temp_cid = mirror_req(0, com-1, 0, 0), com); while (connected[com]) { status = rx_mecif(); /* read a message */ if (status->cmd == MIRROR_RSP) { if (debug = debug_save) fprintf(stderr, "com%d (port %d) initialized at %u baud\n", com, pid[com], speed); return (1); /* success */ } } debug = debug_save; disconnect_req(temp_cid); /* ^^^ This disconnect_req is critically important! If a mirror request is issued on a line that (a) is not properly configured, and (b) does not get disconnected, then continuous mode (but not single) requests on a properly configured line will fail (actually, the request itself succeeds, but none of the requested messages get delivered). In this program, this can happen if the disconnect_req is omitted and one of the com lines is not connected to the monitor. This looks like a MECIF library bug. */ close_port(pid[com]); pid[com] = -1; return (0); /* failure */ } /* This function determines the capacity in bytes per second of a com line, given its baud rate. The capacities are based on those given in the table on page 4-8 of the HP Guide, which are stated to include allowances for escape sequences in the transmissions. (It is not clear if these numbers represent bytes per second, as stated on page 4-8, or bytes per 1.024 seconds, as stated in the example on page 4-9.) Note that the full capacity of a 9600 baud line is 920 bytes/second. If port 1 or 3 on the monitor is running at 38400 baud, however, the HP Guide (p. 4-9) states that the capacity of the second port (2 or 4) on the same card is limited to 600 bytes/second. This program assumes that any port running at 9600 baud is subject to this restriction (a reasonable assumption, since there is no other reason to lower the baud rate in this way). */ int cap(unsigned speed) { switch (speed) { case 9600U: return (600); case 19200U: return (1850); case 38400U: return (3750); default: return (0); } } /* assign_com chooses a port to be used to receive a given message type. It does this by finding a port with sufficient bandwidth for the message. This is not quite a "knapsack problem", because we don't particularly want to use all of the available bandwidth of any given port, since the bandwidth calculations are only approximate. The goal is rather to leave some unused bandwidth for each port if possible, ideally in equal amounts. */ void assign_com(int i, int cost) { int j, k, l; /* Find the port with the greatest remaining capacity. */ for (j = 1, k = l = 0; j < 5; j++) if (capacity[j] - bandwidth[j] > k) { k = capacity[j] - bandwidth[j]; l = j; } /* If this port has sufficient bandwidth, accept the assignment. */ if (k >= cost) { tune_list_active[i] = l; bandwidth[l] += cost; } /* Otherwise, set flags to indicate bandwidth exceeds capacity. */ else { tune_list_active[i] = -1; bwtoohigh = 1; } } /* Reverse the effects of assign_com. */ void deassign_com(int i, int credit) { /* Find the port to which the parameter has been assigned, and credit it with the cost. */ if (tune_list_active[i] > 0) { bandwidth[tune_list_active[i]] -= credit; tune_list_active[i] = -1; } } /* Determine the priority of the waveform indexed by i. */ int wave_priority(int i) { char *p = tune_list_ascii_id[i], *q; if (q = strstr(p, "ECG-CH")) { fprintf(errorlogfp, "pr(%s) = %d\n", p, ecg_priority[q[6] - '0']); return (ecg_priority[q[6] - '0']); } else if (q = strstr(p, "PRESS")) { fprintf(errorlogfp, "pr(%s) = %d\n", p, press_priority[q[6] - '0']); return (press_priority[p[6] - '0']); } else if (strstr(p, "RESP")) { fprintf(errorlogfp, "pr(%s) = %d\n", p, resp_priority); return (resp_priority); } else if (strstr(p, "PLETH")) { fprintf(errorlogfp, "pr(%s) = %d\n", p, pleth_priority); return (pleth_priority); } else if (strstr(p, "CO2")) { fprintf(errorlogfp, "pr(%s) = %d\n", p, co2_priority); return (co2_priority); } else { fprintf(errorlogfp, "pr(%s) = %d\n", p, other_priority); return (other_priority); } } #define CONTABSIZE 100 struct { i_16 con; /* MECIF connection id */ char com; /* associated com number (1 or 2) */ } contab[CONTABSIZE]; /* as connections are opened, addcon adds entries to this table.*/ int ncons = 0; /* number of connections (entries in contab) */ void addcon(i_16 con, int com) { int i; for (i = 0; i < ncons; i++) if (contab[i].con == con) { contab[i].com = (char)com; return; } if (ncons < CONTABSIZE) { contab[ncons].con = con; contab[ncons].com = (char)com; ncons++; } if (debug) fprintf(stderr, "Connection %d is via com%d\n", con, com); } int findcon(i_16 con) { int i; for (i = 0; i < ncons; i++) if (contab[i].con == con) return ((int)contab[i].com); return (0); } void make_timestamp() { struct tm *now; time_t t, time(); static char *monthname[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; t = time((time_t *)NULL); /* get current time from system clock */ now = localtime(&t); (void)sprintf(date_string, "%2d %s %d", now->tm_mday, monthname[now->tm_mon], now->tm_year+1900); (void)sprintf(time_string, "%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec); } void show_fixup_message() { gotoxy(2, 16); cprintf("Interrupt -- exiting."); gotoxy(2, 17); cprintf("Please wait 10 seconds to allow the computer to" " correct the communication"); gotoxy(2, 18); cprintf("problem, then start this program again." " If the problem persists on a"); gotoxy(2, 19); cprintf("second attempt, please restart this computer" " using the reset button on its"); gotoxy(2, 20); cprintf("front panel and then try once more. " "If that fails, or if the list of signals"); gotoxy(2, 21); cprintf("and measurements is incomplete, please restart the monitor."); gotoxy(1, 25); } void openlogerr(char *file_name) { if (errorlogfp) { make_timestamp(); fprintf(errorlogfp, "%s %s Could not create `%s'\n", date_string, time_string, file_name); } } /* The string `altext' describes the most recent alarm reported by the monitor (see rx_lib_al() below for details). Since `altext' is used as an `aux' string for an annotation (see the DB Guide), its first byte is a byte count of the remainder of the string, not including the terminating null. */ static char altext[80]; /* About sample numbers: Samples are identified by `sample numbers' (defined as the number of samples of the same signal that preceed the given sample). The records collected by this program are organized as segments to be collected into composite records. Within this program, an `absolute sample number' refers to the sample number within the composite record (i.e., the number of samples from the first sample of the first segment to the sample immediately before the given sample), and a `relative sample number' refers to the sample number within the current segment only. */ static long t[MAXSIG]; /* absolute sample numbers of most recently received samples for each signal */ static long t0; /* absolute sample number of sample 0 of the current segment */ int openlog() { static char fname[100]; int i, status = 0; unsigned used; static unsigned avail, maxused; struct dfree free; static DB_Anninfo ai = { "al", WRITE }; seqno++; if (*recname) { if (error_flag) { resynch(); error_flag = 0; } newheader(recname); dbquit(); getdfree(drive+1, &free); used = avail - free.df_avail; gotoxy(4, 22 + (drive == altdrive)); cprintf("%4.1f hours", free.df_avail / clusters_per_hour); if (used > maxused) maxused = used; if (free.df_avail < 2*maxused) { if (drive != altdrive) { /* switch to alternate drive */ setdisk(altdrive); drive = altdrive; getdfree(drive+1, &free); avail = free.df_avail; chdir(log2dir); } else { /* exit, not enough space to continue */ if (errorlogfp) { make_timestamp(); fprintf(errorlogfp, "%s %s File space exhausted\n", date_string, time_string); } cleanup(0); exit(0); } } else avail = free.df_avail; } else { /* first call -- perform once-only initialization */ dbquiet(); getdfree(drive+1, &free); avail = free.df_avail; gotoxy(1, 21 + (drive == altdrive)); cprintf("Disk space:"); gotoxy(1, 22 + (drive == altdrive)); clusters_per_hour= 1.15e7/((double)free.df_bsec*(double)free.df_sclus); cprintf("%c: %4.1f hours", drive+'A', free.df_avail / clusters_per_hour); if (drive != altdrive) { getdfree(altdrive+1, &free); gotoxy(1, 23); cprintf("%c: %4.1f hours", altdrive+'A', free.df_avail/clusters_per_hour); } for (i = 1; i < 5; i++) { if (com_speed[i]) { gotoxy(1, 18+i); cprintf("com%d err:", i); gotoxy(10, 18+i); cprintf("%5d", com_errors[i] = 0U); } } } setsampfreq(125.0); sprintf(recname, "%08ld", seqno); sprintf(fname, "%s.dat", recname); for (i = 0; i < nsig; i++) si[i].fname = fname; if (osigfopen(si, nsig) < nsig) openlogerr(fname); else status = 1; setbasetime(NULL); sprintf(fname, "%s.txt", recname); if (nufp) fclose(nufp); if ((nufp = fopen(fname, "wt")) == NULL) openlogerr(fname); else { fprintf(nufp, "%s\t%s\n", timstr(0L), fname); status += 2; } if (annopen(recname, &ai, 1) < 0) { sprintf(fname, "%s.al", recname); openlogerr(fname); } else { /* Reset the alarm text string so that any pending alarm is recorded in the new annotation file (altext[0] = length, altext[1-7] = "ALARM: ", altext[8-80] = description of alarm). */ altext[8] = '\0'; t0 = t[0]; /* sample number of the first sample in this segment, relative to the beginning of the first segment */ status += 4; } return (status); } /* Callbacks. The functions below are called by the MECIF library, not directly by the functions above. */ /* rx_error is the error handler called by the MECIF library. */ void rx_error(u_16 error_code, i_16 con) { int com = findcon(con); static char buf[80], *error; /* For definitions (only) of the error codes below, see meciflib.h. For cryptic interpretations, see the HP Guide, pp. 5-23 and 5-24. */ switch (error_code) { case WINDOW_SIZE_OVERFLOW: /* from the monitor */ error = "Window size overflow"; /* This apparently means that the monitor did not receive all of the data transmitted to it by the PC. */ break; case TX_BUFFER_OVERFLOW: /* from the monitor */ error = "TX buffer overflow"; /* This apparently means that the PC did not receive all of the data transmitted to it by the monitor. */ break; case HW_HANDSHAKE_MALFUNCTION: /* from the monitor */ error = "Hardware handshake failed"; /* ??? */ break; case MPB_FIFO_OVERFLOW: /* from the monitor */ error = "MPB FIFO overflow"; /* Possibly means the PC didn't receive all of the data sent by the monitor, or else that the monitor couldn't transmit everything requested. ??? */ break; case PARITY_ERROR: /* from the monitor */ error = "Parity error"; /* The HP Guide suggests that this refers to data received by the monitor. ??? */ break; case NOTIFIC_ABORTED: /* from the monitor */ error = "Notification aborted"; /* related somehow to lifetick timeout (see below) ??? */ /* possibly due to physical connection failure */ break; case LIFETICK_TIMEOUT: /* from the MECIF library */ error = "Lifetick timeout"; /* Connection was closed because the monitor didn't answer two lifetick requests. */ /* possibly due to physical connection failure */ connected[com] = 0; break; case LIFETICK_FAILED: /* from the MECIF library */ error = "Lifetick failed"; /* Connection was closed because the monitor answered a lifetick request incorrectly. */ /* possibly due to physical connection failure */ connected[com] = 0; break; case NO_RSP_MIR_REQ: /* from the MECIF library */ error = "Connection failed (mirror request)"; /* possibly due to physical connection failure */ connected[com] = 0; break; case NO_RSP_DIR_REQ: /* from the MECIF library */ error = "Connection failed (directory request)"; /* possibly due to physical connection failure */ connected[com] = 0; break; case NO_RSP_RAWEN_REQ: /* from the MECIF library ??? */ error = "NO_RSP_RAWEN_REQ"; /* no clue what this means */ /* possibly due to physical connection failure */ break; case NO_RSP_CON_REQ: /* from the MECIF library */ error = "Connection failed (connect request)"; /* possibly due to physical connection failure */ connected[com] = 0; break; case NO_RSP_DISCON_REQ: /* from the MECIF library */ error = "Connection failed (disconnect request)"; /* possibly due to physical connection failure */ connected[com] = 0; break; case MESSAGE_LOST: /* from the MECIF library */ error = "Message lost"; /* The HP Guide seems to suggest this means that a message from the PC to the monitor has been lost, not the other way around. ??? */ break; case INVALID_COMMAND: /* from the MECIF library ??? */ error = "Invalid command"; /* Presumably means the monitor couldn't interpret something it received over the serial line. No comment in the HP Guide. ??? */ break; case RS232_RESTART_OV: /* from the monitor */ error = "RS232 restart: MPB RX FIFO overflow"; /* Either the monitor or the PC lost something ??? */ break; case RS232_RESTART_OTHER: /* from the monitor */ error = "Serial board in monitor may be defective"; /* See the HP Guide. */ break; default: sprintf(buf, "Unknown error (code %u)", error_code); error = buf; break; } make_timestamp(); if (error_show && error_code != NO_RSP_DISCON_REQ) { gotoxy(20, 25); clreol(); textcolor(BLUE); cprintf("%s com%d: %s", time_string, com, error); textcolor(WHITE); } if (errorlogfp) fprintf(errorlogfp, "%s %s com%d (cid %d): %s\n", date_string, time_string, com, con, error); if (!initializing) { gotoxy(10, 18+com); cprintf("%5d", ++com_errors[com]); } error_flag = 1; } #define VZERO 2048 int findsig(mp) MsgIdTyp *mp; { int i; MsgIdTyp *tp; for (i = 0; i < nsig; i++) { tp = &fsmap[i]; if (mp->SourceId == tp->SourceId && mp->ChannelId == tp->ChannelId && mp->ChannelNo == tp->ChannelNo && mp->SourceNo == tp->SourceNo) break; } return (i); } static long pvt; static DB_Annotation annot = { 0L, NOTE, 0, 0, 0, NULL }; #define put_sample(S,V) (vbuf[((t[S]++ & (BSIZE-1))<<4) | S] = V) void rx_lib_ws(WaveSupp *ws) { int active, i, s; static int was_active[MAXSIG]; static char onmsg[] = "\02on"; /* first char is length of remainder */ static char offmsg[] = "\03off"; if ((unsigned)(ws->status & 0xb100) == (unsigned)(0xa100)) /* Parameter is on (bit 15 = 1), front end is plugged in (bit 13 = 1), transducer is connected (bit 12 = 0), and channel is on (bit 8 = 1). */ active = 1; else active = 0; if (initializing) { for (i = 0; i < 20; i++) { /* 20: largest possible value for nwaves */ if (ws->msg_id.SourceId == tune_list_msg_id[i].SourceId && ws->msg_id.SourceNo == tune_list_msg_id[i].SourceNo && ws->msg_id.ChannelId == tune_list_msg_id[i].ChannelId && ws->msg_id.ChannelNo == tune_list_msg_id[i].ChannelNo) { strncpy(tune_list_label[i], ws->label, 12); tune_list_active[i] = active; break; } } for (i = 0; i < MAXSIG; i++) was_active[i] = 1; nresp++; } else { gotoxy(5, 15); cprintf("%10lu", ++nws); s = findsig(&ws->msg_id); if (s == 0) { make_timestamp(); gotoxy(60, 1); cprintf(date_string); gotoxy(60, 2); cprintf(time_string); } gotoxy(16, s+15); cprintf("%6s:", si[s].desc); gotoxy(33, s+15); if (active) cprintf(" on "); if (!active) { textcolor(BLUE); cprintf(" off"); textcolor(WHITE); if (was_active[s]) { if (s) make_timestamp(); if (errorlogfp) fprintf(errorlogfp, "%s %s: Signal %d (%s) off at sample %ld\n", date_string, time_string, s, si[s].desc, t[s]); annot.time = t[s] - t0; annot.chan = s; annot.aux = offmsg; putann(0, &annot); } switch (si[s].spf) { case 1: for (i = 0; i < 128; i++) put_sample(smap[s], 0); break; case 4: for (i = 0; i < 512; i++) put_sample(smap[s] + (i&3), 0); break; default: /* shouldn't happen */ break; } flush_buf(); } else if (!was_active[s] && errorlogfp) { make_timestamp(); fprintf(errorlogfp, "%s %s: Signal %d (%s) on at sample %ld\n", date_string, time_string, s, si[s].desc, t[s]); annot.time = t[s] - t0; annot.chan = s; annot.aux = onmsg; putann(0, &annot); } was_active[s] = active; } } void flush_buf() { int s; long tt; static long rl; for (s = 0, tt = pvt+2*BSIZE; s < tspf; s++) if (t[s] < tt) tt = t[s]; while (pvt < tt) { putvec(&vbuf[(pvt++ & (BSIZE-1)) << 4]); if (++rl >= record_length) { rl = 0L; openlog(); } } } void rx_lib_cw(CookedWave *cw) { int i, s; static long np[MAXSIG]; if (initializing) nresp++; else { gotoxy(5, 16); cprintf("%10lu", ++ncw); s = findsig(&cw->msg_id); /* get signal number */ gotoxy(23, 15+s); cprintf("%10lu", ++np[s]); switch (cw->num_of_samples) { case 4: for (i = 0; i < 4; i++) put_sample(smap[s], cw->samples[i] - VZERO); break; case 16: for (i = 0; i < 16; i++) put_sample(smap[s]+(i&3), cw->samples[i] - VZERO); break; default: /* shouldn't happen */ break; } flush_buf(); } } int findmeas(s) char *s; { int i; for (i = 0; i < nmeas && strcmp(numap[i], s); i++) ; return (i); } void rx_lib_nu(Numeric *nu) { int active, i, m; if ((unsigned)(nu->status & 0xb100) == (unsigned)(0xa100)) /* Parameter is on (bit 15 = 1), front end is plugged in (bit 13 = 1), transducer is connected (bit 12 = 0), and channel is on (bit 8 = 1). */ active = 1; else active = 0; if (initializing) { for (i = 40; i < 88; i++) { if (nu->msg_id.SourceId == tune_list_msg_id[i].SourceId && nu->msg_id.SourceNo == tune_list_msg_id[i].SourceNo && nu->msg_id.ChannelId == tune_list_msg_id[i].ChannelId && nu->msg_id.ChannelNo == tune_list_msg_id[i].ChannelNo) { strncpy(tune_list_label[i], nu->label, 12); tune_list_active[i] = active; } } nresp++; } else { gotoxy(5, 17); cprintf("%10lu", ++nnu); m = findmeas(nu->label); gotoxy(40, m+15); fprintf(nufp, "%s", nu->label); if (active) { cprintf("%s:", nu->label); for (i = 0; i < nu->num_of_values; i++) { cprintf("%10g", nu->numeric[i].value); fprintf(nufp, "\t%g", nu->numeric[i].value); } } else { textcolor(BLUE); cprintf("%s: [inactive]", nu->label); textcolor(WHITE); fprintf(nufp, "[inactive, status = %x]", nu->status); } clreol(); fprintf(nufp, "\n"); } } char textbuf[80]; void rx_lib_al(Alert *al) { int i, m; if (initializing) nresp++; else { static long altime; /* time of most recent alarm */ if (strcmp(al->unsptext, altext+8) || (t[0] - altime > 250L)) { /* Alarms are raised repeatedly at 1024 ms intervals. Only the first alarm of a given series occuring in any given segment is recorded in the annotation file (all are recorded in the .txt file, however). The openlog() function resets altext so that an alarm condition that spans a segment boundary appears once in each annotation file. */ sprintf(altext+1, "ALARM: %s", al->unsptext); altext[0] = strlen(altext+1); annot.time = t[0] - t0; /* sample number within current segment */ annot.chan = 0; annot.aux = altext; putann(0, &annot); } altime = t[0]; gotoxy(8, 18); cprintf("%7lu", ++nal); fprintf(nufp, "ALARM\t%s\n", al->unsptext); } } void rx_lib_alstat(AlStat *al) { int i, m; if (initializing) nresp++; else { gotoxy(8, 18); cprintf("%7lu", ++nal); fprintf(nufp, "ALSTAT\t%d %d %d %d\n", al->al_severity, al->inop_severity, al->mon_status, al->al_suspended); } } void rx_lib_inop(Alert *al) { int i, m; if (initializing) nresp++; else { #if 0 /* enable this block to log inops in the annotation files */ static char inoptext[80]; sprintf(inoptext+1, "INOP: %s", al->unsptext); inoptext[0] = strlen(inoptext+1); annot.time = t[0] - t0; annot.chan = 0; annot.aux = inoptext; putann(0, &annot); #endif gotoxy(8, 18); cprintf("%7lu", ++nal); fprintf(nufp, "INOP\t%s\n", al->unsptext); } } void resynch() { int i, s; long tt; LibRet *status; if (cid[1] && tune_list[1].num_tune) { detune_all_req(cid[1], 0); for (i = 0; i < 25; i++) { status = rx_mecif(); if (status->cmd == DETUNE_ALL_REQ) break; } } if (cid[2] && tune_list[2].num_tune) { detune_all_req(cid[2], 0); for (i = 0; i < 25; i++) { status = rx_mecif(); if (status->cmd == DETUNE_ALL_REQ) break; } } for (s = 1, tt = t[0]; s < tspf; s++) if (t[s] > tt) tt = t[s]; for (s = 0; s < tspf; s++) t[s] = tt; flush_buf(); if (tune_lib_req(cid[porta],&tune_list[porta],CONTINUOUS) != SUCCESS) cleanup(6); do { status = rx_mecif(); if (kbhit()) cleanup(7); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); if (portb && tune_list[portb].num_tune) { if (tune_lib_req(cid[portb], &tune_list[portb], CONTINUOUS) != SUCCESS) cleanup(8); do { status = rx_mecif(); if (kbhit()) cleanup(9); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } if (portc && tune_list[portc].num_tune) { if (tune_lib_req(cid[portc], &tune_list[portc], CONTINUOUS) != SUCCESS) cleanup(10); do { status = rx_mecif(); if (kbhit()) cleanup(11); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } if (portd && tune_list[portd].num_tune) { if (tune_lib_req(cid[portd], &tune_list[portd], CONTINUOUS) != SUCCESS) cleanup(12); do { status = rx_mecif(); if (kbhit()) cleanup(13); } while (status->cmd != TUNE_RSP || status->ret.pret == NULL); } }