diff --git a/config.json b/config.json index 7a2c964..9fff90c 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "basePath": "./firmware", + "staticPath": "/firmware", "role": { "gui": { "icon": "gradient", @@ -14,12 +14,14 @@ "companionBle": { "icon": "smartphone", "class": "primary-text", - "title": "Companion radio: Bluetooth", + "title": "Companion radio", + "subTitle": "Bluetooth", "tooltip": "Chat via mobile phone App or Web Client" }, "companionUsb": { "icon": "usb", - "title": "Companion radio: USB", + "title": "Companion radio", + "subTitle": "USB", "tooltip": "Chat via Web client or command line client" }, "repeater": { @@ -41,33 +43,69 @@ "firmware": [ { "role": "gui", - "files": [ - { - "type": "flash", - "name": "RippleUltra-TDeck-v6.0-beta21-merged.bin", - "title": "Combined app+partition+bootloader firmware bin" + "version": { + "v6.2": { + "files": [ + { + "type": "flash", + "name": "RippleUltra-TDeck-v6.2-merged.bin", + "title": "Combined app+partition+bootloader firmware bin" + }, + { + "type": "download", + "name": "RippleUltra-TDeck-v6.2.bin", + "title": "App firmware bin (use with m5 booloader)" + } + ] }, - { - "type": "download", - "name": "RippleUltra-TDeck-v6.0-beta21.bin", - "title": "App firmware bin (use with m5 booloader)" + "v6.0-beta22": { + "files": [ + { + "type": "flash", + "name": "RippleUltra-TDeck-v6.0-beta22-merged.bin", + "title": "Combined app+partition+bootloader firmware bin" + }, + { + "type": "download", + "name": "RippleUltra-TDeck-v6.0-beta22.bin", + "title": "App firmware bin (use with m5 booloader)" + } + ] } - ] + } }, { "role": "guiSD", - "files": [ - { - "type": "flash", - "name": "RippleUltra-TDeck-SD-v6.0-beta21-merged.bin", - "title": "Combined app+partition+bootloader firmware bin" + "version": { + "v6.2": { + "files": [ + { + "type": "flash", + "name": "RippleUltra-TDeck-SD-v6.2-merged.bin", + "title": "Combined app+partition+bootloader firmware bin" + }, + { + "type": "download", + "name": "RippleUltra-TDeck-SD-v6.2.bin", + "title": "App firmware bin (use with m5 booloader)" + } + ] }, - { - "type": "download", - "name": "RippleUltra-TDeck-SD-v6.0-beta21.bin", - "title": "App firmware bin (use with m5 booloader)" + "v6.0-beta22": { + "files": [ + { + "type": "flash", + "name": "RippleUltra-TDeck-SD-v6.0-beta22-merged.bin", + "title": "Combined app+partition+bootloader firmware bin" + }, + { + "type": "download", + "name": "RippleUltra-TDeck-SD-v6.0-beta22.bin", + "title": "App firmware bin (use with m5 booloader)" + } + ] } - ] + } } ] }, @@ -78,18 +116,22 @@ "firmware": [ { "role": "gui", - "files": [ - { - "type": "flash", - "name": "RippleUltra-T5-epaper-v6.0-beta21-merged.bin", - "title": "Combined app+partition+bootloader firmware bin" - }, - { - "type": "download", - "name": "RippleUltra-T5-epaper-v6.0-beta21.bin", - "title": "App firmware bin (use with m5 booloader)" + "version": { + "v6.0-beta22": { + "files": [ + { + "type": "flash", + "name": "RippleUltra-T5-epaper-v6.0-beta21-merged.bin", + "title": "Combined app+partition+bootloader firmware bin" + }, + { + "type": "download", + "name": "RippleUltra-T5-epaper-v6.0-beta21.bin", + "title": "App firmware bin (use with m5 booloader)" + } + ] } - ] + } } ] }, @@ -100,33 +142,110 @@ "firmware": [ { "role": "companionUsb", - "files": [ - { - "type": "flash", - "name": "LilyGo_T3S3_sx1262_companion_radio_usb.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "companion", + "files": { + "flash": ["LilyGo_T3S3_sx1262_companion_radio_usb","merged.bin"] } - ] + } }, { "role": "companionBle", - "files": [ - { - "type": "flash", - "name": "LilyGo_T3S3_sx1262_companion_radio_ble.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "companion", + "files": { + "flash": ["LilyGo_T3S3_sx1262_companion_radio_ble","merged.bin"] } - ] + } }, { "role": "repeater", - "files": [ - { - "type": "flash", - "name": "LilyGo_T3S3_sx1262_Repeater.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "repeater", + "files": { + "flash": ["LilyGo_T3S3_sx1262_Repeater","merged.bin"] } - ] + } + } + ] + }, + { + "name": "Lilygo T-Echo", + "type": "nrf52", + "tooltip": "", + "firmware": [ + { + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["LilyGo_T-Echo_companion_radio_ble","zip"], + "download": ["LilyGo_T-Echo_companion_radio_ble","uf2"] + } + } + }, + { + "role": "repeater", + "github": { + "type": "repeater", + "files": { + "flash": ["LilyGo_T-Echo_repeater","zip"], + "download": ["LilyGo_T-Echo_repeater","uf2"] + } + } + }, + { + "role": "roomServer", + "github": { + "type": "room-server", + "files": { + "flash": ["LilyGo_T-Echo_room_server","zip"], + "download": ["LilyGo_T-Echo_room_server","uf2"] + } + } + } + ] + }, + { + "name": "Lilygo LoRa32 V2.1_1.6", + "type": "esp32", + "tooltip": "", + "firmware": [ + { + "role": "companionUsb", + "github": { + "type": "companion", + "files": { + "flash": ["LilyGo_TLora_V2_1_1_6_companion_radio_usb","merged.bin"] + } + } + }, + { + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["LilyGo_TLora_V2_1_1_6_companion_radio_ble","merged.bin"] + } + } + }, + { + "role": "repeater", + "github": { + "type": "repeater", + "files": { + "flash": ["LilyGo_TLora_V2_1_1_6_Repeater","merged.bin"] + } + } + }, + { + "role": "roomServer", + "github": { + "type": "room-server", + "files": { + "flash": ["LilyGo_TLora_V2_1_1_6_room_server","merged.bin"] + } + } } ] }, @@ -137,23 +256,30 @@ "firmware": [ { "role": "companionUsb", - "files": [ - { - "type": "flash", - "name": "Heltec_v2_companion_radio_usb.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_v2_companion_radio_usb","merged.bin"] } - ] + } + }, + { + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_v2_companion_radio_ble","merged.bin"] + } + } }, { "role": "repeater", - "files": [ - { - "type": "flash", - "name": "Heltec_v2_repeater.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "repeater", + "files": { + "flash": ["Heltec_v2_repeater","merged.bin"] } - ] + } } ] }, @@ -164,43 +290,39 @@ "firmware": [ { "role": "companionUsb", - "files": [ - { - "type": "flash", - "name": "Heltec_v3_companion_radio_usb.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_v3_companion_radio_usb","merged.bin"] } - ] + } }, { "role": "companionBle", - "files": [ - { - "type": "flash", - "name": "Heltec_v3_companion_radio_ble.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_v3_companion_radio_ble","merged.bin"] } - ] + } }, { "role": "repeater", - "files": [ - { - "type": "flash", - "name": "Heltec_v3_repeater.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "repeater", + "files": { + "flash": ["Heltec_v3_repeater","merged.bin"] } - ] + } }, { "role": "roomServer", - "files": [ - { - "type": "flash", - "name": "Heltec_v3_room_server.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "room-server", + "files": { + "flash": ["Heltec_v3_room_server","merged.bin"] } - ] + } } ] }, @@ -210,34 +332,50 @@ "tooltip": "", "firmware": [ { - "role": "repeater", - "files": [ - { - "type": "flash", - "name": "Heltec_T114_repeater.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "Heltec_T114_repeater.uf2", - "title": "firmware uf2" + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_t114_companion_radio_ble","zip"], + "download": ["Heltec_t114_companion_radio_ble","uf2"] } - ] + } + }, + { + "role": "repeater", + "github": { + "type": "repeater", + "files": { + "flash": ["Heltec_t114_repeater","zip"], + "download": ["Heltec_t114_repeater","uf2"] + } + } }, { "role": "roomServer", - "files": [ - { - "type": "flash", - "name": "Heltec_T114_room_server.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "Heltec_T114_room_server.uf2", - "title": "firmware uf2" + "github": { + "type": "room-server", + "files": { + "flash": ["Heltec_t114_room_server","zip"], + "download": ["Heltec_t114_room_server","uf2"] } - ] + } + } + ] + }, + { + "name": "Heltec WSL3", + "type": "esp32", + "tooltip": "", + "firmware": [ + { + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["Heltec_WSL3_companion_radio_ble","merged.bin"] + } + } } ] }, @@ -248,63 +386,43 @@ "firmware": [ { "role": "companionUsb", - "files": [ - { - "type": "flash", - "name": "RAK_4631_companion_radio_usb.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "RAK_4631_companion_radio_usb.uf2", - "title": "firmware uf2" + "github": { + "type": "companion", + "files": { + "flash": ["RAK_4631_companion_radio_usb", "zip"], + "download": ["RAK_4631_companion_radio_usb", "uf2"] } - ] + } }, { "role": "companionBle", - "files": [ - { - "type": "flash", - "name": "RAK_4631_companion_radio_ble.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "RAK_4631_companion_radio_ble.uf2", - "title": "firmware uf2" + "github": { + "type": "companion", + "files": { + "flash": ["RAK_4631_companion_radio_ble", "zip"], + "download": ["RAK_4631_companion_radio_ble", "uf2"] } - ] + } }, { "role": "repeater", - "files": [ - { - "type": "flash", - "name": "RAK_4631_Repeater.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "RAK_4631_Repeater.uf2", - "title": "firmware uf2" + "github": { + "type": "repeater", + "files": { + "flash": ["RAK_4631_Repeater", "zip"], + "download": ["RAK_4631_Repeater", "uf2"] } - ] + } }, { "role": "roomServer", - "files": [ - { - "type": "flash", - "name": "RAK_4631_room_server.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "RAK_4631_room_server.uf2", - "title": "firmware uf2" + "github": { + "type": "room-server", + "files": { + "flash": ["RAK_4631_room_server", "zip"], + "download": ["RAK_4631_room_server", "uf2"] } - ] + } } ] }, @@ -315,18 +433,13 @@ "firmware": [ { "role": "companionBle", - "files": [ - { - "type": "flash", - "name": "Seeed_T1000e_companion_radio_ble.zip", - "title": "firmware OTA zip" - }, - { - "type": "download", - "name": "Seeed_T1000e_companion_radio_ble.uf2", - "title": "firmware uf2" + "github": { + "type": "companion", + "files": { + "flash": ["t1000e_companion_radio_ble", "zip"], + "download": ["t1000e_companion_radio_ble", "uf2"] } - ] + } } ] }, @@ -337,25 +450,23 @@ "firmware": [ { "role": "repeater", - "title": "Repeater (Semtech SX1262)", - "files": [ - { - "type": "flash", - "name": "Xiao_C3_Repeater_sx1262.bin", - "title": "Combined app+partition+bootloader firmware bin" + "subTitle": "(sx1262 version)", + "github": { + "type": "repeater", + "files": { + "flash": ["Xiao_C3_Repeater_sx1262","merged.bin"] } - ] + } }, { "role": "repeater", - "title": "Repeater (Semtech SX1268)", - "files": [ - { - "type": "flash", - "name": "Xiao_C3_Repeater_sx1268.bin", - "title": "Combined app+partition+bootloader firmware bin" + "subTitle": "(sx1268 version)", + "github": { + "type": "repeater", + "files": { + "flash": ["Xiao_C3_Repeater_sx1268","merged.bin"] } - ] + } } ] }, @@ -365,14 +476,22 @@ "type": "esp32", "firmware": [ { - "role": "repeater", - "files": [ - { - "type": "flash", - "name": "Xiao_S3_WIO_Repeater.bin", - "title": "Combined app+partition+bootloader firmware bin" + "role": "companionBle", + "github": { + "type": "companion", + "files": { + "flash": ["Xiao_S3_WIO_companion_radio_ble","merged.bin"] } - ] + } + }, + { + "role": "repeater", + "github": { + "type": "repeater", + "files": { + "flash": ["Xiao_S3_WIO_Repeater","merged.bin"] + } + } } ] }, @@ -383,13 +502,21 @@ "firmware": [ { "role": "repeater", - "files": [ - { - "type": "flash", - "name": "Station_G2_repeater.bin", - "title": "Combined app+partition+bootloader firmware bin" + "github": { + "type": "repeater", + "files": { + "flash": ["Station_G2_repeater", "merged.bin"] } - ] + } + }, + { + "role": "roomServer", + "github": { + "type": "room-server", + "files": { + "flash": ["Station_G2_room_server", "merged.bin"] + } + } } ] } diff --git a/css/flasher.css b/css/flasher.css index 7e35a9f..bd9a074 100644 --- a/css/flasher.css +++ b/css/flasher.css @@ -1,40 +1,47 @@ +[v-cloak] { + display: none; +} + body { display: flex; height: 100vh; } -#flasher { +#app { flex-grow: 1; } -#flasher img.device { +#app select { + cursor: pointer +} +#app img.device { width: 300px; } -#flasher div.autoscroller { +#app div.autoscroller { overflow: auto; max-height: 300px; display: flex; flex-direction: column-reverse; } -#flasher pre.term { +#app pre.term { font-family: monospace; } -#flasher .overlay { +#app .overlay { display: flex !important; flex-direction: column; } -#flasher .console { +#app .console { overflow: auto; display: flex; flex-direction: column; flex-grow: 1; margin: 0; } -#flasher .console .holder { +#app .console .holder { display: flex; flex-direction: row; gap: 10px; } -#flasher .console-input { +#app .console-input { flex-grow: 1; display: block; appearance: none; @@ -43,6 +50,6 @@ body { font-family: monospace; font-size: .875rem; } -#flasher .console-input:focus, #flasher console-input:focus{ +#app .console-input:focus, #app console-input:focus{ outline: none; } \ No newline at end of file diff --git a/flasher.js b/flasher.js index cf63565..a6b20f1 100644 --- a/flasher.js +++ b/flasher.js @@ -1,13 +1,16 @@ import "./lib/beer.min.js"; -import { createApp, reactive, ref, nextTick } from "./lib/vue.min.js"; +import { createApp, reactive, ref, nextTick, watch, computed } from "./lib/vue.min.js"; import { Dfu } from "./lib/dfu.js"; import { ESPLoader, Transport, HardReset } from "./lib/esp32.js"; import { SerialConsole } from './lib/console.js'; -const res = await fetch('./config.json'); -const config = await res.json(); +const configRes = await fetch('./config.json'); +const config = await configRes.json(); + +const githubRes = await fetch('/releases'); +const github = await githubRes.json(); + const commandReference = { - 'set freq ': 'Set frequency {Mhz}', 'time ': 'Set time {epoch-secs}', 'erase': 'Erase filesystem', 'advert': 'Send Advertisment packet', @@ -19,6 +22,7 @@ const commandReference = { 'log stop': 'Stop packet logging to file system', 'log erase': 'Erase the packet logs from file system', 'ver': 'Show device version', + 'set freq ': 'Set frequency {Mhz}', 'set af ': 'Set Air-time factor', 'set tx ': 'Set Tx power {dBm}', 'set repeat ': 'Set repeater mode {on|off}', @@ -27,8 +31,53 @@ const commandReference = { 'set name ': 'Set advertisement name', 'set lat': 'Set the advertisement map latitude', 'set lon': 'Set the advertisement map longitude', + 'get freq ': 'Get frequency (Mhz)', + 'get af': 'Get Air-time factor', + 'get tx': 'Get Tx power (dBm)', + 'get repeat': 'Get repeater mode', + 'get advert.interval': 'Get advert rebroadcast interval (minutes)', + 'get name': 'Get advertisement name', + 'get lat': 'Get the advertisement map latitude', + 'get lon': 'Get the advertisement map longitude', }; +function getGithubReleases(roleType, files) { + const versions = {}; + for(const [fileType, [startsWith, endsWith]] of Object.entries(files)) { + for(const versionType of github) { + if(versionType.type !== roleType) { continue } + const version = versions[versionType.version] ??= { + notes: versionType.notes, + files: [] + }; + for(const file of versionType.files) { + if(!(file.name.startsWith(startsWith) && file.name.endsWith(endsWith))) { continue } + version.files.push({ + type: fileType, + name: file.url, + title: file.name, + }) + } + } + } + + return versions; +} + +function addGithubFiles() { + for(const device of config.device) { + for(const firmware of device.firmware) { + const gDef = firmware.github; + if(!gDef?.files) { continue } + firmware.version = getGithubReleases(gDef.type, gDef.files); + } + } + + return config; +} + +console.log(addGithubFiles()); + function setup() { const consoleEditBox = ref(); const consoleWindow = ref(); @@ -36,11 +85,23 @@ function setup() { const selected = reactive({ device: null, firmware: null, + version: null, wipe: false, - port: null + port: null, }); + const getRoleFwValue = (firmware, key) => { + return firmware[key] || config.role[firmware.role][key] || ''; + } + + const getSelFwValue = (key) => { + const fwVersion = selected.firmware.version[selected.version]; + + return fwVersion ? fwVersion[key] || '' : ''; + } + const flashing = reactive({ + supported: 'Serial' in window, instance: null, active: false, percentage: 0, @@ -68,14 +129,47 @@ function setup() { location.reload(); } + const getFirmwarePath = (file) => { + return file.name.startsWith('/') ? file.name : `${config.staticPath}/${file.name}`; + } + + const firmwareHasData = (firmware) => { + const firstVersion = Object.keys(firmware.version)[0]; + if(!firstVersion) return false; + + return firmware.version[firstVersion].files.length > 0; + } + + const stepBack = () => { + if(selected.device && selected.firmware) { + if(selected.firmware.version[selected.version].customFile) { + selected.firmware = null; + selected.device = null; + return + } + + selected.firmware = null; + return; + } + + if(selected.device) { + selected.device = null; + } + } + + watch(() => selected.firmware, (firmware) => { + if(firmware == null) return; + selected.version = Object.keys(firmware.version)[0]; + }); + const flasherCleanup = async () => { - const port = selected.port; flashing.active = false; flashing.log = ''; flashing.error = ''; flashing.dfuComplete = false; flashing.percentage = 0; selected.firmware = null; + selected.version = null; selected.wipe = false; selected.device = null; if(flashing.instance instanceof ESPLoader) { @@ -118,17 +212,49 @@ function setup() { flashing.dfuComplete = true; } + const customFirmwareLoad = async(ev) => { + const firmwareFile = ev.target.files[0]; + const type = firmwareFile.name.endsWith('.bin') ? 'esp32' : 'nrf52'; + selected.device = { + name: 'Custom device', + type, + }; + + selected.firmware = { + icon: 'unknown_document', + title: firmwareFile.name, + version: {}, + } + selected.version = firmwareFile.name; + selected.firmware.version[selected.version] = { + customFile: true, + files: [{ type: 'flash', file: firmwareFile }] + } + } + const flashDevice = async() => { const device = selected.device; - const firmware = selected.firmware; - const flashFile = firmware.files.find(f => f.type === 'flash'); + const firmware = selected.firmware.version[selected.version]; + let flashFile; + + flashFile = firmware.files.find(f => f.type === 'flash'); if(!flashFile) { alert('Cannot find configuration for flash file! please report this to Discord.') flasherCleanup(); return; } - const url = `${config.basePath}/${flashFile.name}`; - const resp = await fetch(url); + + console.log({flashFile, instanceFile: flashFile instanceof File}); + + if(flashFile.file) { + flashFile = flashFile.file; + } else { + const url = getFirmwarePath(flashFile); + console.log('downloading: ' + url); + const resp = await fetch(url); + flashFile = await resp.blob(); + } + const port = selected.port = await navigator.serial.requestPort({}); if(device.type === 'esp32') { @@ -138,9 +264,15 @@ function setup() { try { const reader = new FileReader(); - fileData = await new Promise(async (resolve) => { + fileData = await new Promise((resolve, reject) => { + reader.addEventListener('error', () => { + reader.abort(); + reject(new DOMException('Problem parsing input file.')); + }); + reader.addEventListener('load', () => resolve(reader.result)); - reader.readAsBinaryString(await resp.blob()); + + reader.readAsBinaryString(flashFile); }); } catch(e) { @@ -163,13 +295,8 @@ function setup() { data: fileData, address: 0 }], - reportProgress: async (fileIndex, written, total) => { + reportProgress: async (_, written, total) => { flashing.percentage = (written / total) * 100; - - // we're done with this file - if (written === total) { - return; - } }, }; @@ -204,11 +331,10 @@ function setup() { else if(device.type === 'nrf52') { const dfu = flashing.instance = new Dfu(port, selected.wipe); - const zipFile = await resp.blob(); flashing.active = true; try { - await dfu.dfuUpdate(zipFile, async (progress) => { + await dfu.dfuUpdate(flashFile, async (progress) => { flashing.percentage = progress; }); } @@ -225,173 +351,11 @@ function setup() { config, selected, flashing, flashDevice, flasherCleanup, dfuMode, serialCon, openSerialCon, sendCommand, closeSerialCon, - refresh, commandReference + refresh, commandReference, + stepBack, + customFirmwareLoad, getFirmwarePath, getSelFwValue, getRoleFwValue, + firmwareHasData } } -const template = ` -
-
-
- -
-
-
-
-
Flashing failed!
-

{{ flashing.error }}

-

-
-
-
-
-
-
-
Flashing...
-

Please do not disconnect the device

-
-
-
Flashing complete!
-

- -

-
-
-
-
{{ flashing.terminal }}
-
- -
-
-
-
- - -
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
- - - -
- -
-
-    {{ serialCon.content }}
-    
- > - -
-
-
-`; - -createApp({ setup, template }).mount('#flasher'); +createApp({ setup }).mount('#app'); diff --git a/img/heltec_wsl3.png b/img/heltec_wsl3.png new file mode 100644 index 0000000..34c5c15 Binary files /dev/null and b/img/heltec_wsl3.png differ diff --git a/img/lilygo_techo.png b/img/lilygo_techo.png new file mode 100644 index 0000000..3ca6a1b Binary files /dev/null and b/img/lilygo_techo.png differ diff --git a/index.html b/index.html index 40548b9..6570326 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,204 @@ -
+
+
+
+
+ +
+
+
+
+
Flashing failed!
+

{{ flashing.error }}

+

+
+
+
+
+
+
+
Flashing...
+

Please do not disconnect the device

+
+
+
Flashing complete!
+

+ +

+
+
+
+
{{ flashing.terminal }}
+
+ +
+
+
+
+ + +
+
    +
  • + +
  • +
+ +
+ +
+
+
+ + +
+
    + +
+
+
+
+ + +
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+ + + +
+ +
+
+        {{ serialCon.content }}
+        
+ > + +
+
+
+ +
\ No newline at end of file