-- Script V1 : 26_03_30

-- Reference for PIDs: https://en.wikipedia.org/wiki/OBD-II_PIDs

-- table with description for possible PIDs (makes easier to understand what the codes mean)
idToDesc = {
    [0x00] = 'PIDs supported 0x01-0x20',
    [0x01] = 'Monitor status since DTCs clear',
    [0x02] = 'DTC that caused Freeze Frame',
    [0x03] = 'Fuel system status',
    [0x04] = 'Calculated engine load / %',
    [0x05] = 'Engine coolant Temperature (ECT) /°C',
    [0x06] = 'Short term fuel trim Bank 1 / %',
    [0x07] = 'Long term fuel trim Bank 1 / %',
    [0x08] = 'Short term fuel trim Bank 2 / %',
    [0x09] = 'Long term fuel trim Bank 2 / %',
    [0x0A] = 'Fuel Pressure / kPa',
    [0x0B] = 'Intake manifold absolute pressure / kPa',
    [0x0C] = 'Engine speed / RPM',
    [0x0D] = 'Vehicle speed / km/h',
    [0x0E] = 'Timing advance / °',
    [0x0F] = 'Intake air temperature (IAT) / °C',
    [0x10] = 'Air flow rate (MAF) / g/s',
    [0x11] = 'Throttle position / %',
    [0x12] = 'Commanded secondary air status',
    [0x13] = 'Oxygen sensor present (2 banks)',
    [0x14] = 'Oxygen sensor 1',
    [0x15] = 'Oxygen sensor 2',
    [0x16] = 'Oxygen sensor 3',
    [0x17] = 'Oxygen sensor 4',
    [0x18] = 'Oxygen sensor 5',
    [0x19] = 'Oxygen sensor 6',
    [0x1A] = 'Oxygen sensor 7',
    [0x1B] = 'Oxygen sensor 8',
    [0x1C] = 'OBD standard supported',
    [0x1D] = 'Oxygen sensor present (4 banks)',
    [0x1E] = 'Aux input status',
    [0x1F] = 'Runtime since engine start / s',

    [0x20] = 'PIDs supported 0x21-0x40',
    [0x21] = 'Distance traveled since MIL on / km',
    [0x22] = 'Fuel rail pressure / kPa',
    [0x23] = 'Fuel rail gauge pressure / kPa',
    [0x24] = 'Oxygen sensor 1',
    [0x25] = 'Oxygen sensor 2',
    [0x26] = 'Oxygen sensor 3',
    [0x27] = 'Oxygen sensor 4',
    [0x28] = 'Oxygen sensor 5',
    [0x29] = 'Oxygen sensor 6',
    [0x2A] = 'Oxygen sensor 7',
    [0x2B] = 'Oxygen sensor 8',
    [0x2C] = 'Commanded EGR / %',
    [0x2D] = 'EGR error / %',
    [0x2E] = 'Commanded evaporative purge / %',
    [0x2F] = 'Fuel tank level input / %',
    [0x30] = 'Warmups since codes clear',
    [0x31] = 'Distance traveled since codes clear / km',
    [0x32] = 'Evaporative system pressure / Pa',
    [0x33] = 'Absolute barometric pressure / kPa',
    [0x34] = 'Oxygen sensor 1',
    [0x35] = 'Oxygen sensor 2',
    [0x36] = 'Oxygen sensor 3',
    [0x37] = 'Oxygen sensor 4',
    [0x38] = 'Oxygen sensor 5',
    [0x39] = 'Oxygen sensor 6',
    [0x3A] = 'Oxygen sensor 7',
    [0x3B] = 'Oxygen sensor 8',
    [0x3C] = 'Catalyst temperature: Bank 1, Sensor 1',
    [0x3D] = 'Catalyst temperature: Bank 2, Sensor 1',
    [0x3E] = 'Catalyst temperature: Bank 1, Sensor 2',
    [0x3F] = 'Catalyst temperature: Bank 2, Sensor 2',

    [0x40] = 'PID supported 0x41-0x60',
    [0x41] = 'Monitor status (this drive cycle)',
    [0x42] = 'Control module voltage / V',
    [0x43] = 'Absolute load value / %',
    [0x44] = 'Commanded air fuel eq. ratio / %',
    [0x45] = 'Relative throttle position / %',
    [0x46] = 'Ambient air temperature (AAT) / °C',
    [0x47] = 'Absolute throttle position B / %',
    [0x48] = 'Absolute throttle position C / %',
    [0x49] = 'Absolute throttle position D / %',
    [0x4A] = 'Absolute throttle position E / %',
    [0x4B] = 'Absolute throttle position F / %',
    [0x4C] = 'Commanded throttle actuator / %',
    [0x4D] = 'Time run with MIL on / min',
    [0x4E] = 'Time since DTC clear / min',
    [0x4F] = 'Max value for Air-fuel ration',
    [0x50] = 'Maximum value fro air flo from MAF / g/s',
    [0x51] = 'Fuel type',
    [0x52] = 'Ethanol fuel %',
    [0x53] = 'Absolute evap. system pressure / kPa',
    [0x54] = 'Evap. system pressure / Pa',
    [0x55] = 'Short term secondary trim',
    [0x56] = 'Short term secondary trim',
    [0x57] = 'Short term secondary trim',
    [0x58] = 'Short term secondary trim',
    [0x59] = 'Fuel rail abs. pressure / kPa',
    [0x5A] = 'Relative accelerator pedal position / %',
    [0x5B] = 'Hybrid battery pack remaining life / %',
    [0x5C] = 'Engine oil temperature / °C',
    [0x5D] = 'Fuel injection timing / °',
    [0x5E] = 'Engine fuel rate / L/h',
    [0x5F] = 'Emission requirement to which vehicle is designed',

    [0x60] = 'PIDs supported 0x61-0x80',
    [0x61] = 'Drivers demanded engine percentage torque / %',
    [0x62] = 'Actual engine percentage torque / %',
    [0x63] = 'Engine reference torque / Nm',
    [0x64] = 'Engine percent torque data / %',
    [0x65] = 'Aux input/output supported',
    [0x66] = 'Mass air flow sensor / g/s',
    [0x67] = 'Engine coolant temperature (ECT) / °C',
    [0x68] = 'Intake air temperature sensor / °C',
    [0x69] = 'Actual / Commanded / Error EGR',
    [0x6A] = 'Commanded diesel intake air flow control and relative intake air flow position',
    [0x6B] = 'Exhaust gas recirculation temperature',
    [0x6C] = 'Commanded throttle actuator control and relative throttle position',
    [0x6D] = 'Fuel pressure control system',
    [0x6E] = 'Injection pressure control system',
    [0x6F] = 'Turbocharger compressor inlet pressure',
    [0x70] = 'Boost pressure control',
    [0x71] = 'VGT control',
    [0x72] = 'Wastegate control',
    [0x73] = 'Exhaust pressure',
    [0x74] = 'Turbocharger RPM / RPM',
    [0x75] = 'Turbocharger temperature',
    [0x76] = 'Turbocharger temperature',
    [0x77] = 'Charge air cooler temperature',
    [0x78] = 'EGT bank 1',
    [0x79] = 'EGT bank 2',
    [0x7A] = 'DPF differential pressure',
    [0x7B] = 'Diesel particulate filter (DPF)',
    [0x7C] = 'DPF temperature',
    [0x7D] = 'NOx NTE control status area',
    [0x7E] = 'PM NTE control status area',
    [0x7F] = 'Engine run time',

    [0x80] = 'PID supported 0x81-0xA0',
    [0x81] = 'Engine run time for auxiliary emission control device (AECD)',
    [0x82] = 'Engine run time for auxiliary emission control device (AECD)',
    [0x83] = 'NOx sensor',
    [0x84] = 'Manifold surface temperature',
    [0x85] = 'NOx reagent system',
    [0x86] = 'PM sensor',
    [0x87] = 'Intake manifold absolute pressure',
    [0x88] = 'SCR induce system',
    [0x89] = 'Runtime for AECD #11-#15',
    [0x8A] = 'Runtime for AECD #16-#20',
    [0x8B] = 'Diesel aftertreatment',
    [0x8C] = 'O2 sensor',
    [0x8D] = 'Throttle position G / %',
    [0x8E] = 'Engine Friction - Percent torque / %',
    [0x8F] = 'PM sensor bank 1&2',
    [0x90] = 'WWH-OBD Vehicle OBD System info',
    [0x91] = 'WWH-OBD Vehicle OBD System info',
    [0x92] = 'Fuel control system',
    [0x93] = 'WWH-OBD Vehicle OBD counters support',
    [0x94] = 'NOx warning and inducement system',
    [0x95] = '',
    [0x96] = '',
    [0x97] = '',
    [0x98] = 'EGT sensor',
    [0x99] = 'EGT sensor',
    [0x9A] = 'Hybrid/EV vehicle system data/battery/voltage',
    [0x9B] = 'Diesel exhaust fluid sensor data',
    [0x9C] = 'O2 sensor data',
    [0x9D] = 'Engine fuel rate / g/s',
    [0x9E] = 'Exhaust engine fuel rate / kg/h',
    [0x9F] = 'Fuel system percentage use',

    [0xA0] = 'PID supported 0xA1-0xC0',
    [0xA1] = 'NOx sensor corrected data',
    [0xA2] = 'Cylinder fuel rate',
    [0xA3] = 'Evap system vapor pressure',
    [0xA4] = 'Transmission actual gear',
    [0xA5] = 'Commanded diesel exhaust fluid dosing',
    [0xA6] = 'Odometer',
    [0xA7] = 'NOx sensor concentration sensor 3 and 4',
    [0xA8] = 'NOx sensor corrected concentration sensor 3 and 4',
    [0xA9] = 'ABS disable switch state',
    [0xAA] = '',
    [0xAB] = '',
    [0xAC] = '',
    [0xAD] = '',
    [0xAE] = '',
    [0xAF] = '',
    [0xB0] = '',
    [0xB1] = '',
    [0xB2] = '',
    [0xB3] = '',
    [0xB4] = '',
    [0xB5] = '',
    [0xB6] = '',
    [0xB7] = '',
    [0xB8] = '',
    [0xB9] = '',
    [0xBA] = '',
    [0xBB] = '',
    [0xBC] = '',
    [0xBD] = '',
    [0xBE] = '',
    [0xBF] = '',

    [0xC0] = 'PID supported 0xC1-0xE0',
    [0xC1] = '',
    [0xC2] = '',
    [0xC3] = '',
    [0xC4] = '',
    [0xC5] = '',
    [0xC6] = '',
    [0xC7] = '',
    [0xC8] = '',
    [0xC9] = '',
    [0xCA] = '',
    [0xCB] = '',
    [0xCC] = '',
    [0xCD] = '',
    [0xCE] = '',
    [0xCF] = ''
}

-- parameters: buffer to convert
-- return: string representing the given buffer
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

-- parameters: input buffer (string of byte), position to start from, length of byte to get the number from
-- returns: number extracted / nil on errors
function readNumber(buf, pos, len)
    local val = nil
    if ((len > 0) and (len <= 4)) then 
        val = 0
        endpos = pos+len-1
        -- Validation of the available data respect to the passed requested length
        if (endpos > #buf) then return nil  end
        for idx = pos, endpos do
            val = (val * 256) + string.byte( string.sub(buf, idx) )
        end 
    end 
    return val
end

-- parameters: U32 value to convert
-- returns: string with the bits representation
function getU32AsBinaryString(val)
  local bits = {}
  for i = 31, 0, -1 do
    local b = (val & (1 << i)) >> i
    table.insert(bits, b)
  end
  return table.concat(bits)
end

-- parameters: remoteID so send request to, lcoalID to receive the message on, filename to log data into
-- returns: nil (all the relevant work is done inside the function)
function handleSAESupportMap(rid, lid, filename)
    local rc
    local resp
    local done = false
    local DID = 0x0000
    local supCnt = 0

    -- open the log file to write to
    local fw = io.open(filename, "w")
    if (fw == nil) then
        print('We have not been able to open the log file.')
        return
    end
    
    -- it is useful to add the useful information to read the log file later
    fw:write('RemoteID = ' .. string.format('%08X', rid) .. '\n')
    fw:write('LocalID = ' .. string.format('%08X', lid) .. '\n')
    fw:write('\n')
    
    -- SAE 0x01 can be parsed in a loop fashion
    repeat
        fw:write('\nDID: ' .. string.format('%04X', DID) ..'\n')
        
        rc, resp = uds.xmit(rid, 0x01, DID, "", lid, 100)
        
        if (rc ~= 0) then
            print('Error in ECM communication: ' .. string.format('%08X', rc))
            fw:write('Error in ECM communication: ' .. string.format('%08X', rc) .. '\n')
            -- in case of communication error we stop the script altogether
            return
        end
        
        local map = readNumber(resp, 1, 4);
        fw:write( string.format('(Data:%s) [Value:%X] - %s\n', hex_dump(resp), map, getU32AsBinaryString(map)) )

        -- Bit position:         31, 30,  .......  ,  1,  0
        -- SAE map positions:     1,  2,  .......  , 31, 32
        for idx = 1, 32 do
            local bm = (1 << (32 - idx))
            --fw:write( string.format('map[%02d] = %08X\n', idx, (map & bm)) )
            if (map & bm) ~= 0 then
                -- bit is 1 / True
                -- NOTE: the bitmap starts with values from 1 , not 0, so the last (bit 31 == 0x20(32d))
                fw:write(string.format('%04X', (DID + idx)) .. ' is supported. ('.. idToDesc[(DID + idx)]  ..') \n' )
                supCnt = supCnt + 1
            else 
                -- last bit is 0 / False -> no more component of the SAE bitmap
                if idx == 32 then done = true end
            end
        end
        
        -- prepare the DID to get the next SAE supported map part
        DID = DID + 0x20
        
        -- safe clean condition: single byte request DID is used by SID 0x01
        if DID > 0xFF then done = true end
        
    until done == true
    
    print('Found ' .. supCnt .. ' SAE PID supported by request at 0x01')
    fw:write('\n\nFound ' .. supCnt .. ' SAE PID supported by request at 0x01\n')
    
    fw:close()
end


print('START...')

print('Turn Key ON for testing..')

ui.sleep(100*5) --sleep 5 secs

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)
if (rc ~= 0) then
    error("CAN Init failed! Error: " .. rc .. " (Invalid BUS / Invalid CAN baudrate / Key off)")
end

can.disableFilters()

can.startLog('OBDTLog.log')

ui.sleep(100*5) --sleep 5 secs

print('Testing 7E0-7E8')
handleSAESupportMap(0x7E0, 0x7E8, '7E07E8.txt')

ui.sleep(100*5) --sleep 5 secs

print('Testing 0x18DB33F1-0x18DAF110')
handleSAESupportMap(0x18DB33F1, 0x18DAF110, '18DB33F1.txt')

ui.sleep(100*5) --sleep 5 secs

-- test for FIAT
print('Testing 0x18DA40F1-0x18DAF140')
handleSAESupportMap(0x18DA40F1, 0x18DAF140, '18DA40F1.txt')

print('Turn Key OFF for testing..')

ui.sleep(100*5) --sleep 5 secs

can.stopLog()

can.enableFilters()
can.uninit()

print('DONE.')