1
0
Fork 0
mirror of https://github.com/silenty4ng/k5web synced 2025-04-07 09:08:15 +00:00
k5web/src/views/list/sat2/index.vue
2025-02-01 23:36:01 +08:00

567 lines
No EOL
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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" />
&nbsp;&nbsp;<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>