diff --git a/public/LOSEHU117P6.bin b/public/LOSEHU117P6.bin new file mode 100644 index 0000000..080fd36 Binary files /dev/null and b/public/LOSEHU117P6.bin differ diff --git a/public/LOSEHU117P6K.bin b/public/LOSEHU117P6K.bin new file mode 100644 index 0000000..f8ecd14 Binary files /dev/null and b/public/LOSEHU117P6K.bin differ diff --git a/src/router/routes/modules/guide.ts b/src/router/routes/modules/guide.ts new file mode 100644 index 0000000..48f30d3 --- /dev/null +++ b/src/router/routes/modules/guide.ts @@ -0,0 +1,28 @@ +import { DEFAULT_LAYOUT } from '../base'; +import { AppRouteRecordRaw } from '../types'; + +const GUIDE: AppRouteRecordRaw = { + path: '/guide', + name: 'guide', + component: DEFAULT_LAYOUT, + meta: { + locale: '指南', + requiresAuth: true, + icon: 'icon-list', + order: 3, + }, + children: [ + { + path: 'f117', + name: 'f117', + component: () => import('@/views/guide/f117/index.vue'), + meta: { + locale: '使用117P6版', + requiresAuth: true, + roles: ['*'], + }, + }, + ], +}; + +export default GUIDE; diff --git a/src/utils/serial.js b/src/utils/serial.js index b4020b8..bd3d027 100644 --- a/src/utils/serial.js +++ b/src/utils/serial.js @@ -1099,6 +1099,143 @@ function getFontByte(string){ return [parseInt(last_byte), parseInt(byte)] } +function flash_generateCommand(data, address, totalSize) { + // the flash command structure is as follows: + /* 0x19 0x5 0xc 0x1 0x8a 0x8d 0x9f 0x1d + * address_msb address_lsb address_final_msb address_final_lsb length_msb length_lsb 0x0 0x0 + * [0x100 bytes of data, if length is <0x100 then fill the rest with zeroes] */ + + // flash is written in 0x100 blocks, if data is less than 0x100 bytes then it is padded with zeroes + if (data.length < 0x100) { + const padding = new Uint8Array(0x100 - data.length); + data = new Uint8Array([...data, ...padding]); + } + if (data.length != 0x100) throw new Error('Tell matt that he is an idiot'); + + // the address is a 16 bit integer, so we need to split it into two bytes + const address_msb = (address & 0xff00) >> 8; + const address_lsb = address & 0xff; + + const address_final = (totalSize + 0xff) & ~0xff; // add 0xff to totalSize and then round down to the next multiple of 0x100 by stripping the last byte + if (address_final > 0xf000) throw new Error('Total size is too large'); + const address_final_msb = (address_final & 0xff00) >> 8; + const address_final_lsb = 0x0; // since address_final can only be a multiple of 0x100, address_final_lsb is always 0x0 + + // the length is fixed to 0x100 bytes + const length_msb = 0x01; + const length_lsb = 0x00; + + return new Uint8Array([0x19, 0x5, 0xc, 0x1, 0x8a, 0x8d, 0x9f, 0x1d, address_msb, address_lsb, address_final_msb, address_final_lsb, length_msb, length_lsb, 0x0, 0x0, ...data]); +} + +async function flash_flashFirmware(port, firmware) { + // for loop to flash the firmware in 0x100 byte blocks + // this loop is safe as long as the firmware file is smaller than 0xf000 bytes + if (firmware.length > 0xefff) throw new Error('Last resort boundary check failed. Whoever touched the code is an idiot.'); + console.log('Flashing... 0%') + + for (let i = 0; i < firmware.length; i += 0x100) { + const data = firmware.slice(i, i + 0x100); + const command = flash_generateCommand(data, i, firmware.length); + + try { + await sendPacket(port, command); + await readPacket(port, 0x1a); + } catch (e) { + console.log('Flash command rejected. Aborting.'); + return Promise.reject(e); + } + + console.log(`Flashing... ${((i / firmware.length) * 100).toFixed(1)}%`, true); + } + console.log('Flashing... 100%', true) + console.log('Successfully flashed firmware.'); + return Promise.resolve(); +} + +const Crc16Tab = [0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920]; + +function crc16_ccitt(data) { + var i2, out; + i2 = 0; + + for (var i3 = 0, _pj_a = data.length; i3 < _pj_a; i3 += 1) { + out = Crc16Tab[(i2 >> 8 ^ data[i3]) & 255]; + i2 = out ^ i2 << 8; + } + + return 65535 & i2; +} + +function crc16_ccitt_le(data) { + var crc; + crc = crc16_ccitt(data); + return new Uint8Array([crc & 255, crc >> 8]); +} + +function firmware_xor(fwcontent) { + const XOR_ARRAY = new Uint8Array([ + 0x47, 0x22, 0xc0, 0x52, 0x5d, 0x57, 0x48, 0x94, 0xb1, 0x60, 0x60, 0xdb, 0x6f, 0xe3, 0x4c, 0x7c, + 0xd8, 0x4a, 0xd6, 0x8b, 0x30, 0xec, 0x25, 0xe0, 0x4c, 0xd9, 0x00, 0x7f, 0xbf, 0xe3, 0x54, 0x05, + 0xe9, 0x3a, 0x97, 0x6b, 0xb0, 0x6e, 0x0c, 0xfb, 0xb1, 0x1a, 0xe2, 0xc9, 0xc1, 0x56, 0x47, 0xe9, + 0xba, 0xf1, 0x42, 0xb6, 0x67, 0x5f, 0x0f, 0x96, 0xf7, 0xc9, 0x3c, 0x84, 0x1b, 0x26, 0xe1, 0x4e, + 0x3b, 0x6f, 0x66, 0xe6, 0xa0, 0x6a, 0xb0, 0xbf, 0xc6, 0xa5, 0x70, 0x3a, 0xba, 0x18, 0x9e, 0x27, + 0x1a, 0x53, 0x5b, 0x71, 0xb1, 0x94, 0x1e, 0x18, 0xf2, 0xd6, 0x81, 0x02, 0x22, 0xfd, 0x5a, 0x28, + 0x91, 0xdb, 0xba, 0x5d, 0x64, 0xc6, 0xfe, 0x86, 0x83, 0x9c, 0x50, 0x1c, 0x73, 0x03, 0x11, 0xd6, + 0xaf, 0x30, 0xf4, 0x2c, 0x77, 0xb2, 0x7d, 0xbb, 0x3f, 0x29, 0x28, 0x57, 0x22, 0xd6, 0x92, 0x8b + ]); + const XOR_LEN = XOR_ARRAY.length; + + for (let i = 0; i < fwcontent.length; i += 1) { + fwcontent[i] ^= XOR_ARRAY[i % XOR_LEN]; + } + + return fwcontent; +} + + +function unpack(encoded_firmware) { + + if (crc16_ccitt_le(encoded_firmware.slice(0, -2)).toString() === encoded_firmware.slice(-2).toString()) { + console.log("CRC check passed..."); + } else { + console.log("WARNING: CRC CHECK FAILED! FIRMWARE NOT VALID!\nMake sure to choose a flashable bin file. "); + } + + const decoded_firmware = firmware_xor(encoded_firmware.slice(0, -2)); + const versionInfoOffset = 0x2000; + const versionInfoLength = 16; + const resultLength = decoded_firmware.length - versionInfoLength; + const result = new Uint8Array(resultLength); + + result.set(decoded_firmware.subarray(0, versionInfoOffset)); + result.set(decoded_firmware.subarray(versionInfoOffset + versionInfoLength), versionInfoOffset); + + return result; +} + +function unpackVersion(encoded_firmware) { + + if (crc16_ccitt_le(encoded_firmware.slice(0, -2)).toString() === encoded_firmware.slice(-2).toString()) { + console.log("CRC check passed..."); + } else { + console.log("WARNING: CRC CHECK FAILED! FIRMWARE NOT VALID!\nMake sure to choose a flashable bin file. "); + } + + const decoded_firmware = firmware_xor(encoded_firmware.slice(0, -2)); + const versionInfoOffset = 0x2000; + const versionInfoLength = 16; + const resultLength = decoded_firmware.length - versionInfoLength; + const result = new Uint8Array(resultLength); + + result.set(decoded_firmware.subarray(0, versionInfoOffset)); + result.set(decoded_firmware.subarray(versionInfoOffset + versionInfoLength), versionInfoOffset); + + const rawVersion = decoded_firmware.subarray(versionInfoOffset, versionInfoOffset + versionInfoLength); + + return rawVersion; +} + export { connect, disconnect, @@ -1113,5 +1250,8 @@ export { eeprom_read, eeprom_reboot, check_eeprom, - eeprom_write + eeprom_write, + flash_flashFirmware, + unpackVersion, + unpack } \ No newline at end of file diff --git a/src/views/guide/f117/assets/cj1.png b/src/views/guide/f117/assets/cj1.png new file mode 100644 index 0000000..390c1f3 Binary files /dev/null and b/src/views/guide/f117/assets/cj1.png differ diff --git a/src/views/guide/f117/assets/cj2.png b/src/views/guide/f117/assets/cj2.png new file mode 100644 index 0000000..8937621 Binary files /dev/null and b/src/views/guide/f117/assets/cj2.png differ diff --git a/src/views/guide/f117/assets/cj3.png b/src/views/guide/f117/assets/cj3.png new file mode 100644 index 0000000..ffeced6 Binary files /dev/null and b/src/views/guide/f117/assets/cj3.png differ diff --git a/src/views/guide/f117/components/card-wrap.vue b/src/views/guide/f117/components/card-wrap.vue new file mode 100644 index 0000000..9b876d8 --- /dev/null +++ b/src/views/guide/f117/components/card-wrap.vue @@ -0,0 +1,204 @@ +<template> + <div class="card-wrap"> + <a-card v-if="loading" :bordered="false" hoverable> + <slot name="skeleton"></slot> + </a-card> + <a-card v-else :bordered="false" hoverable> + <a-space align="start"> + <a-avatar + v-if="icon" + :size="24" + style="margin-right: 8px; background-color: #626aea" + > + <icon-filter /> + </a-avatar> + <a-card-meta> + <template #title> + <a-typography-text style="margin-right: 10px"> + {{ title }} + </a-typography-text> + <template v-if="showTag"> + <a-tag + v-if="open && isExpires === false" + size="small" + color="green" + > + <template #icon> + <icon-check-circle-fill /> + </template> + <span>{{ tagText }}</span> + </a-tag> + <a-tag v-else-if="isExpires" size="small" color="red"> + <template #icon> + <icon-check-circle-fill /> + </template> + <span>{{ expiresTagText }}</span> + </a-tag> + </template> + </template> + <template #description> + {{ description }} + <slot></slot> + </template> + </a-card-meta> + </a-space> + <template #actions> + <a-switch v-if="actionType === 'switch'" v-model="open" /> + <a-space v-else-if="actionType === 'button'"> + <template v-if="isExpires"> + <a-button type="outline" @click="renew"> + {{ expiresText }} + </a-button> + </template> + <template v-else> + <a-button v-if="open" @click="handleToggle"> + {{ closeTxt }} + </a-button> + <a-button v-else-if="!open" type="outline" @click="handleToggle"> + {{ openTxt }} + </a-button> + </template> + </a-space> + <div v-else> + <a-space> + <a-button @click="toggle(false)"> + {{ closeTxt }} + </a-button> + <a-button type="primary" @click="toggle(true)"> + {{ openTxt }} + </a-button> + </a-space> + </div> + </template> + </a-card> + </div> +</template> + +<script lang="ts" setup> + import { ref } from 'vue'; + import { useToggle } from '@vueuse/core'; + + const props = defineProps({ + loading: { + type: Boolean, + default: false, + }, + title: { + type: String, + default: '', + }, + description: { + type: String, + default: '', + }, + actionType: { + type: String, + default: '', + }, + defaultValue: { + type: Boolean, + default: false, + }, + openTxt: { + type: String, + default: '', + }, + closeTxt: { + type: String, + default: '', + }, + expiresText: { + type: String, + default: '', + }, + icon: { + type: String, + default: '', + }, + showTag: { + type: Boolean, + default: true, + }, + tagText: { + type: String, + default: '', + }, + expires: { + type: Boolean, + default: false, + }, + expiresTagText: { + type: String, + default: '', + }, + }); + const [open, toggle] = useToggle(props.defaultValue); + const handleToggle = () => { + toggle(); + }; + const isExpires = ref(props.expires); + const renew = () => { + isExpires.value = false; + }; +</script> + +<style scoped lang="less"> + .card-wrap { + height: 100%; + transition: all 0.3s; + border: 1px solid var(--color-neutral-3); + border-radius: 4px; + &:hover { + transform: translateY(-4px); + // box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.1); + } + :deep(.arco-card) { + height: 100%; + border-radius: 4px; + .arco-card-body { + height: 100%; + .arco-space { + width: 100%; + height: 100%; + .arco-space-item { + height: 100%; + &:last-child { + flex: 1; + } + .arco-card-meta { + height: 100%; + display: flex; + flex-flow: column; + .arco-card-meta-content { + flex: 1; + .arco-card-meta-description { + margin-top: 8px; + color: rgb(var(--gray-6)); + line-height: 20px; + font-size: 12px; + } + } + .arco-card-meta-footer { + margin-top: 0; + } + } + } + } + } + } + :deep(.arco-card-meta-title) { + display: flex; + align-items: center; + + // To prevent the shaking + line-height: 28px; + } + :deep(.arco-skeleton-line) { + &:last-child { + display: flex; + justify-content: flex-end; + margin-top: 20px; + } + } + } +</style> diff --git a/src/views/guide/f117/components/quality-inspection.vue b/src/views/guide/f117/components/quality-inspection.vue new file mode 100644 index 0000000..88167b6 --- /dev/null +++ b/src/views/guide/f117/components/quality-inspection.vue @@ -0,0 +1,115 @@ +<template> + <div class="list-wrap"> + <a-typography-title class="block-title" :heading="6"> + {{ $t('cardList.tab.title.content') }} + </a-typography-title> + <a-row class="list-row" :gutter="24"> + <a-col + :xs="12" + :sm="12" + :md="12" + :lg="6" + :xl="6" + :xxl="6" + class="list-col" + > + <div class="card-wrap empty-wrap"> + <a-card :bordered="false" hoverable> + <a-result :status="null" :title="$t('cardList.content.action')"> + <template #icon> + <icon-plus style="font-size: 20px" /> + </template> + </a-result> + </a-card> + </div> + </a-col> + <a-col + v-for="item in renderData" + :key="item.id" + class="list-col" + :xs="12" + :sm="12" + :md="12" + :lg="6" + :xl="6" + :xxl="6" + > + <CardWrap + :loading="loading" + :title="item.title" + :description="item.description" + :default-value="item.enable" + :action-type="item.actionType" + :icon="item.icon" + :open-txt="$t('cardList.content.inspection')" + :close-txt="$t('cardList.content.delete')" + :show-tag="false" + > + <a-descriptions + style="margin-top: 16px" + :data="item.data" + layout="inline-horizontal" + :column="2" + /> + <template #skeleton> + <a-skeleton :animation="true"> + <a-skeleton-line + :widths="['50%', '50%', '100%', '40%']" + :rows="4" + /> + <a-skeleton-line :widths="['40%']" :rows="1" /> + </a-skeleton> + </template> + </CardWrap> + </a-col> + </a-row> + </div> +</template> + +<script lang="ts" setup> + import { queryInspectionList, ServiceRecord } from '@/api/list'; + import useRequest from '@/hooks/request'; + import CardWrap from './card-wrap.vue'; + + const defaultValue: ServiceRecord[] = new Array(3).fill({}); + const { loading, response: renderData } = useRequest<ServiceRecord[]>( + queryInspectionList, + defaultValue + ); +</script> + +<style scoped lang="less"> + .card-wrap { + height: 100%; + transition: all 0.3s; + border: 1px solid var(--color-neutral-3); + &:hover { + transform: translateY(-4px); + } + :deep(.arco-card-meta-description) { + color: rgb(var(--gray-6)); + .arco-descriptions-item-label-inline { + font-weight: normal; + font-size: 12px; + color: rgb(var(--gray-6)); + } + .arco-descriptions-item-value-inline { + color: rgb(var(--gray-8)); + } + } + } + .empty-wrap { + height: 200px; + border-radius: 4px; + :deep(.arco-card) { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + .arco-result-title { + color: rgb(var(--gray-6)); + } + } + } +</style> diff --git a/src/views/guide/f117/components/rules-preset.vue b/src/views/guide/f117/components/rules-preset.vue new file mode 100644 index 0000000..e5a2878 --- /dev/null +++ b/src/views/guide/f117/components/rules-preset.vue @@ -0,0 +1,51 @@ +<template> + <div class="list-wrap"> + <a-typography-title class="block-title" :heading="6"> + {{ $t('cardList.tab.title.preset') }} + </a-typography-title> + <a-row class="list-row" :gutter="24"> + <a-col + v-for="item in renderData" + :key="item.id" + :xs="12" + :sm="12" + :md="12" + :lg="6" + :xl="6" + :xxl="6" + class="list-col" + style="min-height: 140px" + > + <CardWrap + :loading="loading" + :title="item.title" + :description="item.description" + :default-value="item.enable" + :action-type="item.actionType" + :tag-text="$t('cardList.preset.tag')" + > + <template #skeleton> + <a-skeleton :animation="true"> + <a-skeleton-line :widths="['100%', '40%']" :rows="2" /> + <a-skeleton-line :widths="['40%']" :rows="1" /> + </a-skeleton> + </template> + </CardWrap> + </a-col> + </a-row> + </div> +</template> + +<script lang="ts" setup> + import { queryRulesPresetList, ServiceRecord } from '@/api/list'; + import useRequest from '@/hooks/request'; + import CardWrap from './card-wrap.vue'; + + const defaultValue: ServiceRecord[] = new Array(6).fill({}); + const { loading, response: renderData } = useRequest<ServiceRecord[]>( + queryRulesPresetList, + defaultValue + ); +</script> + +<style scoped lang="less"></style> diff --git a/src/views/guide/f117/components/the-service.vue b/src/views/guide/f117/components/the-service.vue new file mode 100644 index 0000000..2f4748e --- /dev/null +++ b/src/views/guide/f117/components/the-service.vue @@ -0,0 +1,57 @@ +<template> + <div class="list-wrap"> + <a-typography-title class="block-title" :heading="6"> + {{ $t('cardList.tab.title.service') }} + </a-typography-title> + <a-row class="list-row" :gutter="24"> + <a-col + v-for="item in renderData" + :key="item.id" + :xs="12" + :sm="12" + :md="12" + :lg="6" + :xl="6" + :xxl="6" + class="list-col" + style="min-height: 162px" + > + <CardWrap + :loading="loading" + :title="item.title" + :description="item.description" + :default-value="item.enable" + :action-type="item.actionType" + :expires="item.expires" + :open-txt="$t('cardList.service.open')" + :close-txt="$t('cardList.service.cancel')" + :expires-text="$t('cardList.service.renew')" + :tag-text="$t('cardList.service.tag')" + :expires-tag-text="$t('cardList.service.expiresTag')" + :icon="item.icon" + > + <template #skeleton> + <a-skeleton :animation="true"> + <a-skeleton-line :widths="['100%', '40%', '100%']" :rows="3" /> + <a-skeleton-line :widths="['40%']" :rows="1" /> + </a-skeleton> + </template> + </CardWrap> + </a-col> + </a-row> + </div> +</template> + +<script lang="ts" setup> + import { queryTheServiceList, ServiceRecord } from '@/api/list'; + import useRequest from '@/hooks/request'; + import CardWrap from './card-wrap.vue'; + + const defaultValue: ServiceRecord[] = new Array(4).fill({}); + const { loading, response: renderData } = useRequest<ServiceRecord[]>( + queryTheServiceList, + defaultValue + ); +</script> + +<style scoped lang="less"></style> diff --git a/src/views/guide/f117/index.vue b/src/views/guide/f117/index.vue new file mode 100644 index 0000000..3ed1814 --- /dev/null +++ b/src/views/guide/f117/index.vue @@ -0,0 +1,333 @@ +<template> + <div class="container"> + <Breadcrumb :items="['指南', '使用117P6版']" /> + <a-row :gutter="20" align="stretch"> + <a-col :span="24"> + <a-card class="general-card" title="使用117P6版"> + <a-steps :current="state.step"> + <a-step>选择固件类型</a-step> + <a-step>使用自定义固件</a-step> + <a-step>完成</a-step> + </a-steps> + <a-divider/> + <div v-show="state.step == 1" style="min-height: 300px; margin: 50px;"> + <p style="font-size: 1.2em; font-weight: bold;">你是否想要扩容你的设备以支持中文信道存储:</p> + <a-radio-group type="button" v-model="state.kIt"> + <a-radio value="yes">是的,我想要硬件改造我的设备</a-radio> + <a-radio value="no">不,我不需要中文信道</a-radio> + </a-radio-group> + <br> + <div v-show="state.kIt == 'yes'"> + <p style="color: #ff0000; font-weight: bold;">声明:本方案及相应固件均为技术探索用途。对原机进行改造需要相应专业知识且可能存在安全和法律风险。使用本方案和固件代表您已认可风险并自行承担后果,包括且不限于财产损失、人员伤亡、违法犯罪等。</p> + <p style="font-weight: bold; font-size: 1.2em;">一、相关法律</p> + <p> + 自制、改装、拼装的无线电发射设备,应符合国家相关技术标准,并按照工信部官网链接(<a href="http://www.miit.gov.cn/jgsj/wgj/kpzs/art/2022/art_c1ffd3c47e3f455dad38246579092136.html" target="_blank" rel="noopener noreferrer">http://www.miit.gov.cn/jgsj/wgj/kpzs/art/2022/art_c1ffd3c47e3f455dad38246579092136.html</a>,或通过工信部官网-工业和信息化部-机关司局-无管局-科普知识-《关于申请设置、使用业余无线电台所用无线电发射设备相关事宜的说明》)所列情况,提供相应材料。 + </p> + <p style="font-weight: bold; font-size: 1.2em;">二、所需工具</p> + <p> + 写频线、螺丝刀套装、电烙铁、焊锡丝、助焊剂、精密电器清洁剂(可选)、撬棒(可选)、万用表(可选)。 + </p> + <p style="font-weight: bold; font-size: 1.2em;">三、购买扩容芯片</p> + <p>芯片型号:<span style="color: #ff0000;">M24M02-DRMN6</span> 或其它 SOP-8 封装 I²C 总线 2Mbit EEPROM 芯片。考虑到芯片质量及操作失误可能导致的损坏,建议多买几片备用。</p> + </div> + <div v-show="state.kIt == 'no'"> + <p style="color: #ff0000; font-weight: bold;">声明:本方案及相应固件均为技术探索用途。对原机进行改造需要相应专业知识且可能存在安全和法律风险。使用本方案和固件代表您已认可风险并自行承担后果,包括且不限于财产损失、人员伤亡、违法犯罪等。</p> + <p style="font-weight: bold; font-size: 1.2em;">一、所需工具</p> + <p> + 写频线。 + </p> + </div> + </div> + <div v-show="state.step == 1" style="text-align: center;"> + <a-button type="primary" :disabled="!state.kIt" @click="state.step = 2">下一步</a-button> + </div> + <div v-show="state.step == 2" style="min-height: 300px; text-align: center; color: #C9CDD4; "> + <a-spin :loading="state.loading" tip="处理中..." style="width: 100%;"> + <a-collapse :activeKey="state.flashStep" @change="(e: any)=>{state.flashStep = e; if(e[0] == 'six'){state.finish = true;}}" accordion> + <a-collapse-item header="备份原机数据" key="one"> + <div style="text-align: left;"> + <p>在对设备进行操作前,应备份原机出厂的配置、校准数据,以保证发射性能符合国家标准。(请妥善保存备份文件)</p> + <p> + <a-space> + <a-button type="primary" :disabled="state.backed" @click="backupIt">备份</a-button><span v-show="state.backed" style="color: #C9CDD4;">已备份✔</span> + <a-button type="primary" :disabled="!state.backed" @click="state.flashStep[0] = 'two'">下一步</a-button> + </a-space> + </p> + </div> + </a-collapse-item> + <a-collapse-item header="拆机" key="two"> + <div style="text-align: left;"> + <p>视频教程链接: + <a href="https://www.bilibili.com/video/BV1ib4y137Ah" target="_blank" rel="noopener noreferrer">https://www.bilibili.com/video/BV1ib4y137Ah</a> + <span style="color: #ff0000;">(硬件拆解及焊接部分可参照本视频,软件刷机部分请务必以本指南为准。)</span> + </p> + <div> + ①拆掉电池、天线和旋钮盖。<br> + ②用撬棒插入主机背面底部正中位置缝隙,向上撬出铝制背板。③向下后方抽出背板。前盖和主板间有扬声器导线连接,此处用力不要过猛,控制幅度,以免拉断导线。<br> + ④拿掉耳机口挡板。<br> + ⑤建议将扬声器导线拆焊,以免阻碍后续拆解和焊接,导致拉断导线。最后组装时再对扬声器导线进行焊接。<br> + ⑥拆卸屏幕(难点,请认真看视频教程!)。在屏幕左下角卡扣位置,用撬棒向内按压同时向上抬起即可拆卸左侧,屏幕左侧松脱后另一侧拆卸相对简单。此处用力不要过猛,控制幅度,屏幕完全拆卸后应妥善固定,以免拉断、折断背面上方排线。<br> + ⑦拆卸全部 5 颗螺丝并分离背板。 + </div> + <img :src="cj1"> + <div> + 需要更换的芯片位于主板背面右下角,型号为 BL24C64A。 + </div> + <img :src="cj2"> + <div> + ①拆焊及焊接。有动手能力的朋友自行操作,要求芯片方向正确(以第1 脚圆点为准),焊点饱满,无虚焊、短路,芯片周围的电子元器件保持完好。建议用300℃以上小刀头烙铁配合助焊剂,不建议用热风枪(高手除外)。手残党可以去手机维修店更换,费用5-30 元不等。若周围元件遭到破坏,可按下图参数更换补救。 + </div> + <img :src="cj3"> + <div> + ②将主板装回背板。(背板上 3x5mm 导热硅胶垫若脱落,请务必装回对应凸起位置;电池触点部分过背板孔时当心压弯。)<br> + ③装入电池,按住 PTT 键开机,进入刷机模式(此时手电筒常亮、屏幕无显示)。如无法进入刷机模式,检查电池接触片是否错位、焊点及周边元件是否完好。 + </div> + <div style="color: #ff0000; font-weight: bold;"> + 此时先不要完全组装手台,待后续工作全部完成后再行组装,以便故障返工。此阶段如尝试正常开机后显示异常、电量异常、接收异常等均为正常情况,不用担心。后续操作后会恢复正常。 + </div> + <div> + <a-button type="primary" @click="state.flashStep[0] = 'three'">下一步</a-button> + </div> + </div> + </a-collapse-item> + <a-collapse-item header="刷入固件" key="three"> + <div style="text-align: left;"> + <p>断开写频线,按住 PTT 键开机,进入刷机模式(此时手电筒常亮、屏幕无显示),手电筒常亮后插回写频线。</p> + <p> + <a-space> + <a-button type="primary" :disabled="state.flashIt" @click="iFlashIt">刷入固件</a-button> + <a-button type="primary" :disabled="!state.flashIt" @click="state.flashStep[0] = 'four'">下一步</a-button> + </a-space> + </p> + </div> + </a-collapse-item> + <a-collapse-item header="刷回原机数据" key="four"> + <div style="text-align: left;"> + <p>正常开机,使设备处于开机状态,点击刷回备份数据。</p> + <p> + <a-space> + <a-button type="primary" :disabled="state.restoreBackupIt" @click="restoreBackup">刷回备份数据</a-button> + <a-button type="primary" :disabled="!state.restoreBackupIt" @click="state.flashStep[0] = 'five'">下一步</a-button> + </a-space> + </p> + </div> + </a-collapse-item> + <a-collapse-item header="刷入字库" key="five"> + <div style="text-align: left;"> + <p>正常开机,使设备处于开机状态,点击刷入字库。</p> + <p> + <a-space> + <a-button type="primary" :disabled="state.flashFontIt" @click="flashFont">刷入字库</a-button> + <a-button type="primary" :disabled="!state.flashFontIt" @click="state.flashStep[0] = 'six'">下一步</a-button> + </a-space> + </p> + </div> + </a-collapse-item> + <a-collapse-item header="完全组装" key="six"> + <div style="text-align: left;">按拆解顺序进行焊接和组装。</div> + </a-collapse-item> + </a-collapse> + </a-spin> + </div> + <div v-show="state.step == 2" style="text-align: center;"> + <a-space> + <a-button :disabled="!state.kIt" @click="state.step = 1">上一步</a-button> + <a-button type="primary" :disabled="!state.finish" @click="state.step = 3">完成</a-button> + </a-space> + </div> + <div v-show="state.step == 3" style="min-height: 300px; text-align: center; color: #C9CDD4; "> + <a-result + class="result" + status="success" + subtitle="刷入成功" + /> + <a-button type="primary" @click="()=>{router.push('/chirp/base');}"> + 返回首页 + </a-button> + </div> + </a-card> + </a-col> + </a-row> + </div> +</template> + +<script lang="ts" setup> +import { reactive, nextTick } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import { useAppStore } from '@/store'; +import { eeprom_write, eeprom_reboot, eeprom_init, eeprom_read, flash_flashFirmware, connect, disconnect, sendPacket, unpackVersion, readPacket, unpack } from '@/utils/serial.js'; +import cj1 from './assets/cj1.png' +import cj2 from './assets/cj2.png' +import cj3 from './assets/cj3.png' + +const appStore = useAppStore(); +const router = useRouter(); + +const state : { + step: any, + flashStep: any, + backed: any, + kIt: any, + flashIt: any, + restoreBackupIt: any, + loading: any, + flashFontIt: any, + finish: any +} = reactive({ + step: 1, + flashStep: ['one'], + backed: undefined, + kIt: undefined, + flashIt: false, + restoreBackupIt: false, + loading: false, + flashFontIt: false, + finish: false +}) + +const backupRange = async (start: any, end: any, name: any = new Date() + '_backup.bin') =>{ + await eeprom_init(appStore.connectPort); + let rawEEPROM = new Uint8Array(end - start); + for (let i = start; i < end; i += 0x80) { + const data = await eeprom_read(appStore.connectPort, i); + rawEEPROM.set(data, i - start); + } + const blob = new Blob([rawEEPROM], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + return rawEEPROM; +} + +const restoreRange = async (start: any = 0, uint8Array: any) => { + await eeprom_init(appStore.connectPort); + for (let i = start; i < uint8Array.length + start; i += 0x80) { + await eeprom_write(appStore.connectPort, i, uint8Array.slice(i - start, i - start + 0x80)); + } + await eeprom_reboot(appStore.connectPort); +} + +const backupIt = async () => { + if(appStore.connectState != true){alert('点击右上角“连接”按钮连接手台。'); return;}; + state.loading = true + state.backed = await backupRange(0, 0x2000); + console.log(state.backed) + state.loading = false +} + +const iFlashIt = async () => { + state.loading = true + let fontPacket = undefined + if(state.kIt){ + fontPacket = await fetch('/LOSEHU117P6K.bin') + }else{ + fontPacket = await fetch('/LOSEHU117P6.bin') + } + const reader = fontPacket.body.getReader(); + const chunks = []; + while(true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + chunks.push(...value) + } + const binary = new Uint8Array(chunks) + if(appStore.connectPort){ + await disconnect(appStore.connectPort); + } + let _connect = await connect(); + await readPacket(_connect, 0x18, 1000); + const rawVersion = unpackVersion(binary); + const _data = new Uint8Array([0x30, 0x5, rawVersion.length, 0x0, ...rawVersion]); + await sendPacket(_connect, _data); + await readPacket(_connect, 0x18) + await flash_flashFirmware(_connect, unpack(binary)) + appStore.updateSettings({ connectPort: _connect }); + state.flashIt = true + state.loading = false +} + +const restoreBackup = async () => { + if(appStore.connectState != true){alert('点击右上角“连接”按钮连接手台。'); return;}; + state.loading = true + await restoreRange(0, state.backed); + state.restoreBackupIt = true; + state.loading = false +} + +const flashFont = async () => { + state.loading = true + const fontPacket = await fetch('/old_font.bin') + const reader = fontPacket.body.getReader(); + const chunks = []; + while(true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + chunks.push(...value) + } + const binary = new Uint8Array(chunks) + await restoreRange(0x02000, binary) + state.flashFontIt = true; + state.loading = false +} + +</script> + +<script lang="ts"> + export default { + name: 'Chi', + }; +</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> diff --git a/src/views/guide/f117/locale/en-US.ts b/src/views/guide/f117/locale/en-US.ts new file mode 100644 index 0000000..6b0681e --- /dev/null +++ b/src/views/guide/f117/locale/en-US.ts @@ -0,0 +1,19 @@ +export default { + 'menu.list.cardList': 'Card List', + 'cardList.tab.title.all': 'All', + 'cardList.tab.title.content': 'Quality Inspection', + 'cardList.tab.title.service': 'The service', + 'cardList.tab.title.preset': 'Rules Preset', + 'cardList.searchInput.placeholder': 'Search', + 'cardList.enable': 'Enable', + 'cardList.disable': 'Disable', + 'cardList.content.delete': 'Delete', + 'cardList.content.inspection': 'Inspection', + 'cardList.content.action': 'Click Create Qc Content queue', + 'cardList.service.open': 'Open', + 'cardList.service.cancel': 'Cancel', + 'cardList.service.renew': 'Contract of service', + 'cardList.service.tag': 'Opened', + 'cardList.service.expiresTag': 'Expired', + 'cardList.preset.tag': 'Enable', +}; diff --git a/src/views/guide/f117/locale/zh-CN.ts b/src/views/guide/f117/locale/zh-CN.ts new file mode 100644 index 0000000..0cd29e7 --- /dev/null +++ b/src/views/guide/f117/locale/zh-CN.ts @@ -0,0 +1,19 @@ +export default { + 'menu.list.cardList': '卡片列表', + 'cardList.tab.title.all': '全部', + 'cardList.tab.title.content': '内容质检', + 'cardList.tab.title.service': '开通服务', + 'cardList.tab.title.preset': '规则预置', + 'cardList.searchInput.placeholder': '搜索', + // 'cardList.statistic.enable': '已启用', + // 'cardList.statistic.disable': '未启用', + 'cardList.content.delete': '删除', + 'cardList.content.inspection': '质检', + 'cardList.content.action': '点击创建质检内容队列', + 'cardList.service.open': '开通服务', + 'cardList.service.cancel': '取消服务', + 'cardList.service.renew': '续约服务', + 'cardList.service.tag': '已开通', + 'cardList.service.expiresTag': '已过期', + 'cardList.preset.tag': '已启用', +}; diff --git a/src/views/guide/f117/mock.ts b/src/views/guide/f117/mock.ts new file mode 100644 index 0000000..68cf096 --- /dev/null +++ b/src/views/guide/f117/mock.ts @@ -0,0 +1,186 @@ +import Mock from 'mockjs'; +import setupMock, { successResponseWrap } from '@/utils/setup-mock'; +import { ServiceRecord } from '@/api/list'; + +const qualityInspectionList: ServiceRecord[] = [ + { + id: 1, + name: 'quality', + title: '视频类-历史导入', + description: '2021-10-12 00:00:00', + data: [ + { + label: '待质检数', + value: '120', + }, + { + label: '积压时长', + value: '60s', + }, + { + label: '待抽检数', + value: '0', + }, + ], + }, + { + id: 2, + name: 'quality', + title: '图文类-图片版权', + description: '2021-12-11 18:30:00', + data: [ + { + label: '待质检数', + value: '120', + }, + { + label: '积压时长', + value: '60s', + }, + { + label: '待抽检数', + value: '0', + }, + ], + }, + { + id: 3, + name: 'quality', + title: '图文类-高清图片', + description: '2021-10-15 08:10:00', + data: [ + { + label: '待质检数', + value: '120', + }, + { + label: '积压时长', + value: '60s', + }, + { + label: '待抽检数', + value: '0', + }, + ], + }, +]; +const theServiceList: ServiceRecord[] = [ + { + id: 1, + icon: 'code', + title: '漏斗分析', + description: + '用户行为分析之漏斗分析模型是企业实现精细化运营、进行用户行为分析的重要数据分析模型。', + enable: true, + actionType: 'button', + }, + { + id: 2, + icon: 'edit', + title: '用户分布', + description: + '快速诊断用户人群,地域细分情况,了解数据分布的集中度,以及主要的数据分布的区间段是什么。', + enable: true, + actionType: 'button', + expires: true, + }, + { + id: 3, + icon: 'user', + title: '资源分发', + description: + '移动端动态化资源分发解决方案。提供稳定大流量服务支持、灵活定制的分发圈选规则,通过离线化预加载。', + enable: false, + actionType: 'button', + }, + { + id: 4, + icon: 'user', + title: '用户画像分析', + description: + '用户画像就是将典型用户信息标签化,根据用户特征、业务场景和用户行为等信息,构建一个标签化的用户模型。', + enable: true, + actionType: 'button', + }, +]; +const rulesPresetList: ServiceRecord[] = [ + { + id: 1, + title: '内容屏蔽规则', + description: + '用户在执行特定的内容分发任务时,可使用内容屏蔽规则根据特定标签,过滤内容集合。', + enable: true, + actionType: 'switch', + }, + { + id: 2, + title: '内容置顶规则', + description: + '该规则支持用户在执行特定内容分发任务时,对固定的几条内容置顶。', + enable: true, + actionType: 'switch', + }, + { + id: 3, + title: '内容加权规则', + description: '选定内容加权规则后可自定义从不同内容集合获取内容的概率。', + enable: false, + actionType: 'switch', + }, + { + id: 4, + title: '内容分发规则', + description: '内容分发时,对某些内容需要固定在C端展示的位置。', + enable: true, + actionType: 'switch', + }, + { + id: 5, + title: '违禁内容识别', + description: '精准识别赌博、刀枪、毒品、造假、贩假等违规物品和违规行为。', + enable: false, + actionType: 'switch', + }, + { + id: 6, + title: '多语言文字符号识别', + description: + '精准识别英语、维语、藏语、蒙古语、朝鲜语等多种语言以及emoji表情形态的语义识别。', + enable: false, + actionType: 'switch', + }, +]; + +setupMock({ + setup() { + // Quality Inspection + Mock.mock(new RegExp('/api/list/quality-inspection'), () => { + return successResponseWrap( + qualityInspectionList.map((_, index) => ({ + ...qualityInspectionList[index % qualityInspectionList.length], + id: Mock.Random.guid(), + })) + ); + }); + + // the service + Mock.mock(new RegExp('/api/list/the-service'), () => { + return successResponseWrap( + theServiceList.map((_, index) => ({ + ...theServiceList[index % theServiceList.length], + id: Mock.Random.guid(), + })) + ); + }); + + // rules preset + Mock.mock(new RegExp('/api/list/rules-preset'), () => { + return successResponseWrap( + rulesPresetList.map((_, index) => ({ + ...rulesPresetList[index % rulesPresetList.length], + id: Mock.Random.guid(), + })) + ); + }); + }, +});