This post was originally written for Iguana 5 so it uses version 5 screenshots, and may contain out of date references.
Sending ASTM messages using Translator is not much different from sending HL7 messages. For this purpose we will use well known llp module from the tools repo, and rename it and modify to match ASTM protocol particulars and exclude LLP specific elements.
This Example Dependencies
In order to test this project, it requires to run active ASTM TCP Server (Listener) and Iguana Webserver to do HTTP POST of ASTM messages to Iguana.
Data presentation
Suggested example will read initial ASTM message data from file (… or from ‘Sample Data’ when working in Translator IDE.) Depending on type of file system, the end-of-line can be <CR> or <LF> or combination of both.
Our testdata can be seen below; and one can note that in this data the end-of-line is 0x0A character (AKA ‘\n’ or <LF>.)
Thus in module main we will examine and normalize input ASTM message frames; calculate and concatenate respective checksum digits; and pipe result to sending module.
Because data itself includes numerous control characters, it could optionally be saved as binary file instead of text.
If data had <CR> instead of <LF> this would make our example more simple, but let’s see below how to handle non-standard terminator.
Frames must be surrounded by leading STX and trailing ETX, or ETB. This sample ‘ASTM Sender’ has no way to know if frame has to be terminated with ETX or ETB unless it has been promptly specified in data itself.
Translator as ASTM TCP Client
For impatient complete ASTM_sender_To_Translator.zip project is available for download.
Modules
We call four modules.
- The astm_send module handles ASTM specific protocol to send a message
- The astn_tcp module handles TCP communications
- The astmutil module contains functions to calculate checksum digits, write debug logs, and send HTTP POST request to Iguana Webserver
- The stringutil module contains few string manipulation functions
Module Dependencies
Modules don’t ‘require’ external to Iguana Lua modules.
Configuration
Three places which will require custom configuration:
- Please remember to adjust TCP host and port values in function main() to match host and port of ASTM TCP Server that ASTM messages will be sent to.
- The webserver access credential in function astmutil.postJson () in module astmutil may have to be adjusted as well.
- The function calculating checksum digits may have to be adjusted to match specific interface design. In this example Checksum calculation is defined for ‘\r’ not ‘\n’ delimiters. Checksum is also defined to exclude <STX>, but to include either <ETX>, or <ETB>. In another environment it potentially can be different. Then algorithm in function astmutil.checksum() in module astmutil has to be adjusted.
Main module
require('astm_send') require('stringutil') require('astmutil') -- Data (frames) must be enclosed in leading STX and trailing ETX, or ETB. -- 'ASTM Sender' has no way to know if frame has to be terminated with ETX -- or ETB unless it has been promptly specified in data itself. function main(Data) local data = Data:split('\002') table.remove(data,1) for i=1,#data do data[i]:trimRNL() data[i]='\002'..data[i] end data = addchecksum(data) local Success, err = astm_send.send( data, '192.168.5.4', -- host 6559, -- port 10, -- timeout true) -- live if not Success then iguana.logError(err) end end function addchecksum(data) for i=1,#data do local d = data[i]:trimRNL() local ck=astmutil.checksum(d) data[i]=d..ck..'\r' end return data end
Modules code
The astm_send module.
require('astm_tcp') local function astm_initiator(param) local dctrl={5,4} -- flow control ENQ and EOT values, respectively local gs local inconversation=false local tcp = astm_tcp.connect{host=param[2],port=param[3],timeout=param[4],live=param[5]} local function sendit(d) if type(d) == 'string' then iguana.logDebug ('sending '..d) tcp:send(d) else tcp:send(string.char(d)..'\n') end if d == dctrl[2] then return end while true do local s = tcp:recv() gs= s if gs then return gs end end end gs = sendit(dctrl[1]) iguana.logDebug ('sent ENQ signal') if not inconversation and string.byte(gs) ==6 then iguana.logDebug ('received ASTM ACK') gs=nil inconversation=true end if not inconversation and string.byte(gs) ==21 then iguana.logDebug ('received ASTM NACK') gs=nil end local repeats local function repeatsends(d) repeats=repeats-1 gs=sendit(d) if string.byte(gs) == 6 then iguana.logDebug ('received ASTM ACK') return elseif repeats > 0 then iguana.logDebug ('received ASTM NACK') repeatsends(d) else if repeats == 0 then iguana.logError('failed to transmit frame "'..d..'"') end return end end local function sendpayload() for k,v in ipairs(param[1]) do repeats = 5 repeatsends(param[1][k]..'\n') end end if inconversation then sendpayload() gs = sendit(dctrl[2]) iguana.logDebug ('sent EOT signal') elseif not inconversation then iguana.logError('remote not accepting') -- some repeat logic could be nice to have. return end tcp:close() end -- public astm_send = {} function astm_send.send(data,Host,Port,Timeout,Live) Success, err = pcall(astm_initiator,{data,Host,Port,Timeout,Live}) return Success, err end
The astm_tcp module.
-- -- Here we expose a single function, connect(), that opens -- an astm_tcp connection to a remote host, and returns a table -- to interact with. -- -- When debugging, it actually returns a fake connection, -- which can be sent ASTM messages, and -- replies with ACKs (one ACK per message sent). -- -- If you want to test a real astm_tcp connection in the editor, -- pass live=true along with your connection settings. -- -- require('astm_tcp') -- -- local s = astm_tcp.connect{host='frink',port=8086} -- s:send(Data) -- local Ack = s:recv() -- s:close() -- __astm_tcp = { send_astm_tcp = function(s, msg) local sent, text = 0, msg repeat sent = sent + s:send(text, sent+1) until sent >= text:len() return sent end; recv_astm_tcp = function(s, buf) buf=s:recv() return buf end; real_meta = { __index = { send = function(self, msg) return __astm_tcp.send_astm_tcp(self.s, msg) end; recv = function(self) local msg, skipped msg = __astm_tcp.recv_astm_tcp(self.s, self.buf) return msg end; close = function(self) self.s:close() end } }; -- -- Metatable for Simulation -- simulation_meta = { __index = { send = function(self, msg) if not self.connected then error('not connected', 2) end self.sent = msg return msg:len() end; recv = function(self) if not self.connected then error('not connected', 2) elseif not self.sent then error('timeout', 2) else local got = '\006' -- ASTM flow control ACK self.sent = nil return got end end; close = function(self) self.connected = false end } }; -- -- Error Checking -- check_arg = function(args, k, t, optional) local help = [[Connect to a remote astm_tcp host. Takes a table with the following required entries: 'host' - the hostname of the remote site 'port' - the port on the remote site and optionally these entries: 'timeout' - maximum wait time, in seconds (default 5s) 'live' - create live astm_tcp connections in the editor e.g. local s = astm_tcp.connect{host='hostname',port=8086} s:send(Data) local Ack = s:recv() s:close() ]] if not args then error(help, 3) elseif type(args) ~= 'table' then error('Parameter 1 is not a table.\n'..help, 3) elseif not optional and not args[k] then error("Parameter '"..k.."' is required.\n"..help, 3) elseif args[k] and type(args[k]) ~= t then error("Parameter '"..k.."' should be a "..t..'.\n'..help, 3) end end; } -- -- Public Interface -- astm_tcp = {} function astm_tcp.connect(args) local required, optional = false, true __astm_tcp.check_arg(args, 'host', 'string', required) __astm_tcp.check_arg(args, 'port', 'number', required) __astm_tcp.check_arg(args, 'timeout', 'number', optional) __astm_tcp.check_arg(args, 'live', 'boolean', optional) if args.live or not iguana.isTest() then -- Normal behaviour (in running channel). args.live = nil local Success, Socket = pcall(net.tcp.connect, args) if not Success then -- raise error to caller level error(Socket, 2) end return setmetatable({ s = Socket, buf = '', -- input buffer. }, __astm_tcp.real_meta) else -- Simulate behaviour while editing. return setmetatable({ connected = true, }, __astm_tcp.simulation_meta) end end
The astmutil module.
astmutil={} function astmutil.checksum(s) -- Some test data may have wrong delimiters, e.g. rn or single n instead of r. -- Checksum calculation is defined for r not n delimiters. -- Checksum below is defined to strip <STX>, but not <ETX>, nor <ETB>. s=s:gsub('n','r') local function zfill(s,N) if s:len() < N then repeat s = '0' .. s until s:len() == N end return s end local r=0 for b in string.gfind(s, ".") do if string.byte(b) == 2 then r=-2 end -- equivalent to strip <STX> which is x02 r=string.byte(b)+r end r=string.format("%x", tostring(((r)%256))):upper() r=zfill(r,2) return r end function astmutil.validate(s) local a = checksum(s:sub(1,-3)) local b = s:sub(-2,-1) if a == b then return true end end local function timestamp() local a,b = math.modf(os.clock()) if b==0 then b='000' else b=tostring(b):sub(3,5) end local tf=os.date('%Y-%m-%d %H:%M:%S.',os.time()) return (tf..b) end function astmutil.logs(s) local function writeBinary(binData) OUTPUT_DIR = 'C:listener2web' FILE_PATTERN = 'astmListenerDebug.log' fPath=OUTPUT_DIR..'' local fn=fPath..FILE_PATTERN local f = io.open(fn, "ab") f:write(binData) f:close() end local s = timestamp()..'t'..s..'nn' print(s) --[[ Please note that Windows cannot display this output properly in Command Prompt window. Rather read this output from suggested above log file or use a debugger. Having said that, this output will show all right in OS X terminal or in any *nix shell.]] writeBinary(s)-- should be wrtten to file or so ..., in production version. end function astmutil.postJson (s) local ltn12 = require 'ltn12' local http = require 'socket.http' local requestbody = s local responsebody b, c, h = http.request { url = "http://'admin':'password'@192.168.5.4:6545/astm", method = 'POST', headers = { ["Content-Type"] = "application/json", ["Content-Length"] = tostring(string.len(requestbody)) }, source = ltn12.source.string(requestbody), sink = ltn12.sink.table(responsebody) } if c == 200 then astmutil.logs('POST retuned OK') end end
The stringutil module: download the latest version of the stringutil module from our code repository.