From bd0d133e1d0bc734ffd3dfad9aadc9e859d9afcb Mon Sep 17 00:00:00 2001 From: randal007 Date: Tue, 13 Oct 2026 11:47:08 -0700 Subject: [PATCH 1/4] js8core: commands and SNR to and from compound calls packed as desktop pack_cmd()/unpack_cmd() packed a compound-directed frame's command as (cmd & 0x1F) << 2 | (num & 0x03). Desktop's Varicode::packCmd() packs an SNR command as [1][type][6-bit number] (type 1 = HEARTBEAT SNR) and any other command as its 7-bit value. So every command to or from a compound call (KG4UHM/6, EA8/K1ABC) differed from desktop's, and an SNR kept only 2 bits: a reply "KG4UHM/6 SNR -03" went out as SNR -31, and a compound station's heartbeat ACK read back as "HW CPY?". Seen on the air 2026-10-11 between an X6100 running js8core and KG4UHM/6 on desktop. varicode_roundtrip: frames to and from KG4UHM/6 checked against desktop's own buildMessageFrames() (built with Qt), and desktop's compound frames read back with their command and SNR. Co-Authored-By: Claude Opus 5.5 --- core/src/protocol/varicode.cpp | 20 ++++++++++++--- core/tools/varicode_roundtrip.cpp | 42 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/core/src/protocol/varicode.cpp b/core/src/protocol/varicode.cpp index 8263bf0c..e1304c75 100644 --- a/core/src/protocol/varicode.cpp +++ b/core/src/protocol/varicode.cpp @@ -731,14 +731,26 @@ std::uint8_t pack_pwr(std::string const& pwr, bool* ok) { } } +// As desktop's Varicode::packCmd(): an SNR command is [1][type][6-bit +// number] (type 1 = HEARTBEAT SNR), any other command its 7-bit value. std::uint8_t pack_cmd(std::uint8_t cmd, std::uint8_t num, bool* p_packed_num) { - if (p_packed_num) *p_packed_num = kSnrCmds.count(cmd) > 0; - return (cmd & 0x1F) << 2 | (num & 0x03); + if (kSnrCmds.count(cmd) > 0) { + if (p_packed_num) *p_packed_num = true; + std::uint8_t value = static_cast(((1 << 1) | (cmd == 29 ? 1 : 0)) << 6); + return static_cast(value + (num & ((1 << 6) - 1))); + } + if (p_packed_num) *p_packed_num = false; + return static_cast(cmd & ((1 << 7) - 1)); } +// As desktop's Varicode::unpackCmd(). std::uint8_t unpack_cmd(std::uint8_t value, std::uint8_t* p_num) { - if (p_num) *p_num = value & 0x03; - return (value >> 2) & 0x1F; + if (value & (1 << 7)) { + if (p_num) *p_num = value & ((1 << 6) - 1); + return (value & (1 << 6)) ? 29 /* HEARTBEAT SNR */ : 25 /* SNR */; + } + if (p_num) *p_num = 0; + return value & ((1 << 7) - 1); } bool is_snr_command(std::string const& cmd) { diff --git a/core/tools/varicode_roundtrip.cpp b/core/tools/varicode_roundtrip.cpp index 31c0f3fc..5753c058 100644 --- a/core/tools/varicode_roundtrip.cpp +++ b/core/tools/varicode_roundtrip.cpp @@ -275,6 +275,48 @@ int main() { check(!saw_directed, "compound sender and recipient do not emit standard directed frame"); } + { + // Commands to and from a compound call (KG4UHM/6) packed as desktop's + // Varicode::packCmd(): frames from desktop's buildMessageFrames() (built + // with Qt), Normal speed. pack_cmd() kept 2 bits of the SNR and shifted + // every command: "KG4UHM/6 SNR -03" went out as SNR -31. + struct Case { char const* mycall; char const* text; char const* frames; }; + for (auto const& c : std::vector{ + {"VE7NHW", "KG4UHM/6 SNR -03", "Bu7hI9zNOrMu 1 IXXfezaRO+cm 2"}, + {"VE7NHW", "KG4UHM/6 SNR +05", "Bu7hI9zNOrMu 1 IXXfezaRO+dm 2"}, + {"VE7NHW", "KG4UHM/6 HEARTBEAT SNR -08", "Bu7hI9zNOrMu 1 IXXfezaRO+k8 2"}, + {"VE7NHW", "KG4UHM/6 HW CPY?", "Bu7hI9zNOrMu 1 IXXfezaRO+Le 2"}, + {"VE7NHW", "KG4UHM/6 RR", "Bu7hI9zNOrMu 1 IXXfezaRO+Lu 2"}, + {"VE7NHW", "KG4UHM/6 SNR?", "Bu7hI9zNOrMu 1 IXXfezaRO+JG 2"}, + {"KG4UHM/6", "KJ7PRS SNR -03", "AXXfezaROrMu 1 IYAexYQ7O+cm 2"}, + {"KG4UHM/6", "KJ7PRS HEARTBEAT SNR -08", "AXXfezaROrMu 1 IYAexYQ7O+k8 2"}, + {"KG4UHM/6", "VE7NHW FB", "AXXfezaROrMu 1 Ju7hI9zNO+LW 2"}, + }) { + std::string got; + for (auto const& [frame, bits] : js8core::protocol::varicode::build_message_frames( + c.mycall, "CN89", "", c.text, false, false, 0, nullptr)) + got += (got.empty() ? "" : " ") + frame + " " + std::to_string(bits); + check(got == c.frames, std::string("'") + c.text + "' from " + c.mycall + " matches desktop's frames"); + } + + // Desktop's frames read back with their command and SNR (KG4UHM/6 acking + // a heartbeat showed as "HW CPY?"). + struct Back { char const* frame; char const* call; char const* cmd; char const* num; }; + for (auto const& b : std::vector{ + {"Ju7hI9zNO+j0", "VE7NHW", " HEARTBEAT SNR", "-17"}, + {"IXXfezaRO+cm", "KG4UHM/6", " SNR", "-03"}, + {"IXXfezaRO+a8", "KG4UHM/6", " SNR", "-24"}, + {"IXXfezaRO+Le", "KG4UHM/6", " HW CPY?", ""}, + {"IXXfezaRO+Mm", "KG4UHM/6", " 73", ""}, + }) { + std::uint8_t type = 0; + auto parts = js8core::protocol::varicode::unpack_compound_message(b.frame, &type, nullptr, nullptr); + bool ok = parts.size() >= 3 && parts[0] == b.call && parts[2] == b.cmd && + (!*b.num || (parts.size() >= 4 && parts[3] == b.num)); + check(ok, std::string("desktop's compound frame ") + b.frame + " reads as" + b.cmd + " " + b.num); + } + } + if (failures != 0) { std::cerr << "varicode_roundtrip failures: " << failures << "\n"; return 1; From 00aaf3f2e095012a54f85e56707cd1cb394f01f6 Mon Sep 17 00:00:00 2001 From: randal007 Date: Tue, 13 Oct 2026 11:47:08 -0700 Subject: [PATCH 2/4] js8core: Ultra (JS8 60) decoded on Turbo's schedule, as desktop Desktop JS8Call-improved (mainwindow.cpp, turboOrUltra) schedules JS8 60 exactly as Turbo: decode once the 79 symbols are in, retry every second. The engine gave only Turbo (C) that path; Ultra (I) got one decode a slot. Co-Authored-By: Claude Opus 5.5 --- core/src/engine/engine.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/engine/engine.cpp b/core/src/engine/engine.cpp index 15d0cbf3..3b01cd83 100644 --- a/core/src/engine/engine.cpp +++ b/core/src/engine/engine.cpp @@ -487,7 +487,8 @@ class Js8EngineImpl : public Js8Engine { int samples_needed = (sm.symbol_samples * JS8_NUM_SYMBOLS) + static_cast((0.5 + sm.start_delay_ms / 1000.0) * sample_rate); int retry_samples = period; - if (sm.id == protocol::SubmodeId::C) { + // Desktop schedules Ultra (I, its "JS8 60") exactly as Turbo (turboOrUltra). + if (sm.id == protocol::SubmodeId::C || sm.id == protocol::SubmodeId::I) { samples_needed = sm.symbol_samples * JS8_NUM_SYMBOLS; retry_samples = sample_rate; } From 744555ebbb0404e19f66dee9e20165e98f13c238 Mon Sep 17 00:00:00 2001 From: randal007 Date: Tue, 13 Oct 2026 11:47:08 -0700 Subject: [PATCH 3/4] js8core: and includes for GCC varicode.hpp uses std::function and jsc.cpp std::strncmp without the headers. Android's libc++ pulls them in transitively; GCC's libstdc++ doesn't always (building the core for the X6100 with GCC failed on them). Co-Authored-By: Claude Opus 5.5 --- core/include/js8core/protocol/varicode.hpp | 1 + core/src/protocol/jsc.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/core/include/js8core/protocol/varicode.hpp b/core/include/js8core/protocol/varicode.hpp index 72a21519..ba80976d 100644 --- a/core/include/js8core/protocol/varicode.hpp +++ b/core/include/js8core/protocol/varicode.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/core/src/protocol/jsc.cpp b/core/src/protocol/jsc.cpp index c774ade0..aeb74b80 100644 --- a/core/src/protocol/jsc.cpp +++ b/core/src/protocol/jsc.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include From 5a108cf42f5681de3e1ac6b019bff65f22e85614 Mon Sep 17 00:00:00 2001 From: randal007 Date: Tue, 13 Oct 2026 21:48:09 -0700 Subject: [PATCH 4/4] Revert "js8core: Ultra (JS8 60) decoded on Turbo's schedule, as desktop" Desktop decodes JS8 60 once a slot at samplesNeeded() (79 symbols + start delay + 0.5 s), which is what the engine already did for I. The early window made Ultra's first try 100 ms before the signal can be complete (punk-kaos on #108). This reverts commit 00aaf3f2. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GD9GNPL51haF49BmUbaxRf --- core/src/engine/engine.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/engine/engine.cpp b/core/src/engine/engine.cpp index 3b01cd83..15d0cbf3 100644 --- a/core/src/engine/engine.cpp +++ b/core/src/engine/engine.cpp @@ -487,8 +487,7 @@ class Js8EngineImpl : public Js8Engine { int samples_needed = (sm.symbol_samples * JS8_NUM_SYMBOLS) + static_cast((0.5 + sm.start_delay_ms / 1000.0) * sample_rate); int retry_samples = period; - // Desktop schedules Ultra (I, its "JS8 60") exactly as Turbo (turboOrUltra). - if (sm.id == protocol::SubmodeId::C || sm.id == protocol::SubmodeId::I) { + if (sm.id == protocol::SubmodeId::C) { samples_needed = sm.symbol_samples * JS8_NUM_SYMBOLS; retry_samples = sample_rate; }