Расширить общие API графиков и GAS обмена

This commit is contained in:
2026-09-05 02:37:11 +03:00
parent 78d3f6690b
commit b1f7b965f4
22 changed files with 294 additions and 39 deletions

View File

@@ -16,12 +16,18 @@ object NativeSetProtocol {
/** {status, N, Fs, jitter, amplitudes...}; status != 0 has no amplitudes. */
external fun nativeSpectrum(times: DoubleArray, values: DoubleArray, maxSize: Int,
window: Int, filter: Int, lowHz: Double, highHz: Double, removeMean: Boolean): DoubleArray?
/** {frequencyHz, amplitude}, empty when no narrow-band peak is present. */
external fun nativeSpectrumPeak(amplitudes: DoubleArray, binHz: Double,
relativeThreshold: Double, absoluteFloor: Double): DoubleArray?
external fun nativeTrendCanValue(
source: Int, address: Long, deviceType: Int, device: Int, byteOffset: Int,
extended: Boolean, signed: Boolean, canId: Long, flags: Int, input: ByteArray,
): Int
external fun nativeTrendWatchRequest(period: Int, addresses: IntArray): ByteArray?
external fun nativeTrendWatchAck(input: ByteArray, period: Int, count: Int): Boolean
external fun nativeTrendWatchValues(input: ByteArray): IntArray?
/** {unsigned timestampMs, word0, ...}. */
external fun nativeTrendWatchDecode(input: ByteArray): LongArray?
external fun nativePackId(
priority: Int,
route: Int,

View File

@@ -10,37 +10,28 @@ object GuiGasWatch {
fun request(periodMs: Int, addresses: List<Int>): ByteArray {
require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 })
if (NativeSetProtocol.available) return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
return ByteArray(4 + addresses.size * 2).also { output ->
put16(output, 0, periodMs)
put16(output, 2, addresses.size)
addresses.forEachIndexed { index, address -> put16(output, 4 + index * 2, address) }
}
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
}
fun validateAck(payload: ByteArray, periodMs: Int, count: Int) {
require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) {
require(NativeSetProtocol.available && NativeSetProtocol.nativeTrendWatchAck(payload, periodMs, count)) {
"Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно"
}
}
fun values(payload: ByteArray, expectedCount: Int): List<Int> {
val values = if (NativeSetProtocol.available) {
requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
} else {
require(payload.size >= 6) { "GAS_WATCH_DATA короче заголовка" }
val count = read16(payload, 4)
require(count <= 64 && payload.size == 6 + count * 2) { "Неверная длина GAS_WATCH_DATA" }
List(count) { read16(payload, 6 + it * 2) }
}
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
val values = requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
require(values.size == expectedCount) { "Число значений GAS не соответствует подписке" }
return values
}
private fun read16(data: ByteArray, offset: Int): Int =
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
private fun put16(data: ByteArray, offset: Int, value: Int) {
data[offset] = value.toByte()
data[offset + 1] = (value ushr 8).toByte()
data class Sample(val timestampMs: Long, val values: List<Int>)
fun decode(payload: ByteArray, expectedCount: Int): Sample {
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
val decoded = requireNotNull(NativeSetProtocol.nativeTrendWatchDecode(payload)) { "Повреждён GAS_WATCH_DATA" }
require(decoded.size == expectedCount + 1) { "Число значений GAS не соответствует подписке" }
return Sample(decoded[0], decoded.drop(1).map(Long::toInt))
}
}

View File

@@ -32,3 +32,10 @@ fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, ran
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
fun plotDbDelta(a: Double, b: Double): Double? =
runCatching { NativePlot.call(7, a, b)[0] }.getOrNull()
data class PlotLimits(val xMin: Double, val xMax: Double, val yMin: Double, val yMax: Double) {
fun validated(): PlotLimits {
val values = NativePlot.call(8, xMin, xMax, yMin, yMax)
return PlotLimits(values[0], values[1], values[2], values[3])
}
}

View File

@@ -34,6 +34,8 @@ data class TrendSpectrum(
val binHz: Double get() = if (size > 0) sampleRate / size else 0.0
}
data class SpectrumPeak(val frequencyHz: Double, val amplitude: Double)
/** Math is implemented once in C and used unchanged by JNI and ctypes. */
object SpectrumAnalyzer {
fun analyze(points: List<TrendPoint>, options: SpectrumOptions): TrendSpectrum {
@@ -64,4 +66,12 @@ object SpectrumAnalyzer {
return TrendSpectrum(output[1].toInt(), output[2], output[3],
if (error == null) output.drop(4) else emptyList(), error)
}
fun dominantPeak(spectrum: TrendSpectrum, relativeThreshold: Double = 3.0,
absoluteFloor: Double = 1e-6): SpectrumPeak? {
if (spectrum.error != null || spectrum.size <= 0 || spectrum.amplitudes.size < 3) return null
val result = NativeSetProtocol.nativeSpectrumPeak(spectrum.amplitudes.toDoubleArray(),
spectrum.binHz, relativeThreshold, absoluteFloor) ?: return null
return result.takeIf { it.size == 2 }?.let { SpectrumPeak(it[0], it[1]) }
}
}

View File

@@ -276,6 +276,30 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendCanValue(
(uint8_t)flags, (const uint8_t *)data, (size_t)size);
}
JNIEXPORT jdoubleArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrumPeak(
JNIEnv *env, jobject self, jdoubleArray input, jdouble bin_hz,
jdouble relative_threshold, jdouble absolute_floor)
{
(void)self;
jsize count = input == NULL ? 0 : (*env)->GetArrayLength(env, input);
if (count < 3 || count > (jsize)(SET_SPECTRUM_MAX / 2U + 1U)) return NULL;
double *buffer = (double *)malloc(sizeof(double) * (size_t)(count * 2 - 1));
if (buffer == NULL) return NULL;
double *amplitudes = buffer, *scratch = buffer + count, peak[2];
(*env)->GetDoubleArrayRegion(env, input, 0, count, amplitudes);
int status = (*env)->ExceptionCheck(env) ? -1 : set_spectrum_dominant_peak(
amplitudes, (size_t)count, bin_hz, relative_threshold, absolute_floor,
scratch, (size_t)count - 1U, peak, 2U);
jdoubleArray result = NULL;
if (status >= 0) {
result = (*env)->NewDoubleArray(env, status == 1 ? 2 : 0);
if (result != NULL && status == 1) (*env)->SetDoubleArrayRegion(env, result, 0, 2, peak);
}
free(buffer);
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
JNIEnv *env, jobject self, jint period, jintArray input)
@@ -298,6 +322,20 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
return result;
}
JNIEXPORT jboolean JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchAck(
JNIEnv *env, jobject self, jbyteArray input, jint period, jint count)
{
(void)self;
if (input == NULL || period < 0 || period > 65535 || count < 0) return JNI_FALSE;
jsize size = (*env)->GetArrayLength(env, input);
if (size != 4) return JNI_FALSE;
jbyte payload[4];
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
return set_trend_watch_ack((const uint8_t *)payload, (size_t)size,
(uint16_t)period, (size_t)count) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jintArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
JNIEnv *env, jobject self, jbyteArray input)
@@ -317,6 +355,28 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
return result;
}
JNIEXPORT jlongArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchDecode(
JNIEnv *env, jobject self, jbyteArray input)
{
(void)self;
jsize size = input == NULL ? 0 : (*env)->GetArrayLength(env, input);
if (size < 6 || size > (jsize)(6U + 2U * SET_TREND_WATCH_MAX)) return NULL;
jbyte payload[6U + 2U * SET_TREND_WATCH_MAX];
uint16_t words[SET_TREND_WATCH_MAX];
uint32_t timestamp = 0U;
jlong values[1U + SET_TREND_WATCH_MAX];
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
int count = set_trend_watch_decode((const uint8_t *)payload, (size_t)size,
&timestamp, words, SET_TREND_WATCH_MAX);
if (count < 0) return NULL;
values[0] = (jlong)timestamp;
for (int i = 0; i < count; ++i) values[i + 1] = (jlong)words[i];
jlongArray result = (*env)->NewLongArray(env, count + 1);
if (result != NULL) (*env)->SetLongArrayRegion(env, result, 0, count + 1, values);
return result;
}
typedef struct {
uint8_t *storage;
size_t storage_size;

View File

@@ -36,4 +36,12 @@ class PlotViewportTest {
}
SpectrumOptions().validate()
}
@Test fun absoluteLimitsAndDominantPeakUseNativeCore() {
assertEquals(PlotLimits(0.0, 500.0, -1.0, 1.0),
PlotLimits(0.0, 500.0, -1.0, 1.0).validated())
assertTrue(runCatching { PlotLimits(1.0, 1.0, -1.0, 1.0).validated() }.isFailure)
val spectrum = TrendSpectrum(12, 60.0, 0.0,
listOf(10.0, .01, .02, .8, .03, .4, .02))
assertEquals(15.0, SpectrumAnalyzer.dominantPeak(spectrum)!!.frequencyHz, 0.0)
}
}