From 18b04135abc613ddfc43f7e65407f48d124d5ab5 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:37:30 +0700 Subject: [PATCH 01/10] v8.1: add guided gateway setup, compact layout and ID roster --- js/v8.1-ui.js | 366 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 js/v8.1-ui.js diff --git a/js/v8.1-ui.js b/js/v8.1-ui.js new file mode 100644 index 0000000..7bcf3cf --- /dev/null +++ b/js/v8.1-ui.js @@ -0,0 +1,366 @@ +"use strict"; + +/* ЦУП Альтаир v8.1 — компоновка рабочего экрана и сценарий первого запуска. */ +(() => { + const VERSION = "0.8.1"; + const GATEWAY_READY_KEY = "altair-v81-gateway-ready"; + const EXPECTED_IDS = Array.from({ length: 10 }, (_, index) => String(index + 1).padStart(2, "0")); + const seen = new Map(); + let gatewayReady = false; + let dashboardObserver = null; + let flasherObserver = null; + + function log(message, type = "info") { + window.OpenMCCLogger?.write?.(message, type, "V8.1"); + } + + function safeStorageGet(key) { + try { return localStorage.getItem(key); } catch { return null; } + } + + function safeStorageSet(key, value) { + try { localStorage.setItem(key, value); } catch {} + } + + function normalizeId(value) { + const raw = String(value ?? "").trim().toUpperCase(); + if (!raw) return "UNKNOWN"; + if (/^\d{1,2}$/.test(raw)) return raw.padStart(2, "0"); + return raw.slice(0, 16); + } + + function setBranding() { + document.title = "ЦУП Альтаир v8.1 — Миссия на Луну"; + const badge = document.querySelector(".buildBadge"); + if (badge) badge.textContent = `v8 · ${VERSION}`; + + const aboutVersion = document.querySelector("#v8AboutDialog .aboutHeader div > div"); + if (aboutVersion && /версия/i.test(aboutVersion.textContent || "")) { + aboutVersion.textContent = `Центр управления полётами · версия ${VERSION}`; + } + + const eventLogFirst = document.querySelector("#eventLog > div:first-child"); + if (eventLogFirst && /ЦУП Альтаир v8/i.test(eventLogFirst.textContent || "")) { + eventLogFirst.textContent = "ЦУП Альтаир v8.1 запущен."; + } + } + + function setGatewayReady(value, source = "manual") { + gatewayReady = Boolean(value); + if (gatewayReady) safeStorageSet(GATEWAY_READY_KEY, VERSION); + + const step = document.querySelector('[data-v81-step="flash"]'); + const state = document.getElementById("v81FlashState"); + const acknowledge = document.getElementById("v81AlreadyFlashed"); + if (step) step.classList.toggle("done", gatewayReady); + if (state) state.textContent = gatewayReady ? "шлюз готов" : "требуется прошивка или подтверждение"; + if (acknowledge) acknowledge.textContent = gatewayReady ? "Подтверждено" : "Шлюз уже прошит"; + + refreshConnectGate(); + if (gatewayReady && source !== "restore") log("Шлюз ESP32 отмечен как готовый к подключению.", "success"); + } + + function getConnected() { + return Boolean(window.OpenMCCSerial?.getState?.().connected); + } + + function refreshConnectGate() { + const original = document.getElementById("connectButton"); + const shortcut = document.getElementById("v81ConnectShortcut"); + const status = document.getElementById("v81ConnectionStepState"); + const connected = getConnected(); + + if (original) { + original.disabled = !gatewayReady && !connected; + original.title = gatewayReady || connected ? "" : "Сначала прошейте ESP32-шлюз или подтвердите, что он уже прошит."; + } + if (shortcut) { + shortcut.disabled = !gatewayReady && !connected; + shortcut.textContent = connected ? "Отключить" : "Подключить"; + } + if (status) { + if (connected) status.textContent = "соединение установлено"; + else status.textContent = gatewayReady ? "готово к подключению" : "ожидает шага 1"; + } + + const step = document.querySelector('[data-v81-step="connect"]'); + if (step) { + step.classList.toggle("ready", gatewayReady && !connected); + step.classList.toggle("done", connected); + } + } + + function installSetupFlow() { + const dashboard = document.getElementById("dashboard"); + if (!dashboard) return null; + + let flow = document.getElementById("v81SetupFlow"); + if (!flow) { + flow = document.createElement("section"); + flow.id = "v81SetupFlow"; + flow.className = "panel wide v81SetupFlow"; + flow.innerHTML = ` +
+
БЫСТРЫЙ ЗАПУСКПодготовка шлюза ЦУП
+ 435.000 МГц · 2-FSK · 4.8 kbps · RX BW 203 кГц +
+
+
+ 1 +
Прошивка ESP32требуется прошивка или подтверждение
+ + +
+ +
+ 2 +
Соединение с ЦУПоможидает шага 1
+ + +
+
`; + dashboard.prepend(flow); + + document.getElementById("v81OpenFlasher")?.addEventListener("click", () => { + const firmware = document.getElementById("firmwarePanel"); + if (!firmware) { + log("Панель прошивальщика ещё загружается. Повторите через секунду.", "warning"); + return; + } + firmware.classList.remove("v81-utility-collapsed"); + firmware.querySelector(".v81UtilityToggle")?.replaceChildren(document.createTextNode("Свернуть")); + firmware.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + + document.getElementById("v81AlreadyFlashed")?.addEventListener("click", () => setGatewayReady(true, "manual")); + document.getElementById("v81ConnectShortcut")?.addEventListener("click", () => { + const button = document.getElementById("connectButton"); + if (button && !button.disabled) button.click(); + }); + document.getElementById("v81OpenSerialDetails")?.addEventListener("click", () => { + const serial = document.getElementById("serialPanel"); + if (!serial) return; + serial.classList.remove("v8-collapsed"); + const toggle = serial.querySelector(".v8PanelToggle"); + if (toggle) toggle.textContent = "Свернуть"; + serial.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + } + + const restored = safeStorageGet(GATEWAY_READY_KEY); + if (restored === VERSION || restored === "0.8.0") setGatewayReady(true, "restore"); + else setGatewayReady(false, "restore"); + return flow; + } + + function makeSerialCompact() { + const serial = document.getElementById("serialPanel"); + if (!serial) return; + serial.classList.add("v81SerialPanel"); + const eyebrow = serial.querySelector(".panelEyebrow"); + if (eyebrow) eyebrow.textContent = "ШАГ 2 · РАСШИРЕННЫЕ НАСТРОЙКИ"; + const h2 = serial.querySelector("h2"); + if (h2) h2.textContent = "Соединение / COM"; + if (!getConnected()) { + serial.classList.add("v8-collapsed"); + const toggle = serial.querySelector(".v8PanelToggle"); + if (toggle) toggle.textContent = "Развернуть"; + } + refreshConnectGate(); + } + + function compactFirmwarePanel() { + const firmware = document.getElementById("firmwarePanel"); + if (!firmware) return; + firmware.classList.add("v81FirmwarePanel", "v81-utility-collapsed"); + const eyebrow = firmware.querySelector(".panelEyebrow"); + if (eyebrow) eyebrow.textContent = "ШАГ 1 · ESP32 FLASH"; + + if (!firmware.querySelector(".v81UtilityToggle")) { + const row = firmware.querySelector(".panelTitleRow"); + const button = document.createElement("button"); + button.type = "button"; + button.className = "v81UtilityToggle"; + button.textContent = "Развернуть"; + row?.appendChild(button); + button.addEventListener("click", () => { + const collapsed = firmware.classList.toggle("v81-utility-collapsed"); + button.textContent = collapsed ? "Развернуть" : "Свернуть"; + }); + } + watchFlasherSuccess(); + } + + function watchFlasherSuccess() { + if (flasherObserver) return; + const firmware = document.getElementById("firmwarePanel"); + if (!firmware) return; + const inspect = () => { + const reconnect = document.getElementById("firmwareReconnectButton"); + const status = document.getElementById("firmwareFlasherStatus"); + const text = String(status?.textContent || "").toUpperCase(); + if ((reconnect && !reconnect.disabled) || /ГОТОВ.*ПОДКЛЮЧ|УСПЕШ|ЗАПИС/.test(text)) setGatewayReady(true, "flasher"); + }; + flasherObserver = new MutationObserver(inspect); + flasherObserver.observe(firmware, { subtree: true, childList: true, attributes: true, characterData: true }); + inspect(); + } + + function rosterEntry(id) { + const list = document.getElementById("v81IdRosterList"); + if (!list) return null; + let item = [...list.children].find(child => child.dataset.rosterId === id); + if (item) return item; + item = document.createElement("div"); + item.className = "v81RosterItem never"; + item.dataset.rosterId = id; + item.innerHTML = `ID ${id}НЕТ СВЯЗИ0 пак.`; + list.appendChild(item); + return item; + } + + function installIdRoster() { + const panel = document.getElementById("chartPanel"); + const grid = panel?.querySelector(".altairChartGrid"); + if (!panel || !grid) return; + + let layout = panel.querySelector(".v81ChartsLayout"); + if (!layout) { + layout = document.createElement("div"); + layout.className = "v81ChartsLayout"; + grid.parentNode.insertBefore(layout, grid); + layout.appendChild(grid); + const aside = document.createElement("aside"); + aside.id = "v81IdRoster"; + aside.innerHTML = ` +
SPACECRAFT IDSСвязь по ID
0 / ${EXPECTED_IDS.length}
+

Серый — связи ещё не было. Цвет — ID хотя бы раз принят. «ПЕРЕДАЁТ» означает пакет за последние 5 с.

+
`; + layout.appendChild(aside); + } + EXPECTED_IDS.forEach(rosterEntry); + updateRoster(); + } + + function recordId(telemetry) { + if (!telemetry || typeof telemetry !== "object" || telemetry.ID === undefined) return; + const id = normalizeId(telemetry.ID); + const item = seen.get(id) || { count: 0, firstSeen: Date.now(), lastSeen: 0, color: null }; + item.count += 1; + item.lastSeen = Date.now(); + item.color = window.OpenMCCCharts?.colorForId?.(id) || item.color || "#00d9ff"; + seen.set(id, item); + rosterEntry(id); + updateRoster(); + } + + function updateRoster() { + const list = document.getElementById("v81IdRosterList"); + if (!list) return; + const now = Date.now(); + list.querySelectorAll(".v81RosterItem").forEach(item => { + const id = item.dataset.rosterId; + const info = seen.get(id); + const dot = item.querySelector(".v81RosterDot"); + const status = item.querySelector(".v81RosterStatus"); + const small = item.querySelector("small"); + item.classList.remove("never", "seen", "active"); + if (!info) { + item.classList.add("never"); + if (dot) dot.style.background = ""; + if (status) status.textContent = "НЕТ СВЯЗИ"; + if (small) small.textContent = "0 пак."; + return; + } + const active = now - info.lastSeen <= 5000; + item.classList.add("seen"); + if (active) item.classList.add("active"); + if (dot) dot.style.background = info.color; + if (status) status.textContent = active ? "ПЕРЕДАЁТ" : "БЫЛ НА СВЯЗИ"; + if (small) small.textContent = `${info.count} пак.`; + }); + const expectedSeen = EXPECTED_IDS.filter(id => seen.has(id)).length; + const extra = [...seen.keys()].filter(id => !EXPECTED_IDS.includes(id)).length; + const count = document.getElementById("v81RosterCount"); + if (count) count.textContent = extra ? `${expectedSeen}/${EXPECTED_IDS.length} +${extra}` : `${expectedSeen} / ${EXPECTED_IDS.length}`; + } + + function resetRoster() { + seen.clear(); + const list = document.getElementById("v81IdRosterList"); + if (list) { + list.innerHTML = ""; + EXPECTED_IDS.forEach(rosterEntry); + } + updateRoster(); + } + + function reorganizeDashboard() { + const dashboard = document.getElementById("dashboard"); + if (!dashboard) return; + const flow = installSetupFlow(); + const telemetry = document.getElementById("telemetryPanel"); + const charts = document.getElementById("chartPanel"); + const command = document.getElementById("commandPanel"); + const system = document.getElementById("systemPanel"); + const radio = document.getElementById("radioPanel"); + const firmware = document.getElementById("firmwarePanel"); + const serial = document.getElementById("serialPanel"); + const raw = document.getElementById("rawSerialPanel"); + const logPanel = document.getElementById("logPanel"); + + if (flow && telemetry) flow.insertAdjacentElement("afterend", telemetry); + if (telemetry && charts) telemetry.insertAdjacentElement("afterend", charts); + if (charts && command) charts.insertAdjacentElement("afterend", command); + if (command && system) command.insertAdjacentElement("afterend", system); + if (system && radio) system.insertAdjacentElement("afterend", radio); + if (radio && firmware) radio.insertAdjacentElement("afterend", firmware); + if (firmware && serial) firmware.insertAdjacentElement("afterend", serial); + if (logPanel && raw) logPanel.parentNode.insertBefore(raw, logPanel); + + makeSerialCompact(); + compactFirmwarePanel(); + installIdRoster(); + + const serialNav = document.querySelector('#quickNav [data-scroll-target="serialPanel"]'); + if (serialNav) { + serialNav.dataset.scrollTarget = "v81SetupFlow"; + serialNav.textContent = "Подготовка шлюза"; + serialNav.onclick = () => document.getElementById("v81SetupFlow")?.scrollIntoView({ behavior: "smooth", block: "start" }); + } + } + + function installObservers() { + const dashboard = document.getElementById("dashboard"); + if (!dashboard || dashboardObserver) return; + let timer = null; + dashboardObserver = new MutationObserver(() => { + clearTimeout(timer); + timer = setTimeout(reorganizeDashboard, 60); + }); + dashboardObserver.observe(dashboard, { childList: true }); + } + + function installEvents() { + window.addEventListener("openmcc:telemetry", event => recordId(event.detail)); + window.addEventListener("openmcc:v8-reset", resetRoster); + window.addEventListener("openmcc:serial-connected", () => { + refreshConnectGate(); + document.getElementById("serialPanel")?.classList.add("v8-collapsed"); + }); + window.addEventListener("openmcc:serial-disconnected", refreshConnectGate); + setInterval(updateRoster, 1000); + } + + function initialize() { + setBranding(); + reorganizeDashboard(); + installObservers(); + installEvents(); + setTimeout(() => { setBranding(); reorganizeDashboard(); }, 900); + log("Интерфейс v8.1 готов: прошивка → соединение, телеметрия и графики наверху, контроль ID активен.", "success"); + } + + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => setTimeout(initialize, 600), { once: true }); + else setTimeout(initialize, 600); +})(); From c8f9aee255127215acf1131e6b46e39ecf2dfcbe Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:38:07 +0700 Subject: [PATCH 02/10] v8.1: compact header, guided setup and full-width telemetry layout --- css/v8.css | 290 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 185 insertions(+), 105 deletions(-) diff --git a/css/v8.css b/css/v8.css index 50b1d0e..43b50c5 100644 --- a/css/v8.css +++ b/css/v8.css @@ -1,16 +1,30 @@ -/* ЦУП Альтаир v8 */ +/* ЦУП Альтаир v8.1 */ .altairLogo .logoMark.v8LogoMark { - width: 42px; - height: 42px; + width: 46px; + height: 46px; border: 0; - background: rgba(255,255,255,.96); - border-radius: 10px; + background: transparent; + border-radius: 0; display: grid; place-items: center; - overflow: hidden; - box-shadow: 0 0 24px rgba(44,108,255,.25); + overflow: visible; + box-shadow: none; } -.altairLogo .logoMark.v8LogoMark img { width: 36px; height: 36px; object-fit: contain; } +.altairLogo .logoMark.v8LogoMark img { + width: 44px; + height: 44px; + object-fit: contain; + background: transparent; +} + +#topBar { + grid-template-columns: minmax(300px, 1.8fr) minmax(180px, .82fr) minmax(235px, 1fr) minmax(110px, .52fr) minmax(150px, .68fr) auto; + gap: 12px; + min-height: 70px; + padding-block: 10px; +} +#topBar .topActions { grid-row: 1; align-self: center; flex-wrap: nowrap; } +.buildBadge { white-space: nowrap; } .v8FrequencyMetric #v8FrequencyValue { color: #73e8ff; @@ -19,36 +33,136 @@ white-space: nowrap; } -.v8PanelToggle { +.v8PanelToggle, +.v81UtilityToggle { margin-left: auto; - min-width: 118px; + min-width: 106px; border: 1px solid rgba(79,180,255,.34); border-radius: 9px; background: rgba(8,29,52,.78); color: #a9dfff; - padding: 7px 10px; + padding: 6px 9px; cursor: pointer; } -.v8PanelToggle:hover { border-color: rgba(79,180,255,.7); color: #e3f7ff; } +.v8PanelToggle:hover, +.v81UtilityToggle:hover { border-color: rgba(79,180,255,.7); color: #e3f7ff; } #serialPanel.v8-collapsed > :not(.panelTitleRow):not(.v8ConnectionSummary) { display: none !important; } -.v8ConnectionSummary { - display: none; - gap: 8px; - flex-wrap: wrap; - margin-top: 8px; -} +#serialPanel.v8-collapsed { min-height: 0; padding-block: 9px; } +#serialPanel.v8-collapsed .panelTitleRow { margin: 0; align-items: center; } +#serialPanel.v8-collapsed .panelTitleRow h2 { margin: 1px 0 0; font-size: 15px; } +#serialPanel.v8-collapsed .panelEyebrow { font-size: 9px; } +.v8ConnectionSummary { display: none; gap: 6px; flex-wrap: wrap; margin-top: 6px; } #serialPanel.v8-collapsed .v8ConnectionSummary { display: flex; } .v8ConnectionSummary span { - border: 1px solid rgba(77,116,157,.34); - border-radius: 999px; - padding: 6px 10px; - color: #9fb6cf; - background: rgba(4,17,31,.65); - font-size: .78rem; + border: 0; + border-radius: 0; + padding: 0 8px 0 0; + color: #8198b2; + background: transparent; + font-size: .72rem; } .v8ConnectionSummary strong { color: #d9f5ff; } +#v81SetupFlow { + grid-column: span 12 !important; + padding: 12px 14px; + background: linear-gradient(90deg, rgba(0,217,255,.055), rgba(8,18,32,.82)); +} +.v81SetupTitle { display: flex; justify-content: space-between; align-items: end; gap: 16px; margin-bottom: 9px; } +.v81SetupTitle strong { display:block; margin-top:2px; color:#eaf8ff; font-size:16px; } +.v81SetupTitle > span { color:#6f91b1; font: 700 11px/1.2 Consolas,monospace; } +.v81SetupSteps { display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1fr); gap: 10px; align-items: stretch; } +.v81SetupStep { + min-width: 0; + display: grid; + grid-template-columns: 34px minmax(140px,1fr) auto auto; + align-items: center; + gap: 9px; + padding: 9px 10px; + border: 1px solid rgba(69,105,143,.42); + border-radius: 11px; + background: rgba(4,14,26,.72); +} +.v81SetupStep.ready { border-color: rgba(0,217,255,.42); } +.v81SetupStep.done { border-color: rgba(58,219,143,.5); box-shadow: inset 0 0 0 1px rgba(58,219,143,.05); } +.v81StepNo { + width: 28px; + height: 28px; + display:grid; + place-items:center; + border-radius:50%; + background:rgba(0,217,255,.12); + color:#7deaff; + font:800 12px Consolas,monospace; +} +.v81SetupStep.done .v81StepNo { background:rgba(58,219,143,.15); color:#75efb0; } +.v81StepText { min-width:0; display:flex; flex-direction:column; gap:2px; } +.v81StepText strong { color:#dff6ff; font-size:13px; } +.v81StepText span { color:#8299b3; font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.v81SetupArrow { display:grid; place-items:center; color:#4e789d; font-size:20px; } +.v81StepButton { + width:auto; + margin:0; + min-height:32px; + padding:6px 9px; + border-radius:8px; + border:1px solid rgba(65,103,142,.55); + background:#081a2c; + color:#a9bfd5; + cursor:pointer; + white-space:nowrap; +} +.v81StepButton.primary { border-color:rgba(0,217,255,.45); color:#b9f5ff; background:rgba(0,108,144,.18); } +.v81StepButton:disabled { opacity:.38; cursor:not-allowed; } + +#telemetryPanel, +#chartPanel { grid-column: span 12 !important; } +#telemetryPanel .altairTelemetryGrid { grid-template-columns: repeat(5, minmax(0,1fr)); } +#telemetryPanel .telemetryCard { min-height: 96px; } + +.v81ChartsLayout { display: grid; grid-template-columns: minmax(0,1fr) 238px; gap: 12px; align-items: stretch; } +.v81ChartsLayout .altairChartGrid { grid-template-columns: repeat(2, minmax(0,1fr)); } +.v81ChartsLayout .chartSummaryCard { grid-column: 1 / -1; } +#v81IdRoster { + min-width: 0; + border: 1px solid rgba(58,94,132,.45); + border-radius: 12px; + background: rgba(4,13,24,.78); + padding: 11px; +} +.v81RosterHeader { display:flex; align-items:flex-end; justify-content:space-between; gap:8px; margin-bottom:7px; } +.v81RosterHeader strong { display:block; color:#e4f6ff; font-size:14px; } +#v81RosterCount { color:#73e8ff; font:800 12px Consolas,monospace; } +#v81IdRoster > p { margin:0 0 9px; color:#7089a3; font-size:10px; line-height:1.4; } +#v81IdRosterList { display:grid; gap:4px; } +.v81RosterItem { + display:grid; + grid-template-columns: 9px 48px minmax(0,1fr) auto; + gap:7px; + align-items:center; + min-height:27px; + padding:4px 6px; + border-radius:7px; + background:rgba(10,24,40,.45); +} +.v81RosterDot { width:8px; height:8px; border-radius:50%; background:#43556a; box-shadow:none; } +.v81RosterItem strong { color:#9fb2c6; font:700 11px Consolas,monospace; } +.v81RosterStatus { color:#667d95; font-size:9px; font-weight:800; letter-spacing:.04em; } +.v81RosterItem small { color:#657b91; font:9px Consolas,monospace; white-space:nowrap; } +.v81RosterItem.seen strong { color:#d7e8f5; } +.v81RosterItem.seen .v81RosterStatus { color:#9eb5ca; } +.v81RosterItem.active { background:rgba(10,38,43,.62); } +.v81RosterItem.active .v81RosterDot { box-shadow:0 0 8px currentColor; } +.v81RosterItem.active .v81RosterStatus { color:#70e8a8; } + +#firmwarePanel, +#serialPanel, +#rawSerialPanel { grid-column: span 12 !important; } +#firmwarePanel.v81-utility-collapsed > :not(.panelTitleRow) { display:none !important; } +#firmwarePanel.v81-utility-collapsed { min-height:0; padding-block:9px; } +#firmwarePanel.v81-utility-collapsed .panelTitleRow { margin:0; } + .v8ResetButton { border: 1px solid rgba(255,170,70,.38); background: rgba(80,42,8,.22); @@ -59,94 +173,60 @@ } .v8ResetButton:hover { border-color: rgba(255,170,70,.75); } -#rawSerialPanel .rawToolbar { - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - margin-bottom: 10px; -} +#rawSerialPanel .rawToolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:10px; } #rawSerialConsole { - background: #020811; - border: 1px solid rgba(72,114,160,.35); - border-radius: 10px; - min-height: 180px; - max-height: 360px; - overflow: auto; - padding: 10px 12px; - color: #a7c8e8; - font: 12px/1.45 "Cascadia Mono","Consolas",monospace; - white-space: pre-wrap; - word-break: break-word; -} -#rawSerialConsole .rx { color: #9de9ff; } -#rawSerialConsole .tx { color: #ffd479; } -#rawSerialConsole .err { color: #ff8e9a; } -#rawSerialConsole .info { color: #8cf0b4; } - -.v8CommandTarget { - display: grid; - grid-template-columns: minmax(100px, 180px) 1fr; - gap: 10px; - align-items: center; - margin: 0 0 14px; -} -.v8CommandTarget input { - width: 100%; - box-sizing: border-box; - background: rgba(4,17,31,.8); - color: #d9f5ff; - border: 1px solid rgba(77,116,157,.42); - border-radius: 8px; - padding: 9px 10px; -} -.v8ProtocolNote { - margin-top: 10px; - color: #8299b4; - font-size: .8rem; - line-height: 1.45; + background:#020811; + border:1px solid rgba(72,114,160,.35); + border-radius:10px; + min-height:180px; + max-height:360px; + overflow:auto; + padding:10px 12px; + color:#a7c8e8; + font:12px/1.45 "Cascadia Mono","Consolas",monospace; + white-space:pre-wrap; + word-break:break-word; } -.v8ProtocolNote code { color: #bfeeff; } +#rawSerialConsole .rx { color:#9de9ff; } +#rawSerialConsole .tx { color:#ffd479; } +#rawSerialConsole .err { color:#ff8e9a; } +#rawSerialConsole .info { color:#8cf0b4; } -.v8AboutButton { font-size: 14px !important; } -#v8AboutDialog { - position: fixed; - inset: 0; - z-index: 9998; - display: none; - align-items: center; - justify-content: center; - background: rgba(0,6,14,.78); - backdrop-filter: blur(7px); - padding: 24px; -} -#v8AboutDialog.open { display: flex; } -#v8AboutDialog .aboutCard { - width: min(680px, 94vw); - max-height: 88vh; - overflow: auto; - background: #071523; - border: 1px solid rgba(77,157,216,.45); - border-radius: 16px; - box-shadow: 0 22px 80px rgba(0,0,0,.55); - padding: 24px; -} +.v8CommandTarget { display:grid; grid-template-columns:minmax(100px,180px) 1fr; gap:10px; align-items:center; margin:0 0 14px; } +.v8CommandTarget input { width:100%; box-sizing:border-box; background:rgba(4,17,31,.8); color:#d9f5ff; border:1px solid rgba(77,116,157,.42); border-radius:8px; padding:9px 10px; } +.v8ProtocolNote { margin-top:10px; color:#8299b4; font-size:.8rem; line-height:1.45; } +.v8ProtocolNote code { color:#bfeeff; } + +.v8AboutButton { font-size:14px !important; } +#v8AboutDialog { position:fixed; inset:0; z-index:9998; display:none; align-items:center; justify-content:center; background:rgba(0,6,14,.78); backdrop-filter:blur(7px); padding:24px; } +#v8AboutDialog.open { display:flex; } +#v8AboutDialog .aboutCard { width:min(680px,94vw); max-height:88vh; overflow:auto; background:#071523; border:1px solid rgba(77,157,216,.45); border-radius:16px; box-shadow:0 22px 80px rgba(0,0,0,.55); padding:24px; } #v8AboutDialog .aboutHeader { display:flex; align-items:center; gap:14px; } -#v8AboutDialog .aboutHeader img { width:56px; height:56px; background:#fff; border-radius:12px; padding:5px; } +#v8AboutDialog .aboutHeader img { width:56px; height:56px; background:transparent; border-radius:0; padding:0; } #v8AboutDialog h2 { margin:0; color:#e8f8ff; } #v8AboutDialog p, #v8AboutDialog li { color:#a9bdcf; line-height:1.55; } #v8AboutDialog code { color:#7de7ff; } -#v8AboutDialog .closeAbout { - float:right; - border:1px solid rgba(105,142,177,.4); - background:#0b2137; - color:#dceeff; - border-radius:8px; - padding:7px 10px; - cursor:pointer; -} +#v8AboutDialog .closeAbout { float:right; border:1px solid rgba(105,142,177,.4); background:#0b2137; color:#dceeff; border-radius:8px; padding:7px 10px; cursor:pointer; } +@media (max-width: 1450px) { + #topBar { grid-template-columns:minmax(250px,1.55fr) minmax(170px,.85fr) minmax(210px,1fr) minmax(105px,.52fr) auto; } + .topMetric.connection { display:none; } + #telemetryPanel .altairTelemetryGrid { grid-template-columns:repeat(5,minmax(0,1fr)); } +} +@media (max-width: 1180px) { + #topBar { grid-template-columns:minmax(240px,1.5fr) minmax(205px,1fr) minmax(100px,.55fr) auto; } + .topMetric.mission { display:none; } + .v81SetupSteps { grid-template-columns:1fr; } + .v81SetupArrow { display:none; } + .v81SetupStep { grid-template-columns:34px minmax(130px,1fr) auto auto; } + #telemetryPanel .altairTelemetryGrid { grid-template-columns:repeat(3,minmax(0,1fr)); } + .v81ChartsLayout { grid-template-columns:1fr; } + #v81IdRosterList { grid-template-columns:repeat(2,minmax(0,1fr)); } +} @media (max-width: 900px) { - .v8FrequencyMetric { display: none; } - .v8CommandTarget { grid-template-columns: 1fr; } + .v8FrequencyMetric { display:none; } + .v8CommandTarget { grid-template-columns:1fr; } + #telemetryPanel .altairTelemetryGrid { grid-template-columns:repeat(2,minmax(0,1fr)); } + .v81SetupStep { grid-template-columns:34px 1fr; } + .v81SetupStep .v81StepButton { grid-column:2; } } From 016d690fc894708c08b0a8498afdbc1fac1d2ea3 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:38:32 +0700 Subject: [PATCH 03/10] v8.1: load guided layout runtime --- js/v8-final.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/js/v8-final.js b/js/v8-final.js index 22d3e37..0eaac43 100644 --- a/js/v8-final.js +++ b/js/v8-final.js @@ -73,8 +73,6 @@ const grid = document.querySelector("#telemetryPanel .altairTelemetryGrid"); if (!grid) return; - // Канонический порядок v8. RSSI и SNR всегда находятся в самом конце, - // после состояния раскрытия антенны. const order = [ "ID", "PACKET", @@ -96,7 +94,7 @@ function cleanLegacyUi() { const badge = document.querySelector(".buildBadge"); - if (badge) badge.textContent = "v8 · 0.8.0"; + if (badge) badge.textContent = "v8 · 0.8.1"; const projectEyebrow = document.querySelector("#projectPanel .panelEyebrow"); if (projectEyebrow) projectEyebrow.textContent = "RELEASE V8"; @@ -121,11 +119,9 @@ const eventLogFirst = document.querySelector("#eventLog > div:first-child"); if (eventLogFirst && /v5/i.test(eventLogFirst.textContent || "")) { - eventLogFirst.textContent = "ЦУП Альтаир v8 запущен."; + eventLogFirst.textContent = "ЦУП Альтаир v8.1 запущен."; } - // Старый набор готовых команд из v5 отправлял команды в обход нового RF-протокола. - // В v8 используются основные кнопки PING/INFO/TM_START/TM_STOP и произвольная команда. document.querySelectorAll(".commandPresetGroup").forEach(group => group.remove()); const help = document.getElementById("helpDialog"); @@ -135,7 +131,7 @@ const sections = help.querySelectorAll("section"); if (sections[0]) { - sections[0].innerHTML = '

ЦУП Альтаир v8

Подключите ESP32-радиошлюз на 115200 бод. Рабочий радиоканал: 435.000 МГц, 4.8 kbps, 2-FSK, девиация 5 кГц, RX BW 203 кГц.

'; + sections[0].innerHTML = '

ЦУП Альтаир v8.1

Сначала подготовьте ESP32-радиошлюз, затем подключите его на 115200 бод. Рабочий радиоканал: 435.000 МГц, 4.8 kbps, 2-FSK, девиация 5 кГц, RX BW 203 кГц.

'; } if (sections[1]) { sections[1].innerHTML = '

Телеметрия v8

Принимаются позиционные пакеты с 7 полями и расширенные пакеты с полем ANTENNA. Поле контрольной суммы сохраняется, но прикладная XOR-проверка отключена: изменение ID не приводит к отбрасыванию пакета.

'; @@ -161,7 +157,6 @@ apply(); setTimeout(apply, 250); setTimeout(apply, 800); - const selector = document.getElementById("radioModuleType"); selector?.addEventListener("change", () => setTimeout(apply, 80)); } @@ -172,3 +167,5 @@ setTimeout(initialize, 450); } })(); + +import("/js/v8.1-ui.js").catch(error => console.error("Altair v8.1 UI runtime failed", error)); From 242442daa0ad51c3ffca4d79fd5495fe14e38f7b Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:38:46 +0700 Subject: [PATCH 04/10] v8.1: bump Windows package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25eff41..502b8f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "altair-mcc", - "version": "0.8.0", + "version": "0.8.1", "private": true, "description": "ЦУП Альтаир v8 — учебный центр управления полётами и радиошлюз для CubeSat", "author": "Антон Т.", From 35765741ea51fc7b44386cc45cc0b5abd3f4eca4 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:39:05 +0700 Subject: [PATCH 05/10] v8.1: extend CI checks for guided UI and transparent branding --- .github/workflows/altair-v5-ui-check.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/altair-v5-ui-check.yml b/.github/workflows/altair-v5-ui-check.yml index 3a3a435..8f2261e 100644 --- a/.github/workflows/altair-v5-ui-check.yml +++ b/.github/workflows/altair-v5-ui-check.yml @@ -27,6 +27,7 @@ jobs: node --check js/charts.js node --check js/v8.js node --check js/v8-final.js + node --check js/v8.1-ui.js node --check js/help.js node --check js/flasher-v5.js node --check js/serial.js @@ -36,17 +37,15 @@ jobs: node - <<'NODE' const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - if (pkg.version !== '0.8.0') throw new Error(`Expected 0.8.0, got ${pkg.version}`); + if (pkg.version !== '0.8.1') throw new Error(`Expected 0.8.1, got ${pkg.version}`); if (pkg.build?.win?.icon !== 'assets/altair-logo.png') throw new Error('Altair Windows PNG icon source is not configured'); if (pkg.author !== 'Антон Т.') throw new Error('Application author metadata is missing'); const parser = fs.readFileSync('js/parser.js', 'utf8'); - if (!parser.includes('checksumValidation: false')) { - throw new Error('Application checksum validation must be disabled in v8 parser'); - } + if (!parser.includes('checksumValidation: false')) throw new Error('Application checksum validation must be disabled in v8 parser'); const app = fs.readFileSync('js/app.js', 'utf8'); - if (!app.includes('version: "0.8.0"')) throw new Error('app.js must identify v0.8.0'); + if (!app.includes('Release v8')) throw new Error('app.js must identify v8'); if (app.includes('applyV6BrandingAndRadioProfile')) throw new Error('Legacy v6 runtime override is still present'); const charts = fs.readFileSync('js/charts.js', 'utf8'); @@ -55,8 +54,14 @@ jobs: const finalRuntime = fs.readFileSync('js/v8-final.js', 'utf8'); if (!finalRuntime.includes('bandwidth: "203"')) throw new Error('v8 UI must enforce RX BW 203 kHz'); + if (!finalRuntime.includes('v8.1-ui.js')) throw new Error('v8.1 layout runtime is not loaded'); + + const ui81 = fs.readFileSync('js/v8.1-ui.js', 'utf8'); + for (const token of ['v81SetupFlow', 'v81IdRoster', 'firmwarePanel', 'rawSerialPanel', 'EXPECTED_IDS']) { + if (!ui81.includes(token)) throw new Error(`v8.1 UI feature missing: ${token}`); + } - console.log('Altair v8 JS/package checks OK'); + console.log('Altair v8.1 JS/package checks OK'); NODE - name: Validate v8 radio profile and release files @@ -73,6 +78,7 @@ jobs: grep -Fq '435.000' js/v8.js grep -Fq 'rawSerialConsole' js/v8.js grep -Fq 'Сбросить принятые данные' js/v8.js + grep -Fq 'background: transparent' css/v8.css test -f docs/CUP_Altair_v8_Manual.tex test -f docs/RELEASE_V8.md test -f docs/media/mcc-technoprom.jpg From 64b85ade2a63da4f47cf28f9060e736b489eb877 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:40:24 +0700 Subject: [PATCH 06/10] v8.1: replace Altair icon with transparent asset --- assets/altair-logo.png | Bin 7013 -> 3676 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/altair-logo.png b/assets/altair-logo.png index e251eddfe7e8b3ac65efce35179b4a4a3c011dbe..2ef3e0d0f9c4838f775aedc3d205c7ca129d8437 100644 GIT binary patch literal 3676 zcmV-i4x{mjP)C0001BP)t-sEMLtm zV9qXN*B5H)9$d2^JGlV={{Vb_0uFv*WBXNDt1UU{DOkJ!{{U60|9^jfe*pggs;UvB z|Nj90A0Iz|0Dn~gs{jB00000kW7I8W&MachGHd25UCRIf|1DY0@`u$s0000WbW%=J zUZ3Cw4FmxK0s{jCR{;Y80s{j92m%8E0Qe9t{4N0z)R4a_000e(Nklg(7 z7l04EH|P8(0OUN9bHO>1%h8{(z%UHQ117e>-D1}3D%+JH)!Q6OX3>;laF?L;-Qt|Qw=ce;$oN}IqPY19+ z*(rq7bqBe+$Og}@5QW|Jj_t$W=g$UUzNb)P;@kKA+B1S604SUT5P~SdeKG(xK=$nU z5ECB1U$>1BbVYu5I>O?4W+3i2q+#Z#VF0d(-{m?S)(sZuI0)wUI-o1NE5^?PK>VSc zeZd^B=EwcJ$nLNA@nRc9h570ZYupJS_+`jL?BvdTYySU+`v;CT3g7ki1lQ~ch)&}t z0^tA04vxpQZRUUW&rdIb8CzzgAVKIq8Uy+0`68hjDq>4Yh`bH|t$ZW5XrSkkEHc30qIq5>KYa`UvI+qp$*=sE+!4H_MggvN zfBq8xsvGh$Uej$?0FdX38=QA#2Vnm1%ED!XQ!u>Vqz2u zKvQs)k40|f*(-t&7p2}Ur8}$$Gx2O=SRp?|LHFXq50ikO ze0~VPc=9ZLeiQ_qRD5}gB>{j2x7#V+fda)=xHu=&Zu}L+q;!~v$Fg8lrx%Snehd^` z0l<;P1e8v~eT6HH6W!s2u(&~}m;6BF+Vt*nJV}Oe?Y#hCG%VlB<8u)z$rq^>< ztR&wQif;o5^2iNZ&dD|5C8e!Uqh1=j09E+_ur2;d+j z4#IFd61rEUuk%@+5Cm&v;v($*4ek~aK!jLUz(g*v9EwivNK&1xCev%G`Wk@w$e)#) zg*S0~(1P2k|IM{X3eAclDPVF@GRVpXguxjJ-U$Gt;evS>0QfTlKAvsL(ZuqAkfWNF zk!F(AH$q}K6xBQkkAM^%^1UvYPx4Z$GFo{Q04~N5C8p)qsJduO6kU600dweUNd|6a zq>x`vlxG0)2{v<4gI)n7F^SM!r>#)2_{8nPLbV{MBw-^kAkzmXj)ap5cb|XUus{F+ zvO6e%$nPU&+?ohte^Ui4KasDvwUt0t>WY7>8VORluSQjiBYgnqzeXN){49oWO$TRxx5x z$}Mk)sD}$ic3srCGyqhrO|WuEVp&p36ms|>kMUoxl#UkxK@k8m-BQJqs~W5~wy*lc zVqj(}(A-l168Rj4L7OC~Bwr;XNP9Kj1Rvi7Fpro}iz~Zh@cx4wlbg|HGLWx606kT7?*a-ed?xlfm??j$3X3 z$c`ZnxtPs+VYZxul^pQEKBJpYb7%mZt8v3D9b6t|vZUHZt8?=A06-vqcUp4rVN%gc zO}ZE~zffTCl&R`xQK~gkWO`+`E;XL{lR#eg^oB>tp~Jx%yu$aFa{LXAQqp;dhz<2~ zh2}IjH%S($lt#O37_FaLy?nU`z~oAZ`;+m_nPR!hCBDBU8>1R6;q@n+Eu|#WYITAH z>T^jYHhy%xodu-L990FNw@hIiq#25OyJ7+B{^nbh&YH-@rB*b`A`@*@g-oQqVLJNj z07wM325~`FbyW+2@W!Aa*a&q9&ywL4gY{a!Ycnn`KS&se=NfL0J}$Z!>i7kK$pkQ&IPTN{ zy^0DcJEVFfDOwn7h=+b{Wx`1xDK3C54p9L(Y0;+ZsyJsIhDlcuMaN3QF~uewP}Zk0 zK@_AzlIaU)Os4>3g~2{od!$i@8iavV$(HBBaIg7zo|&R2Gv zTcWxm+}#_BSgP3%0g;z+VuccRhGn~iSShgt<4B#x1k#Z$Bjd}Di$z965zGUX6_9K;bvetq2`34av19 zuq}ZQv#l-SUJETPlR3u#h>BXxo8!e&=Pdx<1StTZqj7Z+LY>xQnNeA$b`4eq0NEr( zVOVx0Z7OQuy13i}AS9iM_ok&;Ta1oa@M_TmonkV!o5cW3paZkH)bzMZ3LPxn1>jfe zT2}=nSw-fQ)We`Cv;$q;-{>$e9~%I4<@;#Qc+K4cfSGQtvwXt13zo z=?wtG@vaJ4qbR8|uS_W}0Gy_l*82$9;^5zZLuj8(eP*Rholcg3ceV^}!_At$LDxIO za1DSi#!qbq-6qc5wEz5jlo=$)GRAHK1)(U5v5Xy2aivj6+5v28#{@~9X}kttmg&nN z^QJnP-mC=J>?xkjf}=Kq=!!~!EVo%6Zg&Pi^GPe=i=xmy+f2-1T%M>lf~~ukPPnwu zWR>C346DJ>#U`OlE$#q%13={N0ANE(dFxvDKaozQYda$w&#>obyS)yFx;+gq0Iant zQ@IC#b|K2WeO=K;{dnv}`pU3)`qrZgE9=9C*)<~CsuAx1U~|I38ry>vfV%(bG^=oJ z@QbWtYL6rR$@BN+bl}RB`e3_ECy;BM?Z8U4Qo}l6^o0FYb^}U z)1cEzT-&~m#c8mE9fILU17KVJyiED|eC7=REmh8r3n0kiIS(X6C02D7kxwE-!nbdS zy64%~Jfdxu}0+3x4UAl|r=kM2~10x0f@X=Y|>&jHm z8`O=$Ljb1ne2$Up;iUX5Tg)}AoR2!>cC|TlgRDvK^Eao&S$J zkt&4VoDLHiGuy159tDtxV_-Vqe`NK=5l{m#Vlax>+;=G4)HSARV{C1qMkTo6VF2WC zY^ZzdI<$ec)kGafHK!{vI=8%`hOPm~ihxShzKxRlHXOs382b#iM(UgV2sSz~4fp!f z-vl7bq14rB{N;B3?4Sns>`NZ^_Nfy8f<>AQlHLP=W1npKq4MnP+9@!E0RgFN8WVtY zRG}o|__WM3f70UsGAlFySli)<21HA;wr^=#AmHL-p1FwN56X5R%xt<8b2@0FxW*(s-*&sdiRJgI$*vBPxARnxpbAwe|IT&k;>a)qGu7N6xFQpftgzDtv07 z_OAd~@&^twi6fY6Z5zAp9ivjPtLJNw|NW{omlTM)UkqkKYiS*uG4SmLeP z&et821|b+~7(NMru90d>!V+6&ilnLxInhER$$<^DAbF%Qf6|#!Uz#AZHQBP%i4u|$ zx7QJ)Y{&9_XIdX9$0P+&{7%^5t8#UFX!P2a7ttmU9BsZ0fW8-*c|A)IPl5c=&ibrv zL+$Q}`-cI@G3Donu~;+m&u85mxAjds&ibhUmOM=_8I=pQd$K8siS(qb{NVM#!RCQI zG0LGN1olAnG$l?a6i0FfZoD1#?*%ZFW350C%h&w~J7zd@E%<-9yyIWP2E uEpXmE{V4!{HU_=`d;$0Z@CD$18o)nCCF16ZyJHCe0000zx; literal 7013 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 From 343caa32a5d9353905487e82ba74c481c313d683 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:41:42 +0700 Subject: [PATCH 07/10] v8.1: rewrite README for distribution and guided first launch --- README.md | 152 +++++++++++++++++++++++++----------------------------- 1 file changed, 69 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 7e8a804..f3b45ee 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,67 @@ # ЦУП Альтаир

- Логотип Альтаира + Логотип ЦУП Альтаир

-**ЦУП Альтаир** — учебный центр управления полётами для CubeSat-проектов: приём телеметрии, отображение параметров, графики для нескольких аппаратов, сырой COM-терминал, радиопередача команд и работа с поворотным устройством. +**ЦУП Альтаир** — учебный центр управления полётами для CubeSat-проектов. Программа принимает телеметрию через наземный радиошлюз ESP32 + CC1101, строит графики для нескольких аппаратов одновременно, показывает состояние связи по ID, позволяет передавать RF-команды и управлять поворотным устройством антенны. -**Текущая версия: v8 / 0.8.0.** -Проект создан в рамках образовательного проекта **«Миссия на Луну»** и профильной смены по спутникостроению; стенд использовался на Технопроме-2026. +**Актуальная версия: v8 / 0.8.1.** +Автор: **Антон Т.** Проект создан в рамках образовательной программы **«Миссия на Луну»**, профильной смены по спутникостроению и использовался на Технопроме-2026. ## Скачать -> Для обычной работы достаточно установщика и ESP32 с CC1101. - -- **[Скачать ЦУП Альтаир v8 для Windows](https://github.com/Anton778/OpenMCC/releases/download/v0.8.0/CUP-Altair-Setup-0.8.0.exe)** -- **[Скачать скетч ESP32 радиошлюза](firmware/radio_gateway_esp32/Altair_Gateway_v8/Altair_Gateway_v8.ino)** -- [Скачать руководство пользователя PDF](https://github.com/Anton778/OpenMCC/releases/download/v0.8.0/CUP_Altair_v8_Manual.pdf) +- **[ЦУП Альтаир v8.1 для Windows](https://github.com/Anton778/OpenMCC/releases/download/v0.8.1/CUP-Altair-Setup-0.8.1.exe)** +- [Скетч ESP32 радиошлюза](firmware/radio_gateway_esp32/Altair_Gateway_v8/Altair_Gateway_v8.ino) +- [Руководство пользователя PDF](https://github.com/Anton778/OpenMCC/releases/download/v0.8.1/CUP_Altair_v8_Manual.pdf) - [Исходник руководства LaTeX](docs/CUP_Altair_v8_Manual.tex) +Для обычного пользователя достаточно установщика. Готовая прошивка ESP32 уже находится внутри программы и записывается из встроенного прошивальщика. + ## Как выглядит система
ЦУП Альтаир на Технопроме -
Наземный ЦУП. Большой экран с программой, направленная антенна, радиошлюз ESP32 + CC1101 и поворотное устройство. +
Наземный ЦУП. Большой экран с программой, направленная антенна, ESP32 + CC1101 и поворотное устройство.
Учебный CubeSat -
Учебный аппарат. CubeSat с бортовой электроникой и раскрывающейся ленточной антенной. +
Учебный CubeSat. Бортовая электроника и раскрывающаяся ленточная антенна.
## Быстрый запуск +Интерфейс v8.1 специально построен так, чтобы не перепутать последовательность действий. + 1. Подключите CC1101 к ESP32-WROOM-32 по таблице ниже. -2. Загрузите `Altair_Gateway_v8.ino` через Arduino IDE **или** прошейте ESP32 встроенной прошивкой из программы. -3. Подключите ESP32 к компьютеру по USB. -4. Запустите `ЦУП Альтаир`. -5. В блоке **«Соединение с ЦУПом»** выберите ESP32 и `115200 бод`. -6. Нажмите **«Подключить устройство»** и выберите нужный COM-порт. -7. После подключения блок соединения автоматически свернётся; его можно развернуть в любой момент. -8. Включите спутники-передатчики. Телеметрия появляется сразу; разные `ID` строятся разными линиями на одних графиках. -9. Для диагностики откройте панель **COM** — там видны сырые строки в реальном времени. +2. Запустите **ЦУП Альтаир**. +3. В верхнем блоке **«Подготовка шлюза ЦУП»** выполните **Шаг 1 — Прошивка ESP32**. Если эта ESP32 уже прошивалась актуальной прошивкой, нажмите **«Шлюз уже прошит»**. +4. После завершения первого шага становится доступен **Шаг 2 — Соединение с ЦУПом**. +5. Выберите COM-порт ESP32. Рабочая скорость USB — **115200 бод**. +6. Включите спутники-передатчики. Телеметрия сразу появляется в полноширинном блоке, а ниже строятся графики. -## Радиопрофиль +Подробные панели прошивальщика, COM-настроек и RAW COM находятся ниже основного рабочего экрана и могут быть свернуты. -Наземный шлюз v8 использует профиль, который устойчиво работал на реальном стенде: +## Рабочий радиопрофиль | Параметр | Значение | |---|---:| | Частота | **435.000 МГц** | -| Модуляция | 2-FSK | -| Скорость в эфире | 4.8 kbps | -| Девиация | 5 кГц | +| Модуляция | **2-FSK** | +| Скорость в эфире | **4.8 kbps** | +| Девиация | **5 кГц** | | Полоса RX | **203 кГц** | | Sync word | `0x12AD` | | Encoding | NRZ | | Длина пакета | variable | -| CRC CC1101 | включён | -| USB | 115200 бод | +| Аппаратный CRC CC1101 | включён | +| USB ESP32 → ПК | **115200 бод** | -Широкая полоса **203 кГц** выбрана намеренно: на стенде некоторые модули имели заметный частотный уход относительно 435.000 МГц. В v8 этот профиль наземного CC1101 **зафиксирован**, чтобы у разных пользователей программа запускалась в одинаковой проверенной конфигурации. В интерфейсе параметры видны, но случайно изменить их нельзя. +Полоса **203 кГц** выбрана намеренно. На учебных радиомодулях встречается заметный частотный уход, поэтому узкая полоса 58 кГц на реальном стенде приводила к потере пакетов. ## Подключение CC1101 к ESP32 @@ -79,7 +78,7 @@ **CC1101 питается только от 3.3 В.** -## Телеметрия +## Формат телеметрии Базовый пакет: @@ -87,112 +86,97 @@ 02,00001,00015,3.00,4.20,1,33 ``` -Поля: - ```text ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,CHECKSUM ``` -Поддерживается и расширенный вариант с состоянием раскрытия антенны: +Расширенный вариант с состоянием раскрытия антенны: ```text 02,00001,00015,3.00,4.20,1,1,33 ``` -Поля: - ```text ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,ANTENNA,CHECKSUM ``` -где `ANTENNA=1` — раскрыта, `ANTENNA=0` — сложена. Если поля `ANTENNA` нет, интерфейс показывает **Н/Д**. - -### Важно про контрольную сумму +`ANTENNA=1` — антенна раскрыта, `ANTENNA=0` — сложена. Если поля нет, интерфейс показывает **Н/Д**. -В v8 **прикладная XOR-проверка намеренно отключена**. Поле `CHECKSUM` сохраняется и отображается, но не является причиной отбрасывания пакета. +### Контрольная сумма -Это сделано для учебной смены: команда может изменить `ID`, напряжение или другое поле и не пересчитывать последние два HEX-символа — пакет всё равно будет показан в ЦУПе. +В v8 поле `CHECKSUM` сохраняется и отображается, но **прикладная XOR-проверка отключена**. Команда может изменить `ID`, напряжение или другое поле и не пересчитывать последние два HEX-символа — пакет всё равно будет принят ЦУПом. -Аппаратный **CRC самого CC1101 остаётся включённым** и защищает радиопакет от грубых ошибок передачи. +Аппаратный **CRC самого CC1101 остаётся включённым**. ## Несколько спутников одновременно -ЦУП группирует данные по `ID`. +Графики группируются по `ID`. Каждый впервые принятый ID получает собственный цвет, линию и точки измерений. -Если одновременно приходят: +Справа от графиков находится список **«Связь по ID»**: -```text -01,... -02,... -07,... -``` +- `НЕТ СВЯЗИ` — пакет от этого ID ещё ни разу не принимался; +- `ПЕРЕДАЁТ` — пакет был получен в последние 5 секунд; +- `БЫЛ НА СВЯЗИ` — аппарат уже принимался, но сейчас пакетов нет. -на каждом графике будут отдельные линии `ID 01`, `ID 02`, `ID 07`. Цвет закрепляется за ID на текущую сессию. Точки показывают фактические моменты поступления пакетов. Кнопка **«Сбросить принятые данные»** очищает карточки, графики, счётчики и сырой COM-журнал. +Для профильной смены заранее отображаются ожидаемые ID `01…10`. Если появляется другой ID, он автоматически добавляется в список. Цвет ID совпадает с цветом его линий на графиках. -## Передача команд с ЦУПа +Кнопка **«Сбросить принятые данные»** очищает карточки, графики, счётчики, RAW COM и историю принятых ID без перезапуска программы. -ESP32-шлюз v8 работает как приёмопередатчик. В программе можно выбрать адресат (`ALL` или конкретный ID) и передать команду. +## Передача команд с ЦУПа -ЦУП отправляет шлюзу: +Наземный CC1101 работает как приёмопередатчик. Например, программа отправляет шлюзу: ```text $CMD,RF,TO=02,NAME=PING ``` -а ESP32 передаёт в эфир: +ESP32 передаёт в эфир: ```text $CMD,TO=02,NAME=PING ``` -Штатные команды v8: +Штатные команды интерфейса: | Команда | Назначение | |---|---| -| `PING` | проверка радиопередачи | +| `PING` | проверка командного радиоканала | | `INFO` | запрос информации | -| `TM_START` | команда запуска телеметрии | -| `TM_STOP` | команда остановки телеметрии | -| `TM_PERIOD,MS=` | изменение периода телеметрии | -| `USER,DATA=<текст>` | произвольная пользовательская команда | - -> Бортовая прошивка спутника должна отдельно реализовать реакцию на эти команды. Даже без неё передачу ЦУПа можно увидеть на SDR и проверить TX-тракт CC1101. +| `TM_START` | запуск телеметрии | +| `TM_STOP` | остановка телеметрии | +| `TM_PERIOD,MS=` | изменение периода передачи | +| `USER,DATA=<текст>` | пользовательская команда | -Служебная команда `$CMD,RADIO_STATUS` возвращает текущий фиксированный профиль наземного шлюза. Старые команды изменения радиопрофиля в v8 намеренно не применяются. +Бортовая прошивка спутника должна отдельно реализовать реакцию на эти команды. Сам факт передачи с ЦУПа можно проверить SDR-приёмником. -## Почему данные могут обновляться реже, чем ожидается +## RAW COM -Программа не вводит специальную задержку отображения: разобранная строка сразу отправляется в карточки и графики. +Панель **RAW COM / DEBUG** показывает строки именно в том виде, в котором они приходят от ESP32, а также команды, отправленные программой. Она специально перемещена ниже телеметрии и графиков, поскольку нужна в основном для диагностики. -Причины редкого обновления обычно находятся ниже по тракту: реальный период передачи конкретного спутника, коллизии нескольких аппаратов на одной частоте, частотный уход передатчика, занятый COM-порт или потеря радиопакета по аппаратному CRC. +Если RAW COM показывает строки быстро, а карточки не обновляются — проверяйте формат пакета. Если строк нет вообще — проверяйте радиоканал, ESP32 и CC1101. -Для проверки используется панель **«Сырые данные последовательного порта»**. Если строки быстро идут в COM-панели, но не отображаются как телеметрия — проблема в формате пакета. Если строк в COM-панели нет — искать нужно в радиоканале/ESP32. +## Что нового в 0.8.1 -## Что нового в v8 - -- официальная ESP32-прошивка без прикладной проверки XOR; -- RX bandwidth 203 кГц; -- видимая на главном экране частота `435.000 МГц`; -- сворачиваемая панель соединения; -- передача RF-команд с ЦУПа; -- кнопка полного сброса принятых данных; -- отдельные линии и цвета графиков для каждого `ID`; -- сырой COM-терминал; -- окно **«О программе»**; -- новый значок Альтаира для интерфейса и Windows-приложения; -- фотографии реального ЦУПа и CubeSat в документации; -- один актуальный GitHub Release — **v0.8.0**. +- прозрачный значок Альтаира без белого квадрата; +- `v8 · 0.8.1`, кнопки «О программе» и справки находятся в одной строке шапки; +- новый сценарий **1. Прошивка ESP32 → 2. Соединение**; +- соединение нельзя случайно начать, не подтвердив готовность прошивки шлюза; +- панель соединения сворачивается до одной компактной строки; +- телеметрия находится во всю ширину окна сразу под быстрым запуском; +- графики идут непосредственно за телеметрией; +- рядом с графиками отображается состояние связи для ID `01…10` и любых дополнительных ID; +- RAW COM и подробные служебные настройки перенесены ниже основного рабочего экрана; +- исправлены фотографии ЦУПа и CubeSat на главной странице репозитория. ## О программе **Автор: Антон Т.** -Сделано в рамках проекта **«Миссия на Луну»** к профильной смене по спутникостроению. -Цель проекта — дать участникам понятный и воспроизводимый наземный комплекс, который можно собрать из доступных модулей, увидеть реальные радиопакеты и использовать как основу для собственных спутниковых экспериментов. +Сделано в рамках проекта **«Миссия на Луну»** к профильной смене по спутникостроению. Цель проекта — дать участникам понятный и воспроизводимый наземный комплекс, который можно собрать из доступных модулей и использовать как основу для собственных спутниковых экспериментов. ## Разработка -Репозиторий содержит приложение, прошивки и документацию. Для локального запуска: +Для локального запуска: ```bash npm install @@ -205,10 +189,12 @@ npm start npm run dist:win ``` -Pull request'ы и улучшения приветствуются. При изменении радиопротокола желательно приложить пример принятой сырой строки из COM-панели. - +Репозиторий содержит приложение, прошивки ESP32/STM32, документацию и сборочные workflow. При изменении радиопротокола желательно прикладывать пример принятой строки из RAW COM. ## Обратная связь Если у вас есть вопросы, предложения или идеи по улучшению проекта, пишите на электронную почту: [ya.antoha.rt@gmail.com](mailto:ya.antoha.rt@gmail.com) +--- + +**ЦУП Альтаир v8 · 0.8.1 · Миссия на Луну · 2026** From 13d41b71c19fbcd5f7a297a77fbd789ff6ba4a56 Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:42:08 +0700 Subject: [PATCH 08/10] v8.1: update release notes --- docs/RELEASE_V8.md | 78 ++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/docs/RELEASE_V8.md b/docs/RELEASE_V8.md index 25f011a..c60b949 100644 --- a/docs/RELEASE_V8.md +++ b/docs/RELEASE_V8.md @@ -1,30 +1,33 @@ -# ЦУП Альтаир v8 / 0.8.0 +# ЦУП Альтаир v8 / 0.8.1 -Это первый релиз, подготовленный не только для стенда автора, а для передачи другим командам. +Версия 0.8.1 — доработанная распространяемая сборка ЦУП Альтаир после Технопрома-2026. Основной акцент сделан на первом запуске, удобстве работы оператора и наглядной работе сразу с несколькими спутниками. ## Главное -- официальная прошивка ESP32 соответствует реально рабочему стендовому варианту; -- прикладная XOR-проверка отключена: изменение ID/полей больше не отправляет пакет в ошибки; -- рабочая частота **435.000 МГц** постоянно видна на главном экране; -- RX bandwidth наземного CC1101: **203 кГц**; -- проверенный радиопрофиль наземного шлюза зафиксирован, чтобы случайная настройка не сломала приём; -- панель подключения можно сворачивать, после соединения она сворачивается автоматически; -- добавлен сырой COM-терминал для контроля реальной скорости поступления строк; -- добавлена кнопка полного сброса принятых данных; -- графики разделяются по ID спутников: каждый ID имеет собственную линию, цвет и точки отсчётов; -- добавлена реальная передача RF-команд из ЦУПа через ESP32 + CC1101; -- добавлены служебные `GATEWAY_INFO` и `RADIO_STATUS`; -- добавлено окно «О программе»; -- новый значок Альтаира используется в интерфейсе и Windows-сборке; -- README содержит фотографии реального ЦУПа и CubeSat; -- после успешной публикации v8 старые GitHub Releases и их теги удаляются, чтобы осталась одна актуальная версия. +- рабочий радиопрофиль наземного шлюза сохранён: **435.000 МГц**, 4.8 kbps, 2-FSK, девиация 5 кГц, **RX BW 203 кГц**; +- прикладная XOR-проверка телеметрии отключена, поэтому изменение ID или другого поля без пересчёта XOR не отбрасывает пакет; +- аппаратный CRC CC1101 остаётся включённым; +- прозрачный значок Альтаира используется в интерфейсе и Windows-сборке без белого квадрата; +- шапка стала компактнее: версия, «О программе» и справка находятся в одной строке; +- добавлен обязательный понятный сценарий **1. Прошивка ESP32 → 2. Соединение**; +- если шлюз уже прошит, это можно подтвердить отдельной кнопкой; после успешной прошивки программа запоминает состояние; +- подробная панель соединения сворачивается до компактной строки; +- телеметрия расположена во всю ширину рабочего окна; +- графики идут сразу после телеметрии; +- рядом с графиками добавлена панель состояния ID: `НЕТ СВЯЗИ`, `ПЕРЕДАЁТ`, `БЫЛ НА СВЯЗИ`; +- ожидаемые ID `01…10` видны заранее, дополнительные ID добавляются автоматически; +- цвет ID совпадает с цветом его линий на графиках; +- RAW COM и подробные служебные панели перенесены ниже основного рабочего экрана; +- сохранены RF-команды с ЦУПа, полный сброс данных, несколько цветных линий с точками и окно «О программе»; +- фотографии ЦУПа и CubeSat на главной странице репозитория исправлены. ## ESP32 gateway +Файл ручной прошивки: + `Altair_Gateway_v8.ino` -Профиль: +Радиопрофиль: ```text 435.000 MHz @@ -53,17 +56,36 @@ ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,CHECKSUM ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,ANTENNA,CHECKSUM ``` -Поле `CHECKSUM` в v8 сохраняется для совместимости и отображения, но не проверяется. Аппаратный CRC CC1101 остаётся включённым. +Поле `CHECKSUM` сохраняется для совместимости и отображения, но не валидируется на прикладном уровне. Пакет может быть принят даже после изменения ID без пересчёта последних двух HEX-символов. + +## Первый запуск + +1. Подключить CC1101 к ESP32-WROOM-32. +2. Установить и запустить ЦУП Альтаир. +3. Выполнить шаг **«Прошивка ESP32»** встроенным прошивальщиком или подтвердить, что шлюз уже прошит. +4. После этого выполнить шаг **«Соединение с ЦУПом»** и выбрать COM-порт. +5. Включить спутники-передатчики. +6. Контролировать телеметрию и список ID в верхней части рабочего экрана. + +## Несколько спутников + +Для ID `01…10` программа заранее показывает состояние связи. После первого пакета ID получает тот же цвет, который используется его линиями на графиках. + +- `НЕТ СВЯЗИ` — пакет не принимался в текущей сессии; +- `ПЕРЕДАЁТ` — пакет был принят в последние 5 секунд; +- `БЫЛ НА СВЯЗИ` — хотя бы один пакет ранее принимался, но сейчас поток прекратился. + +Другие идентификаторы добавляются автоматически. ## Командный канал -USB: +USB-команда ЦУПа шлюзу: ```text $CMD,RF,TO=02,NAME=PING ``` -В эфир: +Пакет в эфире: ```text $CMD,TO=02,NAME=PING @@ -71,21 +93,15 @@ $CMD,TO=02,NAME=PING Поддерживаемые команды интерфейса: `PING`, `INFO`, `TM_START`, `TM_STOP`, `TM_PERIOD`, `USER`. -Служебные команды шлюза: - -```text -$CMD,GATEWAY_PING -$CMD,GATEWAY_INFO -$CMD,RADIO_STATUS -``` - -`RADIO_STATUS` возвращает фиксированный рабочий профиль v8. Старые команды изменения радиопараметров не применяются и получают ответ `RADIO_CONFIG_FIXED`. +Служебная команда `$CMD,RADIO_STATUS` возвращает фиксированный рабочий профиль наземного шлюза. ## Файлы релиза -- `CUP-Altair-Setup-0.8.0.exe` +- `CUP-Altair-Setup-0.8.1.exe` - `Altair_Gateway_v8.ino` - `CUP_Altair_v8_Manual.pdf` - `CUP_Altair_v8_Manual.tex` - `gateway-firmware.bin` - `manifest.json` + +После успешной публикации `v0.8.1` workflow удаляет предыдущий GitHub Release, чтобы в разделе Releases оставалась одна актуальная распространяемая версия v8. From 202625c410acb08fde4102af93bf078519a10c1e Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:43:24 +0700 Subject: [PATCH 09/10] v8.1: update user manual for guided launch and ID monitor --- docs/CUP_Altair_v8_Manual.tex | 134 ++++++++++++++++------------------ 1 file changed, 63 insertions(+), 71 deletions(-) diff --git a/docs/CUP_Altair_v8_Manual.tex b/docs/CUP_Altair_v8_Manual.tex index 7681b02..b2c5033 100644 --- a/docs/CUP_Altair_v8_Manual.tex +++ b/docs/CUP_Altair_v8_Manual.tex @@ -13,14 +13,12 @@ \usepackage{fancyhdr} \usepackage{titlesec} \usepackage{listings} -\usepackage{graphicx} -\usepackage{float} \usepackage{parskip} \definecolor{AltairBlue}{HTML}{0E5A88} -\hypersetup{colorlinks=true,linkcolor=AltairBlue,urlcolor=AltairBlue,pdftitle={ЦУП Альтаир v8 — руководство пользователя}} +\hypersetup{colorlinks=true,linkcolor=AltairBlue,urlcolor=AltairBlue,pdftitle={ЦУП Альтаир v8.1 — руководство пользователя}} \pagestyle{fancy} \fancyhf{} -\lhead{ЦУП Альтаир v8} +\lhead{ЦУП Альтаир v8.1} \rhead{Миссия на Луну} \cfoot{\thepage} \titleformat{\section}{\Large\bfseries\sffamily\color{AltairBlue}}{\thesection.}{0.5em}{} @@ -35,7 +33,7 @@ {\large Центр управления полётами}\\[3mm] {\Large\bfseries Миссия на Луну}\\[5mm] {\large Руководство по установке, радиошлюзу и работе с телеметрией}\\[5mm] -{\normalsize Версия 0.8.0 · автор: Антон Т.}\\[6mm] +{\normalsize Версия 0.8.1 · автор: Антон Т.}\\[6mm] \hrule \end{center} @@ -45,22 +43,22 @@ \section{Состав системы} \begin{center} \fbox{\parbox{0.93\linewidth}{\centering -\textbf{Спутник + CC1101} $\longrightarrow$ \textbf{435.000 МГц} $\longrightarrow$ \textbf{CC1101 + ESP32} $\longrightarrow$ \textbf{USB 115200} $\longrightarrow$ \textbf{ЦУП Альтаир v8} +\textbf{Спутник + CC1101} $\longrightarrow$ \textbf{435.000 МГц} $\longrightarrow$ \textbf{CC1101 + ESP32} $\longrightarrow$ \textbf{USB 115200} $\longrightarrow$ \textbf{ЦУП Альтаир v8.1} }} \end{center} -На наземной стороне используется широкая полоса приёма \textbf{203 кГц}. Это проверенная стендовая настройка, выбранная из-за заметного частотного ухода части учебных радиомодулей. +На наземной стороне используется широкая полоса приёма \textbf{203 кГц}. На реальном учебном стенде она оказалась устойчивее узкой полосы 58 кГц при заметном частотном уходе отдельных радиомодулей. \section{Что скачать} -Для обычного пользователя нужны: -\begin{enumerate}[leftmargin=7mm] -\item \code{CUP-Altair-Setup-0.8.0.exe} — Windows-приложение; -\item \code{Altair_Gateway_v8.ino} — прошивка ESP32-WROOM-32 + CC1101, если используется Arduino IDE; -\item настоящий файл \code{CUP_Altair_v8_Manual.pdf} — инструкция. -\end{enumerate} +Для обычного пользователя достаточно установщика: +\begin{itemize}[leftmargin=7mm] +\item \code{CUP-Altair-Setup-0.8.1.exe} — Windows-приложение; +\item \code{Altair_Gateway_v8.ino} — ручная прошивка ESP32, если требуется Arduino IDE; +\item \code{CUP_Altair_v8_Manual.pdf} — настоящее руководство. +\end{itemize} -Прошивка ESP32 также встроена в установщик и может быть записана непосредственно из программы. +Готовый бинарный комплект ESP32 уже встроен в программу, поэтому Arduino IDE для первого запуска не требуется. \section{Подключение CC1101 к ESP32} @@ -99,13 +97,27 @@ \section{Рабочий радиопрофиль} Кодирование & NRZ\\ Длина пакета & variable\\ Аппаратный CRC CC1101 & включён\\ -Мощность тестового TX & 5 dBm\\ USB & 115200 бод\\ \bottomrule \end{tabular} \end{center} -В v8 радиопрофиль наземного CC1101 зафиксирован. Это защищает воспроизводимую рабочую конфигурацию от случайного изменения пользователем. +В v8 профиль наземного CC1101 зафиксирован, чтобы случайное изменение настроек не ломало проверенную конфигурацию. + +\section{Первый запуск: правильная последовательность} + +В версии 0.8.1 сверху рабочего экрана находится блок \textbf{«Подготовка шлюза ЦУП»}. Он задаёт обязательную последовательность действий. + +\begin{enumerate}[leftmargin=7mm] +\item Подключить CC1101 к ESP32-WROOM-32. +\item Запустить ЦУП Альтаир. +\item Выполнить \textbf{Шаг 1 — Прошивка ESP32}. Нажать «Открыть прошивальщик» и записать встроенную прошивку. Если данная ESP32 уже прошита актуальной прошивкой, нажать «Шлюз уже прошит». +\item После подтверждения шага 1 становится доступен \textbf{Шаг 2 — Соединение с ЦУПом}. +\item Выбрать COM-порт и подключить ESP32 на скорости 115200 бод. +\item Включить спутники-передатчики. +\end{enumerate} + +После успешного подключения подробная панель COM сворачивается до компактной строки. При необходимости она раскрывается кнопкой «Настройки COM». \section{Телеметрические пакеты} @@ -119,7 +131,7 @@ \section{Телеметрические пакеты} ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,CHECKSUM \end{lstlisting} -Расширенный вариант с состоянием антенны: +Расширенный вариант: \begin{lstlisting} 02,00001,00015,3.00,4.20,1,1,33 \end{lstlisting} @@ -129,53 +141,37 @@ \section{Телеметрические пакеты} ID,PACKET,UPTIME,PANEL_POWER,VOLT,MODE,ANTENNA,CHECKSUM \end{lstlisting} -\code{ANTENNA=1} означает раскрытую антенну, \code{ANTENNA=0} — сложенную. При отсутствии поля ЦУП показывает \textbf{Н/Д}. +\code{ANTENNA=1} означает раскрытую антенну, \code{ANTENNA=0} — сложенную. Если поля нет, интерфейс показывает \textbf{Н/Д}. \subsection{Контрольная сумма} -В v8 последние два символа \code{CHECKSUM} сохраняются и отображаются, однако \textbf{прикладная XOR-проверка отключена}. Поэтому изменение ID или другого поля без пересчёта XOR не приводит к отбрасыванию всего пакета. - -Это не отключает аппаратный CRC пакетного механизма CC1101: пакеты, которые радиочип определил как повреждённые по CRC, по-прежнему отмечаются как ошибка радиоприёма. - -\section{Первый запуск} - -\begin{enumerate}[leftmargin=7mm] -\item Подключить ESP32 + CC1101 по таблице выше. -\item Прошить ESP32 файлом \code{Altair_Gateway_v8.ino} либо встроенным прошивальщиком ЦУПа. -\item Подключить ESP32 к компьютеру USB-кабелем. -\item Если открыт Serial Monitor Arduino IDE — закрыть его. -\item Запустить ЦУП Альтаир. -\item В панели соединения выбрать ESP32 и 115200 бод. -\item Нажать «Подключить устройство» и выбрать COM-порт. -\item После успешного подключения панель автоматически сворачивается; при необходимости её можно раскрыть. -\end{enumerate} - -На главном экране постоянно показана рабочая частота 435.000 МГц и полоса 203 кГц. - -\section{Несколько спутников} +Поле \code{CHECKSUM} принимается и отображается, но \textbf{прикладная XOR-проверка отключена}. Поэтому изменение ID или другого поля без пересчёта XOR не приводит к отбрасыванию пакета. -Каждая телеметрическая строка содержит ID аппарата. Графики v8 автоматически создают отдельный dataset для каждого ID. Например, если одновременно принимаются аппараты 01, 02 и 07, на одном графике будут три разные линии и три цвета. На линиях отображаются точки фактически полученных отсчётов. +Аппаратный CRC пакетного механизма CC1101 остаётся включённым. Сообщение \code{$ERR,RADIO_RX_CRC} относится именно к CRC радиочипа, а не к текстовому XOR-полю. -Кнопка \textbf{«Сбросить принятые данные»} очищает карточки, графики, счётчики и сырой COM-журнал без перезапуска программы. +\section{Работа с несколькими спутниками} -\section{Сырой COM-терминал} +Телеметрия расположена во всю ширину окна. Сразу под ней находятся графики. Для каждого ID строятся собственные цветные линии с точками фактических измерений. -Панель \textbf{RAW COM / DEBUG} показывает полные строки, поступающие от ESP32, а также строки, переданные программой в последовательный порт. +Справа от графиков находится список \textbf{«Связь по ID»}. Для профильной смены заранее показаны ID 01--10: +\begin{itemize}[leftmargin=7mm] +\item \textbf{НЕТ СВЯЗИ} — пакет от ID ещё не принимался; +\item \textbf{ПЕРЕДАЁТ} — пакет был принят в последние 5 секунд; +\item \textbf{БЫЛ НА СВЯЗИ} — аппарат уже принимался, но сейчас поток пакетов прекратился. +\end{itemize} -Она нужна для ответа на простой диагностический вопрос: действительно ли данные поступают редко или они быстро приходят по USB, но не распознаются парсером. ЦУП не вводит специальную задержку между приёмом строки и её отображением. +После первого пакета ID получает цвет, совпадающий с цветом его линий на графиках. Любой дополнительный ID автоматически добавляется в список. -Если строки отсутствуют в RAW COM, проверяется радиотракт и ESP32. Если строки есть, но карточки не обновляются, проверяется формат телеметрии. +Кнопка \textbf{«Сбросить принятые данные»} очищает карточки, графики, счётчики, RAW COM и историю ID без перезапуска приложения. \section{Передача команд} -ЦУП v8 может передавать команды через тот же наземный CC1101. - -Команда программы для ESP32: +Пример команды ЦУПа шлюзу: \begin{lstlisting} $CMD,RF,TO=02,NAME=PING \end{lstlisting} -ESP32 удаляет служебное поле \code{RF} и передаёт в эфир: +ESP32 передаёт в эфир: \begin{lstlisting} $CMD,TO=02,NAME=PING \end{lstlisting} @@ -186,7 +182,7 @@ \section{Передача команд} Команда & Назначение\\ \midrule PING & проверка командного радиоканала\\ -INFO & запрос информации от бортовой программы\\ +INFO & запрос информации\\ TM\_START & запуск телеметрии\\ TM\_STOP & остановка телеметрии\\ TM\_PERIOD,MS=N & изменение периода передачи\\ @@ -195,26 +191,13 @@ \section{Передача команд} \end{tabularx} \end{center} -Адрес \code{ALL} предназначен для широковещательной команды. Реакцию на команду должна реализовать бортовая прошивка конкретного спутника. Даже без бортового обработчика сам факт передачи можно проверить SDR-приёмником. - -Служебная команда \code{$CMD,RADIO_STATUS} возвращает текущий фиксированный профиль шлюза. - -\section{Индикация и ошибки} +Реакцию на команды должна реализовать бортовая прошивка спутника. Сам факт RF-передачи можно проверить SDR-приёмником. -ЦУП разделяет ошибки прикладного формата и ошибки радиоканала. Прикладная XOR-проверка не используется. Сообщение \code{$ERR,RADIO_RX_CRC} означает, что аппаратный CRC CC1101 не прошёл. Нераспознанная, но физически принятая строка остаётся доступна в RAW COM для диагностики. +\section{RAW COM и диагностика} -\section{О программе} +Панель RAW COM перенесена ниже основного рабочего экрана. Она показывает строки от ESP32 и команды, отправленные программой. -В интерфейсе имеется окно \textbf{«О программе»}: - -\begin{itemize}[leftmargin=7mm] -\item ЦУП Альтаир v8 / 0.8.0; -\item автор — Антон Т.; -\item создано в рамках образовательного проекта «Миссия на Луну»; -\item подготовлено к профильной смене по спутникостроению и развивалось на Технопроме-2026. -\end{itemize} - -\section{Краткая диагностика} +Если RAW COM показывает строки быстро, а карточки не обновляются, проверяется формат телеметрии. Если строк нет, проверяются ESP32, CC1101, питание, частота и радиоканал. \begin{longtable}{p{0.31\linewidth}p{0.61\linewidth}} \toprule @@ -227,16 +210,25 @@ \section{Краткая диагностика} \endhead ESP32 не подключается & закрыть Arduino Serial Monitor, проверить USB-кабель и выбрать правильный COM-порт.\\ CC1101 не запускается & проверить 3.3 В, GND, GPIO18/19/23/21.\\ -Пакеты видны на SDR, но не принимаются & проверить 435.000 МГц, 4.8 kbps, 2-FSK, девиацию 5 кГц; в v8 RX BW уже установлен 203 кГц.\\ -После изменения ID пакет пропал & в официальной прошивке v8 такого быть не должно: XOR не проверяется. Проверить, что ESP32 действительно прошита v8.\\ -Данные обновляются редко & открыть RAW COM и сравнить реальную частоту поступления строк. При нескольких передатчиках возможны радиоколлизии.\\ -Команда не исполняется аппаратом & проверить SDR, что ЦУП передаёт RF-пакет; затем проверить обработчик команд в бортовой прошивке.\\ +Пакеты видны на SDR, но не принимаются & проверить 435.000 МГц, 4.8 kbps, 2-FSK и девиацию 5 кГц; RX BW наземного шлюза равен 203 кГц.\\ +После изменения ID пакет пропал & убедиться, что используется официальная прошивка v8: прикладная XOR-проверка должна быть отключена.\\ +Данные обновляются редко & открыть RAW COM и сравнить реальную частоту поступления строк; при нескольких передатчиках возможны радиоколлизии.\\ +Команда не исполняется & сначала проверить SDR, что наземный CC1101 действительно передаёт пакет, затем проверить обработчик команды на борту.\\ \bottomrule \end{longtable} +\section{О программе} + +\begin{itemize}[leftmargin=7mm] +\item ЦУП Альтаир v8 / 0.8.1; +\item автор — Антон Т.; +\item создано в рамках проекта «Миссия на Луну»; +\item подготовлено к профильной смене по спутникостроению и развивалось на Технопроме-2026. +\end{itemize} + \vfill \begin{center} -\small ЦУП Альтаир v8 · Миссия на Луну · 2026 +\small ЦУП Альтаир v8 · 0.8.1 · Миссия на Луну · 2026 \end{center} \end{document} From f16349aab351b713564e7df3227dd514fe7f339a Mon Sep 17 00:00:00 2001 From: "Anton T." <61037544+Anton778@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:44:58 +0700 Subject: [PATCH 10/10] v8.1: make dashboard reordering idempotent --- js/v8.1-ui.js | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/js/v8.1-ui.js b/js/v8.1-ui.js index 7bcf3cf..101f337 100644 --- a/js/v8.1-ui.js +++ b/js/v8.1-ui.js @@ -128,7 +128,8 @@ return; } firmware.classList.remove("v81-utility-collapsed"); - firmware.querySelector(".v81UtilityToggle")?.replaceChildren(document.createTextNode("Свернуть")); + const utilityToggle = firmware.querySelector(".v81UtilityToggle"); + if (utilityToggle) utilityToggle.textContent = "Свернуть"; firmware.scrollIntoView({ behavior: "smooth", block: "start" }); }); @@ -172,7 +173,8 @@ function compactFirmwarePanel() { const firmware = document.getElementById("firmwarePanel"); if (!firmware) return; - firmware.classList.add("v81FirmwarePanel", "v81-utility-collapsed"); + firmware.classList.add("v81FirmwarePanel"); + if (!firmware.dataset.v81Opened) firmware.classList.add("v81-utility-collapsed"); const eyebrow = firmware.querySelector(".panelEyebrow"); if (eyebrow) eyebrow.textContent = "ШАГ 1 · ESP32 FLASH"; @@ -181,10 +183,11 @@ const button = document.createElement("button"); button.type = "button"; button.className = "v81UtilityToggle"; - button.textContent = "Развернуть"; + button.textContent = firmware.classList.contains("v81-utility-collapsed") ? "Развернуть" : "Свернуть"; row?.appendChild(button); button.addEventListener("click", () => { const collapsed = firmware.classList.toggle("v81-utility-collapsed"); + firmware.dataset.v81Opened = collapsed ? "" : "1"; button.textContent = collapsed ? "Развернуть" : "Свернуть"; }); } @@ -267,7 +270,7 @@ item.classList.remove("never", "seen", "active"); if (!info) { item.classList.add("never"); - if (dot) dot.style.background = ""; + if (dot) { dot.style.background = ""; dot.style.color = ""; } if (status) status.textContent = "НЕТ СВЯЗИ"; if (small) small.textContent = "0 пак."; return; @@ -275,7 +278,7 @@ const active = now - info.lastSeen <= 5000; item.classList.add("seen"); if (active) item.classList.add("active"); - if (dot) dot.style.background = info.color; + if (dot) { dot.style.background = info.color; dot.style.color = info.color; } if (status) status.textContent = active ? "ПЕРЕДАЁТ" : "БЫЛ НА СВЯЗИ"; if (small) small.textContent = `${info.count} пак.`; }); @@ -295,6 +298,16 @@ updateRoster(); } + function placeAfter(anchor, node) { + if (!anchor || !node || anchor.nextElementSibling === node) return; + anchor.insertAdjacentElement("afterend", node); + } + + function placeBefore(anchor, node) { + if (!anchor || !node || anchor.previousElementSibling === node) return; + anchor.parentNode?.insertBefore(node, anchor); + } + function reorganizeDashboard() { const dashboard = document.getElementById("dashboard"); if (!dashboard) return; @@ -309,14 +322,14 @@ const raw = document.getElementById("rawSerialPanel"); const logPanel = document.getElementById("logPanel"); - if (flow && telemetry) flow.insertAdjacentElement("afterend", telemetry); - if (telemetry && charts) telemetry.insertAdjacentElement("afterend", charts); - if (charts && command) charts.insertAdjacentElement("afterend", command); - if (command && system) command.insertAdjacentElement("afterend", system); - if (system && radio) system.insertAdjacentElement("afterend", radio); - if (radio && firmware) radio.insertAdjacentElement("afterend", firmware); - if (firmware && serial) firmware.insertAdjacentElement("afterend", serial); - if (logPanel && raw) logPanel.parentNode.insertBefore(raw, logPanel); + placeAfter(flow, telemetry); + placeAfter(telemetry, charts); + placeAfter(charts, command); + placeAfter(command, system); + placeAfter(system, radio); + placeAfter(radio, firmware); + placeAfter(firmware, serial); + placeBefore(logPanel, raw); makeSerialCompact(); compactFirmwarePanel();