Version 1 of HL7

Updated 2003-05-13 18:25:11

Health Level Seven

Started on 13May2003 by PS

HL7 is a data interchange standard for medical purposes. It is completely text based, and very easy to handle in Tcl.

Communication is done over TCP sockets, where every application (at least in the Philips system I know) sits and waits until the comunications server opens a single channel for message traffic and keeps that open.

Each message begins with a \xb character and ends with \x1c\xd character sequence, everything in between is the message. Segments in the message are separated by a single <CR> (\xd) character. The first segment is always the message header (MSH) segment, based on that the receiver should expect other segments.

A typical message looks like:

 \xb
 MSH|^~\&|DISPADT||ASTRAIA|astraia|20030508110000||ADT^A08|1437549872|P|2.2||
 EVN|A08|200305081100
 PID|1||00000123456||Public^""^J.^""^""^""||19700101|F|||Somestreet^1^Nieuwegein^^3432AA^""||030-1234567|||""|""||||||""|""
 ZPI|1|||DoctorDr.^^""^""^""|||||||""
 PV1|1|O||
 IN1|||PART|Partikulier|||||||||||P|||||||||||||||||||||""
 \x1c

Where you should note that the first | is actually not only a field separator, but also an indication of what the field separator is. The four characters after that specify the sub, subsub, subsubsub and subsubsubsub field separators. [split] is our big friend with all those characters.

After you receive an unsolicited message (i.e. not a query), you must send an ACK message in response otherwise the server will not send you any more messages.

 \xb
 MSH|^~\&|ASTRAIA|astraia|DISPADT||||ACK||P|2.2||
 MSA|AA|1437549872||
 \x1c

The important number being 1437549872, the message reference from the message you just received.


Message receiver/dispatcher

My HL7 software is only a client, this dispatcher will also answer query responses with an ACK message, which is illegal.

 proc processData { channel } {
    #Do we have a full message?
    set begin [string first \xb $::peers($channel.data)]
    set end [string first \x1c $::peers($channel.data)]
    set sender ""
    set msgid ""
    if { $end > $begin } {
        set msg [string range $::peers($channel.data) [expr $begin +1] [expr $end-1]]
        foreach line [split $msg \xd] {
            set fields [split $line |]
            switch [lindex $fields 0] {
                MSH {
                    set sender [lindex $fields 2]
                    set msgid [lindex $fields 9]
                    set environ [lindex $fields 10]
                }
            }
        }
        if { $sender ne "" && $msgid ne "" } {
            #TODO: insert check to see if the the message is a query response, those must not
            #be answered with ACK!
            set ack "\xbMSH|^~\\&|ASTRAIA|astraia|$sender||||ACK||$environ|2.2||\xdMSA|AA|$msgid||\xd\x1c\xd"
            puts -nonewline $channel $ack
            set ::peers($channel.data) [string range $::peers($channel.data) [expr $end + 1] end]

            #For debugging, I write all messages to a timestamped file, so I can replay them:
            puts $::peers($::peers($channel).tsd) [list msg [clock clicks -milliseconds] $msg]
            puts $::peers($::peers($channel).tsd) [list out [clock clicks -milliseconds] $ack]

            #And call the message handler: 
            processMsg $msg

        } else {
            log "No sender/msgid $sender/$msgid"
        }
    }
 } 

The message parser

This handles an HL7 message, one at a time.

 proc processMsg { msg } {
    global db

    #first split the message into individual segments.
    set segments [split $msg \xd]
    foreach segment $segments {
        #as it is unlikely that we don't need to split on fields, I split
        #every segment on level one (|).

        set fields [split $segment |]

        #and for easy access everything goes into an array keyed on segment ID
        #which is, of course, a list!
        lappend seg([lindex $fields 0]) $fields
        switch [lindex $fields 0] {
            MSH {
                #this will set some variables in my environment prefixed MSH_
                #more on this later.
                hl7_set_segment_variables $fields
            }
            default {

            }
        }        
    }

    #Choose what to do based on the message type from the MSH header segment.
    switch $MSH_type {
        ADT {
            foreach pid $seg(PID) {
                #only update:
                processPID $db $pid 1
            }
        }
        SIU {            
            processPID $db [lindex $seg(PID) 0] 0
            hl7_set_segment_variables [lindex $seg(AIS) 0]
            if { [lsearch {FE50 GR01 GR02 GR03} $AIS_code] == -1 } {
                log "Niet geintereseerd in SIU $AIS_code '$AIS_description'"
                return            
            }            
            hl7_set_segment_variables [lindex $seg(SCH) 0]
            hl7_set_segment_variables [lindex $seg(AIL) 0]
            hl7_set_segment_variables [lindex $seg(PID) 0]
            switch $MSH_eventtype {
                S12 { 
                    #worker code to insert appointment details into database goes here.
                }
                S15 { 
                    #worker code to delete/cancel an appointment.
                    if { [string trim $SCH_id] ne "" } {
                        ns_db dml $db "delete from diary where appointmentID=[ns_dbquotevalue $SCH_id] and application=[ns_dbquotevalue $SCH_application]"
                    }
                } 
            }
        }
    }
 }

Work in progess - I just saved to not lose anything - standby