-- Script V1 : 26_03_30

-- handling of logging for this script
doCANLogging = false
generateScriptLog = false

-- filename where the file index is kept
idxFile = 'DTCIdx.txt'

-- definition of UDS communication timeout
xmitTimeout = 100*1



-- #####################
-- FUNCTIONS --
-- #####################

-- Returns a string with all the bytes in HEX format, separated by spaces
function hex_dump(buf)
    if buf == nil then return '' end
    local txt = ''
    for byte=1, #buf, 16 do
        local chunk = buf:sub(byte, byte+15)
        chunk:gsub('.', function (c) txt = txt .. string.format('%02X ',string.byte(c)) end)
    end
    return txt
end

 
-- Returns true if the file exists, false otherwise
function file_exists(name)
   local f = io.open(name, 'r')
   if (f ~= nil) then 
        io.close(f) 
        return true 
    else 
        return false 
    end
end


-- Prints the give text to the display and, if outFile is defined, into the file too
-- NOTE: Set outFile as global so it can be accessed from inside functions!
function log(text, skip_view)
    if generateScriptLog == false then return end -- if the script log is not required, just exit here
    
    skip_view = skip_view or nil 
    -- handle saving to file,. if a filename has been specified
    if (outFile ~= nil) then
        file = io.open (outFile , 'a+')
        file:write(text..'\n') -- When writing to file you have to manually add the new line separator on your own
        file:close()
    end
    -- show the text to the screen
    if skip_view == nil then
        print(text)  --Commented out to not have prints in this script
    end
end


-- Manages the automatic retrieving and updating of and index file
-- Returns the current index to be used (0-255) with rollover
function getNextIndexFrom(idxFile) 
    local idx = 0
    local nIdx = 0
    
    --Read the next index to use
    if file_exists(idxFile) then
        local f = io.open(idxFile, 'r')
        idx = f:read('*n') --reads a number in lua-formatted format
        --print('idxFile ' .. idxFile .. ' read: ' .. idx) --DEBUG
        f:close()
    end
    
    nIdx = idx + 1
    if nIdx > 0xFF then 
        nIdx = 0 
    end --rollover
    
    --save the new index in the file for next time
    f = io.open(idxFile, 'w')
    f:write(nIdx) 
    f:close()

    --print('Index file updated to' .. string.format('%02X',nIdx)) --DEBUG
    return idx
end


-- This is the MOST GENERIC version!
function getNumberAt(data, pos, size)
    log( string.format('%d <-> %d', data:len(), pos+(size-1)) )
    if data:len() < pos+1 then 
        -- not enough bytes in data
        log('getNumberAt: not enough data')
        return nil 
    end 
    local x = 0
    for i = pos, pos+(size-1) do 
        x = (x * 256) + data:byte(i)
        --print( string.format(' >> 0x%04X', x) )
    end
    return x
end



-- Extract the description of a give DTC from the passed file.
-- In order to function properly, the source file for the description must respect some requirements:
-- * all the lines must start with the DTC code that the description refers to
-- * all line must have the same length (now hard-coded as 88 bytes)
function getDecriptionForDTC(dtc, dtcDescFile)

    local dataFile = io.open (dtcDescFile, 'r')
    
    -- handle the case of midding DTC description file
    if dataFile == nil then 
        return dtc 
    end
    
    -- read initial non-printable characters
    while true do
        local c = dataFile:read(1) --read a single char
        if c >= '0' and c <= 'Z' then break end
--      log( 'Dropped char : ' .. string.byte(c) )
    end

    local dataStart = dataFile:seek() - 1

    --rewind by 1 so we have the beginning of the first printable character ready
    dataFile:seek('set', dataStart)

    -- count the number of lines in the file
    dataFile:seek('end')
    local fileLen = dataFile:seek()
    dataFile:seek('set', dataStart)

    local LINE_LEN = 88
    local numOfLines = math.floor((fileLen - dataStart) / LINE_LEN)
    local idxStart = 0
    local idxEnd = numOfLines

    local msg = ''
    local done = false
    local idx = 0
    local prevIdx
    
    while done == false do
        prevIdx = idx
        -- find middle line
        idx = math.floor((idxEnd + idxStart) / 2)
        curFilePos = dataStart + (idx * LINE_LEN)
        dataFile:seek('set', curFilePos)
        -- log( string.format('Comparing idx %d (s:%d - e:%d) at %d', idx, idxStart, idxEnd, curFilePos) )
        
        -- get a line form the file
        dataLine = dataFile:read('*line')
        
        if dataLine == nil then 
            -- reached the end of the line and code not found (append only the CODE)
            -- log( string.format( '\tEnd of file reached. DTC %s not found.', dtc) )
            msg = msg .. dtc .. '\n' 
            break 
        end 
        
        -- extract the error code form the data file line
        local code = dataLine:sub(1,5)
        
        -- log ( string.format('Comparing %s <-> %s', dtc, code) )

        -- compare with the DTC info
        if dtc == code then
            -- exit if found
            -- log( 'DCT found as ' .. dataLine )
            msg = msg .. dataLine .. '\n'
            done = true
        else 
            if (idxStart == idxEnd or idx == prevIdx) then
                -- start and end are the same line... this did not match -> not found
                -- idx was not update respect to previous iteration -> not found
                -- log( string.format( '\tEnd search reached. DTC %s not found.', dtc) )
                msg = msg .. dtc .. '\n' 
                break 
            end
            
            if dtc > code then
                -- dtc is AFTER the code at the current line
                idxStart = idx
                -- log( string.format('Updating idxStart: %d', idxStart) )
            else
                -- dtc is BEFORE the code at the current line
                idxEnd = idx
                -- log( string.format('Updating idxEnd: %d', idxEnd) )
            end
        end
        
    end -- ENDOF WHILE done

    dataFile:close()    
    
    return msg
end --ENDOF getDecriptionForDTC




function parseDTCs(data, req, qty)
--[[
      0x03            02          C0 19                  90 AA
    0x1902            4F          C0 19 01 08            90 AA 4A 08  
    0x1908            4F    80 40 C0 19 01 08      40 40 90 AA 4A 08
    
    C019 -> (11 00) 0 1 9 -> U 0 0 1 9
    90AA -> (10 01) 0 A A -> B 1 0 A A
]]    

    -- NOTE: in LUA all indexes start a 1, not 0 ! (Start of 2 means to drop the first byte only!)
    --             Start of data, each data len, bytes to drop for each entry
    defs = { 
        [0x03] =   {2,             2,             0},
        [0x1902] = {2,             4,             0},
        [0x1908] = {2,             6,             2}, 
    }
  
    letter = {
        [0] = 'P',
        [1] = 'C',
        [2] = 'B',
        [3] = 'U',
    }
    
    if qty == nil then
        log('parseDTC called with qty = nil...')
        return nil
    end
    
    if qty == 0 then
        log('parseDTC called with qty = 0...')
        return nil
    end
    
    
-- TESTING section
    
--    P2122 / P0118 / P2228 / P2509 / P1193 / P0113 / P0107 / P0472 / P040D / P0193 / P0254 / P2127 / P051C 
--    qty = 0x0D
--    req = 0x03
--    data = '\x0D\x21\x22\x01\x18\x22\x28\x25\x09\x11\x93\x01\x13\x01\x07\x04\x72\x04\x0D\x01\x93\x02\x54\x21\x27\x05\x1C' -- result of 0x03   

--    U0019 / B10AA
--    qty = 2
--    req = 0x1902 
--    data = '\x4F\xC0\x19\x01\x08\x90\xAA\x4A\x08' -- result of 0x19 0x02 --test
    
--    U0019 / B10AA    
--    qty = 2
--    req = 0x1908
--    data = '\x4F\x80\x40\xC0\x19\x01\x08\x40\x40\x90\xAA\x4A\x08' -- result of 0x19 0x08 --test
  
    local dtcListTxt = ''
  
    d = defs[req]
    if d == nil then return -1 end --invalid req parameter
    for idx = 0, qty - 1 do
        local pos = d[1] + (idx * d[2]) + d[3]

        --log( string.format('%d) pos = %d', idx, pos) )
        --local dtc = getNumberAt(data, pos, 2)
        --log( string.format('Got dtc = %d(%04X)', dtc, dtc) )
        
        local hb = data:byte(pos)
        local lb = data:byte(pos+1)

        --log( string.format('%02X %02X', hb, lb) )

        local c1 = (hb & 0xC0) >> 6
        local c2 = (hb & 0x30) >> 4
        local c3 = (hb & 0x0F)
        local c4 = (lb & 0xF0) >> 4
        local c5 = (lb & 0x0F)

        --line = string.format('%d) @ %d (%02X %02X) %s %X %X %X %X', idx, pos, hb, lb, letter[c1], c2, c3, c4, c5)
        
        local l = letter[c1]
        line = string.format('%d) @ %d (%02X %02X) %s %X %X %X %X', idx, pos, hb, lb, l, c2, c3, c4, c5)
        
        -- construct the compatible DTC string 
        dtc = string.format('%s%X%X%X%X', l, c2, c3, c4, c5)
        -- log( string.format('Looking for description of %s', dtc) )
        
        local dtcDest = getDecriptionForDTC(dtc, 'data\\dtc.txt')
        line = string.gsub(dtcDest, '^%s*(.-)%s*$', '%1')
        
        -- log(line)
        
        dtcListTxt = dtcListTxt .. line .. '\n\n' -- REAL
    end
    return dtcListTxt
end --ENDOF parseDTCs

    
firstDTCsFound = false  
function handleDTCs(rID, lID, padding, n, sidClr, didClr, dataClr, dtcListTxt, reqTitle)
    local done = false
    local req = nil

    -- recap the parameter this function has been called with
    -- this helps in development, can be removed when script is done
    
    local file = nil
    
    if firstDTCsFound == false then
        -- first time in this run that we found DTCs, overwrite previous file
        file = io.open ('DTC.txt' , 'w')
        firstDTCsFound = true
    else
        -- in this run we already found DTCs ... open in append
        file = io.open ('DTC.txt' , 'a+')
    end
    
    file:write(reqTitle .. '\n') 
    file:write(dtcListTxt .. '\n\n')
    file:close()
    
    if didClr == nil then
        req = string.format( '%02X', sidClr )
    else 
        req = string.format( '%02X%04X', sidClr, didClr )
    end
    log( string.format('handleDTCs (%s) p:%s n:%d' , req, tostring(padding), n) )
    
    if n > 0 then
        log( string.format('Clearing DTCs will be with padding: %s', tostring(curPadding)) )
        
        -- show the list of DTCs found
        choice = ui.messageBox( reqTitle, dtcListTxt, 'w',  'Clear', 'Leave')
        if choice == 'Clear' then
            -- user wants to clear the DTCs
            rc, resp = uds.xmit(rID, sidClr, didClr, dataClr, lID, xmitTimeout, curPadding)
            
            log( string.format('Clearing DTCs using command %02X returned: %08X', sidClr, rc) )
            if rc == 0 then
                -- OK, DTCs should have been cleared...
                ui.messageBox('DTCs Clear', 'DTC Clear command accepted.', 'i', 'OK')
                done = true
            else
                -- Clear was not performed...
                ui.messageBox('DTCs Clear', string.format('DTC Clear command failed with error: %d', rc), 'e', 'OK')
                -- done is already a false from the beginning
            end
        else 
            -- use do NOT want to clear DTCs
            -- done is already a false from the beginning
        end
    else 
        -- DTCs count is 0 -> do nothing (count as cleared)
        done = true 
    end
    
    return done
end --EDNOF function handleDTCs
    

-- #####################
-- BEGIN OF EXECUTION --
-- #####################

-- NOTE: after the script execution, this file contains the NEXT index to be used!!!
-- So, if you want to know which was the last Log used, take the content of this file and do -1!!!

if (doCANLogging == true or generateScriptLog == true) then
    -- if at least one between doCANLogging and generateScriptLog is enable, we have to update the index file
    fileNum = getNextIndexFrom(idxFile)

    if generateScriptLog == true then
        -- For uniformity with the CAN log, we save in the root also the VIN test result
        outFile = string.format('DTCD_%02X.txt', fileNum)
    end 
    
    if doCANLogging == true then 
        -- CanLog MUST be done only in the ROOT (at least for now.. 2023_12_06) because it is verified to be 8.3 file name
        canLogFile = string.format('DTCDC_%02X.log', fileNum)
    end
end


log('Turn key ON ...')
ui.sleep(100 * 1)

-- Get the CAN Baudrate set by the user Display
local rc, currentBR = can.getCurrentBaudrate()
if (currentBR == 0) then
    --Ask the user here, since there is no current CAN baudrate set and validated
    currentBR = ui.inputValue('Set CAN Baudrate', 'u', 1, 500000, 500000)
end

if (currentBR == nil) then 
    error('No CAN Baudrate provided.')
end

rc = can.init(currentBR)
log( string.format('can.init with baudrate %d: %d', currentBR, rc) )
if (rc ~= 0) then
    error("Failed to connect to CAN bus!")
end

can.disableFilters()

if doCANLogging == true then 
    can.startLog(canLogFile)
end

local rc
local data

-- This table represents the CAN IDs to be tried
local AllIDs = {
    {0x7E0, 0x7E8},
    {0x18DA40F1, 0x18DAF140},
    {0x18DA10F1, 0x18DAF110},
    {0x18DA28F1, 0x18DAF128},
}

local Paddings = {true, false}

-- Local variables for CAN UDS requests
local rID
local lID

-- Loop on the defined CAN IDs
for i = 1, #AllIDs do

    rID = AllIDs[i][1]
    lID = AllIDs[i][2]

    log ( string.format('Testing couple (%08X:%08X)', rID, lID) )

    can.setFilters(lID)
    can.enableFilters()

    local n = 0
    local dtcListTxt = nil
    local curPadding = nil
    
    local handled = false
        
    for p = 1, #Paddings do
        curPadding = Paddings[p]
        log ('\nTesting padding: ' .. tostring(curPadding))
    
        --let's try with the 0x03 (OBD) service
        rc, resp = uds.xmit(rID, 0x03, nil, '', lID, xmitTimeout, curPadding)
        
        log( string.format('0x03 req rc: %08X', rc) )
        if rc == 0 or rc == 3 then
            -- Got valid answer (may be NAK though)
            log( hex_dump(resp), true ) 
            
            if rc == 0 then 
                -- for 0x03 we already have everything we need: first byte is the count, and is followed by the data
                n = resp:byte(1) --take the first byte as len
                log( string.format('0x03 found %d(%02X) DTCs active.', n, n) ) 
                if n ~= 0 then
                    dtcListTxt = parseDTCs(resp, 0x03, n)
                    handled = handleDTCs(rID, lID, curPadding, n, 0x04, nil, '', dtcListTxt, string.format('%d DTCs found for %08X:03', n, rID))
                else
                    ui.messageBox( string.format('DTCs found for %08X:03', rID), string.format(' No DTCs found for %08X:03', rID), 'i', 'OK')
                end
            end 
            
            -- since we got a valid response, the ECU accepted this padding. Now we can skip other attempts
            break 
        else
            -- NO response to 0x03: Do nothing, we can continue with the other test
        end
    end
            
        
    for p = 1, #Paddings do
        curPadding = Paddings[p]
        log ('\nTesting padding: ' .. tostring(curPadding))
        
        -- let's test the 0x19 service
        -- start by asking for reportNumberOfDTCByStatusMask (0x01)
        rc, resp = uds.xmit(rID, 0x19, 0x01, '\x08', lID, xmitTimeout, curPadding) --0x08 = confirmedDTCs only
        log( string.format('0x19 0x01 req rc: %08X', rc) )
        if rc == 0 then
            -- Got valid answer 
            log( hex_dump(resp), true )

            if resp:len() == 3 then
                n = getNumberAt(resp, 2, 2) --500
            elseif resp:len() == 4 then
                n = getNumberAt(resp, 3, 2) --Doblo
            else
                log( '0x1901 returned unsupported response length: ' .. tostring(resp:len()) )
            end
            log( string.format('0x1901 found %d(%04X) DTCs active.', n, n) )      
            
            if n ~= 0 then
                -- we have the DTC count -> we can ask for reportDTCByStatusMask (0x02)
                rc, resp = uds.xmit(rID, 0x19, 0x02, '\x08', lID, xmitTimeout, curPadding) --0x08 = confirmedDTCs only
                log( string.format('0x19 0x02 req rc: %08X', rc) )
                if rc == 0 then
                    -- Got valid answer (or NAK but already tells us that ECM is responding there)
                    log( hex_dump(resp), true )
                    if n ~= 0 then
                        dtcListTxt = parseDTCs(resp, 0x1902, n) 
                        handled = handleDTCs(rID, lID, curPadding, n, 0x14, nil, '\xFF\xFF\xFF', dtcListTxt, string.format('%d DTCs found for %08X:1902', n, rID))
                    end
                else
                    -- No DTC found -> Do nothing, we can go on with the other test!
                end
            else
                -- No DTC found -> Do nothing, we can go on with the other test!
                ui.messageBox( string.format('DTCs found for %08X:1901', rID), string.format(' No DTCs found for %08X:1901', rID), 'i', 'OK')
            end
            
            -- since we got a valid response, the ECU accepted this padding. Now we can skip other attempts
            break 
            
        else
            -- Error reading DTC count -> Do nothing, we can go on with the other test!
        end
    end


    for p = 1, #Paddings do
        curPadding = Paddings[p]
        log ('\nTesting padding: ' .. tostring(curPadding))
        
        -- let's test the 0x19 service
        -- start by asking for reportNumberOfDTCByStatusMask (0x01)
        rc, resp = uds.xmit(rID, 0x19, 0x07, '\xE0\x08', lID, xmitTimeout, curPadding) --0xE0=maintenanceOnly|checkAtNextHalt|checkImmediately  0x08=confirmedDTCs only
        log( string.format('0x19 0x07 req rc: %08X', rc) )
        if rc == 0 then
            -- Got valid answer 
            log( hex_dump(resp), true )

            if resp:len() == 3 then
                n = getNumberAt(resp, 2, 2) --500
            elseif resp:len() == 4 then
                n = getNumberAt(resp, 3, 2) --Doblo
            else
                log( '0x1901 returned unsupported response length: ' .. tostring(resp:len()) )
            end
            log( string.format('0x1907 found %d(%04X) DTCs active.', n, n) )      
            
            if n ~= 0 then
                -- we have the DTC count -> we can ask for reportDTCByStatusMask (0x02)
                rc, resp = uds.xmit(rID, 0x19, 0x08, '\xE0\x08', lID, xmitTimeout, curPadding) --0xE0=maintenanceOnly|checkAtNextHalt|checkImmediately  0x08=confirmedDTCs only
                log( string.format('0x19 0x08 req rc: %08X', rc) )
                if rc == 0 then
                    -- Got valid answer (or NAK but already tells us that ECM is responding there)
                    log( hex_dump(resp), true )
                    if n ~= 0 then
                        dtcListTxt = parseDTCs(resp, 0x1908, n) 
                        handled = handleDTCs(rID, lID, curPadding, n, 0x14, nil, '\xFF\xFF\xFF', dtcListTxt, string.format('%d DTCs found for %08X:1908', n, rID))
                    end
                else
                    -- No DTC found -> Do nothing, we can go on with the other test!
                end
            else 
                -- No DTC found -> Do nothing, we can go on with the other test!
                ui.messageBox( string.format('DTCs found for %08X:1907', rID), string.format(' No DTCs found for %08X:1907', rID), 'i', 'OK')
            end
            
            -- since we got a valid response, the ECU accepted this padding. Now we can skip other attempts
            break 
        else
            -- Error reading DTC count -> Do nothing, we can go on with the other test!
        end
    end
            
    log ( string.format('Testing couple (%08X:%08X) DONE.\n\n', rID, lID) )
end --ENDOF loop on all IDs

if doCANLogging == true then 
    can.stopLog()
end

can.enableFilters()
can.uninit()

--recap
log('-----------------------------------')

local lastMsg = 'DTC check completed.'

if generateScriptLog == true then
    log('Results saved in: ' .. outFile)
end

if doCANLogging == true then 
    log('CAN Log saved in: ' .. canLogFile)
    lastMsg = lastMsg .. '\nCAN Log saved in: ' .. canLogFile
end

ui.messageBox('DTCs', lastMsg, 'i', 'OK')


log('DONE.')
