initial commit
This commit is contained in:
0
Projects/SDR_Rover/tests/.gitkeep
Normal file
0
Projects/SDR_Rover/tests/.gitkeep
Normal file
0
Projects/SDR_Rover/tests/__init__.py
Normal file
0
Projects/SDR_Rover/tests/__init__.py
Normal file
54
Projects/SDR_Rover/tests/lab001_text_bytes.py
Normal file
54
Projects/SDR_Rover/tests/lab001_text_bytes.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Lab001. Преобразование текста в байты и биты.
|
||||
|
||||
Цель:
|
||||
1. Взять обычную текстовую строку.
|
||||
2. Преобразовать её в байты UTF-8.
|
||||
3. Показать каждый байт как десятичное число.
|
||||
4. Показать каждый байт как восемь бит.
|
||||
5. Восстановить исходный текст.
|
||||
"""
|
||||
|
||||
# Исходное сообщение
|
||||
message = "HELLO SDR"
|
||||
|
||||
# Кодирование текста в байты.
|
||||
# UTF-8 — способ представить символы числами.
|
||||
encoded_message = message.encode("utf-8")
|
||||
|
||||
# Преобразование каждого байта в строку из восьми бит.
|
||||
binary_message = " ".join(
|
||||
f"{byte:08b}" for byte in encoded_message
|
||||
)
|
||||
|
||||
# Обратное преобразование байтов в текст.
|
||||
decoded_message = encoded_message.decode("utf-8")
|
||||
|
||||
|
||||
print("Исходный текст:")
|
||||
print(message)
|
||||
|
||||
print("\nОбъект bytes:")
|
||||
print(encoded_message)
|
||||
|
||||
print("\nБайты в десятичном виде:")
|
||||
print(list(encoded_message))
|
||||
|
||||
print("\nБайты в двоичном виде:")
|
||||
print(binary_message)
|
||||
|
||||
print("\nПодробно по каждому символу:")
|
||||
for character, byte_value in zip(message, encoded_message):
|
||||
print(
|
||||
f"{character!r:>4} "
|
||||
f"→ число {byte_value:3d} "
|
||||
f"→ биты {byte_value:08b}"
|
||||
)
|
||||
|
||||
print("\nВосстановленный текст:")
|
||||
print(decoded_message)
|
||||
|
||||
# Автоматическая проверка результата.
|
||||
assert decoded_message == message
|
||||
|
||||
print("\nПроверка пройдена: исходный текст восстановлен без ошибок.")
|
||||
61
Projects/SDR_Rover/tests/lab001_utf8_russian.py
Normal file
61
Projects/SDR_Rover/tests/lab001_utf8_russian.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Lab001, часть 2.
|
||||
Русский текст, символы, байты и кодировка UTF-8.
|
||||
|
||||
Цель:
|
||||
1. Увидеть разницу между количеством символов и количеством байтов.
|
||||
2. Посмотреть, сколько байтов занимает каждая русская буква.
|
||||
3. Восстановить исходный текст из байтов.
|
||||
"""
|
||||
|
||||
# Исходное сообщение
|
||||
message = "ПРИВЕТ SDR"
|
||||
|
||||
# Преобразование текста в байты UTF-8
|
||||
encoded_message = message.encode("utf-8")
|
||||
|
||||
# Обратное преобразование байтов в текст
|
||||
decoded_message = encoded_message.decode("utf-8")
|
||||
|
||||
|
||||
print("Исходный текст:")
|
||||
print(message)
|
||||
|
||||
print("\nКоличество символов:")
|
||||
print(len(message))
|
||||
|
||||
print("\nКоличество байтов UTF-8:")
|
||||
print(len(encoded_message))
|
||||
|
||||
print("\nОбъект bytes:")
|
||||
print(encoded_message)
|
||||
|
||||
print("\nВсе байты в десятичном виде:")
|
||||
print(list(encoded_message))
|
||||
|
||||
print("\nПодробно по каждому символу:")
|
||||
|
||||
for character in message:
|
||||
character_bytes = character.encode("utf-8")
|
||||
|
||||
decimal_bytes = list(character_bytes)
|
||||
|
||||
binary_bytes = " ".join(
|
||||
f"{byte:08b}" for byte in character_bytes
|
||||
)
|
||||
|
||||
print(
|
||||
f"{character!r:>4} "
|
||||
f"→ байтов: {len(character_bytes)} "
|
||||
f"→ числа: {decimal_bytes} "
|
||||
f"→ биты: {binary_bytes}"
|
||||
)
|
||||
|
||||
print("\nВосстановленный текст:")
|
||||
print(decoded_message)
|
||||
|
||||
# Автоматические проверки
|
||||
assert decoded_message == message
|
||||
assert len(encoded_message) >= len(message)
|
||||
|
||||
print("\nПроверка пройдена: русский текст восстановлен без ошибок.")
|
||||
153
Projects/SDR_Rover/tests/lab002_bit_corruption.py
Normal file
153
Projects/SDR_Rover/tests/lab002_bit_corruption.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Lab002, часть 2.
|
||||
Повреждение одного бита пакета без контроля целостности.
|
||||
|
||||
Цель:
|
||||
1. Сформировать цифровой пакет.
|
||||
2. Намеренно изменить один бит полезной нагрузки.
|
||||
3. Разобрать повреждённый пакет.
|
||||
4. Убедиться, что без CRC повреждение не обнаруживается.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Константы протокола
|
||||
# ============================================================
|
||||
|
||||
SYNC_WORD = 0xAA55
|
||||
PROTOCOL_VERSION = 1
|
||||
MESSAGE_TYPE_TEXT = 1
|
||||
|
||||
HEADER_FORMAT = ">HBBHH"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование исходного пакета
|
||||
# ============================================================
|
||||
|
||||
message = "ПРИВЕТ SDR"
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
sequence_number = 1
|
||||
message_type = MESSAGE_TYPE_TEXT
|
||||
payload_length = len(payload)
|
||||
|
||||
header = struct.pack(
|
||||
HEADER_FORMAT,
|
||||
SYNC_WORD,
|
||||
PROTOCOL_VERSION,
|
||||
message_type,
|
||||
sequence_number,
|
||||
payload_length,
|
||||
)
|
||||
|
||||
packet = header + payload
|
||||
|
||||
|
||||
print("Исходное сообщение:")
|
||||
print(message)
|
||||
|
||||
print("\nИсходный пакет:")
|
||||
print(packet.hex(" "))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Имитация повреждения пакета
|
||||
# ============================================================
|
||||
|
||||
# bytes нельзя изменять напрямую.
|
||||
# Поэтому преобразуем пакет в изменяемый массив bytearray.
|
||||
corrupted_packet = bytearray(packet)
|
||||
|
||||
header_size = struct.calcsize(HEADER_FORMAT)
|
||||
|
||||
# В строке "ПРИВЕТ SDR":
|
||||
#
|
||||
# "ПРИВЕТ" занимает 12 байтов UTF-8,
|
||||
# пробел занимает 1 байт,
|
||||
# буква S находится по индексу 13 внутри PAYLOAD.
|
||||
payload_byte_index = 13
|
||||
|
||||
# Полный индекс внутри пакета:
|
||||
packet_byte_index = header_size + payload_byte_index
|
||||
|
||||
original_byte = corrupted_packet[packet_byte_index]
|
||||
|
||||
# XOR с 0x01 изменяет младший бит:
|
||||
#
|
||||
# 0x53 = 01010011 = S
|
||||
# 0x52 = 01010010 = R
|
||||
corrupted_packet[packet_byte_index] ^= 0x01
|
||||
|
||||
corrupted_byte = corrupted_packet[packet_byte_index]
|
||||
|
||||
|
||||
print("\nИзменяемый байт внутри пакета:")
|
||||
|
||||
print(
|
||||
f"До повреждения: "
|
||||
f"{original_byte:3d} "
|
||||
f"= 0x{original_byte:02X} "
|
||||
f"= {original_byte:08b}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"После повреждения: "
|
||||
f"{corrupted_byte:3d} "
|
||||
f"= 0x{corrupted_byte:02X} "
|
||||
f"= {corrupted_byte:08b}"
|
||||
)
|
||||
|
||||
print("\nПовреждённый пакет:")
|
||||
print(bytes(corrupted_packet).hex(" "))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Разбор повреждённого пакета
|
||||
# ============================================================
|
||||
|
||||
received_header = corrupted_packet[:header_size]
|
||||
received_payload = corrupted_packet[header_size:]
|
||||
|
||||
(
|
||||
received_sync,
|
||||
received_version,
|
||||
received_type,
|
||||
received_sequence,
|
||||
received_length,
|
||||
) = struct.unpack(HEADER_FORMAT, received_header)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки структуры
|
||||
# ============================================================
|
||||
|
||||
assert received_sync == SYNC_WORD
|
||||
assert received_version == PROTOCOL_VERSION
|
||||
assert received_type == MESSAGE_TYPE_TEXT
|
||||
assert received_sequence == sequence_number
|
||||
assert received_length == len(received_payload)
|
||||
|
||||
print("\nВсе проверки заголовка пройдены.")
|
||||
|
||||
restored_message = bytes(received_payload).decode("utf-8")
|
||||
|
||||
|
||||
print("\nСообщение после повреждения:")
|
||||
print(restored_message)
|
||||
|
||||
print("\nИсходное сообщение:")
|
||||
print(message)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка результата эксперимента
|
||||
# ============================================================
|
||||
|
||||
assert restored_message != message
|
||||
|
||||
print("\nПовреждение данных произошло.")
|
||||
print("Но заголовок и длина пакета остались правильными.")
|
||||
print("Без CRC приёмник не смог определить ошибку.")
|
||||
146
Projects/SDR_Rover/tests/lab002_packet_structure.py
Normal file
146
Projects/SDR_Rover/tests/lab002_packet_structure.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Lab002. Формирование и разбор первого цифрового пакета.
|
||||
|
||||
Пакет содержит:
|
||||
- маркер начала;
|
||||
- версию протокола;
|
||||
- тип сообщения;
|
||||
- порядковый номер;
|
||||
- длину полезных данных;
|
||||
- полезные данные.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Константы протокола
|
||||
# ============================================================
|
||||
|
||||
SYNC_WORD = 0xAA55
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
MESSAGE_TYPE_TEXT = 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Исходные данные
|
||||
# ============================================================
|
||||
|
||||
message = "ПРИВЕТ SDR"
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
sequence_number = 1
|
||||
message_type = MESSAGE_TYPE_TEXT
|
||||
payload_length = len(payload)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование заголовка
|
||||
# ============================================================
|
||||
|
||||
# Формат:
|
||||
# > — порядок байтов от старшего к младшему, big-endian
|
||||
# H — беззнаковое целое число размером 2 байта
|
||||
# B — беззнаковое целое число размером 1 байт
|
||||
# B — ещё одно число размером 1 байт
|
||||
# H — порядковый номер размером 2 байта
|
||||
# H — длина данных размером 2 байта
|
||||
HEADER_FORMAT = ">HBBHH"
|
||||
|
||||
header = struct.pack(
|
||||
HEADER_FORMAT,
|
||||
SYNC_WORD,
|
||||
PROTOCOL_VERSION,
|
||||
message_type,
|
||||
sequence_number,
|
||||
payload_length,
|
||||
)
|
||||
|
||||
packet = header + payload
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод сформированного пакета
|
||||
# ============================================================
|
||||
|
||||
print("Исходное сообщение:")
|
||||
print(message)
|
||||
|
||||
print("\nПолезная нагрузка PAYLOAD:")
|
||||
print(payload)
|
||||
|
||||
print("\nДлина PAYLOAD:")
|
||||
print(payload_length, "байт")
|
||||
|
||||
print("\nЗаголовок пакета:")
|
||||
print(header)
|
||||
|
||||
print("\nПолный пакет:")
|
||||
print(packet)
|
||||
|
||||
print("\nПакет в шестнадцатеричном виде:")
|
||||
print(packet.hex(" "))
|
||||
|
||||
print("\nОбщая длина пакета:")
|
||||
print(len(packet), "байт")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Разбор пакета на стороне приёмника
|
||||
# ============================================================
|
||||
|
||||
header_size = struct.calcsize(HEADER_FORMAT)
|
||||
|
||||
received_header = packet[:header_size]
|
||||
received_payload = packet[header_size:]
|
||||
|
||||
(
|
||||
received_sync,
|
||||
received_version,
|
||||
received_type,
|
||||
received_sequence,
|
||||
received_length,
|
||||
) = struct.unpack(HEADER_FORMAT, received_header)
|
||||
|
||||
|
||||
print("\n--- Разбор принятого пакета ---")
|
||||
|
||||
print("SYNC:")
|
||||
print(hex(received_sync))
|
||||
|
||||
print("\nВерсия протокола:")
|
||||
print(received_version)
|
||||
|
||||
print("\nТип сообщения:")
|
||||
print(received_type)
|
||||
|
||||
print("\nПорядковый номер:")
|
||||
print(received_sequence)
|
||||
|
||||
print("\nДлина из заголовка:")
|
||||
print(received_length, "байт")
|
||||
|
||||
print("\nФактически принято данных:")
|
||||
print(len(received_payload), "байт")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки
|
||||
# ============================================================
|
||||
|
||||
assert received_sync == SYNC_WORD
|
||||
assert received_version == PROTOCOL_VERSION
|
||||
assert received_type == MESSAGE_TYPE_TEXT
|
||||
assert received_sequence == sequence_number
|
||||
assert received_length == len(received_payload)
|
||||
|
||||
restored_message = received_payload.decode("utf-8")
|
||||
|
||||
assert restored_message == message
|
||||
|
||||
|
||||
print("\nВосстановленное сообщение:")
|
||||
print(restored_message)
|
||||
|
||||
print("\nПроверка пройдена: пакет сформирован и разобран без ошибок.")
|
||||
198
Projects/SDR_Rover/tests/lab003_crc32.py
Normal file
198
Projects/SDR_Rover/tests/lab003_crc32.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Lab003. Обнаружение повреждения пакета с помощью CRC-32.
|
||||
|
||||
Цель:
|
||||
1. Сформировать пакет с CRC-32.
|
||||
2. Проверить исправный пакет.
|
||||
3. Изменить один бит полезной нагрузки.
|
||||
4. Убедиться, что CRC обнаруживает повреждение.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Константы протокола
|
||||
# ============================================================
|
||||
|
||||
SYNC_WORD = 0xAA55
|
||||
PROTOCOL_VERSION = 1
|
||||
MESSAGE_TYPE_TEXT = 1
|
||||
|
||||
HEADER_FORMAT = ">HBBHH"
|
||||
CRC_FORMAT = ">I"
|
||||
|
||||
CRC_SIZE = struct.calcsize(CRC_FORMAT)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование исходного пакета
|
||||
# ============================================================
|
||||
|
||||
message = "ПРИВЕТ SDR"
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
sequence_number = 1
|
||||
payload_length = len(payload)
|
||||
|
||||
header = struct.pack(
|
||||
HEADER_FORMAT,
|
||||
SYNC_WORD,
|
||||
PROTOCOL_VERSION,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
sequence_number,
|
||||
payload_length,
|
||||
)
|
||||
|
||||
# CRC вычисляется по заголовку и полезной нагрузке.
|
||||
packet_without_crc = header + payload
|
||||
|
||||
crc_value = zlib.crc32(packet_without_crc) & 0xFFFFFFFF
|
||||
|
||||
crc_bytes = struct.pack(
|
||||
CRC_FORMAT,
|
||||
crc_value,
|
||||
)
|
||||
|
||||
packet = packet_without_crc + crc_bytes
|
||||
|
||||
|
||||
print("Исходное сообщение:")
|
||||
print(message)
|
||||
|
||||
print("\nCRC-32 исходного пакета:")
|
||||
print(f"0x{crc_value:08X}")
|
||||
|
||||
print("\nПолный пакет:")
|
||||
print(packet.hex(" "))
|
||||
|
||||
print("\nДлина полного пакета:")
|
||||
print(len(packet), "байт")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исправного пакета
|
||||
# ============================================================
|
||||
|
||||
received_data = packet[:-CRC_SIZE]
|
||||
received_crc_bytes = packet[-CRC_SIZE:]
|
||||
|
||||
(received_crc,) = struct.unpack(
|
||||
CRC_FORMAT,
|
||||
received_crc_bytes,
|
||||
)
|
||||
|
||||
calculated_crc = zlib.crc32(received_data) & 0xFFFFFFFF
|
||||
|
||||
|
||||
print("\n--- Проверка исправного пакета ---")
|
||||
|
||||
print(f"CRC из пакета: 0x{received_crc:08X}")
|
||||
print(f"CRC вычисленный: 0x{calculated_crc:08X}")
|
||||
|
||||
assert received_crc == calculated_crc
|
||||
|
||||
print("Результат: пакет не повреждён.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Имитация повреждения одного бита
|
||||
# ============================================================
|
||||
|
||||
corrupted_packet = bytearray(packet)
|
||||
|
||||
header_size = struct.calcsize(HEADER_FORMAT)
|
||||
|
||||
# Буква S находится по индексу 13 внутри PAYLOAD.
|
||||
payload_byte_index = 13
|
||||
packet_byte_index = header_size + payload_byte_index
|
||||
|
||||
original_byte = corrupted_packet[packet_byte_index]
|
||||
|
||||
# Меняем младший бит:
|
||||
# S = 0x53
|
||||
# R = 0x52
|
||||
corrupted_packet[packet_byte_index] ^= 0x01
|
||||
|
||||
corrupted_byte = corrupted_packet[packet_byte_index]
|
||||
|
||||
|
||||
print("\n--- Повреждение одного бита ---")
|
||||
|
||||
print(
|
||||
f"До повреждения: "
|
||||
f"0x{original_byte:02X} = {original_byte:08b}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"После повреждения: "
|
||||
f"0x{corrupted_byte:02X} = {corrupted_byte:08b}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка повреждённого пакета
|
||||
# ============================================================
|
||||
|
||||
corrupted_data = bytes(
|
||||
corrupted_packet[:-CRC_SIZE]
|
||||
)
|
||||
|
||||
corrupted_crc_bytes = bytes(
|
||||
corrupted_packet[-CRC_SIZE:]
|
||||
)
|
||||
|
||||
(received_crc_after_corruption,) = struct.unpack(
|
||||
CRC_FORMAT,
|
||||
corrupted_crc_bytes,
|
||||
)
|
||||
|
||||
calculated_crc_after_corruption = (
|
||||
zlib.crc32(corrupted_data) & 0xFFFFFFFF
|
||||
)
|
||||
|
||||
|
||||
print("\n--- Проверка повреждённого пакета ---")
|
||||
|
||||
print(
|
||||
f"CRC из пакета: "
|
||||
f"0x{received_crc_after_corruption:08X}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"CRC вычисленный: "
|
||||
f"0x{calculated_crc_after_corruption:08X}"
|
||||
)
|
||||
|
||||
|
||||
if received_crc_after_corruption != calculated_crc_after_corruption:
|
||||
print("\nРезультат: CRC обнаружил повреждение пакета.")
|
||||
else:
|
||||
print("\nРезультат: повреждение не обнаружено.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Показываем повреждённое сообщение
|
||||
# ============================================================
|
||||
|
||||
corrupted_payload = corrupted_data[header_size:]
|
||||
|
||||
corrupted_message = corrupted_payload.decode("utf-8")
|
||||
|
||||
print("\nСообщение внутри повреждённого пакета:")
|
||||
print(corrupted_message)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert corrupted_message == "ПРИВЕТ RDR"
|
||||
|
||||
assert (
|
||||
received_crc_after_corruption
|
||||
!= calculated_crc_after_corruption
|
||||
)
|
||||
|
||||
print("\nПроверка пройдена: CRC-32 обнаружил изменение одного бита.")
|
||||
120
Projects/SDR_Rover/tests/lab004_protocol_module.py
Normal file
120
Projects/SDR_Rover/tests/lab004_protocol_module.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Fri Jul 10 17:19:11 2026
|
||||
|
||||
@author: user
|
||||
"""
|
||||
|
||||
"""
|
||||
Lab004. Проверка модуля protocol.packet.
|
||||
|
||||
Проверяем:
|
||||
1. Формирование пакета.
|
||||
2. Разбор исправного пакета.
|
||||
3. Восстановление текста.
|
||||
4. Обнаружение повреждения CRC.
|
||||
"""
|
||||
|
||||
from protocol.packet import (
|
||||
CRCError,
|
||||
HEADER_SIZE,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование пакета
|
||||
# ============================================================
|
||||
|
||||
message = "ПРИВЕТ SDR"
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
sequence_number = 7
|
||||
|
||||
packet = build_packet(
|
||||
payload=payload,
|
||||
message_type=MESSAGE_TYPE_TEXT,
|
||||
sequence_number=sequence_number,
|
||||
)
|
||||
|
||||
|
||||
print("Исходное сообщение:")
|
||||
print(message)
|
||||
|
||||
print("\nСформированный пакет:")
|
||||
print(packet.hex(" "))
|
||||
|
||||
print("\nРазмер пакета:")
|
||||
print(len(packet), "байт")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Разбор исправного пакета
|
||||
# ============================================================
|
||||
|
||||
parsed_packet = parse_packet(packet)
|
||||
|
||||
restored_message = parsed_packet.payload.decode("utf-8")
|
||||
|
||||
|
||||
print("\n--- Разобранный пакет ---")
|
||||
|
||||
print("Версия:")
|
||||
print(parsed_packet.version)
|
||||
|
||||
print("\nТип сообщения:")
|
||||
print(parsed_packet.message_type)
|
||||
|
||||
print("\nПорядковый номер:")
|
||||
print(parsed_packet.sequence_number)
|
||||
|
||||
print("\nПолезная нагрузка:")
|
||||
print(parsed_packet.payload)
|
||||
|
||||
print("\nВосстановленное сообщение:")
|
||||
print(restored_message)
|
||||
|
||||
|
||||
assert parsed_packet.message_type == MESSAGE_TYPE_TEXT
|
||||
assert parsed_packet.sequence_number == sequence_number
|
||||
assert restored_message == message
|
||||
|
||||
print("\nИсправный пакет успешно разобран.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Повреждение одного бита
|
||||
# ============================================================
|
||||
|
||||
corrupted_packet = bytearray(packet)
|
||||
|
||||
# В строке "ПРИВЕТ SDR" буква S находится
|
||||
# по индексу 13 внутри полезной нагрузки.
|
||||
payload_byte_index = 13
|
||||
|
||||
packet_byte_index = (
|
||||
HEADER_SIZE + payload_byte_index
|
||||
)
|
||||
|
||||
corrupted_packet[packet_byte_index] ^= 0x01
|
||||
|
||||
|
||||
print("\n--- Проверка повреждённого пакета ---")
|
||||
|
||||
|
||||
try:
|
||||
parse_packet(corrupted_packet)
|
||||
|
||||
except CRCError as error:
|
||||
print("Повреждение обнаружено.")
|
||||
print(error)
|
||||
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Ошибка: повреждённый пакет был принят как исправный"
|
||||
)
|
||||
|
||||
|
||||
print("\nВсе проверки Lab004 успешно выполнены.")
|
||||
254
Projects/SDR_Rover/tests/lab005_virtual_channel.py
Normal file
254
Projects/SDR_Rover/tests/lab005_virtual_channel.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Lab005. Передача пакетов через виртуальный радиоканал.
|
||||
|
||||
Эксперимент:
|
||||
1. Формируем 100 цифровых пакетов.
|
||||
2. Передаём их через виртуальный канал.
|
||||
3. Канал может:
|
||||
- полностью потерять пакет;
|
||||
- изменить отдельные биты.
|
||||
4. Приёмник проверяет структуру пакета и CRC-32.
|
||||
5. Выводится статистика доставки.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from protocol.packet import (
|
||||
CRCError,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
PacketError,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
from tools.virtual_channel import transmit
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
# Количество передаваемых пакетов.
|
||||
PACKET_COUNT = 100
|
||||
|
||||
# Вероятность полной потери одного пакета.
|
||||
#
|
||||
# 0.10 означает 10 %.
|
||||
DROP_PROBABILITY = 0.10
|
||||
|
||||
# Вероятность повреждения каждого отдельного бита.
|
||||
#
|
||||
# 0.002 означает:
|
||||
# 0,2 % вероятности ошибки для каждого бита.
|
||||
BIT_ERROR_PROBABILITY = 0.002
|
||||
|
||||
# Генератор случайных чисел.
|
||||
#
|
||||
# Число 2026 позволяет получать одинаковый результат
|
||||
# при каждом запуске программы.
|
||||
random_generator = random.Random(2026)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Счётчики результатов
|
||||
# ============================================================
|
||||
|
||||
sent_packets = 0
|
||||
|
||||
# Пакеты, полностью исчезнувшие в канале.
|
||||
dropped_packets = 0
|
||||
|
||||
# Доставленные, но повреждённые пакеты.
|
||||
rejected_packets = 0
|
||||
|
||||
# Пакеты, правильно принятые и разобранные.
|
||||
received_ok = 0
|
||||
|
||||
# Общее количество изменённых битов.
|
||||
total_flipped_bits = 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Передача серии пакетов
|
||||
# ============================================================
|
||||
|
||||
for sequence_number in range(PACKET_COUNT):
|
||||
|
||||
# Создаём уникальное сообщение для каждого пакета.
|
||||
message = f"ПАКЕТ {sequence_number}"
|
||||
|
||||
# Текст превращается в байты UTF-8.
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
# Формируем полный пакет с заголовком и CRC-32.
|
||||
packet = build_packet(
|
||||
payload=payload,
|
||||
message_type=MESSAGE_TYPE_TEXT,
|
||||
sequence_number=sequence_number,
|
||||
)
|
||||
|
||||
sent_packets += 1
|
||||
|
||||
# Передаём пакет через виртуальный канал.
|
||||
received_packet, flipped_bits = transmit(
|
||||
packet=packet,
|
||||
drop_probability=DROP_PROBABILITY,
|
||||
bit_error_probability=BIT_ERROR_PROBABILITY,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
total_flipped_bits += flipped_bits
|
||||
|
||||
# None означает, что пакет полностью потерян.
|
||||
if received_packet is None:
|
||||
dropped_packets += 1
|
||||
continue
|
||||
|
||||
# Пытаемся разобрать принятый пакет.
|
||||
try:
|
||||
parsed_packet = parse_packet(received_packet)
|
||||
|
||||
except (CRCError, PacketError):
|
||||
# CRCError:
|
||||
# контрольная сумма не совпала.
|
||||
#
|
||||
# PacketError:
|
||||
# повреждены SYNC, версия, длина
|
||||
# или другая часть структуры.
|
||||
rejected_packets += 1
|
||||
continue
|
||||
|
||||
# Восстанавливаем текст из полезной нагрузки.
|
||||
restored_message = parsed_packet.payload.decode("utf-8")
|
||||
|
||||
# Проверяем, что получен именно тот пакет,
|
||||
# который был передан.
|
||||
assert parsed_packet.sequence_number == sequence_number
|
||||
assert restored_message == message
|
||||
|
||||
received_ok += 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод настроек
|
||||
# ============================================================
|
||||
|
||||
print("=== Lab005. Виртуальный канал ===")
|
||||
|
||||
print("\nНастройки эксперимента:")
|
||||
|
||||
print(
|
||||
"Количество пакетов:",
|
||||
PACKET_COUNT,
|
||||
)
|
||||
|
||||
print(
|
||||
"Вероятность полной потери пакета:",
|
||||
DROP_PROBABILITY,
|
||||
)
|
||||
|
||||
print(
|
||||
"Вероятность ошибки отдельного бита:",
|
||||
BIT_ERROR_PROBABILITY,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print("\nРезультаты:")
|
||||
|
||||
print(
|
||||
"Передано пакетов:",
|
||||
sent_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Потеряно полностью:",
|
||||
dropped_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Отклонено из-за повреждений:",
|
||||
rejected_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Принято правильно:",
|
||||
received_ok,
|
||||
)
|
||||
|
||||
print(
|
||||
"Всего изменено битов:",
|
||||
total_flipped_bits,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт процентов
|
||||
# ============================================================
|
||||
|
||||
delivery_rate = (
|
||||
received_ok
|
||||
/ sent_packets
|
||||
* 100
|
||||
)
|
||||
|
||||
drop_rate = (
|
||||
dropped_packets
|
||||
/ sent_packets
|
||||
* 100
|
||||
)
|
||||
|
||||
rejection_rate = (
|
||||
rejected_packets
|
||||
/ sent_packets
|
||||
* 100
|
||||
)
|
||||
|
||||
packet_failure_rate = (
|
||||
dropped_packets
|
||||
+ rejected_packets
|
||||
) / sent_packets * 100
|
||||
|
||||
|
||||
print("\nСтатистика:")
|
||||
|
||||
print(
|
||||
f"Успешно доставлено: "
|
||||
f"{delivery_rate:.1f} %"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Потеряно полностью: "
|
||||
f"{drop_rate:.1f} %"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Отклонено приёмником: "
|
||||
f"{rejection_rate:.1f} %"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Общая доля недоставленных пакетов: "
|
||||
f"{packet_failure_rate:.1f} %"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Контроль правильности
|
||||
# ============================================================
|
||||
|
||||
accounted_packets = (
|
||||
received_ok
|
||||
+ dropped_packets
|
||||
+ rejected_packets
|
||||
)
|
||||
|
||||
assert accounted_packets == sent_packets
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"все переданные пакеты учтены."
|
||||
)
|
||||
325
Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py
Normal file
325
Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Lab006. Надёжная передача пакетов методом Stop-and-Wait ARQ.
|
||||
|
||||
Передатчик:
|
||||
1. Отправляет пакет.
|
||||
2. Ожидает подтверждение ACK.
|
||||
3. При отсутствии правильного ACK повторяет передачу.
|
||||
4. После исчерпания попыток считает сообщение недоставленным.
|
||||
|
||||
Приёмник:
|
||||
1. Проверяет пакет.
|
||||
2. Отбрасывает повреждённые пакеты.
|
||||
3. Отправляет ACK для правильного пакета.
|
||||
4. Не передаёт приложению повторные копии одного сообщения.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from protocol.packet import (
|
||||
CRCError,
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
PacketError,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
from tools.virtual_channel import transmit
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
MESSAGE_COUNT = 100
|
||||
|
||||
# Максимальное количество передач одного сообщения.
|
||||
MAX_ATTEMPTS = 5
|
||||
|
||||
# Параметры канала от передатчика к приёмнику.
|
||||
DATA_DROP_PROBABILITY = 0.10
|
||||
DATA_BIT_ERROR_PROBABILITY = 0.002
|
||||
|
||||
# Параметры обратного канала для ACK.
|
||||
ACK_DROP_PROBABILITY = 0.10
|
||||
ACK_BIT_ERROR_PROBABILITY = 0.002
|
||||
|
||||
# Фиксированное число делает эксперимент повторяемым.
|
||||
random_generator = random.Random(2026)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Счётчики
|
||||
# ============================================================
|
||||
|
||||
delivered_messages = 0
|
||||
failed_messages = 0
|
||||
|
||||
data_transmissions = 0
|
||||
ack_transmissions = 0
|
||||
|
||||
duplicate_packets = 0
|
||||
|
||||
invalid_data_packets = 0
|
||||
invalid_ack_packets = 0
|
||||
|
||||
dropped_data_packets = 0
|
||||
dropped_ack_packets = 0
|
||||
|
||||
# Номера пакетов, уже переданных приложению приёмника.
|
||||
received_sequences = set()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Передача сообщений
|
||||
# ============================================================
|
||||
|
||||
for sequence_number in range(MESSAGE_COUNT):
|
||||
|
||||
message = f"ПАКЕТ {sequence_number}"
|
||||
payload = message.encode("utf-8")
|
||||
|
||||
data_packet = build_packet(
|
||||
payload=payload,
|
||||
message_type=MESSAGE_TYPE_TEXT,
|
||||
sequence_number=sequence_number,
|
||||
)
|
||||
|
||||
message_confirmed = False
|
||||
|
||||
# Повторяем передачу до MAX_ATTEMPTS раз.
|
||||
for attempt_number in range(1, MAX_ATTEMPTS + 1):
|
||||
|
||||
data_transmissions += 1
|
||||
|
||||
received_data_packet, _ = transmit(
|
||||
packet=data_packet,
|
||||
drop_probability=DATA_DROP_PROBABILITY,
|
||||
bit_error_probability=DATA_BIT_ERROR_PROBABILITY,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
# Пакет полностью потерялся.
|
||||
if received_data_packet is None:
|
||||
dropped_data_packets += 1
|
||||
continue
|
||||
|
||||
# Приёмник проверяет пакет.
|
||||
try:
|
||||
parsed_data = parse_packet(received_data_packet)
|
||||
|
||||
except (CRCError, PacketError):
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
# Проверка типа сообщения.
|
||||
if parsed_data.message_type != MESSAGE_TYPE_TEXT:
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
# Проверяем, не получали ли мы этот пакет ранее.
|
||||
if parsed_data.sequence_number in received_sequences:
|
||||
duplicate_packets += 1
|
||||
|
||||
else:
|
||||
restored_message = parsed_data.payload.decode("utf-8")
|
||||
|
||||
assert restored_message == message
|
||||
|
||||
received_sequences.add(
|
||||
parsed_data.sequence_number
|
||||
)
|
||||
|
||||
# Формируем подтверждение.
|
||||
#
|
||||
# ACK имеет тот же sequence_number,
|
||||
# что и подтверждаемый пакет.
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=parsed_data.sequence_number,
|
||||
)
|
||||
|
||||
ack_transmissions += 1
|
||||
|
||||
received_ack_packet, _ = transmit(
|
||||
packet=ack_packet,
|
||||
drop_probability=ACK_DROP_PROBABILITY,
|
||||
bit_error_probability=ACK_BIT_ERROR_PROBABILITY,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
# ACK полностью потерялся.
|
||||
if received_ack_packet is None:
|
||||
dropped_ack_packets += 1
|
||||
continue
|
||||
|
||||
# Передатчик проверяет ACK.
|
||||
try:
|
||||
parsed_ack = parse_packet(received_ack_packet)
|
||||
|
||||
except (CRCError, PacketError):
|
||||
invalid_ack_packets += 1
|
||||
continue
|
||||
|
||||
# ACK должен иметь правильный тип и номер.
|
||||
if (
|
||||
parsed_ack.message_type != MESSAGE_TYPE_ACK
|
||||
or parsed_ack.sequence_number != sequence_number
|
||||
):
|
||||
invalid_ack_packets += 1
|
||||
continue
|
||||
|
||||
# Подтверждение получено.
|
||||
message_confirmed = True
|
||||
break
|
||||
|
||||
if message_confirmed:
|
||||
delivered_messages += 1
|
||||
else:
|
||||
failed_messages += 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт статистики
|
||||
# ============================================================
|
||||
|
||||
delivery_rate = (
|
||||
delivered_messages
|
||||
/ MESSAGE_COUNT
|
||||
* 100
|
||||
)
|
||||
|
||||
failure_rate = (
|
||||
failed_messages
|
||||
/ MESSAGE_COUNT
|
||||
* 100
|
||||
)
|
||||
|
||||
average_data_transmissions = (
|
||||
data_transmissions
|
||||
/ MESSAGE_COUNT
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результата
|
||||
# ============================================================
|
||||
|
||||
print("=== Lab006. Stop-and-Wait ARQ ===")
|
||||
|
||||
print("\nНастройки:")
|
||||
|
||||
print("Количество сообщений:", MESSAGE_COUNT)
|
||||
print("Максимум попыток:", MAX_ATTEMPTS)
|
||||
|
||||
print(
|
||||
"Вероятность потери DATA:",
|
||||
DATA_DROP_PROBABILITY,
|
||||
)
|
||||
|
||||
print(
|
||||
"Вероятность битовой ошибки DATA:",
|
||||
DATA_BIT_ERROR_PROBABILITY,
|
||||
)
|
||||
|
||||
print(
|
||||
"Вероятность потери ACK:",
|
||||
ACK_DROP_PROBABILITY,
|
||||
)
|
||||
|
||||
print(
|
||||
"Вероятность битовой ошибки ACK:",
|
||||
ACK_BIT_ERROR_PROBABILITY,
|
||||
)
|
||||
|
||||
|
||||
print("\nОсновные результаты:")
|
||||
|
||||
print(
|
||||
"Успешно подтверждено сообщений:",
|
||||
delivered_messages,
|
||||
)
|
||||
|
||||
print(
|
||||
"Не подтверждено сообщений:",
|
||||
failed_messages,
|
||||
)
|
||||
|
||||
print(
|
||||
f"Доля успешной доставки: "
|
||||
f"{delivery_rate:.1f} %"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Доля недоставленных сообщений: "
|
||||
f"{failure_rate:.1f} %"
|
||||
)
|
||||
|
||||
|
||||
print("\nПередачи:")
|
||||
|
||||
print(
|
||||
"Всего передач DATA:",
|
||||
data_transmissions,
|
||||
)
|
||||
|
||||
print(
|
||||
"Всего передач ACK:",
|
||||
ack_transmissions,
|
||||
)
|
||||
|
||||
print(
|
||||
f"Среднее число передач DATA "
|
||||
f"на одно сообщение: "
|
||||
f"{average_data_transmissions:.2f}"
|
||||
)
|
||||
|
||||
|
||||
print("\nОшибки канала:")
|
||||
|
||||
print(
|
||||
"Полностью потеряно DATA:",
|
||||
dropped_data_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Повреждено DATA:",
|
||||
invalid_data_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Полностью потеряно ACK:",
|
||||
dropped_ack_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Повреждено ACK:",
|
||||
invalid_ack_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Получено повторных DATA-пакетов:",
|
||||
duplicate_packets,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert (
|
||||
delivered_messages
|
||||
+ failed_messages
|
||||
== MESSAGE_COUNT
|
||||
)
|
||||
|
||||
assert delivered_messages <= len(received_sequences)
|
||||
|
||||
assert data_transmissions >= MESSAGE_COUNT
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"все сообщения учтены."
|
||||
)
|
||||
270
Projects/SDR_Rover/tests/lab007_image_fragmentation.py
Normal file
270
Projects/SDR_Rover/tests/lab007_image_fragmentation.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Lab007. Разбиение JPEG на фрагменты и обратная сборка.
|
||||
|
||||
Этапы:
|
||||
1. Создание тестового JPEG.
|
||||
2. Чтение файла как последовательности байтов.
|
||||
3. Разбиение изображения на фрагменты.
|
||||
4. Формирование цифрового пакета для каждого фрагмента.
|
||||
5. Перемешивание порядка доставки пакетов.
|
||||
6. Проверка CRC и разбор пакетов.
|
||||
7. Обратная сборка изображения.
|
||||
8. Сравнение контрольных сумм файлов.
|
||||
"""
|
||||
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
import random
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from protocol.image_fragments import (
|
||||
decode_image_fragment,
|
||||
encode_image_fragment,
|
||||
reassemble_image,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
IMAGE_WIDTH = 320
|
||||
IMAGE_HEIGHT = 240
|
||||
|
||||
IMAGE_ID = 20260713
|
||||
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
random_generator = random.Random(2026)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Пути к файлам
|
||||
# ============================================================
|
||||
|
||||
source_directory = Path("data/raw")
|
||||
received_directory = Path("data/received")
|
||||
|
||||
source_directory.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
received_directory.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
source_path = (
|
||||
source_directory
|
||||
/ "lab007_source.jpg"
|
||||
)
|
||||
|
||||
received_path = (
|
||||
received_directory
|
||||
/ "lab007_reassembled.jpg"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Создание тестового JPEG
|
||||
# ============================================================
|
||||
|
||||
image = Image.new(
|
||||
"RGB",
|
||||
(IMAGE_WIDTH, IMAGE_HEIGHT),
|
||||
)
|
||||
|
||||
pixels = image.load()
|
||||
|
||||
for y in range(IMAGE_HEIGHT):
|
||||
for x in range(IMAGE_WIDTH):
|
||||
|
||||
red = (
|
||||
x * 255
|
||||
// (IMAGE_WIDTH - 1)
|
||||
)
|
||||
|
||||
green = (
|
||||
y * 255
|
||||
// (IMAGE_HEIGHT - 1)
|
||||
)
|
||||
|
||||
blue = (
|
||||
(x + y) * 255
|
||||
// (
|
||||
IMAGE_WIDTH
|
||||
+ IMAGE_HEIGHT
|
||||
- 2
|
||||
)
|
||||
)
|
||||
|
||||
pixels[x, y] = (
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
)
|
||||
|
||||
image.save(
|
||||
source_path,
|
||||
format="JPEG",
|
||||
quality=60,
|
||||
optimize=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Чтение изображения
|
||||
# ============================================================
|
||||
|
||||
source_bytes = source_path.read_bytes()
|
||||
|
||||
source_hash = sha256(
|
||||
source_bytes
|
||||
).hexdigest()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Разбиение изображения на фрагменты
|
||||
# ============================================================
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=source_bytes,
|
||||
image_id=IMAGE_ID,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование цифровых пакетов
|
||||
# ============================================================
|
||||
|
||||
packets = []
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment.fragment_index,
|
||||
)
|
||||
|
||||
packets.append(packet)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Имитация доставки в произвольном порядке
|
||||
# ============================================================
|
||||
|
||||
random_generator.shuffle(packets)
|
||||
|
||||
received_fragments = []
|
||||
|
||||
for packet in packets:
|
||||
|
||||
parsed_packet = parse_packet(packet)
|
||||
|
||||
assert (
|
||||
parsed_packet.message_type
|
||||
== MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||
)
|
||||
|
||||
fragment = decode_image_fragment(
|
||||
parsed_packet.payload
|
||||
)
|
||||
|
||||
assert (
|
||||
parsed_packet.sequence_number
|
||||
== fragment.fragment_index
|
||||
)
|
||||
|
||||
received_fragments.append(fragment)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сборка изображения
|
||||
# ============================================================
|
||||
|
||||
received_bytes = reassemble_image(
|
||||
received_fragments
|
||||
)
|
||||
|
||||
received_path.write_bytes(
|
||||
received_bytes
|
||||
)
|
||||
|
||||
received_hash = sha256(
|
||||
received_bytes
|
||||
).hexdigest()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки
|
||||
# ============================================================
|
||||
|
||||
assert received_bytes == source_bytes
|
||||
assert received_hash == source_hash
|
||||
|
||||
with Image.open(received_path) as received_image:
|
||||
received_image.verify()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Результаты
|
||||
# ============================================================
|
||||
|
||||
print("=== Lab007. Фрагментация изображения ===")
|
||||
|
||||
print("\nИсходный файл:")
|
||||
print(source_path)
|
||||
|
||||
print("\nВосстановленный файл:")
|
||||
print(received_path)
|
||||
|
||||
print("\nРазмер изображения:")
|
||||
print(
|
||||
IMAGE_WIDTH,
|
||||
"x",
|
||||
IMAGE_HEIGHT,
|
||||
"пикселей",
|
||||
)
|
||||
|
||||
print("\nРазмер JPEG:")
|
||||
print(
|
||||
len(source_bytes),
|
||||
"байт",
|
||||
)
|
||||
|
||||
print("\nРазмер данных одного фрагмента:")
|
||||
print(
|
||||
FRAGMENT_DATA_SIZE,
|
||||
"байт",
|
||||
)
|
||||
|
||||
print("\nКоличество фрагментов:")
|
||||
print(
|
||||
len(fragments)
|
||||
)
|
||||
|
||||
print("\nSHA-256 исходного файла:")
|
||||
print(source_hash)
|
||||
|
||||
print("\nSHA-256 восстановленного файла:")
|
||||
print(received_hash)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"JPEG восстановлен байт в байт."
|
||||
)
|
||||
428
Projects/SDR_Rover/tests/lab008_image_transfer_arq.py
Normal file
428
Projects/SDR_Rover/tests/lab008_image_transfer_arq.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
Lab008. Передача JPEG через виртуальный канал с ARQ.
|
||||
|
||||
Этапы:
|
||||
1. Чтение тестового JPEG из Lab007.
|
||||
2. Разбиение файла на фрагменты.
|
||||
3. Передача каждого фрагмента через виртуальный канал.
|
||||
4. Проверка CRC на приёмнике.
|
||||
5. Передача ACK по обратному каналу.
|
||||
6. Повторная передача при потере или повреждении.
|
||||
7. Сборка принятого JPEG.
|
||||
8. Сравнение SHA-256 исходного и принятого файлов.
|
||||
"""
|
||||
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
import random
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from protocol.image_fragments import (
|
||||
ImageFragmentError,
|
||||
decode_image_fragment,
|
||||
encode_image_fragment,
|
||||
reassemble_image,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
CRCError,
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
PacketError,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
from tools.virtual_channel import transmit
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
IMAGE_ID = 20260713
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
# Максимальное количество передач одного фрагмента.
|
||||
MAX_ATTEMPTS = 12
|
||||
|
||||
# Прямой канал: передатчик → приёмник.
|
||||
DATA_DROP_PROBABILITY = 0.10
|
||||
DATA_BIT_ERROR_PROBABILITY = 0.0002
|
||||
|
||||
# Обратный канал: приёмник → передатчик.
|
||||
ACK_DROP_PROBABILITY = 0.10
|
||||
ACK_BIT_ERROR_PROBABILITY = 0.0002
|
||||
|
||||
# Фиксированное значение делает результат воспроизводимым.
|
||||
random_generator = random.Random(2026)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Пути к файлам
|
||||
# ============================================================
|
||||
|
||||
source_path = Path(
|
||||
"data/raw/lab007_source.jpg"
|
||||
)
|
||||
|
||||
received_path = Path(
|
||||
"data/received/lab008_received.jpg"
|
||||
)
|
||||
|
||||
if not source_path.exists():
|
||||
raise FileNotFoundError(
|
||||
"Не найден файл data/raw/lab007_source.jpg. "
|
||||
"Сначала необходимо выполнить Lab007."
|
||||
)
|
||||
|
||||
received_path.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Чтение и разбиение изображения
|
||||
# ============================================================
|
||||
|
||||
source_bytes = source_path.read_bytes()
|
||||
|
||||
source_hash = sha256(
|
||||
source_bytes
|
||||
).hexdigest()
|
||||
|
||||
source_fragments = split_image_bytes(
|
||||
image_bytes=source_bytes,
|
||||
image_id=IMAGE_ID,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Счётчики
|
||||
# ============================================================
|
||||
|
||||
data_transmissions = 0
|
||||
ack_transmissions = 0
|
||||
|
||||
dropped_data_packets = 0
|
||||
invalid_data_packets = 0
|
||||
|
||||
dropped_ack_packets = 0
|
||||
invalid_ack_packets = 0
|
||||
|
||||
duplicate_data_packets = 0
|
||||
|
||||
flipped_data_bits = 0
|
||||
flipped_ack_bits = 0
|
||||
|
||||
confirmed_fragments = 0
|
||||
failed_fragment_indexes = []
|
||||
|
||||
# Принятые фрагменты хранятся по их номеру.
|
||||
received_fragments = {}
|
||||
|
||||
# Число попыток передачи каждого фрагмента.
|
||||
attempts_per_fragment = {}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Передача всех фрагментов
|
||||
# ============================================================
|
||||
|
||||
for source_fragment in source_fragments:
|
||||
|
||||
fragment_index = source_fragment.fragment_index
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
source_fragment
|
||||
)
|
||||
|
||||
data_packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment_index,
|
||||
)
|
||||
|
||||
fragment_confirmed = False
|
||||
|
||||
for attempt_number in range(
|
||||
1,
|
||||
MAX_ATTEMPTS + 1,
|
||||
):
|
||||
data_transmissions += 1
|
||||
|
||||
received_data_packet, changed_bits = transmit(
|
||||
packet=data_packet,
|
||||
drop_probability=DATA_DROP_PROBABILITY,
|
||||
bit_error_probability=DATA_BIT_ERROR_PROBABILITY,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
flipped_data_bits += changed_bits
|
||||
|
||||
# DATA полностью потерян.
|
||||
if received_data_packet is None:
|
||||
dropped_data_packets += 1
|
||||
continue
|
||||
|
||||
# Проверка структуры и CRC.
|
||||
try:
|
||||
parsed_data_packet = parse_packet(
|
||||
received_data_packet
|
||||
)
|
||||
|
||||
except (CRCError, PacketError):
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
if (
|
||||
parsed_data_packet.message_type
|
||||
!= MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||
):
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
# Разбор заголовка фрагмента изображения.
|
||||
try:
|
||||
received_fragment = decode_image_fragment(
|
||||
parsed_data_packet.payload
|
||||
)
|
||||
|
||||
except ImageFragmentError:
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
# Проверяем согласованность номеров.
|
||||
if (
|
||||
parsed_data_packet.sequence_number
|
||||
!= received_fragment.fragment_index
|
||||
):
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
if received_fragment.image_id != IMAGE_ID:
|
||||
invalid_data_packets += 1
|
||||
continue
|
||||
|
||||
# Сохраняем новый фрагмент либо распознаём дубликат.
|
||||
existing_fragment = received_fragments.get(
|
||||
received_fragment.fragment_index
|
||||
)
|
||||
|
||||
if existing_fragment is None:
|
||||
received_fragments[
|
||||
received_fragment.fragment_index
|
||||
] = received_fragment
|
||||
|
||||
else:
|
||||
# DATA мог прийти повторно, если предыдущий ACK потерялся.
|
||||
assert existing_fragment.data == received_fragment.data
|
||||
duplicate_data_packets += 1
|
||||
|
||||
# ====================================================
|
||||
# Формирование ACK
|
||||
# ====================================================
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=received_fragment.fragment_index,
|
||||
)
|
||||
|
||||
ack_transmissions += 1
|
||||
|
||||
received_ack_packet, changed_ack_bits = transmit(
|
||||
packet=ack_packet,
|
||||
drop_probability=ACK_DROP_PROBABILITY,
|
||||
bit_error_probability=ACK_BIT_ERROR_PROBABILITY,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
flipped_ack_bits += changed_ack_bits
|
||||
|
||||
# ACK полностью потерян.
|
||||
if received_ack_packet is None:
|
||||
dropped_ack_packets += 1
|
||||
continue
|
||||
|
||||
# Передатчик проверяет ACK.
|
||||
try:
|
||||
parsed_ack = parse_packet(
|
||||
received_ack_packet
|
||||
)
|
||||
|
||||
except (CRCError, PacketError):
|
||||
invalid_ack_packets += 1
|
||||
continue
|
||||
|
||||
if (
|
||||
parsed_ack.message_type != MESSAGE_TYPE_ACK
|
||||
or parsed_ack.sequence_number != fragment_index
|
||||
or parsed_ack.payload != b""
|
||||
):
|
||||
invalid_ack_packets += 1
|
||||
continue
|
||||
|
||||
# Фрагмент успешно подтверждён.
|
||||
fragment_confirmed = True
|
||||
confirmed_fragments += 1
|
||||
|
||||
attempts_per_fragment[
|
||||
fragment_index
|
||||
] = attempt_number
|
||||
|
||||
break
|
||||
|
||||
if not fragment_confirmed:
|
||||
failed_fragment_indexes.append(
|
||||
fragment_index
|
||||
)
|
||||
|
||||
attempts_per_fragment[
|
||||
fragment_index
|
||||
] = MAX_ATTEMPTS
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Результаты передачи
|
||||
# ============================================================
|
||||
|
||||
print("=== Lab008. Передача JPEG с ARQ ===")
|
||||
|
||||
print("\nИсходный файл:")
|
||||
print(source_path)
|
||||
|
||||
print("\nРазмер JPEG:")
|
||||
print(len(source_bytes), "байт")
|
||||
|
||||
print("\nКоличество фрагментов:")
|
||||
print(len(source_fragments))
|
||||
|
||||
print("\nПодтверждено фрагментов:")
|
||||
print(confirmed_fragments)
|
||||
|
||||
print("\nПолучено уникальных фрагментов:")
|
||||
print(len(received_fragments))
|
||||
|
||||
print("\nНе подтверждены фрагменты:")
|
||||
print(failed_fragment_indexes)
|
||||
|
||||
|
||||
print("\nПередачи:")
|
||||
|
||||
print(
|
||||
"Всего передач DATA:",
|
||||
data_transmissions,
|
||||
)
|
||||
|
||||
print(
|
||||
"Всего передач ACK:",
|
||||
ack_transmissions,
|
||||
)
|
||||
|
||||
|
||||
print("\nОшибки прямого канала:")
|
||||
|
||||
print(
|
||||
"Полностью потеряно DATA:",
|
||||
dropped_data_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Повреждено DATA:",
|
||||
invalid_data_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Изменено битов DATA:",
|
||||
flipped_data_bits,
|
||||
)
|
||||
|
||||
|
||||
print("\nОшибки обратного канала:")
|
||||
|
||||
print(
|
||||
"Полностью потеряно ACK:",
|
||||
dropped_ack_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Повреждено ACK:",
|
||||
invalid_ack_packets,
|
||||
)
|
||||
|
||||
print(
|
||||
"Изменено битов ACK:",
|
||||
flipped_ack_bits,
|
||||
)
|
||||
|
||||
print(
|
||||
"Получено повторных DATA:",
|
||||
duplicate_data_packets,
|
||||
)
|
||||
|
||||
|
||||
print("\nКоличество попыток по фрагментам:")
|
||||
|
||||
for fragment_index in sorted(
|
||||
attempts_per_fragment
|
||||
):
|
||||
print(
|
||||
f"Фрагмент {fragment_index}: "
|
||||
f"{attempts_per_fragment[fragment_index]}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сборка принятого изображения
|
||||
# ============================================================
|
||||
|
||||
assert not failed_fragment_indexes
|
||||
|
||||
assert (
|
||||
len(received_fragments)
|
||||
== len(source_fragments)
|
||||
)
|
||||
|
||||
received_bytes = reassemble_image(
|
||||
received_fragments.values()
|
||||
)
|
||||
|
||||
received_path.write_bytes(
|
||||
received_bytes
|
||||
)
|
||||
|
||||
received_hash = sha256(
|
||||
received_bytes
|
||||
).hexdigest()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Финальные проверки
|
||||
# ============================================================
|
||||
|
||||
assert received_bytes == source_bytes
|
||||
assert received_hash == source_hash
|
||||
|
||||
with Image.open(received_path) as image:
|
||||
image.verify()
|
||||
|
||||
|
||||
print("\nSHA-256 исходного JPEG:")
|
||||
print(source_hash)
|
||||
|
||||
print("\nSHA-256 принятого JPEG:")
|
||||
print(received_hash)
|
||||
|
||||
print("\nПринятый файл:")
|
||||
print(received_path)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"все фрагменты подтверждены, "
|
||||
"JPEG восстановлен байт в байт."
|
||||
)
|
||||
450
Projects/SDR_Rover/tests/lab009_real_photo_estimate.py
Normal file
450
Projects/SDR_Rover/tests/lab009_real_photo_estimate.py
Normal file
@@ -0,0 +1,450 @@
|
||||
"""
|
||||
Lab009. Расчёт параметров передачи реальной фотографии.
|
||||
|
||||
Программа:
|
||||
1. Читает реальный JPEG-файл.
|
||||
2. Определяет его разрешение и размер.
|
||||
3. Разбивает файл на фрагменты.
|
||||
4. Формирует реальные пакеты протокола.
|
||||
5. Учитывает ACK для каждого фрагмента.
|
||||
6. Рассчитывает время передачи при разных скоростях канала.
|
||||
|
||||
Расчёт времени показывает чистое эфирное время без учёта:
|
||||
- пауз между пакетами;
|
||||
- обработки на передатчике и приёмнике;
|
||||
- ожидания ACK;
|
||||
- повторных передач;
|
||||
- преамбулы и служебных символов физического уровня.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from protocol.image_fragments import (
|
||||
FRAGMENT_HEADER_SIZE,
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
CRC_SIZE,
|
||||
HEADER_SIZE,
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
SOURCE_PATH = Path(
|
||||
"data/raw/lab009_source.jpg"
|
||||
)
|
||||
|
||||
IMAGE_ID = 20260713
|
||||
|
||||
# Сколько байтов JPEG помещается в один фрагмент.
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
# Скорости полезного цифрового канала в кбит/с.
|
||||
BITRATES_KBPS = [
|
||||
1,
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def format_bytes(byte_count: int) -> str:
|
||||
"""
|
||||
Представить размер одновременно в байтах и килобайтах.
|
||||
"""
|
||||
|
||||
kibibytes = byte_count / 1024
|
||||
|
||||
return (
|
||||
f"{byte_count} байт "
|
||||
f"({kibibytes:.2f} КиБ)"
|
||||
)
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""
|
||||
Представить длительность в удобной форме.
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.1f} мс"
|
||||
|
||||
if seconds < 60:
|
||||
return f"{seconds:.2f} с"
|
||||
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = seconds % 60
|
||||
|
||||
return (
|
||||
f"{minutes} мин "
|
||||
f"{remaining_seconds:.1f} с"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исходного файла
|
||||
# ============================================================
|
||||
|
||||
if not SOURCE_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Не найден файл: {SOURCE_PATH}"
|
||||
)
|
||||
|
||||
source_bytes = SOURCE_PATH.read_bytes()
|
||||
|
||||
if not source_bytes:
|
||||
raise ValueError(
|
||||
"Исходный JPEG-файл пуст"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Чтение параметров изображения
|
||||
# ============================================================
|
||||
|
||||
with Image.open(SOURCE_PATH) as image:
|
||||
|
||||
image_format = image.format
|
||||
image_mode = image.mode
|
||||
image_width, image_height = image.size
|
||||
|
||||
# Проверяем, что файл действительно читается как изображение.
|
||||
image.verify()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Разбиение JPEG на фрагменты
|
||||
# ============================================================
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=source_bytes,
|
||||
image_id=IMAGE_ID,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование DATA-пакетов
|
||||
# ============================================================
|
||||
|
||||
data_packets = []
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment.fragment_index,
|
||||
)
|
||||
|
||||
data_packets.append(packet)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование типового ACK-пакета
|
||||
# ============================================================
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
ack_packet_size = len(ack_packet)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт объёмов
|
||||
# ============================================================
|
||||
|
||||
source_size = len(source_bytes)
|
||||
|
||||
fragment_count = len(fragments)
|
||||
|
||||
# Полный размер всех DATA-пакетов.
|
||||
total_data_packet_bytes = sum(
|
||||
len(packet)
|
||||
for packet in data_packets
|
||||
)
|
||||
|
||||
# Для каждого DATA-пакета ожидается один ACK.
|
||||
total_ack_bytes = (
|
||||
fragment_count
|
||||
* ack_packet_size
|
||||
)
|
||||
|
||||
# Идеальный радиообмен:
|
||||
# каждый DATA и каждый ACK переданы ровно один раз.
|
||||
total_radio_bytes = (
|
||||
total_data_packet_bytes
|
||||
+ total_ack_bytes
|
||||
)
|
||||
|
||||
total_radio_bits = (
|
||||
total_radio_bytes
|
||||
* 8
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Детализация служебных расходов
|
||||
# ============================================================
|
||||
|
||||
total_fragment_header_bytes = (
|
||||
fragment_count
|
||||
* FRAGMENT_HEADER_SIZE
|
||||
)
|
||||
|
||||
data_protocol_overhead = (
|
||||
fragment_count
|
||||
* (HEADER_SIZE + CRC_SIZE)
|
||||
)
|
||||
|
||||
total_overhead_bytes = (
|
||||
total_radio_bytes
|
||||
- source_size
|
||||
)
|
||||
|
||||
payload_efficiency = (
|
||||
source_size
|
||||
/ total_radio_bytes
|
||||
* 100
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод параметров фотографии
|
||||
# ============================================================
|
||||
|
||||
print("=== Lab009. Расчёт передачи реальной фотографии ===")
|
||||
|
||||
print("\nИсходный файл:")
|
||||
print(SOURCE_PATH)
|
||||
|
||||
print("\nФормат изображения:")
|
||||
print(image_format)
|
||||
|
||||
print("\nЦветовой режим:")
|
||||
print(image_mode)
|
||||
|
||||
print("\nРазрешение:")
|
||||
print(
|
||||
image_width,
|
||||
"x",
|
||||
image_height,
|
||||
"пикселей",
|
||||
)
|
||||
|
||||
print("\nРазмер исходного JPEG:")
|
||||
print(
|
||||
format_bytes(source_size)
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод структуры передачи
|
||||
# ============================================================
|
||||
|
||||
print("\n--- Фрагментация ---")
|
||||
|
||||
print(
|
||||
"Размер данных одного полного фрагмента:",
|
||||
FRAGMENT_DATA_SIZE,
|
||||
"байт",
|
||||
)
|
||||
|
||||
print(
|
||||
"Количество фрагментов:",
|
||||
fragment_count,
|
||||
)
|
||||
|
||||
print(
|
||||
"Размер заголовка фрагмента:",
|
||||
FRAGMENT_HEADER_SIZE,
|
||||
"байт",
|
||||
)
|
||||
|
||||
print(
|
||||
"Размер заголовка основного пакета:",
|
||||
HEADER_SIZE,
|
||||
"байт",
|
||||
)
|
||||
|
||||
print(
|
||||
"Размер CRC-32:",
|
||||
CRC_SIZE,
|
||||
"байт",
|
||||
)
|
||||
|
||||
print(
|
||||
"Размер ACK-пакета:",
|
||||
ack_packet_size,
|
||||
"байт",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод полного объёма
|
||||
# ============================================================
|
||||
|
||||
print("\n--- Объём передачи без повторов ---")
|
||||
|
||||
print("Исходные данные JPEG:")
|
||||
print(
|
||||
format_bytes(source_size)
|
||||
)
|
||||
|
||||
print("\nЗаголовки фрагментов:")
|
||||
print(
|
||||
format_bytes(total_fragment_header_bytes)
|
||||
)
|
||||
|
||||
print("\nСлужебные поля и CRC DATA-пакетов:")
|
||||
print(
|
||||
format_bytes(data_protocol_overhead)
|
||||
)
|
||||
|
||||
print("\nВсе DATA-пакеты:")
|
||||
print(
|
||||
format_bytes(total_data_packet_bytes)
|
||||
)
|
||||
|
||||
print("\nВсе ACK-пакеты:")
|
||||
print(
|
||||
format_bytes(total_ack_bytes)
|
||||
)
|
||||
|
||||
print("\nПолный радиообмен DATA + ACK:")
|
||||
print(
|
||||
format_bytes(total_radio_bytes)
|
||||
)
|
||||
|
||||
print("\nОбщие служебные расходы:")
|
||||
print(
|
||||
format_bytes(total_overhead_bytes)
|
||||
)
|
||||
|
||||
print(
|
||||
"\nПолезная эффективность протокола:"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{payload_efficiency:.2f} %"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт времени передачи
|
||||
# ============================================================
|
||||
|
||||
print("\n--- Чистое эфирное время ---")
|
||||
|
||||
print(
|
||||
"Скорость DATA без ACK DATA + ACK"
|
||||
)
|
||||
|
||||
for bitrate_kbps in BITRATES_KBPS:
|
||||
|
||||
bitrate_bits_per_second = (
|
||||
bitrate_kbps
|
||||
* 1000
|
||||
)
|
||||
|
||||
data_time_seconds = (
|
||||
total_data_packet_bytes
|
||||
* 8
|
||||
/ bitrate_bits_per_second
|
||||
)
|
||||
|
||||
full_time_seconds = (
|
||||
total_radio_bits
|
||||
/ bitrate_bits_per_second
|
||||
)
|
||||
|
||||
print(
|
||||
f"{bitrate_kbps:>4} кбит/с"
|
||||
f" "
|
||||
f"{format_duration(data_time_seconds):>12}"
|
||||
f" "
|
||||
f"{format_duration(full_time_seconds):>12}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сценарий с повторами
|
||||
# ============================================================
|
||||
|
||||
# Коэффициент 2 означает, что суммарно передано
|
||||
# примерно в два раза больше байтов из-за ошибок,
|
||||
# потерь и повторных передач.
|
||||
RETRY_TRAFFIC_MULTIPLIER = 2.0
|
||||
|
||||
print(
|
||||
"\n--- Оценка при удвоении трафика из-за повторов ---"
|
||||
)
|
||||
|
||||
for bitrate_kbps in BITRATES_KBPS:
|
||||
|
||||
bitrate_bits_per_second = (
|
||||
bitrate_kbps
|
||||
* 1000
|
||||
)
|
||||
|
||||
estimated_seconds = (
|
||||
total_radio_bits
|
||||
* RETRY_TRAFFIC_MULTIPLIER
|
||||
/ bitrate_bits_per_second
|
||||
)
|
||||
|
||||
print(
|
||||
f"{bitrate_kbps:>4} кбит/с"
|
||||
f" "
|
||||
f"{format_duration(estimated_seconds)}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert fragment_count > 0
|
||||
|
||||
assert (
|
||||
total_data_packet_bytes
|
||||
> source_size
|
||||
)
|
||||
|
||||
assert (
|
||||
total_radio_bytes
|
||||
> total_data_packet_bytes
|
||||
)
|
||||
|
||||
assert (
|
||||
0 < payload_efficiency < 100
|
||||
)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"параметры передачи фотографии рассчитаны."
|
||||
)
|
||||
785
Projects/SDR_Rover/tests/lab010_image_optimization.py
Normal file
785
Projects/SDR_Rover/tests/lab010_image_optimization.py
Normal file
@@ -0,0 +1,785 @@
|
||||
"""
|
||||
Lab010. Оптимизация изображения для слабого радиоканала.
|
||||
|
||||
Программа формирует несколько вариантов одного кадра:
|
||||
|
||||
1. Исходный JPEG.
|
||||
2. Цветной кадр до 640x480.
|
||||
3. Серый кадр до 640x480.
|
||||
4. Серый кадр до 320x240, JPEG quality 30.
|
||||
5. Серый кадр до 320x240, JPEG quality 15.
|
||||
6. Карта контуров 320x240.
|
||||
7. Серый кадр с наложенными контурами.
|
||||
|
||||
Для каждого варианта рассчитываются:
|
||||
|
||||
- размер JPEG;
|
||||
- число фрагментов;
|
||||
- полный объём DATA + ACK;
|
||||
- эффективность протокола;
|
||||
- время передачи на нескольких скоростях;
|
||||
- время при удвоении трафика из-за повторов.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from hashlib import sha256
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageFilter,
|
||||
ImageOps,
|
||||
)
|
||||
|
||||
from protocol.image_fragments import (
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
SOURCE_PATH = Path(
|
||||
"data/raw/lab009_source.jpg"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab010"
|
||||
)
|
||||
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
IMAGE_ID_BASE = 2026071300
|
||||
|
||||
BITRATES_KBPS = [
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def resize_inside(
|
||||
image: Image.Image,
|
||||
maximum_size: tuple[int, int],
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Уменьшить изображение с сохранением пропорций.
|
||||
|
||||
Изображение не растягивается и не искажается.
|
||||
"""
|
||||
|
||||
resized_image = image.copy()
|
||||
|
||||
resized_image.thumbnail(
|
||||
maximum_size,
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
|
||||
return resized_image
|
||||
|
||||
|
||||
def save_jpeg(
|
||||
image: Image.Image,
|
||||
output_path: Path,
|
||||
quality: int,
|
||||
) -> None:
|
||||
"""
|
||||
Сохранить изображение в JPEG.
|
||||
"""
|
||||
|
||||
output_path.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
image.save(
|
||||
output_path,
|
||||
format="JPEG",
|
||||
quality=quality,
|
||||
optimize=True,
|
||||
)
|
||||
|
||||
|
||||
def format_bytes(byte_count: int) -> str:
|
||||
"""
|
||||
Представить размер в КиБ.
|
||||
"""
|
||||
|
||||
return f"{byte_count / 1024:.2f} КиБ"
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""
|
||||
Представить время в удобной форме.
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.0f} мс"
|
||||
|
||||
if seconds < 60:
|
||||
return f"{seconds:.2f} с"
|
||||
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = seconds % 60
|
||||
|
||||
return (
|
||||
f"{minutes} мин "
|
||||
f"{remaining_seconds:.1f} с"
|
||||
)
|
||||
|
||||
|
||||
def estimate_transfer(
|
||||
file_path: Path,
|
||||
image_id: int,
|
||||
) -> dict:
|
||||
"""
|
||||
Рассчитать параметры передачи одного JPEG.
|
||||
"""
|
||||
|
||||
image_bytes = file_path.read_bytes()
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=image_bytes,
|
||||
image_id=image_id,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
total_data_packet_bytes = 0
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment.fragment_index,
|
||||
)
|
||||
|
||||
total_data_packet_bytes += len(packet)
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
total_ack_bytes = (
|
||||
len(fragments)
|
||||
* len(ack_packet)
|
||||
)
|
||||
|
||||
total_radio_bytes = (
|
||||
total_data_packet_bytes
|
||||
+ total_ack_bytes
|
||||
)
|
||||
|
||||
efficiency_percent = (
|
||||
len(image_bytes)
|
||||
/ total_radio_bytes
|
||||
* 100
|
||||
)
|
||||
|
||||
transfer_times = {}
|
||||
|
||||
for bitrate_kbps in BITRATES_KBPS:
|
||||
|
||||
bitrate_bits_per_second = (
|
||||
bitrate_kbps * 1000
|
||||
)
|
||||
|
||||
transfer_times[bitrate_kbps] = (
|
||||
total_radio_bytes
|
||||
* 8
|
||||
/ bitrate_bits_per_second
|
||||
)
|
||||
|
||||
return {
|
||||
"file_size_bytes": len(image_bytes),
|
||||
"fragment_count": len(fragments),
|
||||
"data_packet_bytes": total_data_packet_bytes,
|
||||
"ack_bytes": total_ack_bytes,
|
||||
"total_radio_bytes": total_radio_bytes,
|
||||
"efficiency_percent": efficiency_percent,
|
||||
"transfer_times": transfer_times,
|
||||
"sha256": sha256(image_bytes).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исходной фотографии
|
||||
# ============================================================
|
||||
|
||||
if not SOURCE_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Не найден исходный файл: {SOURCE_PATH}"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Чтение исходного изображения
|
||||
# ============================================================
|
||||
|
||||
with Image.open(SOURCE_PATH) as source_image:
|
||||
|
||||
source_image.load()
|
||||
|
||||
source_rgb = source_image.convert("RGB")
|
||||
|
||||
original_width, original_height = (
|
||||
source_rgb.size
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование вариантов изображения
|
||||
# ============================================================
|
||||
|
||||
variant_paths = []
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 0. Исходный JPEG без изменения
|
||||
# ------------------------------------------------------------
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"00_original",
|
||||
SOURCE_PATH,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 1. Цветной кадр до 640x480, quality 40
|
||||
# ------------------------------------------------------------
|
||||
|
||||
color_640 = resize_inside(
|
||||
source_rgb,
|
||||
(640, 480),
|
||||
)
|
||||
|
||||
color_640_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "01_color_640_q40.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
color_640,
|
||||
color_640_path,
|
||||
quality=40,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"01_color_640_q40",
|
||||
color_640_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 2. Серый кадр до 640x480, quality 40
|
||||
# ------------------------------------------------------------
|
||||
|
||||
gray_640 = ImageOps.grayscale(
|
||||
color_640
|
||||
)
|
||||
|
||||
gray_640_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "02_gray_640_q40.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_640,
|
||||
gray_640_path,
|
||||
quality=40,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"02_gray_640_q40",
|
||||
gray_640_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 3. Серый кадр до 320x240, quality 30
|
||||
# ------------------------------------------------------------
|
||||
|
||||
color_320 = resize_inside(
|
||||
source_rgb,
|
||||
(320, 240),
|
||||
)
|
||||
|
||||
gray_320 = ImageOps.grayscale(
|
||||
color_320
|
||||
)
|
||||
|
||||
gray_320_q30_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "03_gray_320_q30.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_320,
|
||||
gray_320_q30_path,
|
||||
quality=30,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"03_gray_320_q30",
|
||||
gray_320_q30_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 4. Серый кадр до 320x240, quality 15
|
||||
# ------------------------------------------------------------
|
||||
|
||||
gray_320_q15_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "04_gray_320_q15.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_320,
|
||||
gray_320_q15_path,
|
||||
quality=15,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"04_gray_320_q15",
|
||||
gray_320_q15_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 5. Только контуры
|
||||
# ------------------------------------------------------------
|
||||
|
||||
edge_map = gray_320.filter(
|
||||
ImageFilter.FIND_EDGES
|
||||
)
|
||||
|
||||
edge_map = ImageOps.autocontrast(
|
||||
edge_map
|
||||
)
|
||||
|
||||
# Инвертируем: белый фон, тёмные контуры.
|
||||
edge_map = ImageOps.invert(
|
||||
edge_map
|
||||
)
|
||||
|
||||
edges_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "05_edges_320_q40.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
edge_map,
|
||||
edges_path,
|
||||
quality=40,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"05_edges_320_q40",
|
||||
edges_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Вариант 6. Серый кадр с усиленными контурами
|
||||
# ------------------------------------------------------------
|
||||
|
||||
gray_edges_overlay = Image.blend(
|
||||
gray_320,
|
||||
edge_map,
|
||||
alpha=0.25,
|
||||
)
|
||||
|
||||
gray_edges_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "06_gray_edges_320_q30.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_edges_overlay,
|
||||
gray_edges_path,
|
||||
quality=30,
|
||||
)
|
||||
|
||||
variant_paths.append(
|
||||
(
|
||||
"06_gray_edges_320_q30",
|
||||
gray_edges_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт параметров всех вариантов
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for variant_number, (
|
||||
variant_name,
|
||||
variant_path,
|
||||
) in enumerate(variant_paths):
|
||||
|
||||
with Image.open(variant_path) as variant_image:
|
||||
|
||||
width, height = variant_image.size
|
||||
mode = variant_image.mode
|
||||
|
||||
transfer_result = estimate_transfer(
|
||||
file_path=variant_path,
|
||||
image_id=IMAGE_ID_BASE + variant_number,
|
||||
)
|
||||
|
||||
result = {
|
||||
"name": variant_name,
|
||||
"path": str(variant_path),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"mode": mode,
|
||||
**transfer_result,
|
||||
}
|
||||
|
||||
results.append(result)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Создание сравнительной картинки
|
||||
# ============================================================
|
||||
|
||||
CONTACT_SHEET_COLUMNS = 3
|
||||
CONTACT_SHEET_CELL_WIDTH = 360
|
||||
CONTACT_SHEET_CELL_HEIGHT = 290
|
||||
|
||||
contact_sheet_rows = ceil(
|
||||
len(results)
|
||||
/ CONTACT_SHEET_COLUMNS
|
||||
)
|
||||
|
||||
contact_sheet = Image.new(
|
||||
"RGB",
|
||||
(
|
||||
CONTACT_SHEET_COLUMNS
|
||||
* CONTACT_SHEET_CELL_WIDTH,
|
||||
contact_sheet_rows
|
||||
* CONTACT_SHEET_CELL_HEIGHT,
|
||||
),
|
||||
"white",
|
||||
)
|
||||
|
||||
draw = ImageDraw.Draw(
|
||||
contact_sheet
|
||||
)
|
||||
|
||||
for result_index, result in enumerate(results):
|
||||
|
||||
column = (
|
||||
result_index
|
||||
% CONTACT_SHEET_COLUMNS
|
||||
)
|
||||
|
||||
row = (
|
||||
result_index
|
||||
// CONTACT_SHEET_COLUMNS
|
||||
)
|
||||
|
||||
cell_x = (
|
||||
column
|
||||
* CONTACT_SHEET_CELL_WIDTH
|
||||
)
|
||||
|
||||
cell_y = (
|
||||
row
|
||||
* CONTACT_SHEET_CELL_HEIGHT
|
||||
)
|
||||
|
||||
with Image.open(result["path"]) as variant_image:
|
||||
|
||||
preview = variant_image.convert("RGB")
|
||||
|
||||
preview.thumbnail(
|
||||
(330, 220),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
|
||||
paste_x = (
|
||||
cell_x
|
||||
+ (
|
||||
CONTACT_SHEET_CELL_WIDTH
|
||||
- preview.width
|
||||
)
|
||||
// 2
|
||||
)
|
||||
|
||||
paste_y = (
|
||||
cell_y + 35
|
||||
)
|
||||
|
||||
contact_sheet.paste(
|
||||
preview,
|
||||
(paste_x, paste_y),
|
||||
)
|
||||
|
||||
label = (
|
||||
f"{result['name']}\n"
|
||||
f"{format_bytes(result['file_size_bytes'])}, "
|
||||
f"{result['fragment_count']} fragments"
|
||||
)
|
||||
|
||||
draw.text(
|
||||
(cell_x + 10, cell_y + 8),
|
||||
label,
|
||||
fill="black",
|
||||
)
|
||||
|
||||
contact_sheet_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab010_comparison.jpg"
|
||||
)
|
||||
|
||||
contact_sheet.save(
|
||||
contact_sheet_path,
|
||||
format="JPEG",
|
||||
quality=90,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение таблицы CSV
|
||||
# ============================================================
|
||||
|
||||
csv_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab010_results.csv"
|
||||
)
|
||||
|
||||
csv_fieldnames = [
|
||||
"name",
|
||||
"path",
|
||||
"width",
|
||||
"height",
|
||||
"mode",
|
||||
"file_size_bytes",
|
||||
"fragment_count",
|
||||
"total_radio_bytes",
|
||||
"efficiency_percent",
|
||||
"time_5_kbps",
|
||||
"time_10_kbps",
|
||||
"time_20_kbps",
|
||||
"time_50_kbps",
|
||||
"time_100_kbps",
|
||||
"time_20_kbps_with_retries_x2",
|
||||
]
|
||||
|
||||
with csv_path.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=csv_fieldnames,
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for result in results:
|
||||
|
||||
writer.writerow(
|
||||
{
|
||||
"name": result["name"],
|
||||
"path": result["path"],
|
||||
"width": result["width"],
|
||||
"height": result["height"],
|
||||
"mode": result["mode"],
|
||||
"file_size_bytes": (
|
||||
result["file_size_bytes"]
|
||||
),
|
||||
"fragment_count": (
|
||||
result["fragment_count"]
|
||||
),
|
||||
"total_radio_bytes": (
|
||||
result["total_radio_bytes"]
|
||||
),
|
||||
"efficiency_percent": (
|
||||
f"{result['efficiency_percent']:.2f}"
|
||||
),
|
||||
"time_5_kbps": (
|
||||
result["transfer_times"][5]
|
||||
),
|
||||
"time_10_kbps": (
|
||||
result["transfer_times"][10]
|
||||
),
|
||||
"time_20_kbps": (
|
||||
result["transfer_times"][20]
|
||||
),
|
||||
"time_50_kbps": (
|
||||
result["transfer_times"][50]
|
||||
),
|
||||
"time_100_kbps": (
|
||||
result["transfer_times"][100]
|
||||
),
|
||||
"time_20_kbps_with_retries_x2": (
|
||||
result["transfer_times"][20]
|
||||
* 2
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab010. Оптимизация изображения ==="
|
||||
)
|
||||
|
||||
print("\nИсходное разрешение:")
|
||||
|
||||
print(
|
||||
original_width,
|
||||
"x",
|
||||
original_height,
|
||||
)
|
||||
|
||||
|
||||
print("\nСравнение вариантов:")
|
||||
|
||||
print(
|
||||
f"{'Вариант':<28}"
|
||||
f"{'Размер':>12}"
|
||||
f"{'Фрагм.':>9}"
|
||||
f"{'10 кбит/с':>13}"
|
||||
f"{'20 кбит/с':>13}"
|
||||
f"{'50 кбит/с':>13}"
|
||||
f"{'20 кбит/с x2':>16}"
|
||||
)
|
||||
|
||||
print("-" * 104)
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['name']:<28}"
|
||||
f"{format_bytes(result['file_size_bytes']):>12}"
|
||||
f"{result['fragment_count']:>9}"
|
||||
f"{format_duration(result['transfer_times'][10]):>13}"
|
||||
f"{format_duration(result['transfer_times'][20]):>13}"
|
||||
f"{format_duration(result['transfer_times'][50]):>13}"
|
||||
f"{format_duration(result['transfer_times'][20] * 2):>16}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Итоговые кандидаты
|
||||
# ============================================================
|
||||
|
||||
smallest_result = min(
|
||||
results,
|
||||
key=lambda item: item["file_size_bytes"],
|
||||
)
|
||||
|
||||
operator_candidate = next(
|
||||
result
|
||||
for result in results
|
||||
if result["name"] == "03_gray_320_q30"
|
||||
)
|
||||
|
||||
|
||||
print("\nСамый маленький файл:")
|
||||
|
||||
print(
|
||||
smallest_result["name"],
|
||||
"-",
|
||||
format_bytes(
|
||||
smallest_result["file_size_bytes"]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
print("\nБазовый кандидат для оператора:")
|
||||
|
||||
print(
|
||||
operator_candidate["name"],
|
||||
"-",
|
||||
format_bytes(
|
||||
operator_candidate["file_size_bytes"]
|
||||
),
|
||||
"-",
|
||||
format_duration(
|
||||
operator_candidate[
|
||||
"transfer_times"
|
||||
][20]
|
||||
),
|
||||
"при 20 кбит/с без повторов",
|
||||
)
|
||||
|
||||
|
||||
print("\nСравнительная картинка:")
|
||||
|
||||
print(contact_sheet_path)
|
||||
|
||||
|
||||
print("\nТаблица результатов CSV:")
|
||||
|
||||
print(csv_path)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки
|
||||
# ============================================================
|
||||
|
||||
assert len(results) == 7
|
||||
|
||||
assert contact_sheet_path.exists()
|
||||
|
||||
assert csv_path.exists()
|
||||
|
||||
assert all(
|
||||
result["fragment_count"] > 0
|
||||
for result in results
|
||||
)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"варианты кадра созданы и рассчитаны."
|
||||
)
|
||||
525
Projects/SDR_Rover/tests/lab011_scene_modes.py
Normal file
525
Projects/SDR_Rover/tests/lab011_scene_modes.py
Normal file
@@ -0,0 +1,525 @@
|
||||
"""
|
||||
Lab011. Проверка облегчённых режимов изображения
|
||||
на сцене, похожей на вид с камеры ровера.
|
||||
|
||||
Создаются варианты:
|
||||
1. Grayscale 320x240, JPEG quality 30.
|
||||
2. Grayscale 320x240, JPEG quality 15.
|
||||
3. Grayscale 320x240 с наложением контуров, quality 30.
|
||||
|
||||
Для каждого варианта рассчитываются:
|
||||
- размер JPEG;
|
||||
- количество радиопакетов;
|
||||
- время передачи при разных скоростях.
|
||||
"""
|
||||
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageChops,
|
||||
ImageDraw,
|
||||
ImageFilter,
|
||||
ImageOps,
|
||||
)
|
||||
|
||||
from protocol.image_fragments import (
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
SOURCE_PATH = Path(
|
||||
"data/raw/lab011_scene.jpg"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab011"
|
||||
)
|
||||
|
||||
FRAME_SIZE = (320, 240)
|
||||
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
IMAGE_ID_BASE = 2026071400
|
||||
|
||||
BITRATES_KBPS = [
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def prepare_frame(
|
||||
image: Image.Image,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Привести изображение к фиксированному кадру 320x240.
|
||||
|
||||
Пропорции сохраняются.
|
||||
Недостающие области заполняются чёрным.
|
||||
"""
|
||||
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = image.convert("RGB")
|
||||
|
||||
return ImageOps.pad(
|
||||
image,
|
||||
FRAME_SIZE,
|
||||
method=Image.Resampling.LANCZOS,
|
||||
color="black",
|
||||
centering=(0.5, 0.5),
|
||||
)
|
||||
|
||||
|
||||
def save_jpeg(
|
||||
image: Image.Image,
|
||||
path: Path,
|
||||
quality: int,
|
||||
) -> None:
|
||||
"""
|
||||
Сохранить изображение в JPEG.
|
||||
"""
|
||||
|
||||
path.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
image.save(
|
||||
path,
|
||||
format="JPEG",
|
||||
quality=quality,
|
||||
optimize=True,
|
||||
)
|
||||
|
||||
|
||||
def format_size(
|
||||
byte_count: int,
|
||||
) -> str:
|
||||
"""
|
||||
Представить размер в КиБ.
|
||||
"""
|
||||
|
||||
return f"{byte_count / 1024:.2f} КиБ"
|
||||
|
||||
|
||||
def format_duration(
|
||||
seconds: float,
|
||||
) -> str:
|
||||
"""
|
||||
Представить длительность передачи.
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.0f} мс"
|
||||
|
||||
if seconds < 60:
|
||||
return f"{seconds:.2f} с"
|
||||
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = seconds % 60
|
||||
|
||||
return (
|
||||
f"{minutes} мин "
|
||||
f"{remaining_seconds:.1f} с"
|
||||
)
|
||||
|
||||
|
||||
def estimate_transfer(
|
||||
file_path: Path,
|
||||
image_id: int,
|
||||
) -> dict:
|
||||
"""
|
||||
Рассчитать полный объём DATA + ACK.
|
||||
"""
|
||||
|
||||
image_bytes = file_path.read_bytes()
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=image_bytes,
|
||||
image_id=image_id,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
data_packet_bytes = 0
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment.fragment_index,
|
||||
)
|
||||
|
||||
data_packet_bytes += len(packet)
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
ack_bytes = (
|
||||
len(fragments)
|
||||
* len(ack_packet)
|
||||
)
|
||||
|
||||
total_radio_bytes = (
|
||||
data_packet_bytes
|
||||
+ ack_bytes
|
||||
)
|
||||
|
||||
times = {}
|
||||
|
||||
for bitrate_kbps in BITRATES_KBPS:
|
||||
|
||||
times[bitrate_kbps] = (
|
||||
total_radio_bytes
|
||||
* 8
|
||||
/ (bitrate_kbps * 1000)
|
||||
)
|
||||
|
||||
return {
|
||||
"file_size": len(image_bytes),
|
||||
"fragment_count": len(fragments),
|
||||
"total_radio_bytes": total_radio_bytes,
|
||||
"times": times,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исходного файла
|
||||
# ============================================================
|
||||
|
||||
if not SOURCE_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Не найден файл: {SOURCE_PATH}"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Подготовка базового кадра
|
||||
# ============================================================
|
||||
|
||||
with Image.open(SOURCE_PATH) as source_image:
|
||||
|
||||
original_size = source_image.size
|
||||
|
||||
color_frame = prepare_frame(
|
||||
source_image
|
||||
)
|
||||
|
||||
gray_frame = ImageOps.grayscale(
|
||||
color_frame
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вариант 1. Grayscale, quality 30
|
||||
# ============================================================
|
||||
|
||||
gray_q30_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "01_gray_320_q30.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_frame,
|
||||
gray_q30_path,
|
||||
quality=30,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вариант 2. Grayscale, quality 15
|
||||
# ============================================================
|
||||
|
||||
gray_q15_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "02_gray_320_q15.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_frame,
|
||||
gray_q15_path,
|
||||
quality=15,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вариант 3. Grayscale + контуры
|
||||
# ============================================================
|
||||
|
||||
edge_map = gray_frame.filter(
|
||||
ImageFilter.FIND_EDGES
|
||||
)
|
||||
|
||||
edge_map = ImageOps.autocontrast(
|
||||
edge_map
|
||||
)
|
||||
|
||||
# Оставляем преимущественно сильные контуры.
|
||||
binary_edges = edge_map.point(
|
||||
lambda value: 255 if value >= 45 else 0
|
||||
)
|
||||
|
||||
# После инверсии контуры становятся чёрными,
|
||||
# а фон — белым.
|
||||
dark_edges = ImageOps.invert(
|
||||
binary_edges
|
||||
)
|
||||
|
||||
# Сохраняем исходный серый фон и добавляем
|
||||
# поверх него тёмные линии контуров.
|
||||
gray_with_edges = ImageChops.darker(
|
||||
gray_frame,
|
||||
dark_edges
|
||||
)
|
||||
|
||||
gray_edges_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "03_gray_edges_320_q30.jpg"
|
||||
)
|
||||
|
||||
save_jpeg(
|
||||
gray_with_edges,
|
||||
gray_edges_path,
|
||||
quality=30,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Список вариантов
|
||||
# ============================================================
|
||||
|
||||
variants = [
|
||||
(
|
||||
"gray_320_q30",
|
||||
gray_q30_path,
|
||||
),
|
||||
(
|
||||
"gray_320_q15",
|
||||
gray_q15_path,
|
||||
),
|
||||
(
|
||||
"gray_edges_320_q30",
|
||||
gray_edges_path,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт параметров
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for variant_index, (
|
||||
variant_name,
|
||||
variant_path,
|
||||
) in enumerate(variants):
|
||||
|
||||
result = estimate_transfer(
|
||||
file_path=variant_path,
|
||||
image_id=IMAGE_ID_BASE + variant_index,
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"name": variant_name,
|
||||
"path": variant_path,
|
||||
**result,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Создание сравнительной картинки
|
||||
# ============================================================
|
||||
|
||||
preview_variants = [
|
||||
(
|
||||
"SOURCE PREVIEW",
|
||||
color_frame,
|
||||
None,
|
||||
)
|
||||
]
|
||||
|
||||
for result in results:
|
||||
|
||||
with Image.open(result["path"]) as image:
|
||||
|
||||
preview_variants.append(
|
||||
(
|
||||
result["name"],
|
||||
image.convert("RGB").copy(),
|
||||
result,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
columns = 2
|
||||
cell_width = 440
|
||||
cell_height = 340
|
||||
|
||||
rows = ceil(
|
||||
len(preview_variants)
|
||||
/ columns
|
||||
)
|
||||
|
||||
comparison_image = Image.new(
|
||||
"RGB",
|
||||
(
|
||||
columns * cell_width,
|
||||
rows * cell_height,
|
||||
),
|
||||
"white",
|
||||
)
|
||||
|
||||
draw = ImageDraw.Draw(
|
||||
comparison_image
|
||||
)
|
||||
|
||||
for index, (
|
||||
label,
|
||||
preview,
|
||||
result,
|
||||
) in enumerate(preview_variants):
|
||||
|
||||
column = index % columns
|
||||
row = index // columns
|
||||
|
||||
x = column * cell_width
|
||||
y = row * cell_height
|
||||
|
||||
preview = preview.copy()
|
||||
|
||||
preview.thumbnail(
|
||||
(400, 270),
|
||||
Image.Resampling.NEAREST,
|
||||
)
|
||||
|
||||
paste_x = (
|
||||
x
|
||||
+ (cell_width - preview.width) // 2
|
||||
)
|
||||
|
||||
comparison_image.paste(
|
||||
preview,
|
||||
(paste_x, y + 55),
|
||||
)
|
||||
|
||||
if result is None:
|
||||
|
||||
text = (
|
||||
f"{label}\n"
|
||||
f"original: "
|
||||
f"{original_size[0]}x{original_size[1]}"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
text = (
|
||||
f"{label}\n"
|
||||
f"{format_size(result['file_size'])}, "
|
||||
f"{result['fragment_count']} fragments, "
|
||||
f"{format_duration(result['times'][20])} "
|
||||
f"at 20 kbps"
|
||||
)
|
||||
|
||||
draw.multiline_text(
|
||||
(x + 10, y + 10),
|
||||
text,
|
||||
fill="black",
|
||||
spacing=4,
|
||||
)
|
||||
|
||||
|
||||
comparison_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab011_comparison.jpg"
|
||||
)
|
||||
|
||||
comparison_image.save(
|
||||
comparison_path,
|
||||
format="JPEG",
|
||||
quality=90,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab011. Режимы кадра для ровера ==="
|
||||
)
|
||||
|
||||
print("\nИсходное разрешение:")
|
||||
|
||||
print(
|
||||
original_size[0],
|
||||
"x",
|
||||
original_size[1],
|
||||
)
|
||||
|
||||
print("\nРезультаты:")
|
||||
|
||||
print(
|
||||
f"{'Вариант':<27}"
|
||||
f"{'Размер':>12}"
|
||||
f"{'Пакеты':>10}"
|
||||
f"{'10 кбит/с':>13}"
|
||||
f"{'20 кбит/с':>13}"
|
||||
f"{'50 кбит/с':>13}"
|
||||
)
|
||||
|
||||
print("-" * 88)
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['name']:<27}"
|
||||
f"{format_size(result['file_size']):>12}"
|
||||
f"{result['fragment_count']:>10}"
|
||||
f"{format_duration(result['times'][10]):>13}"
|
||||
f"{format_duration(result['times'][20]):>13}"
|
||||
f"{format_duration(result['times'][50]):>13}"
|
||||
)
|
||||
|
||||
|
||||
print("\nСравнительная картинка:")
|
||||
|
||||
print(comparison_path)
|
||||
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"режимы кадра созданы и рассчитаны."
|
||||
)
|
||||
558
Projects/SDR_Rover/tests/lab012_color_vs_gray.py
Normal file
558
Projects/SDR_Rover/tests/lab012_color_vs_gray.py
Normal file
@@ -0,0 +1,558 @@
|
||||
"""
|
||||
Lab012. Сравнение цветного и серого кадров
|
||||
для обнаружения человека и препятствий.
|
||||
|
||||
Создаются четыре варианта:
|
||||
|
||||
1. Цветной 320x240, JPEG quality 30.
|
||||
2. Серый 320x240, JPEG quality 30.
|
||||
3. Цветной 320x240, JPEG quality 15.
|
||||
4. Серый 320x240, JPEG quality 15.
|
||||
|
||||
Для каждого варианта рассчитываются:
|
||||
|
||||
- размер JPEG;
|
||||
- количество фрагментов;
|
||||
- полный объём DATA + ACK;
|
||||
- время передачи;
|
||||
- стоимость сохранения цвета.
|
||||
"""
|
||||
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageOps,
|
||||
)
|
||||
|
||||
from protocol.image_fragments import (
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
SOURCE_PATH = Path(
|
||||
"data/raw/lab012_safety_scene.jpg"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab012"
|
||||
)
|
||||
|
||||
FRAME_SIZE = (320, 240)
|
||||
|
||||
FRAGMENT_DATA_SIZE = 512
|
||||
|
||||
IMAGE_ID_BASE = 2026071500
|
||||
|
||||
BITRATES_KBPS = [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def prepare_frame(
|
||||
image: Image.Image,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Подготовить кадр фиксированного размера 320x240.
|
||||
|
||||
Изображение:
|
||||
- автоматически поворачивается по EXIF;
|
||||
- переводится в RGB;
|
||||
- масштабируется с сохранением пропорций;
|
||||
- при необходимости дополняется чёрными полями.
|
||||
"""
|
||||
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = image.convert("RGB")
|
||||
|
||||
return ImageOps.pad(
|
||||
image,
|
||||
FRAME_SIZE,
|
||||
method=Image.Resampling.LANCZOS,
|
||||
color="black",
|
||||
centering=(0.5, 0.5),
|
||||
)
|
||||
|
||||
|
||||
def save_jpeg(
|
||||
image: Image.Image,
|
||||
output_path: Path,
|
||||
quality: int,
|
||||
) -> None:
|
||||
"""
|
||||
Сохранить изображение в JPEG.
|
||||
"""
|
||||
|
||||
output_path.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
image.save(
|
||||
output_path,
|
||||
format="JPEG",
|
||||
quality=quality,
|
||||
optimize=True,
|
||||
)
|
||||
|
||||
|
||||
def format_size(
|
||||
byte_count: int,
|
||||
) -> str:
|
||||
"""
|
||||
Представить размер файла в КиБ.
|
||||
"""
|
||||
|
||||
return f"{byte_count / 1024:.2f} КиБ"
|
||||
|
||||
|
||||
def format_duration(
|
||||
seconds: float,
|
||||
) -> str:
|
||||
"""
|
||||
Представить время передачи.
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.0f} мс"
|
||||
|
||||
if seconds < 60:
|
||||
return f"{seconds:.2f} с"
|
||||
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = seconds % 60
|
||||
|
||||
return (
|
||||
f"{minutes} мин "
|
||||
f"{remaining_seconds:.1f} с"
|
||||
)
|
||||
|
||||
|
||||
def estimate_transfer(
|
||||
file_path: Path,
|
||||
image_id: int,
|
||||
) -> dict:
|
||||
"""
|
||||
Рассчитать параметры передачи JPEG
|
||||
с использованием текущего протокола.
|
||||
"""
|
||||
|
||||
image_bytes = file_path.read_bytes()
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=image_bytes,
|
||||
image_id=image_id,
|
||||
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||
)
|
||||
|
||||
total_data_bytes = 0
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
sequence_number=fragment.fragment_index,
|
||||
)
|
||||
|
||||
total_data_bytes += len(packet)
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
total_ack_bytes = (
|
||||
len(fragments)
|
||||
* len(ack_packet)
|
||||
)
|
||||
|
||||
total_radio_bytes = (
|
||||
total_data_bytes
|
||||
+ total_ack_bytes
|
||||
)
|
||||
|
||||
transfer_times = {}
|
||||
|
||||
for bitrate_kbps in BITRATES_KBPS:
|
||||
|
||||
transfer_times[bitrate_kbps] = (
|
||||
total_radio_bytes
|
||||
* 8
|
||||
/ (bitrate_kbps * 1000)
|
||||
)
|
||||
|
||||
return {
|
||||
"file_size": len(image_bytes),
|
||||
"fragment_count": len(fragments),
|
||||
"total_data_bytes": total_data_bytes,
|
||||
"total_ack_bytes": total_ack_bytes,
|
||||
"total_radio_bytes": total_radio_bytes,
|
||||
"times": transfer_times,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исходного файла
|
||||
# ============================================================
|
||||
|
||||
if not SOURCE_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Не найден файл: {SOURCE_PATH}"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Подготовка базового кадра
|
||||
# ============================================================
|
||||
|
||||
with Image.open(SOURCE_PATH) as source_image:
|
||||
|
||||
original_size = source_image.size
|
||||
|
||||
color_frame = prepare_frame(
|
||||
source_image
|
||||
)
|
||||
|
||||
gray_frame = ImageOps.grayscale(
|
||||
color_frame
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Создание вариантов
|
||||
# ============================================================
|
||||
|
||||
variant_definitions = [
|
||||
{
|
||||
"name": "color_320_q30",
|
||||
"image": color_frame,
|
||||
"quality": 30,
|
||||
"path": (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "01_color_320_q30.jpg"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "gray_320_q30",
|
||||
"image": gray_frame,
|
||||
"quality": 30,
|
||||
"path": (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "02_gray_320_q30.jpg"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "color_320_q15",
|
||||
"image": color_frame,
|
||||
"quality": 15,
|
||||
"path": (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "03_color_320_q15.jpg"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "gray_320_q15",
|
||||
"image": gray_frame,
|
||||
"quality": 15,
|
||||
"path": (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "04_gray_320_q15.jpg"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for variant in variant_definitions:
|
||||
|
||||
save_jpeg(
|
||||
image=variant["image"],
|
||||
output_path=variant["path"],
|
||||
quality=variant["quality"],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт параметров передачи
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for variant_index, variant in enumerate(
|
||||
variant_definitions
|
||||
):
|
||||
|
||||
transfer_result = estimate_transfer(
|
||||
file_path=variant["path"],
|
||||
image_id=IMAGE_ID_BASE + variant_index,
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"name": variant["name"],
|
||||
"path": variant["path"],
|
||||
"quality": variant["quality"],
|
||||
**transfer_result,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Создание сравнительной картинки
|
||||
# ============================================================
|
||||
|
||||
columns = 2
|
||||
cell_width = 440
|
||||
cell_height = 340
|
||||
|
||||
rows = ceil(
|
||||
len(results)
|
||||
/ columns
|
||||
)
|
||||
|
||||
comparison_image = Image.new(
|
||||
"RGB",
|
||||
(
|
||||
columns * cell_width,
|
||||
rows * cell_height,
|
||||
),
|
||||
"white",
|
||||
)
|
||||
|
||||
draw = ImageDraw.Draw(
|
||||
comparison_image
|
||||
)
|
||||
|
||||
|
||||
for result_index, result in enumerate(results):
|
||||
|
||||
column = result_index % columns
|
||||
row = result_index // columns
|
||||
|
||||
cell_x = column * cell_width
|
||||
cell_y = row * cell_height
|
||||
|
||||
with Image.open(result["path"]) as image:
|
||||
|
||||
preview = image.convert("RGB")
|
||||
|
||||
# NEAREST не сглаживает пиксели при увеличении.
|
||||
# Поэтому дефекты сжатия будут хорошо видны.
|
||||
preview = preview.resize(
|
||||
(400, 300),
|
||||
Image.Resampling.NEAREST,
|
||||
)
|
||||
|
||||
paste_x = (
|
||||
cell_x
|
||||
+ (cell_width - preview.width) // 2
|
||||
)
|
||||
|
||||
paste_y = cell_y + 35
|
||||
|
||||
comparison_image.paste(
|
||||
preview,
|
||||
(paste_x, paste_y),
|
||||
)
|
||||
|
||||
label = (
|
||||
f"{result['name']}\n"
|
||||
f"{format_size(result['file_size'])}, "
|
||||
f"{result['fragment_count']} fragments, "
|
||||
f"{format_duration(result['times'][20])} "
|
||||
f"at 20 kbps"
|
||||
)
|
||||
|
||||
draw.multiline_text(
|
||||
(cell_x + 10, cell_y + 5),
|
||||
label,
|
||||
fill="black",
|
||||
spacing=3,
|
||||
)
|
||||
|
||||
|
||||
comparison_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab012_comparison.jpg"
|
||||
)
|
||||
|
||||
comparison_image.save(
|
||||
comparison_path,
|
||||
format="JPEG",
|
||||
quality=92,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Поиск результатов по имени
|
||||
# ============================================================
|
||||
|
||||
results_by_name = {
|
||||
result["name"]: result
|
||||
for result in results
|
||||
}
|
||||
|
||||
color_q30 = results_by_name[
|
||||
"color_320_q30"
|
||||
]
|
||||
|
||||
gray_q30 = results_by_name[
|
||||
"gray_320_q30"
|
||||
]
|
||||
|
||||
color_q15 = results_by_name[
|
||||
"color_320_q15"
|
||||
]
|
||||
|
||||
gray_q15 = results_by_name[
|
||||
"gray_320_q15"
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Расчёт цены сохранения цвета
|
||||
# ============================================================
|
||||
|
||||
color_overhead_q30_bytes = (
|
||||
color_q30["file_size"]
|
||||
- gray_q30["file_size"]
|
||||
)
|
||||
|
||||
color_overhead_q30_percent = (
|
||||
color_overhead_q30_bytes
|
||||
/ gray_q30["file_size"]
|
||||
* 100
|
||||
)
|
||||
|
||||
color_overhead_q15_bytes = (
|
||||
color_q15["file_size"]
|
||||
- gray_q15["file_size"]
|
||||
)
|
||||
|
||||
color_overhead_q15_percent = (
|
||||
color_overhead_q15_bytes
|
||||
/ gray_q15["file_size"]
|
||||
* 100
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab012. Цветной или серый кадр ==="
|
||||
)
|
||||
|
||||
print("\nИсходное разрешение:")
|
||||
|
||||
print(
|
||||
original_size[0],
|
||||
"x",
|
||||
original_size[1],
|
||||
)
|
||||
|
||||
|
||||
print("\nСравнение вариантов:")
|
||||
|
||||
print(
|
||||
f"{'Вариант':<22}"
|
||||
f"{'Размер':>12}"
|
||||
f"{'Пакеты':>10}"
|
||||
f"{'10 кбит/с':>13}"
|
||||
f"{'20 кбит/с':>13}"
|
||||
f"{'50 кбит/с':>13}"
|
||||
)
|
||||
|
||||
print("-" * 83)
|
||||
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['name']:<22}"
|
||||
f"{format_size(result['file_size']):>12}"
|
||||
f"{result['fragment_count']:>10}"
|
||||
f"{format_duration(result['times'][10]):>13}"
|
||||
f"{format_duration(result['times'][20]):>13}"
|
||||
f"{format_duration(result['times'][50]):>13}"
|
||||
)
|
||||
|
||||
|
||||
print("\nЦена сохранения цвета при quality 30:")
|
||||
|
||||
print(
|
||||
format_size(
|
||||
color_overhead_q30_bytes
|
||||
),
|
||||
f"или {color_overhead_q30_percent:.1f} %",
|
||||
)
|
||||
|
||||
|
||||
print("\nЦена сохранения цвета при quality 15:")
|
||||
|
||||
print(
|
||||
format_size(
|
||||
color_overhead_q15_bytes
|
||||
),
|
||||
f"или {color_overhead_q15_percent:.1f} %",
|
||||
)
|
||||
|
||||
|
||||
print("\nСравнительная картинка:")
|
||||
|
||||
print(comparison_path)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки
|
||||
# ============================================================
|
||||
|
||||
assert len(results) == 4
|
||||
|
||||
assert comparison_path.exists()
|
||||
|
||||
assert all(
|
||||
result["fragment_count"] > 0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert color_q30["file_size"] > 0
|
||||
assert gray_q30["file_size"] > 0
|
||||
assert color_q15["file_size"] > 0
|
||||
assert gray_q15["file_size"] > 0
|
||||
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"цветные и серые варианты созданы и рассчитаны."
|
||||
)
|
||||
528
Projects/SDR_Rover/tests/lab013_bpsk_python.py
Normal file
528
Projects/SDR_Rover/tests/lab013_bpsk_python.py
Normal file
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
Lab013. Первая BPSK-передача цифрового пакета в Python.
|
||||
|
||||
Программа:
|
||||
1. Формирует пакет SDR Rover Link с CRC-32.
|
||||
2. Преобразует байты пакета в отдельные биты.
|
||||
3. Преобразует биты в BPSK-символы.
|
||||
4. Представляет символы как комплексные IQ-сэмплы.
|
||||
5. Добавляет комплексный белый гауссов шум.
|
||||
6. Демодулирует BPSK.
|
||||
7. Восстанавливает байты пакета.
|
||||
8. Проверяет пакет и CRC.
|
||||
9. Сравнивает результат при разных уровнях SNR.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from protocol.packet import (
|
||||
CRCError,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
PacketError,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
MESSAGE = "ПРИВЕТ SDR"
|
||||
|
||||
SEQUENCE_NUMBER = 13
|
||||
|
||||
# Проверим несколько уровней отношения сигнал/шум.
|
||||
SNR_VALUES_DB = [
|
||||
12.0,
|
||||
6.0,
|
||||
2.0,
|
||||
0.0,
|
||||
]
|
||||
|
||||
# Фиксированное значение обеспечивает повторяемость.
|
||||
RANDOM_SEED = 2026
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab013"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Преобразование байтов в биты
|
||||
# ============================================================
|
||||
|
||||
def bytes_to_bits(
|
||||
data: bytes,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Преобразовать последовательность bytes
|
||||
в массив отдельных битов 0 и 1.
|
||||
|
||||
Каждый исходный байт превращается в восемь битов.
|
||||
"""
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError(
|
||||
"data должен иметь тип bytes"
|
||||
)
|
||||
|
||||
byte_array = np.frombuffer(
|
||||
data,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
bits = np.unpackbits(
|
||||
byte_array
|
||||
)
|
||||
|
||||
return bits
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Преобразование битов обратно в байты
|
||||
# ============================================================
|
||||
|
||||
def bits_to_bytes(
|
||||
bits: np.ndarray,
|
||||
) -> bytes:
|
||||
"""
|
||||
Упаковать отдельные биты обратно в bytes.
|
||||
"""
|
||||
|
||||
bits = np.asarray(
|
||||
bits,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
if bits.ndim != 1:
|
||||
raise ValueError(
|
||||
"bits должен быть одномерным массивом"
|
||||
)
|
||||
|
||||
if len(bits) % 8 != 0:
|
||||
raise ValueError(
|
||||
"Количество битов должно быть кратно восьми"
|
||||
)
|
||||
|
||||
if not np.all(
|
||||
(bits == 0) | (bits == 1)
|
||||
):
|
||||
raise ValueError(
|
||||
"Массив должен содержать только 0 и 1"
|
||||
)
|
||||
|
||||
packed_bytes = np.packbits(
|
||||
bits
|
||||
)
|
||||
|
||||
return packed_bytes.tobytes()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# BPSK-модулятор
|
||||
# ============================================================
|
||||
|
||||
def bpsk_modulate(
|
||||
bits: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Преобразовать биты в комплексные BPSK-символы.
|
||||
|
||||
Используем отображение:
|
||||
|
||||
0 → -1 + 0j
|
||||
1 → +1 + 0j
|
||||
"""
|
||||
|
||||
bits = np.asarray(
|
||||
bits,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
real_symbols = (
|
||||
2.0 * bits.astype(np.float64)
|
||||
- 1.0
|
||||
)
|
||||
|
||||
iq_symbols = real_symbols.astype(
|
||||
np.complex128
|
||||
)
|
||||
|
||||
return iq_symbols
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Модель радиошума
|
||||
# ============================================================
|
||||
|
||||
def add_awgn(
|
||||
iq_samples: np.ndarray,
|
||||
snr_db: float,
|
||||
random_generator: np.random.Generator,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Добавить комплексный белый гауссов шум AWGN.
|
||||
|
||||
AWGN:
|
||||
Additive White Gaussian Noise —
|
||||
аддитивный белый гауссов шум.
|
||||
|
||||
snr_db:
|
||||
Отношение средней мощности сигнала
|
||||
к средней мощности шума в децибелах.
|
||||
"""
|
||||
|
||||
signal_power = np.mean(
|
||||
np.abs(iq_samples) ** 2
|
||||
)
|
||||
|
||||
snr_linear = 10.0 ** (
|
||||
snr_db / 10.0
|
||||
)
|
||||
|
||||
noise_power = (
|
||||
signal_power / snr_linear
|
||||
)
|
||||
|
||||
# Комплексный шум имеет две составляющие:
|
||||
# действительную I и мнимую Q.
|
||||
#
|
||||
# Поэтому мощность делится между ними пополам.
|
||||
noise_sigma = np.sqrt(
|
||||
noise_power / 2.0
|
||||
)
|
||||
|
||||
noise = noise_sigma * (
|
||||
random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
+ 1j
|
||||
* random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
)
|
||||
|
||||
return iq_samples + noise
|
||||
|
||||
|
||||
# ============================================================
|
||||
# BPSK-демодулятор
|
||||
# ============================================================
|
||||
|
||||
def bpsk_demodulate(
|
||||
received_iq: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Преобразовать принятые IQ-сэмплы в биты.
|
||||
|
||||
Правило решения:
|
||||
|
||||
I < 0 → бит 0
|
||||
I >= 0 → бит 1
|
||||
"""
|
||||
|
||||
received_bits = (
|
||||
received_iq.real >= 0.0
|
||||
).astype(np.uint8)
|
||||
|
||||
return received_bits
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Формирование исходного пакета
|
||||
# ============================================================
|
||||
|
||||
payload = MESSAGE.encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
original_packet = build_packet(
|
||||
payload=payload,
|
||||
message_type=MESSAGE_TYPE_TEXT,
|
||||
sequence_number=SEQUENCE_NUMBER,
|
||||
)
|
||||
|
||||
transmitted_bits = bytes_to_bits(
|
||||
original_packet
|
||||
)
|
||||
|
||||
transmitted_iq = bpsk_modulate(
|
||||
transmitted_bits
|
||||
)
|
||||
|
||||
|
||||
print(
|
||||
"=== Lab013. BPSK в Python ==="
|
||||
)
|
||||
|
||||
print("\nИсходное сообщение:")
|
||||
print(MESSAGE)
|
||||
|
||||
print("\nРазмер пакета:")
|
||||
print(
|
||||
len(original_packet),
|
||||
"байт",
|
||||
)
|
||||
|
||||
print("\nКоличество передаваемых битов:")
|
||||
print(
|
||||
len(transmitted_bits)
|
||||
)
|
||||
|
||||
print("\nПервые 32 бита:")
|
||||
|
||||
print(
|
||||
" ".join(
|
||||
str(bit)
|
||||
for bit in transmitted_bits[:32]
|
||||
)
|
||||
)
|
||||
|
||||
print("\nПервые 16 BPSK-символов:")
|
||||
|
||||
print(
|
||||
transmitted_iq[:16]
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Передача при разных уровнях SNR
|
||||
# ============================================================
|
||||
|
||||
experiment_results = []
|
||||
|
||||
constellation_samples = {}
|
||||
|
||||
for experiment_index, snr_db in enumerate(
|
||||
SNR_VALUES_DB
|
||||
):
|
||||
|
||||
random_generator = np.random.default_rng(
|
||||
RANDOM_SEED + experiment_index
|
||||
)
|
||||
|
||||
received_iq = add_awgn(
|
||||
iq_samples=transmitted_iq,
|
||||
snr_db=snr_db,
|
||||
random_generator=random_generator,
|
||||
)
|
||||
|
||||
received_bits = bpsk_demodulate(
|
||||
received_iq
|
||||
)
|
||||
|
||||
bit_error_count = int(
|
||||
np.count_nonzero(
|
||||
transmitted_bits != received_bits
|
||||
)
|
||||
)
|
||||
|
||||
bit_error_rate = (
|
||||
bit_error_count
|
||||
/ len(transmitted_bits)
|
||||
)
|
||||
|
||||
received_packet = bits_to_bytes(
|
||||
received_bits
|
||||
)
|
||||
|
||||
packet_status = "CRC OK"
|
||||
restored_message = None
|
||||
|
||||
try:
|
||||
parsed_packet = parse_packet(
|
||||
received_packet
|
||||
)
|
||||
|
||||
restored_message = (
|
||||
parsed_packet.payload.decode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
except CRCError:
|
||||
packet_status = "CRC ERROR"
|
||||
|
||||
except PacketError:
|
||||
packet_status = "PACKET ERROR"
|
||||
|
||||
except UnicodeDecodeError:
|
||||
packet_status = "UTF-8 ERROR"
|
||||
|
||||
experiment_results.append(
|
||||
{
|
||||
"snr_db": snr_db,
|
||||
"bit_errors": bit_error_count,
|
||||
"ber": bit_error_rate,
|
||||
"packet_status": packet_status,
|
||||
"restored_message": restored_message,
|
||||
}
|
||||
)
|
||||
|
||||
# Для графика достаточно первых 300 символов.
|
||||
constellation_samples[snr_db] = (
|
||||
received_iq[:300]
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print("\nРезультаты передачи:")
|
||||
|
||||
print(
|
||||
f"{'SNR':>8}"
|
||||
f"{'Ошибки битов':>16}"
|
||||
f"{'BER':>14}"
|
||||
f"{'Пакет':>18}"
|
||||
)
|
||||
|
||||
print("-" * 56)
|
||||
|
||||
for result in experiment_results:
|
||||
|
||||
print(
|
||||
f"{result['snr_db']:>6.1f} дБ"
|
||||
f"{result['bit_errors']:>16}"
|
||||
f"{result['ber']:>14.6f}"
|
||||
f"{result['packet_status']:>18}"
|
||||
)
|
||||
|
||||
|
||||
print("\nВосстановленные сообщения:")
|
||||
|
||||
for result in experiment_results:
|
||||
|
||||
print(
|
||||
f"SNR {result['snr_db']:>4.1f} дБ: "
|
||||
f"{result['restored_message']}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График созвездия
|
||||
# ============================================================
|
||||
|
||||
figure, axes = plt.subplots(
|
||||
2,
|
||||
2,
|
||||
figsize=(10, 8),
|
||||
)
|
||||
|
||||
axes = axes.ravel()
|
||||
|
||||
for axis, snr_db in zip(
|
||||
axes,
|
||||
SNR_VALUES_DB,
|
||||
):
|
||||
|
||||
received_iq = constellation_samples[
|
||||
snr_db
|
||||
]
|
||||
|
||||
axis.scatter(
|
||||
received_iq.real,
|
||||
received_iq.imag,
|
||||
s=12,
|
||||
alpha=0.6,
|
||||
)
|
||||
|
||||
axis.axvline(
|
||||
0.0,
|
||||
linewidth=1,
|
||||
)
|
||||
|
||||
axis.set_title(
|
||||
f"BPSK, SNR = {snr_db:.1f} дБ"
|
||||
)
|
||||
|
||||
axis.set_xlabel(
|
||||
"I — синфазная компонента"
|
||||
)
|
||||
|
||||
axis.set_ylabel(
|
||||
"Q — квадратурная компонента"
|
||||
)
|
||||
|
||||
axis.set_xlim(
|
||||
-2.5,
|
||||
2.5,
|
||||
)
|
||||
|
||||
axis.set_ylim(
|
||||
-1.8,
|
||||
1.8,
|
||||
)
|
||||
|
||||
axis.grid(
|
||||
True
|
||||
)
|
||||
|
||||
figure.tight_layout()
|
||||
|
||||
constellation_path = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab013_bpsk_constellation.png"
|
||||
)
|
||||
|
||||
figure.savefig(
|
||||
constellation_path,
|
||||
dpi=150,
|
||||
)
|
||||
|
||||
plt.close(
|
||||
figure
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
# Без добавления шума преобразование должно быть обратимым.
|
||||
ideal_received_bits = bpsk_demodulate(
|
||||
transmitted_iq
|
||||
)
|
||||
|
||||
ideal_received_packet = bits_to_bytes(
|
||||
ideal_received_bits
|
||||
)
|
||||
|
||||
assert ideal_received_packet == original_packet
|
||||
|
||||
ideal_parsed_packet = parse_packet(
|
||||
ideal_received_packet
|
||||
)
|
||||
|
||||
assert (
|
||||
ideal_parsed_packet.payload.decode(
|
||||
"utf-8"
|
||||
)
|
||||
== MESSAGE
|
||||
)
|
||||
|
||||
assert len(experiment_results) == len(
|
||||
SNR_VALUES_DB
|
||||
)
|
||||
|
||||
assert constellation_path.exists()
|
||||
|
||||
|
||||
print("\nГрафик созвездия:")
|
||||
print(constellation_path)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"пакет преобразован в BPSK IQ-сэмплы "
|
||||
"и демодулирован обратно."
|
||||
)
|
||||
617
Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py
Normal file
617
Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py
Normal file
@@ -0,0 +1,617 @@
|
||||
"""
|
||||
Lab014. Измерение BER для BPSK в канале AWGN.
|
||||
|
||||
Программа:
|
||||
|
||||
1. Генерирует 1 000 000 случайных битов.
|
||||
2. Преобразует их в BPSK-символы.
|
||||
3. Добавляет комплексный гауссов шум.
|
||||
4. Демодулирует принятый сигнал.
|
||||
5. Измеряет экспериментальный BER.
|
||||
6. Рассчитывает теоретический BER.
|
||||
7. Оценивает вероятность повреждения пакетов разной длины.
|
||||
8. Строит BER-кривую.
|
||||
9. Сохраняет результаты в CSV.
|
||||
|
||||
Упрощения модели:
|
||||
|
||||
- один бит передаётся одним BPSK-символом;
|
||||
- один символ представлен одним IQ-сэмплом;
|
||||
- частотная и фазовая синхронизация идеальны;
|
||||
- межсимвольные искажения отсутствуют;
|
||||
- канал содержит только AWGN.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from math import (
|
||||
erfc,
|
||||
expm1,
|
||||
log1p,
|
||||
sqrt,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
BIT_COUNT = 1_000_000
|
||||
|
||||
EB_N0_VALUES_DB = [
|
||||
-4.0,
|
||||
-2.0,
|
||||
0.0,
|
||||
2.0,
|
||||
4.0,
|
||||
6.0,
|
||||
8.0,
|
||||
10.0,
|
||||
12.0,
|
||||
]
|
||||
|
||||
RANDOM_SEED = 2026
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab014"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab014_bpsk_ber_curve.png"
|
||||
)
|
||||
|
||||
CSV_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab014_bpsk_ber_results.csv"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Размеры пакетов для оценки PER
|
||||
# ============================================================
|
||||
|
||||
# Пакет из Lab013:
|
||||
#
|
||||
# заголовок 8 байт
|
||||
# PAYLOAD 16 байт
|
||||
# CRC 4 байта
|
||||
#
|
||||
# Итого 28 байт = 224 бита
|
||||
SHORT_PACKET_BITS = 224
|
||||
|
||||
# Типовой пакет изображения:
|
||||
#
|
||||
# заголовок протокола 8 байт
|
||||
# заголовок фрагмента 12 байт
|
||||
# данные JPEG 512 байт
|
||||
# CRC 4 байта
|
||||
#
|
||||
# Итого 536 байт
|
||||
IMAGE_PACKET_BITS = 536 * 8
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def bpsk_modulate(
|
||||
bits: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Преобразовать биты в BPSK-символы.
|
||||
|
||||
Отображение:
|
||||
|
||||
0 → -1
|
||||
1 → +1
|
||||
"""
|
||||
|
||||
bits = np.asarray(
|
||||
bits,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
if bits.ndim != 1:
|
||||
raise ValueError(
|
||||
"bits должен быть одномерным массивом"
|
||||
)
|
||||
|
||||
if not np.all(
|
||||
(bits == 0) | (bits == 1)
|
||||
):
|
||||
raise ValueError(
|
||||
"bits должен содержать только 0 и 1"
|
||||
)
|
||||
|
||||
symbols = (
|
||||
2.0 * bits.astype(np.float64)
|
||||
- 1.0
|
||||
)
|
||||
|
||||
return symbols.astype(
|
||||
np.complex128
|
||||
)
|
||||
|
||||
|
||||
def add_awgn(
|
||||
iq_samples: np.ndarray,
|
||||
eb_n0_db: float,
|
||||
random_generator: np.random.Generator,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Добавить комплексный AWGN-шум.
|
||||
|
||||
Энергия BPSK-символа равна единице.
|
||||
|
||||
Для одного бита на символ:
|
||||
|
||||
Es/N0 = Eb/N0
|
||||
"""
|
||||
|
||||
eb_n0_linear = 10.0 ** (
|
||||
eb_n0_db / 10.0
|
||||
)
|
||||
|
||||
# Для комплексного AWGN каждая компонента
|
||||
# I и Q получает половину полной мощности шума.
|
||||
noise_sigma = sqrt(
|
||||
1.0
|
||||
/ (
|
||||
2.0
|
||||
* eb_n0_linear
|
||||
)
|
||||
)
|
||||
|
||||
noise = noise_sigma * (
|
||||
random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
+ 1j
|
||||
* random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
)
|
||||
|
||||
return iq_samples + noise
|
||||
|
||||
|
||||
def bpsk_demodulate(
|
||||
received_iq: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Демодулировать BPSK по знаку компоненты I.
|
||||
|
||||
I < 0 → бит 0
|
||||
I >= 0 → бит 1
|
||||
"""
|
||||
|
||||
return (
|
||||
received_iq.real >= 0.0
|
||||
).astype(np.uint8)
|
||||
|
||||
|
||||
def theoretical_bpsk_ber(
|
||||
eb_n0_db: float,
|
||||
) -> float:
|
||||
"""
|
||||
Рассчитать теоретический BER когерентной BPSK
|
||||
в канале AWGN.
|
||||
|
||||
BER = 0.5 * erfc(sqrt(Eb/N0))
|
||||
"""
|
||||
|
||||
eb_n0_linear = 10.0 ** (
|
||||
eb_n0_db / 10.0
|
||||
)
|
||||
|
||||
return 0.5 * erfc(
|
||||
sqrt(eb_n0_linear)
|
||||
)
|
||||
|
||||
|
||||
def ber_to_per(
|
||||
ber: float,
|
||||
packet_bit_count: int,
|
||||
) -> float:
|
||||
"""
|
||||
Оценить Packet Error Rate из BER.
|
||||
|
||||
Предполагается:
|
||||
|
||||
- ошибки отдельных битов независимы;
|
||||
- пакет считается повреждённым,
|
||||
если ошибся хотя бы один бит.
|
||||
|
||||
PER = 1 - (1 - BER) ** N
|
||||
"""
|
||||
|
||||
if not 0.0 <= ber <= 1.0:
|
||||
raise ValueError(
|
||||
"BER должен находиться в диапазоне 0...1"
|
||||
)
|
||||
|
||||
if packet_bit_count <= 0:
|
||||
raise ValueError(
|
||||
"packet_bit_count должен быть положительным"
|
||||
)
|
||||
|
||||
if ber == 0.0:
|
||||
return 0.0
|
||||
|
||||
if ber == 1.0:
|
||||
return 1.0
|
||||
|
||||
# Такая запись численно устойчивее,
|
||||
# чем прямое возведение в степень
|
||||
# для очень маленьких BER.
|
||||
return -expm1(
|
||||
packet_bit_count
|
||||
* log1p(-ber)
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Генерация исходной последовательности
|
||||
# ============================================================
|
||||
|
||||
bit_generator = np.random.default_rng(
|
||||
RANDOM_SEED
|
||||
)
|
||||
|
||||
transmitted_bits = bit_generator.integers(
|
||||
low=0,
|
||||
high=2,
|
||||
size=BIT_COUNT,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
transmitted_iq = bpsk_modulate(
|
||||
transmitted_bits
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Эксперимент при разных Eb/N0
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for experiment_index, eb_n0_db in enumerate(
|
||||
EB_N0_VALUES_DB
|
||||
):
|
||||
|
||||
noise_generator = np.random.default_rng(
|
||||
RANDOM_SEED
|
||||
+ 1000
|
||||
+ experiment_index
|
||||
)
|
||||
|
||||
received_iq = add_awgn(
|
||||
iq_samples=transmitted_iq,
|
||||
eb_n0_db=eb_n0_db,
|
||||
random_generator=noise_generator,
|
||||
)
|
||||
|
||||
received_bits = bpsk_demodulate(
|
||||
received_iq
|
||||
)
|
||||
|
||||
bit_error_count = int(
|
||||
np.count_nonzero(
|
||||
transmitted_bits
|
||||
!= received_bits
|
||||
)
|
||||
)
|
||||
|
||||
experimental_ber = (
|
||||
bit_error_count
|
||||
/ BIT_COUNT
|
||||
)
|
||||
|
||||
theoretical_ber = theoretical_bpsk_ber(
|
||||
eb_n0_db
|
||||
)
|
||||
|
||||
short_packet_per = ber_to_per(
|
||||
theoretical_ber,
|
||||
SHORT_PACKET_BITS,
|
||||
)
|
||||
|
||||
image_packet_per = ber_to_per(
|
||||
theoretical_ber,
|
||||
IMAGE_PACKET_BITS,
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"eb_n0_db": eb_n0_db,
|
||||
"bit_errors": bit_error_count,
|
||||
"experimental_ber": experimental_ber,
|
||||
"theoretical_ber": theoretical_ber,
|
||||
"short_packet_per": short_packet_per,
|
||||
"image_packet_per": image_packet_per,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод основных результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab014. BER-кривая BPSK ==="
|
||||
)
|
||||
|
||||
print("\nКоличество переданных битов:")
|
||||
|
||||
print(
|
||||
f"{BIT_COUNT:,}".replace(",", " ")
|
||||
)
|
||||
|
||||
print("\nРезультаты:")
|
||||
|
||||
print(
|
||||
f"{'Eb/N0':>9}"
|
||||
f"{'Ошибки':>12}"
|
||||
f"{'BER эксперимент':>19}"
|
||||
f"{'BER теория':>16}"
|
||||
f"{'PER 224 бит':>16}"
|
||||
f"{'PER 4288 бит':>17}"
|
||||
)
|
||||
|
||||
print("-" * 89)
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['eb_n0_db']:>6.1f} дБ"
|
||||
f"{result['bit_errors']:>12}"
|
||||
f"{result['experimental_ber']:>19.6e}"
|
||||
f"{result['theoretical_ber']:>16.6e}"
|
||||
f"{result['short_packet_per']:>16.6f}"
|
||||
f"{result['image_packet_per']:>17.6f}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение CSV
|
||||
# ============================================================
|
||||
|
||||
with CSV_PATH.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
fieldnames = [
|
||||
"eb_n0_db",
|
||||
"bit_errors",
|
||||
"experimental_ber",
|
||||
"theoretical_ber",
|
||||
"short_packet_per_224_bits",
|
||||
"image_packet_per_4288_bits",
|
||||
]
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=fieldnames,
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for result in results:
|
||||
|
||||
writer.writerow(
|
||||
{
|
||||
"eb_n0_db": result["eb_n0_db"],
|
||||
"bit_errors": result["bit_errors"],
|
||||
"experimental_ber": (
|
||||
result["experimental_ber"]
|
||||
),
|
||||
"theoretical_ber": (
|
||||
result["theoretical_ber"]
|
||||
),
|
||||
"short_packet_per_224_bits": (
|
||||
result["short_packet_per"]
|
||||
),
|
||||
"image_packet_per_4288_bits": (
|
||||
result["image_packet_per"]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Подготовка данных для графика
|
||||
# ============================================================
|
||||
|
||||
eb_n0_plot_values = np.array(
|
||||
[
|
||||
result["eb_n0_db"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
experimental_ber_values = np.array(
|
||||
[
|
||||
result["experimental_ber"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
theoretical_ber_values = np.array(
|
||||
[
|
||||
result["theoretical_ber"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
# Нулевой измеренный BER невозможно показать
|
||||
# на логарифмической шкале.
|
||||
#
|
||||
# Поэтому для графика ставим такую точку
|
||||
# на уровень половины одного наблюдаемого события.
|
||||
measurement_floor = (
|
||||
0.5 / BIT_COUNT
|
||||
)
|
||||
|
||||
experimental_ber_for_plot = np.maximum(
|
||||
experimental_ber_values,
|
||||
measurement_floor,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Построение BER-графика
|
||||
# ============================================================
|
||||
|
||||
figure = plt.figure(
|
||||
figsize=(10, 7)
|
||||
)
|
||||
|
||||
plt.semilogy(
|
||||
eb_n0_plot_values,
|
||||
theoretical_ber_values,
|
||||
marker="o",
|
||||
label="Теоретический BER BPSK",
|
||||
)
|
||||
|
||||
plt.semilogy(
|
||||
eb_n0_plot_values,
|
||||
experimental_ber_for_plot,
|
||||
marker="s",
|
||||
linestyle="--",
|
||||
label="Экспериментальный BER",
|
||||
)
|
||||
|
||||
plt.axhline(
|
||||
measurement_floor,
|
||||
linestyle=":",
|
||||
label=(
|
||||
"Предел измерения "
|
||||
f"{measurement_floor:.1e}"
|
||||
),
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Eb/N0, дБ"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"BER"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"BPSK в канале AWGN: "
|
||||
"эксперимент и теория"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True,
|
||||
which="both",
|
||||
)
|
||||
|
||||
plt.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
figure.savefig(
|
||||
GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close(
|
||||
figure
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Инженерные контрольные точки
|
||||
# ============================================================
|
||||
|
||||
print("\nИнженерные контрольные точки:")
|
||||
|
||||
for target_db in [
|
||||
6.0,
|
||||
8.0,
|
||||
10.0,
|
||||
]:
|
||||
|
||||
result = next(
|
||||
item
|
||||
for item in results
|
||||
if item["eb_n0_db"] == target_db
|
||||
)
|
||||
|
||||
print(
|
||||
f"\nEb/N0 = {target_db:.1f} дБ"
|
||||
)
|
||||
|
||||
print(
|
||||
"Теоретический BER:",
|
||||
f"{result['theoretical_ber']:.6e}",
|
||||
)
|
||||
|
||||
print(
|
||||
"PER короткого пакета 224 бита:",
|
||||
f"{result['short_packet_per'] * 100:.2f} %",
|
||||
)
|
||||
|
||||
print(
|
||||
"PER JPEG-пакета 4288 бит:",
|
||||
f"{result['image_packet_per'] * 100:.2f} %",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert len(transmitted_bits) == BIT_COUNT
|
||||
|
||||
assert len(received_bits) == BIT_COUNT
|
||||
|
||||
assert all(
|
||||
0.0
|
||||
<= result["experimental_ber"]
|
||||
<= 1.0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert all(
|
||||
0.0
|
||||
<= result["theoretical_ber"]
|
||||
<= 1.0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert GRAPH_PATH.exists()
|
||||
|
||||
assert CSV_PATH.exists()
|
||||
|
||||
|
||||
print("\nГрафик BER:")
|
||||
|
||||
print(GRAPH_PATH)
|
||||
|
||||
print("\nТаблица CSV:")
|
||||
|
||||
print(CSV_PATH)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"экспериментальная BER-кривая построена."
|
||||
)
|
||||
810
Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py
Normal file
810
Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py
Normal file
@@ -0,0 +1,810 @@
|
||||
"""
|
||||
Lab015. Подбор оптимального размера фрагмента изображения.
|
||||
|
||||
Программа сравнивает фрагменты размером:
|
||||
|
||||
- 64 байта;
|
||||
- 128 байт;
|
||||
- 256 байт;
|
||||
- 512 байт;
|
||||
- 1024 байта.
|
||||
|
||||
Для каждого размера рассчитываются:
|
||||
|
||||
1. Количество фрагментов изображения.
|
||||
2. Служебные расходы протокола.
|
||||
3. Теоретический BER BPSK.
|
||||
4. Вероятность успешной доставки DATA-пакета.
|
||||
5. Вероятность успешной доставки ACK.
|
||||
6. Среднее число передач одного фрагмента.
|
||||
7. Ожидаемый полный радиообмен с ARQ.
|
||||
8. Время передачи фотографии при 20 кбит/с.
|
||||
9. Эффективная полезная скорость.
|
||||
|
||||
Модель канала:
|
||||
|
||||
- когерентная BPSK;
|
||||
- AWGN;
|
||||
- ошибки битов независимы;
|
||||
- повреждённый пакет отбрасывается по CRC;
|
||||
- применяется Stop-and-Wait ARQ;
|
||||
- DATA и ACK работают при одинаковом Eb/N0.
|
||||
|
||||
Не учитываются:
|
||||
|
||||
- ожидание тайм-аута;
|
||||
- паузы между DATA и ACK;
|
||||
- преамбула;
|
||||
- синхронизация;
|
||||
- FEC;
|
||||
- многолучёвость;
|
||||
- частотные и фазовые ошибки.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from math import (
|
||||
erfc,
|
||||
exp,
|
||||
log1p,
|
||||
sqrt,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from protocol.image_fragments import (
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
SOURCE_PATH = Path(
|
||||
"data/raw/lab009_source.jpg"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab015"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
FRAGMENT_SIZES = [
|
||||
64,
|
||||
128,
|
||||
256,
|
||||
512,
|
||||
1024,
|
||||
]
|
||||
|
||||
EB_N0_VALUES_DB = [
|
||||
6.0,
|
||||
8.0,
|
||||
10.0,
|
||||
12.0,
|
||||
]
|
||||
|
||||
CHANNEL_BITRATE_BPS = 20_000
|
||||
|
||||
IMAGE_ID_BASE = 2026071600
|
||||
|
||||
CSV_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab015_fragment_results.csv"
|
||||
)
|
||||
|
||||
TIME_GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab015_transfer_time.png"
|
||||
)
|
||||
|
||||
GOODPUT_GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab015_effective_goodput.png"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def theoretical_bpsk_ber(
|
||||
eb_n0_db: float,
|
||||
) -> float:
|
||||
"""
|
||||
Теоретический BER когерентной BPSK в AWGN.
|
||||
|
||||
BER = 0.5 * erfc(sqrt(Eb/N0))
|
||||
"""
|
||||
|
||||
eb_n0_linear = 10.0 ** (
|
||||
eb_n0_db / 10.0
|
||||
)
|
||||
|
||||
return 0.5 * erfc(
|
||||
sqrt(eb_n0_linear)
|
||||
)
|
||||
|
||||
|
||||
def packet_success_probability(
|
||||
ber: float,
|
||||
packet_bit_count: int,
|
||||
) -> float:
|
||||
"""
|
||||
Вероятность того, что весь пакет будет принят
|
||||
без единой битовой ошибки.
|
||||
|
||||
P_success = (1 - BER) ** N
|
||||
"""
|
||||
|
||||
if not 0.0 <= ber <= 1.0:
|
||||
raise ValueError(
|
||||
"BER должен находиться в диапазоне 0...1"
|
||||
)
|
||||
|
||||
if packet_bit_count <= 0:
|
||||
raise ValueError(
|
||||
"Размер пакета должен быть положительным"
|
||||
)
|
||||
|
||||
if ber == 0.0:
|
||||
return 1.0
|
||||
|
||||
if ber == 1.0:
|
||||
return 0.0
|
||||
|
||||
# Численно устойчивый вариант выражения:
|
||||
#
|
||||
# (1 - BER) ** N
|
||||
return exp(
|
||||
packet_bit_count
|
||||
* log1p(-ber)
|
||||
)
|
||||
|
||||
|
||||
def format_bytes(
|
||||
byte_count: float,
|
||||
) -> str:
|
||||
"""
|
||||
Представить объём в удобном виде.
|
||||
"""
|
||||
|
||||
if byte_count < 1024:
|
||||
return f"{byte_count:.0f} байт"
|
||||
|
||||
kibibytes = byte_count / 1024
|
||||
|
||||
if kibibytes < 1024:
|
||||
return f"{kibibytes:.2f} КиБ"
|
||||
|
||||
mebibytes = kibibytes / 1024
|
||||
|
||||
return f"{mebibytes:.2f} МиБ"
|
||||
|
||||
|
||||
def format_duration(
|
||||
seconds: float,
|
||||
) -> str:
|
||||
"""
|
||||
Представить длительность в удобном виде.
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.0f} мс"
|
||||
|
||||
if seconds < 60:
|
||||
return f"{seconds:.2f} с"
|
||||
|
||||
if seconds < 3600:
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = seconds % 60
|
||||
|
||||
return (
|
||||
f"{minutes} мин "
|
||||
f"{remaining_seconds:.1f} с"
|
||||
)
|
||||
|
||||
hours = int(seconds // 3600)
|
||||
remaining_minutes = (
|
||||
seconds % 3600
|
||||
) / 60
|
||||
|
||||
return (
|
||||
f"{hours} ч "
|
||||
f"{remaining_minutes:.1f} мин"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверка исходного JPEG
|
||||
# ============================================================
|
||||
|
||||
if not SOURCE_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Не найден файл: {SOURCE_PATH}. "
|
||||
"Сначала необходимо выполнить Lab009."
|
||||
)
|
||||
|
||||
source_bytes = SOURCE_PATH.read_bytes()
|
||||
|
||||
if not source_bytes:
|
||||
raise ValueError(
|
||||
"Исходный JPEG пуст"
|
||||
)
|
||||
|
||||
source_size = len(source_bytes)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Размер ACK
|
||||
# ============================================================
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
ack_packet_size_bytes = len(
|
||||
ack_packet
|
||||
)
|
||||
|
||||
ack_packet_size_bits = (
|
||||
ack_packet_size_bytes * 8
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Основной расчёт
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for fragment_size_index, fragment_size in enumerate(
|
||||
FRAGMENT_SIZES
|
||||
):
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=source_bytes,
|
||||
image_id=(
|
||||
IMAGE_ID_BASE
|
||||
+ fragment_size_index
|
||||
),
|
||||
fragment_data_size=fragment_size,
|
||||
)
|
||||
|
||||
data_packet_sizes_bytes = []
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
data_packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=(
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||
),
|
||||
sequence_number=(
|
||||
fragment.fragment_index
|
||||
),
|
||||
)
|
||||
|
||||
data_packet_sizes_bytes.append(
|
||||
len(data_packet)
|
||||
)
|
||||
|
||||
ideal_data_bytes = sum(
|
||||
data_packet_sizes_bytes
|
||||
)
|
||||
|
||||
ideal_ack_bytes = (
|
||||
len(fragments)
|
||||
* ack_packet_size_bytes
|
||||
)
|
||||
|
||||
ideal_total_bytes = (
|
||||
ideal_data_bytes
|
||||
+ ideal_ack_bytes
|
||||
)
|
||||
|
||||
ideal_efficiency_percent = (
|
||||
source_size
|
||||
/ ideal_total_bytes
|
||||
* 100
|
||||
)
|
||||
|
||||
for eb_n0_db in EB_N0_VALUES_DB:
|
||||
|
||||
ber = theoretical_bpsk_ber(
|
||||
eb_n0_db
|
||||
)
|
||||
|
||||
ack_success_probability = (
|
||||
packet_success_probability(
|
||||
ber=ber,
|
||||
packet_bit_count=(
|
||||
ack_packet_size_bits
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
expected_data_bytes = 0.0
|
||||
expected_ack_bytes = 0.0
|
||||
|
||||
expected_data_transmissions = 0.0
|
||||
|
||||
data_success_probabilities = []
|
||||
|
||||
for data_packet_size_bytes in (
|
||||
data_packet_sizes_bytes
|
||||
):
|
||||
|
||||
data_packet_size_bits = (
|
||||
data_packet_size_bytes * 8
|
||||
)
|
||||
|
||||
data_success_probability = (
|
||||
packet_success_probability(
|
||||
ber=ber,
|
||||
packet_bit_count=(
|
||||
data_packet_size_bits
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
data_success_probabilities.append(
|
||||
data_success_probability
|
||||
)
|
||||
|
||||
# Для успешного завершения попытки должны
|
||||
# одновременно правильно пройти DATA и ACK.
|
||||
confirmed_attempt_probability = (
|
||||
data_success_probability
|
||||
* ack_success_probability
|
||||
)
|
||||
|
||||
if confirmed_attempt_probability == 0.0:
|
||||
raise RuntimeError(
|
||||
"Вероятность подтверждения "
|
||||
"оказалась равной нулю"
|
||||
)
|
||||
|
||||
# Число DATA-передач до успешного ACK
|
||||
# подчиняется геометрическому распределению.
|
||||
expected_attempt_count = (
|
||||
1.0
|
||||
/ confirmed_attempt_probability
|
||||
)
|
||||
|
||||
expected_data_transmissions += (
|
||||
expected_attempt_count
|
||||
)
|
||||
|
||||
expected_data_bytes += (
|
||||
data_packet_size_bytes
|
||||
* expected_attempt_count
|
||||
)
|
||||
|
||||
# ACK передаётся только после правильно
|
||||
# принятого DATA.
|
||||
#
|
||||
# Среднее количество передач ACK до
|
||||
# успешного ACK равно 1 / P_ACK.
|
||||
expected_ack_bytes += (
|
||||
ack_packet_size_bytes
|
||||
/ ack_success_probability
|
||||
)
|
||||
|
||||
expected_total_bytes = (
|
||||
expected_data_bytes
|
||||
+ expected_ack_bytes
|
||||
)
|
||||
|
||||
expected_transfer_seconds = (
|
||||
expected_total_bytes
|
||||
* 8
|
||||
/ CHANNEL_BITRATE_BPS
|
||||
)
|
||||
|
||||
expected_efficiency_percent = (
|
||||
source_size
|
||||
/ expected_total_bytes
|
||||
* 100
|
||||
)
|
||||
|
||||
effective_goodput_bps = (
|
||||
source_size
|
||||
* 8
|
||||
/ expected_transfer_seconds
|
||||
)
|
||||
|
||||
average_data_transmissions = (
|
||||
expected_data_transmissions
|
||||
/ len(fragments)
|
||||
)
|
||||
|
||||
minimum_data_success_probability = min(
|
||||
data_success_probabilities
|
||||
)
|
||||
|
||||
maximum_data_success_probability = max(
|
||||
data_success_probabilities
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"fragment_size": fragment_size,
|
||||
"fragment_count": len(fragments),
|
||||
"eb_n0_db": eb_n0_db,
|
||||
"ber": ber,
|
||||
"ideal_total_bytes": (
|
||||
ideal_total_bytes
|
||||
),
|
||||
"ideal_efficiency_percent": (
|
||||
ideal_efficiency_percent
|
||||
),
|
||||
"ack_success_probability": (
|
||||
ack_success_probability
|
||||
),
|
||||
"minimum_data_success_probability": (
|
||||
minimum_data_success_probability
|
||||
),
|
||||
"maximum_data_success_probability": (
|
||||
maximum_data_success_probability
|
||||
),
|
||||
"average_data_transmissions": (
|
||||
average_data_transmissions
|
||||
),
|
||||
"expected_total_bytes": (
|
||||
expected_total_bytes
|
||||
),
|
||||
"expected_transfer_seconds": (
|
||||
expected_transfer_seconds
|
||||
),
|
||||
"expected_efficiency_percent": (
|
||||
expected_efficiency_percent
|
||||
),
|
||||
"effective_goodput_bps": (
|
||||
effective_goodput_bps
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод общих данных
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab015. Оптимальный размер фрагмента ==="
|
||||
)
|
||||
|
||||
print("\nИсходный JPEG:")
|
||||
|
||||
print(SOURCE_PATH)
|
||||
|
||||
print("\nРазмер JPEG:")
|
||||
|
||||
print(
|
||||
format_bytes(source_size)
|
||||
)
|
||||
|
||||
print("\nСкорость физического канала:")
|
||||
|
||||
print(
|
||||
CHANNEL_BITRATE_BPS / 1000,
|
||||
"кбит/с",
|
||||
)
|
||||
|
||||
print("\nРазмер ACK-пакета:")
|
||||
|
||||
print(
|
||||
ack_packet_size_bytes,
|
||||
"байт",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов отдельно для каждого Eb/N0
|
||||
# ============================================================
|
||||
|
||||
for eb_n0_db in EB_N0_VALUES_DB:
|
||||
|
||||
selected_results = [
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == eb_n0_db
|
||||
]
|
||||
|
||||
print(
|
||||
f"\n--- Eb/N0 = {eb_n0_db:.1f} дБ ---"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{'Фрагмент':>10}"
|
||||
f"{'Кол-во':>9}"
|
||||
f"{'P DATA':>12}"
|
||||
f"{'Попыток':>11}"
|
||||
f"{'Объём с ARQ':>16}"
|
||||
f"{'Время':>16}"
|
||||
f"{'Goodput':>13}"
|
||||
)
|
||||
|
||||
print("-" * 87)
|
||||
|
||||
for result in selected_results:
|
||||
|
||||
print(
|
||||
f"{result['fragment_size']:>8} Б"
|
||||
f"{result['fragment_count']:>9}"
|
||||
f"{result['minimum_data_success_probability']:>12.6f}"
|
||||
f"{result['average_data_transmissions']:>11.2f}"
|
||||
f"{format_bytes(result['expected_total_bytes']):>16}"
|
||||
f"{format_duration(result['expected_transfer_seconds']):>16}"
|
||||
f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с"
|
||||
)
|
||||
|
||||
optimum_result = min(
|
||||
selected_results,
|
||||
key=lambda item: (
|
||||
item["expected_transfer_seconds"]
|
||||
),
|
||||
)
|
||||
|
||||
print(
|
||||
"\nОптимальный размер фрагмента:"
|
||||
)
|
||||
|
||||
print(
|
||||
optimum_result["fragment_size"],
|
||||
"байт",
|
||||
)
|
||||
|
||||
print(
|
||||
"Ожидаемое время:",
|
||||
format_duration(
|
||||
optimum_result[
|
||||
"expected_transfer_seconds"
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
print(
|
||||
"Полезная скорость:",
|
||||
f"{optimum_result['effective_goodput_bps'] / 1000:.2f}",
|
||||
"кбит/с",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение CSV
|
||||
# ============================================================
|
||||
|
||||
with CSV_PATH.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
fieldnames = [
|
||||
"fragment_size",
|
||||
"fragment_count",
|
||||
"eb_n0_db",
|
||||
"ber",
|
||||
"ideal_total_bytes",
|
||||
"ideal_efficiency_percent",
|
||||
"ack_success_probability",
|
||||
"minimum_data_success_probability",
|
||||
"maximum_data_success_probability",
|
||||
"average_data_transmissions",
|
||||
"expected_total_bytes",
|
||||
"expected_transfer_seconds",
|
||||
"expected_efficiency_percent",
|
||||
"effective_goodput_bps",
|
||||
]
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=fieldnames,
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
writer.writerows(results)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График ожидаемого времени передачи
|
||||
# ============================================================
|
||||
|
||||
plt.figure(
|
||||
figsize=(10, 7)
|
||||
)
|
||||
|
||||
for eb_n0_db in EB_N0_VALUES_DB:
|
||||
|
||||
selected_results = [
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == eb_n0_db
|
||||
]
|
||||
|
||||
fragment_sizes = [
|
||||
result["fragment_size"]
|
||||
for result in selected_results
|
||||
]
|
||||
|
||||
transfer_times = [
|
||||
result["expected_transfer_seconds"]
|
||||
for result in selected_results
|
||||
]
|
||||
|
||||
plt.plot(
|
||||
fragment_sizes,
|
||||
transfer_times,
|
||||
marker="o",
|
||||
label=f"Eb/N0 = {eb_n0_db:.0f} дБ",
|
||||
)
|
||||
|
||||
plt.xscale(
|
||||
"log",
|
||||
base=2,
|
||||
)
|
||||
|
||||
plt.yscale(
|
||||
"log",
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Размер данных фрагмента, байт"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"Ожидаемое время передачи, с"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Влияние размера фрагмента "
|
||||
"на время передачи JPEG"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True,
|
||||
which="both",
|
||||
)
|
||||
|
||||
plt.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(
|
||||
TIME_GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График эффективной полезной скорости
|
||||
# ============================================================
|
||||
|
||||
plt.figure(
|
||||
figsize=(10, 7)
|
||||
)
|
||||
|
||||
for eb_n0_db in EB_N0_VALUES_DB:
|
||||
|
||||
selected_results = [
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == eb_n0_db
|
||||
]
|
||||
|
||||
fragment_sizes = [
|
||||
result["fragment_size"]
|
||||
for result in selected_results
|
||||
]
|
||||
|
||||
goodput_values = [
|
||||
result["effective_goodput_bps"] / 1000
|
||||
for result in selected_results
|
||||
]
|
||||
|
||||
plt.plot(
|
||||
fragment_sizes,
|
||||
goodput_values,
|
||||
marker="o",
|
||||
label=f"Eb/N0 = {eb_n0_db:.0f} дБ",
|
||||
)
|
||||
|
||||
plt.xscale(
|
||||
"log",
|
||||
base=2,
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Размер данных фрагмента, байт"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"Полезная скорость, кбит/с"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Полезная скорость JPEG "
|
||||
"с учётом CRC и ARQ"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True,
|
||||
which="both",
|
||||
)
|
||||
|
||||
plt.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(
|
||||
GOODPUT_GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert results
|
||||
|
||||
assert CSV_PATH.exists()
|
||||
|
||||
assert TIME_GRAPH_PATH.exists()
|
||||
|
||||
assert GOODPUT_GRAPH_PATH.exists()
|
||||
|
||||
assert all(
|
||||
result["expected_transfer_seconds"] > 0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert all(
|
||||
0.0
|
||||
< result["ack_success_probability"]
|
||||
<= 1.0
|
||||
for result in results
|
||||
)
|
||||
|
||||
|
||||
print("\nCSV:")
|
||||
|
||||
print(CSV_PATH)
|
||||
|
||||
print("\nГрафик времени:")
|
||||
|
||||
print(TIME_GRAPH_PATH)
|
||||
|
||||
print("\nГрафик полезной скорости:")
|
||||
|
||||
print(GOODPUT_GRAPH_PATH)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"оптимальный размер фрагмента рассчитан."
|
||||
)
|
||||
605
Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py
Normal file
605
Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py
Normal file
@@ -0,0 +1,605 @@
|
||||
"""
|
||||
Lab016. Адаптивный выбор размера фрагмента изображения.
|
||||
|
||||
Программа имитирует изменение качества канала во времени.
|
||||
|
||||
Для каждой оценки Eb/N0 передатчик решает:
|
||||
|
||||
- отключить изображения;
|
||||
- использовать 128 байт;
|
||||
- использовать 512 байт;
|
||||
- использовать 1024 байта.
|
||||
|
||||
Дополнительно рассчитывается ожидаемое время передачи
|
||||
реального JPEG-файла.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from protocol.image_fragments import (
|
||||
encode_image_fragment,
|
||||
split_image_bytes,
|
||||
)
|
||||
|
||||
from protocol.link_adaptation import (
|
||||
choose_image_mode,
|
||||
packet_success_probability,
|
||||
)
|
||||
|
||||
from protocol.packet import (
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
build_packet,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
CHANNEL_BITRATE_BPS = 20_000
|
||||
|
||||
MAX_ATTEMPTS = 5
|
||||
|
||||
CANDIDATE_FRAGMENT_SIZES = (
|
||||
128,
|
||||
512,
|
||||
1024,
|
||||
)
|
||||
|
||||
# Изменение качества канала во времени.
|
||||
EB_N0_PROFILE_DB = [
|
||||
12.0,
|
||||
10.0,
|
||||
9.0,
|
||||
8.0,
|
||||
7.0,
|
||||
6.0,
|
||||
7.0,
|
||||
8.0,
|
||||
9.0,
|
||||
10.0,
|
||||
12.0,
|
||||
]
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab016"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
CSV_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab016_adaptation_results.csv"
|
||||
)
|
||||
|
||||
MODE_GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab016_selected_mode.png"
|
||||
)
|
||||
|
||||
TIME_GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab016_expected_transfer_time.png"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Выбор исходного кадра
|
||||
# ============================================================
|
||||
|
||||
source_candidates = [
|
||||
Path(
|
||||
"data/processed/lab012/"
|
||||
"03_color_320_q15.jpg"
|
||||
),
|
||||
Path(
|
||||
"data/raw/lab009_source.jpg"
|
||||
),
|
||||
]
|
||||
|
||||
SOURCE_PATH = next(
|
||||
(
|
||||
path
|
||||
for path in source_candidates
|
||||
if path.exists()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if SOURCE_PATH is None:
|
||||
raise FileNotFoundError(
|
||||
"Не найден кадр для передачи. "
|
||||
"Необходимо выполнить Lab009 или Lab012."
|
||||
)
|
||||
|
||||
source_bytes = SOURCE_PATH.read_bytes()
|
||||
|
||||
if not source_bytes:
|
||||
raise ValueError(
|
||||
"Исходный JPEG пуст"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Оценка передачи всего изображения
|
||||
# ============================================================
|
||||
|
||||
def estimate_image_transfer(
|
||||
image_bytes: bytes,
|
||||
fragment_size: int,
|
||||
ber: float,
|
||||
image_id: int,
|
||||
) -> dict:
|
||||
"""
|
||||
Оценить передачу полного изображения с бесконечным ARQ.
|
||||
|
||||
Расчёт учитывает фактический размер последнего фрагмента.
|
||||
"""
|
||||
|
||||
fragments = split_image_bytes(
|
||||
image_bytes=image_bytes,
|
||||
image_id=image_id,
|
||||
fragment_data_size=fragment_size,
|
||||
)
|
||||
|
||||
ack_packet = build_packet(
|
||||
payload=b"",
|
||||
message_type=MESSAGE_TYPE_ACK,
|
||||
sequence_number=0,
|
||||
)
|
||||
|
||||
ack_size_bytes = len(
|
||||
ack_packet
|
||||
)
|
||||
|
||||
ack_success_probability = (
|
||||
packet_success_probability(
|
||||
ber=ber,
|
||||
packet_bit_count=(
|
||||
ack_size_bytes * 8
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
expected_total_bytes = 0.0
|
||||
|
||||
for fragment in fragments:
|
||||
|
||||
fragment_payload = encode_image_fragment(
|
||||
fragment
|
||||
)
|
||||
|
||||
data_packet = build_packet(
|
||||
payload=fragment_payload,
|
||||
message_type=(
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||
),
|
||||
sequence_number=(
|
||||
fragment.fragment_index
|
||||
),
|
||||
)
|
||||
|
||||
data_success_probability = (
|
||||
packet_success_probability(
|
||||
ber=ber,
|
||||
packet_bit_count=(
|
||||
len(data_packet) * 8
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
confirmed_probability = (
|
||||
data_success_probability
|
||||
* ack_success_probability
|
||||
)
|
||||
|
||||
expected_total_bytes += (
|
||||
len(data_packet)
|
||||
/ confirmed_probability
|
||||
)
|
||||
|
||||
expected_total_bytes += (
|
||||
ack_size_bytes
|
||||
/ ack_success_probability
|
||||
)
|
||||
|
||||
expected_seconds = (
|
||||
expected_total_bytes
|
||||
* 8
|
||||
/ CHANNEL_BITRATE_BPS
|
||||
)
|
||||
|
||||
effective_goodput_bps = (
|
||||
len(image_bytes)
|
||||
* 8
|
||||
/ expected_seconds
|
||||
)
|
||||
|
||||
return {
|
||||
"fragment_count": len(fragments),
|
||||
"expected_total_bytes": expected_total_bytes,
|
||||
"expected_seconds": expected_seconds,
|
||||
"effective_goodput_bps": (
|
||||
effective_goodput_bps
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Адаптация по профилю канала
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for step_index, eb_n0_db in enumerate(
|
||||
EB_N0_PROFILE_DB
|
||||
):
|
||||
|
||||
decision = choose_image_mode(
|
||||
eb_n0_db=eb_n0_db,
|
||||
candidate_fragment_sizes=(
|
||||
CANDIDATE_FRAGMENT_SIZES
|
||||
),
|
||||
channel_bitrate_bps=(
|
||||
CHANNEL_BITRATE_BPS
|
||||
),
|
||||
max_attempts=MAX_ATTEMPTS,
|
||||
minimum_success_probability=0.85,
|
||||
minimum_goodput_bps=2_000.0,
|
||||
)
|
||||
|
||||
if decision.images_enabled:
|
||||
|
||||
selected_estimate = next(
|
||||
estimate
|
||||
for estimate in decision.estimates
|
||||
if (
|
||||
estimate.fragment_size
|
||||
== decision.selected_fragment_size
|
||||
)
|
||||
)
|
||||
|
||||
image_result = estimate_image_transfer(
|
||||
image_bytes=source_bytes,
|
||||
fragment_size=(
|
||||
decision.selected_fragment_size
|
||||
),
|
||||
ber=selected_estimate.ber,
|
||||
image_id=2026071700 + step_index,
|
||||
)
|
||||
|
||||
fragment_size = (
|
||||
decision.selected_fragment_size
|
||||
)
|
||||
|
||||
fragment_count = (
|
||||
image_result["fragment_count"]
|
||||
)
|
||||
|
||||
expected_seconds = (
|
||||
image_result["expected_seconds"]
|
||||
)
|
||||
|
||||
effective_goodput_bps = (
|
||||
image_result[
|
||||
"effective_goodput_bps"
|
||||
]
|
||||
)
|
||||
|
||||
success_with_retries = (
|
||||
selected_estimate
|
||||
.success_probability_with_retries
|
||||
)
|
||||
|
||||
expected_attempts = (
|
||||
selected_estimate.expected_attempts
|
||||
)
|
||||
|
||||
mode_name = (
|
||||
f"{fragment_size} B"
|
||||
)
|
||||
|
||||
else:
|
||||
fragment_size = 0
|
||||
fragment_count = 0
|
||||
expected_seconds = None
|
||||
effective_goodput_bps = 0.0
|
||||
success_with_retries = 0.0
|
||||
expected_attempts = 0.0
|
||||
mode_name = "IMAGE OFF"
|
||||
|
||||
results.append(
|
||||
{
|
||||
"step": step_index,
|
||||
"eb_n0_db": eb_n0_db,
|
||||
"mode": mode_name,
|
||||
"fragment_size": fragment_size,
|
||||
"fragment_count": fragment_count,
|
||||
"expected_seconds": expected_seconds,
|
||||
"effective_goodput_bps": (
|
||||
effective_goodput_bps
|
||||
),
|
||||
"success_with_retries": (
|
||||
success_with_retries
|
||||
),
|
||||
"expected_attempts": (
|
||||
expected_attempts
|
||||
),
|
||||
"reason": decision.reason,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab016. Адаптация размера фрагмента ==="
|
||||
)
|
||||
|
||||
print("\nИсходный кадр:")
|
||||
|
||||
print(SOURCE_PATH)
|
||||
|
||||
print("\nРазмер JPEG:")
|
||||
|
||||
print(
|
||||
len(source_bytes),
|
||||
"байт",
|
||||
)
|
||||
|
||||
print("\nРезультаты адаптации:")
|
||||
|
||||
print(
|
||||
f"{'Шаг':>5}"
|
||||
f"{'Eb/N0':>10}"
|
||||
f"{'Режим':>14}"
|
||||
f"{'Фрагм.':>9}"
|
||||
f"{'Попыток':>11}"
|
||||
f"{'Успех x5':>12}"
|
||||
f"{'Время кадра':>15}"
|
||||
f"{'Goodput':>13}"
|
||||
)
|
||||
|
||||
print("-" * 89)
|
||||
|
||||
for result in results:
|
||||
|
||||
if result["expected_seconds"] is None:
|
||||
time_text = "—"
|
||||
else:
|
||||
time_text = (
|
||||
f"{result['expected_seconds']:.2f} с"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{result['step']:>5}"
|
||||
f"{result['eb_n0_db']:>7.1f} дБ"
|
||||
f"{result['mode']:>14}"
|
||||
f"{result['fragment_count']:>9}"
|
||||
f"{result['expected_attempts']:>11.2f}"
|
||||
f"{result['success_with_retries'] * 100:>10.1f} %"
|
||||
f"{time_text:>15}"
|
||||
f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение CSV
|
||||
# ============================================================
|
||||
|
||||
with CSV_PATH.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
fieldnames = list(
|
||||
results[0].keys()
|
||||
)
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=fieldnames,
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
writer.writerows(results)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График выбранного режима
|
||||
# ============================================================
|
||||
|
||||
steps = [
|
||||
result["step"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
fragment_sizes = [
|
||||
result["fragment_size"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
plt.figure(
|
||||
figsize=(11, 6)
|
||||
)
|
||||
|
||||
plt.step(
|
||||
steps,
|
||||
fragment_sizes,
|
||||
where="mid",
|
||||
marker="o",
|
||||
)
|
||||
|
||||
plt.yticks(
|
||||
[
|
||||
0,
|
||||
128,
|
||||
512,
|
||||
1024,
|
||||
],
|
||||
[
|
||||
"IMAGE OFF",
|
||||
"128 B",
|
||||
"512 B",
|
||||
"1024 B",
|
||||
],
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Шаг времени"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"Выбранный режим"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Автоматический выбор размера фрагмента"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True
|
||||
)
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(
|
||||
MODE_GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График времени передачи кадра
|
||||
# ============================================================
|
||||
|
||||
transfer_times = np.array(
|
||||
[
|
||||
(
|
||||
result["expected_seconds"]
|
||||
if result["expected_seconds"] is not None
|
||||
else np.nan
|
||||
)
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
plt.figure(
|
||||
figsize=(11, 6)
|
||||
)
|
||||
|
||||
plt.plot(
|
||||
steps,
|
||||
transfer_times,
|
||||
marker="o",
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Шаг времени"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"Ожидаемое время передачи кадра, с"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Время передачи кадра при адаптации канала"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True
|
||||
)
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(
|
||||
TIME_GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки ожидаемой логики
|
||||
# ============================================================
|
||||
|
||||
decision_at_6_db = next(
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == 6.0
|
||||
)
|
||||
|
||||
decision_at_8_db = next(
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == 8.0
|
||||
)
|
||||
|
||||
decision_at_9_db = next(
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == 9.0
|
||||
)
|
||||
|
||||
decision_at_10_db = next(
|
||||
result
|
||||
for result in results
|
||||
if result["eb_n0_db"] == 10.0
|
||||
)
|
||||
|
||||
assert (
|
||||
decision_at_6_db["fragment_size"]
|
||||
== 0
|
||||
)
|
||||
|
||||
assert (
|
||||
decision_at_8_db["fragment_size"]
|
||||
== 128
|
||||
)
|
||||
|
||||
assert (
|
||||
decision_at_9_db["fragment_size"]
|
||||
== 512
|
||||
)
|
||||
|
||||
assert (
|
||||
decision_at_10_db["fragment_size"]
|
||||
== 1024
|
||||
)
|
||||
|
||||
assert CSV_PATH.exists()
|
||||
assert MODE_GRAPH_PATH.exists()
|
||||
assert TIME_GRAPH_PATH.exists()
|
||||
|
||||
|
||||
print("\nCSV:")
|
||||
|
||||
print(CSV_PATH)
|
||||
|
||||
print("\nГрафик режимов:")
|
||||
|
||||
print(MODE_GRAPH_PATH)
|
||||
|
||||
print("\nГрафик времени:")
|
||||
|
||||
print(TIME_GRAPH_PATH)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"адаптивный выбор режима работает."
|
||||
)
|
||||
468
Projects/SDR_Rover/tests/lab017_link_hysteresis.py
Normal file
468
Projects/SDR_Rover/tests/lab017_link_hysteresis.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""
|
||||
Lab017. Фильтрация Eb/N0 и гистерезис режимов.
|
||||
|
||||
Эксперимент сравнивает два адаптера:
|
||||
|
||||
1. Наивный адаптер:
|
||||
переключает режим при каждом пересечении порога.
|
||||
|
||||
2. Устойчивый адаптер:
|
||||
использует:
|
||||
- экспоненциальное усреднение;
|
||||
- гистерезис;
|
||||
- подтверждение повышения;
|
||||
- аварийное быстрое понижение.
|
||||
|
||||
Профиль Eb/N0 специально содержит колебания
|
||||
около порогов переключения.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from protocol.link_controller import (
|
||||
FRAGMENT_MODE_1024,
|
||||
IMAGE_OFF,
|
||||
LinkModeController,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки
|
||||
# ============================================================
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab017"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
CSV_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab017_hysteresis_results.csv"
|
||||
)
|
||||
|
||||
GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab017_hysteresis_comparison.png"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Профиль качества канала
|
||||
# ============================================================
|
||||
|
||||
EB_N0_PROFILE_DB = [
|
||||
10.2,
|
||||
9.7,
|
||||
9.4,
|
||||
9.6,
|
||||
9.3,
|
||||
9.7,
|
||||
9.2,
|
||||
9.6,
|
||||
9.0,
|
||||
8.7,
|
||||
8.4,
|
||||
8.6,
|
||||
8.3,
|
||||
8.7,
|
||||
8.2,
|
||||
8.6,
|
||||
8.0,
|
||||
7.8,
|
||||
7.5,
|
||||
7.7,
|
||||
7.4,
|
||||
7.8,
|
||||
7.2,
|
||||
6.8,
|
||||
6.5,
|
||||
6.3,
|
||||
6.6,
|
||||
6.2,
|
||||
6.7,
|
||||
6.1,
|
||||
5.5,
|
||||
6.0,
|
||||
6.4,
|
||||
6.7,
|
||||
6.9,
|
||||
7.1,
|
||||
7.3,
|
||||
8.0,
|
||||
8.8,
|
||||
9.0,
|
||||
9.4,
|
||||
9.8,
|
||||
10.0,
|
||||
10.2,
|
||||
10.1,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Наивное решение без гистерезиса
|
||||
# ============================================================
|
||||
|
||||
def choose_naive_mode(
|
||||
eb_n0_db: float,
|
||||
) -> int:
|
||||
"""
|
||||
Немедленно выбрать режим по одному измерению.
|
||||
|
||||
Гистерезиса и фильтрации нет.
|
||||
"""
|
||||
|
||||
if eb_n0_db < 6.5:
|
||||
return 0
|
||||
|
||||
if eb_n0_db < 8.5:
|
||||
return 128
|
||||
|
||||
if eb_n0_db < 9.5:
|
||||
return 512
|
||||
|
||||
return 1024
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Запуск устойчивого контроллера
|
||||
# ============================================================
|
||||
|
||||
controller = LinkModeController(
|
||||
initial_mode=FRAGMENT_MODE_1024,
|
||||
filter_alpha=0.35,
|
||||
upgrade_confirmation_count=3,
|
||||
minimum_hold_steps=2,
|
||||
)
|
||||
|
||||
results = []
|
||||
|
||||
for measured_eb_n0_db in EB_N0_PROFILE_DB:
|
||||
|
||||
stable_result = controller.update(
|
||||
measured_eb_n0_db
|
||||
)
|
||||
|
||||
naive_mode = choose_naive_mode(
|
||||
measured_eb_n0_db
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"step": stable_result.step,
|
||||
"measured_eb_n0_db": (
|
||||
stable_result.measured_eb_n0_db
|
||||
),
|
||||
"filtered_eb_n0_db": (
|
||||
stable_result.filtered_eb_n0_db
|
||||
),
|
||||
"naive_mode": naive_mode,
|
||||
"stable_mode": (
|
||||
stable_result.selected_mode
|
||||
),
|
||||
"mode_changed": (
|
||||
stable_result.mode_changed
|
||||
),
|
||||
"pending_upgrade_mode": (
|
||||
stable_result.pending_upgrade_mode
|
||||
),
|
||||
"pending_upgrade_count": (
|
||||
stable_result.pending_upgrade_count
|
||||
),
|
||||
"reason": stable_result.reason,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Число переключений
|
||||
# ============================================================
|
||||
|
||||
def count_mode_switches(
|
||||
modes: list[int],
|
||||
) -> int:
|
||||
"""
|
||||
Посчитать число изменений режима.
|
||||
"""
|
||||
|
||||
return sum(
|
||||
previous_mode != current_mode
|
||||
for previous_mode, current_mode in zip(
|
||||
modes,
|
||||
modes[1:],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
naive_modes = [
|
||||
result["naive_mode"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
stable_modes = [
|
||||
result["stable_mode"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
naive_switch_count = count_mode_switches(
|
||||
naive_modes
|
||||
)
|
||||
|
||||
stable_switch_count = count_mode_switches(
|
||||
stable_modes
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab017. Гистерезис режима канала ==="
|
||||
)
|
||||
|
||||
print("\nРезультаты по шагам:")
|
||||
|
||||
print(
|
||||
f"{'Шаг':>5}"
|
||||
f"{'Eb/N0':>10}"
|
||||
f"{'Фильтр':>11}"
|
||||
f"{'Наивный':>11}"
|
||||
f"{'Устойчивый':>13}"
|
||||
f"{'Изменён':>10}"
|
||||
f"{'Причина':>45}"
|
||||
)
|
||||
|
||||
print("-" * 105)
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['step']:>5}"
|
||||
f"{result['measured_eb_n0_db']:>7.1f} дБ"
|
||||
f"{result['filtered_eb_n0_db']:>8.2f} дБ"
|
||||
f"{result['naive_mode']:>11}"
|
||||
f"{result['stable_mode']:>13}"
|
||||
f"{str(result['mode_changed']):>10}"
|
||||
f" {result['reason']}"
|
||||
)
|
||||
|
||||
|
||||
print("\nЧисло переключений:")
|
||||
|
||||
print(
|
||||
"Наивный адаптер:",
|
||||
naive_switch_count,
|
||||
)
|
||||
|
||||
print(
|
||||
"Устойчивый адаптер:",
|
||||
stable_switch_count,
|
||||
)
|
||||
|
||||
print(
|
||||
"Предотвращено лишних переключений:",
|
||||
naive_switch_count - stable_switch_count,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение CSV
|
||||
# ============================================================
|
||||
|
||||
with CSV_PATH.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=list(
|
||||
results[0].keys()
|
||||
),
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
writer.writerows(results)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# График
|
||||
# ============================================================
|
||||
|
||||
steps = [
|
||||
result["step"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
measured_values = [
|
||||
result["measured_eb_n0_db"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
filtered_values = [
|
||||
result["filtered_eb_n0_db"]
|
||||
for result in results
|
||||
]
|
||||
|
||||
|
||||
figure, axes = plt.subplots(
|
||||
2,
|
||||
1,
|
||||
figsize=(12, 9),
|
||||
sharex=True,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Верхний график: Eb/N0
|
||||
# ------------------------------------------------------------
|
||||
|
||||
axes[0].plot(
|
||||
steps,
|
||||
measured_values,
|
||||
marker=".",
|
||||
label="Измеренное Eb/N0",
|
||||
)
|
||||
|
||||
axes[0].plot(
|
||||
steps,
|
||||
filtered_values,
|
||||
linewidth=2,
|
||||
label="Фильтрованное Eb/N0",
|
||||
)
|
||||
|
||||
axes[0].axhline(
|
||||
6.5,
|
||||
linestyle=":",
|
||||
label="Порог наивного адаптера",
|
||||
)
|
||||
|
||||
axes[0].axhline(
|
||||
8.5,
|
||||
linestyle=":",
|
||||
)
|
||||
|
||||
axes[0].axhline(
|
||||
9.5,
|
||||
linestyle=":",
|
||||
)
|
||||
|
||||
axes[0].set_ylabel(
|
||||
"Eb/N0, дБ"
|
||||
)
|
||||
|
||||
axes[0].set_title(
|
||||
"Фильтрация измерения качества канала"
|
||||
)
|
||||
|
||||
axes[0].grid(
|
||||
True
|
||||
)
|
||||
|
||||
axes[0].legend()
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Нижний график: режимы
|
||||
# ------------------------------------------------------------
|
||||
|
||||
axes[1].step(
|
||||
steps,
|
||||
naive_modes,
|
||||
where="mid",
|
||||
marker=".",
|
||||
label="Наивный адаптер",
|
||||
)
|
||||
|
||||
axes[1].step(
|
||||
steps,
|
||||
stable_modes,
|
||||
where="mid",
|
||||
marker="o",
|
||||
label="Гистерезис и фильтрация",
|
||||
)
|
||||
|
||||
axes[1].set_yticks(
|
||||
[
|
||||
0,
|
||||
128,
|
||||
512,
|
||||
1024,
|
||||
],
|
||||
[
|
||||
"IMAGE OFF",
|
||||
"128 B",
|
||||
"512 B",
|
||||
"1024 B",
|
||||
],
|
||||
)
|
||||
|
||||
axes[1].set_xlabel(
|
||||
"Шаг времени"
|
||||
)
|
||||
|
||||
axes[1].set_ylabel(
|
||||
"Режим"
|
||||
)
|
||||
|
||||
axes[1].set_title(
|
||||
"Сравнение переключений режимов"
|
||||
)
|
||||
|
||||
axes[1].grid(
|
||||
True
|
||||
)
|
||||
|
||||
axes[1].legend()
|
||||
|
||||
|
||||
figure.tight_layout()
|
||||
|
||||
figure.savefig(
|
||||
GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close(
|
||||
figure
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Проверки
|
||||
# ============================================================
|
||||
|
||||
assert results
|
||||
|
||||
assert IMAGE_OFF in stable_modes
|
||||
|
||||
assert FRAGMENT_MODE_1024 in stable_modes
|
||||
|
||||
assert stable_switch_count < naive_switch_count
|
||||
|
||||
assert CSV_PATH.exists()
|
||||
|
||||
assert GRAPH_PATH.exists()
|
||||
|
||||
|
||||
print("\nCSV:")
|
||||
|
||||
print(CSV_PATH)
|
||||
|
||||
print("\nГрафик:")
|
||||
|
||||
print(GRAPH_PATH)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"гистерезис уменьшил количество переключений."
|
||||
)
|
||||
1148
Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py
Normal file
1148
Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py
Normal file
File diff suppressed because it is too large
Load Diff
1212
Projects/SDR_Rover/tests/lab019_bpsk_receiver.py
Normal file
1212
Projects/SDR_Rover/tests/lab019_bpsk_receiver.py
Normal file
File diff suppressed because it is too large
Load Diff
1494
Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py
Normal file
1494
Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py
Normal file
File diff suppressed because it is too large
Load Diff
1483
Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py
Normal file
1483
Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py
Normal file
File diff suppressed because it is too large
Load Diff
1716
Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py
Normal file
1716
Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py
Normal file
File diff suppressed because it is too large
Load Diff
1637
Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py
Normal file
1637
Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py
Normal file
File diff suppressed because it is too large
Load Diff
172
Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py
Normal file
172
Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Lab024A. Первый приём реальных IQ-сэмплов с Pluto+.
|
||||
|
||||
Схема:
|
||||
антенна 40–860 МГц -> RX1
|
||||
|
||||
Передатчики TX1 и TX2 не используются.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
import adi
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Параметры приёмника
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
PLUTO_URI = "ip:192.168.2.1"
|
||||
|
||||
CENTER_FREQUENCY_HZ = 100_000_000
|
||||
SAMPLE_RATE_HZ = 2_400_000
|
||||
RX_BANDWIDTH_HZ = 2_000_000
|
||||
|
||||
RX_BUFFER_SIZE = 262_144
|
||||
|
||||
OUTPUT_DIRECTORY = Path("data/raw")
|
||||
IQ_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.npy"
|
||||
METADATA_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.json"
|
||||
SPECTRUM_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz_spectrum.png"
|
||||
|
||||
|
||||
def calculate_spectrum(
|
||||
samples: np.ndarray,
|
||||
sample_rate_hz: float,
|
||||
center_frequency_hz: float,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Рассчитывает спектр принятого комплексного IQ-сигнала.
|
||||
|
||||
Возвращает:
|
||||
frequencies_hz — абсолютные радиочастоты;
|
||||
power_db — относительная мощность спектра в дБ.
|
||||
"""
|
||||
|
||||
sample_count = len(samples)
|
||||
|
||||
window = np.hanning(sample_count)
|
||||
windowed_samples = samples * window
|
||||
|
||||
spectrum = np.fft.fftshift(np.fft.fft(windowed_samples))
|
||||
power = np.abs(spectrum) ** 2
|
||||
|
||||
power_db = 10.0 * np.log10(power + 1e-12)
|
||||
power_db -= np.max(power_db)
|
||||
|
||||
baseband_frequencies_hz = np.fft.fftshift(
|
||||
np.fft.fftfreq(sample_count, d=1.0 / sample_rate_hz)
|
||||
)
|
||||
|
||||
absolute_frequencies_hz = (
|
||||
center_frequency_hz + baseband_frequencies_hz
|
||||
)
|
||||
|
||||
return absolute_frequencies_hz, power_db
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("Подключение к Pluto+...")
|
||||
sdr = adi.Pluto(uri=PLUTO_URI)
|
||||
|
||||
# Используем только первый приёмный канал RX1.
|
||||
sdr.rx_enabled_channels = [0]
|
||||
|
||||
sdr.sample_rate = SAMPLE_RATE_HZ
|
||||
sdr.rx_lo = CENTER_FREQUENCY_HZ
|
||||
sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ
|
||||
|
||||
# Автоматическая регулировка усиления.
|
||||
sdr.gain_control_mode_chan0 = "slow_attack"
|
||||
|
||||
sdr.rx_buffer_size = RX_BUFFER_SIZE
|
||||
|
||||
print()
|
||||
print("Параметры приёмника:")
|
||||
print(f" URI: {PLUTO_URI}")
|
||||
print(f" Центральная частота: {sdr.rx_lo / 1e6:.3f} МГц")
|
||||
print(f" Частота дискретизации: {sdr.sample_rate / 1e6:.3f} Мвыб/с")
|
||||
print(f" Полоса RX: {sdr.rx_rf_bandwidth / 1e6:.3f} МГц")
|
||||
print(f" Режим усиления: {sdr.gain_control_mode_chan0}")
|
||||
print(f" Размер буфера: {sdr.rx_buffer_size} отсчётов")
|
||||
|
||||
print()
|
||||
print("Получение IQ-сэмплов...")
|
||||
|
||||
# Первый буфер после перенастройки иногда содержит переходный процесс.
|
||||
_ = sdr.rx()
|
||||
|
||||
# Второй буфер сохраняем и анализируем.
|
||||
samples = np.asarray(sdr.rx(), dtype=np.complex64)
|
||||
|
||||
print("IQ-сэмплы получены.")
|
||||
|
||||
np.save(IQ_FILE_PATH, samples)
|
||||
|
||||
mean_value = np.mean(samples)
|
||||
rms_value = np.sqrt(np.mean(np.abs(samples) ** 2))
|
||||
peak_value = np.max(np.abs(samples))
|
||||
|
||||
metadata = {
|
||||
"pluto_uri": PLUTO_URI,
|
||||
"center_frequency_hz": int(sdr.rx_lo),
|
||||
"sample_rate_hz": int(sdr.sample_rate),
|
||||
"rx_bandwidth_hz": int(sdr.rx_rf_bandwidth),
|
||||
"gain_control_mode": sdr.gain_control_mode_chan0,
|
||||
"sample_count": int(len(samples)),
|
||||
"sample_dtype": str(samples.dtype),
|
||||
"mean_i": float(np.real(mean_value)),
|
||||
"mean_q": float(np.imag(mean_value)),
|
||||
"rms": float(rms_value),
|
||||
"peak": float(peak_value),
|
||||
}
|
||||
|
||||
with METADATA_FILE_PATH.open("w", encoding="utf-8") as metadata_file:
|
||||
json.dump(metadata, metadata_file, ensure_ascii=False, indent=4)
|
||||
|
||||
frequencies_hz, power_db = calculate_spectrum(
|
||||
samples=samples,
|
||||
sample_rate_hz=float(sdr.sample_rate),
|
||||
center_frequency_hz=float(sdr.rx_lo),
|
||||
)
|
||||
|
||||
plt.figure(figsize=(12, 6))
|
||||
plt.plot(frequencies_hz / 1e6, power_db)
|
||||
|
||||
plt.title("Lab024A. Спектр сигнала, принятого Pluto+")
|
||||
plt.xlabel("Частота, МГц")
|
||||
plt.ylabel("Относительная мощность, дБ")
|
||||
plt.grid(True)
|
||||
plt.ylim(-100, 5)
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(SPECTRUM_FILE_PATH, dpi=150)
|
||||
plt.show()
|
||||
|
||||
print()
|
||||
print("Статистика:")
|
||||
print(f" Количество сэмплов: {len(samples)}")
|
||||
print(f" Тип данных: {samples.dtype}")
|
||||
print(f" Среднее I: {np.real(mean_value):.3f}")
|
||||
print(f" Среднее Q: {np.imag(mean_value):.3f}")
|
||||
print(f" RMS: {rms_value:.3f}")
|
||||
print(f" Пиковая амплитуда: {peak_value:.3f}")
|
||||
|
||||
print()
|
||||
print("Созданы файлы:")
|
||||
print(f" IQ: {IQ_FILE_PATH}")
|
||||
print(f" Метаданные:{METADATA_FILE_PATH}")
|
||||
print(f" Спектр: {SPECTRUM_FILE_PATH}")
|
||||
|
||||
print()
|
||||
print("Lab024A выполнена успешно.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
573
Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py
Normal file
573
Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Lab024B. Усреднённый спектр Pluto+ методом Уэлча.
|
||||
|
||||
Схема подключения:
|
||||
антенна 40–860 МГц -> RX1
|
||||
|
||||
Передатчики TX1 и TX2 не используются.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import csv
|
||||
|
||||
import adi
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from scipy.signal import find_peaks, welch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Настройки Pluto+
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
PLUTO_URI = "ip:192.168.2.1"
|
||||
|
||||
CENTER_FREQUENCY_HZ = 100_000_000
|
||||
SAMPLE_RATE_HZ = 2_400_000
|
||||
RX_BANDWIDTH_HZ = 2_000_000
|
||||
|
||||
RX_BUFFER_SIZE = 65_536
|
||||
BUFFER_COUNT = 16
|
||||
DISCARD_BUFFER_COUNT = 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Настройки спектрального анализа
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
WELCH_SEGMENT_LENGTH = 8_192
|
||||
WELCH_OVERLAP_LENGTH = 4_096
|
||||
|
||||
SMOOTHING_BANDWIDTH_HZ = 40_000
|
||||
|
||||
MINIMUM_PEAK_DISTANCE_HZ = 120_000
|
||||
MINIMUM_PEAK_PROMINENCE_DB = 4.0
|
||||
MINIMUM_PEAK_HEIGHT_DB = -25.0
|
||||
|
||||
DC_EXCLUSION_HALF_WIDTH_HZ = 40_000
|
||||
|
||||
MAXIMUM_CANDIDATE_COUNT = 12
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Выходные файлы
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
OUTPUT_DIRECTORY = Path("data/processed/lab024b")
|
||||
|
||||
SPECTRUM_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.csv"
|
||||
CANDIDATES_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_fm_candidates.csv"
|
||||
GRAPH_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.png"
|
||||
REPORT_PATH = OUTPUT_DIRECTORY / "lab024b_report.txt"
|
||||
|
||||
|
||||
def configure_receiver() -> adi.Pluto:
|
||||
"""
|
||||
Подключается к Pluto+ и настраивает приёмник RX1.
|
||||
"""
|
||||
|
||||
print("Подключение к Pluto+...")
|
||||
|
||||
sdr = adi.Pluto(uri=PLUTO_URI)
|
||||
|
||||
# Используем первый приёмный канал.
|
||||
sdr.rx_enabled_channels = [0]
|
||||
|
||||
sdr.sample_rate = SAMPLE_RATE_HZ
|
||||
sdr.rx_lo = CENTER_FREQUENCY_HZ
|
||||
sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ
|
||||
|
||||
# Автоматическая регулировка усиления.
|
||||
sdr.gain_control_mode_chan0 = "slow_attack"
|
||||
|
||||
sdr.rx_buffer_size = RX_BUFFER_SIZE
|
||||
|
||||
return sdr
|
||||
|
||||
|
||||
def receive_samples(sdr: adi.Pluto) -> np.ndarray:
|
||||
"""
|
||||
Получает несколько последовательных IQ-буферов.
|
||||
|
||||
Первые буферы отбрасываются, поскольку сразу после настройки
|
||||
приёмника могут наблюдаться переходные процессы АРУ и фильтров.
|
||||
"""
|
||||
|
||||
print()
|
||||
print("Отбрасывание переходных буферов...")
|
||||
|
||||
for _ in range(DISCARD_BUFFER_COUNT):
|
||||
_ = sdr.rx()
|
||||
|
||||
received_buffers: list[np.ndarray] = []
|
||||
|
||||
print("Получение рабочих IQ-буферов...")
|
||||
|
||||
for buffer_number in range(1, BUFFER_COUNT + 1):
|
||||
samples = np.asarray(sdr.rx(), dtype=np.complex64)
|
||||
received_buffers.append(samples)
|
||||
|
||||
print(
|
||||
f" Буфер {buffer_number:02d}/{BUFFER_COUNT}: "
|
||||
f"{len(samples)} отсчётов"
|
||||
)
|
||||
|
||||
combined_samples = np.concatenate(received_buffers)
|
||||
|
||||
if len(combined_samples) == 0:
|
||||
raise RuntimeError("Pluto+ не вернул IQ-сэмплы.")
|
||||
|
||||
if not np.all(np.isfinite(combined_samples)):
|
||||
raise RuntimeError("В IQ-буфере обнаружены NaN или Inf.")
|
||||
|
||||
return combined_samples
|
||||
|
||||
|
||||
def calculate_welch_spectrum(
|
||||
samples: np.ndarray,
|
||||
sample_rate_hz: float,
|
||||
center_frequency_hz: float,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Рассчитывает спектральную плотность мощности методом Уэлча.
|
||||
|
||||
Возвращает:
|
||||
absolute_frequencies_hz — абсолютные радиочастоты;
|
||||
power_density — мощность в линейном масштабе.
|
||||
"""
|
||||
|
||||
baseband_frequencies_hz, power_density = welch(
|
||||
samples,
|
||||
fs=sample_rate_hz,
|
||||
window="hann",
|
||||
nperseg=WELCH_SEGMENT_LENGTH,
|
||||
noverlap=WELCH_OVERLAP_LENGTH,
|
||||
detrend=False,
|
||||
return_onesided=False,
|
||||
scaling="density",
|
||||
)
|
||||
|
||||
baseband_frequencies_hz = np.fft.fftshift(
|
||||
baseband_frequencies_hz
|
||||
)
|
||||
|
||||
power_density = np.fft.fftshift(power_density)
|
||||
|
||||
absolute_frequencies_hz = (
|
||||
center_frequency_hz + baseband_frequencies_hz
|
||||
)
|
||||
|
||||
return absolute_frequencies_hz, power_density
|
||||
|
||||
|
||||
def smooth_power_spectrum(
|
||||
power_density: np.ndarray,
|
||||
frequency_step_hz: float,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Сглаживает спектр скользящим средним в линейном масштабе.
|
||||
|
||||
Усреднять следует мощность, а не значения в децибелах.
|
||||
"""
|
||||
|
||||
smoothing_bin_count = int(
|
||||
round(SMOOTHING_BANDWIDTH_HZ / frequency_step_hz)
|
||||
)
|
||||
|
||||
smoothing_bin_count = max(1, smoothing_bin_count)
|
||||
|
||||
# Нечётная длина ядра обеспечивает симметрию относительно центра.
|
||||
if smoothing_bin_count % 2 == 0:
|
||||
smoothing_bin_count += 1
|
||||
|
||||
kernel = (
|
||||
np.ones(smoothing_bin_count, dtype=np.float64)
|
||||
/ smoothing_bin_count
|
||||
)
|
||||
|
||||
smoothed_power = np.convolve(
|
||||
power_density,
|
||||
kernel,
|
||||
mode="same",
|
||||
)
|
||||
|
||||
return smoothed_power
|
||||
|
||||
|
||||
def convert_power_to_relative_db(
|
||||
raw_power: np.ndarray,
|
||||
smoothed_power: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Переводит мощность в относительные децибелы.
|
||||
|
||||
0 дБ соответствует максимальному значению сглаженного спектра.
|
||||
"""
|
||||
|
||||
minimum_positive_value = np.finfo(np.float64).tiny
|
||||
|
||||
raw_power_db = 10.0 * np.log10(
|
||||
raw_power + minimum_positive_value
|
||||
)
|
||||
|
||||
smoothed_power_db = 10.0 * np.log10(
|
||||
smoothed_power + minimum_positive_value
|
||||
)
|
||||
|
||||
reference_power_db = np.max(smoothed_power_db)
|
||||
|
||||
raw_relative_db = raw_power_db - reference_power_db
|
||||
smoothed_relative_db = smoothed_power_db - reference_power_db
|
||||
|
||||
return raw_relative_db, smoothed_relative_db
|
||||
|
||||
|
||||
def detect_signal_candidates(
|
||||
frequencies_hz: np.ndarray,
|
||||
smoothed_power_db: np.ndarray,
|
||||
center_frequency_hz: float,
|
||||
) -> list[dict[str, float]]:
|
||||
"""
|
||||
Ищет локальные максимумы сглаженного спектра.
|
||||
|
||||
Максимумы сначала рассчитываются по исходному спектру.
|
||||
После этого отбрасываются точки из DC-зоны и с краёв диапазона.
|
||||
|
||||
Такой порядок не создаёт искусственных провалов -200 дБ
|
||||
и не искажает значение prominence.
|
||||
"""
|
||||
|
||||
frequency_step_hz = float(
|
||||
np.mean(np.diff(frequencies_hz))
|
||||
)
|
||||
|
||||
minimum_distance_bins = max(
|
||||
1,
|
||||
int(
|
||||
round(
|
||||
MINIMUM_PEAK_DISTANCE_HZ
|
||||
/ abs(frequency_step_hz)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
edge_bin_count = max(
|
||||
1,
|
||||
int(
|
||||
round(
|
||||
SMOOTHING_BANDWIDTH_HZ
|
||||
/ abs(frequency_step_hz)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Важно: find_peaks получает настоящий спектр без вставок -200 дБ.
|
||||
peak_indices, peak_properties = find_peaks(
|
||||
smoothed_power_db,
|
||||
height=MINIMUM_PEAK_HEIGHT_DB,
|
||||
prominence=MINIMUM_PEAK_PROMINENCE_DB,
|
||||
distance=minimum_distance_bins,
|
||||
)
|
||||
|
||||
candidates: list[dict[str, float]] = []
|
||||
|
||||
for peak_number, peak_index in enumerate(peak_indices):
|
||||
frequency_hz = float(frequencies_hz[peak_index])
|
||||
|
||||
# Отбрасываем возможный аппаратный DC-пик.
|
||||
if (
|
||||
abs(frequency_hz - center_frequency_hz)
|
||||
<= DC_EXCLUSION_HALF_WIDTH_HZ
|
||||
):
|
||||
continue
|
||||
|
||||
# Отбрасываем максимумы возле краёв наблюдаемого диапазона.
|
||||
if peak_index < edge_bin_count:
|
||||
continue
|
||||
|
||||
if peak_index >= len(smoothed_power_db) - edge_bin_count:
|
||||
continue
|
||||
|
||||
candidates.append(
|
||||
{
|
||||
"frequency_hz": frequency_hz,
|
||||
"relative_power_db": float(
|
||||
smoothed_power_db[peak_index]
|
||||
),
|
||||
"prominence_db": float(
|
||||
peak_properties["prominences"][peak_number]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Оставляем наиболее выраженные сигналы.
|
||||
candidates.sort(
|
||||
key=lambda item: item["prominence_db"],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
candidates = candidates[:MAXIMUM_CANDIDATE_COUNT]
|
||||
|
||||
# В итоговой таблице располагаем сигналы по частоте.
|
||||
candidates.sort(
|
||||
key=lambda item: item["frequency_hz"]
|
||||
)
|
||||
|
||||
return candidates
|
||||
|
||||
def save_candidates_csv(
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Сохраняет найденные кандидаты на радиосигналы.
|
||||
"""
|
||||
|
||||
with CANDIDATES_CSV_PATH.open(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
) as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
"frequency_hz",
|
||||
"frequency_mhz",
|
||||
"relative_power_db",
|
||||
"prominence_db",
|
||||
]
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
writer.writerow(
|
||||
[
|
||||
f"{candidate['frequency_hz']:.3f}",
|
||||
f"{candidate['frequency_hz'] / 1e6:.6f}",
|
||||
f"{candidate['relative_power_db']:.3f}",
|
||||
f"{candidate['prominence_db']:.3f}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def save_report(
|
||||
sample_count: int,
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Создаёт текстовый отчёт лабораторной.
|
||||
"""
|
||||
|
||||
report_lines = [
|
||||
"Lab024B. Усреднённый спектр Pluto+ методом Уэлча",
|
||||
"",
|
||||
f"URI: {PLUTO_URI}",
|
||||
f"Центральная частота: {CENTER_FREQUENCY_HZ / 1e6:.3f} МГц",
|
||||
f"Частота дискретизации: {SAMPLE_RATE_HZ / 1e6:.3f} Мвыб/с",
|
||||
f"Полоса RX: {RX_BANDWIDTH_HZ / 1e6:.3f} МГц",
|
||||
f"Количество IQ-сэмплов: {sample_count}",
|
||||
f"Размер сегмента Уэлча: {WELCH_SEGMENT_LENGTH}",
|
||||
f"Перекрытие сегментов: {WELCH_OVERLAP_LENGTH}",
|
||||
"",
|
||||
f"Найдено кандидатов: {len(candidates)}",
|
||||
"",
|
||||
]
|
||||
|
||||
for candidate_number, candidate in enumerate(
|
||||
candidates,
|
||||
start=1,
|
||||
):
|
||||
report_lines.append(
|
||||
f"{candidate_number:02d}. "
|
||||
f"{candidate['frequency_hz'] / 1e6:.6f} МГц, "
|
||||
f"уровень {candidate['relative_power_db']:.2f} дБ, "
|
||||
f"выраженность {candidate['prominence_db']:.2f} дБ"
|
||||
)
|
||||
|
||||
REPORT_PATH.write_text(
|
||||
"\n".join(report_lines),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def create_graph(
|
||||
frequencies_hz: np.ndarray,
|
||||
raw_power_db: np.ndarray,
|
||||
smoothed_power_db: np.ndarray,
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Строит исходный и сглаженный спектры.
|
||||
"""
|
||||
|
||||
plt.figure(figsize=(13, 7))
|
||||
|
||||
plt.plot(
|
||||
frequencies_hz / 1e6,
|
||||
raw_power_db,
|
||||
linewidth=0.6,
|
||||
alpha=0.45,
|
||||
label="Спектр Уэлча",
|
||||
)
|
||||
|
||||
plt.plot(
|
||||
frequencies_hz / 1e6,
|
||||
smoothed_power_db,
|
||||
linewidth=1.5,
|
||||
label="Сглаженный спектр",
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
frequency_mhz = candidate["frequency_hz"] / 1e6
|
||||
power_db = candidate["relative_power_db"]
|
||||
|
||||
plt.scatter(
|
||||
[frequency_mhz],
|
||||
[power_db],
|
||||
marker="o",
|
||||
)
|
||||
|
||||
plt.annotate(
|
||||
f"{frequency_mhz:.3f}",
|
||||
xy=(frequency_mhz, power_db),
|
||||
xytext=(0, 10),
|
||||
textcoords="offset points",
|
||||
ha="center",
|
||||
fontsize=8,
|
||||
rotation=45,
|
||||
)
|
||||
|
||||
plt.axvspan(
|
||||
(CENTER_FREQUENCY_HZ - DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6,
|
||||
(CENTER_FREQUENCY_HZ + DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6,
|
||||
alpha=0.15,
|
||||
label="Исключённая DC-зона",
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Lab024B. Усреднённый спектр Pluto+ и кандидаты на FM-сигналы"
|
||||
)
|
||||
plt.xlabel("Частота, МГц")
|
||||
plt.ylabel("Относительная мощность, дБ")
|
||||
plt.grid(True)
|
||||
plt.ylim(-60, 5)
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(GRAPH_PATH, dpi=160)
|
||||
plt.show()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sdr = configure_receiver()
|
||||
|
||||
print()
|
||||
print("Параметры приёмника:")
|
||||
print(f" URI: {PLUTO_URI}")
|
||||
print(
|
||||
f" Центральная частота: "
|
||||
f"{sdr.rx_lo / 1e6:.3f} МГц"
|
||||
)
|
||||
print(
|
||||
f" Частота дискретизации: "
|
||||
f"{sdr.sample_rate / 1e6:.3f} Мвыб/с"
|
||||
)
|
||||
print(
|
||||
f" Полоса RX: "
|
||||
f"{sdr.rx_rf_bandwidth / 1e6:.3f} МГц"
|
||||
)
|
||||
print(
|
||||
f" Режим усиления: "
|
||||
f"{sdr.gain_control_mode_chan0}"
|
||||
)
|
||||
|
||||
samples = receive_samples(sdr)
|
||||
|
||||
print()
|
||||
print(f"Всего получено: {len(samples)} IQ-сэмплов")
|
||||
|
||||
frequencies_hz, power_density = calculate_welch_spectrum(
|
||||
samples=samples,
|
||||
sample_rate_hz=float(sdr.sample_rate),
|
||||
center_frequency_hz=float(sdr.rx_lo),
|
||||
)
|
||||
|
||||
frequency_step_hz = float(
|
||||
np.mean(np.diff(frequencies_hz))
|
||||
)
|
||||
|
||||
smoothed_power = smooth_power_spectrum(
|
||||
power_density=power_density,
|
||||
frequency_step_hz=frequency_step_hz,
|
||||
)
|
||||
|
||||
raw_power_db, smoothed_power_db = (
|
||||
convert_power_to_relative_db(
|
||||
raw_power=power_density,
|
||||
smoothed_power=smoothed_power,
|
||||
)
|
||||
)
|
||||
|
||||
candidates = detect_signal_candidates(
|
||||
frequencies_hz=frequencies_hz,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
center_frequency_hz=float(sdr.rx_lo),
|
||||
)
|
||||
|
||||
save_spectrum_csv(
|
||||
frequencies_hz=frequencies_hz,
|
||||
raw_power_db=raw_power_db,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
)
|
||||
|
||||
save_candidates_csv(candidates)
|
||||
save_report(len(samples), candidates)
|
||||
|
||||
print()
|
||||
print("Найденные кандидаты на радиосигналы:")
|
||||
print()
|
||||
|
||||
if candidates:
|
||||
print(
|
||||
" № Частота, МГц Уровень, дБ "
|
||||
"Выраженность, дБ"
|
||||
)
|
||||
print(
|
||||
" -- ------------ ----------- "
|
||||
"-----------------"
|
||||
)
|
||||
|
||||
for candidate_number, candidate in enumerate(
|
||||
candidates,
|
||||
start=1,
|
||||
):
|
||||
print(
|
||||
f" {candidate_number:2d} "
|
||||
f"{candidate['frequency_hz'] / 1e6:12.6f} "
|
||||
f"{candidate['relative_power_db']:11.2f} "
|
||||
f"{candidate['prominence_db']:17.2f}"
|
||||
)
|
||||
else:
|
||||
print(" Кандидаты не найдены.")
|
||||
|
||||
create_graph(
|
||||
frequencies_hz=frequencies_hz,
|
||||
raw_power_db=raw_power_db,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
candidates=candidates,
|
||||
)
|
||||
|
||||
print()
|
||||
print("Созданы файлы:")
|
||||
print(f" Спектр: {SPECTRUM_CSV_PATH}")
|
||||
print(f" Кандидаты: {CANDIDATES_CSV_PATH}")
|
||||
print(f" График: {GRAPH_PATH}")
|
||||
print(f" Отчёт: {REPORT_PATH}")
|
||||
|
||||
print()
|
||||
print("Lab024B выполнена успешно.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user