From db5033a6440f3798bb604fc5c337d7f344a8f9d3 Mon Sep 17 00:00:00 2001 From: Optio Agent Date: Thu, 9 Jul 2026 06:22:08 +0000 Subject: [PATCH 1/2] Fix FlexRadio meter-stream crash on a named-less dBm/swr meter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FlexMeterList.setMeters() already guards the unknown-meter case (infos.get(val) == null) but then unconditionally dereferences meter.name.contains("PWR") in the dBm/swr branch. FlexMeterInfo.nam is left null whenever the meter-definition frame carried a .unit token but no (or an empty) .nam token — e.g. a partial or split METER_LIST frame that supplies "N.unit=dBm" for a meter whose .nam was never seen (the existing FlexMeterInfosTest.reparseUpdates... case exercises exactly this "unit without name" shape). Reaching the dBm/swr branch for such a meter threw an uncaught NullPointerException. Meter value frames are parsed on the FlexRadio meter-stream read thread, whose loop catches only SocketException/IOException (same context as the FlexMeterInfos crash fixed in PR #491), so the escaping NPE crashed the whole app on every subsequent meter update. Root cause: unsafe coupling — the consumer assumes .nam and .unit are always present together, but FlexMeterInfo populates them independently. Fix: null-guard meter.name before contains("PWR"). A nameless meter can't be identified as a PWR meter, so it simply keeps its raw dBm value, which is the correct conservative behavior. Adds FlexMeterListTest (pure JVM, no Robolectric) covering the nameless dBm and swr meters (the crash), plus the named FWDPWR dBm->watt conversion, a named non-PWR dBm meter, and the unknown-id skip guard to lock in existing behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/k1af/ft8af/flex/FlexMeterList.java | 7 +- .../k1af/ft8af/flex/FlexMeterListTest.java | 100 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 ft8af/app/src/test/java/com/k1af/ft8af/flex/FlexMeterListTest.java diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java b/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java index 1e0fe0cf..6ef250b2 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java @@ -33,7 +33,12 @@ public synchronized void setMeters(byte[] data, FlexMeterInfos infos) { case dBm: case swr: meter.value = readShortData(data, i * 4 + 2) / 128f; - if (meter.name.contains("PWR")){//Convert dBm to power value + // meter.name is null when the definition frame carried .unit + // but no .nam token (e.g. a partial/split METER_LIST frame), + // so guard the dereference — an escaping NPE here crashes the + // FlexRadio meter-stream read thread (which catches only + // IOException) and takes down the whole app. + if (meter.name != null && meter.name.contains("PWR")){//Convert dBm to power value meter.value=(float) Math.pow(10,meter.value/10f)/1000f; } //Save resources by pre-assigning values diff --git a/ft8af/app/src/test/java/com/k1af/ft8af/flex/FlexMeterListTest.java b/ft8af/app/src/test/java/com/k1af/ft8af/flex/FlexMeterListTest.java new file mode 100644 index 00000000..c2ebb6c9 --- /dev/null +++ b/ft8af/app/src/test/java/com/k1af/ft8af/flex/FlexMeterListTest.java @@ -0,0 +1,100 @@ +package com.k1af.ft8af.flex; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +/** + * Pure-JVM coverage for {@link FlexMeterList#setMeters(byte[], FlexMeterInfos)} — + * the crash fix for a FlexRadio meter definition that carries a {@code .unit} + * but no {@code .nam}. + * + *

Meter value frames arrive on the FlexRadio meter-stream read thread, whose + * loop catches only {@code SocketException}/{@code IOException} (mirroring the + * {@link FlexMeterInfos} crash fixed earlier). {@code setMeters} already guards + * the unknown-meter case ({@code infos.get(val) == null}), but then + * unconditionally dereferenced {@code meter.name.contains("PWR")}. + * {@link FlexMeterInfos.FlexMeterInfo#nam} is {@code null} whenever the + * {@code .nam} token was absent or empty in the meter-definition frame (e.g. a + * partial/split {@code METER_LIST} frame that carried only {@code N.unit=...}), + * so a dBm/swr meter with no name threw an uncaught + * {@link NullPointerException} on the read thread and crashed the whole app. + * + *

{@link FlexMeterList} touches no Android types, so no Robolectric runner is + * needed. + */ +public class FlexMeterListTest { + + /** + * Encodes one meter (id + raw value) into the 4-byte-per-meter payload that + * {@code setMeters} consumes. Both fields are big-endian 16-bit, matching + * {@link VITA#readShortData(byte[], int)}. + */ + private static byte[] meterPayload(int id, int rawValue) { + return new byte[]{ + (byte) (id >> 8), (byte) id, + (byte) (rawValue >> 8), (byte) rawValue, + }; + } + + @Test + public void dbmMeterWithoutName_doesNotCrash() { + // A meter definition that carried .unit=dBm but no .nam token: nam stays + // null while unit becomes dBm. Reaching the dBm branch used to NPE on + // meter.name.contains("PWR"). + FlexMeterInfos infos = new FlexMeterInfos("meter 7.unit=dBm"); + assertThat(infos.get(7).nam).isNull(); + assertThat(infos.get(7).unit).isEqualTo(FlexMeterType.dBm); + + FlexMeterList meters = new FlexMeterList(); + meters.setMeters(meterPayload(7, 128), infos); // 128 / 128f == 1.0 dBm + + FlexMeterList.FlexMeter meter = meters.get(7); + assertThat(meter).isNotNull(); + assertThat(meter.name).isNull(); + assertThat(meter.value).isEqualTo(1.0f); + } + + @Test + public void swrMeterWithoutName_doesNotCrash() { + // The swr branch shares the meter.name.contains("PWR") dereference. + FlexMeterInfos infos = new FlexMeterInfos("meter 3.unit=SWR"); + assertThat(infos.get(3).nam).isNull(); + assertThat(infos.get(3).unit).isEqualTo(FlexMeterType.swr); + + FlexMeterList meters = new FlexMeterList(); + meters.setMeters(meterPayload(3, 256), infos); // 256 / 128f == 2.0 + + assertThat(meters.get(3).value).isEqualTo(2.0f); + } + + @Test + public void namedPwrMeter_stillConvertsDbmToPower() { + // The PWR-name path must still work: FWDPWR is dBm and gets converted to + // watts via 10^(dBm/10)/1000. 30 dBm -> 1.0 W. + FlexMeterInfos infos = new FlexMeterInfos("meter 5.nam=FWDPWR#5.unit=dBm"); + FlexMeterList meters = new FlexMeterList(); + meters.setMeters(meterPayload(5, 30 * 128), infos); // 3840 / 128f == 30 dBm + + assertThat(meters.get(5).value).isWithin(1e-4f).of(1.0f); + } + + @Test + public void namedNonPwrDbmMeter_keepsRawDbm() { + FlexMeterInfos infos = new FlexMeterInfos("meter 1.nam=LEVEL#1.unit=dBm"); + FlexMeterList meters = new FlexMeterList(); + meters.setMeters(meterPayload(1, -20 * 128), infos); // -2560 / 128f == -20 dBm + + assertThat(meters.get(1).value).isEqualTo(-20.0f); + } + + @Test + public void unknownMeterId_isSkipped() { + // Pre-existing guard: a value frame for an id with no definition is + // ignored rather than crashing. + FlexMeterInfos infos = new FlexMeterInfos("meter 1.nam=LEVEL#1.unit=dBm"); + FlexMeterList meters = new FlexMeterList(); + meters.setMeters(meterPayload(99, 128), infos); + assertThat(meters).isEmpty(); + } +} From 299c38fc43ce074542a544dfffbf2d8e356c5acd Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Thu, 9 Jul 2026 08:58:40 -0500 Subject: [PATCH 2/2] Correct the read-thread exception note in FlexMeterList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RadioTcpClient.SocketThread.run() catches both SocketException and IOException (RadioTcpClient.java:151-153), not only IOException. The NPE escapes regardless because neither catches RuntimeException, so the fix is unchanged — this just makes the inline note accurate. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA --- ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java b/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java index 6ef250b2..b609b181 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/flex/FlexMeterList.java @@ -37,7 +37,8 @@ public synchronized void setMeters(byte[] data, FlexMeterInfos infos) { // but no .nam token (e.g. a partial/split METER_LIST frame), // so guard the dereference — an escaping NPE here crashes the // FlexRadio meter-stream read thread (which catches only - // IOException) and takes down the whole app. + // SocketException/IOException, not RuntimeException) and takes + // down the whole app. if (meter.name != null && meter.name.contains("PWR")){//Convert dBm to power value meter.value=(float) Math.pow(10,meter.value/10f)/1000f; }