Transmission Control for 4 gears; more, maybe later!

It's all about the code!
Post Reply
User avatar
NormanAlphaspeed
Posts: 80
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: 14727
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: 80
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: 80
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
jasaircraft
Posts: 6
Joined: Tue Apr 30, 2024 9:24 pm
Location: Bolivia

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

Post by jasaircraft »

Great news i have an AT install in the near future i could use this for
User avatar
AndreyB
Site Admin
Posts: 14727
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 »

would it run GM 4 speed?
image.png
You do not have the required permissions to view the files attached to this post.
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
AndreyB
Site Admin
Posts: 14727
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 »

Looks like script uses incoming CANbus packets for main logic flow? Let me adjust it a bit to make it functional within ECU

I've made some progress but have not finished :( https://github.com/rusefi/rusefi/blob/master/firmware/controllers/lua/examples/TCU-4-speed.txt
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: 80
Joined: Fri Jan 13, 2017 7:15 am
Location: Puerto Rico
Contact:

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

Post by NormanAlphaspeed »

I made this one, but have nothing to test on:

Code: Select all

-- TCU Lua Script: WOT Downshift RPM Curve, Renamed WOT Upshift Curve, Raw VSS, Spike Filter, Sequential Shifts, 5 Gauges
-- CONVERTED FOR SINGLE-UNIT OPERATION (NO CAN)

-- =============================================================================
-- CONFIGURATION & STATE
-- =============================================================================

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

-- Core State Variables
local intendedGear = 3
local initialDataReceived = false
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
local wotDownshiftRpmCurveIndex
local solenoidPatternCurveIndex

-- =============================================================================
-- HELPER & LOGIC FUNCTIONS
-- =============================================================================

-- Sets PWM duty cycles for solenoids based on a target gear and pattern curve.
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)
    
    -- Solenoid A
    if numberOfGearSolenoidsSetting >= 1 then
        local solenoidADutyCycle = 0.0
        if numberOfGearSolenoidsSetting == 1 then if patternCode == 1 then solenoidADutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 2 then if math.floor(patternCode / 10) == 1 then solenoidADutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then if math.floor(patternCode / 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

    -- Solenoid B
    if numberOfGearSolenoidsSetting >= 2 then
        local solenoidBDutyCycle = 0.0
        if numberOfGearSolenoidsSetting == 2 then if patternCode % 10 == 1 then solenoidBDutyCycle = 1.0 end
        elseif numberOfGearSolenoidsSetting == 3 then if math.floor((patternCode % 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

-- Applies spike and sanity filtering to the raw VSS sensor reading.
function getFilteredVss(rawVss)
    local useLastVss = false

    if rawVss == nil then
        useLastVss = true 
    elseif enableVssFiltering and initialDataReceived then
        -- Reject unrealistic values or sudden spikes
        if rawVss > maxRealisticRawVssSetting or math.abs(rawVss - lastSetVssForLogic) > vssSpikeDeltaThresholdSetting then
            useLastVss = true
        end
    end

    if useLastVss then
        return lastSetVssForLogic
    else
        return rawVss
    end
end

-- Determines the single desired gear based on current conditions.
function determineDesiredGear(rpm, tps, vss, currentGear, actualGear)
    idleStateTriggeredShiftToFirst = false
    
    -- 1. Idle State Check: Force to 1st gear when stopped at idle in Neutral
    if rpm < idleRpmThresholdSetting and tps < idleTpsThresholdSetting and vss < effectivelyStoppedRawVssThreshold then
        if actualGear ~= nil and actualGear == 0 then
            idleStateTriggeredShiftToFirst = true
            return 1
        end
    end

    local newPotentialDesiredGear = currentGear
    local isWOT = (tps >= wotTpsThresholdSetting)
    
    -- 2. WOT Upshift Logic
    if isWOT and currentGear < 4 then 
        local wotTargetRpm = curve(wotUpshiftRpmCurveIndex, currentGear) 
        if wotTargetRpm ~= nil and rpm >= wotTargetRpm then
            newPotentialDesiredGear = currentGear + 1
        end
    end

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

    -- 4. Table-Based Downshift Logic
    if newPotentialDesiredGear == currentGear and currentGear > 1 then
        local targetVss = table3d(downshiftScheduleTableIndex, tps, currentGear)
        if targetVss ~= nil and vss <= targetVss and rpm <= downshiftLockoutRpmSetting then
            newPotentialDesiredGear = currentGear - 1
        end
    end
    
    -- 5. Table-Based Upshift Logic
    if newPotentialDesiredGear == currentGear and currentGear < 4 then 
        local vssForLookup = vss
        -- Prevent hunting at a stop by forcing VSS lookup to zero in certain conditions
        if rpm < 1200 and vss < effectivelyStoppedRawVssThreshold then 
            vssForLookup = 0.0 
        end
        local targetVss = table3d(upshiftScheduleTableIndex, tps, currentGear)
        if targetVss ~= nil and vssForLookup >= targetVss then
            newPotentialDesiredGear = currentGear + 1
        end
    end
    
    if newPotentialDesiredGear >= 1 and newPotentialDesiredGear <= 4 then
        return newPotentialDesiredGear
    end
    
    return currentGear
end

-- Executes a shift to the new gear and manages state changes.
function executeShift(newGear)
    intendedGear = newGear
    setSolenoidsForGear(intendedGear)
    
    isShifting = true
    shiftInProgressTimer:reset()
    
    isCheckingGearMatch = true
    gearMatchTimer:reset()
    shiftErrorActive = false
end

-- Manages timers that determine the duration of the shifting state.
function updateShiftTimers()
    if isShifting and shiftInProgressTimer:getElapsedSeconds() >= shiftExecutionTimeSecondsSetting then
        isShifting = false
    end
end

-- Manages gear match error checking state.
function updateGearErrorStatus(actualGear)
    if not isCheckingGearMatch then
        return
    end

    if enableGearErrorCheck then
        if actualGear ~= nil and intendedGear == actualGear then
            shiftErrorActive = false
            isCheckingGearMatch = false
        elseif gearMatchTimer:getElapsedSeconds() >= gearMatchTimeoutSecondsSetting then
            shiftErrorActive = true
            isCheckingGearMatch = false
            print("ERROR: Gear match timeout! Intended: " .. intendedGear .. ", Actual: " .. (actualGear or "nil"))
        end
    else
        -- If check is disabled, immediately stop checking.
        isCheckingGearMatch = false
        shiftErrorActive = false
    end
end

-- Updates all Lua gauges for debugging and display.
function updateGauges(currentTps, currentVss)
    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 placeholderValue = 999 

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

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

-- =============================================================================
-- INITIALIZATION
-- =============================================================================

function initializeShiftLogic()
    -- Load settings from the configuration
    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    

    -- Find required tables and curves
    upshiftScheduleTableIndex = findTableIndex("upSched")
    downshiftScheduleTableIndex = findTableIndex("downSched")
    wotUpshiftRpmCurveIndex = findCurveIndex("wotUpRpm")
    wotDownshiftRpmCurveIndex = findCurveIndex("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

    -- Initialize PWM outputs
    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
    
    -- Set initial gear
    setSolenoidsForGear(intendedGear)
end

-- Run initialization once on script load
initializeShiftLogic()

-- =============================================================================
-- MAIN LOOP (onTick)
-- =============================================================================

function onTick()
    -- 1. GATHER SENSOR INPUTS
    local currentRpm = getSensor("Rpm")
    local currentTps = getSensor("Tps1")
    local rawVss = getSensor("VehicleSpeed")
    local actualGear = getSensor("DetectedGear")
    
    -- 2. GUARD CLAUSE: Ensure essential data is available before running logic
    if currentRpm == nil or currentTps == nil or rawVss == nil then
        return 
    end
    
    -- 3. INITIALIZATION ON FIRST VALID DATA
    if not initialDataReceived then
        initialDataReceived = true
        lastSetVssForLogic = rawVss -- Initialize VSS filter baseline
    end

    -- 4. PROCESS INPUTS AND UPDATE STATE
    updateShiftTimers()
    local currentVssForLogic = getFilteredVss(rawVss)
    lastSetVssForLogic = currentVssForLogic -- Persist the VSS value for the next cycle's filter
    
    -- 5. CORE SHIFT DECISION LOGIC
    if not isShifting then
        local desiredGear = determineDesiredGear(currentRpm, currentTps, currentVssForLogic, intendedGear, actualGear)
        if desiredGear ~= intendedGear then
            executeShift(desiredGear)
        end
    end

    -- 6. POST-SHIFT AND DISPLAY UPDATES
    updateGearErrorStatus(actualGear)
    updateGauges(currentTps, currentVssForLogic)
end

setTickRate(100)
Post Reply