mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add optional RX/TX log streaming to K5Viewer
This commit is contained in:
1 parent
f8e1b7a436
commit
e2d21c61c2
8 files changed
+309
-37
No files matched your search
@@ -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).")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user