diff --git a/scripts/test_file_link.sh b/scripts/test_file_link.sh index 2e59e0f..819d77d 100755 --- a/scripts/test_file_link.sh +++ b/scripts/test_file_link.sh @@ -14,7 +14,8 @@ SIZE_KB="${3:-1024}" FREQ=915000000 RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md BW=1500000 -TXGAIN=-40 # для кабеля; антенны: −20…0 +TXGAIN="${TXGAIN:--30}" # OTA: переопредели, напр. TXGAIN=-20 scripts/test_file_link.sh +FEC="${FEC:-rs8}" # rs8 | none — фаза 3.2 гоняет сначала none, потом rs8 TIMEOUT=$(( SIZE_KB / 100 + 30 )) # ~160 КБ/с + запас if command -v sshpass >/dev/null; then @@ -36,14 +37,14 @@ echo "=== [2/5] Заливка файла на Pluto A ($A) ===" echo "=== [3/5] Старт приёмника на Pluto B ($B), таймаут ${TIMEOUT}с ===" "${SSH[@]}" "root@$B" \ "rm -f /tmp/out.bin; timeout $TIMEOUT /tmp/receiver -f $FREQ -r $RATE \ - -b $BW -c rs8 -u local: > /tmp/out.bin 2>/tmp/rx.log" & + -b $BW -c $FEC -u local: > /tmp/out.bin 2>/tmp/rx.log" & RX_PID=$! sleep 3 # дать RX прогреться до старта TX echo "=== [4/5] Передача с Pluto A ===" "${SSH[@]}" "root@$A" \ "cat /tmp/link_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \ - -g $TXGAIN -p 0 -c rs8 -u local: 2>/tmp/tx.log" + -g $TXGAIN -p 0 -c $FEC -u local: 2>/tmp/tx.log" sleep 3 # дохлебать хвост эфира "${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true" wait "$RX_PID" 2>/dev/null || true diff --git a/src/receiver.c b/src/receiver.c index 9cb47e7..5016974 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -13,9 +13,11 @@ extern volatile sig_atomic_t stop; static uint64_t calls = 0, hdr_ok = 0, crc_raw = 0; static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0; +static uint64_t rs_fail = 0, lost = 0; +static uint32_t last_seq = 0; +static int seq_init = 0; static float rssi = -100, evm = 0; static float cfo_est = 0.0f; -static nco_crcf nco = NULL; static fec decoder = NULL; static int fec_on = 0; static liquid_float_complex frame[MAX_FRAME_SAMPLES]; @@ -29,16 +31,28 @@ static int callback(unsigned char *hdr, int hdr_valid, rssi = stats.rssi; evm = stats.evm; - // Обновляем CFO только если NCO инициализирован - if (nco && fabsf(stats.cfo) < CFO_MAX_VALID) { + // CFO — только телеметрия: ofdmflexframesync сам компенсирует смещение + // по преамбуле. Внешний NCO-контур убран (техдолг #4): с set_phase(0) + // на границах буферов он рвал фазу посреди кадра. + if (fabsf(stats.cfo) < CFO_MAX_VALID) cfo_est = CFO_FILTER_ALPHA * cfo_est + (1.0f - CFO_FILTER_ALPHA) * stats.cfo; - nco_crcf_set_frequency(nco, cfo_est); - } if (!hdr_valid) return 0; hdr_ok++; if (pay_valid) crc_raw++; + // Детектор пропусков seq (техдолг #6, минимум): потерю кадра в эфире + // надо отличать от порчи payload. ARQ нет — только лог дыры. + uint32_t seq = ((uint32_t)hdr[2] << 24) | ((uint32_t)hdr[3] << 16) | + ((uint32_t)hdr[4] << 8) | hdr[5]; + if (seq_init && seq > last_seq + 1) { + lost += seq - last_seq - 1; + fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n", + last_seq, seq, seq - last_seq - 1); + } + last_seq = seq; + seq_init = 1; + uint16_t original_len = (hdr[6] << 8) | hdr[7]; uint8_t last_block_bytes = hdr[10]; @@ -49,26 +63,38 @@ static int callback(unsigned char *hdr, int hdr_valid, if (nblocks == 0 || nblocks > 4096 / RS_DATA) return 0; uint8_t recovered[4096]; - int rs_ok = 1; for (size_t b = 0; b < nblocks; b++) { - if (fec_decode(decoder, RS_DATA, pay + b * RS_ENC, - recovered + b * RS_DATA) != 0) { - rs_ok = 0; break; - } + // fec_decode() liquid всегда "успешен" — код возврата бесполезен. + fec_decode(decoder, RS_DATA, pay + b * RS_ENC, + recovered + b * RS_DATA); } - if (rs_ok) { - if (!pay_valid) rs_saved++; - size_t out_len = (nblocks - 1) * RS_DATA + - (last_block_bytes > 0 ? last_block_bytes : RS_DATA); - if (out_len > nblocks * RS_DATA) out_len = nblocks * RS_DATA; - if (out_len > original_len) out_len = original_len; - if (out_len > 0) { - fwrite(recovered, 1, out_len, stdout); - fflush(stdout); - total_bytes += out_len; - crc_final++; - } + + // Декодировано framed_len = original_len + 4 (CRC32) байт. Настоящий + // критерий целостности — CRC поверх декода (техдолг #3): fec_decode + // отдаёт мусор молча, поэтому проверяем сами и мусор в файл не пишем. + size_t framed_len = (nblocks - 1) * RS_DATA + + (last_block_bytes > 0 ? last_block_bytes : RS_DATA); + if (framed_len > nblocks * RS_DATA) framed_len = nblocks * RS_DATA; + + if (original_len == 0 || (size_t)original_len + 4 > framed_len) { + rs_fail++; + return 0; } + unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, recovered, original_len); + unsigned int tx_crc = ((uint32_t)recovered[original_len] << 24) | + ((uint32_t)recovered[original_len + 1] << 16) | + ((uint32_t)recovered[original_len + 2] << 8) | + recovered[original_len + 3]; + if (rx_crc != tx_crc) { + rs_fail++; // RS не справился, кадр битый — не пишем + return 0; + } + if (!pay_valid) rs_saved++; // liquid CRC по payload не прошёл, а наш + // CRC поверх RS — да: значит RS реально спас + fwrite(recovered, 1, original_len, stdout); + fflush(stdout); + total_bytes += original_len; + crc_final++; } else if (pay_valid && original_len > 0 && original_len <= pay_len) { fwrite(pay, 1, original_len, stdout); fflush(stdout); @@ -141,20 +167,11 @@ int main(int argc, char *argv[]) { } } - // Инициализация NCO для частотной коррекции - nco = nco_crcf_create(LIQUID_NCO); - if (!nco) { - fprintf(stderr, "Ошибка создания NCO\n"); - } else { - nco_crcf_set_frequency(nco, 0.0f); - } - ofdmflexframesync fs = ofdmflexframesync_create( OFDM_M, OFDM_CP, OFDM_TAPER, NULL, callback, NULL); if (!fs) { fprintf(stderr, "Ошибка создания OFDM-синхронизатора\n"); if (decoder) fec_destroy(decoder); - if (nco) nco_crcf_destroy(nco); iio_buffer_destroy(buf); iio_context_destroy(ctx); return 1; @@ -163,8 +180,8 @@ int main(int argc, char *argv[]) { ofdmflexframesync_set_header_len(fs, HDR_SIZE); fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n"); - fprintf(stderr, "Кадры | Загол. | CRC | RS испр | Вывод | RSSI | EVM | CFO\n"); - fprintf(stderr, "--------+--------+------+---------+--------+-------+-------+----------\n"); + fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Вывод | RSSI | EVM | CFO\n"); + fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+-------+-------+----------\n"); time_t last = time(NULL); @@ -179,19 +196,15 @@ int main(int argc, char *argv[]) { for (int i = 0; i < ns; i++) { frame[i] = ((float)bb[2*i] / 2048.0f) + ((float)bb[2*i+1] / 2048.0f) * _Complex_I; - if (nco) { - nco_crcf_mix_down(nco, frame[i], &frame[i]); - nco_crcf_step(nco); - } } - if (nco) nco_crcf_set_phase(nco, 0.0f); ofdmflexframesync_execute(fs, frame, ns); if (time(NULL) > last) { - fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f", + fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %5llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f", (unsigned long long)calls, (unsigned long long)hdr_ok, (unsigned long long)crc_raw, (unsigned long long)rs_saved, + (unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)crc_final, rssi, evm, cfo_est); fflush(stderr); last = time(NULL); @@ -199,13 +212,13 @@ int main(int argc, char *argv[]) { } fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n"); - fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | Вывод: %llu | Байт: %llu\n", + fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | RS битых: %llu | Потери seq: %llu | Вывод: %llu | Байт: %llu\n", (unsigned long long)calls, (unsigned long long)hdr_ok, (unsigned long long)crc_raw, (unsigned long long)rs_saved, + (unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)crc_final, (unsigned long long)total_bytes); if (decoder) fec_destroy(decoder); - if (nco) nco_crcf_destroy(nco); ofdmflexframesync_destroy(fs); iio_buffer_destroy(buf); iio_context_destroy(ctx); diff --git a/src/transmitter.c b/src/transmitter.c index 7dcb58a..01824c3 100644 --- a/src/transmitter.c +++ b/src/transmitter.c @@ -120,24 +120,36 @@ int main(int argc, char *argv[]) { // Кодирование данных if (enc) { - size_t total_blocks = (n + RS_DATA - 1) / RS_DATA; + // CRC32 поверх исходных данных ДО RS: fec_decode() liquid не + // сообщает о неисправимых блоках, поэтому единственный надёжный + // критерий целостности на RX — этот CRC поверх декода (техдолг #3). + uint8_t framed[MAX_PAYLOAD + 4]; + memcpy(framed, raw, n); + unsigned int crc = crc_generate_key(LIQUID_CRC_32, raw, n); + framed[n] = (crc >> 24) & 0xFF; + framed[n + 1] = (crc >> 16) & 0xFF; + framed[n + 2] = (crc >> 8) & 0xFF; + framed[n + 3] = crc & 0xFF; + size_t framed_n = n + 4; + + size_t total_blocks = (framed_n + RS_DATA - 1) / RS_DATA; tx_len = total_blocks * RS_ENC; tx_payload = malloc(tx_len); for (size_t b = 0; b < total_blocks; b++) { uint8_t blk[RS_DATA] = {0}; - size_t bytes = (b == total_blocks - 1) ? n - b * RS_DATA : RS_DATA; - memcpy(blk, raw + b * RS_DATA, bytes); + size_t bytes = (b == total_blocks - 1) ? framed_n - b * RS_DATA : RS_DATA; + memcpy(blk, framed + b * RS_DATA, bytes); fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC); } - // Формирование заголовка + // Заголовок: len = исходные n (без 4 байт CRC), nblocks/last по framed hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1; hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF; hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF; hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF; hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF; - hdr[10] = n % RS_DATA; hdr[11] = 0; + hdr[10] = framed_n % RS_DATA; hdr[11] = 0; } else { tx_len = n; tx_payload = malloc(tx_len); @@ -167,7 +179,10 @@ int main(int argc, char *argv[]) { idx++; } } - if (idx > 0) iio_buffer_push(buf); + // Пушим только фактически заполненные idx сэмплов, а не весь буфер + // 16384 (техдолг #5): иначе ~35% мёртвого эфира на кадр и сбитый + // межкадровый тайминг. push_partial даёт ту же длину, что write. + if (idx > 0) iio_buffer_push_partial(buf, idx); // Обновление статистики frames++; bytes_tx += n; seq++; @@ -183,6 +198,15 @@ int main(int argc, char *argv[]) { fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n", (unsigned long long)frames, (unsigned long long)bytes_tx); + // Дренаж: выпихнуть последний кадр из kernel-буферов DMA перед закрытием. + // Без паузы iio_buffer_destroy() срезает хвост файла в эфире (техдолг #5). + { + int16_t *out = (int16_t *)iio_buffer_start(buf); + memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t)); + iio_buffer_push(buf); + usleep(100000); // ~4 kernel-буфера при 1.92 MSPS ≈ 35 мс, с запасом + } + // Освобождение ресурсов if (enc) fec_destroy(enc); ofdmflexframegen_destroy(fg);