Transmission Control for 4 gears; more, maybe later!

It's all about the code!
Post Reply
User avatar
NormanAlphaspeed
Posts: 79
Joined: Fri Jan 13, 2017 7:15 am
Location: Puerto Rico
Contact:

Transmission Control for 4 gears; more, maybe later!

Post by NormanAlphaspeed »

Hey guys
I made this Lua, which I have succesfully driven on to control a 4R70W

Right now it has Intended vs Desired checking, Downshift RPM lockout, and uses two seperate shift tables which have TPS on X axis, and shift direction on Y axis (logic I copied from GM OEM tunes). It has a few other features which you cna paste the code in Gemini and it'll properly comment the code more lol, but here it is; later I will update with videos of the truck i'm using running

Code: Select all

-- TCU Lua Script: Normal var names, Corrected VSS logic, Toggleable filters

-- Configuration Flags (Manually change for testing)
local enableVssSpikeFilter = false   -- Set to false to disable VSS spike filtering
local enableGearErrorCheck = false   -- Set to false to disable gear match error detection

-- Sensor Declarations
local cltSensor = Sensor.new("Clt")
local rpmSensor = Sensor.new("Rpm")
local mapSensor = Sensor.new("Map")
local tps1Sensor = Sensor.new("Tps1")
local vehicleSpeedSensor = Sensor.new("VehicleSpeed")
local inputShaftSpeedSensor = Sensor.new("InputShaftSpeed")
local detectedGearSensor = Sensor.new("DetectedGear")

-- Sensor Timeouts
local SENSOR_TIMEOUT_MS = 3000
cltSensor:setTimeout(SENSOR_TIMEOUT_MS)
rpmSensor:setTimeout(SENSOR_TIMEOUT_MS)
mapSensor:setTimeout(SENSOR_TIMEOUT_MS)
tps1Sensor:setTimeout(SENSOR_TIMEOUT_MS)
vehicleSpeedSensor:setTimeout(SENSOR_TIMEOUT_MS)
inputShaftSpeedSensor:setTimeout(SENSOR_TIMEOUT_MS)
detectedGearSensor:setTimeout(SENSOR_TIMEOUT_MS)

-- Core State Variables
local IntendedGear = 3
local initialDataReceived = false
local lastKnownRawVss = nil             -- Raw VSS after idle override, before scaling (used by spike filter & print)
local lastSetVehicleSpeed = 0.0         -- Scaled VSS value that was last set to the firmware sensor
local idleStateForcedInLastCanCycle = false

-- PWM Configuration
local SOLENOID_A_PWM_INDEX = 0
local SOLENOID_B_PWM_INDEX = 1
local PWM_FREQUENCY = 10

-- Solenoid State Strings for Printing
local solenoid_A_state_str = "OFF"
local solenoid_B_state_str = "OFF"      -- Will be correctly set during init by setSolenoidsForGear

-- Shift Process Timers and Flags
local shift_in_progress_timer = Timer.new()
local is_shifting = false

local gearMatchTimer = Timer.new()
local is_checking_gear_match = false
local shiftErrorActive = false

local printStatusTimer = Timer.new()

-- User-Configurable Settings (loaded in init, using normal length Lua variable names)
local wotTpsThresholdSetting
local shiftExecutionTimeSecondsSetting
local downshiftLockoutRpmSetting
local gearMatchTimeoutSecondsSetting
local numberOfGearSolenoidsSetting

-- Table and Curve Indices (loaded in init)
local upshiftScheduleTableIndex
local downshiftScheduleTableIndex
local wotRpmCurveIndex
local solenoidPatternCurveIndex

-- Helper: Get 16-bit unsigned value from CAN data (Big Endian based on your usage)
function getTwoBytesUnsignedLsb(data, startIndex)
    local msb = data[startIndex]
    local lsb = data[startIndex + 1]
    if msb == nil or lsb == nil then return 0 end
    return lsb + (msb * 256)
end

-- Helper: Get signed byte
function getSignedByte(rawValue)
    if rawValue > 127 then return rawValue - 256
    else return rawValue end
end

-- Helper: Command solenoids based on pattern from "solPattern" curve
function setSolenoidsForGear(targetGear)
    if solenoidPatternCurveIndex == nil then
        print("ERR: solPattern Idx nil")
        if numberOfGearSolenoidsSetting >= 1 then setPwmDuty(SOLENOID_A_PWM_INDEX, 0.0); solenoid_A_state_str = "OFF" end
        if numberOfGearSolenoidsSetting >= 2 then setPwmDuty(SOLENOID_B_PWM_INDEX, 0.0); solenoid_B_state_str = "OFF" end
        return
    end

    local patternCode = curve(solenoidPatternCurveIndex, targetGear)
    if patternCode == nil then
        print("WARN: No solPattern for gear " .. targetGear)
        patternCode = 0 
    end
    patternCode = math.floor(patternCode)
    local tempPattern = patternCode

    -- Solenoid A
    if numberOfGearSolenoidsSetting >= 1 then
        local solA_duty = 0.0
        if numberOfGearSolenoidsSetting == 1 then 
            if tempPattern == 1 then solA_duty = 1.0 end
        elseif numberOfGearSolenoidsSetting == 2 then 
            if math.floor(tempPattern / 10) == 1 then solA_duty = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then 
            if math.floor(tempPattern / 100) == 1 then solA_duty = 1.0 end
        end
        setPwmDuty(SOLENOID_A_PWM_INDEX, solA_duty)
        solenoid_A_state_str = (solA_duty == 1.0) and "ON" or "OFF"
    else 
        solenoid_A_state_str = "N/A" 
    end

    -- Solenoid B
    if numberOfGearSolenoidsSetting >= 2 then
        local solB_duty = 0.0
        if numberOfGearSolenoidsSetting == 2 then 
            if tempPattern % 10 == 1 then solB_duty = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then 
            if math.floor((tempPattern % 100) / 10) == 1 then solB_duty = 1.0 end
        end
        setPwmDuty(SOLENOID_B_PWM_INDEX, solB_duty)
        solenoid_B_state_str = (solB_duty == 1.0) and "ON" or "OFF"
    else 
        solenoid_B_state_str = "N/A" 
    end
end

-- Initialization Function
function initializeShiftLogic()
    -- Load user settings using shortened camelCase names for findSetting
    wotTpsThresholdSetting = findSetting("wotTps", 90.0)
    local shiftExecutionTimeMsSetting = findSetting("shiftTime", 500)
    shiftExecutionTimeSecondsSetting = shiftExecutionTimeMsSetting / 1000.0
    downshiftLockoutRpmSetting = findSetting("dsLockRpm", 7000)
    local gearMatchTimeoutMsSetting = findSetting("gearMatch", 3000)
    gearMatchTimeoutSecondsSetting = gearMatchTimeoutMsSetting / 1000.0
    numberOfGearSolenoidsSetting = findSetting("numGearSol", 2)

    -- Load table and curve indices using shortened camelCase names
    upshiftScheduleTableIndex = findTableIndex("upSched")
    downshiftScheduleTableIndex = findTableIndex("downSched")
    wotRpmCurveIndex = findCurveIndex("wotRpm")
    solenoidPatternCurveIndex = findCurveIndex("solPattern")

    if upshiftScheduleTableIndex == nil or downshiftScheduleTableIndex == nil or wotRpmCurveIndex == nil or solenoidPatternCurveIndex == nil then 
        print("ERR: Table/Curve nil") 
    end

    if numberOfGearSolenoidsSetting >= 1 then startPwm(SOLENOID_A_PWM_INDEX, PWM_FREQUENCY, 0.0) end
    if numberOfGearSolenoidsSetting >= 2 then startPwm(SOLENOID_B_PWM_INDEX, PWM_FREQUENCY, 0.0) end
    
    setSolenoidsForGear(IntendedGear) -- Set solenoids to initial IntendedGear (3rd)
    printStatusTimer:reset()
end

initializeShiftLogic()

-- onCanRx: Callback for received CAN messages
function onCanRx(bus, id, dlc, data)
    if id == 0x360 then -- RPM, MAP, TPS
        rpmSensor:set(getTwoBytesUnsignedLsb(data, 1))
        mapSensor:set(getTwoBytesUnsignedLsb(data, 3) / 10.0)
        tps1Sensor:set(getTwoBytesUnsignedLsb(data, 5) / 10.0)
    elseif id == 0x3E0 then -- Coolant Temp
        cltSensor:set(getTwoBytesUnsignedLsb(data, 1) / 100.0)
    elseif id == 0x470 then -- ECU Transmitted Gear Data
        local rawGearValue = data[8] 
        if rawGearValue ~= nil then
            local actualGear = getSignedByte(rawGearValue)
            detectedGearSensor:set(actualGear)
        end
    elseif id == 0x370 then -- Vehicle Speed, and main shift logic trigger
        idleStateForcedInLastCanCycle = false -- Reset per 0x370 cycle
        local newRawVssFromCan = getTwoBytesUnsignedLsb(data, 1)
        local processedRawVss = newRawVssFromCan -- This will hold raw VSS after idle forcing
        local currentFinalVehicleSpeed           -- This will hold scaled VSS after spike filter & idle forcing
        local forceIdleState = false             -- True if idle conditions (low RPM/TPS) are met

        -- One-time check for initial sensor data
        if not initialDataReceived then
            if getSensor("Rpm") ~= nil and getSensor("Tps1") ~= nil and newRawVssFromCan ~= nil then
                initialDataReceived = true
            end
        end

        local currentRpm = getSensor("Rpm")
        local currentTps = getSensor("Tps1")

        -- Force VSS to 0 if idle-like conditions met
        if currentRpm ~= nil and currentTps ~= nil then
            if currentRpm < 1300 and currentTps < 1.0 then
                processedRawVss = 0 -- Force the VSS value used for logic to 0
                forceIdleState = true
                idleStateForcedInLastCanCycle = true
            end
        end

        -- VSS Spike Filter (conditionally active)
        local useLastSpeedDueToSpike = false
        if enableVssSpikeFilter then 
            if lastKnownRawVss ~= nil and processedRawVss ~= nil and math.abs(processedRawVss - lastKnownRawVss) > 100 then
                useLastSpeedDueToSpike = true
            end
        end

        if useLastSpeedDueToSpike then
            currentFinalVehicleSpeed = lastSetVehicleSpeed -- Use last scaled VSS
            -- lastKnownRawVss is NOT updated with the spiky processedRawVss
        elseif processedRawVss ~= nil then
            currentFinalVehicleSpeed = processedRawVss  -- Scale the (potentially overridden) raw VSS
            lastKnownRawVss = processedRawVss                 -- Store the raw (but potentially overridden) value
        else
            currentFinalVehicleSpeed = lastSetVehicleSpeed    -- Fallback
        end
        
        vehicleSpeedSensor:set(currentFinalVehicleSpeed)
        inputShaftSpeedSensor:set(currentFinalVehicleSpeed)
        lastSetVehicleSpeed = currentFinalVehicleSpeed

        -- Check "Shift In Progress" Timer
        if is_shifting then
            if shift_in_progress_timer:getElapsedSeconds() >= shiftExecutionTimeSecondsSetting then
                is_shifting = false
            end
        end

        -- High Priority: Force to 1st gear if idle state is active and not already in 1st/shifting
        if forceIdleState and IntendedGear ~= 1 and not is_shifting then
            IntendedGear = 1
            setSolenoidsForGear(IntendedGear)
            is_shifting = true; shift_in_progress_timer:reset()
            is_checking_gear_match = true; gearMatchTimer:reset()
            shiftErrorActive = false
        end

        -- Main Shift Decision Logic: Only if not shifting, data received, tables loaded
        if not is_shifting and initialDataReceived and 
           upshiftScheduleTableIndex ~= nil and downshiftScheduleTableIndex ~= nil and 
           wotRpmCurveIndex ~= nil and solenoidPatternCurveIndex ~= nil then
            
            currentRpm = getSensor("Rpm") -- Re-fetch for current values
            currentTps = getSensor("Tps1")

            if currentRpm == nil or currentTps == nil then
                -- Skip if essential sensor data still missing for this cycle
            else
                local isWOT = (currentTps >= wotTpsThresholdSetting)
                local newPotentialDesiredGear = IntendedGear

                -- 1. WOT Upshift
                if isWOT and IntendedGear < 4 then 
                    local wotTargetRpm = curve(wotRpmCurveIndex, IntendedGear)
                    if wotTargetRpm ~= nil and currentRpm >= wotTargetRpm then
                        newPotentialDesiredGear = IntendedGear + 1
                    end
                end

                -- 2. Table Downshift (if no WOT upshift)
                if newPotentialDesiredGear == IntendedGear and IntendedGear > 1 then
                    local yAxis = IntendedGear
                    local targetVss = table3d(downshiftScheduleTableIndex, currentTps, yAxis)
                    if targetVss ~= nil and currentFinalVehicleSpeed <= targetVss and currentRpm <= downshiftLockoutRpmSetting then
                        newPotentialDesiredGear = IntendedGear - 1
                    end
                end
                
                -- 3. Table Upshift (if no WOT upshift or downshift)
                if newPotentialDesiredGear == IntendedGear and IntendedGear < 4 then 
                    local yAxis = IntendedGear
                    local vssForTableLookup = currentFinalVehicleSpeed
                    if currentRpm < 1200 then vssForTableLookup = 0.0 end -- RPM override for VSS in table lookup
                    
                    local targetVss = table3d(upshiftScheduleTableIndex, currentTps, yAxis)
                    if targetVss ~= nil and vssForTableLookup >= targetVss then
                        newPotentialDesiredGear = IntendedGear + 1
                    end
                end

                -- Shift Execution
                if newPotentialDesiredGear ~= IntendedGear and newPotentialDesiredGear >= 1 and newPotentialDesiredGear <= 4 then
                    IntendedGear = newPotentialDesiredGear
                    setSolenoidsForGear(IntendedGear)
                    is_shifting = true; shift_in_progress_timer:reset()
                    is_checking_gear_match = true; gearMatchTimer:reset()
                    shiftErrorActive = false
                end
            end
        end
    end
end

canRxAdd(0x360); canRxAdd(0x3E0); canRxAdd(0x370); canRxAdd(0x470) -- 0x470 is added in initializeShiftLogic

-- onTick: Called periodically by TCU firmware
function onTick()
    -- Check "Gear Match" Timer (conditionally active)
    if enableGearErrorCheck and is_checking_gear_match then 
        local actualGear = getSensor("DetectedGear")
        if actualGear ~= nil and IntendedGear == actualGear then
            shiftErrorActive = false
            is_checking_gear_match = false
        elseif gearMatchTimer:getElapsedSeconds() >= gearMatchTimeoutSecondsSetting then
            shiftErrorActive = true
            is_checking_gear_match = false
        end
    elseif not enableGearErrorCheck and is_checking_gear_match then
        is_checking_gear_match = false
        shiftErrorActive = false
    end

    -- Periodic Status Print (every 500ms)
    if printStatusTimer:getElapsedSeconds() >= 0.5 then
        if initialDataReceived then
            local detectedGearString = "nil"; local detectedGearValue = getSensor("DetectedGear")
            if detectedGearValue ~= nil then detectedGearString = detectedGearValue end
            
            local rpmString = "nil"; local rpmValue = getSensor("Rpm")
            if rpmValue ~= nil then rpmString = rpmValue end
            
            local tpsString = "nil"; local tpsValue = getSensor("Tps1")
            if tpsValue ~= nil then tpsString = tpsValue end
            
            local lastKnownRawVssString = "nil"
            if lastKnownRawVss ~= nil then lastKnownRawVssString = lastKnownRawVss end
            
            local shiftErrorString = "N/A"
            if enableGearErrorCheck then shiftErrorString = (shiftErrorActive and "TRUE" or "false") end
            
            local isShiftingString = (is_shifting and "YES" or "no")
            local idleForcedString = (idleStateForcedInLastCanCycle and "YES" or "no")

            print("VSS:"..lastSetVehicleSpeed.."(Raw:"..lastKnownRawVssString..") RPM:"..rpmString.." TPS:"..tpsString..
                  " IGear:"..IntendedGear.." AGear:"..detectedGearString..
                  " SolA:"..solenoid_A_state_str.." SolB:"..solenoid_B_state_str..
                  " SErr:"..shiftErrorString.." Shft'ing:"..isShiftingString.." IdleFrc:"..idleForcedString)
        end
        printStatusTimer:reset()
    end
end

setTickRate(100) -- 100 Hz


Oh and Gemini made most of this code, I take no real credit
User avatar
AndreyB
Site Admin
Posts: 14681
Joined: Wed Aug 28, 2013 1:28 am
Location: Jersey City
Github Username: rusefillc
Slack: Andrey B

Re: Transmission Control for 4 gears; more, maybe later!

Post by AndreyB »

Video or never happened?
Very limited telepathic abilities - please post logs & tunes where appropriate - http://rusefi.com/s/questions

Always looking for C/C++/Java/PHP developers! Please help us see https://rusefi.com/s/howtocontribute
User avatar
NormanAlphaspeed
Posts: 79
Joined: Fri Jan 13, 2017 7:15 am
Location: Puerto Rico
Contact:

Re: Transmission Control for 4 gears; more, maybe later!

Post by NormanAlphaspeed »

Best I could do while debugging and trying not to die:

https://www.youtube.com/shorts/z5SX9Dk5ck0
User avatar
NormanAlphaspeed
Posts: 79
Joined: Fri Jan 13, 2017 7:15 am
Location: Puerto Rico
Contact:

Re: Transmission Control for 4 gears; more, maybe later!

Post by NormanAlphaspeed »

https://youtube.com/shorts/DWDJleEFc0E

Code: Select all

-- TCU Lua Script: WOT Downshift RPM Curve, Renamed WOT Upshift Curve, Raw VSS, Spike Filter, Sequential Shifts, 5 Gauges, CAN TX

-- Configuration Flags
local enableGearErrorCheck = false
local enableVssFiltering = true 

-- Sensor Declarations
local coolantTempSensor = Sensor.new("Clt")
local engineRpmSensor = Sensor.new("Rpm")
local manifoldPressureSensor = Sensor.new("Map")
local throttlePosSensor = Sensor.new("Tps1")
local vehicleSpeedSensor = Sensor.new("VehicleSpeed")
local detectedGearSensor = Sensor.new("DetectedGear")

-- Sensor Timeouts
local SENSOR_TIMEOUT_MILLISECONDS = 3000
coolantTempSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)
engineRpmSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)
manifoldPressureSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)
throttlePosSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)
vehicleSpeedSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)
detectedGearSensor:setTimeout(SENSOR_TIMEOUT_MILLISECONDS)

-- Core State Variables
local intendedGear = 3
local initialDataReceived = false
local lastKnownRawVssFromCan = nil
local lastSetVssForLogic = 0.0
local idleStateTriggeredShiftToFirst = false

-- PWM Configuration
local SOLENOID_A_PWM_INDEX = 0
local SOLENOID_B_PWM_INDEX = 1
local PWM_FREQUENCY = 10

-- Solenoid State Strings
local solenoidAStateString = "OFF"
local solenoidBStateString = "OFF"

-- Shift Process Timers and Flags
local shiftInProgressTimer = Timer.new()
local isShifting = false 
local gearMatchTimer = Timer.new()
local isCheckingGearMatch = false
local shiftErrorActive = false

-- User-Configurable Settings
local wotTpsThresholdSetting
local shiftExecutionTimeSecondsSetting
local downshiftLockoutRpmSetting
local gearMatchTimeoutSecondsSetting
local numberOfGearSolenoidsSetting
local idleRpmThresholdSetting
local idleTpsThresholdSetting
local effectivelyStoppedRawVssThreshold = 50
local vssSpikeDeltaThresholdSetting
local maxRealisticRawVssSetting

-- Table and Curve Indices
local upshiftScheduleTableIndex
local downshiftScheduleTableIndex
local wotUpshiftRpmCurveIndex       -- RENAMED
local wotDownshiftRpmCurveIndex     -- NEW
local solenoidPatternCurveIndex

-- CAN Transmit Configuration
local SHIFT_STATUS_CAN_ID = 0x701 
local CAN_BUS_INDEX = 1           

function getTwoBytesUnsignedLsb(data, startIndex)
    local msb = data[startIndex]
    local lsb = data[startIndex + 1]
    if msb == nil or lsb == nil then return nil end
    return (msb * 256) + lsb
end

function getSignedByte(rawValue)
    if rawValue > 127 then return rawValue - 256
    else return rawValue end
end

function setSolenoidsForGear(targetGear)
    if solenoidPatternCurveIndex == nil then
        print("ERROR: Solenoid pattern curve index is nil.")
        if numberOfGearSolenoidsSetting >= 1 then setPwmDuty(SOLENOID_A_PWM_INDEX, 0.0); solenoidAStateString = "OFF" end
        if numberOfGearSolenoidsSetting >= 2 then setPwmDuty(SOLENOID_B_PWM_INDEX, 0.0); solenoidBStateString = "OFF" end
        return
    end
    local patternCode = curve(solenoidPatternCurveIndex, targetGear)
    if patternCode == nil then
        print("WARNING: No solenoid pattern for gear " .. targetGear)
        patternCode = 0
    end
    patternCode = math.floor(patternCode)
    local tempPattern = patternCode
    if numberOfGearSolenoidsSetting >= 1 then
        local solenoidADutyCycle = 0.0
        if numberOfGearSolenoidsSetting == 1 then if tempPattern == 1 then solenoidADutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 2 then if math.floor(tempPattern / 10) == 1 then solenoidADutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then if math.floor(tempPattern / 100) == 1 then solenoidADutyCycle = 1.0 end
        end
        setPwmDuty(SOLENOID_A_PWM_INDEX, solenoidADutyCycle)
        solenoidAStateString = (solenoidADutyCycle == 1.0) and "ON" or "OFF"
    else solenoidAStateString = "N/A" end
    if numberOfGearSolenoidsSetting >= 2 then
        local solenoidBDutyCycle = 0.0
        if numberOfGearSolenoidsSetting == 2 then if tempPattern % 10 == 1 then solenoidBDutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then if math.floor((tempPattern % 100) / 10) == 1 then solenoidBDutyCycle = 1.0 end
        end
        setPwmDuty(SOLENOID_B_PWM_INDEX, solenoidBDutyCycle)
        solenoidBStateString = (solenoidBDutyCycle == 1.0) and "ON" or "OFF"
    else solenoidBStateString = "N/A" end
end

function initializeShiftLogic()
    wotTpsThresholdSetting = findSetting("wotTps", 90.0)
    local shiftExecutionTimeMsSetting = findSetting("shiftTime", 500)
    shiftExecutionTimeSecondsSetting = shiftExecutionTimeMsSetting / 1000.0
    downshiftLockoutRpmSetting = findSetting("dsLockRpm", 7000)
    local gearMatchTimeoutMsSetting = findSetting("gearMatch", 3000)
    gearMatchTimeoutSecondsSetting = gearMatchTimeoutMsSetting / 1000.0
    numberOfGearSolenoidsSetting = findSetting("numGearSol", 2)
    idleRpmThresholdSetting = 1300
    idleTpsThresholdSetting = 1.0
    vssSpikeDeltaThresholdSetting = 200 
    maxRealisticRawVssSetting = 2400    

    upshiftScheduleTableIndex = findTableIndex("upSched")
    downshiftScheduleTableIndex = findTableIndex("downSched")
    wotUpshiftRpmCurveIndex = findCurveIndex("wotUpRpm")     -- Use new key "wotUpRpm"
    wotDownshiftRpmCurveIndex = findCurveIndex("wotDsRpm")   -- NEW: Use key "wotDsRpm"
    solenoidPatternCurveIndex = findCurveIndex("solPattern")

    if upshiftScheduleTableIndex == nil or downshiftScheduleTableIndex == nil or 
       wotUpshiftRpmCurveIndex == nil or wotDownshiftRpmCurveIndex == nil or 
       solenoidPatternCurveIndex == nil then
        print("ERROR: One or more essential tables/curves not found.")
    end
    if numberOfGearSolenoidsSetting >= 1 then startPwm(SOLENOID_A_PWM_INDEX, PWM_FREQUENCY, 0.0) end
    if numberOfGearSolenoidsSetting >= 2 then startPwm(SOLENOID_B_PWM_INDEX, PWM_FREQUENCY, 0.0) end
    setSolenoidsForGear(intendedGear)
end

initializeShiftLogic()

function onCanRx(bus, id, dlc, data)
    if id == 0x360 then
        engineRpmSensor:set(getTwoBytesUnsignedLsb(data, 1))
        manifoldPressureSensor:set(getTwoBytesUnsignedLsb(data, 3) / 10.0)
        local tpsCanVal = getTwoBytesUnsignedLsb(data, 5)
        if tpsCanVal ~= nil then
             throttlePosSensor:set(tpsCanVal / 10.0)
        end
    elseif id == 0x3E0 then
        coolantTempSensor:set(getTwoBytesUnsignedLsb(data, 1) / 100.0)
    elseif id == 0x470 then
        local rawGearValue = data[8]
        if rawGearValue ~= nil then
            detectedGearSensor:set(getSignedByte(rawGearValue))
        end
    elseif id == 0x370 then
        idleStateTriggeredShiftToFirst = false
        
        local vssFromCan = getTwoBytesUnsignedLsb(data, 1)
        lastKnownRawVssFromCan = vssFromCan 

        local currentVssForLogic 
        local useLastVss = false

        if vssFromCan == nil then
            useLastVss = true 
        elseif enableVssFiltering and initialDataReceived then
            if vssFromCan > maxRealisticRawVssSetting then
                useLastVss = true
            elseif math.abs(vssFromCan - lastSetVssForLogic) > vssSpikeDeltaThresholdSetting then
                useLastVss = true
            end
        end

        if useLastVss then
            currentVssForLogic = lastSetVssForLogic
        else
            currentVssForLogic = vssFromCan
        end
        
        if currentVssForLogic == nil then currentVssForLogic = 0.0 end

        if not initialDataReceived then
            if getSensor("Rpm") ~= nil and getSensor("Tps1") ~= nil and vssFromCan ~= nil then
                initialDataReceived = true
                if enableVssFiltering then lastSetVssForLogic = currentVssForLogic end
            end
        end

        local currentRpm = getSensor("Rpm")
        local currentTps = getSensor("Tps1")
        local triggerShiftToFirstForIdle = false

        if currentRpm ~= nil and currentTps ~= nil then 
            if currentRpm < idleRpmThresholdSetting and currentTps < idleTpsThresholdSetting then
                local actualGear = getSensor("DetectedGear")
                if actualGear ~= nil and actualGear == 0 then
                    if currentVssForLogic < effectivelyStoppedRawVssThreshold then 
                        triggerShiftToFirstForIdle = true 
                        idleStateTriggeredShiftToFirst = true
                    end
                end
            end
        end
        
        vehicleSpeedSensor:set(currentVssForLogic)
        lastSetVssForLogic = currentVssForLogic

        if isShifting then
            if shiftInProgressTimer:getElapsedSeconds() >= shiftExecutionTimeSecondsSetting then
                isShifting = false
            end
        end

        if triggerShiftToFirstForIdle and intendedGear ~= 1 and not isShifting then
            intendedGear = 1
            setSolenoidsForGear(intendedGear)
            isShifting = true; shiftInProgressTimer:reset()
            isCheckingGearMatch = true; gearMatchTimer:reset()
            shiftErrorActive = false
        end
        
        if not isShifting and initialDataReceived and
           upshiftScheduleTableIndex ~= nil and downshiftScheduleTableIndex ~= nil and
           wotUpshiftRpmCurveIndex ~= nil and wotDownshiftRpmCurveIndex ~= nil and -- Added new curve check
           solenoidPatternCurveIndex ~= nil then
            
            currentRpm = getSensor("Rpm") 
            currentTps = getSensor("Tps1") 

            if currentRpm == nil or currentTps == nil then
                -- Skip
            else
                local isWOT = (currentTps >= wotTpsThresholdSetting)
                local newPotentialDesiredGear = intendedGear

                -- 1. WOT Upshift Logic
                if isWOT and intendedGear < 4 then 
                    local wotTargetRpm = curve(wotUpshiftRpmCurveIndex, intendedGear) 
                    if wotTargetRpm ~= nil and currentRpm >= wotTargetRpm then
                        newPotentialDesiredGear = intendedGear + 1
                    end
                end

                -- 2. WOT Downshift Logic
                if newPotentialDesiredGear == intendedGear and isWOT and intendedGear > 1 then
                    if wotDownshiftRpmCurveIndex ~= nil then 
                        local wotTargetRpmForDownshift = curve(wotDownshiftRpmCurveIndex, intendedGear)
                        if wotTargetRpmForDownshift ~= nil and currentRpm < wotTargetRpmForDownshift then
                            if currentRpm <= downshiftLockoutRpmSetting then 
                                newPotentialDesiredGear = intendedGear - 1
                            end
                        end
                    end
                end

                -- 3. Table-Based Downshift Logic
                if newPotentialDesiredGear == intendedGear and intendedGear > 1 then
                    local yAxisForTable = intendedGear
                    local targetVssForDownshift = table3d(downshiftScheduleTableIndex, currentTps, yAxisForTable)
                    if targetVssForDownshift ~= nil and currentVssForLogic <= targetVssForDownshift and currentRpm <= downshiftLockoutRpmSetting then
                        newPotentialDesiredGear = intendedGear - 1
                    end
                end
                
                -- 4. Table-Based Upshift Logic
                if newPotentialDesiredGear == intendedGear and intendedGear < 4 then 
                    local yAxisForTable = intendedGear
                    local vssForTableLookup = currentVssForLogic
                    if currentRpm < 1200 and vssForTableLookup < effectivelyStoppedRawVssThreshold then 
                        vssForTableLookup = 0.0 
                    end 
                    
                    local targetVssForUpshift = table3d(upshiftScheduleTableIndex, currentTps, yAxisForTable)
                    if targetVssForUpshift ~= nil and vssForTableLookup >= targetVssForUpshift then
                        newPotentialDesiredGear = intendedGear + 1
                    end
                end

                -- Shift Execution
                if newPotentialDesiredGear ~= intendedGear and newPotentialDesiredGear >= 1 and newPotentialDesiredGear <= 4 then
                    intendedGear = newPotentialDesiredGear
                    setSolenoidsForGear(intendedGear)
                    isShifting = true; shiftInProgressTimer:reset()
                    isCheckingGearMatch = true; gearMatchTimer:reset()
                    shiftErrorActive = false
                end
            end
        end
    end
end

canRxAdd(0x360); canRxAdd(0x3E0); canRxAdd(0x370); canRxAdd(0x470)

function onTick()
    if enableGearErrorCheck and isCheckingGearMatch then 
        local actualGearValue = getSensor("DetectedGear") 
        if actualGearValue ~= nil and intendedGear == actualGearValue then
            shiftErrorActive = false
            isCheckingGearMatch = false
        elseif gearMatchTimer:getElapsedSeconds() >= gearMatchTimeoutSecondsSetting then
            shiftErrorActive = true
            isCheckingGearMatch = false
            print("ERROR: Gear match timeout! Intended: " .. intendedGear .. ", Actual: " .. (actualGearValue or "nil"))
        end
    elseif not enableGearErrorCheck and isCheckingGearMatch then
        isCheckingGearMatch = false
        shiftErrorActive = false
    end

    setLuaGauge(1, intendedGear)

    local solenoidComboValue = 0
    if solenoidAStateString == "ON" and solenoidBStateString == "ON" then
        solenoidComboValue = 11
    elseif solenoidAStateString == "ON" and solenoidBStateString == "OFF" then
        solenoidComboValue = 10
    elseif solenoidAStateString == "OFF" and solenoidBStateString == "ON" then
        solenoidComboValue = 1
    end
    setLuaGauge(2, solenoidComboValue)

    setLuaGauge(3, idleStateTriggeredShiftToFirst and 1 or 0)

    local currentTpsForGauges = getSensor("Tps1") 
    local currentSpeedForGauges = lastSetVssForLogic
    local placeholderValue = 999 

    local upshiftDistance = placeholderValue
    if currentTpsForGauges ~= nil and intendedGear < 4 and upshiftScheduleTableIndex ~= nil then
        local targetVss = table3d(upshiftScheduleTableIndex, currentTpsForGauges, intendedGear)
        if targetVss ~= nil then
            upshiftDistance = targetVss - currentSpeedForGauges
        end
    end
    setLuaGauge(4, upshiftDistance)

    local downshiftDistance = placeholderValue
    if currentTpsForGauges ~= nil and intendedGear > 1 and downshiftScheduleTableIndex ~= nil then
        local targetVss = table3d(downshiftScheduleTableIndex, currentTpsForGauges, intendedGear)
        if targetVss ~= nil then
            downshiftDistance = currentSpeedForGauges - targetVss 
        end
    end
    setLuaGauge(5, downshiftDistance)

    local shiftingStatusPayload = {}
    if isShifting then
        shiftingStatusPayload[1] = 1
    else
        shiftingStatusPayload[1] = 0
    end
    txCan(CAN_BUS_INDEX, SHIFT_STATUS_CAN_ID, 0, shiftingStatusPayload)
end

setTickRate(100)
uses a curve for WOT kickdown/upshift
uses two tables for up/downshift with shift direction vs tps
has downshift lockout RPM
has WOT threshold tps
talk with haltech CAN protocol to get most data
send shift request over CAN

a bunch of other stuff
Post Reply