\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|||||||||||||||||||||"" \x1cWhere 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 an answer to a query you previously sent), 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|| \x1cThe important number being 1437549872, the message reference from the message you just received.
Message receiver/dispatcherMy HL7 software is only a client, this dispatcher will also answer query responses with an ACK message, which is illegal.The socket/channel must be [fconfigure $channel -translation binary -blocking 0 -buffering none] in your accept connection [proc], otherwise it won't work. See socket for examples on starting a server socket.
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 parserThis 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]"
}
}
}
}
}
}The segment parserUsing [upvar], [split] and [lindex], it is really very easy to assign each field to a variable:
proc hl7_set_segment_variables { segment } {
#segment contains a list of fields from a single segment (line) from the message.
#this means, it has already been [split $line |]
#the first element contains the segment ID:
set seg [lindex $segment 0]
#fields is a list with list indices (starting at one, because $seg is at 0!) and
#desired field names. If a field is to be devided in subfields, the field name is
#a list of field names and the field will be split again, this time with the level 2
#character (usually the carret: ^)
#because the messages I need to parse go no deeper than two levels, this code doesn't
#go any further, but the same can of course be done for the next levels.
#Note that my segments dictionaries are not complete!
switch $seg {
MSH {
set fields {
1 encodingchars
2 sendingapplication
3 sendingfacility
4 receivingapplication
5 receivingfacility
6 datetime
8 {type eventtype}
9 msgid
10 processingid
11 version
12 sequencenumber
13 continuationptr
}
}
SCH {
set fields {
1 {id application}
16 {fillercode fillername}
20 {enteredbycode enteredbyname}
25 statuscode
}
}
AIL {
set fields {
1 setid
2 actioncode
3 {resourceid roomnumber}
5 locationgroup
}
}
AIS {
set fields {
1 setid
3 {code description codingsystem}
4 startdatetime
7 duration
8 durationunits
9 allowsubstitution
10 fillerstatus
}
}
PID {
set fields {
1 setid
2 {externalid externalid_checkdigit}
3 {pid pid_checkdigit}
4 alternateid
5 {eigennaam roepnaam initialen achtervoegsels voorvoegsels titulatuur}
7 birthdate
8 sex
11 {straatnaam huisnr woonplaats provincie postcode land}
13 phonehome
14 phonebus
15 languagepatient
16 maritalstatus
17 religion
23 birthplace
26 citizenship
}
}
default {
#error or return for unknow segment types?
return
}
}
#Now set the variables in the callers environment:
foreach {fieldno names} $fields {
if {[llength $names] == 1} {
#A single variable name, no further split needed.
#Use [upvar] to create/set a variable in the callers environment
upvar ${seg}_$names var
#hl7string does nothing more than return an empty string when the field has
#the 'empty' marker "" in it.
#HL7 makes a difference between NULL values and empty string.
set var [hl7string [lindex $segment $fieldno]]
} else {
#More than one fieldname: split another level
#Even better would have been the same syntax for each sublevel
#and then call a function like [setsubsegment <segmentid> <fieldnames> <sepchars-for-remaining-levels>]
set subfields [split [lindex $segment $fieldno] ^]
set i 0
foreach name $names {
upvar ${seg}_$name var
set var [hl7string [lindex $subfields $i]]
incr i
}
}
}
}I have some helper functions too:
proc hl7string { s } {
if { $s eq {""} } {
return ""
}
return $s
}
proc hl7dateToISO { date } {
set dpart [string range $date 0 7]
set tpart [string range $date 8 13]
set res ""
if { [string length $dpart] == 8 } {
append res "[string range $dpart 0 3]-[string range $dpart 4 5]-[string range $dpart 6 7] "
}
if { [string length $tpart] == 6 } {
append res "[string range $tpart 0 1]:[string range $tpart 2 3]:[string range $tpart 4 5].000"
} elseif { [string length $tpart] == 4 } {
append res "[string range $tpart 0 1]:[string range $tpart 2 3]:00.000"
}
#log "Date $date $dpart/$tpart $res"
return $res
}
proc removezeros { s } {
regexp {^0+(.+)$} $s -> s
return $s
}-- PS.[ Category Data Structure | Category Example | Category Medicine ]

