Initial commit

This commit is contained in:
Djuri Baars 2024-12-07 17:48:13 +01:00
commit a892dfb3ba
27 changed files with 6607 additions and 0 deletions

3
.bolt/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"template": "nuxt"
}

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.data
.env
dist
public/firmware_v3

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# BTClock Web Flasher
Powered by Nuxt, Vue and esptool-js
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install
```
## Development Server
Start the development server on http://localhost:3000
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```

129
app.vue Normal file
View file

@ -0,0 +1,129 @@
<script setup lang="ts">
// import pkg from '@xterm/xterm';
// const { Terminal } = pkg;
import { Terminal } from '@xterm/xterm';
// const { Terminal } = pkg;
const term = new Terminal({ rows: 15 });
const {
isConnected,
flashProgress,
status,
error,
connect,
flash,
disconnect,
reset
} = useEspFlasher(term);
const { theme } = useTheme();
const { getManifest } = useManifest();
const eraseFlash = ref(false);
const selectedDevice = ref();
const advancedSettings = ref({
hasFrontlight: false,
displayColors: 'black-on-white'
});
const startFlashing = async () => {
const manifest = await getManifest(
selectedDevice.value,
advancedSettings.value.customize,
advancedSettings.value.hasFrontlight,
advancedSettings.value.displayColors
);
await flash(manifest, eraseFlash.value);
};
const webSerialSupport = ref(false)
onMounted(() => {
term.open(document.getElementById('xterm-terminal'));
// const userAgent = navigator.userAgent.toLowerCase()
// const isChromium = /chrome|chromium|crios|edge/i.test(userAgent)
webSerialSupport.value = 'serial' in navigator
})
</script>
<template>
<div class="container mx-auto p-4">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2">BTClock Web Flasher</h1>
<p class="text-gray-600 dark:text-gray-400">Flash your BTClock directly from the browser</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<DeviceSelector @update:device="selectedDevice = $event" class="row-span-2" />
<div class="card bg-base-100 shadow-xl ">
<div class="card-body">
<h2 class="card-title">Connection Status</h2>
<FlashProgress :progress="flashProgress" />
<div v-if="error" class="alert alert-error mt-4">
{{ error }}
</div>
<div class="card-actions justify-stretch place-items-center" v-if="webSerialSupport">
<div class="justify-start grow">{{ status || 'Not connected' }}</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Erase flash</span>
&nbsp;
<input type="checkbox" v-model="eraseFlash" class="checkbox" />
</label>
</div>
<button class="btn btn-primary" @click="startFlashing" v-if="isConnected"
:disabled="!isConnected || (flashProgress != 0 && flashProgress != 100) || selectedDevice === undefined">
Start Flashing
</button>
<button class="btn btn-primary" @click="isConnected ? disconnect() : connect()"
:class="{ 'btn-success': isConnected }">
{{ isConnected ? 'Disconnect' : 'Connect' }}
</button>
<button v-if="isConnected" class="btn btn-secondary" @click="() => { reset() }">
Reset
</button>
</div>
<div v-else>
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>The flasher is not available because your browser does not support Web Serial.<br>Open this page in Google Chrome, Chromium, Brave or Microsoft Edge instead.</span>
</div>
</div>
</div>
</div>
<VersionInformation></VersionInformation>
<AdvancedSettings @update:settings="advancedSettings = $event" />
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Console</h2>
<div id="xterm-terminal"></div>
</div>
</div>
</div>
</div>
</template>

20
assets/css/main.scss Normal file
View file

@ -0,0 +1,20 @@
@tailwind base;
@layer base {
p {
@apply mb-2;
}
a {
@apply link link-primary;
}
}
@tailwind components;
@tailwind utilities;
@import "@xterm/xterm/css/xterm.css";
body {
@apply min-h-screen bg-base-200;
}

View file

@ -0,0 +1,79 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="">
<div class="flex justify-between items-center">
<h2 class="card-title">Customize settings</h2>
<!-- <button class="btn btn-ghost btn-sm" @click="isExpanded = !isExpanded">
<span class="text-lg">{{ isExpanded ? '↑' : '↓' }}</span>
</button> -->
<div class="form-control">
<label class="label cursor-pointer p-0">
<span class="label-text">Enable</span>
&nbsp;
<input type="checkbox" v-model="localSettings.customize" class="checkbox" />
</label>
</div>
</div>
</div>
<div role="alert" class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-xs">If you customize settings, all existing settings including WiFi credentials will be overwritten. This is only
recommended for initial flashing.</span>
</div>
<div v-if="isExpanded" class="space-y-4 mt-4">
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Has Frontlight <small>Only Rev. B</small></span>
<input type="checkbox" class="toggle toggle-primary" :disabled="!localSettings.customize"
v-model="localSettings.hasFrontlight" @change="updateSettings" />
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Display options</span>
</label>
<div class="space-y-2 ml-2">
<label class="label cursor-pointer">
<span class="label-text">White text on Black background</span>
<input type="radio" name="display-colors" class="radio radio-primary" value="white-on-black"
v-model="localSettings.displayColors" :disabled="!localSettings.customize" @change="updateSettings" />
</label>
<label class="label cursor-pointer">
<span class="label-text">Black text on White background</span>
<input type="radio" name="display-colors" class="radio radio-primary" value="black-on-white"
:disabled="!localSettings.customize" v-model="localSettings.displayColors" @change="updateSettings" />
</label>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Settings {
hasFrontlight: boolean;
displayColors: string;
customize: boolean,
}
const emit = defineEmits(['update:settings']);
const isExpanded = ref(true);
const localSettings = reactive<Settings>({
customize: false,
hasFrontlight: false,
displayColors: 'black-on-white'
});
const updateSettings = () => {
emit('update:settings', { ...toRaw(localSettings) });
};
</script>

View file

@ -0,0 +1,69 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="">
<h2 class="card-title">Device Selection</h2>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Rev. A (2.13 inch)</span>
<input
type="radio"
name="device-type"
class="radio"
value="lolin_s3_mini-213epd"
v-model="selectedDevice"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Rev. A (2.9 inch) <small>unsupported</small></span>
<input
type="radio"
name="device-type"
class="radio"
value="lolin_s3_mini-29epd"
v-model="selectedDevice"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Rev. B (2.13 inch)</span>
<input
type="radio"
name="device-type"
class="radio"
value="btclock_rev_b-213epd"
v-model="selectedDevice"
/>
</label>
</div>
<div class="w-full md:flex hidden">
<div class=" grid flex-grow place-items-center">
<img src="/rev_b.png" class="max-w-none">
</div>
<div class="grid flex-grow p-5 text-sm">
<p>If you are unsure about which version you have, check the back of the BTClock.</p>
<p>The Rev. B has "Rev. B" written on the backside and two buttons on the back (Reset and Boot).</p>
<small>All versions before block #841273 (2024-04-28) are rev. A.<br>The 2.9 inch version is offered as a courtesy, you most likely have the 2.13 inch version.</small>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const selectedDevice = ref();
const emit = defineEmits<{
(e: 'update:device', value: string): void;
}>();
watch(selectedDevice, (newValue) => {
emit('update:device', newValue);
});
</script>

View file

@ -0,0 +1,16 @@
<template>
<div class="w-full">
<progress
class="progress progress-primary w-full"
:value="progress"
max="100"
></progress>
<div class="text-center mt-2">{{ progress }}%</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
progress: number;
}>();
</script>

View file

@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const version = ref<string>('')
const commitHash = ref<string>('')
const buildDate = ref<string>('')
const fetchFirmwareData = async () => {
try {
const [tagResponse, commitResponse, dateResponse] = await Promise.all([
fetch('firmware_v3/tag.txt'),
fetch('firmware_v3/commit.txt'),
fetch('firmware_v3/date.txt')
])
version.value = await tagResponse.text()
commitHash.value = await commitResponse.text()
const dateText = await dateResponse.text()
buildDate.value = new Date(dateText.trim()).toLocaleString()
} catch (error) {
console.error('Error fetching firmware data:', error)
}
}
onMounted(() => {
fetchFirmwareData()
})
</script>
<template>
<div class="card bg-base-100 shadow-xl ">
<div class="card-body">
<h2 class="card-title">Version information</h2>
<ul class="text-sm">
<li>Version {{ version }}</li>
<li>Commit hash: <a :href="'https://git.btclock.dev/btclock/btclock_v3/commit/' + commitHash "><code>{{ commitHash }}</code></a></li>
<li>Build date: <code>{{ buildDate }}</code></li>
<li><a href="https://git.btclock.dev/btclock/btclock_v3/src/branch/main/.forgejo/workflows/push.yaml">CI
script
compiling the
V3
firmware</a> | <a href="https://git.btclock.dev/btclock/btclock_v3/releases/latest">Release to be flashed</a> |
<a href="https://git.btclock.dev/btclock/btclock_v3">git repository</a></li>
</ul>
</div>
</div>
</template>

View file

@ -0,0 +1,161 @@
import { ESPLoader, Transport, type FlashOptions } from 'esptool-js';
import { serial } from "web-serial-polyfill";
export const useEspFlasher = (term) => {
const isConnected = ref(false);
const flashProgress = ref(0);
const status = ref('');
const error = ref('');
const espLoaderTerminal = {
clean() {
term.clear();
},
writeLine(data) {
term.writeln(data);
},
write(data) {
term.write(data);
},
};
let port = null;
let espLoader: ESPLoader = null;
let transport: Transport;
const SERIAL_FILTERS: SerialPortFilter[] = [
{ usbVendorId: 0x1a86 }, // QinHeng Electronics CH340
{ usbVendorId: 0x303a } // Espressif USB JTAG/serial debug unit
];
const connect = async () => {
try {
if (transport) {
await disconnect();
}
const serialLib = !navigator.serial && navigator.usb ? serial : navigator.serial;
if (port === null) {
port = await serialLib.requestPort({
filters: SERIAL_FILTERS
});
// await port.open({ baudRate: 115200 });
transport = new Transport(port, true);
}
espLoader = new ESPLoader({
transport,
baudrate: 115200,
terminal: espLoaderTerminal,
logger: (message: string) => {
status.value = "LOG: " + message;
}
});
// await espLoader.connect();
// await espLoader.sync();
const chipInfo = await espLoader.main();
status.value = `Connected to ${chipInfo}`;
// await espLoader.loadStub();
isConnected.value = true;
error.value = '';
} catch (err: any) {
error.value = err.message;
isConnected.value = false;
await disconnect();
}
};
const flash = async (manifest: FirmwareManifest, eraseFlash: boolean) => {
if (!espLoader || !isConnected.value) {
error.value = 'Not connected to device';
return;
}
try {
const build = manifest.builds[0];
if (eraseFlash) {
status.value = 'Erasing flash...';
await espLoader.eraseFlash();
}
const fileArray = [];
status.value = `Flashing ${manifest.name}...`;
for (const part of build.parts) {
const response = await fetch(part.path);
const uint8Array = new Uint8Array(await response.arrayBuffer());
const buffer = Array.from(uint8Array)
.map(byte => String.fromCharCode(byte))
.join('');
fileArray.push({ data: buffer, address: part.offset });
}
const flashOptions: FlashOptions = {
fileArray: fileArray,
flashSize: "keep",
eraseAll: false,
compress: true,
reportProgress: (fileIndex, written, total) => {
flashProgress.value = Math.round((written / total) * 100);
},
} as FlashOptions;
await espLoader.writeFlash(
flashOptions
);
status.value = 'Flash complete!';
flashProgress.value = 100;
espLoader.hardReset();
} catch (err: any) {
error.value = err.message;
}
};
const disconnect = async () => {
if (transport) {
await transport.disconnect();
transport = null;
}
if (espLoader) {
espLoader = null;
}
isConnected.value = false;
status.value = '';
port = null;
flashProgress.value = 0;
};
const reset = async() => {
// console.log(transport)
// // if (transport) {
// await transport.setDTR(false);
// await new Promise((resolve) => setTimeout(resolve, 100));
// await transport.setDTR(true);
// //}
await espLoader.hardReset();
await disconnect();
}
return {
isConnected,
flashProgress,
status,
error,
connect,
flash,
disconnect,
reset
};
};

View file

@ -0,0 +1,29 @@
import type { FirmwareManifest } from '~/types/manifest';
export const useManifest = () => {
const getManifest = async(
deviceType: string,
customize: boolean,
hasFrontlight: boolean,
displayColors: string
): FirmwareManifest => {
const response = await fetch(`/${deviceType}.json`);
const baseManifest: FirmwareManifest = await response.json();
if (customize) {
// Add NVS partition based on settings
const nvsVariant = `${hasFrontlight ? 'frontlight_' : ''}${displayColors}`;
baseManifest.builds[0].parts.push({
path: `firmware_v3/nvs/${nvsVariant}.bin`,
offset: "0x9000"
});
}
return baseManifest;
};
return {
getManifest
};
};

20
composables/useTheme.ts Normal file
View file

@ -0,0 +1,20 @@
export const useTheme = () => {
const theme = ref('light');
onMounted(() => {
// Check system preference
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = (e: MediaQueryListEvent | MediaQueryList) => {
theme.value = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme.value);
};
darkModeMediaQuery.addEventListener('change', updateTheme);
updateTheme(darkModeMediaQuery);
});
return {
theme
};
};

60
get_latest.sh Executable file
View file

@ -0,0 +1,60 @@
# Fetch the latest release data
release_data=$(curl -s "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases" | jq '.[0]')
# Extract and write published_at date
echo $release_data | jq -r '.published_at' > public/firmware_v3/date.txt
# Get the tag name
tag_name=$(echo $release_data | jq -r '.tag_name')
# Fetch the commit hash from the tag's API endpoint
commit_hash=$(curl -s "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/tags/$tag_name" | jq -r '.commit.sha')
# Write the commit hash to commit.sh
echo $commit_hash > public/firmware_v3/commit.txt
echo $tag_name > public/firmware_v3/tag.txt
cd public/
# Download and distribute littlefs files
littlefs_url=$(echo $release_data | jq -r '.assets[] | select(.name=="littlefs.bin") | .browser_download_url')
littlefs_sha_url=$(echo $release_data | jq -r '.assets[] | select(.name=="littlefs.bin.sha256") | .browser_download_url')
for dir in firmware_v3/build-btclock_rev_b-213epd firmware_v3/build-lolin_s3_mini-29epd firmware_v3/build-lolin_s3_mini-213epd; do
curl -sSL $littlefs_url -o "$dir/littlefs.bin"
curl -sSL $littlefs_sha_url -o "$dir/littlefs.bin.sha256"
done
# Function to download and distribute board-specific files
download_board_files() {
local board=$1
local dir=$2
local files=("${@:3}")
for file in "${files[@]}"; do
url=$(echo $release_data | jq -r ".assets[] | select(.name==\"$file\") | .browser_download_url")
echo "Downloading $url to $dir/$file"
curl -sSL -o "$dir/$file" $url
done
}
# Download btclock_rev_b_213epd files
download_board_files "btclock_rev_b_213epd" "firmware_v3/build-btclock_rev_b-213epd" \
"btclock_rev_b_213epd.bin" \
"btclock_rev_b_213epd.bin.sha256" \
"btclock_rev_b_213epd_firmware.bin" \
"btclock_rev_b_213epd_firmware.bin.sha256"
# Download lolin_s3_mini_29epd files
download_board_files "lolin_s3_mini_29epd" "firmware_v3/build-lolin_s3_mini-29epd" \
"lolin_s3_mini_29epd.bin" \
"lolin_s3_mini_29epd.bin.sha256" \
"lolin_s3_mini_29epd_firmware.bin" \
"lolin_s3_mini_29epd_firmware.bin.sha256"
# Download lolin_s3_mini_213epd files
download_board_files "lolin_s3_mini_213epd" "firmware_v3/build-lolin_s3_mini-213epd" \
"lolin_s3_mini_213epd.bin" \
"lolin_s3_mini_213epd.bin.sha256" \
"lolin_s3_mini_213epd_firmware.bin" \
"lolin_s3_mini_213epd_firmware.bin.sha256"

21
nuxt.config.ts Normal file
View file

@ -0,0 +1,21 @@
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: false },
modules: ['@nuxtjs/tailwindcss'],
css: ['~/assets/css/main.scss'],
app: {
head: {
title: 'BTClock Web Flasher',
meta: [
{ charset: 'utf-8' },
// { name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
},
components: {
dirs: [
'~/components'
]
},
ssr: false
})

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "HOST=0.0.0.0 nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@types/w3c-web-usb": "^1.0.10",
"@xterm/xterm": "^5.5.0",
"daisyui": "^4.12.14",
"esptool-js": "^0.4.7",
"nuxt": "^3.14.0",
"vue": "^3.5.13",
"web-serial-polyfill": "^1.0.15"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.2",
"@types/w3c-web-serial": "^1.0.7",
"sass": "^1.82.0",
"typescript": "^5.4.2"
}
}

View file

@ -0,0 +1,17 @@
{
"name": "BTClock (Rev. B)",
"version": "V3",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "firmware_v3/build-btclock_rev_b-213epd/bootloader.bin", "offset": "0" },
{ "path": "firmware_v3/build-btclock_rev_b-213epd/partitions.bin", "offset": "32768" },
{ "path": "firmware_v3/build-btclock_rev_b-213epd/ota_data_initial.bin", "offset": "57344" },
{ "path": "firmware_v3/build-btclock_rev_b-213epd/btclock_rev_b_213epd_firmware.bin", "offset": "65536" },
{ "path": "firmware_v3/build-btclock_rev_b-213epd/littlefs.bin", "offset": "0x388000" }
]
}
]
}

View file

@ -0,0 +1,17 @@
{
"name": "BTClock (Rev. A, 2.13 inch)",
"version": "V3",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "firmware_v3/build-lolin_s3_mini-213epd/bootloader.bin", "offset": "0" },
{ "path": "firmware_v3/build-lolin_s3_mini-213epd/partitions.bin", "offset": "32768" },
{ "path": "firmware_v3/build-lolin_s3_mini-213epd/ota_data_initial.bin", "offset": "57344" },
{ "path": "firmware_v3/build-lolin_s3_mini-213epd/lolin_s3_mini_213epd_firmware.bin", "offset": "65536" },
{ "path": "firmware_v3/build-lolin_s3_mini-213epd/littlefs.bin", "offset": "0x388000" }
]
}
]
}

View file

@ -0,0 +1,17 @@
{
"name": "BTClock (Rev. A, 2.9 inch)",
"version": "V3",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "firmware_v3/build-lolin_s3_mini-29epd/bootloader.bin", "offset": "0" },
{ "path": "firmware_v3/build-lolin_s3_mini-29epd/partitions.bin", "offset": "32768" },
{ "path": "firmware_v3/build-lolin_s3_mini-29epd/ota_data_initial.bin", "offset": "57344" },
{ "path": "firmware_v3/build-lolin_s3_mini-29epd/lolin_s3_mini_29epd_firmware.bin", "offset": "65536" },
{ "path": "firmware_v3/build-lolin_s3_mini-29epd/littlefs.bin", "offset": "0x388000" }
]
}
]
}

BIN
public/rev_b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/rev_b.xcf Normal file

Binary file not shown.

1
public/robots.txt Normal file
View file

@ -0,0 +1 @@

3
server/tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

16
tailwind.config.ts Normal file
View file

@ -0,0 +1,16 @@
import type { Config } from 'tailwindcss'
export default {
content: [],
plugins: [require('daisyui')],
daisyui: {
themes: ['light', 'dark'],
},
theme: {
extend: {
colors: {
// Add any custom colors here
}
}
}
} satisfies Config

4
tsconfig.json Normal file
View file

@ -0,0 +1,4 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

16
types/manifest.ts Normal file
View file

@ -0,0 +1,16 @@
export interface FirmwarePart {
path: string;
offset: string;
}
export interface FirmwareBuild {
chipFamily: string;
parts: FirmwarePart[];
}
export interface FirmwareManifest {
name: string;
version: string;
new_install_prompt_erase: boolean;
builds: FirmwareBuild[];
}

63
utils/SerialTransport.ts Normal file
View file

@ -0,0 +1,63 @@
export class SerialTransport {
private port: SerialPort;
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
constructor(port: SerialPort) {
this.port = port;
}
async connect() {
if (this.reader || this.writer) {
await this.disconnect();
}
const decoder = new TextDecoderStream();
const inputDone = this.port.readable!.pipeTo(decoder.writable);
const inputStream = decoder.readable;
this.reader = inputStream.getReader();
this.writer = this.port.writable!.getWriter();
}
async disconnect() {
if (this.reader) {
await this.reader.cancel();
this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.close();
this.writer.releaseLock();
this.writer = null;
}
if (this.port.readable) {
await this.port.readable.cancel();
}
}
async write(data: Uint8Array) {
if (!this.writer) {
throw new Error('Transport not connected');
}
await this.writer.write(data);
}
async read() {
if (!this.reader) {
throw new Error('Transport not connected');
}
const { value, done } = await this.reader.read();
if (done) {
throw new Error('Serial port closed');
}
return new TextEncoder().encode(value);
}
get_info() {
return {
transport: 'serial',
baud: 115200,
};
}
}

5721
yarn.lock Normal file

File diff suppressed because it is too large Load diff