diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 0c5a654..04a22cc 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -185,6 +185,7 @@ export default { 'chat': 'Radio Chat', 'menu.cps.writeNoticeTitle': 'Confirm', 'menu.cps.writeNoticeContent': "Confirmation to write the channel shown on the web page to the device? (will override the device's current channel configuration)", + 'menu.satellite2': 'Satcom 2.0', ...localeSettings, ...localeMessageBox, ...localeLogin, diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index e89eafe..c2a5994 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -185,6 +185,7 @@ export default { 'chat': '无线电聊天', 'menu.cps.writeNoticeTitle': '确认', 'menu.cps.writeNoticeContent': '确认将网页显示的信道写入设备吗?(将覆盖设备当前信道配置)', + 'menu.satellite2': '星历写入 2.0', ...localeSettings, ...localeMessageBox, ...localeLogin, diff --git a/src/router/routes/modules/list.ts b/src/router/routes/modules/list.ts index b7439ec..22ae473 100644 --- a/src/router/routes/modules/list.ts +++ b/src/router/routes/modules/list.ts @@ -62,6 +62,16 @@ const LIST: AppRouteRecordRaw = { roles: ['*'], }, }, + { + path: 'sat2', + name: 'Sat2', + component: () => import('@/views/list/sat2/index.vue'), + meta: { + locale: 'menu.satellite2', + requiresAuth: true, + roles: ['*'], + }, + }, { path: 'bl', name: 'BL', diff --git a/src/views/list/sat2/index.vue b/src/views/list/sat2/index.vue new file mode 100644 index 0000000..f79d385 --- /dev/null +++ b/src/views/list/sat2/index.vue @@ -0,0 +1,567 @@ +<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> \ No newline at end of file diff --git a/src/views/thanks/index.vue b/src/views/thanks/index.vue index 7496c92..5d37d40 100644 --- a/src/views/thanks/index.vue +++ b/src/views/thanks/index.vue @@ -64,6 +64,7 @@ onMounted(() => { { "name": "BG4IWE", "channel": "QQ", "time": "2024-06-07", "money": "2.00" }, { "name": "BA7IPG", "channel": "QQ", "time": "2024-06-16", "money": "5.00" }, { "name": "BA3QT", "channel": "QQ", "time": "2024-11-18", "money": "6.00" }, + { "name": "BI4ACB", "channel": "微信", "time": "2025-01-29", "money": "66.00"} ]; data.sort((a, b) => { if (parseFloat(b.money) - parseFloat(a.money) === 0){