Speed up AirCopy by skipping identical blocks

This commit is contained in:
Armel FAUVEAU committed 2026-09-22 06:01:40 +02:00
1 parent 398482e06b
commit 720a5e8861
3 files changed
+312 -68

No files matched your search

+257 -47
View File
@@ -21,6 +21,9 @@
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/eeprom.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#endif
#include "driver/system.h"
#include "frequencies.h"
#include "misc.h"
@@ -47,17 +50,21 @@ bool gAircopyAll;
uint16_t g_FSK_Buffer[AIRCOPY_FRAME_WORDS_MAX];
uint8_t gFskRxExpectedWords = AIRCOPY_DATA_WORDS;
// Stop-and-wait protocol. Every frame keeps the original 64-byte payload:
// DATA is acknowledged only after storage, ACK confirms that offset, and
// RESEND requests the same offset immediately instead of waiting for timeout.
// Stop-and-wait protocol. HASH sends one CRC32 per block in a logical group.
// DIFF returns a bitmap of blocks needing DATA; ACK confirms stored DATA.
#define AIRCOPY_PACKET_DATA 0xABCDu
#define AIRCOPY_PACKET_ACK 0xABCEu
#define AIRCOPY_PACKET_RESEND 0xABCFu
#define AIRCOPY_PACKET_SKIP 0xABD0u
#define AIRCOPY_PACKET_HASH 0xABD1u
#define AIRCOPY_PACKET_DIFF 0xABD2u
#define AIRCOPY_PACKET_END 0xDCBAu
// Sender's wait for an ACK after a DATA frame. The real round trip once the
// DATA TX finishes is well under 1 s (receiver stores + sends a ~0.35 s ACK), so
// 1.5 s leaves a ~3x margin while recovering a lost frame far faster than the
// old 4 s did.
#define AIRCOPY_HASH_GROUP_BLOCKS 24u
#if AIRCOPY_HASH_GROUP_BLOCKS > 32u || (2u + 2u * AIRCOPY_HASH_GROUP_BLOCKS + 2u) > AIRCOPY_DATA_WORDS
#error AirCopy hash group does not fit the bitmap or forward frame
#endif
// Sender's wait for an ACK after a DATA frame. The receiver stores the frame
// and returns a short control packet before this timeout expires.
#define AIRCOPY_ACK_TIMEOUT_10MS 150u
#define AIRCOPY_RX_TIMEOUT_10MS 2000u
#define AIRCOPY_RX_LINGER_10MS 500u
@@ -65,16 +72,21 @@ uint8_t gFskRxExpectedWords = AIRCOPY_DATA_WORDS;
static uint16_t AircopyCountdown;
static uint8_t AircopyRetries;
static uint8_t AircopyInFlight; // blocks the sender packed into the DATA frame awaiting ACK
static uint8_t AircopyInFlight;
static uint8_t AircopyGroupCount;
static uint16_t AircopyGroupStart;
static uint32_t AircopyPendingMask;
static bool AircopyProbePending;
static uint16_t AircopyLastAckStart;
static uint16_t AircopyLastAckType;
static uint32_t AircopyLastAckMask;
// Header packing for a DATA frame: g_FSK_Buffer[1] = start block | (count << 12).
#define AIRCOPY_HDR_BLOCK(hdr) ((hdr) & 0x0FFFu)
#define AIRCOPY_HDR_COUNT(hdr) ((hdr) >> 12)
#define AIRCOPY_MAKE_HDR(block, count) ((uint16_t)(((count) << 12) | ((block) & 0x0FFFu)))
#define AIRCOPY_BANK_BLOCKS 68u
#define AIRCOPY_SETTINGS_BLOCKS 12u
#define AIRCOPY_ALL_BLOCKS (AIRCOPY_NUM_BANKS * AIRCOPY_BANK_BLOCKS + AIRCOPY_SETTINGS_BLOCKS)
static uint8_t AircopySkippedBlocks[(AIRCOPY_ALL_BLOCKS + 7u) / 8u];
// ============================================================================
// Helper Functions
@@ -89,6 +101,19 @@ uint16_t AIRCOPY_GetTotalBlocks(void)
: AIRCOPY_BANK_BLOCKS;
}
bool AIRCOPY_BlockWasSkipped(uint16_t block)
{
return block < AIRCOPY_ALL_BLOCKS &&
(AircopySkippedBlocks[block / 8u] & (1u << (block % 8u))) != 0u;
}
static void AIRCOPY_MarkSkipped(uint16_t start, uint8_t count, uint32_t changedMask)
{
for (uint8_t i = 0; i < count; i++)
if ((changedMask & (1u << i)) == 0u)
AircopySkippedBlocks[(start + i) / 8u] |= 1u << ((start + i) % 8u);
}
// Resolve the map that a (possibly global, in All mode) block index lands in.
// On return, *block is rewritten to the block index within that map.
static uint8_t AIRCOPY_ResolveMap(uint16_t *block)
@@ -136,6 +161,43 @@ static uint16_t AIRCOPY_GetBlockOffset(uint16_t block)
+ (block - 64u) * AIRCOPY_BLOCK_SIZE;
}
static uint8_t AIRCOPY_GroupBlockCount(uint16_t start, uint16_t total)
{
const uint16_t remaining = total - start;
const uint16_t groupRemaining = AIRCOPY_HASH_GROUP_BLOCKS
- (start % AIRCOPY_HASH_GROUP_BLOCKS);
return (uint8_t)(remaining < groupRemaining ? remaining : groupRemaining);
}
// Hash one logical block. Each call needs only a 64-byte scratch buffer.
static uint32_t AIRCOPY_BlockCRC32(uint16_t blockNumber)
{
uint8_t block[AIRCOPY_BLOCK_SIZE];
EEPROM_ReadBuffer(AIRCOPY_GetBlockOffset(blockNumber), block, sizeof(block));
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
return MB_Crc32Bytes(block, sizeof(block));
#else
uint32_t crc = 0xFFFFFFFFu;
for (unsigned int j = 0; j < sizeof(block); j++)
{
crc ^= block[j];
for (uint8_t bit = 0; bit < 8u; bit++)
crc = (crc >> 1) ^ ((crc & 1u) ? 0xEDB88320u : 0u);
}
return crc ^ 0xFFFFFFFFu;
#endif
}
static uint16_t AIRCOPY_NextPendingBlock(void)
{
for (uint8_t i = 0; i < AircopyGroupCount; i++)
if (AircopyPendingMask & (1u << i))
return AircopyGroupStart + i;
return AircopyGroupStart + AircopyGroupCount;
}
static void AIRCOPY_clear()
{
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
@@ -169,7 +231,7 @@ static uint16_t AIRCOPY_Reg5D(uint8_t words)
}
// Arm FSK RX for the frame this role expects: the receiver waits for DATA, the
// sender waits for a tiny ACK/RESEND. Both REG_5D and the drain length must be
// sender waits for a tiny ACK/DIFF/SKIP/RESEND. Both REG_5D and the drain length must be
// set before the frame arrives so RX-finished fires at the right byte count.
static void AIRCOPY_ArmReceive(void)
{
@@ -201,11 +263,14 @@ static void AIRCOPY_FinalizeAndSend(uint8_t words)
AIRCOPY_ArmReceive();
}
static void AIRCOPY_SendControl(uint16_t type, uint16_t block)
static void AIRCOPY_SendControl(uint16_t type, uint16_t block, uint32_t mask)
{
g_FSK_Buffer[0] = type;
g_FSK_Buffer[1] = block;
memset(&g_FSK_Buffer[2], 0, (AIRCOPY_CTRL_WORDS - 4u) * 2u); // pad payload words [2 .. CTRL-3]
g_FSK_Buffer[2] = (uint16_t)mask;
g_FSK_Buffer[3] = (uint16_t)(mask >> 16);
g_FSK_Buffer[4] = 0;
g_FSK_Buffer[5] = 0;
AIRCOPY_FinalizeAndSend(AIRCOPY_CTRL_WORDS);
}
@@ -214,7 +279,7 @@ static void AIRCOPY_RequestResend(void)
gErrorsDuringAirCopy++;
gUpdateDisplay = true;
AircopyCountdown = AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_RESEND, gAirCopyBlockNumber);
AIRCOPY_SendControl(AIRCOPY_PACKET_RESEND, gAirCopyBlockNumber, 0);
}
static bool AIRCOPY_Retry(void)
@@ -264,19 +329,44 @@ bool AIRCOPY_SendMessage(void)
const uint16_t total = AIRCOPY_GetTotalBlocks();
const uint16_t start = gAirCopyBlockNumber;
uint8_t count = (uint8_t)((total - start) < AIRCOPY_BLOCKS_PER_FRAME
? (total - start)
: AIRCOPY_BLOCKS_PER_FRAME);
AircopyInFlight = count;
if (AircopyProbePending)
{
AircopyGroupStart = start;
AircopyGroupCount = AIRCOPY_GroupBlockCount(start, total);
AircopyInFlight = 0;
g_FSK_Buffer[0] = AIRCOPY_PACKET_HASH;
g_FSK_Buffer[1] = start;
memset(&g_FSK_Buffer[2], 0, (AIRCOPY_DATA_WORDS - 4u) * sizeof(g_FSK_Buffer[0]));
for (uint8_t i = 0; i < AircopyGroupCount; i++)
{
const uint32_t crc = AIRCOPY_BlockCRC32(start + i);
g_FSK_Buffer[2u + 2u * i] = (uint16_t)crc;
g_FSK_Buffer[3u + 2u * i] = (uint16_t)(crc >> 16);
}
}
else
{
const uint8_t offset = (uint8_t)(start - AircopyGroupStart);
uint8_t count = 0;
while (count < AIRCOPY_BLOCKS_PER_FRAME && offset + count < AircopyGroupCount &&
(AircopyPendingMask & (1u << (offset + count))))
count++;
if (count == 0u)
{
AIRCOPY_Finish(AIRCOPY_FAILED);
return 0;
}
g_FSK_Buffer[0] = AIRCOPY_PACKET_DATA;
g_FSK_Buffer[1] = AIRCOPY_MAKE_HDR(start, count);
// Full N-block payload every time (unused blocks zero-padded) so DATA frames
// are a constant length the receiver can arm for.
memset(&g_FSK_Buffer[2], 0, AIRCOPY_BLOCKS_PER_FRAME * AIRCOPY_BLOCK_SIZE);
for (uint8_t i = 0; i < count; i++)
EEPROM_ReadBuffer(AIRCOPY_GetBlockOffset(start + i),
&g_FSK_Buffer[2 + i * AIRCOPY_BLOCK_WORDS], AIRCOPY_BLOCK_SIZE);
AircopyInFlight = count;
g_FSK_Buffer[0] = AIRCOPY_PACKET_DATA;
g_FSK_Buffer[1] = AIRCOPY_MAKE_HDR(start, count);
memset(&g_FSK_Buffer[AIRCOPY_DATA_HEADER_WORDS], 0,
AIRCOPY_BLOCKS_PER_FRAME * AIRCOPY_BLOCK_SIZE);
for (uint8_t i = 0; i < count; i++)
EEPROM_ReadBuffer(AIRCOPY_GetBlockOffset(start + i),
&g_FSK_Buffer[AIRCOPY_DATA_HEADER_WORDS + i * AIRCOPY_BLOCK_WORDS],
AIRCOPY_BLOCK_SIZE);
}
AIRCOPY_FinalizeAndSend(AIRCOPY_DATA_WORDS);
AircopyCountdown = AIRCOPY_ACK_TIMEOUT_10MS;
@@ -287,7 +377,7 @@ bool AIRCOPY_SendMessage(void)
void AIRCOPY_StorePacket(void)
{
// The role decides the frame length: the receiver waits for a full DATA
// frame, the sender for a tiny ACK/RESEND. CRC and END sit at the tail.
// frame, the sender for a tiny ACK/DIFF/SKIP/RESEND. CRC and END sit at the tail.
const uint8_t words = gFskRxExpectedWords;
if (gFSKWriteIndex < words) {
return;
@@ -296,11 +386,15 @@ void AIRCOPY_StorePacket(void)
gFSKWriteIndex = 0;
const uint16_t status = BK4819_ReadRegister(BK4819_REG_0B);
const uint16_t type = g_FSK_Buffer[0];
bool valid = (status & 0x0010u) == 0 &&
const bool statusOk = (status & 0x0010u) == 0;
const bool endOk = g_FSK_Buffer[words - 1u] == AIRCOPY_PACKET_END;
bool valid = statusOk && endOk &&
(type == AIRCOPY_PACKET_DATA ||
type == AIRCOPY_PACKET_ACK ||
type == AIRCOPY_PACKET_RESEND) &&
g_FSK_Buffer[words - 1u] == AIRCOPY_PACKET_END;
type == AIRCOPY_PACKET_RESEND ||
type == AIRCOPY_PACKET_SKIP ||
type == AIRCOPY_PACKET_HASH ||
type == AIRCOPY_PACKET_DIFF);
if (valid)
{
@@ -314,24 +408,59 @@ void AIRCOPY_StorePacket(void)
if (gAirCopyIsSendMode)
{
const uint16_t ackBlock = AIRCOPY_HDR_BLOCK(g_FSK_Buffer[1]);
const uint32_t responseMask = (uint32_t)g_FSK_Buffer[2]
| ((uint32_t)g_FSK_Buffer[3] << 16);
if (valid && type == AIRCOPY_PACKET_ACK && ackBlock == gAirCopyBlockNumber)
if (valid && ackBlock == gAirCopyBlockNumber && AircopyProbePending &&
AircopyGroupCount != 0u &&
(type == AIRCOPY_PACKET_SKIP || type == AIRCOPY_PACKET_DIFF))
{
// The whole in-flight run was stored: advance past it.
const uint32_t allowedMask = (1u << AircopyGroupCount) - 1u;
if ((responseMask & ~allowedMask) != 0u ||
(type == AIRCOPY_PACKET_SKIP && responseMask != 0u) ||
(type == AIRCOPY_PACKET_DIFF && responseMask == 0u))
{
AIRCOPY_ArmReceive();
return;
}
AircopyCountdown = 0;
AircopyRetries = 0;
gAirCopyBlockNumber += AircopyInFlight;
AIRCOPY_MarkSkipped(ackBlock, AircopyGroupCount, responseMask);
if (type == AIRCOPY_PACKET_SKIP)
gAirCopyBlockNumber += AircopyGroupCount;
else
{
AircopyPendingMask = responseMask;
AircopyProbePending = false;
gAirCopyBlockNumber = AIRCOPY_NextPendingBlock();
}
gUpdateDisplay = true;
if (gAirCopyBlockNumber >= total)
AIRCOPY_Finish(AIRCOPY_COMPLETE);
return;
}
if (valid && type == AIRCOPY_PACKET_RESEND)
if (valid && ackBlock == gAirCopyBlockNumber && !AircopyProbePending &&
AircopyInFlight != 0u &&
type == AIRCOPY_PACKET_ACK)
{
const uint8_t offset = (uint8_t)(ackBlock - AircopyGroupStart);
const uint32_t sentMask = ((1u << AircopyInFlight) - 1u) << offset;
AircopyPendingMask &= ~sentMask;
gAirCopyBlockNumber = AIRCOPY_NextPendingBlock();
if (AircopyPendingMask == 0u)
AircopyProbePending = true;
AircopyCountdown = 0;
AircopyRetries = 0;
gUpdateDisplay = true;
if (gAirCopyBlockNumber >= total)
AIRCOPY_Finish(AIRCOPY_COMPLETE);
return;
}
if (valid && type == AIRCOPY_PACKET_RESEND && ackBlock == gAirCopyBlockNumber)
{
// The receiver asks to (re)start from ackBlock; rewind and resend.
if (ackBlock <= gAirCopyBlockNumber)
gAirCopyBlockNumber = ackBlock;
(void)AIRCOPY_Retry();
return;
}
@@ -342,13 +471,64 @@ void AIRCOPY_StorePacket(void)
if (!valid)
{
if (type == AIRCOPY_PACKET_DATA)
if (type == AIRCOPY_PACKET_DATA || type == AIRCOPY_PACKET_HASH)
AIRCOPY_RequestResend();
else
AIRCOPY_ArmReceive();
return;
}
if (type == AIRCOPY_PACKET_HASH)
{
const uint16_t start = AIRCOPY_HDR_BLOCK(g_FSK_Buffer[1]);
if (start >= total || start % AIRCOPY_HASH_GROUP_BLOCKS != 0u)
{
AIRCOPY_RequestResend();
return;
}
if (start == AircopyLastAckStart &&
(AircopyLastAckType == AIRCOPY_PACKET_SKIP ||
AircopyLastAckType == AIRCOPY_PACKET_DIFF))
{
AircopyCountdown = gAirCopyBlockNumber >= total
? AIRCOPY_RX_LINGER_10MS : AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(AircopyLastAckType, start, AircopyLastAckMask);
return;
}
if (start != gAirCopyBlockNumber || AircopyPendingMask != 0u)
{
AIRCOPY_RequestResend();
return;
}
AircopyGroupStart = start;
AircopyGroupCount = AIRCOPY_GroupBlockCount(start, total);
uint32_t mask = 0;
for (uint8_t i = 0; i < AircopyGroupCount; i++)
{
const uint32_t sentCRC = (uint32_t)g_FSK_Buffer[2u + 2u * i]
| ((uint32_t)g_FSK_Buffer[3u + 2u * i] << 16);
if (AIRCOPY_BlockCRC32(start + i) != sentCRC)
mask |= 1u << i;
}
AircopyPendingMask = mask;
AIRCOPY_MarkSkipped(start, AircopyGroupCount, mask);
const uint16_t reply = mask == 0u ? AIRCOPY_PACKET_SKIP : AIRCOPY_PACKET_DIFF;
gAirCopyBlockNumber = AIRCOPY_NextPendingBlock();
AircopyLastAckStart = start;
AircopyLastAckType = reply;
AircopyLastAckMask = mask;
gErrorsDuringAirCopy = 0;
gUpdateDisplay = true;
AircopyCountdown = gAirCopyBlockNumber >= total
? AIRCOPY_RX_LINGER_10MS : AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(reply, start, mask);
return;
}
if (type != AIRCOPY_PACKET_DATA)
{
AIRCOPY_ArmReceive();
@@ -356,7 +536,7 @@ void AIRCOPY_StorePacket(void)
}
const uint16_t start = AIRCOPY_HDR_BLOCK(g_FSK_Buffer[1]);
uint8_t count = (uint8_t)AIRCOPY_HDR_COUNT(g_FSK_Buffer[1]);
const uint8_t count = (uint8_t)AIRCOPY_HDR_COUNT(g_FSK_Buffer[1]);
if (count == 0u || count > AIRCOPY_BLOCKS_PER_FRAME || start >= total)
{
@@ -366,11 +546,16 @@ void AIRCOPY_StorePacket(void)
if (start < gAirCopyBlockNumber)
{
// Already stored (our ACK was lost): re-ACK so the sender advances.
// Repeat the exact decision when an ACK or SKIP was lost.
if (start != AircopyLastAckStart || AircopyLastAckType != AIRCOPY_PACKET_ACK)
{
AIRCOPY_RequestResend();
return;
}
AircopyCountdown = gAirCopyBlockNumber >= total
? AIRCOPY_RX_LINGER_10MS
: AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, start);
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, start, 0);
return;
}
@@ -381,23 +566,40 @@ void AIRCOPY_StorePacket(void)
return;
}
if ((uint16_t)(start + count) > total)
count = (uint8_t)(total - start); // clamp the final short frame
if (AircopyPendingMask == 0u || start < AircopyGroupStart ||
(uint16_t)(start + count) > AircopyGroupStart + AircopyGroupCount)
{
AIRCOPY_RequestResend();
return;
}
const uint8_t offset = (uint8_t)(start - AircopyGroupStart);
const uint32_t frameMask = ((1u << count) - 1u) << offset;
if ((AircopyPendingMask & frameMask) != frameMask)
{
AIRCOPY_RequestResend();
return;
}
for (uint8_t i = 0; i < count; i++)
EEPROM_WriteBuffer(AIRCOPY_GetBlockOffset(start + i),
&g_FSK_Buffer[2 + i * AIRCOPY_BLOCK_WORDS], AIRCOPY_BLOCK_SIZE);
&g_FSK_Buffer[AIRCOPY_DATA_HEADER_WORDS + i * AIRCOPY_BLOCK_WORDS],
AIRCOPY_BLOCK_SIZE);
AircopyPendingMask &= ~frameMask;
// All pending RX errors concerned this run.
gErrorsDuringAirCopy = 0;
gAirCopyBlockNumber += count;
gAirCopyBlockNumber = AIRCOPY_NextPendingBlock();
AircopyLastAckStart = start;
AircopyLastAckType = AIRCOPY_PACKET_ACK;
AircopyLastAckMask = 0;
gUpdateDisplay = true;
AircopyCountdown = gAirCopyBlockNumber < total
? AIRCOPY_RX_TIMEOUT_10MS
: AIRCOPY_RX_LINGER_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, start);
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, start, 0);
}
static void AIRCOPY_InitTransfer(bool isSendMode)
@@ -409,12 +611,20 @@ static void AIRCOPY_InitTransfer(bool isSendMode)
gFSKWriteIndex = 0;
gAirCopyBlockNumber = 0;
gErrorsDuringAirCopy = 0;
memset(AircopySkippedBlocks, 0, sizeof(AircopySkippedBlocks));
gInputBoxIndex = 0;
gAirCopyIsSendMode = isSendMode;
AircopyCountdown = isSendMode ? 0 : AIRCOPY_RX_TIMEOUT_10MS;
AircopyRetries = 0;
AircopyInFlight = 0;
AircopyGroupCount = 0;
AircopyGroupStart = 0;
AircopyPendingMask = 0;
AircopyProbePending = true;
AircopyLastAckStart = 0xFFFFu;
AircopyLastAckType = AIRCOPY_PACKET_ACK;
AircopyLastAckMask = 0;
// The sender listens for tiny ACKs, the receiver for full DATA frames.
gFskRxExpectedWords = isSendMode ? AIRCOPY_CTRL_WORDS : AIRCOPY_DATA_WORDS;
+18 -14
View File
@@ -28,28 +28,31 @@
#define AIRCOPY_BLOCK_SIZE 0x0040u // 64 bytes per AirCopy block
#define AIRCOPY_BLOCK_WORDS (AIRCOPY_BLOCK_SIZE / 2u) // 32 FSK words per block
// Multi-block framing. Each DATA frame carries N blocks so the fixed per-frame
// overhead (TX/RX guard delays + the ACK round-trip) is amortised over more
// payload. A frame is [0]=type, [1]=header (start block | count<<12), then
// N*32 payload words (unused blocks zero-padded), then [CRC][END]. Control
// frames (ACK/RESEND) stay tiny to keep the reverse channel almost free.
// Every forward frame stays 100 words long. HASH carries 24 per-block CRC32
// values; the receiver replies SKIP or a 24-bit difference mask (DIFF).
// DATA retains the proven [type][header][up to 3 blocks][CRC16][END] layout,
// with unused blocks zero-padded. Only blocks selected by DIFF are sent.
// Reverse control frames (ACK/DIFF/SKIP/RESEND) are eight words long.
//
// This is a NEW wire format, NOT compatible with the previous single-block
// protocol: [1] now carries a block index (+ count), not the raw EEPROM offset,
// and ACKs are AIRCOPY_CTRL_WORDS long instead of a full 36-word frame. Even
// AIRCOPY_BLOCKS_PER_FRAME==1 differs from the old format on both counts, so both
// radios must run this same firmware to exchange - it is a debug/bisect knob, not
// a legacy-compatibility mode. Keep the DATA frame within the 128-word TX FIFO
// (N<=3) to avoid TX-FIFO refills.
// This wire format is not compatible with earlier AirCopy versions. Both radios
// must run the same firmware.
#define AIRCOPY_BLOCKS_PER_FRAME 3u
#define AIRCOPY_DATA_WORDS (2u + AIRCOPY_BLOCKS_PER_FRAME * AIRCOPY_BLOCK_WORDS + 2u)
#define AIRCOPY_CTRL_WORDS 8u // tiny ACK/RESEND frame (multiple of the 4-word RX FIFO threshold)
#define AIRCOPY_DATA_HEADER_WORDS 2u
#define AIRCOPY_DATA_WORDS (AIRCOPY_DATA_HEADER_WORDS + AIRCOPY_BLOCKS_PER_FRAME * AIRCOPY_BLOCK_WORDS + 2u)
#define AIRCOPY_CTRL_WORDS 8u // multiple of the 4-word RX FIFO threshold
#define AIRCOPY_FRAME_WORDS_MAX AIRCOPY_DATA_WORDS
#if AIRCOPY_DATA_WORDS > 128u
#error AirCopy DATA frame exceeds the radio TX FIFO
#endif
#define AIRCOPY_CHANNELS_PER_BANK 128
#define AIRCOPY_NUM_BANKS MR_CHANNELS_MAX / AIRCOPY_CHANNELS_PER_BANK
#define AIRCOPY_NUM_MAPS (AIRCOPY_NUM_BANKS + 1u) // banks + one settings map
#define AIRCOPY_ALL_INDEX AIRCOPY_NUM_MAPS // selection sentinel: send/receive everything
#define AIRCOPY_BANK_BLOCKS 68u
#define AIRCOPY_SETTINGS_BLOCKS 12u
#define AIRCOPY_ALL_BLOCKS (AIRCOPY_NUM_BANKS * AIRCOPY_BANK_BLOCKS + AIRCOPY_SETTINGS_BLOCKS)
#define AIRCOPY_BAR_WIDTH 120 // Visible width of the progress gauge
// ============================================================================
@@ -84,6 +87,7 @@ bool AIRCOPY_SendMessage(void);
void AIRCOPY_StorePacket(void);
void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
uint16_t AIRCOPY_GetTotalBlocks(void);
bool AIRCOPY_BlockWasSkipped(uint16_t block);
uint8_t AIRCOPY_CurrentSliceMap(void); // map index of the block in progress (All slice label)
// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1.
+37 -7
View File
@@ -29,14 +29,20 @@
void UI_DisplayAircopy(void)
{
char String[16];
char String[20];
char *pPrintStr;
UI_DisplayClear();
const uint16_t totalBlocks = AIRCOPY_GetTotalBlocks();
// RX keeps listening briefly for a repeated final frame after all blocks arrive.
const bool receiveComplete = gAircopyState == AIRCOPY_TRANSFER &&
!gAirCopyIsSendMode &&
gAirCopyBlockNumber >= totalBlocks;
if (gAircopyState == AIRCOPY_READY) {
pPrintStr = "AIR COPY(RDY)";
} else if (gAircopyState == AIRCOPY_TRANSFER) {
} else if (gAircopyState == AIRCOPY_TRANSFER && !receiveComplete) {
if (gAircopyAll) {
// All mode: show the slice being replicated in place of the title.
const uint8_t m = AIRCOPY_CurrentSliceMap();
@@ -48,7 +54,7 @@ void UI_DisplayAircopy(void)
} else {
pPrintStr = "AIR COPY";
}
} else if (gAircopyState == AIRCOPY_COMPLETE) {
} else if (gAircopyState == AIRCOPY_COMPLETE || receiveComplete) {
pPrintStr = "AIR COPY OK";
} else {
pPrintStr = "AIR COPY FAIL";
@@ -70,7 +76,6 @@ void UI_DisplayAircopy(void)
// show the main large frequency digits
UI_DisplayFrequency(String, 16, 2, false);
const uint16_t totalBlocks = AIRCOPY_GetTotalBlocks();
uint16_t doneBlocks = gAirCopyBlockNumber;
if (doneBlocks > totalBlocks)
@@ -95,9 +100,10 @@ void UI_DisplayAircopy(void)
? 99u
: gErrorsDuringAirCopy;
if (gAircopyState == AIRCOPY_COMPLETE || gAircopyState == AIRCOPY_FAILED) {
if (gAircopyState == AIRCOPY_COMPLETE || receiveComplete ||
gAircopyState == AIRCOPY_FAILED) {
sprintf(String, "%s %u/%u %s:%u",
gAircopyState == AIRCOPY_COMPLETE ? "OK" : "KO",
gAircopyState == AIRCOPY_COMPLETE || receiveComplete ? "OK" : "KO",
doneBlocks, totalBlocks,
gAirCopyIsSendMode ? "RT" : "ER",
displayedErrors);
@@ -117,8 +123,32 @@ void UI_DisplayAircopy(void)
// Match the former DDA gauge exactly, including its partial first pixel.
const uint8_t filled = (doneBlocks * AIRCOPY_BAR_WIDTH + totalBlocks - 1u)
/ totalBlocks;
// Each interior row has one hatch pixel followed by two clear pixels.
static const uint8_t hatch[3] = { 0xA5, 0x89, 0x91 };
for (uint8_t col = 0; col < AIRCOPY_BAR_WIDTH; col++)
gFrameBuffer[4][col + 4] = col < filled ? 0xBD : 0x81;
gFrameBuffer[4][col + 4] = col < filled ? hatch[col % 3u] : 0x81;
// A changed block makes its entire pixel span solid, including pixels
// shared with skipped blocks.
for (uint16_t block = 0; block < doneBlocks; block++)
{
if (AIRCOPY_BlockWasSkipped(block))
continue;
const uint8_t first = (uint32_t)block * AIRCOPY_BAR_WIDTH / totalBlocks;
const uint8_t last = ((uint32_t)(block + 1u) * AIRCOPY_BAR_WIDTH
+ totalBlocks - 1u) / totalBlocks;
for (uint8_t col = first; col < last && col < filled; col++)
gFrameBuffer[4][col + 4] = 0xBD;
}
// Leave one clear interior column on each side of a copied run.
for (uint8_t col = 0; col < filled; col++)
{
if (gFrameBuffer[4][col + 4] != 0xBD)
continue;
if (col > 0 && gFrameBuffer[4][col + 3] != 0xBD)
gFrameBuffer[4][col + 3] = 0x81;
if (col + 1u < filled && gFrameBuffer[4][col + 5] != 0xBD)
gFrameBuffer[4][col + 5] = 0x81;
}
gFrameBuffer[4][124] = 0x81;
gFrameBuffer[4][125] = 0x42;
gFrameBuffer[4][126] = 0x3c;