Files
f5b33104ef [DOCS] Overhaul new-user onboarding, home page, and upload flow (#2347)
Getting started:
- New 15-minute Getting Started guide featuring the two most popular
  tracks (BLE on esp32dev-ble, RF on lilygo-rtl_433), with broker setup
  and a verification step
- Sidebar restructured around the new-user journey; upload options
  numbered by ease; Gitpod upload method removed

Home page:
- VitePress hero with clear actions, larger hero image
- How-it-works pipeline diagram shared as a component (responsive via
  container queries), use-case grid, bigger video tiles without the
  image lightbox, refreshed press coverage list, functions pills
- Support section pointing to Theengs Bridge and
  github.com/sponsors/1technophile

Upload flow:
- Decision-oriented upload landing page (journey strip, method cards,
  comparison table)
- Pre-flight checklist and driver troubleshooting on the web installer
- Guided environment picker (bridge + hardware wizard) replacing the
  filter chips, with manifest-verified recommendations
- Post-flash next-steps card linking to the portal and verification

Board images:
- custom_img key in environments.ini and board photos for all
  web-flashable environments (licenses recorded); private prod_env.ini
  environments excluded from boards-info.json

Corrections:
- RF frequency coverage stated as 433/315/868/915MHz
- Removed references to the retired compatible.openmqttgateway.com
  site (BLE list is decoder.theengs.io), the discontinued Theengs
  Plug, and the private sponsor forum


Claude-Session: https://claude.ai/code/session_01TYysQW8tpmt1dWWuzKhTLi

Co-authored-by: Florian <1technophile@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:23:43 -05:00

249 lines
8.0 KiB
JavaScript

'use strict';
/**
* Universal parser for PlatformIO dependencies.
* Formats URLs and registry strings into a consistent "Registry Style":
* Name @ Version (provider:user)
*/
function smartFormat(dep) {
if (!dep) return "";
if (typeof dep !== 'string') return dep;
const cleanDep = dep.trim();
// Configuration for Git providers with specific regex for archives, releases, and git repos
const providers = [
{
id: 'gh',
name: 'github',
// Captures: 1. Author, 2. Repo, 3. Version from path (releases), 4. Version from filename/branch
regex: /github\.com\/([^/]+)\/([^/.]+)(?:\/(?:archive|releases\/download\/([^/]+)|tree)\/)?([^/]+)?(?:\.zip|\.git)?$/i
},
{
id: 'gl',
name: 'gitlab',
// Captures: 1. Author, 2. Repo, 3. Version from path, 4. Version from filename
regex: /gitlab\.com\/([^/]+)\/([^/.]+)(?:\/(?:-\/)?(?:archive|releases)\/([^/]+))?\/([^/]+)?(?:\.zip|\.git)?$/i
},
{
id: 'bb',
name: 'bitbucket',
// Captures: 1. Author, 2. Repo, 3. Version from path, 4. Version from filename
regex: /bitbucket\.org\/([^/]+)\/([^/.]+)(?:\/(?:get|downloads)\/([^/]+))?\/([^/]+)?(?:\.zip|\.git)?$/i
}
];
// 1. Try to match against Git providers (GitHub, GitLab, Bitbucket)
for (const p of providers) {
const match = cleanDep.match(p.regex);
if (match) {
let [_, author, repo, pathVer, fileVer] = match;
// Prioritize version from path (typical in releases) over filename
let version = pathVer || fileVer || "latest";
// Clean up version string: remove extensions and 'v' prefix
version = version
.replace(/\.(zip|git|tar\.gz)$/i, '')
.replace(/^v(\d)/i, '$1'); // Removes 'v' only if followed by a number
// Avoid redundancy if the version string is identical to the repo name
if (version.toLowerCase() === repo.toLowerCase()) {
version = "latest";
}
return `${repo} @ ${version} (${p.id}:${author})`;
}
}
// 2. Fallback for Standard PlatformIO Registry format (e.g., owner/lib @ ^1.0.0)
if (cleanDep.includes('/') || cleanDep.includes('@')) {
const parts = cleanDep.split('@');
const fullName = parts[0].trim(); // Includes owner/name
// Clean up version if present
let version = "latest";
if (parts[1]) {
version = parts[1].trim().replace(/^[\^~=]/, '');
}
// Separate owner and library name for consistent formatting
if (fullName.includes('/')) {
const [owner, libName] = fullName.split('/');
return `${libName.trim()} @ ${version} (pio:${owner.trim()})`;
}
return `${fullName} @ ${version}`;
}
// 3. Return original string if no patterns match
return cleanDep;
}
function rowConfigFromPlatformIO() {
const { execSync } = require('child_process');
try {
const jsonConfig = execSync('pio project config --json-output').toString();
const config = JSON.parse(jsonConfig);
return config;
} catch (error) {
console.error("Make sure PlatformIO Core is installed and in PATH");
throw error;
}
}
function cleanValue(v) {
if (typeof v !== 'string') return v;
return v
.replace(/{/g, '')
.replace(/}/g, '')
.replace(/\$/g, '')
.replace(/env:/g, '')
.replace(/'/g, '')
.replace(/-D/g, '');
}
function convertJsonToSections(jsonConfig) {
const sections = {};
jsonConfig.forEach(([sectionName, configArray]) => {
sections[sectionName] = {};
configArray.forEach(([key, value]) => {
sections[sectionName][key] = value;
});
});
return sections;
}
function cleanLibraries(raw) {
if (!raw) return [];
if (typeof raw === 'string') {
raw = raw.split(',')
}
return raw.map((dep) => smartFormat(dep));
}
function extractModulesFromFlags(flags) {
if (!flags) return [];
let flagArray = [];
if (Array.isArray(flags)) {
flagArray = flags;
} else if (typeof flags === 'string') {
flagArray = flags.split(',').map(s => s.trim()).filter(s => s.length > 0);
} else {
return [];
}
const modules = [];
flagArray.forEach((flag) => {
// Match -DZmoduleName, allowing surrounding quotes
const match = flag.match(/^['" ]*-DZ([^=]+)/);
if (match) {
const moduleName = match[1];
// Additional constraint: must contain 'gateway', 'sensor', or 'actuator'
if (moduleName.includes('gateway') || moduleName.includes('sensor') || moduleName.includes('actuator')) {
modules.push(moduleName);
}
}
//if MQTT_BROKER_MODE = true then modules.push("MQTT Broker Mode");
const brokerMatch = flag.match(/^['" ]*-DMQTT_BROKER_MODE(?:=([^'"\s]+))?/);
if (brokerMatch) {
const value = brokerMatch[1];
// Add only if not explicitly set to false (case insensitive)
if (!value || value.toLowerCase() !== 'false') {
modules.push("MQTT Broker Mode");
}
}
});
return modules;
}
function collectBoardsInformations(sections, { includeTests = false } = {}) {
const rows = [];
Object.entries(sections).forEach(([section, items]) => {
if (!section.includes('env:')) return;
if (!includeTests && section.includes('-test')) return;
const env = section.replace('env:', '');
let uc = '';
let hardware = '';
let description = '';
let modules = [];
let platform = '';
let partitions = '';
let libraries = [];
let options = [];
let customImg = '';
Object.entries(items).forEach(([k, raw]) => {
const v = cleanValue(raw);
if (k === 'board') uc = v;
if (k === 'platform') platform = smartFormat(v);
if (k === 'board_build.partitions') partitions = v;
if (k === 'custom_description') description = v;
if (k === 'custom_hardware') hardware = v;
if (k === 'custom_img') customImg = v;
if (k === 'lib_deps') {
libraries = cleanLibraries(raw);
}
if (k === 'build_flags') {
options = v;
modules = extractModulesFromFlags(v);
}
});
rows.push({
Environment: env,
uC: uc,
Hardware: hardware,
Description: description,
Modules: modules,
Platform: platform,
Partitions: partitions,
Libraries: libraries,
Options: options,
CustomImg: customImg
});
});
rows.sort((a, b) => a.Environment.localeCompare(b.Environment, 'en', { sensitivity: 'base' }));
return rows;
}
// pio project config resolves extra_configs, which pulls in user-specific
// files like prod_env.ini; only environments declared in environments.ini
// belong in the published boards list.
function publicEnvironmentNames() {
const fs = require('fs');
const path = require('path');
const ini = fs.readFileSync(path.resolve(__dirname, '..', 'environments.ini'), 'utf8');
const names = new Set();
for (const match of ini.matchAll(/^\[env:([^\]]+)\]/gm)) {
names.add(match[1].trim());
}
return names;
}
function loadBoardsInfo(options = {}) {
const { includeTests = false } = options;
const config = rowConfigFromPlatformIO();
const sections = convertJsonToSections(config);
const rows = collectBoardsInformations(sections, { includeTests });
const publicEnvs = publicEnvironmentNames();
return rows.filter(row => publicEnvs.has(row.Environment));
}
function ensureDir(dir) {
const fs = require('fs');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
module.exports = {
loadBoardsInfo,
ensureDir
};