Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4323e77
Added a navigation rail for tablets in landscape. A new layout-sw600d…
tekstrand Aug 20, 2026
8fe97f6
Merged the Decodes tab into the Monitor tab. The decode list is embed…
tekstrand Aug 21, 2026
05a7020
Added transmitted messages to the decode list. The decode list only s…
tekstrand Aug 21, 2026
31166ed
Restyled the Monitor tab with Material components. The status card ge…
tekstrand Aug 21, 2026
39feb1e
Turned the decode list into a chat thread. Decodes flow newest at the…
tekstrand Aug 21, 2026
d03158c
Redesigned the Messages tab as a texting app. The index lists the pin…
tekstrand Aug 21, 2026
caeab38
Added a Contacts tab, modeled on the desktop call activity list. Ever…
tekstrand Aug 21, 2026
3cc9574
Added a reference doc for the Android emulator setup on this Mac. It …
tekstrand Aug 21, 2026
db44614
Renamed the Everything thread to All activity and wired its preview l…
tekstrand Aug 21, 2026
e6702e5
Added a station info field to contacts, separate from the user commen…
tekstrand Aug 21, 2026
1265a69
Fixed the frequency dropdown losing its entries after a tab switch. S…
tekstrand Aug 21, 2026
10cca90
Switched the Messages tab to the Material Symbols chat bubble.
tekstrand Aug 21, 2026
ab3629d
Documented the phone AVD and corrected the audio limits.
tekstrand Aug 21, 2026
9015310
Added CQ, heartbeat and All activity buttons to the decode header.
tekstrand Aug 21, 2026
1f0b048
Fixed the Monitor tab doing nothing after opening All activity.
tekstrand Aug 21, 2026
97ba5e7
Replaced the Monitor status card with a compact status strip.
tekstrand Aug 21, 2026
c3fd70e
Offered the audio device in Settings as well as on the Monitor strip.
tekstrand Aug 21, 2026
ebdc38c
Gave the error state its own mark on the Monitor strip.
tekstrand Aug 21, 2026
d8aa127
Taught the directed-command parser the full JS8 command vocabulary. T…
tekstrand Aug 21, 2026
fdc6150
Stopped the callsign-prefix heuristic from corrupting continuation fr…
tekstrand Aug 21, 2026
c613fcf
Made the MSG reassembly timeout scale with the decode's submode. The …
tekstrand Aug 21, 2026
f182066
Added the store-and-forward mailbox tables and removed the destructiv…
tekstrand Aug 21, 2026
d91f491
Added a debug-only decode injection path for protocol testing. A broa…
tekstrand Aug 21, 2026
a5c1ba2
Implemented the receive side of store-and-forward messaging. A MSG TO…
tekstrand Aug 21, 2026
7d996c9
Added the Held messages screen for store-and-forward mail. The Messag…
tekstrand Aug 22, 2026
98d243d
Implemented the send side of store-and-forward messaging. A thread's …
tekstrand Aug 22, 2026
93dfc71
Made groups first-class threads and gave unsubscribed groups their ow…
tekstrand Aug 22, 2026
f958fc8
Made relayed traffic that reaches its destination actually arrive.
tekstrand Aug 22, 2026
6d90b17
Added a relay path to each direct-message thread.
tekstrand Aug 22, 2026
31e3ea6
Gave stations names, and gave contacts a screen.
tekstrand Aug 22, 2026
bcb88f9
Documented how the Android app layer is put together.
tekstrand Aug 22, 2026
4c5fe6e
Added link observations, the data layer under the coming network map.…
tekstrand Aug 22, 2026
d240b55
Added Maidenhead grid math and a distance row on the contact card. A …
tekstrand Aug 22, 2026
aa845af
Added a distance units setting, defaulting to miles. The contact card…
tekstrand Aug 22, 2026
96ff576
Added the network map, a force-directed graph of who hears whom. Netw…
tekstrand Aug 22, 2026
e43fd14
Moved the network map's empty-state text to the top of the frame. The…
tekstrand Aug 22, 2026
6eafde4
Gave the network map labels real clearance from their nodes. The labe…
tekstrand Aug 22, 2026
a6f2446
Fixed transmissions starting mid-frame and keying the radio immediate…
tekstrand Aug 22, 2026
3156309
Fixed the app freezing when the engine is stopped with the radio gone…
tekstrand Aug 22, 2026
8b27d46
Moved rig control teardown off the main thread, which is what actuall…
tekstrand Aug 22, 2026
263a547
Fixed the transmit queue jamming when the engine is stopped mid-trans…
tekstrand Aug 22, 2026
16b3585
Shortened the comments added with the transmit fixes to match the res…
tekstrand Aug 22, 2026
6602e40
Added engine-level tests that push real signal through the decoder an…
tekstrand Aug 23, 2026
cf4d5c6
Added a blind search that finds a badly wrong clock on its own, and a…
tekstrand Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Please open bug reports when you find things wrong, bug reports help me build th
ViewModel-backed decode buffering for multipart messages.

See `android/README.md` for build and usage details, and
`android/ENGINE_INTEGRATION_COMPLETE.md` for the end-to-end architecture notes.
`android/ARCHITECTURE.md` for how the app layer is put together.

## License

Expand Down
42 changes: 40 additions & 2 deletions adapters/android/jni/js8_engine_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ JNIEXPORT void JNICALL Java_com_js8call_core_JS8Engine_nativeStopTransmit(JNIEnv
JNIEXPORT jboolean JNICALL Java_com_js8call_core_JS8Engine_nativeIsTransmitting(JNIEnv*, jobject, jlong);
JNIEXPORT jboolean JNICALL Java_com_js8call_core_JS8Engine_nativeIsTransmittingAudio(JNIEnv*, jobject, jlong);
JNIEXPORT jint JNICALL Java_com_js8call_core_JS8Engine_nativeTxMillisecondsUntilAudio(JNIEnv*, jobject, jlong);
JNIEXPORT jint JNICALL Java_com_js8call_core_JS8Engine_nativeTxFrameIndex(JNIEnv*, jobject, jlong);
JNIEXPORT jint JNICALL Java_com_js8call_core_JS8Engine_nativeTxFrameCount(JNIEnv*, jobject, jlong);
JNIEXPORT void JNICALL Java_com_js8call_core_JS8Engine_nativeSetTxReady(JNIEnv*, jobject, jlong, jboolean);
JNIEXPORT void JNICALL Java_com_js8call_core_JS8Engine_nativeSetTimeDriftMs(JNIEnv*, jobject, jlong, jlong);
JNIEXPORT jlong JNICALL Java_com_js8call_core_JS8Engine_nativeGetTimeDriftMs(JNIEnv*, jobject, jlong);
Expand Down Expand Up @@ -223,6 +225,9 @@ static bool is_callsign_like(std::string const& token) {
return has_digit;
}

// Only valid on a line's first frame. A continuation frame of a buffered
// command is plain payload text, and rewriting "KA0XYZ N0CALL QRV" into
// "KA0XYZ: N0CALL QRV" there corrupts the body and breaks its checksum.
static std::string maybe_insert_callsign_prefix(std::string const& text) {
std::size_t first_sep = std::string::npos;
for (std::size_t i = 0; i < text.size(); ++i) {
Expand Down Expand Up @@ -269,6 +274,10 @@ static std::string render_decoded_text(js8core::events::Decoded const& decoded)
}

bool is_data_flag = (decoded.type & 0b100) == 0b100;
// The transmitter sets this bit only on a line's first frame
// (varicode.cpp build_message_frames), so a continuation frame of a
// buffered command never carries it.
bool is_first_frame = (decoded.type & 0b1) == 0b1;
// Try data payloads first (mirrors desktop unpack order).
__android_log_print(ANDROID_LOG_DEBUG, "JS8FrameDebug",
"Trying data unpacker: frame='%s', decoded.type=0x%02x",
Expand All @@ -277,7 +286,9 @@ static std::string render_decoded_text(js8core::events::Decoded const& decoded)
auto data = unpack_fast_data_message(frame);
__android_log_print(ANDROID_LOG_DEBUG, "JS8FrameDebug",
"unpack_fast_data returned: '%s'", data.c_str());
if (!data.empty()) return maybe_insert_callsign_prefix(data);
if (!data.empty()) {
return is_first_frame ? maybe_insert_callsign_prefix(data) : data;
}
// Fast-data frames should not be treated as heartbeat/compound/directed.
__android_log_print(ANDROID_LOG_WARN, "JS8FrameDebug",
"Fast data unpack failed, returning raw frame: '%s'", frame.c_str());
Expand All @@ -286,7 +297,9 @@ static std::string render_decoded_text(js8core::events::Decoded const& decoded)
auto data = unpack_data_message(frame);
__android_log_print(ANDROID_LOG_DEBUG, "JS8FrameDebug",
"unpack_data returned: '%s'", data.c_str());
if (!data.empty()) return maybe_insert_callsign_prefix(data);
if (!data.empty()) {
return is_first_frame ? maybe_insert_callsign_prefix(data) : data;
}
}

// Heartbeat (most common for status beacons)
Expand Down Expand Up @@ -436,6 +449,17 @@ static void event_callback(JS8Engine_Native* native, js8core::events::Variant co
env->CallVoidMethod(native->callback_handler, method,
static_cast<jint>(decode_finished->decoded));
}
} else if (auto* timing = std::get_if<js8core::events::TimingSuggestion>(&event)) {
// Call onTimingSuggestion(int kind, int driftMs, int step, int steps)
jmethodID method = env->GetMethodID(handler_class, "onTimingSuggestion", "(IIIII)V");
if (method) {
env->CallVoidMethod(native->callback_handler, method,
static_cast<jint>(timing->kind),
static_cast<jint>(timing->drift_ms),
static_cast<jint>(timing->step),
static_cast<jint>(timing->steps),
static_cast<jint>(timing->period_ms));
}
}

env->DeleteLocalRef(handler_class);
Expand Down Expand Up @@ -888,6 +912,16 @@ int js8_engine_tx_milliseconds_until_audio(JS8Engine_Native* engine) {
return engine->engine->tx_milliseconds_until_audio();
}

int js8_engine_tx_frame_index(JS8Engine_Native* engine) {
if (!engine || !engine->engine) return 0;
return engine->engine->tx_frame_index();
}

int js8_engine_tx_frame_count(JS8Engine_Native* engine) {
if (!engine || !engine->engine) return 0;
return engine->engine->tx_frame_count();
}

void js8_engine_set_tx_ready(JS8Engine_Native* engine, bool ready) {
if (!engine || !engine->engine) return;
engine->engine->set_tx_ready(ready);
Expand Down Expand Up @@ -945,6 +979,10 @@ int js8_register_natives(JavaVM* vm, JNIEnv* env) {
(void*)Java_com_js8call_core_JS8Engine_nativeIsTransmittingAudio},
{"nativeTxMillisecondsUntilAudio", "(J)I",
(void*)Java_com_js8call_core_JS8Engine_nativeTxMillisecondsUntilAudio},
{"nativeTxFrameIndex", "(J)I",
(void*)Java_com_js8call_core_JS8Engine_nativeTxFrameIndex},
{"nativeTxFrameCount", "(J)I",
(void*)Java_com_js8call_core_JS8Engine_nativeTxFrameCount},
{"nativeSetTxReady", "(JZ)V",
(void*)Java_com_js8call_core_JS8Engine_nativeSetTxReady},
{"nativeSetTimeDriftMs", "(JJ)V",
Expand Down
2 changes: 2 additions & 0 deletions adapters/android/jni/js8_engine_jni.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ void js8_engine_stop_transmit(JS8Engine_Native* engine);
int js8_engine_is_transmitting(JS8Engine_Native* engine);
int js8_engine_is_transmitting_audio(JS8Engine_Native* engine);
int js8_engine_tx_milliseconds_until_audio(JS8Engine_Native* engine);
int js8_engine_tx_frame_index(JS8Engine_Native* engine);
int js8_engine_tx_frame_count(JS8Engine_Native* engine);
void js8_engine_set_tx_ready(JS8Engine_Native* engine, bool ready);

// Status queries
Expand Down
18 changes: 18 additions & 0 deletions adapters/android/jni/js8_jni_methods.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,24 @@ Java_com_js8call_core_JS8Engine_nativeTxMillisecondsUntilAudio(
return static_cast<jint>(js8_engine_tx_milliseconds_until_audio(engine));
}

JNIEXPORT jint JNICALL
Java_com_js8call_core_JS8Engine_nativeTxFrameIndex(
JNIEnv* /* env */,
jobject /* thiz */,
jlong handle) {
JS8Engine_Native* engine = reinterpret_cast<JS8Engine_Native*>(handle);
return static_cast<jint>(js8_engine_tx_frame_index(engine));
}

JNIEXPORT jint JNICALL
Java_com_js8call_core_JS8Engine_nativeTxFrameCount(
JNIEnv* /* env */,
jobject /* thiz */,
jlong handle) {
JS8Engine_Native* engine = reinterpret_cast<JS8Engine_Native*>(handle);
return static_cast<jint>(js8_engine_tx_frame_count(engine));
}

JNIEXPORT void JNICALL
Java_com_js8call_core_JS8Engine_nativeSetTxReady(
JNIEnv* /* env */,
Expand Down
23 changes: 23 additions & 0 deletions adapters/android/jni/kotlin/JS8Engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,20 @@ class JS8Engine private constructor(
return withNativeHandleOr(-1) { nativeTxMillisecondsUntilAudio(it) }
}

/**
* 1-based index of the frame now being sent, or 0 when idle or tuning.
*/
fun txFrameIndex(): Int {
return withNativeHandleOr(0) { nativeTxFrameIndex(it) }
}

/**
* Total frames in the current message, or 0 when idle or tuning.
*/
fun txFrameCount(): Int {
return withNativeHandleOr(0) { nativeTxFrameCount(it) }
}

/** Controls whether scheduled modulation may advance and reach the output. */
fun setTransmitReady(ready: Boolean) {
withNativeHandle { nativeSetTxReady(it, ready) }
Expand Down Expand Up @@ -337,6 +351,8 @@ class JS8Engine private constructor(
private external fun nativeIsTransmitting(handle: Long): Boolean
private external fun nativeIsTransmittingAudio(handle: Long): Boolean
private external fun nativeTxMillisecondsUntilAudio(handle: Long): Int
private external fun nativeTxFrameIndex(handle: Long): Int
private external fun nativeTxFrameCount(handle: Long): Int
private external fun nativeSetTxReady(handle: Long, ready: Boolean)
private external fun nativeSetTimeDriftMs(handle: Long, driftMs: Long)
private external fun nativeGetTimeDriftMs(handle: Long): Long
Expand Down Expand Up @@ -396,5 +412,12 @@ class JS8Engine private constructor(
* Called with TX audio PCM samples when enabled.
*/
fun onTxAudio(samples: ShortArray, sampleRateHz: Int) {}

/**
* Raised when decoding has gone dead and the engine is trying shifted
* windows to find the clock offset. [kind] is 0 searching, 1 found,
* 2 gave up. [driftMs] is the total drift to apply on a find.
*/
fun onTimingSuggestion(kind: Int, driftMs: Int, step: Int, steps: Int, periodMs: Int) {}
}
}
Loading
Loading