diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendMarkers.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendMarkers.kt index 326dc76..e8f702c 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendMarkers.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendMarkers.kt @@ -3,7 +3,8 @@ package ru.setcorp.setprotocol.trends import kotlin.math.abs enum class TrendMarker(val title: String, val horizontal: Boolean) { - A("A", false), B("B", false), C("C", true), D("D", true) + A("A", false), B("B", false), C("C", false), D("D", false), + E("E", true), F("F", true), G("G", true), H("H", true) } data class TrendMarkers( @@ -14,18 +15,28 @@ data class TrendMarkers( val b: Double? = null, val c: Double? = null, val d: Double? = null, + val e: Double? = null, + val f: Double? = null, + val g: Double? = null, + val h: Double? = null, ) { fun value(marker: TrendMarker): Double? = when (marker) { TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d + TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h } fun move(marker: TrendMarker, value: Double): TrendMarkers = when (marker) { TrendMarker.A -> copy(a = value); TrendMarker.B -> copy(b = value) TrendMarker.C -> copy(c = value); TrendMarker.D -> copy(d = value) + TrendMarker.E -> copy(e = value); TrendMarker.F -> copy(f = value) + TrendMarker.G -> copy(g = value); TrendMarker.H -> copy(h = value) } fun positioned(bounds: PlotBounds): TrendMarkers = copy( - a = a ?: bounds.value(1.0 / 3, false), b = b ?: bounds.value(2.0 / 3, false), - c = c ?: bounds.value(2.0 / 3, true), d = d ?: bounds.value(1.0 / 3, true)) - fun reset(bounds: PlotBounds): TrendMarkers = copy(a = null, b = null, c = null, d = null).positioned(bounds) + a = a ?: bounds.value(0.2, false), b = b ?: bounds.value(0.4, false), + c = c ?: bounds.value(0.6, false), d = d ?: bounds.value(0.8, false), + e = e ?: bounds.value(0.2, true), f = f ?: bounds.value(0.4, true), + g = g ?: bounds.value(0.6, true), h = h ?: bounds.value(0.8, true)) + fun reset(bounds: PlotBounds): TrendMarkers = copy( + a = null, b = null, c = null, d = null, e = null, f = null, g = null, h = null).positioned(bounds) fun fraction(marker: TrendMarker, bounds: PlotBounds): Double = bounds.fraction(requireNotNull(value(marker)), marker.horizontal) fun drag(marker: TrendMarker, delta: Double, length: Double, bounds: PlotBounds): TrendMarkers = move(marker, bounds.drag(requireNotNull(value(marker)), delta, length, marker.horizontal)).copy(selected = marker) diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt index 580d523..c254742 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendModels.kt @@ -48,6 +48,8 @@ data class TrendSignal( val deviceType: Int = 7, val device: Int = 13, val byteOffset: Int = 0, + val multiplier: Double = 1.0, + val iq: Int = 0, val extended: Boolean = true, ) { fun validationError(profile: TrendProfile): String? = when { @@ -68,12 +70,24 @@ data class TrendSignal( source == TrendSource.CAN_RAW && parseTrendAddress(address, if (extended) 0x1FFF_FFFF else 0x7FF) == null -> "CAN ID вне диапазона ${if (extended) "29" else "11"} бит" source == TrendSource.CAN_RAW && byteOffset !in 0..6 -> "Смещение слова: 0…6 байт" + !multiplier.isFinite() -> "Множитель должен быть конечным числом" + iq !in 0..30 -> "IQ должен быть в диапазоне 0…30" else -> null } + fun displayValue(rawValue: Double): Double = + rawValue * multiplier / (1L shl iq).toDouble() + /** Only source changes invalidate the historical series. */ fun sameInput(other: TrendSignal): Boolean = - copy(order = other.order, name = other.name, color = other.color, visible = other.visible) == other + copy( + order = other.order, + name = other.name, + color = other.color, + visible = other.visible, + multiplier = other.multiplier, + iq = other.iq, + ) == other companion object { val COLORS = listOf("#2F91FF", "#FFB547", "#52D6A4", "#F8798D", "#B79AFF", "#4AD9E8", "#E8DA68", "#E7ECF3") @@ -105,7 +119,16 @@ fun parseTrendAddress(value: String, max: Long): Long? { fun normalizeRom(value: String): String = value.trim().replace("-", "").replace(" ", "").uppercase() -data class TrendPoint(val timestampMs: Long, val value: Double) +data class TrendPoint( + val timestampMs: Long, + val value: Double, + /** Nanoseconds inside [timestampMs], preserving spacing above 1 kHz without overflowing epoch nanoseconds. */ + val subMillisecondNanos: Int = 0, +) { + init { require(subMillisecondNanos in 0..999_999) } + fun nanosSince(other: TrendPoint): Long = + (timestampMs - other.timestampMs) * 1_000_000L + subMillisecondNanos - other.subMillisecondNanos +} data class TrendUiState( val section: TrendSection = TrendSection.SIGNALS, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSettingsJson.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSettingsJson.kt index c7403a6..763eb2c 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSettingsJson.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSettingsJson.kt @@ -25,6 +25,8 @@ object TrendSettingsJson { put("deviceType", signal.deviceType) put("device", signal.device) put("byteOffset", signal.byteOffset) + put("multiplier", signal.multiplier) + put("iq", signal.iq) put("extended", signal.extended) }) } @@ -53,7 +55,10 @@ object TrendSettingsJson { color = item.strictString("color"), visible = item.strictBoolean("visible"), valueType = TrendValueType.valueOf(item.strictString("valueType")), deviceType = item.strictInt("deviceType"), device = item.strictInt("device"), - byteOffset = item.strictInt("byteOffset"), extended = item.strictBoolean("extended"), + byteOffset = item.strictInt("byteOffset"), + multiplier = if (item.has("multiplier")) item.strictDouble("multiplier") else 1.0, + iq = if (item.has("iq")) item.strictInt("iq") else 0, + extended = item.strictBoolean("extended"), ) } } @@ -75,6 +80,12 @@ object TrendSettingsJson { return number.toInt() } + private fun JSONObject.strictDouble(key: String): Double { + val value = get(key) + require(value is Number) { "$key должен быть числом" } + return value.toDouble().also { require(it.isFinite()) { "$key должен быть конечным числом" } } + } + fun validate(settings: Map>) { val allIds = settings.values.flatten().map { it.id } require(allIds.distinct().size == allIds.size) { "Идентификаторы трендов не должны повторяться" } diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt index 3c58740..5f02bd3 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/trends/TrendSpectrum.kt @@ -42,8 +42,8 @@ object SpectrumAnalyzer { if (!NativeSetProtocol.available) return TrendSpectrum(error = "Модуль FFT недоступен") val count = minOf(points.size, options.maxSize) val start = points.size - count - val origin = points[start].timestampMs.toDouble() - val times = DoubleArray(count) { (points[start + it].timestampMs.toDouble() - origin) / 1000.0 } + val origin = points[start] + val times = DoubleArray(count) { points[start + it].nanosSince(origin) / 1_000_000_000.0 } val values = DoubleArray(count) { points[start + it].value } val output = NativeSetProtocol.nativeSpectrum(times, values, options.maxSize, options.window.ordinal, options.filter.ordinal, options.lowHz, options.highHz, options.removeMean) diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotContractTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotContractTest.kt index f334c56..af427fa 100644 --- a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotContractTest.kt +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/trends/PlotContractTest.kt @@ -30,7 +30,7 @@ class PlotContractTest { val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed) assertEquals(markers.a!! + 50, dragged.a!!, 1e-9) assertEquals(markers.b, dragged.b) - assertEquals(TrendMarker.A, dragged.hit(100.0, 50.0, 500.0, 100.0, 100.0, zoomed)) + assertEquals(TrendMarker.A, dragged.hit(0.0, 50.0, 500.0, 100.0, 20.0, zoomed)) val crossed = markers.move(TrendMarker.A, 1900.0).move(TrendMarker.B, 1100.0) assertEquals(-800.0, plotDelta(crossed.a!!, crossed.b!!), 0.0) } diff --git a/python/protocan/plot.py b/python/protocan/plot.py index 4a297c7..127242b 100644 --- a/python/protocan/plot.py +++ b/python/protocan/plot.py @@ -21,10 +21,14 @@ class Marker(IntEnum): B = 1 C = 2 D = 3 + E = 4 + F = 5 + G = 6 + H = 7 @property def horizontal(self) -> bool: - return self in (Marker.C, Marker.D) + return self in (Marker.E, Marker.F, Marker.G, Marker.H) class PlotMath: @@ -103,6 +107,10 @@ class Markers: b: Optional[float] = None c: Optional[float] = None d: Optional[float] = None + e: Optional[float] = None + f: Optional[float] = None + g: Optional[float] = None + h: Optional[float] = None def value(self, marker: Marker) -> Optional[float]: return getattr(self, marker.name.lower()) @@ -115,13 +123,14 @@ class Markers: def positioned(self, core: PlotMath, bounds: Bounds) -> "Markers": result = self - for marker, fraction in ((Marker.A, 1/3), (Marker.B, 2/3), (Marker.C, 2/3), (Marker.D, 1/3)): + for marker, fraction in ((Marker.A, .2), (Marker.B, .4), (Marker.C, .6), (Marker.D, .8), + (Marker.E, .2), (Marker.F, .4), (Marker.G, .6), (Marker.H, .8)): if result.value(marker) is None: result = result.move(marker, bounds.value(core, fraction, marker.horizontal)) return result def reset(self, core: PlotMath, bounds: Bounds) -> "Markers": - return replace(self, a=None, b=None, c=None, d=None).positioned(core, bounds) + return replace(self, a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None).positioned(core, bounds) def fraction(self, core: PlotMath, marker: Marker, bounds: Bounds) -> float: value = self.value(marker) diff --git a/python/tests/test_plot.py b/python/tests/test_plot.py index 6234391..b229824 100644 --- a/python/tests/test_plot.py +++ b/python/tests/test_plot.py @@ -35,7 +35,7 @@ class PlotTests(unittest.TestCase): moved = markers.drag(self.core, Marker.A, 50, 500, zoomed) self.assertAlmostEqual(moved.a, markers.a + 50) self.assertEqual(markers.b, moved.b) - self.assertEqual(Marker.A, moved.hit(self.core, 100, 50, 500, 100, 100, zoomed)) + self.assertEqual(Marker.A, moved.hit(self.core, 0, 50, 500, 100, 20, zoomed)) crossed = markers.move(Marker.A, 1900).move(Marker.B, 1100) self.assertEqual(-800, self.core.delta(crossed.a, crossed.b))