Add optional RX/TX log streaming to K5Viewer

This commit is contained in:
Armel FAUVEAU committed 2026-07-09 05:12:57 +02:00
1 parent f8e1b7a436
commit e2d21c61c2
8 files changed
+309 -37

No files matched your search

+5
View File
@@ -211,6 +211,11 @@ enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
app/rxtx_log.c
)
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP)
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER)
if(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER AND NOT (ENABLE_FEAT_F4HWN_RXTX_LOG AND ENABLE_FEAT_F4HWN_SCREENSHOT))
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER requires ENABLE_FEAT_F4HWN_RXTX_LOG (log data) and ENABLE_FEAT_F4HWN_SCREENSHOT (serial transport).")
endif()
if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).")
+126
View File
@@ -81,6 +81,13 @@ static_assert(RXTX_LOG_VIEW_ANCHOR_COUNT <= 32);
// the copied prefix.
static_assert(RXTX_LOG_ENTRY_COPY_SIZE == offsetof(RXTX_LogEntry_t, battVolt) + sizeof(((RXTX_LogEntry_t *)0)->battVolt));
static_assert(sizeof(RXTX_LogEntry_t) >= RXTX_LOG_ENTRY_COPY_SIZE);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
// Both sizes are hardcoded in k5viewer.js (RF_LOG_ROW_SIZE and
// RF_LOG_PACKET_SIZE): pin them so a struct change breaks the build
// instead of the viewer.
static_assert(sizeof(RXTX_LogK5ViewerRow_t) == 25);
static_assert(RXTX_LOG_K5VIEWER_PACKET_SIZE == 1629);
#endif
static RXTX_LogEntry_t gViewCache[RXTX_LOG_VIEW_CACHE_COUNT];
static uint16_t gViewCacheStart;
@@ -728,6 +735,125 @@ static bool RXTX_LOG_GetFilteredEntry(uint16_t indexFromNewest, RXTX_LogEntry_t
return true;
}
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
static uint32_t RXTX_LOG_K5ViewerMix(uint32_t hash, uint32_t value)
{
hash ^= value & 0xFFu;
hash *= 16777619u;
hash ^= (value >> 8) & 0xFFu;
hash *= 16777619u;
hash ^= (value >> 16) & 0xFFu;
hash *= 16777619u;
hash ^= (value >> 24) & 0xFFu;
hash *= 16777619u;
return hash;
}
uint32_t RXTX_LOG_K5ViewerSignature(void)
{
uint32_t hash = 2166136261u;
hash = RXTX_LOG_K5ViewerMix(hash, gSessionActive);
hash = RXTX_LOG_K5ViewerMix(hash, gClearActive);
hash = RXTX_LOG_K5ViewerMix(hash, gLogHasTraffic);
hash = RXTX_LOG_K5ViewerMix(hash, gNextTrafficSequence);
hash = RXTX_LOG_K5ViewerMix(hash, gSessionFlags);
hash = RXTX_LOG_K5ViewerMix(hash, gSessionFrequency);
hash = RXTX_LOG_K5ViewerMix(hash, gSessionChannel);
// Mix the exported seconds, not the raw ticks: two consecutive ticks
// map to the same durationSeconds, hashing them would resend an
// identical packet every 500 ms during an active session.
hash = RXTX_LOG_K5ViewerMix(hash, (uint32_t)((gSessionTicks500ms + 1u) / 2u));
hash = RXTX_LOG_K5ViewerMix(hash, gSessionSMeter);
hash = RXTX_LOG_K5ViewerMix(hash, gSessionBattVolt);
return hash;
}
static void RXTX_LOG_SetK5ViewerChannelName(RXTX_LogK5ViewerRow_t *row, uint16_t channel)
{
memset(row->channelName, 0, sizeof(row->channelName));
if (channel == RXTX_LOG_CHANNEL_NONE)
return;
char name[RXTX_LOG_K5VIEWER_NAME_LENGTH + 1u];
SETTINGS_FetchChannelName(name, channel);
for (uint8_t i = 0; i < RXTX_LOG_K5VIEWER_NAME_LENGTH && name[i] != 0; i++)
row->channelName[i] = name[i];
}
static void RXTX_LOG_CopyK5ViewerRow(RXTX_LogK5ViewerRow_t *dst, const RXTX_LogFlashEntry_t *src)
{
dst->frequency = src->frequency;
dst->trafficSeq = src->trafficSeq;
dst->durationSeconds = src->durationSeconds;
dst->channel = src->channel;
dst->flags = src->flags;
dst->meter = src->sMeter;
dst->battVolt = src->battVolt;
RXTX_LOG_SetK5ViewerChannelName(dst, src->channel);
}
// Stream the whole packet through `send` without ever holding it in RAM:
// peak stack stays at one row plus one flash entry. The row area is always
// full-length; rowCount is not known before scanning, so the header
// announces every slot and padding rows are all-zero (the viewer already
// skips rows with frequency == 0).
void RXTX_LOG_SendK5ViewerPacket(void (*send)(const uint8_t *data, uint16_t size))
{
RXTX_LogK5ViewerRow_t row;
uint8_t rowsSent = 0;
uint8_t header[4] = {RXTX_LOG_K5VIEWER_VERSION, 0, RXTX_LOG_K5VIEWER_ROW_COUNT, 0};
if (gSessionActive)
header[1] |= RXTX_LOG_K5VIEWER_STATUS_ACTIVE;
if (gLogHasTraffic)
header[1] |= RXTX_LOG_K5VIEWER_STATUS_HAS_TRAFFIC;
if (gClearActive)
header[1] |= RXTX_LOG_K5VIEWER_STATUS_CLEARING;
send(header, sizeof(header));
row.frequency = gSessionFrequency;
row.trafficSeq = gNextTrafficSequence;
row.durationSeconds = (gSessionTicks500ms + 1u) / 2u;
row.channel = gSessionChannel;
row.flags = gSessionFlags;
row.meter = gSessionSMeter;
row.battVolt = gSessionBattVolt;
RXTX_LOG_SetK5ViewerChannelName(&row, gSessionChannel);
send((const uint8_t *)&row, sizeof(row));
if (gLogHasTraffic) {
uint16_t slot = RXTX_LOG_AddressToSlot(gNextFlashAddress);
for (uint16_t scanned = 0;
scanned < RXTX_LOG_SLOT_COUNT && rowsSent < RXTX_LOG_K5VIEWER_ROW_COUNT;
scanned++)
{
RXTX_LogFlashEntry_t flashEntry;
slot = RXTX_LOG_PreviousSlot(slot);
PY25Q16_ReadBuffer(RXTX_LOG_SlotToAddress(slot), &flashEntry, sizeof(flashEntry));
if (RXTX_LOG_IsBlankFlashEntry(&flashEntry))
break;
if (!RXTX_LOG_IsValidFlashEntry(&flashEntry) ||
!RXTX_LOG_IsTrafficFlags(flashEntry.flags))
continue;
RXTX_LOG_CopyK5ViewerRow(&row, &flashEntry);
send((const uint8_t *)&row, sizeof(row));
rowsSent++;
}
}
memset(&row, 0, sizeof(row));
for (; rowsSent < RXTX_LOG_K5VIEWER_ROW_COUNT; rowsSent++)
send((const uint8_t *)&row, sizeof(row));
}
#endif
static void RXTX_LOG_CaptureSession(uint8_t flags, const VFO_Info_t *vfo)
{
if (gClearActive)
+31 -2
View File
@@ -21,8 +21,8 @@
// (rxtx_log.c) so both layouts match byte-for-byte up to and including
// battVolt, copied in one pass. Scan-only fields (sequence) sit past the
// copied prefix in the flash layout and are not cached in RAM.
// The channel name is not stored: it is resolved from `channel` at display
// time via SETTINGS_FetchChannelName.
// The channel name is not stored in flash: it is resolved from `channel` when
// exporting the K5Viewer packet.
typedef struct {
uint32_t frequency;
uint32_t trafficSeq;
@@ -33,6 +33,35 @@ typedef struct {
uint8_t battVolt;
} RXTX_LogEntry_t;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define RXTX_LOG_K5VIEWER_VERSION 2u
#define RXTX_LOG_K5VIEWER_ROW_COUNT 64u
#define RXTX_LOG_K5VIEWER_NAME_LENGTH 10u
#define RXTX_LOG_K5VIEWER_STATUS_ACTIVE (1u << 0)
#define RXTX_LOG_K5VIEWER_STATUS_HAS_TRAFFIC (1u << 1)
#define RXTX_LOG_K5VIEWER_STATUS_CLEARING (1u << 2)
typedef struct __attribute__((packed)) {
uint32_t frequency;
uint32_t trafficSeq;
uint16_t durationSeconds;
uint16_t channel;
uint8_t flags;
uint8_t meter;
uint8_t battVolt;
char channelName[RXTX_LOG_K5VIEWER_NAME_LENGTH];
} RXTX_LogK5ViewerRow_t;
// On-wire packet layout: 4-byte header (version, status, rowCount,
// reserved), one live row, then RXTX_LOG_K5VIEWER_ROW_COUNT row slots,
// zero-padded past the last valid row. The packet is streamed row by row
// and never built whole in RAM, so only its size is defined here.
#define RXTX_LOG_K5VIEWER_PACKET_SIZE (4u + ((RXTX_LOG_K5VIEWER_ROW_COUNT + 1u) * sizeof(RXTX_LogK5ViewerRow_t)))
uint32_t RXTX_LOG_K5ViewerSignature(void);
void RXTX_LOG_SendK5ViewerPacket(void (*send)(const uint8_t *data, uint16_t size));
#endif
void RXTX_LOG_Init(void);
void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function);
void RXTX_LOG_BeginTx(const VFO_Info_t *vfo);
+31 -3
View File
@@ -39,8 +39,14 @@ bool gWasFKeyPressed = false;
// Packet types for serial key injection (K5Viewer → radio)
#define SERIAL_KEY_TYPE 0x03
#define SERIAL_KEY_TYPE_LONG 0x04
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define SERIAL_FEATURE_TYPE 0x05
#endif
volatile KEY_Code_t gKeyFromSerial = KEY_INVALID;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
volatile uint8_t gSerialViewerFeatures = 0;
#endif
static uint8_t gSerialKeyHoldCount = 0;
static uint8_t gSerialKeyLong = 0; // 0 = short press, 1 = long press
@@ -71,13 +77,35 @@ bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b)
break;
case STATE_KA_2:
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (b == 0x00)
*state = STATE_KA_3;
else if (b == SERIAL_FEATURE_TYPE)
*state = STATE_KA_FEATURE;
else
*state = STATE_IDLE;
#else
*state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE;
#endif
break;
case STATE_KA_3:
if (b == 0x00) connected = true;
if (b == 0x00) {
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
gSerialViewerFeatures = 0;
#endif
connected = true;
}
*state = STATE_IDLE;
break;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
case STATE_KA_FEATURE:
gSerialViewerFeatures = b;
connected = true;
*state = STATE_IDLE;
break;
#endif
case STATE_KEY_1:
*state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE;
@@ -265,4 +293,4 @@ KEY_Code_t KEYBOARD_GetKey(void)
void HideFKeyIcon(void) {
gWasFKeyPressed = false;
gUpdateStatus = true;
}
}
+11 -1
View File
@@ -50,6 +50,9 @@ typedef enum {
STATE_KA_1,
STATE_KA_2,
STATE_KA_3,
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
STATE_KA_FEATURE,
#endif
STATE_KEY_1,
STATE_KEY_2,
STATE_KEY_3,
@@ -65,6 +68,13 @@ extern bool gWasFKeyPressed;
// Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll).
extern volatile KEY_Code_t gKeyFromSerial;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define SERIAL_VIEWER_FEATURE_RF_LOG 0x01u
// Extension flags announced by the viewer's feature keepalive.
extern volatile uint8_t gSerialViewerFeatures;
#endif
bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b);
#endif
@@ -73,4 +83,4 @@ KEY_Code_t KEYBOARD_GetKey(void);
void HideFKeyIcon(void);
#endif
#endif
+9 -5
View File
@@ -46,15 +46,19 @@ void VCP_Init()
bool VCP_ScreenshotPing(void)
{
// State machine for parsing incoming packets:
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive, no extensions
// Feature keepalive (ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER builds only):
// 0x55 0xAA 0x05 <flags> → viewer alive, extensions enabled
// flags bit 0 = RF log stream
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
//
// State transitions:
// IDLE → 0x55 → KA_1
// KA_1 → 0xAA → KA_2 (else IDLE)
// KA_2 → 0x00 → KA_3 (else IDLE)
// KA_3 → 0x00 → keepalive OK, IDLE
// KA_2 → 0x00 → KA_3 (else 0x05 → KA_FEATURE, else IDLE)
// KA_3 → 0x00 → keepalive OK, clear extensions, IDLE
// KA_FEATURE → <flags> → keepalive OK, set extensions, IDLE
//
// IDLE → 0xAA → KEY_1
// KEY_1 → 0x55 → KEY_2 (else IDLE)
+94 -26
View File
@@ -21,6 +21,9 @@
#include "driver/vcp.h"
#include "driver/keyboard.h"
#include "driver/bk4819.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#include "app/rxtx_log.h"
#endif
// SRAM optimization: minimize static allocations
// - previousHash: one fingerprint per 8-byte chunk instead of a full
@@ -41,6 +44,10 @@ static uint8_t forcedBlock = 0;
static uint8_t keepAlive = 3;
static bool hasConnectionPing = false;
static bool wasConnected = false;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
static uint32_t previousRfLogSignature = 0;
static bool rfLogSent = false;
#endif
// FNV-1a over one 8-byte chunk, folded to 16 bits
static uint16_t SCREENSHOT_Hash(const uint8_t *data)
@@ -83,6 +90,8 @@ enum {
SCREENSHOT_CHUNKS_PER_LINE = 16,
SCREENSHOT_HALF_LINE_COLUMNS = LCD_WIDTH / 2,
SCREENSHOT_MARKER_BASE = 0xF0,
SCREENSHOT_TYPE_DIFF = 0x02,
SCREENSHOT_TYPE_RXTX_LOG = 0x05,
SCREENSHOT_FLAG_DEEP_SLEEP = 1 << 0,
SCREENSHOT_FLAG_LED_RED = 1 << 1,
SCREENSHOT_FLAG_LED_GREEN = 1 << 2,
@@ -106,11 +115,48 @@ static uint8_t SCREENSHOT_StateFlags(void)
return flags;
}
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
static bool SCREENSHOT_HasPendingRfLogUpdate(void)
{
if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG) == 0)
return false;
return !rfLogSent || RXTX_LOG_K5ViewerSignature() != previousRfLogSignature;
}
static void SCREENSHOT_SendRfLog(void)
{
// Capture the signature before streaming: a state change landing
// during the blocking send still differs afterwards and triggers a
// resend on the next cycle.
previousRfLogSignature = RXTX_LOG_K5ViewerSignature();
rfLogSent = true;
const uint16_t len = RXTX_LOG_K5VIEWER_PACKET_SIZE;
uint8_t header[5] = {
0xAA, 0x55, SCREENSHOT_TYPE_RXTX_LOG,
(uint8_t)(len >> 8),
(uint8_t)(len & 0xFF)
};
SCREENSHOT_Send(header, 5);
RXTX_LOG_SendK5ViewerPacket(SCREENSHOT_Send);
uint8_t end = 0x0A;
SCREENSHOT_Send(&end, 1);
}
#endif
bool SCREENSHOT_HasPendingStateChange(void)
{
if (gUART_LockScreenshot > 0 || keepAlive == 0 || !hasConnectionPing)
return false;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (SCREENSHOT_HasPendingRfLogUpdate())
return true;
#endif
return !wasConnected || SCREENSHOT_StateFlags() != previousStateFlags;
}
@@ -146,6 +192,10 @@ void SCREENSHOT_Update(bool force)
wasConnected = false;
hasConnectionPing = false;
previousStateFlags = 0xFF;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
gSerialViewerFeatures = 0;
rfLogSent = false;
#endif
return;
}
} else {
@@ -159,6 +209,11 @@ void SCREENSHOT_Update(bool force)
const uint8_t stateFlags = SCREENSHOT_StateFlags();
const bool stateChanged = (stateFlags != previousStateFlags);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
const bool rfLogPending = SCREENSHOT_HasPendingRfLogUpdate();
#else
const bool rfLogPending = false;
#endif
// ==== FIRST PASS: Count changed chunks ====
uint16_t deltaLen = 0;
@@ -179,7 +234,7 @@ void SCREENSHOT_Update(bool force)
forcedBlock = (forcedBlock + 1) % 128;
if (deltaLen == 0 && !stateChanged)
if (deltaLen == 0 && !stateChanged && !rfLogPending)
return;
// Skip transmission if a key is currently pressed
@@ -187,39 +242,52 @@ void SCREENSHOT_Update(bool force)
if (gKeyReading0 != KEY_INVALID)
return;
// ==== Send version marker and state flags ====
// 0xF0 keeps a resync-safe marker before the standard AA 55 header.
uint8_t versionMarker = SCREENSHOT_MARKER_BASE | stateFlags;
SCREENSHOT_Send(&versionMarker, 1);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
// A pending RF log packet may be the only thing to send: skip the
// frame section when it carries nothing. Without the RF log stream
// the early return above already guarantees this condition.
if (deltaLen != 0 || stateChanged)
#endif
{
// ==== Send version marker and state flags ====
// 0xF0 keeps a resync-safe marker before the standard AA 55 header.
uint8_t versionMarker = SCREENSHOT_MARKER_BASE | stateFlags;
SCREENSHOT_Send(&versionMarker, 1);
// ==== Send header ====
uint8_t header[5] = {
0xAA, 0x55, 0x02,
(uint8_t)(deltaLen >> 8),
(uint8_t)(deltaLen & 0xFF)
};
// ==== Send header ====
uint8_t header[5] = {
0xAA, 0x55, SCREENSHOT_TYPE_DIFF,
(uint8_t)(deltaLen >> 8),
(uint8_t)(deltaLen & 0xFF)
};
SCREENSHOT_Send(header, 5);
SCREENSHOT_Send(header, 5);
// ==== SECOND PASS: Send only changed chunks ====
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
if (!(changedBitmap[chunkIdx >> 3] & (1 << (chunkIdx & 7))))
continue;
// ==== SECOND PASS: Send only changed chunks ====
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
if (!(changedBitmap[chunkIdx >> 3] & (1 << (chunkIdx & 7))))
continue;
chunk[0] = chunkIdx;
SCREENSHOT_Chunk(chunkIdx, &chunk[1]);
chunk[0] = chunkIdx;
SCREENSHOT_Chunk(chunkIdx, &chunk[1]);
SCREENSHOT_Send(chunk, 9);
SCREENSHOT_Send(chunk, 9);
// Update the fingerprint only once the chunk is actually sent,
// so chunks skipped by an early return stay marked as changed.
// Hashing the recomputed payload also keeps the fingerprint in
// sync if the display buffer changed between the two passes.
previousHash[chunkIdx] = SCREENSHOT_Hash(&chunk[1]);
// Update the fingerprint only once the chunk is actually sent,
// so chunks skipped by an early return stay marked as changed.
// Hashing the recomputed payload also keeps the fingerprint in
// sync if the display buffer changed between the two passes.
previousHash[chunkIdx] = SCREENSHOT_Hash(&chunk[1]);
}
uint8_t end = 0x0A;
SCREENSHOT_Send(&end, 1);
}
uint8_t end = 0x0A;
SCREENSHOT_Send(&end, 1);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (rfLogPending)
SCREENSHOT_SendRfLog();
#endif
previousStateFlags = stateFlags;
wasConnected = true;
+2
View File
@@ -74,6 +74,7 @@
"ENABLE_FEAT_F4HWN_BEAM": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_LOGO": false,
"ENABLE_FEAT_F4HWN_LOGO_SAV": false,
"ENABLE_AGC_SHOW_DATA": false,
@@ -207,6 +208,7 @@
"ENABLE_FEAT_F4HWN_BEAM": true,
"ENABLE_FEAT_F4HWN_RXTX_LOG": true,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_QRCODE": true,
"ENABLE_FEAT_F4HWN_MEM": true,
"ENABLE_FEAT_F4HWN_LOGO": true,