Compare commits
7 Commits
f6416ca106
...
ac0e3944ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0e3944ab | ||
|
|
3cceda32f4 | ||
|
|
24b7cc402e | ||
|
|
9d6452c860 | ||
|
|
b00af27058 | ||
|
|
8e296daacc | ||
|
|
941a7cf6e6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ progress-server
|
||||
|
||||
# Тестовые бинарники
|
||||
*.test
|
||||
test.jpg
|
||||
|
||||
# Результаты тестов с покрытием
|
||||
*.out
|
||||
|
||||
320
SDR/README.md
Normal file
320
SDR/README.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# OFDM Radio Link — PlutoSDR
|
||||
|
||||
Система цифровой радиосвязи на базе PlutoSDR с использованием OFDM-модуляции и помехоустойчивого кодирования Рида-Соломона RS(255,223).
|
||||
|
||||
## Оглавление
|
||||
|
||||
- [Возможности](#возможности)
|
||||
- [Требования](#требования)
|
||||
- [Структура проекта](#структура-проекта)
|
||||
- [Быстрый старт](#быстрый-старт)
|
||||
- [Сборка](#сборка)
|
||||
- [Использование](#использование)
|
||||
- [Параметры командной строки](#параметры-командной-строки)
|
||||
- [Формат кадра](#формат-кадра)
|
||||
- [Диагностика](#диагностика)
|
||||
- [Устранение неполадок](#устранение-неполадок)
|
||||
- [Лицензия](#лицензия)
|
||||
|
||||
## Возможности
|
||||
|
||||
- **OFDM-модуляция**: 64 поднесущих, циклический префикс 16, сглаживающее окно 4
|
||||
- **Помехоустойчивое кодирование**: Рида-Соломона RS(255,223) с CRC32
|
||||
- **Частотная коррекция**: адаптивная компенсация CFO через NCO с экспоненциальным фильтром (α=0.95)
|
||||
- **Ручное управление усилением**: фиксированное усиление для максимальной стабильности
|
||||
- **Потоковая передача**: работа через stdin/stdout для конвейерной обработки
|
||||
- **Мониторинг в реальном времени**: RSSI, EVM, CFO, статистика кадров
|
||||
- **Гибкая настройка**: частота, полоса, усиление, амплитуда, паузы между кадрами
|
||||
|
||||
## Требования
|
||||
|
||||
### Аппаратное обеспечение
|
||||
- 2× PlutoSDR (один на передачу, один на приём) или 1× для тестирования в петле
|
||||
- Raspberry Pi (рекомендуется) или любой Linux-компьютер
|
||||
- Антенны/аттенюаторы для RF-тракта
|
||||
|
||||
### Программное обеспечение
|
||||
```bash
|
||||
# Установка зависимостей
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential python3 cmake libfftw3-dev
|
||||
|
||||
# libiio (для работы с PlutoSDR)
|
||||
sudo apt install -y libiio-dev
|
||||
|
||||
# liquid-dsp (OFDM и FEC)
|
||||
git clone https://github.com/jgaeddert/liquid-dsp.git
|
||||
cd liquid-dsp
|
||||
./bootstrap.sh
|
||||
./configure
|
||||
make -j4
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
pluto/
|
||||
├── common.h # Общие определения и прототипы
|
||||
├── common.c # Реализация общих функций
|
||||
├── transmitter.c # Передатчик
|
||||
├── receiver.c # Приёмник с частотной коррекцией
|
||||
├── Makefile # Система сборки
|
||||
├── README.md # Этот файл
|
||||
└── test_input.bin # Тестовый файл (создаётся make test)
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# 1. Сборка
|
||||
make
|
||||
|
||||
# 2. Создание тестового файла (1 МБ)
|
||||
make test
|
||||
|
||||
# 3. Запуск приёмника (на одном устройстве)
|
||||
./receiver -f 1265000000 -r 3840000 -b 2000000 -c rs8 > output.bin
|
||||
|
||||
# 4. Запуск передатчика (на другом устройстве)
|
||||
cat test_input.bin | ./transmitter -f 1265000000 -r 3840000 -b 2000000 -g -20 -a 0.3 -c rs8
|
||||
|
||||
# 5. Проверка результата
|
||||
md5sum test_input.bin output.bin
|
||||
```
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
# Сборка всего проекта
|
||||
make
|
||||
|
||||
# Сборка только передатчика (если есть transmitter.c)
|
||||
make transmitter
|
||||
|
||||
# Сборка только приёмника (если есть receiver.c)
|
||||
make receiver
|
||||
|
||||
# Создание тестового файла 1 МБ
|
||||
make test
|
||||
|
||||
# Очистка
|
||||
make clean
|
||||
|
||||
# Справка
|
||||
make help
|
||||
```
|
||||
|
||||
**Флаги компиляции:**
|
||||
- `-Wall -O2` — все предупреждения и оптимизация
|
||||
- `-liio` — библиотека libiio для PlutoSDR
|
||||
- `-lliquid` — liquid-dsp (OFDM, FEC, NCO)
|
||||
- `-lfftw3f` — FFTW для преобразований Фурье
|
||||
- `-lm -lpthread` — математика и потоки
|
||||
|
||||
## Использование
|
||||
|
||||
### Передатчик
|
||||
|
||||
```bash
|
||||
# Базовая передача тестового файла
|
||||
cat test_input.bin | ./transmitter -f 1265000000 -r 3840000 -b 2000000 -c rs8
|
||||
|
||||
# Передача случайных данных
|
||||
cat /dev/urandom | ./transmitter -f 1265000000 -r 3840000 -b 2000000 -g -20 -a 0.3
|
||||
|
||||
# Высокоскоростная передача без пауз
|
||||
cat data.bin | ./transmitter -f 2400000000 -r 7680000 -b 6000000 -g 0 -a 0.5 -p 0
|
||||
|
||||
# Передача текста
|
||||
echo "Hello, OFDM World!" | ./transmitter -f 1265000000 -r 3840000 -b 2000000
|
||||
```
|
||||
|
||||
### Приёмник
|
||||
|
||||
```bash
|
||||
# Базовый приём с декодированием FEC
|
||||
./receiver -f 1265000000 -r 3840000 -b 2000000 -c rs8 > output.bin
|
||||
|
||||
# Приём без FEC (выше скорость, но нет исправления ошибок)
|
||||
./receiver -f 1265000000 -r 3840000 -b 2000000 -c none > output.bin
|
||||
|
||||
# Приём с сохранением лога статистики
|
||||
./receiver -f 1265000000 -r 3840000 -b 2000000 -c rs8 > output.bin 2> stats.log
|
||||
|
||||
# Мониторинг статистики в реальном времени
|
||||
watch -n 1 'tail -1 stats.log'
|
||||
```
|
||||
|
||||
### Тестирование на одном устройстве (петля)
|
||||
|
||||
```bash
|
||||
# Терминал 1: Приёмник
|
||||
./receiver -f 2400000000 -r 3840000 -b 2000000 -c rs8 > output.bin
|
||||
|
||||
# Терминал 2: Передатчик
|
||||
cat test_input.bin | ./transmitter -f 2400000000 -r 3840000 -b 2000000 -g -40 -c rs8
|
||||
```
|
||||
|
||||
## Параметры командной строки
|
||||
|
||||
### Общие параметры (для TX и RX)
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `-f ЧАСТОТА` | Несущая частота в Гц | 1265000000 (1265 МГц) |
|
||||
| `-r СКОРОСТЬ` | Частота дискретизации в Гц | 3840000 (3.84 МГц) |
|
||||
| `-b ПОЛОСА` | Полоса пропускания в Гц | 3000000 (3 МГц) |
|
||||
| `-u URI` | Адрес PlutoSDR | ip:192.168.2.1 (RX) / ip:192.168.3.1 (TX) |
|
||||
| `-c РЕЖИМ` | FEC: `rs8` или `none` | rs8 |
|
||||
| `-h` | Показать справку | — |
|
||||
|
||||
### Специфичные параметры передатчика
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `-g УСИЛЕНИЕ` | Усиление TX в дБ | -30.0 |
|
||||
| `-a АМПЛИТУДА` | Амплитуда сигнала (0..1) | 0.2 |
|
||||
| `-p ПАУЗА` | Пауза между кадрами в мкс | 2000 |
|
||||
|
||||
### Специфичные параметры приёмника
|
||||
|
||||
| Параметр | Значение | Примечание |
|
||||
|----------|----------|------------|
|
||||
| Усиление | 50 дБ | Фиксированное, ручной режим |
|
||||
| CFO фильтр | α=0.95 | Экспоненциальное сглаживание |
|
||||
|
||||
## Формат кадра
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OFDM КАДР │
|
||||
├──────────┬──────────────────────────────────────────────────────┤
|
||||
│ Заголовок│ Полезная нагрузка │
|
||||
│ 12 байт │ до 1024 байт │
|
||||
└──────────┴──────────────────────────────────────────────────────┘
|
||||
|
||||
Заголовок (12 байт):
|
||||
┌───────┬───────┬──────────────────┬───────────────┬───────────────┬──────────────┬──────────┐
|
||||
│ Байты │ 0-1 │ 2-5 │ 6-7 │ 8-9 │ 10 │ 11 │
|
||||
├───────┼───────┼──────────────────┼───────────────┼───────────────┼──────────────┼──────────┤
|
||||
│ Знач. │ 0xF0 │ Порядковый номер │ Длина исходных│ Кол-во блоков │ Последний │ Резерв │
|
||||
│ │ 0xAA │ кадра (32 бита) │ данных (16 бит)│ FEC (16 бит) │ блок RS │ │
|
||||
└───────┴───────┴──────────────────┴───────────────┴───────────────┴──────────────┴──────────┘
|
||||
```
|
||||
|
||||
## Диагностика
|
||||
|
||||
### Вывод приёмника в реальном времени
|
||||
|
||||
```
|
||||
Кадры | Загол. | CRC | RS испр | Вывод | RSSI | EVM | CFO
|
||||
--------+--------+------+---------+--------+-------+-------+----------
|
||||
1234 | 1200 | 1180 | 15 | 1195 | -45 | 2.3 | 0.0015
|
||||
```
|
||||
|
||||
**Расшифровка:**
|
||||
- **Кадры** — всего обнаружено кадров синхронизатором
|
||||
- **Загол.** — количество корректно декодированных заголовков
|
||||
- **CRC** — кадры с корректной контрольной суммой
|
||||
- **RS испр** — кадры, исправленные кодом Рида-Соломона
|
||||
- **Вывод** — успешно выведено кадров в stdout
|
||||
- **RSSI** — уровень сигнала в дБм (чем ближе к 0, тем лучше)
|
||||
- **EVM** — вектор ошибки в % (чем меньше, тем лучше)
|
||||
- **CFO** — оценка частотного сдвига в рад/семпл
|
||||
|
||||
### Итоговая статистика
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════
|
||||
ПРИЁМ ЗАВЕРШЁН
|
||||
═══════════════════════════════════════════════════
|
||||
Обнаружено кадров: 1024
|
||||
Корректных заголовков: 1024 (100.0%)
|
||||
Корректных CRC: 1024 (100.0%)
|
||||
Исправлено RS: 0
|
||||
Успешно выведено: 1024 кадров
|
||||
Всего принято данных: 1048576 байт (1.00 МБ)
|
||||
Финальная оценка CFO: 0.0012 рад/семпл
|
||||
═══════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
### Ошибка подключения к PlutoSDR
|
||||
|
||||
```bash
|
||||
# Проверка подключения
|
||||
iio_info -s
|
||||
|
||||
# Пинг устройства
|
||||
ping 192.168.2.1
|
||||
|
||||
# Проверка интерфейса
|
||||
ifconfig eth1
|
||||
```
|
||||
|
||||
### Нет приёма сигнала
|
||||
|
||||
1. **Проверьте частоту** — убедитесь, что TX и RX на одной частоте
|
||||
2. **Увеличьте усиление TX** — попробуйте `-g 0` вместо `-g -30`
|
||||
3. **Уменьшите расстояние** — или используйте проводное соединение
|
||||
4. **Проверьте антенны** — убедитесь, что они подключены к правильным портам
|
||||
5. **Используйте петлю** — подключите TX напрямую к RX через аттенюатор 30-40 дБ
|
||||
|
||||
### Высокий EVM / много ошибок
|
||||
|
||||
1. **Уменьшите полосу** — `-b 1500000` вместо `-b 3000000`
|
||||
2. **Увеличьте усиление RX** — отредактируйте `RX_GAIN` в `receiver.c`
|
||||
3. **Добавьте паузы** — `-p 5000` для стабилизации
|
||||
4. **Уменьшите амплитуду TX** — `-a 0.15` для снижения искажений
|
||||
5. **Проверьте CFO** — значение > 0.1 требует настройки частоты
|
||||
|
||||
### Ошибка сегментирования (Segmentation Fault)
|
||||
|
||||
1. **Проверьте версию liquid-dsp** — требуется >= 1.3.0
|
||||
2. **Пересоберите с отладкой**:
|
||||
```bash
|
||||
gcc -g -O0 -o receiver_debug receiver.c common.c -liio -lliquid -lfftw3f -lm -lpthread
|
||||
gdb ./receiver_debug
|
||||
run -f 1265000000 -r 3840000 -b 2000000 -c rs8
|
||||
bt
|
||||
```
|
||||
3. **Проверьте права доступа к USB**:
|
||||
```bash
|
||||
sudo chmod 666 /dev/iio*
|
||||
```
|
||||
|
||||
### Предупреждения компиляции
|
||||
|
||||
Предупреждения типа `-Wformat` исправлены добавлением суффикса `LL` к константам:
|
||||
```c
|
||||
#define DEFAULT_FREQ 1265000000LL // было 1265000000
|
||||
```
|
||||
|
||||
## Оптимальные параметры для разных сценариев
|
||||
|
||||
### Максимальная скорость
|
||||
```bash
|
||||
# TX
|
||||
cat data.bin | ./transmitter -f 2400000000 -r 7680000 -b 6000000 -g 0 -a 0.5 -p 0
|
||||
|
||||
# RX
|
||||
./receiver -f 2400000000 -r 7680000 -b 6000000 -c none > output.bin
|
||||
```
|
||||
|
||||
### Максимальная надёжность
|
||||
```bash
|
||||
# TX
|
||||
cat data.bin | ./transmitter -f 1265000000 -r 3840000 -b 1500000 -g -10 -a 0.4 -p 5000 -c rs8
|
||||
|
||||
# RX
|
||||
./receiver -f 1265000000 -r 3840000 -b 1500000 -c rs8 > output.bin
|
||||
```
|
||||
|
||||
|
||||
## Лицензия
|
||||
|
||||
Данный проект распространяется под лицензией MIT. Используйте на свой страх и риск.
|
||||
|
||||
106
SDR/common.c
Normal file
106
SDR/common.c
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* common.c — Реализация общих функций
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
volatile sig_atomic_t stop = 0;
|
||||
|
||||
static void sigint_handler(int s) {
|
||||
(void)s;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
void setup_signal_handlers(void) {
|
||||
signal(SIGINT, sigint_handler);
|
||||
signal(SIGTERM, sigint_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
void print_usage(const char *prog_name, const char *mode) {
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "OFDM %s — PlutoSDR Radio Link\n", mode);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "Использование: %s [ОПЦИИ]\n\n", prog_name);
|
||||
fprintf(stderr, "ОБЩИЕ ОПЦИИ:\n");
|
||||
fprintf(stderr, " -f ЧАСТОТА Частота несущей в Гц (по умолчанию: %lld)\n", DEFAULT_FREQ);
|
||||
fprintf(stderr, " -r СКОРОСТЬ Частота дискретизации (по умолчанию: %lld)\n", DEFAULT_RATE);
|
||||
fprintf(stderr, " -b ПОЛОСА Полоса пропускания (по умолчанию: %lld)\n", DEFAULT_BW);
|
||||
fprintf(stderr, " -u URI URI PlutoSDR\n");
|
||||
fprintf(stderr, " -c РЕЖИМ FEC: rs8 или none (по умолчанию: rs8)\n");
|
||||
fprintf(stderr, " -h Показать эту справку\n");
|
||||
}
|
||||
|
||||
void print_config(const char *mode, long long freq, long long rate,
|
||||
long long bw, int fec_on) {
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "КОНФИГУРАЦИЯ %s\n", mode);
|
||||
fprintf(stderr, "Частота: %.1f МГц\n", freq / 1e6);
|
||||
fprintf(stderr, "Дискрет.: %.2f МГц\n", rate / 1e6);
|
||||
fprintf(stderr, "Полоса: %.2f МГц\n", bw / 1e6);
|
||||
fprintf(stderr, "OFDM: M=%d, CP=%d, Taper=%d\n", OFDM_M, OFDM_CP, OFDM_TAPER);
|
||||
fprintf(stderr, "FEC: %s\n", fec_on ? "RS(255,223)" : "ОТКЛЮЧЕН");
|
||||
}
|
||||
|
||||
int pluto_init(const char *uri, struct iio_context **ctx,
|
||||
struct iio_device **phy, struct iio_device **dev, int is_tx) {
|
||||
*ctx = iio_create_context_from_uri(uri);
|
||||
if (!*ctx) {
|
||||
LOG_ERROR("Не удалось подключиться к PlutoSDR по адресу %s", uri);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*phy = iio_context_find_device(*ctx, "ad9361-phy");
|
||||
*dev = iio_context_find_device(*ctx,
|
||||
is_tx ? "cf-ad9361-dds-core-lpc" : "cf-ad9361-lpc");
|
||||
|
||||
if (!*phy || !*dev) {
|
||||
LOG_ERROR("Радиоустройства не найдены в контексте");
|
||||
iio_context_destroy(*ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pluto_configure(struct iio_device *phy, long long freq, long long rate,
|
||||
long long bw, double gain, int is_tx) {
|
||||
if (is_tx) {
|
||||
// Настройка передатчика
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "altvoltage1", true),
|
||||
"frequency", freq);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"sampling_frequency", rate);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"rf_bandwidth", bw);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"rf_port_select", "A");
|
||||
iio_channel_attr_write_double(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"hardwaregain", gain);
|
||||
} else {
|
||||
// Настройка приёмника
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "altvoltage0", true),
|
||||
"frequency", freq);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"sampling_frequency", rate);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"rf_bandwidth", bw);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"gain_control_mode", "manual");
|
||||
iio_channel_attr_write_double(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"hardwaregain", gain);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"rf_port_select", "A_BALANCED");
|
||||
}
|
||||
}
|
||||
61
SDR/common.h
Normal file
61
SDR/common.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* common.h — Общие определения и конфигурация для приёмника и передатчика
|
||||
* OFDM + RS(255,223) FEC + PlutoSDR
|
||||
*/
|
||||
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <signal.h>
|
||||
#include <getopt.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <iio.h>
|
||||
#include <liquid/liquid.h>
|
||||
|
||||
// Параметры OFDM-модуляции
|
||||
#define OFDM_M 64
|
||||
#define OFDM_CP 16
|
||||
#define OFDM_TAPER 4
|
||||
|
||||
// Параметры Рида-Соломона
|
||||
#define RS_DATA 223
|
||||
#define RS_ENC 255
|
||||
|
||||
// Параметры радиотракта по умолчанию
|
||||
#define DEFAULT_FREQ 1265000000LL
|
||||
#define DEFAULT_RATE 3840000LL
|
||||
#define DEFAULT_BW 3000000LL
|
||||
|
||||
// Адреса PlutoSDR
|
||||
#define TX_URI_DEFAULT "ip:192.168.3.1" // адрес Pluto передатчика
|
||||
#define RX_URI_DEFAULT "ip:192.168.2.1" // адрес Pluto приемника
|
||||
|
||||
// Формат заголовка кадра
|
||||
#define HDR_SIGNATURE_0 0xF0
|
||||
#define HDR_SIGNATURE_1 0xAA
|
||||
#define HDR_SIZE 12
|
||||
|
||||
// Макросы логирования
|
||||
#define LOG_ERROR(fmt, ...) fprintf(stderr, "[ОШИБКА] " fmt "\n", ##__VA_ARGS__)
|
||||
#define LOG_INFO(fmt, ...) fprintf(stderr, "[INFO] " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
// Глобальный флаг завершения
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
// Прототипы общих функций
|
||||
void setup_signal_handlers(void);
|
||||
void print_usage(const char *prog_name, const char *mode);
|
||||
void print_config(const char *mode, long long freq, long long rate,
|
||||
long long bw, int fec_on);
|
||||
int pluto_init(const char *uri, struct iio_context **ctx,
|
||||
struct iio_device **phy, struct iio_device **dev, int is_tx);
|
||||
void pluto_configure(struct iio_device *phy, long long freq, long long rate,
|
||||
long long bw, double gain, int is_tx);
|
||||
|
||||
#endif
|
||||
47
SDR/makefile
Normal file
47
SDR/makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Makefile для сборки OFDM-приёмника и передатчика для PlutoSDR
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -O2
|
||||
LDFLAGS = -liio -lliquid -lfftw3f -lm -lpthread
|
||||
|
||||
TX_BIN = transmitter
|
||||
RX_BIN = receiver
|
||||
TX_SRC = transmitter.c
|
||||
RX_SRC = receiver.c
|
||||
|
||||
TX_EXISTS := $(shell test -f $(TX_SRC) && echo 1 || echo 0)
|
||||
RX_EXISTS := $(shell test -f $(RX_SRC) && echo 1 || echo 0)
|
||||
|
||||
ifeq ($(TX_EXISTS),1)
|
||||
TARGETS += $(TX_BIN)
|
||||
endif
|
||||
|
||||
ifeq ($(RX_EXISTS),1)
|
||||
TARGETS += $(RX_BIN)
|
||||
endif
|
||||
|
||||
all: $(TARGETS)
|
||||
@echo "Готово: $(TARGETS)"
|
||||
|
||||
$(TX_BIN): $(TX_SRC) common.c common.h
|
||||
$(CC) $(CFLAGS) -o $@ $(TX_SRC) common.c $(LDFLAGS)
|
||||
@echo " ✓ $(TX_BIN) собран"
|
||||
|
||||
$(RX_BIN): $(RX_SRC) common.c common.h
|
||||
$(CC) $(CFLAGS) -o $@ $(RX_SRC) common.c $(LDFLAGS)
|
||||
@echo " ✓ $(RX_BIN) собран"
|
||||
|
||||
test:
|
||||
python3 -c "import struct; f=open('test_input.bin','wb'); [f.write(struct.pack('B', i%256)) for i in range(1048576)]; f.close()"
|
||||
@ls -lh test_input.bin
|
||||
|
||||
clean:
|
||||
rm -f $(TX_BIN) $(RX_BIN) *.o test_input.bin output.bin
|
||||
|
||||
help:
|
||||
@echo "Цели:"
|
||||
@echo " make — сборка всего, что есть"
|
||||
@echo " make test — создать тестовый файл 1 МБ"
|
||||
@echo " make clean — удалить всё собранное"
|
||||
|
||||
.PHONY: all test clean help
|
||||
206
SDR/reciever.c
Normal file
206
SDR/reciever.c
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* receiver.c — OFDM-приёмник с RS(255,223) FEC и частотной коррекцией
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#define BUF_SAMPLES 32768
|
||||
#define MAX_FRAME_SAMPLES (1 << 16)
|
||||
#define RX_GAIN 50.0
|
||||
#define CFO_FILTER_ALPHA 0.95f
|
||||
#define CFO_MAX_VALID 0.5f
|
||||
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
static uint64_t calls = 0, hdr_ok = 0, crc_raw = 0;
|
||||
static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0;
|
||||
static float rssi = -100, evm = 0;
|
||||
static float cfo_est = 0.0f;
|
||||
static nco_crcf nco = NULL;
|
||||
static fec decoder = NULL;
|
||||
static int fec_on = 0;
|
||||
static liquid_float_complex frame[MAX_FRAME_SAMPLES];
|
||||
|
||||
static int callback(unsigned char *hdr, int hdr_valid,
|
||||
unsigned char *pay, unsigned int pay_len,
|
||||
int pay_valid, framesyncstats_s stats,
|
||||
void *user) {
|
||||
(void)user;
|
||||
calls++;
|
||||
rssi = stats.rssi;
|
||||
evm = stats.evm;
|
||||
|
||||
// Обновляем CFO только если NCO инициализирован
|
||||
if (nco && fabsf(stats.cfo) < CFO_MAX_VALID) {
|
||||
cfo_est = CFO_FILTER_ALPHA * cfo_est + (1.0f - CFO_FILTER_ALPHA) * stats.cfo;
|
||||
nco_crcf_set_frequency(nco, cfo_est);
|
||||
}
|
||||
|
||||
if (!hdr_valid) return 0;
|
||||
hdr_ok++;
|
||||
if (pay_valid) crc_raw++;
|
||||
|
||||
uint16_t original_len = (hdr[6] << 8) | hdr[7];
|
||||
uint8_t last_block_bytes = hdr[10];
|
||||
|
||||
if (fec_on && decoder && pay_len >= RS_ENC) {
|
||||
size_t nblocks = pay_len / RS_ENC;
|
||||
if (nblocks == 0 || nblocks > 100) return 0;
|
||||
|
||||
uint8_t recovered[4096];
|
||||
int rs_ok = 1;
|
||||
for (size_t b = 0; b < nblocks; b++) {
|
||||
if (fec_decode(decoder, RS_DATA, pay + b * RS_ENC,
|
||||
recovered + b * RS_DATA) != 0) {
|
||||
rs_ok = 0; break;
|
||||
}
|
||||
}
|
||||
if (rs_ok) {
|
||||
if (!pay_valid) rs_saved++;
|
||||
size_t out_len = (nblocks - 1) * RS_DATA +
|
||||
(last_block_bytes > 0 ? last_block_bytes : RS_DATA);
|
||||
if (out_len > nblocks * RS_DATA) out_len = nblocks * RS_DATA;
|
||||
if (out_len > original_len) out_len = original_len;
|
||||
if (out_len > 0) {
|
||||
fwrite(recovered, 1, out_len, stdout);
|
||||
fflush(stdout);
|
||||
total_bytes += out_len;
|
||||
crc_final++;
|
||||
}
|
||||
}
|
||||
} else if (pay_valid && original_len > 0 && original_len <= pay_len) {
|
||||
fwrite(pay, 1, original_len, stdout);
|
||||
fflush(stdout);
|
||||
total_bytes += original_len;
|
||||
crc_final++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
|
||||
const char *uri = RX_URI_DEFAULT;
|
||||
const char *fec_opt = "rs8";
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "f:r:b:u:c:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'f': freq = atoll(optarg); break;
|
||||
case 'r': rate = atoll(optarg); break;
|
||||
case 'b': bw = atoll(optarg); break;
|
||||
case 'u': uri = optarg; break;
|
||||
case 'c': fec_opt = optarg; break;
|
||||
case 'h':
|
||||
print_usage(argv[0], "ПРИЁМНИК");
|
||||
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ RX:\n");
|
||||
fprintf(stderr, " Усиление фиксированное: %.0f дБ\n", RX_GAIN);
|
||||
fprintf(stderr, " Частотная коррекция: адаптивная (α=%.2f)\n", CFO_FILTER_ALPHA);
|
||||
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
|
||||
fprintf(stderr, " %s -f %lld -r %lld -b %lld -c rs8 > output.bin\n",
|
||||
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
|
||||
return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
fec_on = !strcmp(fec_opt, "rs8");
|
||||
setup_signal_handlers();
|
||||
|
||||
print_config("ПРИЁМНИК", freq, rate, bw, fec_on);
|
||||
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
|
||||
fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
|
||||
struct iio_context *ctx = NULL;
|
||||
struct iio_device *phy = NULL, *rx = NULL;
|
||||
if (pluto_init(uri, &ctx, &phy, &rx, 0) != 0) return 1;
|
||||
|
||||
pluto_configure(phy, freq, rate, bw, RX_GAIN, 0);
|
||||
|
||||
iio_channel_enable(iio_device_find_channel(rx, "voltage0", false));
|
||||
iio_channel_enable(iio_device_find_channel(rx, "voltage1", false));
|
||||
|
||||
struct iio_buffer *buf = iio_device_create_buffer(rx, BUF_SAMPLES, false);
|
||||
if (!buf) {
|
||||
fprintf(stderr, "Ошибка создания буфера приёма\n");
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Инициализация FEC
|
||||
if (fec_on) {
|
||||
decoder = fec_create(LIQUID_FEC_RS_M8, NULL);
|
||||
if (!decoder) {
|
||||
fprintf(stderr, "Ошибка создания FEC-декодера\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация NCO для частотной коррекции
|
||||
nco = nco_crcf_create(LIQUID_NCO);
|
||||
if (!nco) {
|
||||
fprintf(stderr, "Ошибка создания NCO\n");
|
||||
} else {
|
||||
nco_crcf_set_frequency(nco, 0.0f);
|
||||
}
|
||||
|
||||
ofdmflexframesync fs = ofdmflexframesync_create(
|
||||
OFDM_M, OFDM_CP, OFDM_TAPER, NULL, callback, NULL);
|
||||
if (!fs) {
|
||||
fprintf(stderr, "Ошибка создания OFDM-синхронизатора\n");
|
||||
if (decoder) fec_destroy(decoder);
|
||||
if (nco) nco_crcf_destroy(nco);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n");
|
||||
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | Вывод | RSSI | EVM | CFO\n");
|
||||
fprintf(stderr, "--------+--------+------+---------+--------+-------+-------+----------\n");
|
||||
|
||||
time_t last = time(NULL);
|
||||
|
||||
while (!stop) {
|
||||
ssize_t n = iio_buffer_refill(buf);
|
||||
if (n < 0) { usleep(500); continue; }
|
||||
|
||||
int16_t *bb = (int16_t *)iio_buffer_start(buf);
|
||||
int ns = (iio_buffer_end(buf) - iio_buffer_start(buf)) / sizeof(int16_t) / 2;
|
||||
if (ns > MAX_FRAME_SAMPLES) ns = MAX_FRAME_SAMPLES;
|
||||
|
||||
for (int i = 0; i < ns; i++) {
|
||||
frame[i] = ((float)bb[2*i] / 2048.0f) +
|
||||
((float)bb[2*i+1] / 2048.0f) * _Complex_I;
|
||||
if (nco) {
|
||||
nco_crcf_mix_down(nco, frame[i], &frame[i]);
|
||||
nco_crcf_step(nco);
|
||||
}
|
||||
}
|
||||
if (nco) nco_crcf_set_phase(nco, 0.0f);
|
||||
|
||||
ofdmflexframesync_execute(fs, frame, ns);
|
||||
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f",
|
||||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||||
(unsigned long long)crc_final, rssi, evm, cfo_est);
|
||||
fflush(stderr);
|
||||
last = time(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n");
|
||||
fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | Вывод: %llu | Байт: %llu\n",
|
||||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||||
(unsigned long long)crc_final, (unsigned long long)total_bytes);
|
||||
|
||||
if (decoder) fec_destroy(decoder);
|
||||
if (nco) nco_crcf_destroy(nco);
|
||||
ofdmflexframesync_destroy(fs);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
185
SDR/transmitter.c
Normal file
185
SDR/transmitter.c
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* transmitter.c — OFDM-передатчик с RS(255,223) FEC для PlutoSDR
|
||||
*
|
||||
* ОПТИМАЛЬНАЯ КОМАНДА ЗАПУСКА:
|
||||
* cat test.bin | ./transmitter -f 1265000000 -r 3840000 -b 2000000 -g -20 -a 0.3 -p 0 -c rs8
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Параметры передатчика по умолчанию
|
||||
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
|
||||
#define DEFAULT_AMP 0.2f // Амплитуда сигнала
|
||||
#define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс
|
||||
#define BUF_SAMPLES 16384 // Размер буфера в отсчётах
|
||||
#define MAX_PAYLOAD 1024 // Максимальный размер полезной нагрузки
|
||||
|
||||
// Внешняя переменная флага завершения (определена в common.c)
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Инициализация параметров
|
||||
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
|
||||
double gain = DEFAULT_GAIN;
|
||||
float amp = DEFAULT_AMP;
|
||||
const char *uri = TX_URI_DEFAULT;
|
||||
int pause_us = DEFAULT_PAUSE, use_fec = 1;
|
||||
|
||||
// Обработка аргументов командной строки
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'f': freq = atoll(optarg); break;
|
||||
case 'r': rate = atoll(optarg); break;
|
||||
case 'b': bw = atoll(optarg); break;
|
||||
case 'g': gain = atof(optarg); break;
|
||||
case 'a': amp = atof(optarg); break;
|
||||
case 'p': pause_us = atoi(optarg); break;
|
||||
case 'u': uri = optarg; break;
|
||||
case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; break;
|
||||
case 'h':
|
||||
print_usage(argv[0], "ПЕРЕДАТЧИК");
|
||||
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ TX:\n");
|
||||
fprintf(stderr, " -g УСИЛЕНИЕ Усиление в дБ (по умолчанию: %.0f)\n", DEFAULT_GAIN);
|
||||
fprintf(stderr, " -a АМПЛИТ. Амплитуда сигнала (по умолчанию: %.2f)\n", DEFAULT_AMP);
|
||||
fprintf(stderr, " -p ПАУЗА Пауза между кадрами в мкс (по умолчанию: %d)\n", DEFAULT_PAUSE);
|
||||
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
|
||||
fprintf(stderr, " cat test.bin | %s -f %lld -r %lld -b %lld -c rs8 > /dev/null\n",
|
||||
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
|
||||
return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Настройка обработчиков сигналов
|
||||
setup_signal_handlers();
|
||||
|
||||
// Вывод конфигурации
|
||||
print_config("ПЕРЕДАТЧИК", freq, rate, bw, use_fec);
|
||||
fprintf(stderr, "Усиление: %.0f дБ\n", gain);
|
||||
fprintf(stderr, "Амплитуда: %.2f\n", amp);
|
||||
fprintf(stderr, "Пауза: %d мкс\n", pause_us);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
|
||||
// Инициализация PlutoSDR
|
||||
struct iio_context *ctx = NULL;
|
||||
struct iio_device *phy = NULL, *tx = NULL;
|
||||
if (pluto_init(uri, &ctx, &phy, &tx, 1) != 0) return 1;
|
||||
|
||||
// Настройка радиотракта
|
||||
pluto_configure(phy, freq, rate, bw, gain, 1);
|
||||
|
||||
// Включение каналов передатчика
|
||||
iio_channel_enable(iio_device_find_channel(tx, "voltage0", true));
|
||||
iio_channel_enable(iio_device_find_channel(tx, "voltage1", true));
|
||||
|
||||
// Создание буфера передачи
|
||||
struct iio_buffer *buf = iio_device_create_buffer(tx, BUF_SAMPLES, false);
|
||||
if (!buf) {
|
||||
LOG_ERROR("Ошибка создания буфера передачи");
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Инициализация FEC и OFDM
|
||||
fec enc = use_fec ? fec_create(LIQUID_FEC_RS_M8, NULL) : NULL;
|
||||
|
||||
ofdmflexframegenprops_s fgp;
|
||||
ofdmflexframegenprops_init_default(&fgp);
|
||||
fgp.check = LIQUID_CRC_32;
|
||||
ofdmflexframegen fg = ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &fgp);
|
||||
|
||||
// Счётчики статистики
|
||||
uint64_t frames = 0, bytes_tx = 0;
|
||||
uint32_t seq = 0;
|
||||
time_t last = time(NULL);
|
||||
|
||||
fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n");
|
||||
|
||||
// Основной цикл передачи
|
||||
while (!stop) {
|
||||
uint8_t raw[MAX_PAYLOAD];
|
||||
size_t n = fread(raw, 1, MAX_PAYLOAD, stdin);
|
||||
if (!n) {
|
||||
if (feof(stdin) || stop) break;
|
||||
usleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t *tx_payload = NULL;
|
||||
size_t tx_len = 0;
|
||||
uint8_t hdr[HDR_SIZE] = {0};
|
||||
|
||||
// Кодирование данных
|
||||
if (enc) {
|
||||
size_t total_blocks = (n + RS_DATA - 1) / RS_DATA;
|
||||
tx_len = total_blocks * RS_ENC;
|
||||
tx_payload = malloc(tx_len);
|
||||
|
||||
for (size_t b = 0; b < total_blocks; b++) {
|
||||
uint8_t blk[RS_DATA] = {0};
|
||||
size_t bytes = (b == total_blocks - 1) ? n - b * RS_DATA : RS_DATA;
|
||||
memcpy(blk, raw + b * RS_DATA, bytes);
|
||||
fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC);
|
||||
}
|
||||
|
||||
// Формирование заголовка
|
||||
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
|
||||
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
|
||||
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
|
||||
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
|
||||
hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF;
|
||||
hdr[10] = n % RS_DATA; hdr[11] = 0;
|
||||
} else {
|
||||
tx_len = n;
|
||||
tx_payload = malloc(tx_len);
|
||||
memcpy(tx_payload, raw, n);
|
||||
|
||||
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
|
||||
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
|
||||
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
|
||||
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
|
||||
}
|
||||
|
||||
// Сборка OFDM-кадра
|
||||
ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len);
|
||||
free(tx_payload);
|
||||
|
||||
// Формирование выходного сигнала
|
||||
int16_t *out = (int16_t *)iio_buffer_start(buf);
|
||||
memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t));
|
||||
int idx = 0, done = 0;
|
||||
float complex symb[OFDM_M + OFDM_CP];
|
||||
|
||||
while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) {
|
||||
done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP);
|
||||
for (int i = 0; i < OFDM_M + OFDM_CP; i++) {
|
||||
out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp);
|
||||
out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
if (idx > 0) iio_buffer_push(buf);
|
||||
|
||||
// Обновление статистики
|
||||
frames++; bytes_tx += n; seq++;
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
fflush(stderr);
|
||||
last = time(NULL);
|
||||
}
|
||||
if (pause_us > 0) usleep(pause_us);
|
||||
}
|
||||
|
||||
fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
|
||||
// Освобождение ресурсов
|
||||
if (enc) fec_destroy(enc);
|
||||
ofdmflexframegen_destroy(fg);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
1
scripts/array.txt
Normal file
1
scripts/array.txt
Normal file
@@ -0,0 +1 @@
|
||||
[0xE0, 0x01, 0x4C, 0x88]
|
||||
15
scripts/emulatohackrf.sh
Normal file
15
scripts/emulatohackrf.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Эмулятор GPIO -> stdout (1 мс на байт)
|
||||
python3 /home/user/temp/golang/scripts/emulator.py \
|
||||
array.txt \
|
||||
- \
|
||||
0.001 \
|
||||
| pv | # Передача через HackRF
|
||||
/home/user/hackrf-liquid-dsp/hackrf-frame64-tx \
|
||||
-x 40 \
|
||||
-a 0 \
|
||||
-d a32868dc347c6e47 \
|
||||
-s 2000000 \
|
||||
-b 1750000 \
|
||||
-f 800000000
|
||||
230
scripts/emulator.py
Normal file
230
scripts/emulator.py
Normal file
@@ -0,0 +1,230 @@
|
||||
import sys
|
||||
import time
|
||||
import ast
|
||||
import signal
|
||||
import os
|
||||
|
||||
OUT_BUFFER_LEN = 32
|
||||
RECONNECT_DELAY = 0.5
|
||||
REPEAT_COUNT = 100 # повтор
|
||||
DEFAULT_SEND_DELAY = 0.1 # по умолчанию (10 байт/с)
|
||||
|
||||
stop_flag = False
|
||||
|
||||
|
||||
# ================= SIGNALS =================
|
||||
|
||||
def handle_sigint(signum, frame):
|
||||
global stop_flag
|
||||
stop_flag = True
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
signal.signal(signal.SIGTERM, handle_sigint)
|
||||
|
||||
|
||||
# ================= UI =================
|
||||
|
||||
last_status = ""
|
||||
|
||||
|
||||
def status(msg):
|
||||
global last_status
|
||||
if msg != last_status:
|
||||
sys.stderr.write("\r" + msg + " " * 20)
|
||||
sys.stderr.flush()
|
||||
last_status = msg
|
||||
|
||||
|
||||
def status_clear():
|
||||
sys.stderr.write("\r" + " " * 80 + "\r")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
# ================= DATA =================
|
||||
|
||||
def load_array(path):
|
||||
while True:
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
content = f.read().strip()
|
||||
|
||||
data = ast.literal_eval(content)
|
||||
|
||||
result = []
|
||||
for x in data:
|
||||
if isinstance(x, str):
|
||||
x = int(x, 0)
|
||||
result.append(x & 0xFF)
|
||||
|
||||
print(f"[emu] Загружено {len(result)} байт", file=sys.stderr)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"[emu] Ошибка чтения: {e}", file=sys.stderr)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# ================= PIPE =================
|
||||
|
||||
def open_pipe(path):
|
||||
while not stop_flag:
|
||||
try:
|
||||
fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
|
||||
status("[emu] pipe: подключен")
|
||||
return os.fdopen(fd, "wb", buffering=0)
|
||||
|
||||
except OSError:
|
||||
status("[emu] pipe: нет читателя")
|
||||
time.sleep(RECONNECT_DELAY)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ================= ARGS PARSING =================
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Разбирает аргументы командной строки.
|
||||
Использование:
|
||||
python3 emulator.py array.txt /tmp/gpio_pipe [задержка] [logfile|-]
|
||||
|
||||
Где:
|
||||
задержка — float, секунд между байтами (по умолчанию DEFAULT_SEND_DELAY)
|
||||
logfile — путь к файлу лога или "-" для stderr
|
||||
"""
|
||||
if len(sys.argv) < 3:
|
||||
print("Использование: python emulator.py array.txt <pipe_path|-> [задержка] [logfile|-]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
array_path = sys.argv[1]
|
||||
pipe_path = sys.argv[2]
|
||||
|
||||
send_delay = DEFAULT_SEND_DELAY
|
||||
log_file = None
|
||||
arg_offset = 0
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
try:
|
||||
send_delay = float(sys.argv[3])
|
||||
arg_offset = 1
|
||||
print(f"[emu] Задержка: {send_delay} с (~{1/send_delay:.0f} байт/с)", file=sys.stderr)
|
||||
except ValueError:
|
||||
arg_offset = 0
|
||||
|
||||
log_idx = 3 + arg_offset
|
||||
if len(sys.argv) > log_idx:
|
||||
log_path = sys.argv[log_idx]
|
||||
if log_path == "-":
|
||||
log_file = sys.stderr
|
||||
else:
|
||||
log_file = open(log_path, "a")
|
||||
|
||||
return array_path, pipe_path, send_delay, log_file
|
||||
|
||||
# ================= MAIN =================
|
||||
|
||||
def main():
|
||||
array_path, pipe_path, send_delay, log_file = parse_args()
|
||||
|
||||
data = load_array(array_path)
|
||||
|
||||
log_buffer = []
|
||||
total_bytes = 0
|
||||
packet_id = 0
|
||||
i = 0
|
||||
repeat = 0
|
||||
|
||||
# ===== STDOUT MODE =====
|
||||
if pipe_path == "-":
|
||||
pipe = sys.stdout.buffer # бинарный stdout
|
||||
print(f"[emu] Режим stdout, {len(data)} байт в цикле", file=sys.stderr)
|
||||
try:
|
||||
while not stop_flag:
|
||||
byte = data[i]
|
||||
pipe.write(bytes([byte]))
|
||||
pipe.flush()
|
||||
|
||||
log_buffer.append(byte)
|
||||
total_bytes += 1
|
||||
|
||||
if len(log_buffer) >= OUT_BUFFER_LEN and log_file:
|
||||
packet_id += 1
|
||||
log_file.write(f"--- Пакет #{packet_id} ({OUT_BUFFER_LEN} байт) ---\n")
|
||||
for idx, b in enumerate(log_buffer):
|
||||
log_file.write(f" [{idx:02d}] = 0x{b:02x} ({b:3d})\n")
|
||||
log_file.flush()
|
||||
log_buffer.clear()
|
||||
|
||||
repeat += 1
|
||||
if repeat >= REPEAT_COUNT:
|
||||
repeat = 0
|
||||
i += 1
|
||||
if i >= len(data):
|
||||
i = 0
|
||||
|
||||
time.sleep(send_delay)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except BrokenPipeError:
|
||||
pass # нормально для pipe
|
||||
finally:
|
||||
status_clear()
|
||||
print("[emu] завершён", file=sys.stderr)
|
||||
return
|
||||
|
||||
# ===== FIFO MODE =====
|
||||
|
||||
pipe = None
|
||||
|
||||
while not stop_flag:
|
||||
|
||||
if pipe is None:
|
||||
pipe = open_pipe(pipe_path)
|
||||
if pipe is None:
|
||||
break
|
||||
|
||||
try:
|
||||
byte = data[i]
|
||||
|
||||
pipe.write(bytes([byte]))
|
||||
|
||||
log_buffer.append(byte)
|
||||
total_bytes += 1
|
||||
|
||||
if len(log_buffer) >= OUT_BUFFER_LEN and log_file:
|
||||
packet_id += 1
|
||||
|
||||
log_file.write(f"--- Пакет #{packet_id} ({OUT_BUFFER_LEN} байт) ---\n")
|
||||
for idx, b in enumerate(log_buffer):
|
||||
log_file.write(f" [{idx:02d}] = 0x{b:02x} ({b:3d})\n")
|
||||
|
||||
log_file.flush()
|
||||
log_buffer.clear()
|
||||
|
||||
repeat += 1
|
||||
|
||||
if repeat >= REPEAT_COUNT:
|
||||
repeat = 0
|
||||
i += 1
|
||||
if i >= len(data):
|
||||
i = 0
|
||||
|
||||
time.sleep(send_delay)
|
||||
|
||||
except BrokenPipeError:
|
||||
pipe.close()
|
||||
pipe = None
|
||||
status("[emu] pipe: отключён")
|
||||
|
||||
except Exception as e:
|
||||
status(f"[emu] ошибка: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
status_clear()
|
||||
print("[emu] завершён", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
scripts/go2monitor.service
Normal file
29
scripts/go2monitor.service
Normal file
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=GPIO Monitor Server
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=user
|
||||
Group=user
|
||||
WorkingDirectory=/home/user/temp/golang
|
||||
|
||||
# Запуск сервера
|
||||
ExecStart=/home/user/temp/golang/build/gpio-monitor-server -pipe /tmp/gpio_pipe
|
||||
|
||||
# Перезапуск при падении
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
|
||||
# Лимиты и безопасность
|
||||
LimitNOFILE=4096
|
||||
NoNewPrivileges=yes
|
||||
|
||||
# Логирование
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=gpio-monitor
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,191 +1,177 @@
|
||||
import sys
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Имитатор параллельной шины для gpio-interrupt (Raspberry Pi 4)
|
||||
"""
|
||||
|
||||
import gpiod
|
||||
import time
|
||||
import ast
|
||||
import argparse
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
|
||||
OUT_BUFFER_LEN = 32
|
||||
RECONNECT_DELAY = 0.5
|
||||
REPEAT_COUNT = 50 # повтор
|
||||
DEFAULT_SEND_DELAY = 0.1 # по умолчанию (10 байт/с)
|
||||
|
||||
stop_flag = False
|
||||
from gpiod.line import Direction, Value
|
||||
|
||||
|
||||
# ================= SIGNALS =================
|
||||
class ParallelBusImitator:
|
||||
"""Имитатор 7-битной параллельной шины данных"""
|
||||
|
||||
def handle_sigint(signum, frame):
|
||||
global stop_flag
|
||||
stop_flag = True
|
||||
# Временные константы (секунды)
|
||||
DATA_SETUP_TIME = 0.000005
|
||||
STROBE_WIDTH = 0.000005
|
||||
HOLD_TIME = 0.000005
|
||||
|
||||
WR_PIN = 12 # BCM12
|
||||
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
signal.signal(signal.SIGTERM, handle_sigint)
|
||||
DATA_PINS_MAPPING = [
|
||||
(21, 18, "D0 (LSB)"),
|
||||
(7, 27, "D1"),
|
||||
(6, 22, "D2"),
|
||||
(5, 23, "D3"),
|
||||
(25, 17, "D4"),
|
||||
(24, 20, "D5"),
|
||||
(23, 21, "D6"),
|
||||
(22, None, "D7 (не подключён)"),
|
||||
]
|
||||
|
||||
def __init__(self, use_mask=True, chip_path='/dev/gpiochip0'):
|
||||
self.use_mask = use_mask
|
||||
self.counter = 0
|
||||
self.active_pins = []
|
||||
|
||||
# ================= UI =================
|
||||
for wpi, bcm, desc in self.DATA_PINS_MAPPING:
|
||||
if bcm is not None:
|
||||
self.active_pins.append((wpi, bcm, desc))
|
||||
|
||||
last_status = ""
|
||||
self.data_pins_bcm = [bcm for _, bcm, _ in self.active_pins]
|
||||
self.all_pins = self.data_pins_bcm + [self.WR_PIN]
|
||||
|
||||
|
||||
def status(msg):
|
||||
global last_status
|
||||
if msg != last_status:
|
||||
sys.stderr.write("\r" + msg + " " * 20)
|
||||
sys.stderr.flush()
|
||||
last_status = msg
|
||||
|
||||
|
||||
def status_clear():
|
||||
sys.stderr.write("\r" + " " * 80 + "\r")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
# ================= DATA =================
|
||||
|
||||
def load_array(path):
|
||||
while True:
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
content = f.read().strip()
|
||||
|
||||
data = ast.literal_eval(content)
|
||||
|
||||
result = []
|
||||
for x in data:
|
||||
if isinstance(x, str):
|
||||
x = int(x, 0)
|
||||
result.append(x & 0xFF)
|
||||
|
||||
print(f"[emu] Загружено {len(result)} байт", file=sys.stderr)
|
||||
return result
|
||||
|
||||
self.chip = gpiod.Chip(chip_path)
|
||||
self.lines = self.chip.request_lines(
|
||||
{
|
||||
p: gpiod.LineSettings(
|
||||
direction=Direction.OUTPUT,
|
||||
output_value=Value.INACTIVE
|
||||
)
|
||||
for p in self.all_pins
|
||||
},
|
||||
consumer="par-bus-imitator"
|
||||
)
|
||||
except PermissionError:
|
||||
print("Нет прав доступа к GPIO. Используйте sudo.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"[emu] Ошибка чтения: {e}", file=sys.stderr)
|
||||
time.sleep(1)
|
||||
print(f"Ошибка GPIO: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
bit_width = 4 if use_mask else len(self.data_pins_bcm)
|
||||
|
||||
# ================= PIPE =================
|
||||
print(f"✓ Имитатор шины ({bit_width}-бит режим)")
|
||||
print("Нажмите Ctrl+C для остановки\n")
|
||||
|
||||
def open_pipe(path):
|
||||
while not stop_flag:
|
||||
try:
|
||||
fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
|
||||
status("[emu] pipe: подключен")
|
||||
return os.fdopen(fd, "wb", buffering=0)
|
||||
|
||||
except OSError:
|
||||
status("[emu] pipe: нет читателя")
|
||||
time.sleep(RECONNECT_DELAY)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ================= ARGS PARSING =================
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Разбирает аргументы командной строки.
|
||||
Использование:
|
||||
python3 emulator.py array.txt /tmp/gpio_pipe [задержка] [logfile|-]
|
||||
|
||||
Где:
|
||||
задержка — float, секунд между байтами (по умолчанию DEFAULT_SEND_DELAY)
|
||||
logfile — путь к файлу лога или "-" для stderr
|
||||
"""
|
||||
if len(sys.argv) < 3:
|
||||
print("Использование: python emulator.py array.txt /tmp/gpio_pipe [задержка] [logfile|-]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
array_path = sys.argv[1]
|
||||
pipe_path = sys.argv[2]
|
||||
|
||||
send_delay = DEFAULT_SEND_DELAY
|
||||
log_file = None
|
||||
arg_offset = 0
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
try:
|
||||
send_delay = float(sys.argv[3])
|
||||
arg_offset = 1 # сдвигаемся, значит logfile будет 4-м аргументом
|
||||
print(f"[emu] Задержка: {send_delay} с (~{1/send_delay:.0f} байт/с)", file=sys.stderr)
|
||||
except ValueError:
|
||||
arg_offset = 0
|
||||
|
||||
# Лог-файл
|
||||
log_idx = 3 + arg_offset
|
||||
if len(sys.argv) > log_idx:
|
||||
log_path = sys.argv[log_idx]
|
||||
if log_path == "-":
|
||||
log_file = sys.stderr
|
||||
def send_byte(self, value):
|
||||
"""Отправка одного байта"""
|
||||
if self.use_mask:
|
||||
value &= 0x0F
|
||||
else:
|
||||
log_file = open(log_path, "a")
|
||||
value &= 0x7F
|
||||
|
||||
return array_path, pipe_path, send_delay, log_file
|
||||
out = {}
|
||||
|
||||
for bit_pos, bcm_pin in enumerate(self.data_pins_bcm):
|
||||
bit = (value >> bit_pos) & 1
|
||||
out[bcm_pin] = Value.ACTIVE if bit else Value.INACTIVE
|
||||
|
||||
self.lines.set_values(out)
|
||||
time.sleep(self.DATA_SETUP_TIME)
|
||||
|
||||
self.lines.set_value(self.WR_PIN, Value.ACTIVE)
|
||||
time.sleep(self.STROBE_WIDTH)
|
||||
self.lines.set_value(self.WR_PIN, Value.INACTIVE)
|
||||
time.sleep(self.HOLD_TIME)
|
||||
|
||||
self.counter += 1
|
||||
self.show_status(value)
|
||||
|
||||
def show_status(self, value):
|
||||
"""Красивый статус без спама"""
|
||||
bits = 4 if self.use_mask else 7
|
||||
bin_val = format(value, f'0{bits}b')
|
||||
|
||||
msg = (
|
||||
f"\r▶ Передача: "
|
||||
f"HEX=0x{value:02X} | "
|
||||
f"BIN={bin_val} | "
|
||||
f"Пакетов: {self.counter}"
|
||||
)
|
||||
|
||||
print(msg, end='', flush=True)
|
||||
|
||||
def send_sequence(self, values, delay=0.2):
|
||||
for val in values:
|
||||
self.send_byte(val)
|
||||
time.sleep(delay)
|
||||
|
||||
def cleanup(self):
|
||||
if hasattr(self, 'lines'):
|
||||
self.lines.release()
|
||||
if hasattr(self, 'chip'):
|
||||
self.chip.close()
|
||||
|
||||
|
||||
# ================= MAIN =================
|
||||
def handle_signal(signum, frame):
|
||||
print("\nЗавершение работы...")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
array_path, pipe_path, send_delay, log_file = parse_args()
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
data = load_array(array_path)
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Имитатор параллельной шины'
|
||||
)
|
||||
|
||||
log_buffer = []
|
||||
total_bytes = 0
|
||||
packet_id = 0
|
||||
i = 0
|
||||
repeat = 0
|
||||
parser.add_argument('--no-mask', action='store_true',
|
||||
help='7-битный режим')
|
||||
parser.add_argument('--test',
|
||||
choices=['default', 'counter', 'pattern'],
|
||||
default='default')
|
||||
parser.add_argument('--delay', type=float, default=0.2)
|
||||
parser.add_argument('--single', type=str)
|
||||
parser.add_argument('--chip', default='/dev/gpiochip0')
|
||||
|
||||
pipe = None
|
||||
args = parser.parse_args()
|
||||
|
||||
while not stop_flag:
|
||||
imitator = ParallelBusImitator(
|
||||
use_mask=not args.no_mask,
|
||||
chip_path=args.chip
|
||||
)
|
||||
|
||||
if pipe is None:
|
||||
pipe = open_pipe(pipe_path)
|
||||
if pipe is None:
|
||||
break
|
||||
try:
|
||||
if args.single:
|
||||
val = int(args.single, 16)
|
||||
imitator.send_byte(val)
|
||||
print()
|
||||
return
|
||||
|
||||
try:
|
||||
byte = data[i]
|
||||
if args.test == 'default':
|
||||
test_data = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40]
|
||||
elif args.test == 'counter':
|
||||
test_data = list(range(128))
|
||||
else:
|
||||
test_data = [0x00, 0x01, 0x03, 0x07, 0x0F, 0x55, 0x2A]
|
||||
|
||||
pipe.write(bytes([byte]))
|
||||
while True:
|
||||
imitator.send_sequence(test_data, args.delay)
|
||||
|
||||
log_buffer.append(byte)
|
||||
total_bytes += 1
|
||||
|
||||
if len(log_buffer) >= OUT_BUFFER_LEN and log_file:
|
||||
packet_id += 1
|
||||
|
||||
log_file.write(f"--- Пакет #{packet_id} ({OUT_BUFFER_LEN} байт) ---\n")
|
||||
for idx, b in enumerate(log_buffer):
|
||||
log_file.write(f" [{idx:02d}] = 0x{b:02x} ({b:3d})\n")
|
||||
|
||||
log_file.flush()
|
||||
log_buffer.clear()
|
||||
|
||||
repeat += 1
|
||||
|
||||
if repeat >= REPEAT_COUNT:
|
||||
repeat = 0
|
||||
i += 1
|
||||
if i >= len(data):
|
||||
i = 0
|
||||
|
||||
time.sleep(send_delay)
|
||||
|
||||
except BrokenPipeError:
|
||||
pipe.close()
|
||||
pipe = None
|
||||
status("[emu] pipe: отключён")
|
||||
|
||||
except Exception as e:
|
||||
status(f"[emu] ошибка: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
status_clear()
|
||||
print("[emu] завершён", file=sys.stderr)
|
||||
except KeyboardInterrupt:
|
||||
print("\nОстановка...")
|
||||
except Exception as e:
|
||||
print(f"\nОшибка: {e}")
|
||||
finally:
|
||||
imitator.cleanup()
|
||||
print("✓ GPIO освобождены")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user