Translator as ASTM TCP Client (Sender)

Sending ASTM messages using Translator is not much different from sending HL7 messages. For this purpose we will use well known ‘llp’ module, 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.

  1. The astm_send module handles ASTM specific protocol to send a message
  2. The astn_tcp module handles TCP communications
  3. The astmutil module contains functions to calculate checksum digits, write debug logs, and send HTTP POST request to Iguana Webserver
  4. 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:

  1. 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.
  2. The webserver access credential in function astmutil.postJson () in module astmutil may have to be adjusted as well.
  3. 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.

Leave A Comment?