- Replace VuePress 1.9.10 with VitePress 1.6.4 - Convert config from CommonJS to ESM TypeScript (config.mts) - Convert shared Theengs nav config to ESM (commonConfig.mts) - Rewrite BoardEnvironmentTable in Vue 3 Composition API - Rewrite FlashEnvironmentSelector in Vue 3 Composition API - Enable built-in local search and dark mode - Add medium-zoom for image zoom - Use VitePress CSS variables for dark mode compatibility - Update CI script paths from .vuepress to .vitepress - Update .gitignore for VitePress build artifacts - Rename README.md files to index.md (VitePress convention) - Fix markdown compatibility (angle-bracket escaping, image paths) - Drop PWA plugin (not needed) Co-authored-by: Florian <1technophile@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
350 lines
12 KiB
JavaScript
350 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Creates web installer manifests for ESP Web Tools firmware installation
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const { loadBoardsInfo, ensureDir } = require('./boards-info');
|
|
|
|
const {
|
|
mf_temp32,
|
|
mf_temp32c3,
|
|
mf_temp32s3,
|
|
mf_temp8266,
|
|
cors_proxy,
|
|
esp32_boot
|
|
} = require('./common_wu.js');
|
|
|
|
// ============================================================================
|
|
// Directory Configuration
|
|
// ============================================================================
|
|
|
|
// Base directories
|
|
const ROOT_DIR = path.join(__dirname, '..');
|
|
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
|
|
const VITEPRESS_DIR = path.join(DOCS_DIR, '.vitepress');
|
|
const PUBLIC_DIR = path.join(VITEPRESS_DIR, 'public');
|
|
const COMPONENTS_DIR = path.join(VITEPRESS_DIR, 'components');
|
|
const ARTIFACTS_DIR = path.join(ROOT_DIR, 'generated', 'artifacts');
|
|
|
|
// Feature-specific directories
|
|
const FIRMWARE_SRC_DIR = path.join(ARTIFACTS_DIR, 'firmware_build');
|
|
const FIRMWARE_BUILD_DIR = path.join(PUBLIC_DIR, 'firmware_build');
|
|
const BOARDS_INFO_FILE = path.join(PUBLIC_DIR, 'boards-info.json');
|
|
|
|
// Configuration files
|
|
const DEFAULTS_CONFIG_PATH = path.join(VITEPRESS_DIR, 'defaults.json');
|
|
const META_CONFIG_PATH = path.join(VITEPRESS_DIR, 'meta.json');
|
|
|
|
|
|
|
|
let meta = require(DEFAULTS_CONFIG_PATH);
|
|
try {
|
|
const meta_overload = require(META_CONFIG_PATH);
|
|
meta = { ...meta, ...meta_overload };
|
|
} catch (e) {
|
|
console.warn('meta.json not found or not valid. Using default configuration.');
|
|
}
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const dev = args.includes('--dev') || meta.mode === 'dev';
|
|
const repo = meta.repo || '1technophile/OpenMQTTGateway';
|
|
const firmwareManifestFolder = dev ? `/dev/firmware_build/` : `/firmware_build/`;
|
|
|
|
// ============================================================================
|
|
// Utility Functions
|
|
// ============================================================================
|
|
|
|
function ensureFirmwareArtifacts() {
|
|
if (!fs.existsSync(FIRMWARE_SRC_DIR)) {
|
|
throw new Error(`Missing firmware artifacts in ${FIRMWARE_SRC_DIR}. Run "ci.sh build ..." first to populate this folder.`);
|
|
}
|
|
console.log(`Found firmware artifacts in: ${FIRMWARE_SRC_DIR}`);
|
|
}
|
|
// Replace version_tag in template and write to destination
|
|
function renderVersionTemplate(templatePath, outputPath, version) {
|
|
if (!fs.existsSync(templatePath)) {
|
|
throw new Error(`Template not found: ${templatePath}`);
|
|
}
|
|
const content = fs.readFileSync(templatePath, 'utf8').replace(/version_tag/g, version);
|
|
fs.writeFileSync(outputPath, content);
|
|
console.log(`Generated version file from template: ${outputPath}`);
|
|
}
|
|
|
|
/**
|
|
* Download file from URL
|
|
*/
|
|
function downloadFile(url) {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(url, (response) => {
|
|
// Handle redirects
|
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
return downloadFile(response.headers.location).then(resolve).catch(reject);
|
|
}
|
|
if (response.statusCode !== 200) {
|
|
return reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
}
|
|
const chunks = [];
|
|
response.on('data', (chunk) => chunks.push(chunk));
|
|
response.on('end', () => resolve(Buffer.concat(chunks)));
|
|
response.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch JSON from URL
|
|
*/
|
|
function fetchJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(url, { headers: { 'User-Agent': 'OpenMQTTGateway-Script' } }, (response) => {
|
|
if (response.statusCode !== 200) {
|
|
return reject(new Error(`Failed to fetch: ${response.statusCode}`));
|
|
}
|
|
let data = '';
|
|
response.on('data', (chunk) => data += chunk);
|
|
response.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
response.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Download and save asset
|
|
*/
|
|
async function downloadAsset(asset, destPath) {
|
|
const buffer = await downloadFile(asset.browser_download_url);
|
|
const filename = asset.browser_download_url.split('/').pop();
|
|
fs.writeFileSync(path.join(destPath, filename), buffer);
|
|
console.log(`Downloaded asset: ${filename} to ${destPath}`);
|
|
}
|
|
|
|
/**
|
|
* Create manifest and Vue option for a firmware
|
|
* Partition path uses filename only (matches Python; split is redundant but harmless)
|
|
*/
|
|
function createManifest(name, templateFn) {
|
|
const fw = name.split('-firmware')[0];
|
|
const man_file = fw + '.manifest.json';
|
|
const fwp_name = fw + '-partitions.bin';
|
|
const fwb_name = fw + '-bootloader.bin';
|
|
|
|
// Use filename to mirror Python behavior (no directories present today)
|
|
const partPath = fwp_name.split('/').pop();
|
|
|
|
const mani_str = templateFn({
|
|
cp: cors_proxy,
|
|
part: firmwareManifestFolder + partPath,
|
|
bin: firmwareManifestFolder + name,
|
|
bl: firmwareManifestFolder + fwb_name,
|
|
boot: firmwareManifestFolder + esp32_boot.split('/').pop()
|
|
});
|
|
|
|
const outPath = path.join(FIRMWARE_BUILD_DIR, man_file);
|
|
fs.writeFileSync(outPath, mani_str);
|
|
console.log(`Created manifest for ${fw}: ${outPath}`);
|
|
}
|
|
|
|
/**
|
|
* Create manifest for ESP8266
|
|
* Python adds manif_folder when writing to file, not in return
|
|
*/
|
|
function createManifest8266(name) {
|
|
const fw = name.split('-firmware')[0];
|
|
const man_file = fw + '.manifest.json';
|
|
|
|
const mani_str = mf_temp8266({
|
|
cp: cors_proxy,
|
|
bin: firmwareManifestFolder + name
|
|
});
|
|
|
|
const outPath = path.join(FIRMWARE_BUILD_DIR, man_file);
|
|
fs.writeFileSync(outPath, mani_str);
|
|
console.log(`Created manifest for ${fw} (ESP8266): ${outPath}`);
|
|
|
|
}
|
|
|
|
/**
|
|
* Device type matchers
|
|
*/
|
|
const ESP32_NAMES = ['esp32', 'ttgo', 'heltec', 'thingpulse', 'theengs', 'lilygo', 'shelly', 'tinypico'];
|
|
const ESP8266_NAMES = ['nodemcu', 'sonoff', 'rf-wifi-gateway', 'manual-wifi-test', 'rfbridge'];
|
|
|
|
const deviceMatchers = {
|
|
esp32: (name) => name.includes('firmware.bin') &&
|
|
!name.includes('esp32c3') && !name.includes('esp32s3') &&
|
|
ESP32_NAMES.some(key => name.includes(key)),
|
|
|
|
esp32c3: (name) => name.includes('firmware.bin') && name.includes('esp32c3'),
|
|
|
|
esp32s3: (name) => name.includes('firmware.bin') && name.includes('esp32s3'),
|
|
|
|
esp8266: (name) => name.includes('firmware.bin') &&
|
|
ESP8266_NAMES.some(key => name.includes(key))
|
|
};
|
|
|
|
/**
|
|
* Setup dev environment
|
|
*/
|
|
async function setupDevEnvironment() {
|
|
console.log('DEV mode: preparing web upload files...');
|
|
ensureFirmwareArtifacts();
|
|
// Generate OTA latest version definition from template
|
|
const tpl = path.join(__dirname, 'latest_version_dev.json.tpl');
|
|
renderVersionTemplate(tpl, path.join(FIRMWARE_BUILD_DIR, 'latest_version_dev.json'), meta.version);
|
|
|
|
// Copy the binaries from FIRMWARE_SRC_DIR to FIRMWARE_BUILD_DIR
|
|
const files = fs.readdirSync(FIRMWARE_SRC_DIR);
|
|
let copied = 0;
|
|
for (const name of files) {
|
|
if (name.includes('.bin')) {
|
|
fs.copyFileSync(
|
|
path.join(FIRMWARE_SRC_DIR, name),
|
|
path.join(FIRMWARE_BUILD_DIR, name)
|
|
);
|
|
copied++;
|
|
console.log(`Copied binary: ${name}`);
|
|
}
|
|
}
|
|
console.log(`Copied ${copied} firmware binaries to ${FIRMWARE_BUILD_DIR}`);
|
|
}
|
|
|
|
/**
|
|
* Setup release environment
|
|
*/
|
|
async function setupReleaseEnvironment() {
|
|
console.log('RELEASE mode: downloading and preparing web upload files...');
|
|
|
|
// Generate OTA latest version definition from template
|
|
const tpl = path.join(__dirname, 'latest_version.json.tpl');
|
|
renderVersionTemplate(tpl, path.join(FIRMWARE_BUILD_DIR, 'latest_version.json'), meta.version);
|
|
|
|
const releaseUrl = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
console.log(`Fetching latest release info from: ${releaseUrl}`);
|
|
const rel_data = await fetchJson(releaseUrl);
|
|
|
|
if (!rel_data.assets) {
|
|
console.error('No assets found in the latest release!');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Download all assets
|
|
let downloaded = 0;
|
|
for (const asset of rel_data.assets) {
|
|
const name = asset.name;
|
|
if (name.includes('firmware.bin') ||
|
|
name.includes('partitions.bin') ||
|
|
name.includes('bootloader.bin')) {
|
|
await downloadAsset(asset, FIRMWARE_BUILD_DIR);
|
|
downloaded++;
|
|
}
|
|
}
|
|
console.log(`Downloaded ${downloaded} firmware assets to ${FIRMWARE_BUILD_DIR}`);
|
|
}
|
|
|
|
/**
|
|
* Process firmware files and generate manifests
|
|
*/
|
|
function processFirmwareFiles(files) {
|
|
let manifestCount = 0;
|
|
for (const name of files) {
|
|
if (deviceMatchers.esp32(name)) {
|
|
createManifest(name, mf_temp32);
|
|
manifestCount++;
|
|
}
|
|
if (deviceMatchers.esp32c3(name)) {
|
|
createManifest(name, mf_temp32c3);
|
|
manifestCount++;
|
|
}
|
|
if (deviceMatchers.esp32s3(name)) {
|
|
createManifest(name, mf_temp32s3);
|
|
manifestCount++;
|
|
}
|
|
if (deviceMatchers.esp8266(name)) {
|
|
createManifest8266(name);
|
|
manifestCount++;
|
|
}
|
|
}
|
|
console.log(`Generated ${manifestCount} manifest files in ${FIRMWARE_BUILD_DIR}`);
|
|
|
|
}
|
|
|
|
/**
|
|
* Main execution function
|
|
*/
|
|
// ===================== OpenMQTTGateway Web Uploader Manifest Generator =====================
|
|
// ===================== MAIN SCRIPT STARTS HERE =====================
|
|
async function main() {
|
|
console.log('================================================================================');
|
|
console.log(' OpenMQTTGateway Web Uploader Manifest Generator - START');
|
|
console.log('================================================================================');
|
|
|
|
// === [1] Load and generate boards info ===
|
|
console.log('\n[1/4] Generating boards-info.json ...');
|
|
const boardsInfo = loadBoardsInfo({ verbose: 0 });
|
|
const boardsJson = boardsInfo.map((row) => ({
|
|
environment: row.Environment,
|
|
hardware: row.Hardware,
|
|
description: row.Description,
|
|
microcontroller: row.uC,
|
|
modules: row.Modules.filter(Boolean),
|
|
platform: row.Platform,
|
|
partitions: row.Partitions,
|
|
libraries: row.Libraries.filter(Boolean),
|
|
options: row.Options,
|
|
customImg: row.CustomImg
|
|
}));
|
|
ensureDir(path.dirname(BOARDS_INFO_FILE));
|
|
fs.writeFileSync(BOARDS_INFO_FILE, JSON.stringify(boardsJson, null, 2), 'utf8');
|
|
console.log(`Generated boards-info.json with ${boardsJson.length} boards: ${BOARDS_INFO_FILE}`);
|
|
|
|
// === [2] Ensure output directory ===
|
|
console.log('\n[2/4] Ensuring output directory ...');
|
|
ensureDir(FIRMWARE_BUILD_DIR);
|
|
console.log(`Ensured output directory exists: ${FIRMWARE_BUILD_DIR}`);
|
|
|
|
// === [3] Setup environment (dev or release) ===
|
|
console.log('\n[3/4] Preparing firmware files ...');
|
|
try {
|
|
if (dev) {
|
|
await setupDevEnvironment();
|
|
} else {
|
|
await setupReleaseEnvironment();
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error setting up environment: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// === [4] Download boot binary and generate manifests ===
|
|
console.log('\n[4/4] Downloading boot binary and generating manifests ...');
|
|
console.log(`Downloading boot binary: ${esp32_boot}`);
|
|
const boot_bin = await downloadFile(esp32_boot);
|
|
const boot_filename = esp32_boot.split('/').pop();
|
|
fs.writeFileSync(path.join(FIRMWARE_BUILD_DIR, boot_filename), boot_bin);
|
|
console.log(`Saved boot binary as: ${boot_filename}`);
|
|
|
|
const files = fs.readdirSync(FIRMWARE_BUILD_DIR).sort();
|
|
console.log(`Processing firmware files in ${FIRMWARE_BUILD_DIR}...`);
|
|
processFirmwareFiles(files);
|
|
|
|
console.log('\n================================================================================');
|
|
console.log(' OpenMQTTGateway Web Uploader Manifest Generator - END');
|
|
console.log('================================================================================');
|
|
}
|
|
// ===================== MAIN SCRIPT ENDS HERE =====================
|
|
|
|
// Run main function
|
|
main().catch(error => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(1);
|
|
});
|