diff --git a/.github/workflows/altair-v5-ui-check.yml b/.github/workflows/altair-v5-ui-check.yml index a14c27c..0846b64 100644 --- a/.github/workflows/altair-v5-ui-check.yml +++ b/.github/workflows/altair-v5-ui-check.yml @@ -34,13 +34,13 @@ jobs: node - <<'NODE' const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - if (pkg.version !== '0.6.0') throw new Error(`Expected 0.6.0, got ${pkg.version}`); + if (pkg.version !== '0.6.1') throw new Error(`Expected 0.6.1, got ${pkg.version}`); const prefix = '02,00001,00015,3.00,4.20,1,'; let xor = 0; for (const ch of prefix) xor ^= ch.charCodeAt(0); if (xor !== 0x33) throw new Error(`Expected XOR 0x33, got 0x${xor.toString(16)}`); if ((prefix + '33').length !== 29) throw new Error('Canonical packet must contain 29 ASCII characters'); - console.log('v6 package and canonical telemetry packet OK'); + console.log('v6.1 package and canonical telemetry packet OK'); NODE - name: Validate v6 telemetry and RF profile @@ -53,6 +53,11 @@ jobs: grep -Fq '"CHECKSUM"' js/app.js grep -Fq '435.000' js/app.js grep -Fq '435.000f' firmware/radio_gateway_esp32/src/main.cpp + grep -Fq 'digitalRead(CC_GDO0)' firmware/radio_gateway_esp32/src/main_fixed.cpp + grep -Fq 'setSyncWord(0x12, 0xAD' firmware/radio_gateway_esp32/src/main_fixed.cpp + grep -Fq 'RADIOLIB_ENCODING_NRZ' firmware/radio_gateway_esp32/src/main_fixed.cpp + grep -Fq 'variablePacketLengthMode' firmware/radio_gateway_esp32/src/main_fixed.cpp + grep -Fq '+' firmware/radio_gateway_esp32/platformio.ini grep -Fq '02,00001,00015,3.00,4.20,1,33' firmware/transmitter_stm32_v6/Transmit_v6_static/Transmit_v6_static.ino grep -Fq 'radio.begin(435.000f' firmware/transmitter_stm32_v6/Transmit_v6_static/Transmit_v6_static.ino if grep -Fq 'id="CURR"' index.html; then diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml index 0ebfba0..606e1c7 100644 --- a/.github/workflows/windows-installer.yml +++ b/.github/workflows/windows-installer.yml @@ -47,6 +47,14 @@ jobs: } $firmwareVersion = $Matches[1] + $rxPatch = "legacy" + if (Test-Path "firmware/radio_gateway_esp32/src/main_fixed.cpp") { + $rxPatchSource = Get-Content "firmware/radio_gateway_esp32/src/main_fixed.cpp" -Raw + if ($rxPatchSource -match 'RX_PATCH_VERSION\[\]\s*=\s*"([^"]+)"') { + $rxPatch = $Matches[1] + } + } + $bootApp0 = Get-ChildItem "$env:USERPROFILE/.platformio/packages" -Recurse -Filter "boot_app0.bin" | Select-Object -First 1 if (-not $bootApp0) { throw "boot_app0.bin was not found." } @@ -75,7 +83,7 @@ jobs: targetChip = "ESP32" firmwareVersion = $firmwareVersion applicationVersion = $appVersion - buildLabel = "Altair MCC v$appVersion CI" + buildLabel = "Altair MCC v$appVersion CI / $rxPatch" flashMode = "dio" flashFreq = "40m" flashSize = "4MB" @@ -105,6 +113,8 @@ jobs: release/*.blockmap bundled_firmware/*.json bundled_firmware/*.bin + firmware/transmitter_stm32_v6/Transmit_v6_static/Transmit_v6_static.ino + docs/TELEMETRY_V6.md if-no-files-found: error retention-days: 30 @@ -132,8 +142,13 @@ jobs: } $installer = Get-ChildItem -Path release -Filter "CUP-Altair-Setup-*.exe" | Select-Object -First 1 if (-not $installer) { throw "Altair Windows installer was not found in release/." } - gh release create $env:RELEASE_TAG $installer.FullName ` + gh release create $env:RELEASE_TAG ` + $installer.FullName ` + "firmware/transmitter_stm32_v6/Transmit_v6_static/Transmit_v6_static.ino" ` + "docs/TELEMETRY_V6.md" ` + "bundled_firmware/gateway-firmware.bin" ` + "bundled_firmware/manifest.json" ` --target main ` - --title "ЦУП Альтаир v6 ($env:RELEASE_TAG) — Технопром 2026. Миссия на Луну" ` - --notes "ЦУП Альтаир v6: фиксированный 29-символьный пакет телеметрии с XOR, режимом 0/1, профиль CC1101 435.000 МГц, ESP32-приёмник с проверкой XOR, RSSI/SNR и STM32-скетч статического передатчика." ` + --title "ЦУП Альтаир v6 исправленный ($env:RELEASE_TAG) — Технопром 2026. Миссия на Луну" ` + --notes "Исправленный релиз v6. Приём ESP32+CC1101 переведён на проверенный стендом путь GDO0 polling; явно зафиксированы NRZ, sync 0x12AD, variable packet length и CRC. Профиль: 435.000 МГц, 4.8 kbps, 2-FSK, deviation 5 кГц, RX BW 58 кГц. Канонический пакет 02,00001,00015,3.00,4.20,1,33. В Release дополнительно приложены STM32-скетч, ESP32 firmware и документация." ` --latest diff --git a/README.md b/README.md index f530145..140c5d1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **ЦУП Альтаир — Центр управления полётами** — локальное Windows-приложение учебной наземной станции для малых космических аппаратов. -**Текущий релиз: v6 / 0.6.0 — Технопром 2026, «Миссия на Луну».** +**Текущий релиз: v6 / 0.6.1 — исправленный приём CC1101, Технопром 2026, «Миссия на Луну».** ## Что нового в v6 @@ -48,22 +48,38 @@ ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,CHECKSUM 58 kHz RX bandwidth 5 dBm 16-bit preamble +NRZ +sync word 0x12AD +variable packet length +CRC ON ``` Наземная архитектура: ```text -STM32 + CC1101 +STM32 + CC1101 IntroSat │ │ 435 MHz ▼ -CC1101 + ESP32-WROOM-32 +обычный CC1101 + ESP32-WROOM-32 │ USB ▼ ЦУП Альтаир ``` -ESP32 проверяет XOR и только после успешной проверки передаёт данные в приложение. Дополнительно наземный шлюз добавляет `RSSI`, `LQI` и оценочный `SNR`. +Совместимость CC1101 IntroSat и обычного CC1101 подтверждена стендовым приёмом полного пакета `02,00001,00015,3.00,4.20,1,33`. ESP32 проверяет XOR и только после успешной проверки передаёт данные в приложение. Дополнительно наземный шлюз добавляет `RSSI`, `LQI` и оценочный `SNR`. + +## Исправление 0.6.1 + +В 0.6.0 сложная прошивка ESP32-шлюза не воспроизводила успешный приём, хотя минимальный тестовый приёмник на том же ESP32 + CC1101 принимал пакет корректно. В 0.6.1 приёмный путь приведён к проверенному стендом варианту: + +- `GDO0` на `GPIO4` опрашивается напрямую вместо зависимости от callback/interrupt; +- явно задаются `NRZ`, sync word `0x12AD`, переменная длина пакета и CRC; +- сохранён неблокирующий USB-интерфейс ЦУПа; +- сохранены нормализация `$TEL`, XOR, RSSI/LQI/SNR и поддержка E32; +- в установщик встраивается исправленная прошивка ESP32. + +Подробности: `docs/RELEASE_V6_1_RX_FIX.md`. ## STM32-передатчик @@ -91,7 +107,7 @@ GDO2 = PA1 firmware/radio_gateway_esp32/ ``` -Версия firmware для v6: **0.3.0**. При первом запуске v6 старые сохранённые настройки v5 мигрируются на новый профиль 435.000 МГц. +Базовая логика firmware v6 — **0.3.0**, исправленный RX-путь релиза 0.6.1 имеет метку `v6.1-gdo0-poll`. При первом запуске v6 старые сохранённые настройки v5 мигрируются на профиль 435.000 МГц. Штатное подключение CC1101 к ESP32-WROOM-32: @@ -136,16 +152,17 @@ mode = (batteryVoltage < 2.00f) ? 0 : 1; ## Установка Windows -Релизный установщик: +Исправленный релизный установщик: ```text -CUP-Altair-Setup-0.6.0.exe +CUP-Altair-Setup-0.6.1.exe ``` -После установки оператору не требуются Python, PlatformIO или Arduino IDE. Прошивка ESP32 по-прежнему может выполняться прямо из ЦУП Альтаир. +После установки оператору не требуются Python, PlatformIO или Arduino IDE. Прошивка ESP32 может выполняться прямо из ЦУП Альтаир. ## Документация +- `docs/RELEASE_V6_1_RX_FIX.md` — исправление реального приёма CC1101; - `docs/TELEMETRY_V6.md` — точный формат пакета v6 и XOR; - `firmware/transmitter_stm32_v6/README.md` — STM32-передатчик; - `docs/TECHNICAL_MANUAL.md` — общее руководство; @@ -153,13 +170,14 @@ CUP-Altair-Setup-0.6.0.exe - `docs/FIRMWARE_FLASHER.md` — встроенная прошивка ESP32; - `firmware/rotator_arduino_uno/README.md` — поворотное устройство. -## Что обязательно проверить на стенде +## Стендовая проверка + +Уже подтверждено физически: -Автоматическая сборка подтверждает компиляцию ПО, но перед демонстрацией нужно физически проверить: +1. ESP32 видит обычный CC1101 по SPI; +2. ESP32 + CC1101 передаёт на 435.000 МГц, сигнал виден на SDR; +3. STM32 + CC1101 IntroSat передаёт пакет; +4. обычный CC1101 + ESP32 принимает полный 29-символьный пакет; +5. принятый пример: `02,00001,00015,3.00,4.20,1,33`, RSSI порядка `-23.5 dBm`, LQI `52` в конкретном близком стендовом опыте. -1. STM32 + CC1101 на 435.000 МГц; -2. приём того же пакета ESP32 + CC1101; -3. правильность XOR `33` для тестовой строки; -4. заполнение карточек ЦУПа; -5. реальные значения RSSI/SNR; -6. работу конкретной STM32-платы и её SPI-распиновку. +После сборки 0.6.1 остаётся проверить уже интегрированную цепочку `STM32 → CC1101 → ESP32 gateway → ЦУП Альтаир` на конкретном компьютере демонстрации. diff --git a/docs/RELEASE_V6.md b/docs/RELEASE_V6.md index dc528f6..d1870af 100644 --- a/docs/RELEASE_V6.md +++ b/docs/RELEASE_V6.md @@ -1,6 +1,6 @@ -# ЦУП Альтаир v6 / 0.6.0 +# ЦУП Альтаир v6 / 0.6.1 -Дата подготовки релиза: 26 августа 2026 г. +Дата исправленного релиза: 27 августа 2026 г. ## Основной пакет телеметрии @@ -31,26 +31,33 @@ Bit rate: 4.8 kbps Девиация: 5 кГц RX bandwidth: 58 кГц TX power: 5 dBm +Preamble: 16 bit +Encoding: NRZ +Sync word: 0x12AD +Packet length: variable +CRC: ON ``` ## Передатчик STM32 В каталоге `firmware/transmitter_stm32_v6` находится пример передатчика STM32 + CC1101, который раз в секунду передаёт статический канонический пакет v6 на 435.000 МГц. -## Наземный шлюз ESP32 +## Наземный шлюз ESP32 — исправление 0.6.1 -ESP32-WROOM-32 + CC1101 принимает пакет, проверяет XOR и передаёт в ЦУП нормализованную строку `$TEL` с добавленными наземными параметрами RSSI, LQI и диагностической оценкой SNR. +Стендовая проверка подтвердила, что STM32 + CC1101 IntroSat и обычный CC1101 + ESP32 аппаратно совместимы: простой приёмник RadioLib принял полный пакет `02,00001,00015,3.00,4.20,1,33` длиной 29 символов. + +В 0.6.1 сложный ESP32 gateway переведён на тот же надёжный путь приёма: GDO0 (GPIO4) опрашивается напрямую, а `NRZ`, sync word `0x12AD`, variable packet length и CRC задаются явно. После приёма ESP32 проверяет XOR и передаёт в ЦУП нормализованную строку `$TEL` с RSSI, LQI и диагностической оценкой SNR. ## Windows-приложение -Версия приложения: `0.6.0`. +Версия приложения: `0.6.1`. Имя установщика: ```text -CUP-Altair-Setup-0.6.0.exe +CUP-Altair-Setup-0.6.1.exe ``` -Релиз содержит исходный код ЦУПа, документацию, STM32-передатчик и встроенную прошивку ESP32-радиошлюза. +GitHub Release v0.6.1 должен содержать установщик, `Transmit_v6_static.ino`, исправленную прошивку ESP32, manifest встроенного прошивальщика и `TELEMETRY_V6.md`. -Финальная публикация v0.6.0 запускается только после успешной автоматической сборки обеих прошивок и Windows-установщика. +Подробности стендового исправления приведены в `docs/RELEASE_V6_1_RX_FIX.md`. diff --git a/docs/RELEASE_V6_1_RX_FIX.md b/docs/RELEASE_V6_1_RX_FIX.md new file mode 100644 index 0000000..927e3b4 --- /dev/null +++ b/docs/RELEASE_V6_1_RX_FIX.md @@ -0,0 +1,77 @@ +# ЦУП Альтаир v6.1 — исправление приёма CC1101 + +Версия приложения: **0.6.1**. Это исправленный релиз поколения v6 для Технопрома 2026. + +## Что исправлено + +На стендовой проверке простой приёмник ESP32 + обычный CC1101 успешно принял пакет от STM32 + CC1101 IntroSat: + +```text +02,00001,00015,3.00,4.20,1,33 +``` + +Параметры рабочей радиолинии: + +```text +435.000 МГц +4.8 kbps +2-FSK +девиация 5 кГц +RX bandwidth 58 кГц +preamble 16 bit +NRZ +sync word 0x12AD +variable packet length +CRC включён +``` + +Проблема релиза 0.6.0 находилась в сложной логике приёмного шлюза ESP32. Для v6.1 путь приёма приведён к проверенному стендом варианту: окончание пакета определяется прямым опросом GDO0 (GPIO4) вместо зависимости от callback/interrupt. USB-интерфейс при этом остаётся неблокирующим. + +Все пакетные параметры, влияющие на совместимость, задаются явно: NRZ, `0x12AD`, переменная длина пакета и CRC. + +## Подключение CC1101 к ESP32-WROOM-32 + +| CC1101 | ESP32 | +|---|---| +| VCC | 3.3 V | +| GND | GND | +| SCK | GPIO18 | +| MISO / SO | GPIO19 | +| MOSI / SI | GPIO23 | +| CSn | GPIO21 | +| GDO0 | GPIO4 | +| GDO2 | GPIO27 | + +## Телеметрия v6 + +Пакет содержит 29 ASCII-символов: + +```text +02,00001,00015,3.00,4.20,1,33 +``` + +Поля: + +```text +ID(2),PACKET(5),UPTIME(5),PANEL_POWER(4),VOLT(4),MODE(1),CHECKSUM(2 HEX) +``` + +XOR вычисляется по строке до контрольной суммы, включая последнюю запятую: + +```text +02,00001,00015,3.00,4.20,1, +``` + +Для приведённого пакета XOR равен `0x33`. + +## Файлы релиза + +GitHub Release v0.6.1 должен содержать: + +- `CUP-Altair-Setup-0.6.1.exe`; +- `Transmit_v6_static.ino` — тестовый STM32 + CC1101 передатчик; +- `gateway-firmware.bin` — прошивка ESP32 радиошлюза; +- `manifest.json` для встроенного прошивальщика; +- `TELEMETRY_V6.md`. + +Аппаратная совместимость CC1101 IntroSat и обычного CC1101 подтверждена фактическим приёмом полного 29-символьного пакета на стенде. diff --git a/firmware/radio_gateway_esp32/platformio.ini b/firmware/radio_gateway_esp32/platformio.ini index cffd424..e85a8f3 100644 --- a/firmware/radio_gateway_esp32/platformio.ini +++ b/firmware/radio_gateway_esp32/platformio.ini @@ -11,3 +11,6 @@ lib_deps = jgromes/RadioLib@7.7.1 build_flags = -D CORE_DEBUG_LEVEL=0 +build_src_filter = + + + - diff --git a/firmware/radio_gateway_esp32/src/main_fixed.cpp b/firmware/radio_gateway_esp32/src/main_fixed.cpp new file mode 100644 index 0000000..ed7139c --- /dev/null +++ b/firmware/radio_gateway_esp32/src/main_fixed.cpp @@ -0,0 +1,151 @@ +#include +#include + +/* + * ЦУП Альтаир v6.1 — исправленный приём CC1101. + * + * Этот файл оборачивает проверенный код gateway v6 и заменяет только цикл + * обслуживания приёмника. Причина: на стенде блокирующий приём RadioLib + * успешно принял канонический пакет + * 02,00001,00015,3.00,4.20,1,33 + * при 435.000 МГц / 4.8 kbps / 2-FSK / deviation 5 kHz / BW 58 kHz, + * тогда как прежняя версия шлюза зависела от callback/interrupt GDO0. + * + * В исправленной версии GDO0 (GPIO4) опрашивается напрямую. Это повторяет + * надёжный путь, которым RadioLib пользуется при обычном receive(), но не + * блокирует USB-интерфейс ЦУПа. + */ + +// main.cpp остаётся источником всей существующей логики gateway: USB-команды, +// E32, нормализация $TEL, XOR, RSSI/LQI/SNR и сохранение конфигурации. +// Переименовываем только старый loop(), чтобы определить исправленный ниже. +#define loop altairLegacyLoop +#include "main.cpp" +#undef loop + +namespace AltairGateway { + +constexpr char RX_PATCH_VERSION[] = "v6.1-gdo0-poll"; + +bool v6RxProfileApplied = false; +RadioType lastRxType = RadioType::None; +float lastRxFreq = 0.0f; +float lastRxRate = 0.0f; +float lastRxBw = 0.0f; +String lastRxMod; +bool lastRxCrc = false; + +bool applyKnownGoodV6PacketProfile(bool verbose = true) { + if (!radioReady || activeType != RadioType::CC1101) return false; + + // Не используем callback GDO0: приём проверен прямым чтением GPIO4. + cc1101.clearPacketReceivedAction(); + ccPacketReceived = false; + + int16_t state = cc1101.standby(); + if (state != RADIOLIB_ERR_NONE) { + if (verbose) err("CC1101_V6_STANDBY,CODE=" + String(state)); + return false; + } + + // Явно задаём все параметры пакетного режима, которые были использованы + // в успешно проверенном простом приёмнике. + state = cc1101.setOOK(false); + if (state == RADIOLIB_ERR_NONE) state = cc1101.setDataShaping(RADIOLIB_SHAPING_NONE); + if (state == RADIOLIB_ERR_NONE) state = cc1101.setEncoding(RADIOLIB_ENCODING_NRZ); + if (state == RADIOLIB_ERR_NONE) state = cc1101.setSyncWord(0x12, 0xAD, 0, false); + if (state == RADIOLIB_ERR_NONE) state = cc1101.variablePacketLengthMode(RADIOLIB_CC1101_MAX_PACKET_LENGTH); + if (state == RADIOLIB_ERR_NONE) state = cc1101.setCrcFiltering(true); + + if (state != RADIOLIB_ERR_NONE) { + radioReady = false; + if (verbose) err("CC1101_V6_PROFILE,CODE=" + String(state)); + return false; + } + + state = cc1101.startReceive(); + if (state != RADIOLIB_ERR_NONE) { + radioReady = false; + if (verbose) err("CC1101_V6_RX_START,CODE=" + String(state)); + return false; + } + + v6RxProfileApplied = true; + lastRxType = activeType; + lastRxFreq = cc.frequency; + lastRxRate = cc.bitRate; + lastRxBw = cc.bandwidth; + lastRxMod = cc.modulation; + lastRxCrc = cc.crc; + + if (verbose) { + info("RADIO_RX_PATCH=" + String(RX_PATCH_VERSION) + + ",TYPE=CC1101,GDO0=POLL,SYNC=12AD,ENC=NRZ,LEN=VARIABLE,CRC=1"); + } + return true; +} + +bool rxProfileNeedsRefresh() { + if (!v6RxProfileApplied) return true; + if (activeType != lastRxType) return true; + if (!nearly(cc.frequency, lastRxFreq)) return true; + if (!nearly(cc.bitRate, lastRxRate)) return true; + if (!nearly(cc.bandwidth, lastRxBw)) return true; + if (cc.modulation != lastRxMod) return true; + if (cc.crc != lastRxCrc) return true; + return false; +} + +void pollCcFixed() { + if (!radioReady || activeType != RadioType::CC1101) return; + + if (rxProfileNeedsRefresh()) { + if (!applyKnownGoodV6PacketProfile(true)) return; + } + + // В режиме startReceive() RadioLib отображает окончание принятого пакета + // на GDO0. На нашей плате это GPIO4. Проверка этого же вывода дала + // стабильный приём на стенде, поэтому здесь не нужен attachInterrupt(). + if (digitalRead(CC_GDO0) != HIGH) return; + + String payload; + const int16_t result = cc1101.readData(payload); + const float rssi = cc1101.getRSSI(); + const uint8_t lqi = cc1101.getLQI(); + + // CC1101 не выдаёт отдельный SNR. Для интерфейса сохраняем диагностическую + // оценку относительно опорного шумового уровня -105 dBm. + const float snr = constrain(rssi - (-105.0f), -10.0f, 60.0f); + + if (result == RADIOLIB_ERR_NONE) { + forwardCcPayload(payload, rssi, lqi, snr); + } else if (result == RADIOLIB_ERR_CRC_MISMATCH) { + err("RADIO_RX_CRC"); + } else { + err("RADIO_RX,TYPE=CC1101,CODE=" + String(result)); + } + + const int16_t restart = cc1101.startReceive(); + if (restart != RADIOLIB_ERR_NONE) { + radioReady = false; + v6RxProfileApplied = false; + err("CC1101_RX_RESTART,CODE=" + String(restart)); + } +} + +} // namespace AltairGateway + +void loop() { + using namespace AltairGateway; + + pollUsb(); + + if (activeType == RadioType::CC1101) { + pollCcFixed(); + } else { + v6RxProfileApplied = false; + pollE32(); + } + + delay(1); +} diff --git a/package.json b/package.json index 2b10f31..0491b5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "altair-mcc", - "version": "0.6.0", + "version": "0.6.1", "private": true, "description": "ЦУП Альтаир — Центр управления полётами, Технопром 2026, Миссия на Луну", "main": "desktop/main.cjs",