<template> <div class="container"> <a-modal width="650px" v-model:visible="state.selfSatModal" @ok="addSelfSat"> <template #title> {{ $t("sat.selfSatInfo") }} </template> <div> <a-textarea v-model="state.selfSatInfo" style="height: 120px;" placeholder="ISS (ZARYA) 1 25544U 98067A 24320.36274227 .00015569 00000+0 28188-3 0 9999 2 25544 51.6413 286.4173 0007936 217.3657 298.3197 15.49809951481990" /> </div> </a-modal> <a-modal v-model:visible="state.visible" @ok="handleOk" :ok-text="$t('tool.scaned')"> <template #title> {{ $t('tool.scanqr') }} </template> <div style="text-align: center;"> <img :src="state.qrcode" /><br> {{ $t('tool.scannotice') }} </div> </a-modal> <Breadcrumb :items="[$t('menu.list'), $t('menu.satellite2')]" /> <a-row :gutter="20" align="stretch"> <a-col :span="24"> <a-card class="general-card" :title="$t('menu.satellite2') + $t('global.onStart')"> <a-spin :loading="loading" style="width: 100%;" tip="正在处理 ..."> <a-form-item :label-col-style="{ width: '25%' }" field="dt" :label="$t('tool.brtime')" @click="() => { state.showHide += 1 }"> {{ state.dt }} </a-form-item> <a-form-item v-show="state.showHide >= 5" :label-col-style="{ width: '25%' }" field="dtCustom" label="自定义时间"> <div> <a-date-picker style="width: 220px; margin: 0 24px 24px 0;" show-time :time-picker-props="{ defaultValue: '00:00:00' }" format="YYYY-MM-DD HH:mm:ss" v-model="state.dtCustom" /> <t-button size="small" theme="success" @click="writeTime">写入时间到台站</t-button> </div> </a-form-item> <a-form-item :label-col-style="{ width: '25%' }" field="sat" :label="$t('tool.selectSatellite')"> <div style="width: 100%;"> <a-select v-model="state.sat" @change="changeSat" :placeholder="$t('tool.selectSatellite') + '...'" allow-search allow-clear> <a-option v-for="item in state.satData" :key="item.name" :value="item.name">{{ item.name }}</a-option> </a-select> <a-link @click="() => { state.selfSatModal = true }" style="margin-top: 10px;">{{ $t("sat.addSelfSat") }}</a-link> </div> </a-form-item> <a-form-item :label-col-style="{ width: '25%' }" field="sat" label=""> <t-table ref="tableRef" row-key="key" :columns="columns" :data="state.satsData" :editable-cell-state="editableCellState" bordered lazy-load /> </a-form-item> <a-form-item :label-col-style="{ width: '25%' }" label=""> <a-button type="primary" @click="writeIt">{{ $t('tool.writeData') }}</a-button> </a-form-item> <a-divider /> <div id="statusArea" style="height: 20em; background-color: var(--color-bg-3); color: var(--color-text-3); overflow: auto; padding: 20px" v-html="state.status"></div> </a-spin> </a-card> </a-col> </a-row> </div> </template> <script lang="ts" setup> import { ref, reactive, nextTick, onMounted, onUnmounted, computed } from 'vue'; import { useAppStore } from '@/store'; import { eeprom_write, eeprom_reboot, eeprom_init, hexReverseStringToUint8Array, stringToUint8Array } from '@/utils/serial.js'; import useLoading from '@/hooks/loading'; import QRCode from 'qrcode'; import { Input, Select } from 'tdesign-vue-next'; const { loading, setLoading } = useLoading(true); const appStore = useAppStore(); const lngRef: any = ref(null) const latRef: any = ref(null) const altRef: any = ref(null) const state: { uuid: string, qrcode: string, showHide: number, status: string, sat: string, satData: any[], lng: number, lat: number, alt: number, tx: number, rx: number, txTone: number | undefined, CTCSSOption: any[], pass: any, passOption: any[], rxTone: number | undefined, dt: any, timer: any, passCustom: any, dtCustom: any, freqDb: any, visible: boolean, selfSatModal: boolean, selfSatInfo: string, satsData: any[] } = reactive({ uuid: '', qrcode: '', visible: false, showHide: 0, status: "点击写入按钮写入卫星数据到设备<br/><br/>", sat: '', satData: [], lng: 0, lat: 0, alt: 0, tx: 0, rx: 0, txTone: 0, rxTone: 0, CTCSSOption: [0, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1], pass: undefined, passOption: [], dt: '', timer: undefined, passCustom: undefined, dtCustom: undefined, freqDb: [], selfSatModal: false, selfSatInfo: '', satsData: [] }) const editableCellState = (cellParams) => { // 第一行不允许编辑 const { row } = cellParams; return row.status !== 2; }; const tableRef = ref(); const columns = computed(() => [ { title: '卫星名称', colKey: 'satName', }, { title: '上行频率', colKey: 'txFreq', align: 'left', edit: { component: Input, props: { clearable: true, autofocus: true, }, // 触发校验的时机(when to validate) validateTrigger: 'change', // 透传给 component: Input 的事件(也可以在 edit.props 中添加) on: (editContext: any) => ({ onBlur: () => { console.log('失去焦点', editContext); }, onEnter: (ctx: any) => { ctx?.e?.preventDefault(); console.log('onEnter', ctx); }, }), // 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态 abortEditOnEvent: ['onEnter'], // 编辑完成,退出编辑态后触发 onEdited: (context: any) => { console.log(context); const newData = [...state.satsData]; newData.splice(context.rowIndex, 1, context.newRowData); state.satsData = newData; console.log('Edit firstName:', context); }, rules: [ { required: true, message: '不能为空' }, ], defaultEditable: true, }, }, { title: '上行亚音', colKey: 'txTone', edit: { component: Select, // props, 透传全部属性到 Select 组件 props: { clearable: true, options: state.CTCSSOption.map((e:any)=>{return {label: e, value: e}}), }, on: (editContext: any) => ({ onChange: (params: any) => { console.log('status changed', editContext, params); }, }), // 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态 // abortEditOnEvent: ['onChange'], // 编辑完成,退出编辑态后触发 onEdited: (context: any) => { state.satsData.splice(context.rowIndex, 1, context.newRowData); console.log('Edit Framework:', context); }, }, }, { title: '下行频率', colKey: 'rxFreq', align: 'left', edit: { component: Input, props: { clearable: true, autofocus: true, }, // 触发校验的时机(when to validate) validateTrigger: 'change', // 透传给 component: Input 的事件(也可以在 edit.props 中添加) on: (editContext: any) => ({ onBlur: () => { console.log('失去焦点', editContext); }, onEnter: (ctx: any) => { ctx?.e?.preventDefault(); console.log('onEnter', ctx); }, }), // 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态 abortEditOnEvent: ['onEnter'], // 编辑完成,退出编辑态后触发 onEdited: (context: any) => { console.log(context); const newData = [...state.satsData]; newData.splice(context.rowIndex, 1, context.newRowData); state.satsData = newData; console.log('Edit firstName:', context); }, rules: [ { required: true, message: '不能为空' }, ], defaultEditable: true, }, }, { title: '下行亚音', colKey: 'rxTone', edit: { component: Select, // props, 透传全部属性到 Select 组件 props: { clearable: true, options: state.CTCSSOption.map((e:any)=>{return {label: e, value: e}}), }, on: (editContext: any) => ({ onChange: (params: any) => { console.log('status changed', editContext, params); }, }), // 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态 // abortEditOnEvent: ['onChange'], // 编辑完成,退出编辑态后触发 onEdited: (context: any) => { state.satsData.splice(context.rowIndex, 1, context.newRowData); console.log('Edit Framework:', context); }, }, } ]); onMounted(async () => { try { if (sessionStorage.getItem('satFrequenciesRst')) { state.freqDb = JSON.parse(sessionStorage.getItem('satFrequenciesRst') || "[]") } else { const rst = await (await fetch('https://github.jobcher.com/gh/https://raw.githubusercontent.com/palewire/ham-satellite-database/main/data/amsat-active-frequencies.json')).text() state.freqDb = JSON.parse(rst) sessionStorage.setItem("satFrequenciesRst", rst) } } catch { } state.timer = setInterval(() => { state.dt = new Date().toLocaleString() localStorage.setItem('myLng', state.lng.toString()); localStorage.setItem('myLat', state.lat.toString()); localStorage.setItem('myAlt', state.alt.toString()); }, 1000) }) onUnmounted(() => { try { clearInterval(state.timer) } catch { } }) const writeTime = async () => { if (appStore.connectState != true) { alert(sessionStorage.getItem('noticeConnectK5')); return; }; setLoading(true) await eeprom_init(appStore.connectPort); await syncTime(); await eeprom_reboot(appStore.connectPort); setLoading(false) } const syncTime = async () => { const date = state.dtCustom ? new Date(state.dtCustom) : new Date(); const dateArray = [ ...hexReverseStringToUint8Array(parseInt(date.getUTCFullYear().toString().substring(2, 4)).toString(16)), ...hexReverseStringToUint8Array((date.getUTCMonth() + 1).toString(16)), ...hexReverseStringToUint8Array(date.getUTCDate().toString(16)), ...hexReverseStringToUint8Array(date.getUTCHours().toString(16)), ...hexReverseStringToUint8Array(date.getUTCMinutes().toString(16)), ...hexReverseStringToUint8Array(date.getUTCSeconds().toString(16)) ] await eeprom_write(appStore.connectPort, 0x02BA0, new Uint8Array(dateArray), 0x06, appStore.configuration?.uart); } const changeSat = async (sat: any) => { const data = state.satData.find(e => e.name == sat); if (data && data.path) { state.status += '<br/>卫星参数:<br/>' data.path.map((e: string) => { state.status += e + '<br/>' }) let freqFlag = false state.freqDb.map((e: any) => { if (data.path[1].split(" ")[1] == e.norad_id && e.mode.indexOf('FM') != -1) { console.log(e) freqFlag = true state.tx = e.uplink ? parseFloat(e.uplink.split('/')[0]) : 0 state.rx = e.downlink ? parseFloat(e.downlink.split('/')[0]) : 0 state.txTone = parseFloat(state.CTCSSOption.reduce((_e: any, _n: any) => { return e.mode.indexOf(_n) != -1 ? _n : _e })) } }) if (!freqFlag) { state.tx = 0 state.rx = 0 state.txTone = 0 state.rxTone = 0 } state.satsData.push({ "satName": sat, "line": data.path, "txFreq": state.tx, "txTone": state.txTone, "rxFreq": state.rx, "rxTone": state.rxTone }) console.log(state.satsData) } nextTick(() => { const textarea = document?.getElementById('statusArea'); if (textarea) textarea.scrollTop = textarea?.scrollHeight; }) } const initSat = async () => { setLoading(true) let rst = '' if (sessionStorage.getItem('satRst')) { rst = sessionStorage.getItem('satRst') || "" } else { rst = await (await fetch('https://celestrak.org/NORAD/elements/amateur.txt')).text() sessionStorage.setItem('satRst', rst) } const lines = rst.split(/\r?\n/); const sat = []; let _sat: any = {}; for (let i = 0; i < lines.length; i++) { if (Number.isNaN(parseInt(lines[i].substring(0, 1)))) { if (_sat.name && _sat.name != '') { sat.push(_sat) _sat = {} } _sat.name = lines[i] } else { if (!_sat.path) { _sat.path = [] } _sat.path.push(lines[i]) } } state.satData = sat setLoading(false) } initSat() const handleOk = async () => { const lol = await (await fetch('https://k5.vicicode.cn/api/lol', { method: "POST", mode: "cors", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ func: 1, uuid: state.uuid }) })).json(); const jsonLol = JSON.parse(lol.cache); if (jsonLol.length >= 3) { state.lng = jsonLol[0], state.lat = jsonLol[1], state.alt = jsonLol[2] } } const restoreRange = async (start: any = 0, uint8Array: any) => { await eeprom_init(appStore.connectPort); for (let i = start; i < uint8Array.length + start; i += 0x32) { await eeprom_write(appStore.connectPort, i, uint8Array.slice(i - start, i - start + 0x32), 0x32, appStore.configuration?.uart); state.status = state.status + "写入进度:" + (((i - start) / uint8Array.length) * 100).toFixed(1) + "%<br/>"; nextTick(() => { const textarea = document?.getElementById('statusArea'); if (textarea) textarea.scrollTop = textarea?.scrollHeight; }) } state.status = state.status + "写入进度:100.0%<br/>"; } const writeIt = async () => { if (appStore.connectState != true) { alert(sessionStorage.getItem('noticeConnectK5')); return; }; setLoading(true) await eeprom_init(appStore.connectPort); // console.log(state.satsData) let payload = new Uint8Array(160 * 45) for(let i = 0; i < state.satsData.length; i++){ const sat = state.satsData[i] //0x1E200~0x20000每160B是一个卫星的TLE // 9B名称 69B第一行 69B第二行 2B上行亚音 2B下行亚音 4B上行频率 4B下行频率 // 卫星名称 payload.set(stringToUint8Array(sat.satName).subarray(0,9), i * 160) // 第一行 payload.set(stringToUint8Array(sat.line[0]).subarray(0,69), i * 160 + 9) // 第二行 payload.set(stringToUint8Array(sat.line[1]).subarray(0,69), i * 160 + 9 + 69) // 上行亚音 const _txTone = new Uint8Array(2) if (sat.txTone && sat.txTone > 0) { _txTone.set(hexReverseStringToUint8Array(parseInt((sat.txTone * 10).toFixed(0)).toString(16)).subarray(0, 0x02)) } payload.set(_txTone, i * 160 + 9 + 69 + 69) // 下行亚音 const _rxTone = new Uint8Array(2) if (sat.rxTone && sat.rxTone > 0) { _rxTone.set(hexReverseStringToUint8Array(parseInt((sat.rxTone * 10).toFixed(0)).toString(16)).subarray(0, 0x02)) } payload.set(_rxTone, i * 160 + 9 + 69 + 69 + 2) // 上行频率 const _tx = new Uint8Array(4) _tx.set(hexReverseStringToUint8Array(parseInt(((sat.txFreq * 1000000) / 10).toFixed(0)).toString(16))) payload.set(_tx, i * 160 + 9 + 69 + 69 + 2 + 2) // 下行频率 const _rx = new Uint8Array(4) _rx.set(hexReverseStringToUint8Array(parseInt(((sat.rxFreq * 1000000) / 10).toFixed(0)).toString(16))) payload.set(_rx, i * 160 + 9 + 69 + 69 + 2 + 2 + 4) } await restoreRange(0x1E200, payload) await syncTime() await eeprom_reboot(appStore.connectPort); setLoading(false) } const isValidURL = (url: string) => { const pattern = new RegExp('^(https?:\\/\\/)?' + // 协议 (http 或 https) '((([a-zA-Z\\d]([a-zA-Z\\d-]*[a-zA-Z\\d])*)\\.)+[a-zA-Z]{2,}|' + // 域名 '((\\d{1,3}\\.){3}\\d{1,3}))' + // 或者 IP 地址 (IPv4) '(\\:\\d+)?(\\/[-a-zA-Z\\d%_.~+]*)*' + // 端口号和路径 '(\\?[;&a-zA-Z\\d%_.~+=-]*)?' + // 查询字符串 '(\\#[-a-zA-Z\\d_]*)?$', 'i'); // 锚点 return !!pattern.test(url); } const addSelfSat = async () => { if (isValidURL(state.selfSatInfo)) { state.selfSatInfo = await (await fetch(state.selfSatInfo)).text() } const lines = (state.selfSatInfo + "\n").split(/\r?\n/); const sat = []; let _sat: any = {}; for (let i = 0; i < lines.length; i++) { if (Number.isNaN(parseInt(lines[i].substring(0, 1)))) { if (_sat.name && _sat.name != '') { sat.push(_sat) _sat = {} } _sat.name = lines[i] } else { if (!_sat.path) { _sat.path = [] } _sat.path.push(lines[i]) } } state.satData = sat.concat(state.satData) state.selfSatInfo = '' } </script> <script lang="ts"> export default { name: 'Sat', }; </script> <style scoped lang="less"> .container { padding: 0 20px 20px 20px; :deep(.arco-list-content) { overflow-x: hidden; } :deep(.arco-card-meta-title) { font-size: 14px; } } :deep(.arco-list-col) { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; } :deep(.arco-list-item) { width: 33%; } :deep(.block-title) { margin: 0 0 12px 0; font-size: 14px; } :deep(.list-wrap) { // min-height: 140px; .list-row { align-items: stretch; .list-col { margin-bottom: 16px; } } :deep(.arco-space) { width: 100%; .arco-space-item { &:last-child { flex: 1; } } } } </style>