--[[ Protocol : 3964(R) Template by : IFTOOLS GmbH Version : 2014.04.24 Initial commit (first version) Version : 2015.04.24 Correct handling of DLE ETX sequences in payload Version : 2018.11.01 Revise split mechanism and fix some issues with DLE pairs in the payload Version : 2018.11.12 Add display of NAK telegrams Version : 2018.11.13 Add global parameter for BCC Version : 2020.04.29 Adapt renamed cfg, bit and protocol module names Version : 2023.06.01 Add setup dialog to select 3964 or 3964(R) (for BCC) and optional show S5 Header as separate data field Version : 2024.03.28 Replace obsolete bit32 calls with native Lua operators Brief: A standardized serial point to point protocol, commonly used between two PLC (programmable logic controller, in German SPS). The protocol has been very popular in process automation, in particular for data exchange between devices with Simatic-SPS. The 3964R protocol is also referred to as DUST. --]] -- the following characters are parametrizable by application local STX = 0x02 local ETX = 0x03 local DLE = 0x10 local NAK = 0x15 local S5HEAD = 0x3d --[[ Initiate all global variables shared between the split/out/dialog --]] if not widgets.BCC then -- 1 means BCC is enabled/active widgets.BCC = 1 end if not widgets.S5HEAD then --1 means S5 Header is enabled/active, default is off widgets.S5HEAD = 0 end -- finally restore the last settings from the dialog widgets.LoadSettings() -- function split is called every time a data byte is received -- with the following parameters: -- data : the data value (9 bit) -- intval : the time distance to the former data (unused) -- alter : is true when the data source (direction) was changed (unused) -- str : all received data bytes of the current telegram as a string -- returns the telegram state: STARTED, MODIFIED, COMPLETED function split( data, intval, alter, str, filter ) -- we use the two global variables complete and last for the -- correct telegram splitting! Lua creates these variables on the -- fly when first specified! local BCC = widgets.BCC -- first check for 1 byte telegrams like STX, DLE, NAK if #str == 1 then -- NOTE! complete is global! complete = 0 if data == STX or data == DLE or data == NAK then return STARTED+COMPLETED -- this is the first byte of the data block else return STARTED end end --[[ A DLE in the data payload is escaped with another DLE (a DLE pair). Such DLE pairs are NOT used for the DLE ETX end sequence and must treated special. --]] if data == DLE then if last == DLE then -- NOTE! last is global! last = nil else last = DLE end end if data == ETX and complete == 0 then if last == DLE then last = nil -- the sequence is complete, we only wait for the final BCC if BCC == 1 then complete = 1 else return COMPLETED end end else -- any next byte following the DLE ETX (complete = 1) is the BCC if complete == 1 then complete = 0 last = nil return COMPLETED end end return MODIFIED end function out() -- the telegram relating to the current line local telegram = telegrams.this() bgcolor = { 0xFF8585, 0x75BBFF } if telegram:data(1) == STX then stx( telegram ) elseif telegram:data(1) == DLE then dle( telegram ) elseif telegram:data(1) == NAK then nak( telegram ) else data( telegram ) end --else error( telegram ) end end function GetBackground( dir ) local bgcolor = { 0xFF8585, 0x75BBFF } return bgcolor[ dir ] end function data( telegram ) local BCC = widgets.BCC local fgColors = {0x800000,0x000080} -- check if the data follows a former DLE local tp = telegrams.prev() if tp and tp:size() == 1 and tp:data(1) == DLE then box.space{em=7} end local bgcolor = GetBackground( telegram:dir() ) local data = telegram:string():sub(1,-4) if widgets.S5HEAD == 1 then box.text{ caption="S5", text=tohex(data:byte()), bg=bgcolor } -- remove the S5 header byte from the data data = data:sub(2) end if IsPrintable( data ) then -- the data as readable string box.text{ caption="Data", text=data, bg=0xFFFF8C, fg=fgColors[telegram:dir()] } else box.text{ caption="Data", text=data:dump( data ), bg=0xFFFF8C, fg=fgColors[telegram:dir()] } end -- the DATA telegram structur: -- (S5)[D1][D2]....[Dn][DLE][EXT][BCC] -- DLE index is -3, ETX index -2, BCC index -1 (last byte) -- the last byte in the telegram is the BCC if BCC == 1 and telegram:size() >= 3 then box.text{ caption="DLE", text=string.format( "%02X", telegram:data(-3)), bg=bgcolor } box.text{ caption="ETX", text=string.format( "%02X", telegram:data(-2)), bg=bgcolor } local chksum_is = telegram:data( -1 ) local chksum_must = chksum( telegram ) if chksum_is ~= chksum_must then box.text{ caption="BCC", text="Invalid! Is:"..chksum_is..", must:"..chksum_must, bg=0xFF5000, fg=0xFFFFFF } else box.text{ caption="BCC", text=string.format( "%02X",chksum_is), bg=0x00FF00 } end elseif telegram:size() >= 2 then box.text{ caption="DLE", text=string.format( "%02X", telegram:data(-2)), bg=bgcolor } box.text{ caption="ETX", text=string.format( "%02X", telegram:data(-1)), bg=bgcolor } end end function dle( telegram ) box.space{em=3} box.text{ caption="DLE", text=string.format("%02X", telegram:data(1)), bg=GetBackground( telegram:dir() ) } end function nak( telegram ) box.space{em=3} box.text{ caption="NAK", text=string.format("%02X", telegram:data(1)), bg=GetBackground( telegram:dir() ) } end function stx( telegram ) box.text{ caption="STX", text=string.format("%02X", telegram:data(1)), bg=GetBackground( telegram:dir() ) } end function error( telegram ) box.text{ caption="Error", text="Unknown telegram: "..telegram:data(1), bg=0xFF0000 } end function chksum( telegram ) local cks = 0 for i=1,telegram:size()-1 do cks = telegram:data(i) ~ cks end return cks end function tohex( data ) if data then return string.format("%02X", data ) else return "?" end end function IsPrintable( data ) for c in string.gmatch( data, "." ) do if c:byte(1) < 0x20 or c:byte() > 0x7F then return false end end return true end function dialog() local row = 1 widgets.SetTitle( "3964(R) Setup" ) widgets.RadioBox{ name="type", label="Type", choices={"3964","3964(R)"}, row=row, col=1, orientation="horizontal", fill=true} row = row + 1 widgets.CheckBox{ name="s5", label="S5 Head",row=row, col=1 } row = row + 1 if widgets.BCC == 1 then widgets.SetValue( "type", "3964(R)" ) else widgets.SetValue( "type", "3964" ) end widgets.SetValue( "s5", widgets.S5HEAD == 1 ) end function apply() if widgets.GetValue( "type" ) == "3964(R)" then widgets.BCC = 1 else widgets.BCC = 0 end if widgets.GetValue( "s5" ) == true then widgets.S5HEAD = 1 else widgets.S5HEAD = 0 end -- save last settings widgets.SaveSettings() -- and force a complete re-parsing since a different number of address bytes -- changes all return "RELOAD" end -- NOTE! The parameter value is always of type string function callback_s5( value ) if value == "true" then widgets.S5HEAD = 1 else widgets.S5HEAD = 0 end end