diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 4d7bf58b..6668eff9 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -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; diff --git a/App/app/aircopy.h b/App/app/aircopy.h index ea99f6f4..6f10c7cc 100644 --- a/App/app/aircopy.h +++ b/App/app/aircopy.h @@ -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. diff --git a/App/ui/aircopy.c b/App/ui/aircopy.c index daba1a45..423ec5d5 100644 --- a/App/ui/aircopy.c +++ b/App/ui/aircopy.c @@ -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;