Initial commit
This commit is contained in:
commit
a892dfb3ba
27 changed files with 6607 additions and 0 deletions
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"template": "nuxt"
|
||||
}
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.data
|
||||
.env
|
||||
dist
|
||||
public/firmware_v3
|
40
README.md
Normal file
40
README.md
Normal 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
129
app.vue
Normal 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>
|
||||
|
||||
<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
20
assets/css/main.scss
Normal 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;
|
||||
}
|
79
components/AdvancedSettings.vue
Normal file
79
components/AdvancedSettings.vue
Normal 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>
|
||||
|
||||
<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>
|
69
components/DeviceSelector.vue
Normal file
69
components/DeviceSelector.vue
Normal 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>
|
16
components/FlashProgress.vue
Normal file
16
components/FlashProgress.vue
Normal 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>
|
48
components/VersionInformation.vue
Normal file
48
components/VersionInformation.vue
Normal 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>
|
161
composables/useEspFlasher.ts
Normal file
161
composables/useEspFlasher.ts
Normal 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
|
||||
};
|
||||
};
|
29
composables/useManifest.ts
Normal file
29
composables/useManifest.ts
Normal 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
20
composables/useTheme.ts
Normal 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
60
get_latest.sh
Executable 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
21
nuxt.config.ts
Normal 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
27
package.json
Normal 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"
|
||||
}
|
||||
}
|
17
public/btclock_rev_b-213epd.json
Normal file
17
public/btclock_rev_b-213epd.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
17
public/lolin_s3_mini-213epd.json
Normal file
17
public/lolin_s3_mini-213epd.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
17
public/lolin_s3_mini-29epd.json
Normal file
17
public/lolin_s3_mini-29epd.json
Normal 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
BIN
public/rev_b.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
public/rev_b.xcf
Normal file
BIN
public/rev_b.xcf
Normal file
Binary file not shown.
1
public/robots.txt
Normal file
1
public/robots.txt
Normal file
|
@ -0,0 +1 @@
|
|||
|
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
16
tailwind.config.ts
Normal file
16
tailwind.config.ts
Normal 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
4
tsconfig.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// https://v3.nuxtjs.org/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
16
types/manifest.ts
Normal file
16
types/manifest.ts
Normal 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
63
utils/SerialTransport.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue