From 98481c5145cf5dc8b6a14ed6742e09cdba5801bc Mon Sep 17 00:00:00 2001 From: Alessandro Staniscia Date: Mon, 9 Mar 2026 13:47:30 +0100 Subject: [PATCH] [SITE] Renew the web board presentation and the ESP32 web upload + [SYS] Security checks (#2277) * Refactor GitHub Actions workflows for build, documentation, and linting - Consolidated build logic into reusable workflows (`task-build.yml` and `task-docs.yml`) to reduce duplication across multiple workflows. - Introduced `environments.json` to centralize the list of PlatformIO build environments, improving maintainability and clarity. - Updated `build.yml` and `build_and_docs_to_dev.yml` to utilize the new reusable workflows and environment definitions. - Enhanced `release.yml` to streamline the release process and integrate documentation generation. - Created reusable linting workflow (`task-lint.yml`) to standardize code formatting checks across the repository. - Simplified manual documentation workflow by leveraging the new reusable documentation workflow. - Improved artifact management and retention policies across workflows. - Updated dependencies and versions in workflows to ensure compatibility and performance. CI/CD pipeline agnostic of Workflow Engine and integrated on github actions - Implemented ci.sh for orchestrating the complete build pipeline. - Created ci_00_config.sh for centralized configuration of build scripts. - Created ci_build_firmware.sh for building firmware for specified PlatformIO environments. - Created ci_prepare_artifacts.sh for preparing firmware artifacts for upload or deployment. - Created ci_set_version.sh for updating version tags in firmware configuration files. - Created ci_build.sh to orchestrate the complete build pipeline. - Created ci_qa.sh for code linting and formatting checks using clang-format. - Created ci_site.sh for building and deploying VuePress documentation with version management. - Implemented checks for required tools and dependencies in the new scripts. - Improved internal scripts for better error handling and logging. UPDATE the web installer manifest generation and update documentation structure - Enhanced ci_list-env.sh to list environments from a JSON file. - Replaced common_wu.py and gen_wu.py scripts with new npm scripts for site generation and previewing on docsgen/gen_wu.js - Replaced generate_board_docs.py with docsgen/generated_board_docs.js - Added new npm scripts for integration of site generation on build phase. - Created preview_site.js to serve locally generated site over HTTPS with improved error handling. - Added new CI environments for CI builds in environments.json. - Deleted lint.yml as part of workflow cleanup. - Enhanced task-build.yml to include linting as a job and added support for specifying PlatformIO version. - Improved task-docs.yml to handle versioning more effectively and added clean option. Enhance documentation - ADD CLEAR Mark of development version of site - Updated README.md to include detailed workflow dependencies and relationships using mermaid diagrams. - Improved development.md with a quick checklist for contributors and clarified the code style guide. - Enhanced quick_start.md with tips for contributors and streamlined the workflow explanation. LINT FIX - Refined User_config.h for better formatting consistency. - Adjusted blufi.cpp and gatewayBT.cpp for improved code readability and consistency in formatting. - Updated gatewaySERIAL.cpp and mqttDiscovery.cpp to enhance logging error messages. - Improved sensorDS1820.cpp for better logging of device information. Add security scan workflows for vulnerability detection Add SBOM generation and upload to release workflow; update security scan summary handling Add shellcheck suppor + FIX shellcheck warning Enhance documentation for CI/CD scripts and workflows, adding details for security scanning and SBOM generation processes Fix formatting and alignment in BLE connection handling Reviewed the full web board presentation and the ESP32 web upload. The project uses a modern pattern where data is divided from the presentation layer. - Removed the `generate_board_docs` script. - Updated the `gen_wu` script in order to generate `boards-info.json`: the fail that containe all information about the configuration - Created and isolate the file `boards-info.js` to streamline the parsing of PlatformIO dependencies, modules, environments and improve the handling of library information. - Introduced vuepress component `BoardEnvironmentTable.vue` that render `boards-info.json` as UI card component - Introduced vuepress component `FlashEnvironmentSelector.vue` that render a selectred environment from `boards-info.json` and provide esp-web-upload feature on it - Introduced a new board page `board-selector.md` for improved firmware selection. - Updated `web-install.md` to enhance the firmware upload process, including a new board environment table. - Enhanced custom descriptions in `environments.ini` to include HTML links for better user guidance and board image link Add CC1101 initialization improvements and logging enhancements Add installation step for PlatformIO dependencies in documentation workflow Remove ci_set_version.sh script and associated versioning functionality * Fix comment provisined Fix PlatformIO version input reference in documentation workflow Remove outdated Squeezelite-ESP32 installer documentation --- .github/workflows/README.md | 669 ++++++++------ .github/workflows/build.yml | 51 +- .github/workflows/build_and_docs_to_dev.yml | 82 +- .github/workflows/environments.json | 31 +- .github/workflows/lint.yml | 12 - .github/workflows/manual_docs.yml | 14 +- .github/workflows/release.yml | 61 +- .github/workflows/security-scan.yml | 44 + .github/workflows/task-build.yml | 82 +- .github/workflows/task-docs.yml | 167 ++-- .github/workflows/task-lint.yml | 22 +- .github/workflows/task-security-scan.yml | 124 +++ .gitignore | 23 +- .shellcheckrc | 1 + .../components/BoardEnvironmentTable.vue | 440 +++++++++ .../components/FlashEnvironmentSelector.vue | 686 ++++++++++++++ docs/.vuepress/config.js | 46 +- docs/.vuepress/defaults.json | 12 + docs/.vuepress/public/img/microcontroller.gif | Bin 0 -> 331744 bytes docs/README.md | 10 + docs/img/upload.png | Bin 0 -> 20549 bytes docs/participate/README.md | 46 + docs/participate/adding-protocols.md | 18 +- docs/participate/community.md | 9 +- docs/participate/development.md | 37 +- docs/participate/quick_start.md | 840 ++++++++++++++++++ docs/participate/support.md | 25 +- docs/prerequisites/board.md | 12 +- docs/prerequisites/boards/.gitignore | 4 - docs/setitup/rf.md | 20 +- docs/upload/ReadMe.md | 79 ++ docs/upload/board-selector.md | 10 + docs/upload/web-install.md | 20 +- docsgen/boards-info.js | 232 +++++ docsgen/common_wu.js | 77 ++ docsgen/gen_wu.js | 349 ++++++++ .../latest_version.json.tpl | 0 .../latest_version_dev.json.tpl | 0 environments.ini | 21 +- .../OpenGatewaySensorCounter.ino | 95 +- examples/LoraWaterCounter/src/main.cpp | 39 +- main/TheengsCommon.h | 13 + main/User_config.h | 2 +- main/blufi.cpp | 8 +- main/commonRF.cpp | 66 +- main/config_RF.h | 2 +- main/gatewayBLEConnect.cpp | 16 +- main/gatewayBT.cpp | 6 +- main/gatewayRF.cpp | 4 +- main/gatewaySERIAL.cpp | 2 +- main/main.cpp | 8 +- main/mqttDiscovery.cpp | 2 +- main/rf/RFConfiguration.cpp | 41 +- main/sensorDS1820.cpp | 8 +- package-lock.json | 630 +++++++------ package.json | 14 +- scripts/CI_SCRIPTS.md | 639 ++++++++----- scripts/ci.sh | 240 ++++- scripts/ci_00_config.sh | 39 +- scripts/ci_build.sh | 125 ++- scripts/ci_build_firmware.sh | 45 +- scripts/ci_list-env.sh | 125 +++ scripts/ci_prepare_artifacts.sh | 242 +++-- scripts/ci_qa.sh | 75 +- scripts/ci_security.sh | 428 +++++++++ scripts/ci_set_version.sh | 197 ---- scripts/ci_site.sh | 471 ++++------ scripts/common_wu.py | 116 --- scripts/ensure_ssl_certs.js | 31 + scripts/gen_wu.py | 149 ---- scripts/generate_board_docs.py | 90 -- scripts/prepare_deploy.sh | 28 - scripts/preview_site.js | 60 ++ 73 files changed, 5985 insertions(+), 2447 deletions(-) delete mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/security-scan.yml create mode 100644 .github/workflows/task-security-scan.yml create mode 100644 .shellcheckrc create mode 100644 docs/.vuepress/components/BoardEnvironmentTable.vue create mode 100644 docs/.vuepress/components/FlashEnvironmentSelector.vue create mode 100644 docs/.vuepress/defaults.json create mode 100644 docs/.vuepress/public/img/microcontroller.gif create mode 100644 docs/img/upload.png create mode 100644 docs/participate/README.md create mode 100644 docs/participate/quick_start.md delete mode 100644 docs/prerequisites/boards/.gitignore create mode 100644 docs/upload/ReadMe.md create mode 100644 docs/upload/board-selector.md create mode 100644 docsgen/boards-info.js create mode 100644 docsgen/common_wu.js create mode 100644 docsgen/gen_wu.js rename scripts/latest_version.json => docsgen/latest_version.json.tpl (100%) rename scripts/latest_version_dev.json => docsgen/latest_version_dev.json.tpl (100%) create mode 100755 scripts/ci_list-env.sh create mode 100755 scripts/ci_security.sh delete mode 100755 scripts/ci_set_version.sh delete mode 100644 scripts/common_wu.py create mode 100644 scripts/ensure_ssl_certs.js delete mode 100644 scripts/gen_wu.py delete mode 100644 scripts/generate_board_docs.py delete mode 100755 scripts/prepare_deploy.sh create mode 100644 scripts/preview_site.js diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 805734f7..a0823045 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,8 +1,7 @@ # GitHub Actions Workflows Documentation -This document provides a overview of all GitHub Actions workflows in the OpenMQTTGateway project. +This document provides an overview of all GitHub Actions workflows in the OpenMQTTGateway project. ---- ## Architecture Overview @@ -14,7 +13,7 @@ Entry points triggered by user actions, schedules, or events: - `build_and_docs_to_dev.yml` - Daily development builds - `release.yml` - Production releases - `manual_docs.yml` - Documentation deployment -- `lint.yml` - Code formatting check +- `security-scan.yml` - Security vulnerability scanning - `stale.yml` - Issue management ### **Task Workflows** (Reusable components) @@ -22,8 +21,8 @@ Parameterized building blocks called by main workflows: - `task-build.yml` - Configurable firmware build - `task-docs.yml` - Configurable documentation build - `task-lint.yml` - Configurable code formatting check +- `task-security-scan.yml` - Configurable security scanning ---- ## Workflow Overview Table @@ -33,11 +32,188 @@ Parameterized building blocks called by main workflows: | `build_and_docs_to_dev.yml` | Daily Cron, Manual | Development Builds + Docs | Firmware + Docs deployment | | `release.yml` | Release Published | Production Release | Release assets + Docs | | `manual_docs.yml` | Manual, Workflow Call | Documentation Only | GitHub Pages docs | -| `lint.yml` | Push, Pull Request | Code Format Check | None | +| `security-scan.yml` | Weekly Cron, Manual | Security Vulnerability Scanning | SARIF, SBOM reports | | `stale.yml` | Daily Cron | Issue Management | None | | **`task-build.yml`** | **Workflow Call** | **Reusable Build Logic** | **Configurable** | | **`task-docs.yml`** | **Workflow Call** | **Reusable Docs Logic** | **GitHub Pages** | | **`task-lint.yml`** | **Workflow Call** | **Reusable Lint Logic** | **None** | +| **`task-security-scan.yml`** | **Workflow Call** | **Reusable Security Scan** | **SARIF, SBOM, Reports** | + + + + +## Workflow Dependencies and Call Chain + +```mermaid + +flowchart TD + %% Triggers + subgraph triggers ["🎯 Triggers"] + push["Push"] + pr["Pull Request"] + release["Release Published"] + cron1["Cron: Daily 00:00 UTC"] + manual["Manual Trigger"] + cron3["Cron: Weekly Monday 02:00 UTC"] + cron2["Cron: Daily 00:30 UTC"] + + end + +subgraph github_workflows ["📋 GitHub Workflows"] + %% Main Workflows + subgraph main ["📋 Main Workflows"] + build["build.yml
CI Build"] + release_wf["release.yml
Production Release"] + build_dev["build_and_docs_to_dev.yml
Dev Builds"] + security_scan["security-scan.yml
Security Scan"] + stale["stale.yml
Issue Management"] + manual_docs["manual_docs.yml
Docs Only"] + end + + %% Task Workflows + subgraph tasks ["⚙️ Task Workflows"] + task_build["task-build.yml
Build Firmware"] + task_lint["task-lint.yml
Code Format"] + task_security["task-security-scan.yml
Security Scan"] + task_docs["task-docs.yml
Build & Deploy Docs"] + + end +end + +subgraph ci_scripts ["🔧 CI Scripts"] + %% CI Scripts Layer + subgraph bash ["🔧 Orchestrator"] + ci_main["ci.sh
(main dispatcher)"] + ci_build_script["ci_build.sh
(build orchestrator)"] + ci_site_script["ci_site.sh
(docs orchestrator)"] + ci_qa_script["ci_qa.sh
(lint orchestrator)"] + ci_security_script["ci_security.sh
(security orchestrator)"] + end + + %% Sub-Scripts Layer + subgraph sub_scripts ["⚙️ Workers"] + ci_build_fw["ci_build_firmware.sh
(PlatformIO build)"] + ci_prep_art["ci_prepare_artifacts.sh
(artifact packaging)"] + gen_board["generate_board_docs
(npm package)"] + gen_wu["gen_wu
(npm package)"] + clang_fmt["clang-format
(code formatter)"] + trivy["Trivy
(vulnerability scanner)"] + end +end + + %% Trigger connections + push -->|"all or subset
(depends on branch)"| build + pr -->|"all
(always full)"| build + + release --> release_wf + manual --> manual_docs + cron1 --> build_dev + cron2 --> stale + cron3 --> security_scan + + %% Main workflow to task workflow connections + build --> task_build + build_dev --> task_build + build_dev --> task_docs + release_wf --> task_build + release_wf --> task_docs + manual_docs --> task_docs + security_scan --> task_security + + %% Task workflow internal dependencies + task_build --> task_lint + task_build --> task_security + + %% Task workflows to CI scripts + task_build -->|"ci.sh build ..."| ci_main + task_docs -->|"ci.sh site ..."| ci_main + task_lint -->|"ci.sh qa ..."| ci_main + task_security -->|"ci.sh security ..."| ci_main + + %% CI main dispatcher to orchestrators + ci_main -->|"route: build"| ci_build_script + ci_main -->|"route: site"| ci_site_script + ci_main -->|"route: qa"| ci_qa_script + ci_main -->|"route: security"| ci_security_script + + %% Orchestrators to workers + ci_build_script --> ci_build_fw + ci_build_script --> ci_prep_art + + ci_site_script --> gen_board + ci_site_script --> gen_wu + + ci_qa_script --> clang_fmt + + ci_security_script --> trivy + + %% Styling + classDef triggerStyle fill:#e1f5ff,stroke:#0066cc,stroke-width:2px + classDef mainStyle fill:#fff4e6,stroke:#ff9900,stroke-width:2px + classDef taskStyle fill:#e6f7e6,stroke:#00aa00,stroke-width:2px + classDef ciStyle fill:#ffe6f0,stroke:#cc0066,stroke-width:2px + classDef subStyle fill:#f0e6ff,stroke:#9933ff,stroke-width:2px + + class push,pr,release,manual,cron1,cron2,cron3 triggerStyle + class build,release_wf,manual_docs,build_dev,stale,security_scan mainStyle + class task_build,task_docs,task_lint,task_security taskStyle + class ci_main,ci_build_script,ci_site_script,ci_qa_script,ci_security_script ciStyle + class ci_build_fw,ci_prep_art,gen_board,gen_wu,clang_fmt,trivy subStyle + + + style github_workflows stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:1.8px,fill:#fbfbfc + style main stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fcfdff + style tasks stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fcfdff + + style ci_scripts stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:1.8px,fill:#fffaf5 + style bash stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fffaf5 + style sub_scripts stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fffaf5 + + style triggers fill:none,stroke:none + + linkStyle 0,1,2,3,4,5,6 stroke:#0066cc,stroke-width:3px; + linkStyle 7,8,9,10,11,12,13,14,15 stroke:orange, stroke-width:2px; + linkStyle 16,17,18,19,20,21,22,23,24,25,26,27,28,29 stroke:red, stroke-width:2px; + +``` + +### Workflow Relationships + +**Main → Task Mapping**: +- `build.yml` → calls `task-build.yml` (also contains inline documentation job) +- `build_and_docs_to_dev.yml` → calls `task-build.yml` + `task-docs.yml` +- `release.yml` → calls `task-build.yml` + `task-docs.yml` +- `manual_docs.yml` → calls `task-docs.yml` +- `security-scan.yml` → calls `task-security-scan.yml` +- `stale.yml` → standalone (no dependencies) + +**Task → CI Script Mapping**: +- `task-build.yml` → `ci.sh build --version --mode --deploy-ready` + - Routes to: `ci_build.sh` → `ci_build_firmware.sh`, `ci_prepare_artifacts.sh` + - Output: `generated/artifacts/firmware_build/` +- `task-docs.yml` → `ci.sh site --mode --version --url-prefix` + - Routes to: `ci_site.sh` → `generate_board_docs` (npm), `gen_wu` (npm), VuePress + - Output: `generated/site/` +- `task-lint.yml` → `ci.sh qa --check --source --extensions --clang-format-version` + - Routes to: `ci_qa.sh` → `clang-format` +- `task-security-scan.yml` → `ci.sh security --scan-type --severity --generate-sbom` + - Routes to: `ci_security.sh` → Trivy (vulnerability scanner) + - Output: `generated/reports/` (SARIF, JSON, SBOM) + +**Job Dependencies**: +- `build_and_docs_to_dev.yml`: prepare → build (task) → deploy & documentation (task) +- `release.yml`: prepare → build (task) → deploy → documentation (task) + +**Script Execution Flow**: +``` +GitHub Action (task-*.yml) + ↓ +./scripts/ci.sh [OPTIONS] ← Main dispatcher + ↓ +./scripts/ci_.sh ← Command orchestrator + ↓ +./scripts/ci_*.sh / *.py ← Worker scripts +``` --- @@ -45,16 +221,19 @@ Parameterized building blocks called by main workflows: ### 1. `build.yml` - Continuous Integration Build -**Purpose**: Validates that code changes compile successfully across all supported hardware platforms. +**Purpose**: Validates that code changes compile successfully with intelligent environment selection based on branch importance. **Triggers**: - **Push**: Every commit pushed to any branch - **Pull Request**: Every PR creation or update **What it does**: -1. **Build job**: Calls `task-build.yml` with CI parameters - - Builds firmware for **83 hardware environments** in parallel -2. **Documentation job**: Inline job that validates docs build (doesn't deploy) +1. **Determine build scope**: Selects environment list based on branch name + - **Full build** (`all` environment): All PRs and Push on important branches (development, master, edge, stable, release/*, hotfix/*) + - **Quick build** (`ci` subset environment): All PRs and Push on non-critical branches +2. **Build job**: Calls `task-build.yml` with appropriate environment set + - Builds firmware in parallel +3. **Documentation job**: Inline job that validates docs build (doesn't deploy) - Downloads common config from theengs.io - Runs `npm install` and `npm run docs:build` - Uses Node.js 14.x @@ -64,15 +243,18 @@ Parameterized building blocks called by main workflows: - Python version: 3.13 (for build job) - Build strategy: Parallel matrix via task workflow - Artifact retention: 7 days -- Development OTA: Disabled (`enable-dev-ota: false`) +- Development OTA: Enabled (`enable-dev-ota: true`) +- Environment selection logic: + - **Full build** (`all`): All Pull Requests + branches: development, master, edge, stable, release/*, hotfix/* + - **Quick build** (`ci` subset): All other feature branches **Outputs**: -- Firmware binaries for each environment (83 artifacts) +- Firmware binaries for selected environments - No documentation deployment (validation only) -**Use Case**: Ensures no breaking changes before merge. Fast feedback for developers. +**Use Case**: Ensures no breaking changes before merge. Fast feedback for feature branches (~10 min), comprehensive validation for PRs and critical branches (~40 min). -**Execution Context**: Runs for ALL contributors on ALL branches. +**Execution Context**: Runs for ALL contributors on ALL branches with smart scaling based on branch importance. --- @@ -86,38 +268,37 @@ Parameterized building blocks called by main workflows: **What it does**: 1. **Prepare job**: Generates 6-character short SHA -2. **Build job**: Calls `task-build.yml` with development parameters - - Builds firmware for **83 hardware environments** in parallel +2. **Handle-firmwares job**: Calls `task-build.yml` with development parameters + - Builds firmware for **all environments** in parallel - Enables development OTA updates with SHA commit version -3. **Deploy job**: Prepares and uploads assets - - Creates library dependency zips for each board - - Generates source code zip - - Removes test environment binaries -4. **Documentation job**: Calls `task-docs.yml` with development parameters - - Deploys to `/dev` subdirectory - - Adds "DEVELOPMENT" watermark - - Runs PageSpeed Insights + - Artifact retention: 1 day +3. **Handle-documentation job**: Calls `task-docs.yml` with development parameters + - Deploys to `/dev` subdirectory on GitHub Pages + - Uses short SHA as version identifier + - Runs PageSpeed Insights on dev site **Technical Details**: - **Calls**: `task-build.yml` + `task-docs.yml` -- Python version: 3.13 (build), 3.11 (docs) -- Node.js version: 16.x (docs) - Repository restriction: Hardcoded to `1technophile` owner only -- Artifact retention: 1 day -- Build flag: `enable-dev-ota: true` (passed to task-build.yml) +- Version: 6-character short SHA (e.g., `abc123`) +- Documentation URL prefix: `/dev/` +- GitHub Pages destination: `dev` subdirectory +- PageSpeed URL: `https://docs.openmqttgateway.com/dev/` + +**Workflow Parameters**: +- Build: `enable-dev-ota: true`, `version-tag: `, `artifact-retention-days: 1` +- Docs: `mode: "dev"`, `version: `, `url-prefix: "/dev/"`, `destination-dir: "dev"`, `run-pagespeed: true` **Outputs**: -- Firmware binaries with `-firmware.bin` suffix +- Firmware binaries with `-firmware.bin` suffix (1 day retention) - Bootloader and partition binaries -- Library dependency zips per board -- Source code zip - Documentation deployed to `docs.openmqttgateway.com/dev/` **Version Labeling**: -- Git SHA (6 chars) injected into firmware -- Docs tagged: "DEVELOPMENT SHA:XXXXXX TEST ONLY" +- Git SHA (6 chars) injected into firmware via `version-tag` +- Docs display short SHA as version -**Use Case**: Daily bleeding-edge builds for early adopters and testing. Preview documentation changes. +**Use Case**: Daily bleeding-edge builds for early adopters and testing. Preview documentation changes before production release. **Execution Context**: Only runs on `1technophile` repository owner. Forks will skip this workflow automatically. @@ -131,33 +312,34 @@ Parameterized building blocks called by main workflows: - **Release**: When a GitHub release is published (tagged) **What it does**: -1. **Prepare job**: Extracts version tag and release info +1. **Prepare job**: Extracts version tag and release info from GitHub event 2. **Build job**: Calls `task-build.yml` with production parameters - - Builds firmware for **83 hardware environments** in parallel - - Injects release tag version into firmware -3. **Deploy job**: Prepares and uploads release assets - - Downloads all build artifacts - - Reorganizes for `prepare_deploy.sh` script - - Creates library zips and source zip - - Uploads to GitHub Release + - Builds firmware for **all environments** in parallel + - Injects release tag version into firmware + - Artifact retention: 90 days +3. **Deploy job**: Downloads and uploads release assets + - Downloads all firmware artifacts from build job + - Uploads binaries to GitHub Release 4. **Documentation job**: Calls `task-docs.yml` for production docs + - Deploys to root (`/`) of GitHub Pages + - Uses release tag as version **Technical Details**: - **Calls**: `task-build.yml` + `task-docs.yml` -- Python version: 3.13 (build), 3.11 (docs) -- Node.js version: 18.x - Build flag: Standard (no DEVELOPMENTOTA) - Artifact retention: 90 days -- Uses `prepare_deploy.sh` script for asset preparation +- Deploy uses `bgpat/release-asset-action` to attach assets to GitHub Release + +**Workflow Parameters**: +- Build: `enable-dev-ota: false`, `version-tag: `, `artifact-retention-days: 90` +- Docs: `mode: "prod"`, `version: `, `url-prefix: "/"`, `destination-dir: "."` **Outputs**: -- Production firmware binaries attached to GitHub Release -- Library zips per board -- Source code zip +- Production firmware binaries attached to GitHub Release (all prepared by `task-build.yml` with `--deploy-ready`) - Production documentation at `docs.openmqttgateway.com/` **Version Labeling**: -- Git tag (e.g., `v1.2.3`) injected into firmware and `latest_version.json` +- Git tag (e.g., `v1.2.3`) injected into firmware **Workflow Chain**: ``` @@ -184,10 +366,11 @@ prepare → build (task-build.yml) → deploy → documentation (task-docs.yml) **Technical Details**: - **Calls**: `task-docs.yml` -- Python version: 3.11 -- Node.js version: 14.x -- Version source: Latest GitHub release tag -- WebUploader manifest: Enabled +- Mode: `prod` +- Version: Uses latest release tag by default (or provided input) +- URL prefix: `/` +- Destination: Root of GitHub Pages +- PageSpeed: Optional (disabled by default) **Outputs**: - Production documentation at `docs.openmqttgateway.com/` @@ -199,34 +382,55 @@ prepare → build (task-build.yml) → deploy → documentation (task-docs.yml) --- -### 5. `lint.yml` - Code Format Validation +### 5. `security-scan.yml` - Security Vulnerability Scanning -**Purpose**: Ensures code follows consistent formatting standards. +**Purpose**: Scans the project for security vulnerabilities and generates Software Bill of Materials (SBOM) for supply chain security. **Triggers**: -- **Push**: Every commit pushed to any branch -- **Pull Request**: Every PR creation or update +- **Schedule**: Weekly on Monday at 02:00 UTC (`0 2 * * 1`) +- **Manual**: Via workflow_dispatch button with input parameters + +**Manual Trigger Inputs**: +- `severity`: Severity levels to scan (choices: UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL; default: HIGH,CRITICAL) +- Allows filtering results to specific severity levels **What it does**: -1. Calls `task-lint.yml` with specific parameters -2. Checks formatting in `./main` directory only +1. **Prepare job**: Sets up environment for scanning +2. **Security scan job**: Calls `task-security-scan.yml` with parameters + - Runs Trivy vulnerability scanner on filesystem + - Filters results by severity level + - Generates SARIF format for GitHub Security tab integration + - Creates SBOM in CycloneDX and SPDX formats + - Uploads findings to GitHub Security tab (code scanning dashboard) +3. **Artifact upload job**: Stores generated reports and SBOM + - SARIF results for GitHub integration + - SBOM files for supply chain tracking + - Retention: 90 days **Technical Details**: -- **Calls**: `task-lint.yml` -- clang-format version: 9 -- File extensions: `.h`, `.ino` (not `.cpp`) -- Source directory: `main` (single directory) +- **Calls**: `task-security-scan.yml` +- Vulnerability scanner: Trivy (vulnerability database updated automatically) +- Report formats: SARIF (GitHub), JSON (detailed), Markdown (summary) +- SBOM formats: CycloneDX and SPDX (standard formats) +- Failure behavior: Does NOT fail the workflow on vulnerabilities (exit-code: 0) +- GitHub Security tab: Auto-uploads SARIF for code scanning dashboard visibility -**Configuration**: -```yaml -source: 'main' -extensions: 'h,ino' -clang-format-version: '9' -``` +**Workflow Parameters**: +- `scan-type: "fs"` (filesystem scan) +- `severity: "HIGH,CRITICAL"` (default, or manual input) +- `generate-sbom: true` (always enabled) +- `upload-to-security-tab: true` (GitHub Security integration) -**Use Case**: Maintains code quality and consistency. Prevents formatting debates in PRs. +**Outputs**: +- SARIF report: `generated/reports/trivy-results.sarif` (GitHub Security tab) +- JSON report: `generated/reports/trivy-results.json` (detailed findings) +- Markdown summary: `generated/reports/security-summary.md` +- SBOM: `generated/reports/sbom/sbom.cyclonedx.json` + `sbom.spdx.json` +- Artifacts retained for 90 days -**Execution Context**: Runs for ALL contributors on ALL branches. +**Use Case**: Regular security audits, compliance tracking, vulnerability management, supply chain security. + +**Execution Context**: Weekly automated scans + manual on-demand scanning for developers. --- @@ -267,17 +471,19 @@ clang-format-version: '9' **Trigger**: `workflow_call` only (called by other workflows) **Parameters**: -- `python-version`: Python version (default: '3.13') -- `enable-dev-ota`: Enable development OTA (default: false) -- `version-tag`: Version to inject (default: 'unspecified') -- `artifact-retention-days`: Artifact retention (default: 7) -- `artifact-name-prefix`: Artifact name prefix (default: '') -- `prepare-for-deploy`: Prepare for deployment (default: false) +- `python-version`: Python version to use (default: '3.13') +- `pio-version`: PlatformIO version to use (default: 'v6.1.18') +- `environment-set`: Which set of environments to build: 'all' or 'ci' (default: 'all') +- `enable-dev-ota`: Enable development OTA builds (default: false) +- `version-tag`: Optional version tag to pass to ci.sh build - omitted if empty (default: '') +- `artifact-retention-days`: Number of days to retain build artifacts (default: 7) **What it does**: -1. **Load environments**: Reads environment list from `environments.json` -2. **Matrix build**: Builds all 83 environments in parallel -3. **Build execution**: Calls unified `ci.sh build [OPTIONS]`: +1. **Lint code**: Runs `task-lint.yml` to check code formatting (main directory, .h and .ino files) +2. **Load environments**: Reads environment list from `environments.json` based on `environment-set` input (`all` or `ci`) +3. **Install PlatformIO**: Uses `uv` to install the `pio-version` input (custom `pioarduino/platformio-core` fork) +4. **Matrix build**: Builds selected environments in parallel, blocking on lint job completion +5. **Build execution**: Calls unified `ci.sh build [OPTIONS]`: - ``: Target hardware (e.g., `esp32dev-ble`) - `--version `: Version to inject (SHA for dev, tag for prod) - `--mode `: Build mode (enables/disables OTA) @@ -289,16 +495,17 @@ clang-format-version: '9' ./scripts/ci.sh build esp32dev-ble --version v1.8.0 --mode prod --deploy-ready ↓ ├─→ ci_build.sh (orchestrator) - │ ├─→ ci_set_version.sh v1.8.0 [--dev] │ ├─→ ci_build_firmware.sh esp32dev-ble [--dev-ota] │ └─→ ci_prepare_artifacts.sh esp32dev-ble [--deploy] → outputs to generated/artifacts/ ``` **Technical Details**: - Runs on: Ubuntu latest -- PlatformIO version: 6.1.18 (custom fork: `pioarduino/platformio-core`) +- PlatformIO version: Configurable via `pio-version` input (default: v6.1.18, custom fork: `pioarduino/platformio-core`) - Python package manager: `uv` (astral-sh/setup-uv@v6) -- Strategy: Matrix with fail-fast: false +- Environment sets: `all` (complete set) or `ci` (subset for quick validation) +- Strategy: Matrix with fail-fast: false (builds complete even if one environment fails) +- Dependencies: Lint job must pass before build matrix starts - Main orchestrator: `ci.sh` → `ci_build.sh` → sub-scripts **Callers**: @@ -315,36 +522,24 @@ clang-format-version: '9' **Trigger**: `workflow_call` only (called by other workflows) **Parameters**: -- `python-version`: Python version (default: '3.11') -- `node-version`: Node.js version (default: '14.x') -- `version-source`: Version source ('release', 'git-tag', 'custom') -- `custom-version`: Custom version string (optional) -- `base-path`: Base URL path (default: '/') -- `destination-dir`: Deploy directory (default: '.') -- `generate-webuploader`: Generate WebUploader manifest (default: true) -- `webuploader-args`: WebUploader generation arguments (optional) -- `run-pagespeed`: Run PageSpeed Insights (default: false) -- `pagespeed-url`: URL for PageSpeed test (optional) +- `mode`: Documentation mode (`prod` or `dev`, default: `prod`) +- `version`: Version string for docs (default: `auto` → tag or short SHA depending on caller) +- `url-prefix`: Base URL path (default: `/` for prod, `/dev/` for dev callers) +- `destination-dir`: Deploy directory on GitHub Pages (default: `.`) +- `run-pagespeed`: Run PageSpeed Insights after deploy (default: false) +- `pagespeed-url`: URL to test with PageSpeed (default: `https://docs.openmqttgateway.com/`) **What it does**: -1. **Build documentation**: Calls unified `ci.sh site [OPTIONS]`: - - `--mode `: Documentation mode - - `--custom-version `: Custom version string - - `--version-source `: Version source - - `--url-prefix `: Base URL path (e.g., `/dev/`) - - `--webuploader-args `: WebUploader options - - `--no-webuploader`: Skip manifest generation +1. **Build documentation**: Calls unified `ci.sh site --mode --version --url-prefix ` 2. **Deploy**: Publishes to GitHub Pages using `peaceiris/actions-gh-pages@v3` -3. **PageSpeed test**: Optionally runs performance audit +3. **PageSpeed test**: Optionally runs performance audit on the provided URL **Command Flow**: ```bash -./scripts/ci.sh site --mode prod --version-source release --url-prefix / - ↓ - └─→ ci_site.sh (orchestrator) - ├─→ generate_board_docs.py (auto-generate board docs) - ├─→ npm run docs:build (VuePress compilation) - └─→ gen_wu.py (WebUpdater manifest) +./scripts/ci.sh site --mode prod --version v1.2.3 --url-prefix / + ↓ + └─→ ci_site.sh (orchestrator) + └─→ npm run docs:build (VuePress compilation) ``` **Callers**: @@ -356,15 +551,14 @@ clang-format-version: '9' ### 9. `task-lint.yml` - Reusable Lint Workflow -**Purpose**: Parameterized code formatting validation. +**Purpose**: Parameterized code formatting validation for consistent code style. **Trigger**: `workflow_call` only (called by other workflows) **Parameters**: -- `source`: Source directories to lint (default: './lib ./main') -- `extensions`: File extensions to check (default: 'h,ino,cpp') -- `clang-format-version`: clang-format version (default: '9') -- `exclude-pattern`: Pattern to exclude (optional) +- `source`: Source directory to lint (default: 'main') +- `extensions`: File extensions to check, comma-separated (default: 'h,ino') +- `clang-format-version`: clang-format version to use (default: '9') **What it does**: 1. Checks out code @@ -386,170 +580,77 @@ clang-format-version: '9' ``` **Technical Details**: +- Runs on: Ubuntu latest - Script: `ci_qa.sh` (custom formatting check script) -- Install: `clang-format-$version` via apt-get -- Default source: `main` (single directory) -- Default extensions: `h,ino` (not cpp) +- Formatter: `clang-format-$version` installed via apt-get +- Default scope: `main` directory only (not lib) +- Default file types: `.h` and `.ino` (not `.cpp`) +- Strategy: Single sequential job (not parallelized) **Callers**: -- `lint.yml` (CI lint check) +- `build.yml` (inline lint check before build) +- Can be called by other workflows as needed -**Default Behavior**: If called without parameters, lints `main` directory for `.h` and `.ino` files. +**Default Behavior**: If called without parameters, lints `main` directory for `.h` and `.ino` files only. ---- -## Workflow Dependencies and Call Chain -```mermaid -flowchart TD - %% Triggers - subgraph triggers ["🎯 Triggers"] - push["Push / Pull Request"] - release["Release Published"] - manual["Manual Trigger"] - cron1["Cron: Daily 00:00 UTC"] - cron2["Cron: Daily 00:30 UTC"] - end +### 10. `task-security-scan.yml` - Reusable Security Scan Workflow -subgraph github_workflows ["📋 GitHub Workflows"] - %% Main Workflows - subgraph main ["📋 Main Workflows"] - lint["lint.yml
Format Check"] - - build["build.yml
CI Build"] - - release_wf["release.yml
Production Release"] - manual_docs["manual_docs.yml
Docs Only"] - build_dev["build_and_docs_to_dev.yml
Dev Builds"] - stale["stale.yml
Issue Management"] - end +**Purpose**: Parameterized security vulnerability scanning and SBOM generation logic. - %% Task Workflows - subgraph tasks ["⚙️ Task Workflows"] - task_build["task-build.yml
Build Firmware"] - task_docs["task-docs.yml
Build & Deploy Docs"] - task_lint["task-lint.yml
Code Format"] - end -end +**Trigger**: `workflow_call` only (called by other workflows) -subgraph ci_scripts ["🔧 CI Scripts"] - %% CI Scripts Layer - subgraph bash ["🔧 Orchestrator"] - ci_main["ci.sh
(main dispatcher)"] - ci_build_script["ci_build.sh
(build orchestrator)"] - ci_site_script["ci_site.sh
(docs orchestrator)"] - ci_qa_script["ci_qa.sh
(lint orchestrator)"] - end +**Parameters**: +- `scan-type`: Type of scan: 'fs' (filesystem), 'config' (configuration), or 'image' (container) (default: 'fs') +- `severity`: Severity levels to report (comma-separated: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) (default: 'HIGH,CRITICAL') +- `scan-path`: Path to scan (default: '.') +- `exit-code`: Exit code when vulnerabilities found (0=continue, 1=fail) (default: '0') +- `upload-to-security-tab`: Upload SARIF to GitHub Security tab (default: true) +- `generate-sbom`: Generate SBOM artifacts (default: true) - %% Sub-Scripts Layer - subgraph sub_scripts ["⚙️ Workers"] - ci_set_ver["ci_set_version.sh
(version injection)"] - ci_build_fw["ci_build_firmware.sh
(PlatformIO build)"] - ci_prep_art["ci_prepare_artifacts.sh
(artifact packaging)"] - gen_board["generate_board_docs.py
(board docs)"] - gen_wu["gen_wu.py
(WebUpdater manifest)"] - clang_fmt["clang-format
(code formatter)"] - end -end +**What it does**: +1. **Install Trivy**: Retrieves and installs Trivy vulnerability scanner +2. **Run security scan**: Calls unified `ci.sh security` with parameters: + - `--scan-type `: Target type + - `--severity `: Filter by severity + - `--scan-path `: Directory to scan + - `--generate-sbom`: Generate SBOM in CycloneDX and SPDX formats + - `--exit-code <0|1>`: Fail behavior on critical vulnerabilities + - `--upload-to-security-tab`: Upload SARIF to GitHub +3. **Upload artifacts**: Stores reports and SBOM for later download +4. **GitHub Security integration**: SARIF automatically appears in Security tab - %% Trigger connections - push --> lint - push --> build - - release --> release_wf - manual --> manual_docs - cron1 --> build_dev - cron2 --> stale - - %% Main workflow to task workflow connections - build -->|calls| task_build - lint -->|calls| task_lint - build_dev -->|calls| task_build - build_dev -->|calls| task_docs - release_wf -->|calls| task_build - release_wf -->|calls| task_docs - manual_docs -->|calls| task_docs - - %% Task workflows to CI scripts - task_build -->|"ci.sh build
--version --mode
--deploy-ready"| ci_main - task_docs -->|"ci.sh site
--mode --url-prefix
--version-source"| ci_main - task_lint -->|"ci.sh qa
--check --source
--extensions"| ci_main - - %% CI main dispatcher to orchestrators - ci_main -->|"route: build"| ci_build_script - ci_main -->|"route: site"| ci_site_script - ci_main -->|"route: qa"| ci_qa_script - - %% Orchestrators to workers - ci_build_script --> ci_set_ver - ci_build_script --> ci_build_fw - ci_build_script --> ci_prep_art - - ci_site_script --> gen_board - ci_site_script --> gen_wu - - ci_qa_script --> clang_fmt - - %% Styling - classDef triggerStyle fill:#e1f5ff,stroke:#0066cc,stroke-width:2px - classDef mainStyle fill:#fff4e6,stroke:#ff9900,stroke-width:2px - classDef taskStyle fill:#e6f7e6,stroke:#00aa00,stroke-width:2px - classDef ciStyle fill:#ffe6f0,stroke:#cc0066,stroke-width:2px - classDef subStyle fill:#f0e6ff,stroke:#9933ff,stroke-width:2px - - class push,release,manual,cron1,cron2 triggerStyle - class build,lint,release_wf,manual_docs,build_dev,stale mainStyle - class task_build,task_docs,task_lint taskStyle - class ci_main,ci_build_script,ci_site_script,ci_qa_script ciStyle - class ci_set_ver,ci_build_fw,ci_prep_art,gen_board,gen_wu,clang_fmt subStyle - - - style github_workflows stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:1.8px,fill:#fbfbfc - style main stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fcfdff - style tasks stroke:#6A7BD8,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fcfdff - - style ci_scripts stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:1.8px,fill:#fffaf5 - style bash stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fffaf5 - style sub_scripts stroke:#FF9A3C,stroke-dasharray:6 4,stroke-width:0.6px,fill:#fffaf5 - - style triggers fill:none,stroke:none - +**Command Flow**: +```bash +./scripts/ci.sh security --scan-type fs --severity HIGH,CRITICAL --generate-sbom --upload-to-security-tab + ↓ + └─→ ci_security.sh (security orchestrator) + └─→ Trivy (vulnerability scanner) + ├─→ Generate SARIF, JSON, summary + ├─→ Generate SBOM (CycloneDX, SPDX) + └─→ Upload to GitHub Security tab ``` -### Workflow Relationships +**Technical Details**: +- Runs on: Ubuntu latest +- Scanner: Trivy (latest version auto-installed) +- Report formats: SARIF (GitHub integration), JSON (detailed), Markdown (summary) +- SBOM formats: CycloneDX and SPDX (industry standards) +- GitHub Security tab: Auto-uploads SARIF for code scanning dashboard +- Strategy: Single sequential job (not parallelized) +- Artifact retention: As configured by caller -**Main → Task Mapping**: -- `build.yml` → calls `task-build.yml` (also contains inline documentation job) -- `lint.yml` → calls `task-lint.yml` -- `build_and_docs_to_dev.yml` → calls `task-build.yml` + `task-docs.yml` (with prepare/deploy jobs) -- `release.yml` → calls `task-build.yml` + `task-docs.yml` (with prepare/deploy jobs) -- `manual_docs.yml` → calls `task-docs.yml` -- `stale.yml` → standalone (no dependencies) +**Output Files**: +- `generated/reports/trivy-results.sarif` - SARIF format (GitHub Security tab upload) +- `generated/reports/trivy-results.json` - JSON format (detailed results) +- `generated/reports/security-summary.md` - Human-readable summary +- `generated/reports/sbom/sbom.cyclonedx.json` - CycloneDX SBOM +- `generated/reports/sbom/sbom.spdx.json` - SPDX SBOM -**Task → CI Script Mapping**: -- `task-build.yml` → `ci.sh build --version --mode --deploy-ready` - - Routes to: `ci_build.sh` → `ci_set_version.sh`, `ci_build_firmware.sh`, `ci_prepare_artifacts.sh` - - Output: `generated/artifacts/` (default, can be overridden with `--output`) -- `task-docs.yml` → `ci.sh site --mode --version-source --url-prefix --webuploader-args` - - Routes to: `ci_site.sh` → `generate_board_docs.py`, `gen_wu.py`, VuePress - - Output: `generated/site/` -- `task-lint.yml` → `ci.sh qa --check --source --extensions --clang-format-version` - - Routes to: `ci_qa.sh` → `clang-format` - -**Job Dependencies**: -- `build_and_docs_to_dev.yml`: prepare → build (task) → deploy & documentation (task) -- `release.yml`: prepare → build (task) → deploy → documentation (task) - -**Script Execution Flow**: -``` -GitHub Action (task-*.yml) - ↓ -./scripts/ci.sh [OPTIONS] ← Main dispatcher - ↓ -./scripts/ci_.sh ← Command orchestrator - ↓ -./scripts/ci_*.sh / *.py ← Worker scripts -``` +**Callers**: +- `security-scan.yml` (weekly + manual scanning) +- Can be called by other workflows for custom security workflows --- @@ -562,25 +663,17 @@ All build environments are defined in `.github/workflows/environments.json`: ```json { "environments": { - "all": [ ...83 environments ], - "metadata": { - "totalCount": 83, - "lastUpdated": "2024-12-25", - "categories": { - "esp32": 50, - "esp8266": 20, - "specialized": 13 - } + "all": [ ...all environments ], + "ci": [...a subset of environments] } } } ``` -**Benefits**: -- ✅ Single source of truth -- ✅ Eliminates duplication across workflows -- ✅ Easier to maintain and update -- ✅ Consistent builds across CI/dev/release +**Environment Sets**: +- `all`: Complete production set for releases and comprehensive validation +- `ci`: Representative subset for fast CI feedback + ### Environment Categories @@ -653,24 +746,33 @@ All build environments are defined in `.github/workflows/environments.json`: - PlatformIO 6.1.18 from custom fork: `pioarduino/platformio-core` - Python package manager: `uv` for fast dependency installation - Orchestrator: `ci_build.sh` - - Worker: `ci_set_version.sh` (version injection) - Worker: `ci_build_firmware.sh` (PlatformIO compilation) - Worker: `ci_prepare_artifacts.sh` (artifact packaging) **Documentation System** (`ci.sh site`): - Documentation framework: VuePress - Orchestrator: `ci_site.sh` - - Worker: `generate_board_docs.py` (auto-generate board pages) - - Worker: `gen_wu.py` (WebUpdater manifest) + - Worker: `generate_board_docs` (npm package - auto-generate board pages) + - Worker: `gen_wu` (npm package - WebUpdater manifest) - External: Common config from theengs.io **Code Quality** (`ci.sh qa`): - Orchestrator: `ci_qa.sh` - Worker: `clang-format` version 9 + - Worker: `shellcheck` for shell scripts - Default scope: `main` directory, `.h` and `.ino` files +**Security Scanning System** (`ci.sh security`): +- Vulnerability scanner: Trivy (container vulnerability database) +- Orchestrator: `ci_security.sh` + - Worker: Trivy (filesystem, configuration, and container image scanning) + - Report formats: SARIF (GitHub Security tab), JSON (detailed), Markdown (summary) + - SBOM generation: CycloneDX and SPDX formats + - GitHub Security integration: Auto-uploads SARIF for code scanning dashboard + - Output: `generated/reports/` (security findings and artifacts) + **Configuration**: -- Environment list: `.github/workflows/environments.json` (83 environments) +- Environment list: `.github/workflows/environments.json` - Task workflows: `task-*.yml` (reusable GitHub Actions components) - Repository owner restriction: Hardcoded to `1technophile` for dev deployments - All scripts located in: `./scripts/` @@ -681,17 +783,20 @@ All build environments are defined in `.github/workflows/environments.json`: ./scripts/ci.sh build esp32dev-ble --mode dev --version test # Build documentation locally -./scripts/ci.sh site --mode dev --preview +./scripts/ci.sh site --mode dev --version test # Check code format -./scripts/ci.sh qa --check +./scripts/ci.sh qa --check --source main --extensions h,ino --clang-format-version 9 + +# Security scanning +./scripts/ci.sh security --scan-type fs --severity HIGH,CRITICAL --generate-sbom # Run complete pipeline -./scripts/ci.sh all esp32dev-ble --version v1.8.0 +./scripts/ci.sh all --mode dev ``` --- -**Document Version**: 2.1 -**Last Updated**: December 29, 2025 +**Document Version**: 2.3 +**Last Updated**: 01/14/2026 **Maintainer**: OpenMQTTGateway Development Team diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 818e6cf9..09f44f98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,30 +1,47 @@ name: Build +# See details in .github/workflows/README.md (workflow docs) +run-name: "CI build for ${{ github.ref_name }}" on: [push, pull_request] jobs: + determine-build-scope: + runs-on: ubuntu-latest + outputs: + environment-set: ${{ steps.decide.outputs.environment-set }} + steps: + - name: Decide environment set + id: decide + run: | + # Full build for: development, master, edge, stable, release/*, hotfix/*, and all PRs + if [[ "${{ github.event_name }}" == "pull_request" ]] || \ + [[ "${{ github.ref }}" == "refs/heads/development" ]] || \ + [[ "${{ github.ref }}" == "refs/heads/master" ]] || \ + [[ "${{ github.ref }}" == "refs/heads/edge" ]] || \ + [[ "${{ github.ref }}" == "refs/heads/stable" ]] || \ + [[ "${{ github.ref }}" == refs/heads/release/* ]] || \ + [[ "${{ github.ref }}" == refs/heads/hotfix/* ]]; then + echo "environment-set=all" >> $GITHUB_OUTPUT + else + echo "environment-set=ci" >> $GITHUB_OUTPUT + fi + + build: + needs: determine-build-scope name: Build firmware uses: ./.github/workflows/task-build.yml with: - python-version: '3.13' - enable-dev-ota: false + enable-dev-ota: true artifact-retention-days: 7 - prepare-for-deploy: false + environment-set: ${{ needs.determine-build-scope.outputs.environment-set }} documentation: + needs: build name: Build documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "14.x" - - name: Download Common Config - run: | - curl -o docs/.vuepress/public/commonConfig.js https://www.theengs.io/commonConfig.js - - name: Install build dependencies - run: npm install - - name: Build documentation - run: npm run docs:build + uses: ./.github/workflows/task-docs.yml + with: + mode: "dev" + run-pagespeed: false + just-check: true + diff --git a/.github/workflows/build_and_docs_to_dev.yml b/.github/workflows/build_and_docs_to_dev.yml index 7f698fd1..02cc07ce 100644 --- a/.github/workflows/build_and_docs_to_dev.yml +++ b/.github/workflows/build_and_docs_to_dev.yml @@ -3,7 +3,7 @@ name: Build binaries, docs and publish to dev folder on: workflow_dispatch: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: prepare: @@ -18,86 +18,26 @@ jobs: with: length: 6 - build: + handle-firmwares: needs: prepare - name: Build development firmware + name: Build and deploy development firmwares artefacts uses: ./.github/workflows/task-build.yml with: - python-version: '3.13' enable-dev-ota: true version-tag: ${{ needs.prepare.outputs.short-sha }} artifact-retention-days: 1 - artifact-name-prefix: 'firmware-' - prepare-for-deploy: true - deploy: - needs: [prepare, build] - runs-on: ubuntu-latest - if: github.repository_owner == '1technophile' - name: Deploy binaries and docs - steps: - - uses: actions/checkout@v4 - - - name: Download all firmware artifacts - uses: actions/download-artifact@v4 - with: - pattern: firmware-* - path: toDeploy - merge-multiple: true - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: "latest" - enable-cache: false - - - name: Install dependencies - run: | - uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip - - - name: Create library zips - run: | - # Install libraries for a representative environment to get libdeps - platformio pkg install -e esp32dev-ble --no-save - cd .pio/libdeps - # Replace spaces with underscores in folder names - find . -type d -name "* *" | while read FNAME; do mv "$FNAME" "${FNAME// /_}"; done - # Zip libraries per board - for i in */; do - zip -r "${i%/}-libraries.zip" "$i" - done - mv *.zip ../../toDeploy/ - - - name: Prepare additional assets - run: | - cd toDeploy - # Remove binaries for *-all*, *-test* env - rm -f *-all*.bin *-test*.bin *-test*.zip || true - cd .. - # Zip source code - zip -r toDeploy/OpenMQTTGateway_sources.zip main LICENSE.txt - ls -lA toDeploy/ - - documentation: - needs: [prepare, build] + handle-documentation: + needs: [prepare, handle-firmwares] name: Build and deploy development documentation uses: ./.github/workflows/task-docs.yml with: - python-version: '3.11' - node-version: '16.x' - version-source: 'custom' - custom-version: 'DEVELOPMENT SHA:${{ needs.prepare.outputs.short-sha }} TEST ONLY' - base-path: '/dev/' - destination-dir: 'dev' - generate-webuploader: true - webuploader-args: '--dev' + mode: "dev" + version: ${{ needs.prepare.outputs.short-sha }} + url-prefix: "/dev/" + destination-dir: "dev" run-pagespeed: true pagespeed-url: 'https://docs.openmqttgateway.com/dev/' secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APIKEY: ${{ secrets.APIKEY }} + github-token: ${{ secrets.GITHUB_TOKEN }} + pagespeed-apikey: ${{ secrets.APIKEY }} \ No newline at end of file diff --git a/.github/workflows/environments.json b/.github/workflows/environments.json index 4f0b145b..fa2a2eff 100644 --- a/.github/workflows/environments.json +++ b/.github/workflows/environments.json @@ -88,19 +88,22 @@ "sonoff-basic", "sonoff-basic-rfr3" ], - "metadata": { - "totalCount": 83, - "categories": { - "esp32": 42, - "esp8266": 17, - "specialized": 24 - }, - "notes": [ - "This unified list contains all environments from both build.yml and build_and_docs_to_dev.yml", - "Previously build.yml had 85 environments and build_and_docs_to_dev.yml had 88", - "After deduplication and proper ordering, the total is 83 unique environments", - "Environments ending in -test or -all-test are typically excluded from production releases" - ] - } + "ci": [ + "esp32dev-all-test", + "esp32dev-ble", + "esp32dev-ir", + "esp32dev-rf", + "esp32dev-rtl_433", + "esp32s3-dev-c1-ble", + "esp32c3-dev-m1-ble", + "heltec-ble", + "lilygo-ble", + "nodemcuv2-all-test", + "nodemcuv2-ir", + "nodemcuv2-rf", + "theengs-bridge", + "theengs-plug", + "rfbridge" + ] } } \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 7c2b2125..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Check Code Format - -on: [push, pull_request] - -jobs: - lint: - name: Lint code format - uses: ./.github/workflows/task-lint.yml - with: - source: 'main' - extensions: 'h,ino' - clang-format-version: '9' diff --git a/.github/workflows/manual_docs.yml b/.github/workflows/manual_docs.yml index e75414cf..b79afd59 100644 --- a/.github/workflows/manual_docs.yml +++ b/.github/workflows/manual_docs.yml @@ -9,13 +9,9 @@ jobs: name: Build and deploy production documentation uses: ./.github/workflows/task-docs.yml with: - python-version: '3.11' - node-version: '14.x' - version-source: 'release' - base-path: '/' - destination-dir: '.' - generate-webuploader: true - webuploader-args: '${GITHUB_REPOSITORY}' - run-pagespeed: false + mode: "prod" + version: "auto" # Will detect latest git tag + url-prefix: "/" + destination-dir: "." secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee93156d..a2809fdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: VERSION_TAG=${GITHUB_REF#refs/tags/} echo "version=${VERSION_TAG}" >> $GITHUB_OUTPUT echo "Extracted version: ${VERSION_TAG}" - + - name: Extract release info id: extract-release run: | @@ -33,63 +33,26 @@ jobs: name: Build release firmware uses: ./.github/workflows/task-build.yml with: - python-version: '3.13' enable-dev-ota: false version-tag: ${{ needs.prepare.outputs.version-tag }} artifact-retention-days: 90 - artifact-name-prefix: 'firmware-' - prepare-for-deploy: true deploy: needs: [prepare, build] runs-on: ubuntu-latest name: Deploy release assets steps: - - uses: actions/checkout@v4 - - name: Download all firmware artifacts uses: actions/download-artifact@v4 with: - pattern: firmware-* - path: .pio/build - merge-multiple: false - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: "latest" - enable-cache: false - - - name: Install platformio - run: | - uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip - - - name: Reorganize artifacts for prepare_deploy.sh - run: | - # Move artifacts from download structure to expected .pio/build structure - for env_dir in .pio/build/firmware-*; do - if [ -d "$env_dir" ]; then - env_name=$(basename "$env_dir" | sed 's/^firmware-//') - mkdir -p ".pio/build/${env_name}" - mv "$env_dir"/*.bin ".pio/build/${env_name}/" 2>/dev/null || true - rm -rf "$env_dir" - fi - done - - - name: Prepare Release Assets - run: | - sudo apt install rename - ./scripts/prepare_deploy.sh + pattern: "*" + path: generated/artifacts + merge-multiple: true - name: Upload Release Assets uses: bgpat/release-asset-action@03b0c30db1c4031ce3474740b0e4275cd7e126a3 with: - pattern: "toDeploy/*" + pattern: "generated/artifacts/*" github-token: ${{ secrets.GITHUB_TOKEN }} release-url: ${{ needs.prepare.outputs.upload-url }} allow-overwrite: true @@ -99,13 +62,9 @@ jobs: name: Build and deploy release documentation uses: ./.github/workflows/task-docs.yml with: - python-version: '3.11' - node-version: '18.x' - version-source: 'git-tag' - base-path: '/' - destination-dir: '.' - generate-webuploader: true - run-pagespeed: false + mode: "prod" + version: ${{ needs.prepare.outputs.version-tag }} + url-prefix: "/" + destination-dir: "." secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APIKEY: ${{ secrets.APIKEY }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 00000000..f27993c9 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,44 @@ +name: Security Scan + +on: + schedule: + - cron: '0 2 * * 1' # Every Monday at 2 AM + workflow_dispatch: + inputs: + severity: + description: 'Severity levels to scan' + required: false + type: choice + default: 'HIGH,CRITICAL' + options: + - 'CRITICAL' + - 'HIGH,CRITICAL' + - 'MEDIUM,HIGH,CRITICAL' + - 'LOW,MEDIUM,HIGH,CRITICAL' + scan-path: + description: 'Path to scan (default: entire repo)' + required: false + type: string + default: '.' + exit-on-error: + description: 'Fail if vulnerabilities found' + required: false + type: boolean + default: false + pull_request: + paths: + - 'main/**' + - 'lib/**' + - 'platformio.ini' + - '.github/workflows/**' + +jobs: + security-scan: + name: Scan for vulnerabilities + uses: ./.github/workflows/task-security-scan.yml + with: + scan-type: 'fs' + severity: ${{ inputs.severity || 'HIGH,CRITICAL' }} + exit-code: ${{ inputs.exit-on-error && '1' || '0' }} + upload-to-security-tab: true + scan-path: ${{ inputs.scan-path || '.' }} diff --git a/.github/workflows/task-build.yml b/.github/workflows/task-build.yml index 787ed1f3..88b356dd 100644 --- a/.github/workflows/task-build.yml +++ b/.github/workflows/task-build.yml @@ -4,37 +4,53 @@ on: workflow_call: inputs: python-version: - description: 'Python version to use' + description: "Python version to use" required: false type: string - default: '3.13' + default: "3.13" + pio-version: + description: "PlatformIO version to use" + required: false + type: string + default: "v6.1.18" + environment-set: + description: 'Which set of environments to build (all, ci)' + required: false + type: string + default: 'all' enable-dev-ota: - description: 'Enable development OTA builds' + description: "Enable development OTA builds" required: false type: boolean default: false version-tag: - description: 'Version tag to inject into firmware (e.g., SHA or release tag)' + description: "Optional version tag to pass to ci.sh build (omitted if empty)" required: false type: string - default: 'unspecified' + default: "" artifact-retention-days: - description: 'Number of days to retain build artifacts' + description: "Number of days to retain build artifacts" required: false type: number default: 7 - artifact-name-prefix: - description: 'Prefix for artifact names (e.g., "firmware-" for dev builds)' - required: false - type: string - default: '' - prepare-for-deploy: - description: 'Prepare firmware files for deployment with specific naming' - required: false - type: boolean - default: false jobs: + lint: + name: Lint code format + uses: ./.github/workflows/task-lint.yml + with: + source: "main" + extensions: "h,ino,cpp" + clang-format-version: "9" + + security-scan: + name: Security vulnerability scan + uses: ./.github/workflows/task-security-scan.yml + with: + severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" + exit-code: "1" + upload-to-security-tab: true + load-environments: runs-on: ubuntu-latest outputs: @@ -43,11 +59,17 @@ jobs: - uses: actions/checkout@v4 - id: set-matrix run: | - ENVIRONMENTS=$(jq -c '.environments.all' .github/workflows/environments.json) + ENV_SET="${{ inputs.environment-set }}" + if [ "$ENV_SET" = "ci" ]; then + ENVIRONMENTS=$(jq -c '.environments.ci' .github/workflows/environments.json) + else + ENVIRONMENTS=$(jq -c '.environments.all' .github/workflows/environments.json) + fi echo "matrix=${ENVIRONMENTS}" >> $GITHUB_OUTPUT + build: - needs: load-environments + needs: [lint, security-scan, load-environments] runs-on: ubuntu-latest strategy: fail-fast: false @@ -70,35 +92,29 @@ jobs: - name: Install PlatformIO dependencies run: | - uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/${{ inputs.pio-version }}.zip - name: Build firmware using ci.sh run: | - BUILD_ARGS="${{ matrix.environments }}" - - # Add version tag if specified - if [ "${{ inputs.version-tag }}" != "unspecified" ]; then + BUILD_ARGS="${{ matrix.environments }}" + BUILD_ARGS="$BUILD_ARGS --deploy-ready" + + # Optional version tag + if [ -n "${{ inputs.version-tag }}" ]; then BUILD_ARGS="$BUILD_ARGS --version ${{ inputs.version-tag }}" fi - - # Add mode flag (dev/prod) + + # Mode if [ "${{ inputs.enable-dev-ota }}" = "true" ]; then BUILD_ARGS="$BUILD_ARGS --mode dev" else BUILD_ARGS="$BUILD_ARGS --mode prod" fi - - # Add deploy flag if needed - if [ "${{ inputs.prepare-for-deploy }}" = "true" ]; then - BUILD_ARGS="$BUILD_ARGS --deploy-ready" - fi - - # Execute build (uses default generated/artifacts/ directory) ./scripts/ci.sh build $BUILD_ARGS - name: Upload firmware artifacts uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-name-prefix }}${{ matrix.environments }} + name: ${{ matrix.environments }} path: generated/artifacts/ retention-days: ${{ inputs.artifact-retention-days }} diff --git a/.github/workflows/task-docs.yml b/.github/workflows/task-docs.yml index 6f962518..4c4c73f7 100644 --- a/.github/workflows/task-docs.yml +++ b/.github/workflows/task-docs.yml @@ -3,61 +3,59 @@ name: Reusable Documentation Workflow on: workflow_call: inputs: - python-version: - description: 'Python version to use' + mode: + description: 'Build mode: "prod" or "dev"' required: false type: string - default: '3.11' - node-version: - description: 'Node.js version to use' + default: "prod" + version: + description: 'Version string for documentation (use "auto" for automatic detection)' required: false type: string - default: '14.x' - version-source: - description: 'Source of version: "release" (from GitHub release) or "custom" (provided)' - required: false - type: string - default: 'release' - custom-version: - description: 'Custom version string when version-source is "custom"' - required: false - type: string - default: '' - base-path: - description: 'Base path for documentation (e.g., "/" for prod, "/dev/" for dev)' - required: false - type: string - default: '/' - destination-dir: - description: 'Destination directory for GitHub Pages deployment' - required: false - type: string - default: '.' - generate-webuploader: - description: 'Generate WebUploader manifest' + default: "auto" + just-check: + description: "Just create a quick build to check for errors" required: false type: boolean - default: true - webuploader-args: - description: 'Additional arguments for gen_wu.py script' + default: false + url-prefix: + description: 'URL prefix for docs (e.g., "/" for prod, "/dev/" for dev)' required: false type: string - default: '' + default: "/" run-pagespeed: - description: 'Run PageSpeed Insights after deployment' + description: "Run PageSpeed Insights after deploy" required: false type: boolean default: false pagespeed-url: - description: 'URL to test with PageSpeed Insights' + description: "URL to test with PageSpeed" required: false type: string - default: 'https://docs.openmqttgateway.com/' - secrets: - GITHUB_TOKEN: - required: true - APIKEY: + default: "https://docs.openmqttgateway.com/" + destination-dir: + description: "GitHub Pages destination directory" required: false + type: string + default: "." + python-version: + description: "Python version to use" + required: false + type: string + default: "3.13" + pio-version: + description: "PlatformIO version to use" + required: false + type: string + default: "v6.1.18" + secrets: + github-token: + description: "GitHub token for deploying to GitHub Pages" + required: false + pagespeed-apikey: + description: "API key for PageSpeed Insights" + required: false + jobs: documentation: @@ -65,63 +63,84 @@ jobs: name: Create and deploy documentation steps: - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 with: - node-version: ${{ inputs.node-version }} + fetch-depth: 0 # Fetch all history for git tags - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - - name: Build documentation site + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + + - name: Install PlatformIO dependencies run: | - # Build script arguments based on inputs - ARGS="" - - # Set version source and custom version - if [ "${{ inputs.version-source }}" = "custom" ] && [ -n "${{ inputs.custom-version }}" ]; then - ARGS="$ARGS --custom-version '${{ inputs.custom-version }}' --version-source custom" - elif [ "${{ inputs.version-source }}" = "release" ]; then - # Get latest release tag - if command -v git >/dev/null 2>&1; then - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "development") - ARGS="$ARGS --custom-version '${LATEST_TAG}' --version-source release" + uv pip install --system -U "https://github.com/pioarduino/platformio-core/archive/refs/tags/${{ inputs.pio-version }}.zip" + + - name: Determine version + id: version + run: | + VERSION="${{ inputs.version }}" + MODE="${{ inputs.mode }}" + + # If version is 'auto', determine it based on mode + if [ "$VERSION" = "auto" ]; then + if [ "$MODE" = "dev" ]; then + # Dev mode: always use short SHA + VERSION="$(git rev-parse --short HEAD)" + echo "Dev mode - using SHA: $VERSION" else - ARGS="$ARGS --version-source release" + # Prod mode: try to get git tag + if GIT_TAG=$(git describe --tags --exact-match 2>/dev/null); then + VERSION="$GIT_TAG" + echo "Using exact git tag: $VERSION" + elif LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null); then + VERSION="$LATEST_TAG" + echo "Using latest tag: $VERSION" + else + VERSION="v0.0.0-unknown" + echo "Warning: No tag found, using: $VERSION" + fi fi fi - - # Set base path/URL prefix - if [ "${{ inputs.base-path }}" != "/" ]; then - ARGS="$ARGS --url-prefix '${{ inputs.base-path }}'" - fi - - # WebUploader generation - if [ "${{ inputs.generate-webuploader }}" != "true" ]; then - ARGS="$ARGS --no-webuploader" - elif [ -n "${{ inputs.webuploader-args }}" ]; then - ARGS="$ARGS --webuploader-args '${{ inputs.webuploader-args }}'" - fi - - # Run the build script - eval "./scripts/ci_site.sh ${ARGS}" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Final version: $VERSION" + + - name: Download all firmware artifacts + if: ${{ inputs.mode == 'dev' }} + uses: actions/download-artifact@v4 + with: + pattern: "*" + path: generated/artifacts + merge-multiple: true + + - name: Build documentation site + run: | + ./scripts/ci.sh site \ + --clean \ + --mode ${{ inputs.mode }} \ + --version ${{ steps.version.outputs.version }} \ + --url-prefix ${{ inputs.url-prefix }} - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 + if: ${{ inputs.just-check == false }} with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.github-token }} publish_dir: ./generated/site destination_dir: ${{ inputs.destination-dir }} cname: docs.openmqttgateway.com - name: Run PageSpeed Insights - if: inputs.run-pagespeed + if: ${{ inputs.just-check == false && inputs.run-pagespeed == true }} uses: jakepartusch/psi-action@v1.3 id: psi with: url: ${{ inputs.pagespeed-url }} threshold: 60 - key: ${{ secrets.APIKEY }} + key: ${{ secrets.pagespeed-apikey }} diff --git a/.github/workflows/task-lint.yml b/.github/workflows/task-lint.yml index e7ba6acf..3eb60040 100644 --- a/.github/workflows/task-lint.yml +++ b/.github/workflows/task-lint.yml @@ -4,20 +4,20 @@ on: workflow_call: inputs: source: - description: 'Source directory to lint (single directory)' + description: "Source directory to lint" required: false type: string - default: 'main' + default: "main" extensions: - description: 'File extensions to check (comma-separated)' + description: "File extensions to check (comma-separated)" required: false type: string - default: 'h,ino,cpp' + default: "h,ino,cpp" clang-format-version: - description: 'clang-format version to use' + description: "clang-format version to use" required: false type: string - default: '9' + default: "9" jobs: lint: @@ -25,16 +25,16 @@ jobs: name: Check code format steps: - uses: actions/checkout@v4 - - name: Install clang-format run: | sudo apt-get update - sudo apt-get install -y clang-format - - - name: Check code format with ci.sh qa + sudo apt-get install -y clang-format shellcheck + + - name: Check code format with ci.sh qa run: | ./scripts/ci.sh qa \ --check \ --source "${{ inputs.source }}" \ --extensions "${{ inputs.extensions }}" \ - --clang-format-version "${{ inputs.clang-format-version }}" \ No newline at end of file + --clang-format-version "${{ inputs.clang-format-version }}" + diff --git a/.github/workflows/task-security-scan.yml b/.github/workflows/task-security-scan.yml new file mode 100644 index 00000000..cc193571 --- /dev/null +++ b/.github/workflows/task-security-scan.yml @@ -0,0 +1,124 @@ +name: Reusable Security Scan Workflow + +on: + workflow_call: + inputs: + scan-type: + description: 'Type of scan: fs (filesystem), config, or image' + required: false + type: string + default: 'fs' + severity: + description: 'Severity levels to report (comma-separated: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL)' + required: false + type: string + default: 'HIGH,CRITICAL' + exit-code: + description: 'Exit code when vulnerabilities found (0=continue, 1=fail)' + required: false + type: string + default: '0' + upload-to-security-tab: + description: 'Upload SARIF to GitHub Security tab (GitHub-specific feature)' + required: false + type: boolean + default: true + scan-path: + description: 'Path to scan (default: entire repository)' + required: false + type: string + default: '.' + generate-sbom: + description: 'Generate Software Bill of Materials (SBOM) in CycloneDX and SPDX formats' + required: false + type: boolean + default: true + +jobs: + security-scan: + runs-on: ubuntu-latest + name: Security vulnerability scan + permissions: + contents: read + security-events: write # Required only for GitHub Security tab upload + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Trivy + run: | + set -euo pipefail + curl -fsSL https://aquasecurity.github.io/trivy-repo/deb/public.key \ + | gpg --dearmor \ + | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" \ + | sudo tee /etc/apt/sources.list.d/trivy.list > /dev/null + sudo apt-get update + sudo apt-get install -y trivy + + - name: Run Security Scan using ci_security.sh + run: | + set -euo pipefail + sbom_flag="" + if [[ "${{ inputs.generate-sbom }}" == "true" ]]; then + sbom_flag="--generate-sbom" + fi + + ./scripts/ci_security.sh \ + --scan-type "${{ inputs.scan-type }}" \ + --scan-path "${{ inputs.scan-path }}" \ + --severity "${{ inputs.severity }}" \ + --exit-code "${{ inputs.exit-code }}" \ + ${sbom_flag} + + # GitHub-specific: Upload to Security tab + - name: Upload SARIF to GitHub Security tab + if: inputs.upload-to-security-tab == true && always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: './generated/reports/trivy-results.sarif' + category: 'trivy-security-scan' + + # Agnostic: Upload all reports as artifacts (works on any CI) + - name: Upload security reports as artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-scan-reports + path: | + ./generated/reports/trivy-results.sarif + ./generated/reports/trivy-report.json + ./generated/reports/trivy-report.txt + ./generated/reports/security-summary.md + ./generated/reports/trivy-scan.log + retention-days: 90 + + # Display summary in workflow output + - name: Display Security Summary + if: always() + run: | + cat ./generated/reports/security-summary.md >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Summary not available" + + # + - name: Upload SBOM reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: sbom-reports + path: | + ./generated/reports/sbom/ + retention-days: 90 + + # Optional: Fail the build if critical vulnerabilities found + - name: Check for critical vulnerabilities + if: inputs.exit-code == '1' + run: | + if [ -f ./generated/reports/trivy-results.sarif ]; then + CRITICAL=$(jq '[.runs[].results[] | select(.level == "error")] | length' ./generated/reports/trivy-results.sarif 2>/dev/null || echo "0") + if [ "$CRITICAL" -gt 0 ]; then + echo "❌ Found ${CRITICAL} critical vulnerabilities!" + echo "Review the security reports in artifacts." + exit 1 + fi + fi diff --git a/.gitignore b/.gitignore index 18c8a66c..276ab074 100644 --- a/.gitignore +++ b/.gitignore @@ -25,11 +25,24 @@ managed_components .github/workflows/_docs # CI/CD Generated outputs docs/.vuepress/dist/ -docs/.vuepress/components/ -docs/.vuepress/public/ -^docs/.vuepress/public/img/ -^docs/.vuepress/public/*.png -^docs/.vuepress/public/*.txt +docs/.vuepress/public/boards-info.json +docs/.vuepress/public/commonConfig.js +docs/.vuepress/public/firmware_build + +# Generated site/firmware/reports files generated/ +# Generated ssl for testing +.ssl + +# Generated JSON files from templates +scripts/latest_version.json +scripts/latest_version_dev.json + +# Generated configuration files +docs/.vuepress/meta.json + +# Generated full documentation pages +docs/prerequisites/board-full.md +docs/upload/web-install-full.md diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..db9bb426 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +disable=SC1091 \ No newline at end of file diff --git a/docs/.vuepress/components/BoardEnvironmentTable.vue b/docs/.vuepress/components/BoardEnvironmentTable.vue new file mode 100644 index 00000000..af5e4fec --- /dev/null +++ b/docs/.vuepress/components/BoardEnvironmentTable.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/docs/.vuepress/components/FlashEnvironmentSelector.vue b/docs/.vuepress/components/FlashEnvironmentSelector.vue new file mode 100644 index 00000000..94c3b37c --- /dev/null +++ b/docs/.vuepress/components/FlashEnvironmentSelector.vue @@ -0,0 +1,686 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 4a4eea07..0662e748 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,20 +1,35 @@ +let meta = require('./defaults.json'); +try { + meta_overload = require('./meta.json'); + meta = { ...meta, ...meta_overload }; +} catch (e) { + console.warn('meta.json not found or not valid. Using default configuration.'); +} + + + +const fs = require('fs'); +const path = require('path'); +const commonConfigPath = path.resolve(__dirname, 'public/commonConfig.js'); +if (!fs.existsSync(commonConfigPath)) { + throw new Error(`commonConfig.js not found in ${commonConfigPath}.\nPlease download from https://www.theengs.io/commonConfig.js or create this file before you build the documentation.`); +} const commonConfig = require('./public/commonConfig'); module.exports = { ...commonConfig, - title: 'Theengs OpenMQTTGateway version_tag', - base: '/', + title: `${meta.title} - ${meta.version}`, + base: meta.url_prefix, + dest: meta.dest, // default is generated/site description: 'One gateway, many technologies: MQTT gateway for ESP8266 or ESP32 with bidirectional 433mhz/315mhz/868mhz, Infrared communications, BLE, LoRa, beacons detection, mi flora / mi jia / LYWSD02/ Mi Scale compatibility, SMS & LORA.', - head: [ - ...commonConfig.head, - ['script', {type: 'module', src: 'https://unpkg.com/esp-web-tools@9.4.3/dist/web/install-button.js?module'}] - ], + head: [...commonConfig.head], themeConfig: { - repo: '1technophile/OpenMQTTGateway', - docsDir: 'docs', ...commonConfig.themeConfig, + repo: meta.theme_config_repo, + docsDir: 'docs', + mode: meta.mode, sidebar: [ - ['/','0 - What is it for 🏠'], + ['/', '0 - What is it for 🏠'], { title: '1 - Prerequisites🧭', // required //collapsable: true, // optional, defaults to true @@ -42,9 +57,10 @@ module.exports = { }, { title: '3 - Upload ➡️', // required + path: '/upload/', sidebarDepth: 1, // optional, defaults to 1 children: [ - 'upload/web-install', + ['upload/web-install', "(Option 1) Upload from the web"], 'upload/binaries', 'upload/builds', 'upload/gitpod', @@ -86,23 +102,25 @@ module.exports = { }, { title: '6 - Participate 💻', // required + path: '/participate/', sidebarDepth: 1, // optional, defaults to 1 children: [ + 'participate/quick_start', 'participate/support', 'participate/development', 'participate/adding-protocols', 'participate/community', - ['https://github.com/1technophile/OpenMQTTGateway/blob/development/LICENSE.txt','License'] + [meta.url_license_file, 'License'] ] } - ] + ] }, plugins: { 'sitemap': { - hostname: 'https://docs.openmqttgateway.com', + hostname: meta.hostname, urls: [ 'https://decoder.theengs.io/devices/devices.html', - 'https://community.openmqttgateway.com/', + meta.url_community_forum, 'https://shop.theengs.io/', 'https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption', 'https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna', diff --git a/docs/.vuepress/defaults.json b/docs/.vuepress/defaults.json new file mode 100644 index 00000000..2c8ddcae --- /dev/null +++ b/docs/.vuepress/defaults.json @@ -0,0 +1,12 @@ +{ + "title": "Theengs OpenMQTTGateway", + "version": "edge", + "url_prefix": "/", + "dest": "generated/site", + "mode": "prod", + "url_license_file": "https://github.com/1technophile/OpenMQTTGateway/blob/development/LICENSE.txt", + "url_community_forum": "https://community.openmqttgateway.com", + "hostname": "https://docs.openmqttgateway.com", + "repo": "1technophile/OpenMQTTGateway", + "theme_config_repo": "1technophile/OpenMQTTGateway" +} \ No newline at end of file diff --git a/docs/.vuepress/public/img/microcontroller.gif b/docs/.vuepress/public/img/microcontroller.gif new file mode 100644 index 0000000000000000000000000000000000000000..40a9bc2f775c32260e3c1f8e81d8153ff3c1c476 GIT binary patch literal 331744 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%0D-fku=3N`*hj`_wq*ktRZ2Ss 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 + + +::: warning Development Version +This is the edge version of the documentation. It is under active development and may contain bugs, incomplete features, or breaking changes. Use it at your own risk. +::: + + + + OpenMQTTGateway aims to unify various technologies and protocols into a single firmware. This reduces the need for multiple physical bridges and streamlines diverse technologies under the widely-used [MQTT](http://mqtt.org/) protocol.
diff --git a/docs/img/upload.png b/docs/img/upload.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea66169b7080f8e8fb21a54213b938f604093e3 GIT binary patch literal 20549 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 +- Visual Studio Code from +- Git Bash comes with "Git for Windows". + +On Linux you can install these with your package manager (for example apt on Ubuntu). For more help you can read the Git book: . + +**Check (common tools):** open a terminal (Git Bash, WSL, or Linux shell) and run: + +```bash +git --version +``` + +If this command fails, install Git again and then close and reopen the terminal. + +### 2.2 Tools for firmware development + +For firmware builds you need **PlatformIO**. PlatformIO itself will install the right compiler and frameworks for your boards. + +You have two main options: + +1. Use **PlatformIO inside VS Code** (recommended for most users). +2. Use **PlatformIO CLI in the terminal** (good for advanced or CI usage). + +#### 2.2.1 PlatformIO inside VS Code (recommended) + +If you only plan to build firmware from VS Code, you usually do **not** need to install Python or PlatformIO CLI manually. The PlatformIO extension will download what it needs. + +- Just install VS Code and the PlatformIO extension (see section 2.3). +- When you first open the project, PlatformIO will set up its own tools. + +**Check:** after installing the extension and opening the project, the VS Code status bar shows something like "PlatformIO: Ready". + +#### 2.2.2 PlatformIO command line (CLI) with Python (for terminal builds) + +If you want to use the bash scripts (ci.sh, ci_build_firmware.sh, etc.) or build from a plain terminal, you need Python and PlatformIO CLI. + +Install these extra tools: + +- Python 3.10 or newer +- PlatformIO CLI (pip package) + +PlatformIO is the main build system for this project. We will use it from VS Code and from the terminal. If you want to read more, the official docs are here: . + +On Windows you can download Python from: + +On Linux you can install Python with your package manager. + +Then install PlatformIO CLI globally with Python: + +```bash +python3 -m pip install -U platformio +``` + +**Check (firmware tools):** + +```bash +python3 --version +platformio --version +``` + +If you see version numbers, the install is good. If the platformio command is not found, check that Python added the scripts folder (for example ~/.local/bin on Linux) to your PATH, then open a new terminal and try again. + +### 2.3 Tools for documentation website (Node.js and npm) +::: warning Note +This section is **only needed** if you want to **build or preview the site on your own computer**. +::: + +If you want to build or edit the **documentation website**, you also need Node.js and npm. You do **not** need these tools just to build firmware. + +Install: + +- Node.js 18 or newer +- npm (usually comes with Node.js) + +On Windows you can get Node.js from: + +On Linux you can install Node.js with your package manager or by following the docs: . + +**Check (docs tools):** + +```bash +node --version +npm --version +``` + +If one of these commands fails, reinstall Node.js, close the terminal, and try again. + +### 2.4 PlatformIO extension in Visual Studio Code +Now add PlatformIO inside VS Code. You can also see pictures of these steps in many ESP32 + PlatformIO tutorials, for example on the PlatformIO site: . + +Steps (based on common PlatformIO + VS Code ESP32 guides, adapted for this project): + +1. Open Visual Studio Code. +2. Click the Extensions icon on the left. +3. In the search box, type "PlatformIO IDE". +4. Click **Install** on the "PlatformIO IDE" extension. + +When the install finishes, a new icon appears on the left sidebar. It looks like an ant or an alien head. This opens the PlatformIO view. + +**Check:** + +- In VS Code, you see the PlatformIO icon on the left. +- If you click it, PlatformIO opens without errors. + +If you do not see the icon, restart VS Code and wait a few seconds. + +**Friendly tip:** the first install can take several minutes because PlatformIO downloads tools for many boards. It happens only once. You can get a coffee while it works. + + + +## 3. Get the source code + +Now download this repository to your computer from GitHub. + +1. Open a terminal (Git Bash, WSL or Linux shell). +2. Choose a parent folder where you want to keep your projects. +3. Run: + +```bash +git clone https://github.com/1technophile/OpenMQTTGateway.git +cd OpenMQTTGateway +``` + +**Check:** + +- Run: + + ```bash + git status + ``` + +- The output should say "On branch" and "nothing to commit". +- In your file explorer or with: + + ```bash + ls + ``` + + you should see main, lib, scripts, docs and other files. + +If the clone fails, check your internet connection or a possible proxy. Then try again. If you are new to Git, the GitHub "Hello World" guide can also help: . + + + +## 4. Firmware development with Visual Studio Code and PlatformIO + +This section explains how to build and flash the firmware using Visual Studio Code with the PlatformIO extension. + +If you know the classic Arduino IDE, you can think of PlatformIO as a more powerful alternative. It manages many boards, libraries, and environments for you. The official introduction is here: . + +You do **not** create a new PlatformIO project. This repository is already a PlatformIO project. You just open it. + +### 4.1 Open the project in VS Code + +1. Start Visual Studio Code. +2. Click **File → Open Folder…**. +3. Select the OpenMQTTGateway folder you cloned in section 3. +4. Click **Open**. + +VS Code will load the folder. PlatformIO will detect the platformio.ini file and start to prepare its toolchain (compiler, libraries, board support). You do not need to install these by hand. + +**Check:** + +- In the bottom status bar you see messages like "PlatformIO: Installing" and then "PlatformIO: Ready". +- You can also open a terminal inside VS Code (View → Terminal) and run: + + ```bash + pio --version + ``` + + You should see the PlatformIO version. + +If PlatformIO stays stuck on "Installing" for a long time, close VS Code, reopen it and wait again. Check that your internet connection is working. + +### 4.2 Look at the PlatformIO project structure + +PlatformIO projects usually have folders like (you will see similar names in many tutorials): + +- src: main source code. +- lib: libraries. +- include: headers. +- platformio.ini: configuration for boards and environments. + +In OpenMQTTGateway the structure is a bit different, because it is a large project: + +- main: main C++ files for the firmware. +- lib: libraries used by the firmware. +- platformio.ini and environments.ini: list many PlatformIO environments. +- test: tests for PlatformIO unit test framework. + +**Check:** open platformio.ini in VS Code and scroll. You should see several [env:...] sections. This means PlatformIO understands the project. + +If you want to know more about this file, see the PlatformIO docs about configuration: . + +### 4.3 Choose a PlatformIO environment + +An "environment" in PlatformIO is a build target. It defines the board, framework and options. You can think of it as a named profile for a specific device. + +To select the environment for your board: + +1. Press Ctrl+Shift+P (on macOS use Cmd+Shift+P). +2. Type "PlatformIO: Switch Project Environment" and press Enter. +3. A list of environments appears (for example esp32dev-all-test, theengs-bridge and many others). +4. Choose an environment that matches your board. For a generic ESP32 DevKit you can start with **esp32dev-all-test**. + +**Check:** + +- Look at the bottom blue bar. You should see the selected environment name. + +If you chose the wrong environment, repeat the steps and pick another one. + +### 4.4 Build the firmware in VS Code + +Now compile the firmware. + +1. In VS Code, open the PlatformIO view (left sidebar icon). +2. In the PlatformIO toolbar (bottom of the window), click the **check** (✓) icon. This runs the "Build" task. + +PlatformIO now compiles the code for the selected environment. This can take several minutes the first time. + +**Check:** + +- In the Terminal panel, the build ends with the word "SUCCESS". +- On disk, a file like: + + ``` + .pio/build//firmware.bin + ``` + + exists. + +If the build fails, read the error at the end of the log. Take your time; errors are part of the normal developer life. + +Common problems: + +- Missing tools: check that platformio works from the terminal (section 2.2). +- Wrong environment: try with esp32dev-all-test first. + +Fix the problem, then run the build again. + +### 4.5 Upload the firmware to the board + +After a successful build you can flash the firmware (upload the program to the board). + +1. Connect your ESP board to the computer with a good USB cable. +2. In the PlatformIO toolbar click the **right arrow** (→) icon. This runs the "Upload" task. + +PlatformIO selects a serial port automatically in many cases. + +**Check:** + +- The upload log ends with success, and no "Failed to connect" error. +- The board reboots after upload. + +If the upload fails: + +- Check the USB cable (some cables are power-only and do not carry data). +- On some boards you must press and hold the BOOT button during upload. +- Make sure no other program is using the same serial port (for example another serial monitor). +- You can set the serial port manually in platformio.ini with the upload_port option. + +### 4.6 Open the Serial Monitor + +The Serial Monitor lets you see log messages from the firmware. + +1. In the PlatformIO toolbar click the **plug** icon. This opens the Serial Monitor. +2. Set the baud rate to 115200 if it is not already set. + +**Check:** + +- After reset, you see text from the board, for example boot messages and MQTT logs. + +If the text is unreadable, check that the baud rate matches the value in platformio.ini (monitor_speed). In this project the default is usually 115200. + +If you want to learn more about Serial Monitor problems, the PlatformIO docs have a short page: . + +### 4.7 Run PlatformIO tests (optional) + +You can run unit tests for the project. + +1. Open a terminal inside VS Code (View → Terminal). +2. In the project root, run: + +```bash +pio test -e test +``` + +**Check:** + +- The output ends with "SUCCESS". If tests fail, read which test failed and open the file under test to understand why. + + +## 5. Firmware development from the terminal with ci.sh + +You can also work only from the terminal. This is useful if you prefer command line, use remote development, or want to reproduce the same steps as the CI (Continuous Integration, the automatic build that runs on GitHub). + +All helper scripts live in the scripts folder. The main entry point is: + +- scripts/ci.sh + +This script calls other scripts: + +- scripts/ci_build.sh +- scripts/ci_build_firmware.sh +- scripts/ci_prepare_artifacts.sh +- scripts/ci_site.sh +- scripts/ci_qa.sh +- scripts/ci_list-env.sh + +The scripts are written for bash. On Windows always use **Git Bash** or **WSL** for them, not plain PowerShell or cmd. + +If you are new to bash, a gentle introduction is here: . + +### 5.1 Check that you are in the right place + +1. Open a bash terminal. +2. Go to your clone: + +```bash +cd path/to/OpenMQTTGateway +``` + +**Check:** run: + +```bash +ls scripts main docs +``` + +You should see these folders listed. If not, you are in the wrong directory. + +### 5.2 List available firmware environments + +You can ask the helper scripts which PlatformIO environments exist. + +```bash +./scripts/ci.sh list-env +``` + +**Check:** + +- The command prints many names in columns (for example esp32dev-all-test, theengs-bridge, rfbridge-direct, and so on). + +If the command fails with "command not found" or similar, confirm that bash is used and that the file has execute permission (on Linux you may need chmod +x scripts/ci.sh). + +### 5.3 Quick build of one firmware (simple mode) + +Use ci.sh with the build command to build one firmware from the terminal. + +Example for a development build on esp32dev-all-test: + +```bash +./scripts/ci.sh build esp32dev-all-test --mode dev --verbose +``` + +What this does: + +- Checks that Python, PlatformIO and git are installed. +- Calls ci_build_firmware.sh to run platformio run -e esp32dev-all-test. +- Optionally prepares artifacts if you add deployment options. + +Useful flags: + +- --mode dev or --mode prod: choose development or production mode. For local work, **dev** is usually enough. +- --clean: remove old build files before building. +- --verbose: show more logs from PlatformIO. + +**Check:** + +- The command ends with a "Build Summary" block and "Status: SUCCESS". +- The folder .pio/build/esp32dev-all-test contains firmware.bin. + +If the build fails, read the error line near the bottom. Fix missing tools (see section 2) or wrong environment name, then run the command again. + +### 5.4 Use ci_build_firmware.sh directly (advanced control) + +Sometimes you want to work closer to PlatformIO. + +You can call scripts/ci_build.sh directly when you want more control or when you debug a CI issue: + +```bash +./scripts/ci_build.sh esp32dev-all-test --dev-ota --clean --verbose +``` + +This script: + +- Validates the environment name. +- Sets environment variables for the build (for example OMG_VERSION, OTA flags). +- Runs platformio run for the chosen environment. +- Verifies that firmware.bin and other files exist. + +**Check:** + +- At the end you see a "Build Summary" for that environment. +- The directory .pio/build/esp32dev-all-test contains .bin and .elf files. + +If there are no artifacts, check that PlatformIO is installed and that the environment name matches one from ci_list-env.sh. + +### 5.5 Prepare artifacts for sharing or flashing tools + +After a successful build, you can create a clean set of files ready for release or for use with external flashing tools (for example esptool.py or web uploaders). + +Use scripts/ci_prepare_artifacts.sh through ci_build.sh, or call it directly. + +Example (after a build) with ci_build.sh wrapper: + +```bash +./scripts/ci_build.sh esp32dev-all-test --mode prod --deploy-ready +``` + +Example direct call to ci_prepare_artifacts.sh: + +```bash +./scripts/ci_prepare_artifacts.sh esp32dev-all-test --clean +``` + +This script: + +- Copies firmware.bin, partitions.bin, bootloader.bin and other files from .pio/build/ into the output folder. +- Renames them with the environment name, for example esp32dev-all-test-firmware.bin. +- Creates archives for the libraries used by that environment. + +**Check:** + +- The folder generated/toDeploy (or your custom output) exists. +- It contains files like -firmware.bin and one or more *-libraries.tgz archives. + +If the script says the build directory is missing, run a firmware build first (section 5.3 or 5.4). + +### 5.6 Run QA checks from the terminal + +Before you send a pull request, it is good to check the code style. + +Run: + +```bash +./scripts/ci.sh qa --check +``` + +This uses scripts/ci_qa.sh to run clang-format checks on files under main by default. + +**Check:** + +- The summary says "Status: SUCCESS". + +If there are formatting problems, you can auto-fix them: + +```bash +./scripts/ci.sh qa --fix +``` + +After this, review the changes with git diff, then commit them. + +### 5.7 Run the full pipeline (QA + firmware build + docs) + +For a full local run, similar to CI, use: + +```bash +./scripts/ci.sh all esp32dev-all-test --mode prod +``` + +This will: + +- Run QA checks. +- Build the firmware. +- Build the documentation site in production mode (unless you add --no-site). + +**Check:** + +- At the end you see a "Complete Pipeline Summary" with "Status: SUCCESS". + +If QA fails, fix style issues first. If the build fails, fix code or environment problems, then run again. + +::: tip Next Steps +Once your code builds successfully, check the [Development contributions guide](./development.md) for: +- Code naming rules (ZgatewayXXX, ZsensorYYY, etc.) +- Code style and quality requirements +- How to open a pull request +- Contributing workflow +::: + + + +## 6. Work on the documentation website (from the terminal) + +The documentation website uses VuePress and Node.js. The site source is in the docs folder. + +::: warning Note +This section is **only needed** if you want to **build or preview the site on your own computer**. +If you make a **very small change** in a markdown (.md) file (for example fix a typo or add one sentence), you can: + +- Edit the file in VS Code. +- Push your change and open a pull request. + +In that case it is **not mandatory** to install Node.js, npm, or to run `./scripts/ci.sh site`. The CI on GitHub will build the site for you and show problems if there are any. + +If you want to see your doc changes locally before you push (recommended for bigger edits), then follow the steps below. +::: + +### 6.1 Install Node dependencies + +From the project root: + +```bash +cd OpenMQTTGateway # only if you are not already inside +npm install +``` + +This command reads package.json and installs all Node modules needed for the docs. + +**Check:** + +- The command finishes without error. +- A node_modules folder exists in the project root. + +If install is slow, your internet may be the reason. If it fails, try again later or clear the npm cache with: + +```bash +npm cache clean --force +``` + +### 6.2 Build the docs site with ci.sh + +If you want to check that everything still builds well after bigger doc changes, you can build the site locally. + +The recommended way is: + +```bash +./scripts/ci.sh site --mode prod --url-prefix / --version 1.8.0 +``` + +Important options: + +- `--mode dev` or `--mode prod`: development or production build (default: dev). +- `--url-prefix PATH`: base URL path for links, e.g. `/` for root or `/dev/` for dev (default: /dev/). +- `--version TAG`: version string written into docs/.vuepress/meta.json (default: edge). +- `--preview`: if added, starts a local HTTPS preview server after build. +- `--clean`: remove generated/site folder before build. +- `--insecure-curl`: allow curl to skip TLS verification if needed. + +The script (scripts/ci_site.sh) will: + +- Check that node, npm and openssl are available. +- Download a shared configuration file for the site. +- Create docs/.vuepress/meta.json with site info. +- Run npm run docs:build to build the site. + +**Check:** + +- The summary at the end says "Site Build Summary" and "Status: SUCCESS". +- The folder docs/.vuepress/dist exists and contains HTML files. + +If openssl or node is missing, go back to section 2 and install them. + +### 6.3 Preview the docs site locally + +To preview the site in your browser: + +```bash +./scripts/ci.sh site --mode dev --url-prefix /dev/ --version edge --preview +``` + +The script will start a local HTTPS server. + +**Check:** + +- The log prints a line similar to: "Preview server running at https://localhost:8443/dev/". +- Open that URL in your browser. You should see the OpenMQTTGateway documentation. + +To stop the preview, go back to the terminal and press Ctrl+C. + +### 6.4 Work on docs with plain npm commands + +If you prefer not to use the ci.sh wrapper, you can work directly with npm and VuePress. + +From the project root: + +```bash +npm run docs:dev +``` + +This runs VuePress in development mode with hot reload. When you change a .md file under docs, the browser reloads. + +For a production build: + +```bash +npm run docs:build +``` + +**Check:** + +- For docs:dev, the terminal prints a local URL like http://localhost:8080. Open it in the browser and see the docs. +- For docs:build, the folder docs/.vuepress/dist is created. + +If docs:build fails with an OpenSSL error on new Node versions, set: + +```bash +export NODE_OPTIONS="--openssl-legacy-provider" +``` + +then run the command again. The ci_site.sh script already does this for you. + +### 6.5 Where to edit docs + +- All documentation pages are markdown (.md) files under docs. +- This file itself is in docs/participate/quick_start.md. + +To add or edit docs: + +1. Open the docs folder in VS Code. +2. Change or create markdown files. +3. Run npm run docs:dev to check how the page looks. + +**Check:** after a change and a page refresh, your new text appears on the site. + + + +## 7. Typical workflows for contributors + +This section gives some example "day to day" flows. + +### 7.1 Quick firmware change with VS Code + +1. Open the project folder in VS Code. +2. Choose the right environment (section 4.3). +3. Edit code in main or lib. +4. Build (section 4.4). +5. Upload (section 4.5). +6. Watch logs in Serial Monitor (section 4.6). + +**Check:** your change has the expected effect on the real device. + +### 7.2 Firmware change using terminal only + +1. Open a bash terminal. +2. Go to the project root. +3. Build firmware with: + + ```bash + ./scripts/ci.sh build --mode dev --verbose + ``` + +4. Flash firmware using PlatformIO CLI or another flash tool with the generated firmware file. + +**Check:** the device boots with your new firmware and behaves as expected. + +### 7.3 Change documentation and preview + +1. Edit markdown files under docs. +2. In a terminal, run npm run docs:dev or ./scripts/ci.sh site --mode dev --preview. +3. Open the local URL in your browser. + +**Check:** your text appears on the page and looks correct. + + + +## 8. Troubleshooting + +This list gives quick help for common problems. + +- **Problem:** platformio command not found. + - **Fix:** install it with python3 -m pip install -U platformio and open a new terminal. + +- **Problem:** Build fails for all environments. + - **Fix:** run ./scripts/ci.sh list-env to confirm you use a valid env. Check that you did not change platformio.ini or environments.ini in a wrong way. + +- **Problem:** Build fails with missing libraries. + - **Fix:** run a clean build (add --clean). If you work only with VS Code, press the trash icon or use the clean target from PlatformIO. + +- **Problem:** Upload fails ("Failed to connect to ESP32"). + - **Fix:** choose the right serial port, check the cable, try holding BOOT during upload. + +- **Problem:** Serial output is garbled. + - **Fix:** set Serial Monitor baud to 115200 and confirm monitor_speed in platformio.ini is the same. + +- **Problem:** npm install is very slow or fails. + - **Fix:** check your internet connection. If needed run npm cache clean --force and try again. + +- **Problem:** Site build fails with an OpenSSL error. + - **Fix:** export NODE_OPTIONS="--openssl-legacy-provider" before running npm run docs:build, or use ./scripts/ci.sh site which already sets this. + +If you still have problems after these steps, you can open an issue on the project GitHub page. Include: + +- Your operating system (Windows or Linux, version). +- What you tried to do. +- The exact command you ran. +- The error message from the end of the log. + +This information helps maintainers reproduce and fix the problem. + + + +## 9. Glossary (simple words) + +This glossary explains some words used in this guide. + +- **Firmware**: the program that runs inside your ESP32 or ESP8266 board. +- **Repository (repo)**: the project folder stored on GitHub and on your computer. +- **Commit**: a saved set of changes in Git, with a message. +- **Branch**: a line of development in Git, like a separate copy where you work. +- **Pull Request (PR)**: a request to merge your branch into the main project on GitHub. +- **Environment (env)**: a PlatformIO configuration for a specific board and options. +- **CI (Continuous Integration)**: automatic scripts that build and test the project on every change. +- **Serial Monitor**: a window that shows text messages sent by your board over USB. +- **MQTT**: a lightweight network protocol used to send messages between devices. +- **VuePress**: a static site generator used to build this documentation. + +If any word in this guide is not clear, you can search it on the web or ask in the project community. Many people had the same question before you. + + + +## 10. External links and further reading + +Here is a list of useful links related to tools used in this project: + +- **OpenMQTTGateway project**: +- **PlatformIO main site**: +- **PlatformIO installation for VS Code**: +- **PlatformIO documentation**: +- **Visual Studio Code documentation**: +- **Git official site**: +- **Pro Git book (free)**: +- **GitHub getting started**: +- **Node.js documentation**: +- **npm documentation**: +- **VuePress documentation** (v1, similar to what this project uses): +- **MQTT introduction (HiveMQ)**: + +You do **not** need to read all of them now. Keep this list as a bookmark and come back when you are curious or stuck. + + +## 11. Final words and friendly advice + +Contributing to an open source project is a journey. The first steps (install tools, learn Git, learn PlatformIO) can feel slow. This is normal. + +Some final tips: + +- Change one thing at a time and test often. +- Keep your changes small; they are easier to review. +- Write clear commit messages. +- When something breaks, read the last 10–20 lines of the log first. +- Do not be afraid to ask questions; everyone started as a beginner. + +You are now ready to work on both firmware and documentation for OpenMQTTGateway. Take your time, follow the checks after each step, and you will build confidence with the toolchain and the project. Step by step, you will become faster and more comfortable. + + +If you are new and want to "play" a bit before you change real logic, here are some safe exercises: + +1. **Practice the build chain only.** + - Clone the repo, open it in VS Code, select `esp32dev-all-test`, build, and upload to your board. + - Check that you can see logs in the Serial Monitor. +2. **Make a tiny log change.** + - Find a `LOG` or `Serial` message in a gateway or sensor file under `main`. + - Change the text slightly (for example add a word), rebuild, upload, and verify you see the new message. +3. **Edit a small doc page.** + - Fix a typo or add one sentence of clarification in a markdown file under `docs`. + - Run `./scripts/ci.sh site --mode prod --preview` and confirm your change appears in the browser. +4. **Run QA locally.** + - Run `./scripts/ci.sh qa --check`. If there are issues, run `./scripts/ci.sh qa --fix` and see how files are changed. +5. **Open a small PR.** + - For example, only the doc change or a very small code improvement. This lets you learn the review process without much stress. + +These small steps build confidence. After that you can move to bigger things: adding new sensors, improving MQTT payloads, or extending the web documentation. diff --git a/docs/participate/support.md b/docs/participate/support.md index 16c718b8..0e808a41 100644 --- a/docs/participate/support.md +++ b/docs/participate/support.md @@ -1,15 +1,22 @@ # Supporting the project -If you like the project and/or used it please consider supporting it! It can be done in different ways: -* Purchase the [Theengs mobile application](https://app.theengs.io) -* Purchase the [Theengs plug](https://shop.theengs.io) -* Helping other users in the [community](https://community.openmqttgateway.com) -* [Contribute](development) to the [code](https://github.com/1technophile/OpenMQTTGateway) or the [documentation](https://docs.openmqttgateway.com) -* Buy devices, boards or parts from the [compatible web site](https://compatible.openmqttgateway.com), the devices and parts linked use affiliated links. -* Donate or sponsor the project [developers](https://github.com/1technophile/OpenMQTTGateway/graphs/contributors) -* Make a video or a blog article about what you have done with [OpenMQTTGateway](https://docs.openmqttgateway.com) and share it. +If you like the project, you can help in different ways. Choose what fits you best. -Support open-source development through sponsorship and gain exclusive access to our private forum. Your questions, issues, and feature requests will receive priority attention, plus you'll gain insider access to our roadmap. +## Give time and knowledge +- Help other users in the [community](https://community.openmqttgateway.com). +- Share logs, payload samples, or how-to posts that unblock others. + +## Contribute code or docs +- Follow the [Development contributions guide](./development.md) to open a PR. +- Improve docs or examples if you spot unclear steps. + +## Financial support +- Purchase the [Theengs mobile application](https://app.theengs.io) or the [Theengs plug](https://shop.theengs.io). +- Buy devices or parts via the [compatible web site](https://compatible.openmqttgateway.com) (affiliate links help the project). +- Sponsor the project [developers](https://github.com/1technophile/OpenMQTTGateway/graphs/contributors). +- Make a video or blog article about what you built with [OpenMQTTGateway](https://docs.openmqttgateway.com) and share it. + +Support open-source development through sponsorship and gain exclusive access to our private forum. Your questions, issues, and feature requests receive priority attention, plus you'll see the roadmap early.
diff --git a/docs/prerequisites/board.md b/docs/prerequisites/board.md index 3ece66cb..71cec487 100644 --- a/docs/prerequisites/board.md +++ b/docs/prerequisites/board.md @@ -1,3 +1,7 @@ +--- +pageClass: table-generated-page +--- + # Boards OpenMQTTGateway is not closed to one board or type of board, by using the power of the Arduino framework and libraries that are cross compatibles it let you many choice of hardware, from an ESP8266 to an ESP32. @@ -43,4 +47,10 @@ The plug is available in North America only, other regions are planned. Choosing your board depends heavily on the technologies you want to use with it. To have a good overview of the compatibilities per board you can refer to the compatible modules attributes of each [board](https://compatible.openmqttgateway.com/index.php/boards/). -The choice between these boards will depend on your knowledge and your requirements in terms of reliability, situation, modules wanted and devices you already have. The table below present those (auto-generated) +The choice between these boards will depend on your knowledge and your requirements in terms of reliability, situation, modules wanted and devices you already have. Use the table below to explore the latest environments. + + + diff --git a/docs/prerequisites/boards/.gitignore b/docs/prerequisites/boards/.gitignore deleted file mode 100644 index 86d0cb27..00000000 --- a/docs/prerequisites/boards/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/docs/setitup/rf.md b/docs/setitup/rf.md index e6f3c7bb..0c44fdb5 100644 --- a/docs/setitup/rf.md +++ b/docs/setitup/rf.md @@ -51,8 +51,22 @@ With SRX882S receiver connect the CS pin to 3.3V |ESP8266|D2/**D3**/D1/D8|**RX**/D2|D5|**3V3**|D7|D6|D8|GND |ESP32|**D27**|D12|D18|**3V3**|D23|D19|D5|GND -To use the CC1101 module, `ZradioCC1101` must be uncomment in the `User_config.h` or added to the `build_flags`. -More information about the [CC1101 wiring](https://github.com/LSatan/SmartRC-CC1101-Driver-Lib#wiring). ( Please note that with OMG we are recommending CC1101 GDO2 to be connected to ESP32 D27 and GDO0 to be connected to D12, this is different than the LSatan diagram. This is due to the ESP32 using D2 as part of the boot process. ) +To use the CC1101 module, `ZradioCC1101` must be uncomment in the `User_config.h` or added to the `build_flags`. + +More information about the [CC1101 wiring](https://github.com/LSatan/SmartRC-CC1101-Driver-Lib#wiring). + + +:::tip Please note that with OMG we are recommending CC1101 GDO2 to be connected to ESP32 D27 and GDO0 to be connected to D12, this is different than the LSatan diagram. This is due to the ESP32 using D2 as part of the boot process. + +If you want to use custom SPI pins for the CC1101 module, you can define the following variables in your `User_config.h` or as `build_flags` in `platformio.ini`: + - `RF_CC1101_SCK`: SPI clock pin (SCK) + - `RF_CC1101_SCK`: SPI clock pin (SCK) + - `RF_CC1101_MISO`: SPI MISO pin (Master In Slave Out) + - `RF_CC1101_MOSI`: SPI MOSI pin (Master Out Slave In) + - `RF_CC1101_CS`: SPI chip select pin (CSN) + +When **all** these variables are defined, OpenMQTTGateway will use your custom pinout for the CC1101 connection. This is useful if your board does not use the default pins or if you want to avoid conflicts with other devices. +::: ## ESP32 Hardware setup ![Addon_RF](../img/OpenMQTTgateway_ESP32_Addon_RF.png) @@ -73,3 +87,5 @@ The RF processing can be achieved after the modification by either RF, RF2 or Pi ## WIFI RF GATEWAY Hardware setup This board doesn't require any hardware modifications. + + diff --git a/docs/upload/ReadMe.md b/docs/upload/ReadMe.md new file mode 100644 index 00000000..7ec95e21 --- /dev/null +++ b/docs/upload/ReadMe.md @@ -0,0 +1,79 @@ +# Upload + +OpenMQTTGateway provides several installation approaches suited for different use cases. The quickest path uses pre-built binaries that work with standard configurations. For custom parameters or specific gateway module combinations, you can build firmware using your development environment. Alternatively, you can flash devices directly from your web browser without installing any software. + +

+ Firmware upload process +

+ +## Choosing Your Upload Method + +### Web-Based Installation + +The [web installation](web-install.md) method represents the easiest way to get started with OpenMQTTGateway. You can flash your device directly from your browser without downloading files or installing development tools. This method works on Windows, macOS, and Linux with Chrome, Edge, or Opera browsers. + +The web installer displays available environments for different boards and gateway configurations. Select your board type, connect via USB, and click the connect button. The installer handles everything automatically, including erasing old firmware and writing the new one. The web installer uses [ESP Web Tools](https://esphome.github.io/esp-web-tools/) technology to communicate directly with your ESP device. + +### Ready-to-Go Binary Installation + +[Downloading and installing pre-built binaries](binaries.md) offers control over the flashing process using desktop tools. Download the binary files for your board from the [GitHub releases page](https://github.com/1technophile/OpenMQTTGateway/releases). + +For ESP32 devices, you need the firmware binary, bootloader, and boot application partition files written to specific memory addresses. Windows users can use the ESP32 Flash Download Tool from Espressif. On Linux and macOS, the esptool.py command-line utility provides a straightforward upload method. + +This method works well for standard configurations without modifying source code. After flashing, you can still configure WiFi, MQTT broker settings, and basic parameters through the configuration portal. + +### Custom Build and Upload + +[Building from source](builds.md) becomes necessary when you need specific pin assignments, custom MQTT topics, or particular gateway module combinations not available in pre-built binaries. + +[PlatformIO](https://platformio.org/) provides the recommended build environment. After downloading the [source code from GitHub](https://github.com/1technophile/OpenMQTTGateway), you will find a `platformio.ini` file defining build environments for various hardware combinations. + +The configuration system uses a layering approach where default values from [`User_config.h`](https://github.com/1technophile/OpenMQTTGateway/blob/development/main/User_config.h) and [`config_XX.h`](https://github.com/1technophile/OpenMQTTGateway/tree/development/main) files can be overridden by build flags in `platformio.ini` or `environments.ini`. You can embed WiFi credentials and MQTT settings at build time for automatic connection on first boot. + +### Browser-Based Building with Gitpod + +For those who want to build custom firmware without setting up a local development environment, [Gitpod](gitpod.md) offers a cloud-based solution. By clicking on the [Gitpod link](https://gitpod.io#https://github.com/1technophile/OpenMQTTGateway/tree/development), you get a complete development environment running in your browser with PlatformIO already installed and configured. + +After the automatic initial build completes, modify the environment configuration by editing `environments.ini` and run build commands in the browser terminal. Download the generated firmware files and flash them using the binary installation method. + +## Configuring Network and MQTT Settings + +After flashing firmware, configure network connectivity and MQTT broker settings using either runtime or build-time approaches. + +### Runtime Configuration Portal + +When you power on a freshly flashed device, it creates a WiFi access point named OpenMQTTGateway or starting with OMG_. You can find detailed information about the [configuration portal here](portal.md). + +Connecting to this access point opens a portal where you configure your WiFi network, MQTT broker details, and optional security settings including TLS encryption and certificates. For devices with Ethernet, access the portal through the LAN IP address and configure WiFi as fallback connectivity. + +The portal accepts broker IP addresses or hostnames with mDNS support like homeassistant.local. Set a gateway password to protect future configuration changes, OTA updates, and web interface access. + +### Build-Time Configuration + +Alternatively, embed network and MQTT settings directly in firmware during the build process. Set parameters in `User_config.h` or add them as build flags in your PlatformIO environment definition. Store sensitive information in a separate `_env.ini` file excluded from version control. + +## Advanced Configuration Options + +Beyond basic connectivity, OpenMQTTGateway supports several [advanced features](advanced-configuration.md) that enhance security and integration capabilities. + +### Secure MQTT Connections + +For deployments over the internet or public networks, enable TLS encryption to secure communication between the gateway and MQTT broker. Configure your broker with a valid certificate and obtain the Certificate Authority certificate. The gateway can verify server identity against this certificate or connect with encryption without validation. + +Provide the CA certificate at build time in `default_server_cert.h` or paste it into the configuration portal. The gateway supports both self-signed certificates and those from public certificate authorities. + +### Home Assistant Auto-Discovery + +When you use [Home Assistant](https://www.home-assistant.io/) as your home automation platform, OpenMQTTGateway automatically creates device entries and sensors through Home Assistant's MQTT discovery protocol, enabled by default in all standard builds. + +Enable discovery in your Home Assistant MQTT integration settings and create a dedicated MQTT user. The gateway registers itself as a device and creates sensor entities automatically, appearing in Configuration → Devices section. + +### Topic Customization + +The gateway publishes messages to MQTT topics following the format `home/OpenMQTTGateway/GATEWAYtoMQTT`. Enable the `valueAsATopic` feature to append received values to the topic path, making topic-based filtering easier and avoiding warnings in certain controllers. + +## Next Steps + +After successfully uploading firmware and configuring your gateway, you can proceed to configure specific gateway modules for [RF](../setitup/rf.md), [IR](../setitup/ir.md), [Bluetooth](../setitup/ble.md), [LoRa](../setitup/lora.md), or other protocols you want to use. Each module has configuration options adjustable through MQTT commands or the web interface without rebuilding firmware. + +The [troubleshooting section](troubleshoot.md) covers common issues, but if you encounter problems not addressed here, the [OpenMQTTGateway community forum](https://community.openmqttgateway.com) provides an active place to ask questions and share solutions with other users. diff --git a/docs/upload/board-selector.md b/docs/upload/board-selector.md new file mode 100644 index 00000000..3c498cba --- /dev/null +++ b/docs/upload/board-selector.md @@ -0,0 +1,10 @@ +--- +pageClass: table-generated-page +--- + + + + + \ No newline at end of file diff --git a/docs/upload/web-install.md b/docs/upload/web-install.md index aeb494f6..3547da57 100644 --- a/docs/upload/web-install.md +++ b/docs/upload/web-install.md @@ -2,7 +2,14 @@ pageClass: table-generated-page --- -# (Option 1) Upload from the web +## Select your firmware +To upload firmware to your ESP device directly from this page, first connect your ESP device to a USB port on your computer. Then, choose the appropriate firmware from the available options. Next, click the **Connect** button and select the USB port where your ESP is plugged in. Wait for the upload process to finish. After the upload completes, you can set up your [WiFi and MQTT credentials](portal.md). + + + ::: tip Running on a tablet or phone If you want to use the BLE decoding capabilities of OpenMQTTGateway with a tablet or smartphone you can use [Theengs App](https://app.theengs.io/). @@ -15,16 +22,8 @@ The correct driver to then select in the popup of this web install is `/dev/cu.wchusbserialXXXXXXXXXXX` ::: -You can upload the firmware to your ESP device directly from here. -1. Plug in your ESP to a USB port. -2. Select the firmware in the box below. -3. Click the install button and choose the port that the ESP is connected to. -4. Wait until the process is complete. -5. Once completed you can configure your [WiFi and MQTT credentials](portal.md) - -Upload powered by [ESP Web Tools](https://esphome.github.io/esp-web-tools/) ## Using OpenMQTTGateway ? Support open-source development through sponsorship and gain exclusive access to our private forum. Your questions, issues, and feature requests will receive priority attention, plus you'll gain insider access to our roadmap. @@ -33,5 +32,4 @@ Support open-source development through sponsorship and gain exclusive access to
-## Environments characteristics -The auto-generated table below describes the libraries and the modules of each board configuration. + diff --git a/docsgen/boards-info.js b/docsgen/boards-info.js new file mode 100644 index 00000000..72717b94 --- /dev/null +++ b/docsgen/boards-info.js @@ -0,0 +1,232 @@ +'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; +} + +function loadBoardsInfo(options = {}) { + const { includeTests = false } = options; + const config = rowConfigFromPlatformIO(); + const sections = convertJsonToSections(config); + return collectBoardsInformations(sections, { includeTests }); +} + +function ensureDir(dir) { + const fs = require('fs'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +module.exports = { + loadBoardsInfo, + ensureDir +}; diff --git a/docsgen/common_wu.js b/docsgen/common_wu.js new file mode 100644 index 00000000..bccc3078 --- /dev/null +++ b/docsgen/common_wu.js @@ -0,0 +1,77 @@ +// Common templates and constants for web installer manifest generation +// Used by: scripts/gen_wu.js + +const mf_temp32 = (vars) => `{ + "name": "OpenMQTTGateway", + "new_install_prompt_erase": true, + "builds": [ + { + "chipFamily": "ESP32", + "improv": false, + "parts": [ + { "path": "${vars.cp}${vars.bl}", "offset": 4096 }, + { "path": "${vars.cp}${vars.part}", "offset": 32768 }, + { "path": "${vars.cp}${vars.boot}", "offset": 57344 }, + { "path": "${vars.cp}${vars.bin}", "offset": 65536 } + ] + } + ] +}`; + +const mf_temp32c3 = (vars) => `{ + "name": "OpenMQTTGateway", + "new_install_prompt_erase": true, + "builds": [ + { + "chipFamily": "ESP32-C3", + "improv": false, + "parts": [ + { "path": "${vars.cp}${vars.bl}", "offset": 0 }, + { "path": "${vars.cp}${vars.part}", "offset": 32768 }, + { "path": "${vars.cp}${vars.boot}", "offset": 57344 }, + { "path": "${vars.cp}${vars.bin}", "offset": 65536 } + ] + } + ] +}`; + +const mf_temp32s3 = (vars) => `{ + "name": "OpenMQTTGateway", + "new_install_prompt_erase": true, + "builds": [ + { + "chipFamily": "ESP32-S3", + "improv": false, + "parts": [ + { "path": "${vars.cp}${vars.bl}", "offset": 0 }, + { "path": "${vars.cp}${vars.part}", "offset": 32768 }, + { "path": "${vars.cp}${vars.boot}", "offset": 57344 }, + { "path": "${vars.cp}${vars.bin}", "offset": 65536 } + ] + } + ] +}`; + +const mf_temp8266 = (vars) => `{ + "name": "OpenMQTTGateway", + "new_install_prompt_erase": true, + "builds": [ + { + "chipFamily": "ESP8266", + "parts": [{ "path": "${vars.cp}${vars.bin}", "offset": 0 }] + } + ] +}`; + + +const cors_proxy = ''; // 'https://cors.bridged.cc/' +const esp32_boot = 'https://github.com/espressif/arduino-esp32/raw/2.0.7/tools/partitions/boot_app0.bin'; + +module.exports = { + mf_temp32, + mf_temp32c3, + mf_temp32s3, + mf_temp8266, + cors_proxy, + esp32_boot +}; diff --git a/docsgen/gen_wu.js b/docsgen/gen_wu.js new file mode 100644 index 00000000..5c5ed630 --- /dev/null +++ b/docsgen/gen_wu.js @@ -0,0 +1,349 @@ +#!/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 VUEPRESS_DIR = path.join(DOCS_DIR, '.vuepress'); +const PUBLIC_DIR = path.join(VUEPRESS_DIR, 'public'); +const COMPONENTS_DIR = path.join(VUEPRESS_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(VUEPRESS_DIR, 'defaults.json'); +const META_CONFIG_PATH = path.join(VUEPRESS_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); +}); diff --git a/scripts/latest_version.json b/docsgen/latest_version.json.tpl similarity index 100% rename from scripts/latest_version.json rename to docsgen/latest_version.json.tpl diff --git a/scripts/latest_version_dev.json b/docsgen/latest_version_dev.json.tpl similarity index 100% rename from scripts/latest_version_dev.json rename to docsgen/latest_version_dev.json.tpl diff --git a/environments.ini b/environments.ini index a849a343..d464ff6c 100644 --- a/environments.ini +++ b/environments.ini @@ -46,7 +46,7 @@ build_flags = board_build.flash_mode = dout board_build.ldscript = eagle.flash.1m64.ld ;this frees more space for firmware uplad via OTA. ;extra_scripts = scripts/compressFirmware.py ;uncomment this to compress the firmware. This helps updating e.g. Sonoff RF Bridge via OTA flash by saving space for the uploaded firmware. -custom_description = RF gateway for the Sonoff RF Bridge requiring direct hack, relying on ESPilight library, [tutorial](https://1technophile.blogspot.com/2019/04/sonoff-rf-bridge-pilight-or-how-to.html). +custom_description = 'RF gateway for the Sonoff RF Bridge requiring direct hack, relying on ESPilight library,
tutorial.' custom_hardware = RFBridge v1 [env:theengs-bridge] @@ -84,7 +84,7 @@ build_flags = '-DGATEWAY_MODEL="TBRIDGE01"' ;'-DBOARD_HAS_PSRAM' ;'-mfix-esp32-psram-cache-issue' -custom_description = BLE gateway with external antenna and Ethernet/WiFi connectivity, [user guide](https://tbridge01.theengs.io/) +custom_description = 'BLE gateway with external antenna and Ethernet/WiFi connectivity, user guide' custom_hardware = Theengs Bridge gateway ethernet [env:theengs-bridge-v11] @@ -123,8 +123,9 @@ build_flags = ;'-DBOARD_HAS_PSRAM' ;'-DSELF_TEST=true' ;'-mfix-esp32-psram-cache-issue' -custom_description = BLE gateway with external antenna and Ethernet/WiFi connectivity, [user guide](https://tbridge02.theengs.io/) +custom_description = 'BLE gateway with external antenna and Ethernet/WiFi connectivity, user guide' custom_hardware = Theengs Bridge gateway ethernet +custom_img = img/Theengs-Bridge-ble-gateway.png ; DISCLAIMER: This is the default environment for Theengs Plug. ; Any modifications to this configuration are not covered by warranty. @@ -181,8 +182,9 @@ build_flags = '-DGATEWAY_MANUFACTURER="Theengs"' '-DGATEWAY_MODEL="TPLUG01"' '-UZwebUI="WebUI"' ; Disable WebUI -custom_description = Smart Plug, BLE Gateway and energy monitoring, [user guide](https://tplug01.theengs.io/) +custom_description = 'Smart Plug, BLE Gateway and energy monitoring, user guide' custom_hardware = Theengs Plug +custom_img = img/Theengs-Plug-OpenMQTTGateway.png [env:esp32dev-all-test] platform = ${com.esp32_platform} @@ -517,7 +519,7 @@ build_flags = ; '-DMQTT_SERVER="192.168.1.17"' ; '-Dwifi_ssid="WIFI_SSID"' ; '-Dwifi_password="WIFI_PASSWORD"' -custom_description = Suitable for low power with BLE gateway, [tutorial](https://1technophile.blogspot.com/2021/04/low-power-esp32-ble-gateway.html) +custom_description = 'Suitable for low power with BLE gateway, tutorial' custom_hardware = LOLIN 32 Lite [env:esp32-olimex-gtw-ble-eth] @@ -1581,7 +1583,8 @@ build_flags = '-DGateway_Name="OMG_AVATTO_IR"' board_build.flash_mode = dout custom_description = IR gateway bi directional -custom_hardware = Avatto Bakey IR first version, [tutorial](https://1technophile.blogspot.com/2020/07/avatto-s06-ir-gateway-compatible-with.html) +custom_hardware = 'Avatto Bakey IR first version,tutorial' +custom_img = https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWdLlg_I1Fxg8PKa0LNCwj3fDtSCVn50Zqima9QZvJfPpIyr3Rh7cvg_WPzRkZpzP4_Tu9inXfo3e6CbLLLpZzo5yGOUX_zFT0CnopCtyuEgJyHHJLP8ctfm1UyHP3KZJGzRbZul4F9JBX/ [env:nodemcuv2-rf] platform = ${com.esp8266_platform} @@ -1664,7 +1667,7 @@ build_flags = '-DRF_RECEIVER_GPIO=5' board_build.flash_mode = dout custom_description = RF gateway for USB stick using RCSwitch -custom_hardware = RF Wifi USB stick, [tutorial](https://1technophile.blogspot.com/2019/09/hack-of-rf-wifi-gateway-usb-stick.html) +custom_hardware = 'RF Wifi USB stick, tutorial' [env:nodemcuv2-rf2] platform = ${com.esp8266_platform} @@ -1773,8 +1776,8 @@ build_flags = '-DZgatewayRF="RF"' '-DGateway_Name="OMG_SONOFF_BASIC_RFR3"' board_build.flash_mode = dout -custom_description = Wifi relay and RF receiver using RCSwitch library, [tutorial](https://1technophile.blogspot.com/2019/08/new-sonoff-rfr3-as-433tomqtt-gateway.html) -custom_hardware = Sonoff Basic RFR3, [tutorial](https://1technophile.blogspot.com/2019/08/new-sonoff-rfr3-as-433tomqtt-gateway.html) +custom_description = 'Wifi relay and RF receiver using RCSwitch library, tutorial' +custom_hardware = 'Sonoff Basic RFR3, tutorial' [env:esp32dev-ble-datatest] platform = ${com.esp32_platform} diff --git a/examples/LoraWaterCounter/OpenGatewaySensorCounter.ino b/examples/LoraWaterCounter/OpenGatewaySensorCounter.ino index c928ca3c..329be406 100644 --- a/examples/LoraWaterCounter/OpenGatewaySensorCounter.ino +++ b/examples/LoraWaterCounter/OpenGatewaySensorCounter.ino @@ -1,16 +1,16 @@ +#include +#include +#include #include #include #include #include #include -#include -#include -#include #include "SSD1306.h" //LoRa pins -#define SCK 5 // GPIO5 -- SX1278's SCK +#define SCK 5 // GPIO5 -- SX1278's SCK #define MISO 19 // GPIO19 -- SX1278's MISnO #define MOSI 27 // GPIO27 -- SX1278's MOSI #define SS 18 // GPIO18 -- SX1278's CS @@ -20,17 +20,17 @@ //OLED pins #define OLED_SDA 21 -#define OLED_SCL 22 +#define OLED_SCL 22 //#define OLED_RST 16 -#define LED 25 +#define LED 25 #define DHT_PIN 14 #define DHT_TYPE DHT22 -#define CNT_PIN 34 +#define CNT_PIN 34 -#define BAT_PIN 35 +#define BAT_PIN 35 const int MAX_ANALOG_VAL = 4095; const float MAX_BATTERY_VOLTAGE = 4.2; @@ -38,7 +38,7 @@ const float MAX_BATTERY_VOLTAGE = 4.2; int cntpkt = 0; unsigned long counter = 0; unsigned long last_interrupt_time = 0; -int pulse_seen = 0; // pulse detected by interrupt function +int pulse_seen = 0; // pulse detected by interrupt function int bounce_delay_ms = 100; SSD1306 display(0x3c, OLED_SDA, OLED_SCL); @@ -46,9 +46,9 @@ String rssi = "RSSI --"; String packSize = "--"; String packet; -DHT_Unified dht(DHT_PIN, DHT_TYPE); //Inizializza oggetto chiamato "dht", parametri: pin a cui è connesso il sensore, tipo di dht 11/22 +DHT_Unified dht(DHT_PIN, DHT_TYPE); // Initialize the "dht" object with sensor pin and DHT type (11/22) -void ICACHE_RAM_ATTR bounceCheck (); +void ICACHE_RAM_ATTR bounceCheck(); void setup() { pinMode(LED, OUTPUT); @@ -75,32 +75,50 @@ void setup() { display.flipScreenVertically(); display.setFont(ArialMT_Plain_10); - pinMode(BAT_PIN, INPUT); + pinMode(BAT_PIN, INPUT); pinMode(CNT_PIN, INPUT_PULLUP); - - attachInterrupt (CNT_PIN, bounceCheck, RISING); + + attachInterrupt(CNT_PIN, bounceCheck, RISING); dht.begin(); sensor_t sensor; dht.temperature().getSensor(&sensor); Serial.println(F("------------------------------------")); Serial.println(F("Temperature Sensor")); - Serial.print (F("Sensor Type: ")); Serial.println(sensor.name); - Serial.print (F("Driver Ver: ")); Serial.println(sensor.version); - Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id); - Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("°C")); - Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("°C")); - Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("°C")); + Serial.print(F("Sensor Type: ")); + Serial.println(sensor.name); + Serial.print(F("Driver Ver: ")); + Serial.println(sensor.version); + Serial.print(F("Unique ID: ")); + Serial.println(sensor.sensor_id); + Serial.print(F("Max Value: ")); + Serial.print(sensor.max_value); + Serial.println(F("°C")); + Serial.print(F("Min Value: ")); + Serial.print(sensor.min_value); + Serial.println(F("°C")); + Serial.print(F("Resolution: ")); + Serial.print(sensor.resolution); + Serial.println(F("°C")); Serial.println(F("------------------------------------")); // Print humidity sensor details. dht.humidity().getSensor(&sensor); Serial.println(F("Humidity Sensor")); - Serial.print (F("Sensor Type: ")); Serial.println(sensor.name); - Serial.print (F("Driver Ver: ")); Serial.println(sensor.version); - Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id); - Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("%")); - Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("%")); - Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("%")); + Serial.print(F("Sensor Type: ")); + Serial.println(sensor.name); + Serial.print(F("Driver Ver: ")); + Serial.println(sensor.version); + Serial.print(F("Unique ID: ")); + Serial.println(sensor.sensor_id); + Serial.print(F("Max Value: ")); + Serial.print(sensor.max_value); + Serial.println(F("%")); + Serial.print(F("Min Value: ")); + Serial.print(sensor.min_value); + Serial.println(F("%")); + Serial.print(F("Resolution: ")); + Serial.print(sensor.resolution); + Serial.println(F("%")); Serial.println(F("------------------------------------")); delay(1500); @@ -117,15 +135,14 @@ void loop() { display.drawString(90, 0, String(cntpkt)); String NodeId = WiFi.macAddress(); - // float temp = dht.readTemperature(); - // float hum = dht.readHumidity(); + // float temp = dht.readTemperature(); + // float hum = dht.readHumidity(); float battery = getBattery(); dht.temperature().getEvent(&event1); if (isnan(event1.temperature)) { Serial.println(F("Error reading temperature!")); - } - else { + } else { Serial.print(F("Temperature: ")); Serial.print(event1.temperature); Serial.println(F("°C")); @@ -134,8 +151,7 @@ void loop() { dht.humidity().getEvent(&event2); if (isnan(event2.relative_humidity)) { Serial.println(F("Error reading humidity!")); - } - else { + } else { Serial.print(F("Humidity: ")); Serial.print(event2.relative_humidity); Serial.println(F("%")); @@ -150,10 +166,10 @@ void loop() { LoRa.endPacket(); Serial.println(msg); - + display.drawString(0, 15, String(NodeId)); display.drawString(0, 30, "count: " + String(counter)); - display.drawString(0, 45, "battery: " + String(battery)+ " %"); + display.drawString(0, 45, "battery: " + String(battery) + " %"); display.display(); delay(5000); @@ -167,10 +183,10 @@ void loop() { delay(6000); } -void ICACHE_RAM_ATTR bounceCheck (){ - unsigned long interrupt_time = millis(); - if (interrupt_time - last_interrupt_time > bounce_delay_ms) counter++; // void loop() then notes pulse == 1 and takes action - last_interrupt_time = interrupt_time; +void ICACHE_RAM_ATTR bounceCheck() { + unsigned long interrupt_time = millis(); + if (interrupt_time - last_interrupt_time > bounce_delay_ms) counter++; // void loop() then notes pulse == 1 and takes action + last_interrupt_time = interrupt_time; } float getBattery(void) { @@ -179,6 +195,5 @@ float getBattery(void) { float voltageLevel = (rawValue / 4095.0) * 2 * 1.1 * 3.3; // calculate voltage level float perc = voltageLevel / MAX_BATTERY_VOLTAGE * 100; - return(perc); + return (perc); } - diff --git a/examples/LoraWaterCounter/src/main.cpp b/examples/LoraWaterCounter/src/main.cpp index 91bcd939..c62cf5e7 100644 --- a/examples/LoraWaterCounter/src/main.cpp +++ b/examples/LoraWaterCounter/src/main.cpp @@ -19,17 +19,17 @@ //OLED pins #define OLED_SDA 21 -#define OLED_SCL 22 +#define OLED_SCL 22 //#define OLED_RST 16 -#define LED 25 +#define LED 25 #define DHT_PIN 12 -#define DHT_TYPE DHT22 +#define DHT_TYPE DHT22 -#define CNT_PIN 34 +#define CNT_PIN 34 -#define BAT_PIN 35 +#define BAT_PIN 35 const int MAX_ANALOG_VAL = 4095; const float MAX_BATTERY_VOLTAGE = 4.2; @@ -37,7 +37,7 @@ const float MAX_BATTERY_VOLTAGE = 4.2; int cntpkt = 0; unsigned long counter = 0; unsigned long last_interrupt_time = 0; -int pulse_seen = 0; // pulse detected by interrupt function +int pulse_seen = 0; // pulse detected by interrupt function int bounce_delay_ms = 100; SSD1306 display(0x3c, OLED_SDA, OLED_SCL); @@ -45,9 +45,9 @@ String rssi = "RSSI --"; String packSize = "--"; String packet; -DHT dht(DHT_PIN, DHT_TYPE); //Inizializza oggetto chiamato "dht", parametri: pin a cui è connesso il sensore, tipo di dht 11/22 +DHT dht(DHT_PIN, DHT_TYPE); // Initialize the "dht" object with sensor pin and DHT type (11/22) -void ICACHE_RAM_ATTR bounceCheck (); +void ICACHE_RAM_ATTR bounceCheck(); void setup() { pinMode(LED, OUTPUT); @@ -74,10 +74,10 @@ void setup() { display.flipScreenVertically(); display.setFont(ArialMT_Plain_10); - pinMode(BAT_PIN, INPUT); + pinMode(BAT_PIN, INPUT); pinMode(CNT_PIN, INPUT_PULLUP); - - attachInterrupt (CNT_PIN, bounceCheck, RISING); + + attachInterrupt(CNT_PIN, bounceCheck, RISING); delay(1500); } @@ -97,7 +97,7 @@ void loop() { // send packet LoRa.beginPacket(); // Build json string to send - //per test rimuovere + // Remove this line for test String msg = "{\"model\":\"ESP32CNT\",\"id\":\"" + NodeId + "\",\"count\":\"" + String(counter) + "\",\"tempc\":\"" + String(temp) + "\",\"hum\":\"" + String(hum) + "\",\"batt\":\"" + String(battery) + "\"}"; // Send json string @@ -105,10 +105,10 @@ void loop() { LoRa.endPacket(); Serial.println(msg); - + display.drawString(0, 15, String(NodeId)); display.drawString(0, 30, "count: " + String(counter)); - display.drawString(0, 45, "battery: " + String(battery)+ " %"); + display.drawString(0, 45, "battery: " + String(battery) + " %"); display.display(); delay(5000); @@ -121,10 +121,10 @@ void loop() { delay(60000); // wait for 60 seconds } -void ICACHE_RAM_ATTR bounceCheck (){ - unsigned long interrupt_time = millis(); - if (interrupt_time - last_interrupt_time > bounce_delay_ms) counter++; // void loop() then notes pulse == 1 and takes action - last_interrupt_time = interrupt_time; +void ICACHE_RAM_ATTR bounceCheck() { + unsigned long interrupt_time = millis(); + if (interrupt_time - last_interrupt_time > bounce_delay_ms) counter++; // void loop() then notes pulse == 1 and takes action + last_interrupt_time = interrupt_time; } float getBattery(void) { @@ -133,6 +133,5 @@ float getBattery(void) { float voltageLevel = (rawValue / 4095.0) * 2 * 1.1 * 3.3; // calculate voltage level float perc = voltageLevel / MAX_BATTERY_VOLTAGE * 100; - return(perc); + return (perc); } - diff --git a/main/TheengsCommon.h b/main/TheengsCommon.h index 5b53adde..44b9276b 100644 --- a/main/TheengsCommon.h +++ b/main/TheengsCommon.h @@ -92,6 +92,19 @@ extern bool cmpToMainTopic(const char*, const char*); extern bool pub(const char*, const char*, bool); extern bool pub(const char*, const char*); +// Float-specific overload to log with the correct format specifier. +inline void Config_update(JsonObject& data, const char* key, float& var) { + if (data.containsKey(key)) { + float newVal = data[key].as(); + if (var != newVal) { + var = newVal; + THEENGS_LOG_NOTICE(F("Config %s changed to: %F" CR), key, newVal); + } else { + THEENGS_LOG_NOTICE(F("Config %s unchanged, currently: %F" CR), key, newVal); + } + } +} + template void Config_update(JsonObject& data, const char* key, T& var) { if (data.containsKey(key)) { diff --git a/main/User_config.h b/main/User_config.h index 31a83510..df73eb8e 100644 --- a/main/User_config.h +++ b/main/User_config.h @@ -29,7 +29,7 @@ #define user_config_h /*-------------------VERSION----------------------*/ #ifndef OMG_VERSION -# define OMG_VERSION "version_tag" +# define OMG_VERSION "edge" #endif /*-------------CONFIGURE WIFIMANAGER-------------(only ESP8266 & SONOFF RFBridge)*/ diff --git a/main/blufi.cpp b/main/blufi.cpp index 669129c7..5449fb9b 100644 --- a/main/blufi.cpp +++ b/main/blufi.cpp @@ -49,9 +49,9 @@ static NimBLEOta* pNimBLEOta; static NimBLECharacteristic* pCommandCharacteristic; static NimBLECharacteristic* pRecvFwCharacteristic; -#ifndef BLUFI_MFG_ID -# define BLUFI_MFG_ID 0xFFFF // Default Manufacturer ID if not defined -#endif +# ifndef BLUFI_MFG_ID +# define BLUFI_MFG_ID 0xFFFF // Default Manufacturer ID if not defined +# endif struct pkt_info { uint8_t* pkt; @@ -201,7 +201,7 @@ void restart_connection_timer() {} void stop_connection_timer() {} # endif -void set_blufi_mfg_data () { +void set_blufi_mfg_data() { if (!NimBLEDevice::isInitialized() || !NimBLEDevice::getAdvertising()->isAdvertising()) { THEENGS_LOG_NOTICE(F("Unable to set advertising data" CR)); return; diff --git a/main/commonRF.cpp b/main/commonRF.cpp index 4eea813d..85d150cd 100644 --- a/main/commonRF.cpp +++ b/main/commonRF.cpp @@ -40,6 +40,7 @@ extern rtl_433_ESP rtl_433; # endif int currentReceiver = ACTIVE_NONE; +boolean isDriverEnabled = false; extern void enableActiveReceiver(); extern void disableCurrentReceiver(); @@ -53,43 +54,63 @@ public: ZCommonRFWrapper() : RFReceiver() {} void enable() override { enableActiveReceiver(); } void disable() override { disableCurrentReceiver(); } - int getReceiverID() const override { return currentReceiver; } }; ZCommonRFWrapper iRFReceiver; RFConfiguration iRFConfig(iRFReceiver); -//TODO review +// Initialize the CC1101 and tune the RX frequency. +// - Uses truncated exponential backoff to avoid tight retry loops +// - Validates the configured frequency before touching the radio +// - Emits explicit logs for success/failure so watchdog resets are traceable void initCC1101() { -# ifdef ZradioCC1101 //receiving with CC1101 - // Loop on getCC1101() until it returns true and break after 10 attempts +# ifdef ZradioCC1101 // receiving with CC1101 + const float freqMhz = iRFConfig.getFrequency(); + if (!iRFConfig.validFrequency(freqMhz)) { + THEENGS_LOG_ERROR(F("C1101 invalid frequency: %F MHz" CR), freqMhz); + return; + } + int delayMS = 16; int delayMaxMS = 500; - for (int i = 0; i < 10; i++) { -# if defined(RF_MODULE_SCK) && defined(RF_MODULE_MISO) && \ - defined(RF_MODULE_MOSI) && defined(RF_MODULE_CS) - ELECHOUSE_cc1101.setSpiPin(RF_MODULE_SCK, RF_MODULE_MISO, RF_MODULE_MOSI, RF_MODULE_CS); + bool connected = false; + + for (int attempt = 1; attempt <= 10; attempt++) { +# if defined(RF_CC1101_SCK) && defined(RF_CC1101_MISO) && \ + defined(RF_CC1101_MOSI) && defined(RF_CC1101_CS) + THEENGS_LOG_TRACE(F("initCC1101 with custom SPI pins, SCK=%d, MISO=%d, MOSI=%d, CS=%d" CR), RF_CC1101_SCK, RF_CC1101_MISO, RF_CC1101_MOSI, RF_CC1101_CS); + ELECHOUSE_cc1101.setSpiPin(RF_CC1101_SCK, RF_CC1101_MISO, RF_CC1101_MOSI, RF_CC1101_CS); # endif + if (ELECHOUSE_cc1101.getCC1101()) { - THEENGS_LOG_NOTICE(F("C1101 spi Connection OK" CR)); + connected = true; + THEENGS_LOG_NOTICE(F("C1101 SPI connection OK on attempt %d" CR), attempt); ELECHOUSE_cc1101.Init(); - ELECHOUSE_cc1101.SetRx(iRFConfig.getFrequency()); + ELECHOUSE_cc1101.SetRx(freqMhz); + THEENGS_LOG_NOTICE(F("C1101 tuned RX to %F MHz" CR), freqMhz); break; - } else { - THEENGS_LOG_ERROR(F("C1101 spi Connection Error" CR)); - delay(delayMS); } + + THEENGS_LOG_ERROR(F("C1101 SPI connection error (attempt %d), retrying" CR), attempt); + delay(delayMS); + // truncated exponential backoff delayMS = delayMS * 2; if (delayMS > delayMaxMS) delayMS = delayMaxMS; } + + if (!connected) { + THEENGS_LOG_ERROR(F("C1101 init failed after retries, radio left disabled" CR)); + } +# else + THEENGS_LOG_TRACE(F("initCC1101 skipped: ZradioCC1101 not enabled" CR)); # endif } void setupCommonRF() { iRFConfig.reInit(); - iRFConfig.loadFromStorage(); + iRFConfig.loadFromStorage(true); } # if !defined(ZgatewayRFM69) && !defined(ZactuatorSomfy) @@ -120,28 +141,37 @@ bool validReceiver(int receiver) { # endif void disableCurrentReceiver() { + if (!isDriverEnabled) { + THEENGS_LOG_TRACE(F("Receiver %d is already disabled" CR), currentReceiver); + return; + } THEENGS_LOG_TRACE(F("disableCurrentReceiver: %d" CR), currentReceiver); switch (currentReceiver) { case ACTIVE_NONE: + isDriverEnabled = false; break; # ifdef ZgatewayPilight case ACTIVE_PILIGHT: disablePilightReceive(); + isDriverEnabled = false; break; # endif # ifdef ZgatewayRF case ACTIVE_RF: disableRFReceive(); + isDriverEnabled = false; break; # endif # ifdef ZgatewayRTL_433 case ACTIVE_RTL: disableRTLreceive(); + isDriverEnabled = false; break; # endif # ifdef ZgatewayRF2 case ACTIVE_RF2: disableRF2Receive(); + isDriverEnabled = false; break; # endif default: @@ -150,6 +180,10 @@ void disableCurrentReceiver() { } void enableActiveReceiver() { + if (isDriverEnabled) { + THEENGS_LOG_TRACE(F("Receiver %d is already enabled" CR), currentReceiver); + return; + } THEENGS_LOG_TRACE(F("enableActiveReceiver: %d" CR), iRFConfig.getActiveReceiver()); switch (iRFConfig.getActiveReceiver()) { # ifdef ZgatewayPilight @@ -157,6 +191,7 @@ void enableActiveReceiver() { initCC1101(); enablePilightReceive(); currentReceiver = ACTIVE_PILIGHT; + isDriverEnabled = true; break; # endif # ifdef ZgatewayRF @@ -164,6 +199,7 @@ void enableActiveReceiver() { initCC1101(); enableRFReceive(iRFConfig.getFrequency(), RF_RECEIVER_GPIO, RF_EMITTER_GPIO); currentReceiver = ACTIVE_RF; + isDriverEnabled = true; break; # endif # ifdef ZgatewayRTL_433 @@ -171,6 +207,7 @@ void enableActiveReceiver() { initCC1101(); enableRTLreceive(); currentReceiver = ACTIVE_RTL; + isDriverEnabled = true; break; # endif # ifdef ZgatewayRF2 @@ -178,6 +215,7 @@ void enableActiveReceiver() { initCC1101(); enableRF2Receive(); currentReceiver = ACTIVE_RF2; + isDriverEnabled = true; break; # endif case ACTIVE_RECERROR: diff --git a/main/config_RF.h b/main/config_RF.h index c166a4b0..71b25ff8 100644 --- a/main/config_RF.h +++ b/main/config_RF.h @@ -190,7 +190,7 @@ extern RFConfiguration iRFConfig; /*-------------------RF frequency----------------------*/ //Match frequency to the hardware version of the radio used. #ifndef RF_FREQUENCY -# define RF_FREQUENCY 433.92 +# define RF_FREQUENCY 433.92f #endif /** diff --git a/main/gatewayBLEConnect.cpp b/main/gatewayBLEConnect.cpp index 206fdd0c..0e43d7a0 100644 --- a/main/gatewayBLEConnect.cpp +++ b/main/gatewayBLEConnect.cpp @@ -337,20 +337,20 @@ void BM2_connect::publishData() { static const unsigned char BM6_AES_KEY[16] = { 108, // l 101, // e - 97, // a + 97, // a 103, // g 101, // e 110, // n 100, // d 255, // 0xff 254, // 0xfe - 48, // 0 - 49, // 1 - 48, // 0 - 48, // 0 - 48, // 0 - 48, // 0 - 57, // 9 + 48, // 0 + 49, // 1 + 48, // 0 + 48, // 0 + 48, // 0 + 48, // 0 + 57, // 9 }; void BM6_connect::notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 260315f7..b9c059d6 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -992,7 +992,7 @@ void launchBTDiscovery(bool overrideDiscovery) { if (!BTConfig.extDecoderEnable && // Do not decode if an external decoder is configured p->sensorModel_id > UNKWNON_MODEL && p->sensorModel_id < TheengsDecoder::BLE_ID_NUM::BLE_ID_MAX && - p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC && + p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC && p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM2 && p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM6) { // Exception on HHCCJCY01HHCC and BM2/BM6 as these ones are discoverable and connectable if (isTracker) { @@ -1025,10 +1025,10 @@ void launchBTDiscovery(bool overrideDiscovery) { // This should not happen if JSON_MSG_BUFFER is large enough for // the Theengs json properties THEENGS_LOG_ERROR(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), - error.c_str(), jsonBuffer.capacity(), properties.c_str()); + error.c_str(), jsonBuffer.capacity(), properties.c_str()); } else { THEENGS_LOG_ERROR(F("JSON deserialization of Theengs properties errored: %" CR), - error.c_str()); + error.c_str()); } } for (JsonPair prop : jsonBuffer["properties"].as()) { diff --git a/main/gatewayRF.cpp b/main/gatewayRF.cpp index f9e884f9..87b5a8ec 100644 --- a/main/gatewayRF.cpp +++ b/main/gatewayRF.cpp @@ -402,7 +402,7 @@ void enableRFReceive( float rfFrequency = iRFConfig.getFrequency(), int rfReceiverGPIO = RF_RECEIVER_GPIO, int rfEmitterGPIO = RF_EMITTER_GPIO) { - THEENGS_LOG_NOTICE(F("[RF] Enable RF Receiver: %fMhz, RF_EMITTER_GPIO: %d, RF_RECEIVER_GPIO: %d" CR), rfFrequency, rfEmitterGPIO, rfReceiverGPIO); + THEENGS_LOG_NOTICE(F("[RF] Enable RF Receiver: %F Mhz, RF_EMITTER_GPIO: %d, RF_RECEIVER_GPIO: %d" CR), rfFrequency, rfEmitterGPIO, rfReceiverGPIO); # ifdef RF_DISABLE_TRANSMIT mySwitch.disableTransmit(); @@ -410,7 +410,7 @@ void enableRFReceive( mySwitch.enableTransmit(rfEmitterGPIO); # endif - mySwitch.setRepeatTransmit(rfEmitterGPIO); + mySwitch.setRepeatTransmit(RF_EMITTER_REPEAT); mySwitch.enableReceive(rfReceiverGPIO); THEENGS_LOG_TRACE(F("[RF] Setup command topic: %s%s%s\n Setup done" CR), (const char*)mqtt_topic, (const char*)gateway_name, (const char*)subjectMQTTtoRF); diff --git a/main/gatewaySERIAL.cpp b/main/gatewaySERIAL.cpp index bcf304d2..5f43111c 100644 --- a/main/gatewaySERIAL.cpp +++ b/main/gatewaySERIAL.cpp @@ -316,7 +316,7 @@ void sendMQTTfromNestedJson(JsonVariant obj, char* topic, int level, int maxLeve topic[topicLength] = '\0'; } else { THEENGS_LOG_ERROR(F("Nested key '%s' at level %d does not fit within max topic length of %d, skipping"), - key, level, mqtt_topic_max_size); + key, level, mqtt_topic_max_size); } } diff --git a/main/main.cpp b/main/main.cpp index 7452920d..54f9de4b 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -96,8 +96,8 @@ char gateway_name[parameters_size + 1] = Gateway_Name; unsigned long lastDiscovery = 0; #if BLEDecryptor - char ble_aes[parameters_size] = BLE_AES; - StaticJsonDocument ble_aes_keys; +char ble_aes[parameters_size] = BLE_AES; +StaticJsonDocument ble_aes_keys; #endif #if !MQTT_BROKER_MODE @@ -2185,11 +2185,11 @@ bool loadConfigFromFlash() { if (json.containsKey("ble_aes")) { strcpy(ble_aes, json["ble_aes"]); THEENGS_LOG_TRACE(F("loaded default BLE AES key %s" CR), ble_aes); - } + } if (json.containsKey("ble_aes_keys")) { ble_aes_keys = json["ble_aes_keys"]; THEENGS_LOG_TRACE(F("loaded %d custom BLE AES keys" CR), ble_aes_keys.size()); - } + } # endif result = true; } else { diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index ef07780f..a36a2cff 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -600,7 +600,7 @@ void createDiscovery(const char* sensor_type, } } - if (diagnostic_entity) { // entity_category + if (diagnostic_entity) { // entity_category sensor["ent_cat"] = "diagnostic"; } diff --git a/main/rf/RFConfiguration.cpp b/main/rf/RFConfiguration.cpp index d8a45f3d..20366cc1 100644 --- a/main/rf/RFConfiguration.cpp +++ b/main/rf/RFConfiguration.cpp @@ -65,6 +65,7 @@ void RFConfiguration::reInit() { activeReceiver = ACTIVE_RECEIVER; rssiThreshold = 0; newOokThreshold = 0; + THEENGS_LOG_TRACE(F("RFConfiguration reInit: frequency=%F, activeReceiver=%d" CR), frequency, activeReceiver); } /** @@ -82,13 +83,13 @@ void RFConfiguration::eraseStorage() { preferences.begin(Gateway_Short_Name, false); if (preferences.isKey("RFConfig")) { int result = preferences.remove("RFConfig"); - Log.notice(F("RF config erase result: %d" CR), result); + THEENGS_LOG_NOTICE(F("RF config erase result: %d" CR), result); } else { - Log.notice(F("RF config not found" CR)); + THEENGS_LOG_NOTICE(F("RF config not found" CR)); } preferences.end(); #else - Log.warning(F("RF Config Erase not support with this board" CR)); + THEENGS_LOG_WARNING(F("RF Config Erase not support with this board" CR)); #endif } @@ -120,9 +121,9 @@ void RFConfiguration::saveOnStorage() { preferences.begin(Gateway_Short_Name, false); int result = preferences.putString("RFConfig", conf); preferences.end(); - Log.notice(F("RF Config_save: %s, result: %d" CR), conf.c_str(), result); + THEENGS_LOG_NOTICE(F("RF Config_save: %s, result: %d" CR), conf.c_str(), result); #else - Log.warning(F("RF Config_save not support with this board" CR)); + THEENGS_LOG_WARNING(F("RF Config_save not support with this board" CR)); #endif } @@ -149,24 +150,25 @@ void RFConfiguration::loadFromStorage(bool reinitReceiver) { auto error = deserializeJson(jsonBuffer, preferences.getString("RFConfig", "{}")); preferences.end(); if (error) { - Log.error(F("RF Config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + THEENGS_LOG_ERROR(F("RF Config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); return; } if (jsonBuffer.isNull()) { - Log.warning(F("RF Config is null" CR)); + THEENGS_LOG_WARNING(F("RF Config is null" CR)); return; } JsonObject jo = jsonBuffer.as(); fromJson(jo); - Log.notice(F("RF Config loaded" CR)); + THEENGS_LOG_NOTICE(F("RF Config loaded" CR)); } else { preferences.end(); - Log.notice(F("RF Config not found using default" CR)); + THEENGS_LOG_NOTICE(F("RF Config not found using default" CR)); } #endif // Disable and re-enable the receiver to ensure proper initialization if (reinitReceiver) { iRFReceiver.disable(); + delay(100); iRFReceiver.enable(); } } @@ -209,10 +211,10 @@ void RFConfiguration::loadFromMessage(JsonObject& RFdata) { if (RFdata.containsKey("erase") && RFdata["erase"].as()) { eraseStorage(); - Log.notice(F("RF Config erased" CR)); + THEENGS_LOG_NOTICE(F("RF Config erased" CR)); } else if (RFdata.containsKey("save") && RFdata["save"].as()) { saveOnStorage(); - Log.notice(F("RF Config saved" CR)); + THEENGS_LOG_NOTICE(F("RF Config saved" CR)); } } @@ -245,17 +247,17 @@ void RFConfiguration::fromJson(JsonObject& RFdata) { if (RFdata.containsKey("frequency") && validFrequency(RFdata["frequency"])) { Config_update(RFdata, "frequency", frequency); - Log.notice(F("RF Receive mhz: %F" CR), frequency); + THEENGS_LOG_NOTICE(F("RF Receive mhz: %F" CR), frequency); success = true; } if (RFdata.containsKey("active")) { Config_update(RFdata, "active", activeReceiver); - Log.notice(F("RF receiver active: %d" CR), activeReceiver); + THEENGS_LOG_NOTICE(F("RF receiver active: %d" CR), activeReceiver); success = true; } #ifdef ZgatewayRTL_433 if (RFdata.containsKey("rssithreshold")) { - Log.notice(F("RTL_433 RSSI Threshold : %d " CR), rssiThreshold); + THEENGS_LOG_NOTICE(F("RTL_433 RSSI Threshold : %d " CR), rssiThreshold); Config_update(RFdata, "rssithreshold", rssiThreshold); rtl_433.setRSSIThreshold(rssiThreshold); success = true; @@ -263,18 +265,18 @@ void RFConfiguration::fromJson(JsonObject& RFdata) { # if defined(RF_SX1276) || defined(RF_SX1278) if (RFdata.containsKey("ookthreshold")) { Config_update(RFdata, "ookthreshold", newOokThreshold); - Log.notice(F("RTL_433 ookThreshold %d" CR), newOokThreshold); + THEENGS_LOG_NOTICE(F("RTL_433 ookThreshold %d" CR), newOokThreshold); rtl_433.setOOKThreshold(newOokThreshold); success = true; } # endif if (RFdata.containsKey("status")) { - Log.notice(F("RF get status:" CR)); + THEENGS_LOG_NOTICE(F("RF get status:" CR)); rtl_433.getStatus(); success = true; } if (!success) { - Log.error(F("MQTTtoRF Fail json" CR)); + THEENGS_LOG_ERROR(F("MQTTtoRF Fail json" CR)); } #endif } @@ -307,11 +309,6 @@ void RFConfiguration::toJson(JsonObject& RFdata) { RFdata["rssithreshold"] = rssiThreshold; RFdata["ookthreshold"] = newOokThreshold; RFdata["active"] = activeReceiver; - - // Add white-list vector to the JSON object - JsonArray whiteListArray = RFdata.createNestedArray("white-list"); - // Add black-list vector to the JSON object - JsonArray blackListArray = RFdata.createNestedArray("black-list"); } /** diff --git a/main/sensorDS1820.cpp b/main/sensorDS1820.cpp index a1bd79a3..3cfd05c4 100644 --- a/main/sensorDS1820.cpp +++ b/main/sensorDS1820.cpp @@ -92,10 +92,10 @@ void setupZsensorDS1820() { } ds1820_resolution[ds1820_count] = ds1820.getResolution(ds1820_address); THEENGS_LOG_TRACE(F("DS1820: Device %d, Type: %s, Address: %s, Resolution: %d" CR), - ds1820_count, - (char*)ds1820_type[ds1820_count].c_str(), - (char*)ds1820_addr[ds1820_count].c_str(), - ds1820_resolution[ds1820_count]); + ds1820_count, + (char*)ds1820_type[ds1820_count].c_str(), + (char*)ds1820_addr[ds1820_count].c_str(), + ds1820_resolution[ds1820_count]); ds1820_count++; } } diff --git a/package-lock.json b/package-lock.json index 360a30ea..60e2bed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "dependencies": { + "ini": "^4.1.1", + "markdown-table": "^3.0.3", + "mime-types": "^2.1.35", "vuepress-plugin-sitemap": "^2.3.1" }, "devDependencies": { @@ -25,13 +28,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -51,6 +55,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -356,18 +361,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -397,38 +402,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz", - "integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1703,26 +1696,23 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1750,14 +1740,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2840,6 +2829,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3588,23 +3578,23 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -3620,12 +3610,41 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -3804,9 +3823,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -3981,6 +4000,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -4437,6 +4457,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4733,7 +4754,8 @@ "node_modules/commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "peer": true }, "node_modules/commondir": { "version": "1.0.1", @@ -5255,9 +5277,9 @@ } }, "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "dependencies": { "nice-try": "^1.0.4", @@ -6400,6 +6422,7 @@ "integrity": "sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw==", "dev": true, "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7643,6 +7666,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/global-dirs/node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true, + "license": "ISC" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -8231,21 +8261,21 @@ } }, "node_modules/http-proxy-middleware/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/http-proxy-middleware/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -8264,12 +8294,12 @@ } }, "node_modules/http-proxy-middleware/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -8538,10 +8568,13 @@ "dev": true }, "node_modules/ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/internal-ip": { "version": "4.3.0", @@ -9521,6 +9554,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", "dev": true, + "peer": true, "dependencies": { "argparse": "^1.0.7", "entities": "~1.1.1", @@ -9588,6 +9622,16 @@ "node": ">6.4.0" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9744,7 +9788,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -9753,7 +9796,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9949,9 +9991,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -10715,9 +10757,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -11685,12 +11727,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -11757,20 +11799,49 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -11786,6 +11857,13 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -11842,12 +11920,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -13626,9 +13698,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", + "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", "dev": true, "dependencies": { "cacache": "^12.0.2", @@ -13881,15 +13953,6 @@ "integrity": "sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -14696,6 +14759,7 @@ "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dev": true, + "peer": true, "dependencies": { "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" @@ -14822,9 +14886,9 @@ "dev": true }, "node_modules/vue-server-renderer/node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -15039,13 +15103,13 @@ } }, "node_modules/watchpack/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "optional": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -15080,9 +15144,9 @@ } }, "node_modules/watchpack/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "optional": true, "dependencies": { @@ -15187,6 +15251,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -15820,13 +15885,14 @@ } }, "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, "@babel/compat-data": { @@ -15840,6 +15906,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -16063,15 +16130,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true }, "@babel/helper-validator-option": { @@ -16092,32 +16159,23 @@ } }, "@babel/helpers": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz", - "integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6" - } - }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" } }, "@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "requires": { + "@babel/types": "^7.28.5" + } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.23.3", @@ -16963,23 +17021,20 @@ "dev": true }, "@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.14.0" - } + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { @@ -17001,14 +17056,13 @@ } }, "@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" } }, "@jridgewell/gen-mapping": { @@ -17974,6 +18028,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18567,23 +18622,23 @@ "dev": true }, "body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "requires": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "dependencies": { "debug": { @@ -18595,11 +18650,30 @@ "ms": "2.0.0" } }, + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true } } }, @@ -18741,9 +18815,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -18882,6 +18956,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -19230,6 +19305,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "peer": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -19464,7 +19540,8 @@ "commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "peer": true }, "commondir": { "version": "1.0.1", @@ -19873,9 +19950,9 @@ } }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "requires": { "nice-try": "^1.0.4", @@ -20806,6 +20883,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.7.tgz", "integrity": "sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw==", "dev": true, + "peer": true, "requires": { "esbuild-android-arm64": "0.14.7", "esbuild-darwin-64": "0.14.7", @@ -21711,6 +21789,14 @@ "dev": true, "requires": { "ini": "1.3.7" + }, + "dependencies": { + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true + } } }, "globals": { @@ -22151,18 +22237,18 @@ }, "dependencies": { "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -22175,12 +22261,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -22384,10 +22470,9 @@ "dev": true }, "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==" }, "internal-ip": { "version": "4.3.0", @@ -23145,6 +23230,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", "dev": true, + "peer": true, "requires": { "argparse": "^1.0.7", "entities": "~1.1.1", @@ -23199,6 +23285,11 @@ "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", "dev": true }, + "markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==" + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -23327,14 +23418,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -23498,9 +23587,9 @@ "optional": true }, "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true }, "nanomatch": { @@ -24091,9 +24180,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { @@ -24923,12 +25012,12 @@ "dev": true }, "qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "requires": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" } }, "query-string": { @@ -24980,15 +25069,36 @@ "dev": true }, "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true + } } }, "rc": { @@ -25001,6 +25111,14 @@ "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + } } }, "readable-stream": { @@ -25053,12 +25171,6 @@ "regenerate": "^1.4.2" } }, - "regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, "regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -26506,9 +26618,9 @@ } }, "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", + "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", "dev": true, "requires": { "cacache": "^12.0.2", @@ -26691,12 +26803,6 @@ "integrity": "sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -27336,6 +27442,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", "dev": true, + "peer": true, "requires": { "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" @@ -27429,9 +27536,9 @@ "dev": true }, "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -27599,13 +27706,13 @@ "optional": true }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "optional": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "chokidar": { @@ -27626,9 +27733,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "optional": true, "requires": { @@ -27720,6 +27827,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dev": true, + "peer": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", diff --git a/package.json b/package.json index 72982903..e30c63a7 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,20 @@ { "scripts": { - "docs:dev": "vuepress dev docs", - "docs:build": "vuepress build docs" + "gen:ssl": "node scripts/ensure_ssl_certs.js", + "gen:metadata": "node docsgen/gen_wu.js", + "gen:site:only": "vuepress build docs", + "docs:dev": "npm run gen:metadata && vuepress dev docs", + "docs:build": "npm run gen:metadata && npm run gen:site:only", + "site:preview": " npm run gen:ssl && node scripts/preview_site.js", + "site:preview:full": "npm run docs:build && npm run gen:ssl && node scripts/preview_site.js" }, "devDependencies": { "vuepress": "^1.9.10" }, "dependencies": { + "ini": "^4.1.1", + "markdown-table": "^3.0.3", + "mime-types": "^2.1.35", "vuepress-plugin-sitemap": "^2.3.1" } -} +} \ No newline at end of file diff --git a/scripts/CI_SCRIPTS.md b/scripts/CI_SCRIPTS.md index e93d37e8..a63f2b7c 100644 --- a/scripts/CI_SCRIPTS.md +++ b/scripts/CI_SCRIPTS.md @@ -11,11 +11,13 @@ This documentation describes the CI/CD scripts used to build OpenMQTTGateway fir - [ci.sh - Main Entry Point](#commands) - [ci.sh build - Build Firmware](#cish-build---build-firmware) - [ci.sh site - Build Documentation](#cish-site---build-documentation) - - [ci.sh qa - Code Formatting Check](#cish-qa---code-formatting-check) + - [ci.sh qa - Code Quality Checks](#cish-qa---code-quality-checks) + - [ci.sh security - Security Vulnerability Scan](#cish-security---security-vulnerability-scan) - [Internal Scripts](#internal-scripts) - - [ci_set_version.sh](#ci_set_versionsh) + - [ci_list-env.sh](#ci_list-envsh) - [ci_build_firmware.sh](#ci_build_firmwaresh) - [ci_prepare_artifacts.sh](#ci_prepare_artifactssh) + - [ci_security.sh](#ci_securitysh) - [ci_00_config.sh](#ci_00_configsh) - [Python Helper Scripts](#python-helper-scripts) - [generate_board_docs.py](#generate_board_docspy) @@ -23,6 +25,7 @@ This documentation describes the CI/CD scripts used to build OpenMQTTGateway fir - [Environment Variables](#environment-variables) - [Exit Codes](#exit-codes) - [Environment Detection](#environment-detection) +- [GitHub Actions Workflows Integration](#github-actions-workflows-integration) @@ -32,27 +35,28 @@ This documentation describes the CI/CD scripts used to build OpenMQTTGateway fir ``` ci.sh (dispatcher) -├── build → ci_build.sh → ci_set_version.sh -│ → ci_build_firmware.sh -│ → ci_prepare_artifacts.sh -├── site → ci_site.sh → generate_board_docs.py -│ → gen_wu.py -├── qa → ci_qa.sh → clang-format -└── all → qa + build + site (sequential) +├── build → ci_build.sh → ci_build_firmware.sh +│ → ci_prepare_artifacts.sh (when --deploy-ready) +├── site → ci_site.sh +├── qa → ci_qa.sh +├── security → ci_security.sh (vulnerability scanning with Trivy) +├── list-env → ci_list-env.sh +└── all → qa + build (all envs with --mode) + site sequential ``` ### Script Description | Script | Purpose | Called By | -|--------|---------|-----------| +|--------|---------|----------| | `ci.sh` | Main command dispatcher | User/GitHub Actions | | `ci_build.sh` | Build firmware orchestrator | ci.sh build | | `ci_site.sh` | Documentation build orchestrator | ci.sh site | -| `ci_qa.sh` | Code formatting checker | ci.sh qa | -| `ci_set_version.sh` | Version injection in firmware | ci_build.sh | +| `ci_00_config.sh` | Shared configuration loader | ci.sh | +| `ci_qa.sh` | Quality assurance and shellcheck | ci.sh qa | +| `ci_security.sh` | Security vulnerability scanning with Trivy | ci.sh security | +| `ci_list-env.sh` | List PlatformIO environments (fast JSON or full scan) | ci.sh list-env / direct | | `ci_build_firmware.sh` | PlatformIO build execution | ci_build.sh | -| `ci_prepare_artifacts.sh` | Artifact packaging | ci_build.sh | -| `ci_00_config.sh` | Shared configuration and functions | All scripts | +| `ci_prepare_artifacts.sh` | Artifact packaging (when requested) | ci_build.sh | ### Output Structure @@ -67,16 +71,14 @@ Build outputs are organized in the project root: generated/ ├── artifacts/ # Packaged firmware artifacts -└── site/ # Built documentation (VuePress output) - -scripts/ -├── latest_version.json # Production version metadata -└── latest_version_dev.json # Development version metadata +├── site/ # Built documentation (VuePress output) +└── reports/ # Security scan and SBOM reports + └── sbom/ # SBOM in CycloneDX and SPDX formats ``` ## Commands -`ci.sh` is the main Entry Point. Command dispatcher that routes to specialized scripts. +`ci.sh` is the main entry point. It dispatches to the specialized scripts. **Usage:** ```bash @@ -84,115 +86,90 @@ scripts/ ``` **Commands:** -- `build` - Build firmware for specified environment -- `site` or `docs` - Build documentation website -- `qa` or `lint` - Run code formatting checks -- `all` or `pipeline` - Run complete pipeline (qa + build + site) +- `build` - Build firmware for a PlatformIO environment (optionally prepare artifacts) +- `site` or `docs` - Build documentation site +- `qa` or `lint` - Run formatting and shellcheck checks +- `security` - Scan for security vulnerabilities using Trivy (filesystem, container images) +- `all` or `pipeline` - Run qa → build (all environments) → site with injected mode +- `list-env` - List available PlatformIO environments (JSON fast list or full ini scan) **Examples:** ```bash -# Get Help +# Help per command ./scripts/ci.sh build --help -./scripts/ci.sh qa --help ./scripts/ci.sh site --help +./scripts/ci.sh qa --help +./scripts/ci.sh security --help +./scripts/ci.sh list-env --help # Build firmware -./scripts/ci.sh build esp32dev-ble --mode dev -./scripts/ci.sh build esp32dev-all-test --version v1.8.0 --deploy-ready +./scripts/ci.sh build esp32dev-all-test --mode dev +./scripts/ci.sh build esp32dev-bt --version v1.8.0 --deploy-ready --output generated/artifacts -# Build documentation -./scripts/ci.sh site --mode prod -./scripts/ci.sh site --mode dev --url-prefix /dev/ +# Build docs +./scripts/ci.sh site --mode prod --url-prefix / +./scripts/ci.sh site --mode dev --preview -# Check code formatting +# QA (formatting + shellcheck) ./scripts/ci.sh qa --check -./scripts/ci.sh qa --fix +./scripts/ci.sh qa --fix --verbose -# Run complete pipeline -./scripts/ci.sh all esp32dev-ble --version v1.8.0 -./scripts/ci.sh all esp32dev-ble --no-site +# Security scanning +./scripts/ci.sh security --scan-type fs --severity HIGH,CRITICAL +./scripts/ci.sh security --scan-type fs --generate-sbom + +# Full pipeline (qa + build all envs + site) +./scripts/ci.sh all --mode dev +./scripts/ci.sh all --mode prod --preview ``` -**Options for `all` command:** -- `--no-site` - Skip documentation build step -- All options from `build` command are passed through - --- ### ci.sh build - Build Firmware -Orchestrates complete firmware build: version injection, compilation, artifact packaging. +Runs the build pipeline (tool checks → PlatformIO build → optional artifact packaging). **Usage:** ```bash ./scripts/ci.sh build [OPTIONS] ``` -**Required Arguments:** -- `` - PlatformIO environment name (e.g., esp32dev-ble, nodemcuv2-rf) +**Required Argument:** +- `` PlatformIO environment name (e.g., esp32dev-ble) **Options:** -- `--version ` - Version string to inject (default: auto-generated from git) -- `--mode ` - Build mode (default: prod) - - `dev` - Enables development OTA, sets DEVELOPMENTOTA=true - - `prod` - Standard production build -- `--deploy-ready` - Package artifacts with deployment naming (env-firmware.bin) -- `--output ` - Output directory for artifacts (default: generated/artifacts) -- `--skip-verification` - Skip build tools verification -- `--clean` - Clean previous build before starting -- `--verbose` - Enable verbose PlatformIO output -- `--list-envs` - List all available PlatformIO environments -- `--help` - Show help message +- `--mode ` Build mode (default: prod). `dev` enables OTA flags in the PlatformIO build. +- `--deploy-ready` Copy/rename build outputs and libs via ci_prepare_artifacts.sh. +- `--version [TAG]` Set version used for `OMG_VERSION` and artifact folder naming. If TAG is omitted the script auto-generates (CI: BUILD_NUMBER/GIT_COMMIT, local: timestamp). +- `--output ` Output directory for packaged artifacts (only used when `--deploy-ready` is set, default `generated/artifacts`). +- `--skip-verification` Skip the tool availability checks. +- `--clean` Clean the PlatformIO environment before building. +- `--verbose` Verbose PlatformIO output. +- `--help` Show help. -**Execution Flow:** -``` -ci.sh build esp32dev-ble --version v1.8.0 --mode prod --deploy-ready - │ - ├─> ci_build.sh (orchestrator) - │ ├─> verify_build_tools() - Check python3, platformio, git - │ ├─> ci_set_version.sh v1.8.0 - Inject version in User_config.h - │ ├─> ci_build_firmware.sh esp32dev-ble - Execute PlatformIO build - │ └─> ci_prepare_artifacts.sh esp32dev-ble --deploy - Package binaries - │ - └─> Outputs in generated/artifacts/ - ├─ esp32dev-ble-firmware.bin - ├─ esp32dev-ble-bootloader.bin - └─ esp32dev-ble-partitions.bin -``` +**Behavior:** +- Tool check verifies python3, platformio, git (can be skipped). +- Builds via ci_build_firmware.sh (adds `--dev-ota` when `--mode dev`). +- Packaging runs only when `--deploy-ready` is provided; artifacts land under `generated/artifacts/firmware_build/` with env-prefixed filenames plus zipped libraries. + **Examples:** ```bash -# Development build +# Dev build with OTA flags ./scripts/ci.sh build esp32dev-ble --mode dev -# Production build with version -./scripts/ci.sh build esp32dev-ble --version v1.8.0 --mode prod +# Prod build with auto-version and deployable artifacts +./scripts/ci.sh build esp32dev-bt --version --deploy-ready --output generated/artifacts -# Deploy-ready build -./scripts/ci.sh build esp32dev-all-test --version v1.8.0 --deploy-ready - -# Clean build with verbose output +# Clean + verbose build ./scripts/ci.sh build nodemcuv2-rf --clean --verbose - -# List available environments -./scripts/ci.sh build --list-envs ``` -**Environment Variables:** -- `CI` - Set to 'true' in CI/CD environments -- `BUILD_NUMBER` - Build number from CI/CD system -- `GIT_COMMIT` - Git commit hash for auto-versioning -- `PLATFORMIO_BUILD_FLAGS` - Additional PlatformIO flags (set by script when --mode dev) - -**Output Files:** -- Standard mode: `firmware.bin`, `partitions.bin` in generated/artifacts/ -- Deploy mode: `-firmware.bin`, `-bootloader.bin`, `-partitions.bin` - --- ### ci.sh site - Build Documentation -Builds VuePress documentation website with version management and WebUploader manifest generation. +Builds the VuePress documentation site. **Usage:** ```bash @@ -200,75 +177,36 @@ Builds VuePress documentation website with version management and WebUploader ma ``` **Options:** -- `--mode ` - Documentation mode (default: prod) - - `dev` - Development documentation with watermark - - `prod` - Production documentation -- `--version-source ` - Version source (default: release) - - `release` - Use git tag as version - - `custom` - Use custom version string -- `--custom-version ` - Custom version string (requires --version-source custom) -- `--url-prefix ` - Base URL path (default: /) - - Example: `/dev/` for development subdirectory -- `--no-webuploader` - Skip WebUploader manifest generation -- `--webuploader-args ` - Additional arguments for gen_wu.py -- `--preview` - Open browser after build (local development) -- `--help` - Show help message +- `--mode ` Build mode (default: dev). +- `--url-prefix ` Base URL path for links (e.g., '/' for root, '/dev/' for dev) (default: /dev/). +- `--version ` Version string written to meta.json (default: edge). +- `--preview` Start the local HTTPS preview server after building (https://localhost:8443). +- `--clean` Remove generated/site folder before building. +- `--insecure-curl` Allow curl to skip TLS verification when downloading common config. +- `--help` Show help. -**Execution Flow:** -``` -ci.sh site --mode prod --version-source release - │ - ├─> ci_site.sh (orchestrator) - │ ├─> check_requirements() - Verify node, npm, python3, pip3 - │ ├─> install_dependencies() - npm install, pip3 install packages - │ ├─> download_common_config() - Fetch from theengs.io - │ ├─> get_version() - Extract from git tag or use custom - │ ├─> set_version() - Update VuePress config and JSON files - │ ├─> set_url_prefix() - Set base path in config - │ ├─> generate_board_docs.py - Auto-generate board documentation - │ ├─> npm run docs:build - Build VuePress site - │ └─> gen_wu.py - Generate WebUploader manifest - │ - └─> Outputs in generated/site/ - ├─ index.html - ├─ assets/ - └─ [board documentation pages] -``` +**Behavior:** +- Checks for node, npm, openssl; installs npm deps; downloads commonConfig.js. +- Writes docs/.vuepress/meta.json with mode/url_prefix/version; builds via `npm run docs:build`. +- Preview mode runs `npm run site:preview`. **Examples:** ```bash -# Production documentation -./scripts/ci.sh site --mode prod +# Production build +./scripts/ci.sh site --mode prod --url-prefix / -# Development documentation with custom version -./scripts/ci.sh site --mode dev --version-source custom --custom-version "DEVELOPMENT SHA:abc123" +# Development build with preview +./scripts/ci.sh site --mode dev --url-prefix /dev/ --version edge --preview -# Documentation for /dev/ subdirectory -./scripts/ci.sh site --mode dev --url-prefix /dev/ - -# Skip WebUploader manifest -./scripts/ci.sh site --no-webuploader - -# Local preview -./scripts/ci.sh site --preview +# Clean then build with custom version +./scripts/ci.sh site --clean --version 1.8.0 ``` -**Required Tools:** -- Node.js (for VuePress) -- npm (for package management) -- Python 3 (for board docs generator) -- pip3 (for Python dependencies: requests, pandas, markdown, pytablereader, tabulate) - -**Output Files:** -- `generated/site/` - Complete static website -- `scripts/latest_version.json` - Production version metadata (updated) -- `scripts/latest_version_dev.json` - Development version metadata (updated) - --- -### ci.sh qa - Code Formatting Check +### ci.sh qa - Code Quality Checks -Checks and fixes code formatting using clang-format. +Checks and fixes code formatting using clang-format and runs shellcheck on shell scripts. **Usage:** ```bash @@ -276,13 +214,16 @@ Checks and fixes code formatting using clang-format. ``` **Options:** -- `--check` - Check formatting without modifying files (default) -- `--fix` - Automatically fix formatting issues -- `--source ` - Source directory to check (default: main) -- `--extensions ` - File extensions to check, comma-separated (default: h,ino,cpp) -- `--clang-format-version ` - clang-format version to use (default: 9) -- `--verbose` - Show detailed output for each file -- `--help` - Show help message +- `--check` Check formatting only (default) +- `--fix` Apply formatting in place +- `--format` Run only format checks +- `--shellcheck` Run shellcheck on shell scripts in scripts/ directory +- `--all` Future hook to run all QA checks (current implementation runs formatting + shellcheck) +- `--source ` Source directory for formatting checks (default: main) +- `--extensions ` File extensions for formatting (comma-separated, default: h,ino,cpp) +- `--clang-format-version ` clang-format version to use (default: 9) +- `--verbose` Verbose output +- `--help` Show help **Execution Flow:** ``` @@ -290,27 +231,35 @@ ci.sh qa --check --source main --extensions h,ino │ ├─> ci_qa.sh (orchestrator) │ ├─> check_clang_format() - Find clang-format-9 or clang-format - │ ├─> find_files() - Locate files matching extensions in source dir - │ └─> check_formatting() - Run clang-format --dry-run --Werror - │ └─> Report files with formatting issues + │ │ ├─> find_files() - Locate files matching extensions in source dir + │ │ └─> check_formatting() - Run clang-format --dry-run --Werror + │ │ └─> Report files with formatting issues + │ │ + │ └─> shellcheck_check() - Find and scan shell scripts + │ ├─> find_shell_scripts() - Locate *.sh files in scripts/ directory + │ └─> run_shellcheck() - Run shellcheck on found scripts + │ └─> Report shell script issues │ - └─> Exit code: 0 (pass) or 1 (formatting issues found) + └─> Exit code: 0 (pass) or 1 (issues found) ``` **Examples:** ```bash -# Check formatting (CI mode) +# Check both formatting and shellcheck (default) ./scripts/ci.sh qa --check # Fix formatting automatically ./scripts/ci.sh qa --fix -# Check specific directory -./scripts/ci.sh qa --check --source lib +# Check only formatting for specific directory +./scripts/ci.sh qa --check --format --source lib # Check only .h and .ino files ./scripts/ci.sh qa --check --extensions h,ino +# Check only shellcheck for shell scripts +./scripts/ci.sh qa --check --shellcheck + # Check with verbose output ./scripts/ci.sh qa --check --verbose @@ -321,46 +270,201 @@ ci.sh qa --check --source main --extensions h,ino **Required Tools:** - clang-format (version specified, default: 9) - Install: `sudo apt-get install clang-format-9` +- shellcheck (for shell script linting) + - Install: `sudo apt-get install shellcheck` **Output:** -- Check mode: Lists files with formatting issues and shows diffs -- Fix mode: Modifies files in-place and reports changes -- Exit code 0: All files properly formatted -- Exit code 1: Formatting issues found (in check mode) +- Check mode: Lists files with formatting issues and shell script errors, shows diffs +- Fix mode: Modifies formatting in-place and reports changes +- Exit code 0: All checks passed (proper formatting and no shellcheck errors) +- Exit code 1: Issues found (formatting or shellcheck violations) + +--- + +### ci.sh security - Security Vulnerability Scan + +Scans the project for security vulnerabilities using Trivy and generates Software Bill of Materials (SBOM). + +**Usage:** +```bash +./scripts/ci.sh security [OPTIONS] +``` + +**Options:** +- `--scan-type ` Type of scan (default: fs) + - `fs` - Filesystem scan (default, scans for vulnerabilities and misconfigurations) + - `config` - Configuration scan only + - `image` - Container image scan +- `--severity ` Severity levels to report (comma-separated: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) (default: HIGH,CRITICAL) +- `--scan-path ` Path to scan (default: current directory `.`) +- `--generate-sbom` Generate Software Bill of Materials in CycloneDX and SPDX formats (default: true) +- `--exit-code <0|1>` Exit code when vulnerabilities found (0=continue, 1=fail) (default: 0) +- `--upload-to-security-tab` Upload SARIF report to GitHub Security tab (GitHub Actions only, default: true) +- `--verbose` Verbose output +- `--help` Show help + +**Behavior:** +- Installs Trivy if not present +- Scans filesystem or configuration for known vulnerabilities +- Generates multiple report formats: SARIF, JSON, table summary +- Creates SBOM in CycloneDX and SPDX formats when `--generate-sbom` is enabled +- Reports are saved to `generated/reports/` directory +- Summary is appended to GitHub job summary when running in GitHub Actions +- Uploads SARIF to GitHub Security tab for dashboard visibility + +**Output Structure:** +``` +generated/reports/ +├── trivy-results.sarif # SARIF format (for GitHub Security tab) +├── trivy-results.json # JSON format (detailed results) +├── security-summary.md # Markdown summary +└── sbom/ + ├── sbom.cyclonedx.json # CycloneDX format + └── sbom.spdx.json # SPDX format +``` + +**Examples:** +```bash +# Scan filesystem for HIGH and CRITICAL vulnerabilities +./scripts/ci.sh security --scan-type fs --severity HIGH,CRITICAL + +# Full scan with all severity levels and SBOM generation +./scripts/ci.sh security --scan-type fs --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL --generate-sbom + +# Scan specific path with verbose output +./scripts/ci.sh security --scan-path ./lib --verbose + +# Configuration scan only +./scripts/ci.sh security --scan-type config + +# Scan and fail on vulnerabilities +./scripts/ci.sh security --exit-code 1 +``` + +**Required Tools:** +- Trivy (vulnerability scanner) + - Install: `wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -` && `echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list` && `sudo apt-get update && sudo apt-get install -y trivy` + +**Output:** +- Detailed SARIF and JSON reports for integration with tools +- Human-readable Markdown summary with findings count +- SBOM artifacts for supply chain tracking +- GitHub Security tab integration when in GitHub Actions +- Exit code 0: Scan completed (vulnerabilities may have been found) +- Exit code 1: Scan failed or critical vulnerabilities found (only if `--exit-code 1`) + +--- + +### ci.sh all - Complete Pipeline + +Runs the complete CI/CD pipeline (qa → build all environments → site) with mode injected to all steps. + +**Usage:** +```bash +./scripts/ci.sh all --mode [--preview] +``` + +**Required Options:** +- `--mode ` Build mode (required). Injected to all pipeline steps: + - `qa`: Always runs `--check` + - `build`: Passes mode to all PlatformIO environments (dev enables OTA flags) + - `site`: Passes mode for documentation generation + +**Optional Options:** +- `--preview` Start local HTTPS preview server at https://localhost:8443 after building the site +- `--help` Show help + +**Behavior:** +- No environment argument needed; builds **all** available environments +- All three steps (qa, build, site) receive the same `--mode` value +- If any step fails, the pipeline aborts +- Site build warnings do not abort the pipeline (continues with success status) + +**Execution Flow:** +``` +ci.sh all --mode dev --preview + │ + ├─> Step 1: ci_qa.sh --check + │ └─> Exit on failure + │ + ├─> Step 2: ci_build.sh --mode dev (builds all environments) + │ └─> Exit on failure + │ + └─> Step 3: ci_site.sh --mode dev --preview + ├─> Build documentation + └─> Start preview server at https://localhost:8443 +``` + +**Examples:** +```bash +# Complete pipeline in dev mode +./scripts/ci.sh all --mode dev + +# Complete pipeline in prod mode with site preview +./scripts/ci.sh all --mode prod --preview + +# Help for this command +./scripts/ci.sh all --help +``` + +**Output:** +- Step-by-step progress messages for each pipeline phase +- Final summary showing mode, preview status, duration, and overall status +- Exit code 0: All steps successful +- Exit code 1: Any step failed + +--- + +### ci.sh list-env - List Environments + +Lists PlatformIO environments available to build. + +**Usage:** +```bash +./scripts/ci.sh list-env [--full] +``` + +**Options:** +- Default: read .github/workflows/environments.json (requires jq) and show curated list. +- `--full` Parse platformio.ini and environments.ini for an exhaustive list (skips *-test and *-all-). +- `--help` Show help. + +**Examples:** +```bash +./scripts/ci.sh list-env +./scripts/ci.sh list-env --full +``` --- ## Internal Scripts -These scripts are called by the main orchestrators. It can be invoked directly but is not raccomanded. +These scripts are called by the main orchestrators. They can be run directly for troubleshooting, but the preferred entrypoints are the ci.sh commands. -### ci_set_version.sh +### ci_list-env.sh -Injects version string into firmware configuration files. +Lists PlatformIO environments for OpenMQTTGateway. -**Called By:** `ci_build.sh` +**Called By:** `ci.sh list-env` or direct call **Usage:** ```bash -./scripts/ci_set_version.sh [--dev] +./scripts/ci_list-env.sh [--full] ``` -**Arguments:** -- `` - Version string to inject (e.g., v1.8.0 or abc123) -- `--dev` - Development mode (updates latest_version_dev.json) +**Options:** +- Default: read .github/workflows/environments.json for a curated list (needs jq) +- `--full` - Parse platformio.ini and environments.ini for all envs except *-test and *-all- +- `-h|--help` - Show help -**Files Modified:** -- `main/User_config.h` - Replaces "version_tag" with actual version -- `scripts/latest_version.json` - Production version metadata -- `scripts/latest_version_dev.json` - Development version metadata (--dev mode) +**Output:** +- Shows sorted environments in columns and prints the total count -**Behavior:** -- Creates .bak backup files before modification -- Replaces all occurrences of "version_tag" string -- Validates version string (must not be empty or "version_tag") -- Cleans up backup files on success - ---- +**Examples:** +```bash +./scripts/ci_list-env.sh +./scripts/ci_list-env.sh --full +``` ### ci_build_firmware.sh @@ -377,14 +481,17 @@ Executes PlatformIO build for specified environment. - `` - PlatformIO environment name **Options:** +- `--version ` - Set OMG_VERSION for the build (passed through from ci_build.sh) - `--dev-ota` - Enable development OTA (sets PLATFORMIO_BUILD_FLAGS) - `--clean` - Clean before build - `--verbose` - Verbose PlatformIO output +- `--no-verify` - Skip artifact verification after build **Environment Variables Set:** - `PYTHONIOENCODING=utf-8` - `PYTHONUTF8=1` - `PLATFORMIO_BUILD_FLAGS="-DDEVELOPMENTOTA=true"` (when --dev-ota) +- `OMG_VERSION` (when --version is provided) **PlatformIO Command:** ```bash @@ -400,7 +507,7 @@ platformio run -e [--verbose] ### ci_prepare_artifacts.sh -Packages firmware binaries from PlatformIO build directory. +Packages firmware binaries and libraries from a PlatformIO build directory; can also only create source archive when no environment is provided. **Called By:** `ci_build.sh` @@ -410,29 +517,19 @@ Packages firmware binaries from PlatformIO build directory. ``` **Arguments:** -- `` - PlatformIO environment name +- `` - PlatformIO environment name (optional; if omitted only source archive is created) **Options:** -- `--deploy` - Use deployment naming (prefix with environment name) -- `--output ` - Output directory (default: generated/artifacts) +- `--output ` Output directory (default: generated/artifacts) +- `--version ` Append a version subfolder inside the output directory +- `--clean` Clean output directory before writing +- `--help` Show help **Behavior:** - -Standard mode (no --deploy): -- Copies: `firmware.bin`, `partitions.bin` -- Does NOT copy: `bootloader.bin` - -Deploy mode (with --deploy): -- Copies and renames: - - `firmware.bin` → `-firmware.bin` - - `bootloader.bin` → `-bootloader.bin` - - `partitions.bin` → `-partitions.bin` - -**Source Location:** -- `.pio/build//` - -**Output Location:** -- Specified by `--output` or default `generated/artifacts/` +- If `version` is provided, outputs go to `//firmware_build/`; otherwise to `/firmware_build/`. +- With an environment: copies and renames firmware.bin/partitions.bin/bootloader.bin/boot_app0.bin to `-*.bin`, then zips libraries for that env into `*-libraries.tgz`. +- Without an environment: only creates `OpenMQTTGateway_sources.tgz` from `main` and `LICENSE.txt`. +- Lists the prepared artifacts and their sizes at the end. --- @@ -452,6 +549,7 @@ Shared configuration and helper functions for all CI scripts. - `BUILD_DIR=".pio/build"` - PlatformIO build directory - `ARTIFACTS_DIR="generated/artifacts"` - Artifact output directory - `SITE_DIR="generated/site"` - Documentation output directory +- `REPORTS_DIR="generated/reports"` - Security scan and quality reports directory **Logging Functions:** ```bash @@ -463,9 +561,74 @@ log_success "message" # Green [SUCCESS] prefix --- +### ci_security.sh + +Performs security vulnerability scanning and Software Bill of Materials (SBOM) generation using Trivy. + +**Called By:** `ci.sh security` + +**Usage:** +```bash +./scripts/ci_security.sh [OPTIONS] +``` + +**Options:** +- `--scan-type ` Type of scan (default: fs) + - `fs` - Filesystem scan (default) + - `config` - Configuration scan + - `image` - Container image scan +- `--severity ` Severity levels (comma-separated: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) (default: HIGH,CRITICAL) +- `--scan-path ` Path to scan (default: .) +- `--generate-sbom` Generate SBOM (default: true) +- `--exit-code <0|1>` Exit code behavior (0=continue, 1=fail) (default: 0) +- `--upload-to-security-tab` Upload SARIF to GitHub (default: true) +- `--verbose` Verbose output +- `--help` Show help + +**Behavior:** +- Ensures Trivy is installed via package manager +- Creates `generated/reports/` directory structure +- Runs Trivy with specified parameters +- Generates SARIF, JSON, and table formats +- Creates SBOM in CycloneDX and SPDX formats (when enabled) +- Uploads SARIF to GitHub Security tab when `GITHUB_TOKEN` and `--upload-to-security-tab` are set +- Appends summary to GitHub job summary if in GitHub Actions +- Validates critical vulnerabilities and exits with code 1 if found and `--exit-code 1` is set + +**Output Files:** +- `generated/reports/trivy-results.sarif` - SARIF format for GitHub integration +- `generated/reports/trivy-results.json` - Full JSON results +- `generated/reports/security-summary.md` - Human-readable summary +- `generated/reports/sbom/sbom.cyclonedx.json` - CycloneDX SBOM +- `generated/reports/sbom/sbom.spdx.json` - SPDX SBOM + +**Exit Codes:** +- `0` - Success (vulnerabilities may have been found) +- `1` - Scan failed, critical vulnerabilities found (only if `--exit-code 1`), or missing dependencies + +**Trivy Integration:** +- Scans for known vulnerabilities in dependencies +- Detects misconfigurations and insecure practices +- Generates compliant SBOM artifacts +- Provides detailed reporting in multiple formats + +**Example:** +```bash +# Scan filesystem with severity filter +./scripts/ci_security.sh --scan-type fs --severity HIGH,CRITICAL + +# Local scan with SBOM and JSON output +./scripts/ci_security.sh --scan-type fs --generate-sbom --verbose + +# In GitHub Actions with security tab upload +./scripts/ci_security.sh --scan-type fs --severity HIGH,CRITICAL --upload-to-security-tab +``` + +--- + ## Python Helper Scripts -Other scripts are present and used as internal scripts and it's used as retrocompatibility. Below the lists: +Legacy helper scripts are kept for compatibility; they are not called by the current ci_site.sh flow. Below the list: - `generate_board_docs.py` - `gen_wu.py` @@ -473,7 +636,7 @@ Other scripts are present and used as internal scripts and it's used as retrocom Auto-generates board-specific documentation pages from platformio.ini. -**Called By:** `ci_site.sh` +**Called By:** Not invoked by current ci_site.sh (legacy helper) **Usage:** ```bash @@ -498,7 +661,7 @@ python3 ./scripts/generate_board_docs.py Generates WebUploader manifest for OTA firmware updates. -**Called By:** `ci_site.sh` +**Called By:** Not invoked by current ci_site.sh (legacy helper) **Usage:** ```bash @@ -527,10 +690,10 @@ python3 ./scripts/gen_wu.py [--dev] [repository] Scripts respect these environment variables: -- `PYTHONIOENCODING=utf-8`: Python encoding -- `PYTHONUTF8=1`: UTF-8 mode -- `PLATFORMIO_BUILD_FLAGS`: Custom build flags -- `ESP32_PLATFORM_VERSION`: Extracted automatically +- `CI`/`BUILD_NUMBER`/`GIT_COMMIT`: Used by ci_build.sh to auto-generate version when `--version` flag has no tag +- `PYTHONIOENCODING=utf-8`, `PYTHONUTF8=1`: Python encoding settings set by ci_build_firmware.sh +- `PLATFORMIO_BUILD_FLAGS`: Set to include development OTA flag when `--dev-ota` is used +- `OMG_VERSION`: Set when `--version` is passed to ci_build.sh/ci_build_firmware.sh --- @@ -567,6 +730,58 @@ CI/CD environments typically set: - `BUILD_NUMBER` (build number) - `GIT_COMMIT` (commit hash) +--- + +## GitHub Actions Workflows Integration + +The CI scripts integrate with GitHub Actions workflows in `.github/workflows/`: + +### task-lint.yml +- Reusable workflow that runs `ci.sh qa --check` +- Installs clang-format and shellcheck +- Validates code formatting and shell script quality +- Can be called with custom source directory and file extensions + +### task-build.yml +- Main build workflow orchestrator +- Calls task-lint.yml for code quality checks +- Calls task-security-scan.yml for vulnerability scanning +- Builds firmware for all or specified environments +- Prepares and uploads build artifacts +- Supports matrix builds for multiple environments +- Manages build artifact retention + +### task-security-scan.yml +- Reusable security scanning workflow +- Installs Trivy vulnerability scanner +- Calls `ci_security.sh` with configurable parameters +- Generates SARIF, JSON, and SBOM reports +- Uploads SARIF to GitHub Security tab for code scanning dashboard +- Fails build on critical vulnerabilities when configured +- Uploads SBOM artifacts for supply chain tracking + +### security-scan.yml +- Scheduled security scanning (runs weekly by default) +- Triggered manually with input parameters +- Allows filtering by severity level +- Configurable exit behavior (fail or continue) +- Optional SBOM generation and upload + +**Workflow Dependencies:** +``` +task-build.yml +├─> task-lint.yml (linting) +├─> task-security-scan.yml (security scanning) +└─> Build environment matrix (firmware compilation) +``` + +**Key Features:** +- Parallel linting and security scans +- Artifact retention policies +- GitHub Security tab integration +- Detailed build reports +- SBOM generation for compliance +- Support for custom build parameters --- diff --git a/scripts/ci.sh b/scripts/ci.sh index 6f68783e..698484c8 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -6,8 +6,9 @@ set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR + # Load shared configuration (colors, logging functions, paths) if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -36,7 +37,9 @@ Commands: build Build firmware for specified environment site Build and deploy documentation/website qa Run quality assurance checks (linting, formatting) + security Run security vulnerability scan using Trivy all Run complete pipeline (qa + build + site) + list-env List available environments for building firmware Examples: # Build firmware @@ -51,78 +54,217 @@ Examples: $0 qa --check $0 qa --fix - # Run complete pipeline - $0 all esp32dev-bt --version v1.8.0 + # Run security scan + $0 security + $0 security --scan-type config --exit-code 1 + $0 security --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL + + # Run complete pipeline (all envs, mode required) + $0 all --mode dev + $0 all --mode prod --preview + $0 all --mode dev -e esp32dev-bt + + # List available environments + $0 list-env Get help for specific commands: $0 build --help $0 site --help $0 qa --help + $0 security --help + $0 list-env --help EOF exit 0 } -# Function to run build pipeline -run_build_pipeline() { - log_info "Executing build pipeline..." - "${SCRIPT_DIR}/ci_build.sh" "$@" + + +# Function to get list of environments to build +# Uses ci_list-env.sh (no parameters) and returns one env per line +get_environments() { + # Run the curated list script and normalize output + "${SCRIPT_DIR}/ci_list-env.sh" \ + | sed -r 's/\x1B\[[0-9;]*[mK]//g' \ + | tr '\t ' '\n' \ + | sed '/^\s*$/d' \ + | grep -E '^[A-Za-z0-9._-]+$' \ + | sort -u } -# Function to run site pipeline -run_site_pipeline() { - log_info "Executing site pipeline..." - "${SCRIPT_DIR}/ci_site.sh" "$@" -} - -# Function to run QA pipeline -run_qa_pipeline() { - log_info "Executing QA pipeline..." - "${SCRIPT_DIR}/ci_qa.sh" "$@" -} - -# Function to run complete pipeline +# Function to run complete pipeline using the underlying scripts +# Usage: run_all_pipeline --mode [--preview] run_all_pipeline() { local start_time start_time=$(date +%s) - - log_info "Starting complete CI/CD pipeline..." + + local mode="" + local preview=false + local env_override="" + local version="" + local do_clean=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + if [[ -z "${2:-}" ]]; then + log_error "--mode requires an argument (dev or prod)" + return 1 + fi + mode="$2" + if [[ "$mode" != "dev" && "$mode" != "prod" ]]; then + log_error "Invalid mode: $mode. Must be 'dev' or 'prod'" + return 1 + fi + shift 2 + ;; + --clean) + do_clean=true + shift + ;; + --preview) + preview=true + shift + ;; + -e|--env) + if [[ -z "${2:-}" ]]; then + log_error "-e|--env requires an environment name" + return 1 + fi + env_override="$2" + shift 2 + ;; + -v|--version) + if [[ -z "${2:-}" ]]; then + log_error "-v|--version requires a version string" + return 1 + fi + version="$2" + shift 2 + ;; + --help|-h) + echo "Usage: $0 all --mode [--preview]" + echo "" + echo "Options:" + echo " --mode Build mode (required)" + echo " --preview Optional Show site in local at http://localhost:8443" + echo " -e, --env Optional Build only the specified environment" + return 0 + ;; + *) + log_error "Unknown option: $1" + return 1 + ;; + esac + done + + # Validate mode is provided + if [[ -z "$mode" ]]; then + log_error "--mode is required. Usage: $0 all --mode [--preview]" + return 1 + fi + + log_info "Starting complete CI/CD pipeline (mode: $mode, preview: $preview)..." echo "" - + # Step 1: Quality Assurance log_info "═══ Step 1/3: Quality Assurance ═══" - run_qa_pipeline --check || { + log_info "RUN: ---> ${SCRIPT_DIR}/ci_qa.sh --check" + if ! "${SCRIPT_DIR}/ci_qa.sh" --check; then log_error "QA checks failed. Pipeline aborted." return 1 - } + fi echo "" - + # Step 2: Build Firmware log_info "═══ Step 2/3: Build Firmware ═══" - run_build_pipeline "$@" || { - log_error "Build failed. Pipeline aborted." - return 1 - } - echo "" - # Step 3: Build Site (only if not in --no-site mode) - if [[ ! " $* " =~ " --no-site " ]]; then - log_info "═══ Step 3/3: Build Documentation ═══" - run_site_pipeline --mode prod || { - log_warn "Site build failed, but continuing..." - } + # Get list of environments + local -a environments + if [[ -n "$env_override" ]]; then + log_info "Using single environment override: $env_override" + environments=("$env_override") else - log_info "Skipping site build (--no-site flag)" + mapfile -t environments < <(get_environments) fi + if [[ ${#environments[@]} -eq 0 ]]; then + log_error "No environments found to build" + return 1 + fi + + log_info "Found ${#environments[@]} environments to build" + echo "" + + local build_count=0 + local failed_builds=() + local build_args=() + + if [[ -n "$version" ]]; then + build_args+=("--version" "$version") + log_info "Using version override: $version" + fi + build_args+=("--mode" "$mode") + build_args+=("--deploy-ready") + if [[ "$do_clean" == true ]]; then + build_args+=("--clean") + fi + + for env in "${environments[@]}"; do + ((++build_count)) + log_info "[$build_count/${#environments[@]}] Building: $env" + + set +e + log_info "RUN: ---> ${SCRIPT_DIR}/ci_build.sh" "$env" "${build_args[@]}" + "${SCRIPT_DIR}/ci_build.sh" "$env" "${build_args[@]}" + local rc=$? + set -e + if [[ $rc -ne 0 ]]; then + log_error "Build failed for environment: $env" + failed_builds+=("$env") + fi + done + + echo "" + if [[ ${#failed_builds[@]} -gt 0 ]]; then + log_error "Build failed for ${#failed_builds[@]} environment(s):" + printf ' - %s\n' "${failed_builds[@]}" + return 1 + fi + + log_success "All environments built successfully (${#environments[@]} total)" + echo "" + + # Step 3: Build Site + log_info "═══ Step 3/3: Build Documentation ═══" + local site_args=("--mode" "$mode") + if [[ "$preview" == true ]]; then + site_args+=("--preview") + fi + if [[ -n "$version" ]]; then + site_args+=("--version" "$version") + fi + if [[ "$do_clean" == true ]]; then + site_args+=("--clean") + fi + + log_info "RUN: --->${SCRIPT_DIR}/ci_site.sh" "${site_args[@]}" + if ! "${SCRIPT_DIR}/ci_site.sh" "${site_args[@]}"; then + log_warn "Site build failed, but continuing..." + fi + echo "" + local end_time end_time=$(date +%s) local duration=$((end_time - start_time)) - + echo "" echo "╔════════════════════════════════════════╗" echo "║ Complete Pipeline Summary ║" echo "╚════════════════════════════════════════╝" + echo " Mode: $mode" + echo " Preview: $preview" echo " Total Duration: ${duration}s" echo " Status: SUCCESS ✓" echo "╚════════════════════════════════════════╝" @@ -151,14 +293,26 @@ main() { # Route to appropriate pipeline case "$command" in build) - run_build_pipeline "$@" + log_info "Executing build pipeline..." + "${SCRIPT_DIR}/ci_build.sh" "$@" ;; site|docs) - run_site_pipeline "$@" + log_info "Executing site pipeline..." + "${SCRIPT_DIR}/ci_site.sh" "$@" ;; qa|lint) - run_qa_pipeline "$@" + log_info "Executing QA pipeline..." + "${SCRIPT_DIR}/ci_qa.sh" "$@" ;; + security) + log_info "Executing security scan..." + "${SCRIPT_DIR}/ci_security.sh" "$@" + ;; + list-env) + log_info "Executing list-env pipeline..." + "${SCRIPT_DIR}/ci_list-env.sh" "$@" + ;; + all|pipeline) run_all_pipeline "$@" ;; diff --git a/scripts/ci_00_config.sh b/scripts/ci_00_config.sh index bf9b8d8a..a29e1cc0 100755 --- a/scripts/ci_00_config.sh +++ b/scripts/ci_00_config.sh @@ -1,39 +1,16 @@ +#!/bin/bash +# shellcheck disable=SC2034 # Build Scripts Configuration # Used by: All build scripts for centralized configuration -# Python Configuration -PYTHON_VERSION="3.13" -PLATFORMIO_VERSION="https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip" - # Centralized Output Directory Structure # All CI/CD generated files go under generated/ -GENERATED_BASE_DIR="generated" -ARTIFACTS_DIR="${GENERATED_BASE_DIR}/artifacts" -SITE_DIR="${GENERATED_BASE_DIR}/site" -REPORTS_DIR="${GENERATED_BASE_DIR}/reports" +ARTIFACTS_DIR="generated/artifacts" +SITE_DIR="generated/site" +REPORTS_DIR="generated/reports" # PlatformIO Directory Configuration BUILD_DIR=".pio/build" -SCRIPTS_DIR="scripts" - -# Build Configuration -DEFAULT_ENVIRONMENT="esp32dev-all-test" -ENABLE_VERBOSE_BUILD="false" -ENABLE_BUILD_CACHE="true" - -# Artifact Configuration -ARTIFACT_RETENTION_DAYS="7" -CREATE_MANIFEST="true" -COMPRESS_ARTIFACTS="false" - -# Version Configuration -VERSION_FILE_PROD="scripts/latest_version.json" -VERSION_FILE_DEV="scripts/latest_version_dev.json" -USER_CONFIG_FILE="main/User_config.h" - -# Logging Configuration -ENABLE_COLOR_OUTPUT="true" -LOG_LEVEL="INFO" # DEBUG, INFO, WARN, ERROR # ============================================================================ # Colors - ANSI color codes for terminal output @@ -50,9 +27,5 @@ readonly NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*" >&2; } log_step() { echo -e "${BLUE}[STEP]${NC} $*" >&2; } - -# Advanced Options -ENABLE_CCACHE="false" -CCACHE_DIR=".ccache" -MAX_BUILD_JOBS="4" diff --git a/scripts/ci_build.sh b/scripts/ci_build.sh index 67bb33ba..9a0e970b 100755 --- a/scripts/ci_build.sh +++ b/scripts/ci_build.sh @@ -6,8 +6,10 @@ set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT # Load shared configuration (colors, logging functions, paths) if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -71,7 +73,6 @@ verify_build_tools() { log_info "Verifying required build tools..." local missing_tools=() - local version_mismatch=() # Check Python if ! command_exists python3; then @@ -130,39 +131,6 @@ cleanup_on_error() { find . -name "*.bak" -type f -exec bash -c 'mv "$1" "${1%.bak}"' _ {} \; 2>/dev/null || true } -# Function to list available environments -list_environments() { - log_info "Available PlatformIO environments:" - echo "" - - local env_files=("${PROJECT_ROOT}/platformio.ini" "${PROJECT_ROOT}/environments.ini") - local envs=() - - for file in "${env_files[@]}"; do - if [[ -f "$file" ]]; then - while IFS= read -r line; do - if [[ "$line" =~ ^\[env:([^\]]+)\] ]]; then - local env_name="${BASH_REMATCH[1]}" - # Skip test environments - if [[ ! "$env_name" =~ -test$ && ! "$env_name" =~ -all- ]]; then - envs+=("$env_name") - fi - fi - done < "$file" - fi - done - - # Sort and display unique environments - if [[ ${#envs[@]} -gt 0 ]]; then - printf '%s\n' "${envs[@]}" | sort -u | column -c 80 - else - log_warn "No environments found in configuration files" - fi - - echo "" - log_info "Total: $(printf '%s\n' "${envs[@]}" | sort -u | wc -l) environments" -} - # Show usage usage() { cat << EOF @@ -174,15 +142,14 @@ Arguments: environment PlatformIO environment name Options: - --version [TAG] Set version tag (if TAG omitted, auto-generated) --mode MODE Build mode: 'prod' or 'dev' [default: prod] - 'dev' enables OTA and development version + 'dev' enables OTA and development features --deploy-ready Prepare for deployment (renamed artifacts) + --version [TAG] Set version tag (if TAG omitted, auto-generated) --output DIR Output directory for artifacts [default: generated/artifacts/] --skip-verification Skip build tools verification --clean Clean build before starting --verbose Enable verbose output - --list-envs List all available PlatformIO environments --help Show this help message Environment Variables: @@ -191,9 +158,6 @@ Environment Variables: GIT_COMMIT Git commit hash for versioning Examples: - # List available environments - $0 --list-envs - # Local development build $0 esp32dev-all-test --mode dev @@ -212,7 +176,7 @@ main() { local version="" local set_version=false local mode="" - local deploy=false + local prepare_for_deploy=false local output_dir="" local skip_verification=false local clean=false @@ -246,7 +210,7 @@ main() { shift 2 ;; --deploy-ready) - deploy=true + prepare_for_deploy=true shift ;; --output) @@ -270,10 +234,6 @@ main() { verbose=true shift ;; - --list-envs) - list_environments - exit 0 - ;; --help|-h) usage exit 0 @@ -290,12 +250,6 @@ main() { esac done - # Set default mode if not specified - if [[ -z "$mode" ]]; then - mode="prod" - log_info "Mode not specified, defaulting to production" - fi - # Validate environment if [[ -z "$environment" ]]; then log_error "Environment name is required" @@ -303,6 +257,13 @@ main() { exit 1 fi + # Set default mode if not specified + if [[ -z "$mode" ]]; then + mode="prod" + log_info "Mode not specified, defaulting to production" + fi + + # Auto-generate version if --version flag is set but no tag provided if [[ "$set_version" == "true" && -z "$version" ]]; then if [[ "${CI:-false}" == "true" ]]; then @@ -338,39 +299,53 @@ main() { echo "" fi - # Step 2: Set version (only if --version flag was provided) - if [[ "$set_version" == "true" ]]; then - log_step "2/4 Setting version: $version" - if [[ "$mode" == "dev" ]]; then - "${SCRIPT_DIR}/ci_set_version.sh" "$version" --dev || exit 1 - else - "${SCRIPT_DIR}/ci_set_version.sh" "$version" --prod || exit 1 - fi - echo "" - else - log_info "Skipping version setting (use --version to set version)" - echo "" - fi - + # Step 2: Set version + # not required + # Step 3: Build firmware log_step "3/4 Building firmware for: $environment" local build_opts=() [[ "$mode" == "dev" ]] && build_opts+=(--dev-ota) [[ "$clean" == "true" ]] && build_opts+=(--clean) [[ "$verbose" == "true" ]] && build_opts+=(--verbose) + [[ "$set_version" == "true" ]] && build_opts+=(--version "$version") "${SCRIPT_DIR}/ci_build_firmware.sh" "$environment" "${build_opts[@]}" || exit 1 echo "" # Step 4: Prepare artifacts log_step "4/4 Preparing artifacts..." - local artifact_opts=() - [[ "$deploy" == "true" ]] && artifact_opts+=(--deploy) || artifact_opts+=(--standard) - artifact_opts+=(--manifest) - [[ -n "$output_dir" ]] && artifact_opts+=(--output "$output_dir") - - "${SCRIPT_DIR}/ci_prepare_artifacts.sh" "$environment" "${artifact_opts[@]}" || exit 1 - echo "" + + if [[ "$prepare_for_deploy" == "true" ]]; then + log_info "Preparing artifacts for deployment" + local artifact_opts=() + [[ "$clean" == "true" ]] && artifact_opts+=(--clean) + [[ -n "$output_dir" ]] && artifact_opts+=(--output "$output_dir") + [[ "$set_version" == "true" ]] && artifact_opts+=(--version "$version") + "${SCRIPT_DIR}/ci_prepare_artifacts.sh" "$environment" "${artifact_opts[@]}" || exit 1 + echo "" + # Check if site folder exists and copy built files to avoid rebuilding the site + if [[ "$mode" == "dev" ]]; then + local site_dir="${PROJECT_ROOT}/generated/site/dev" + local artifacts_dir="${output_dir:-${PROJECT_ROOT}/generated/artifacts/firmware_build}" + + if [[ -d "$site_dir" ]]; then + log_info "Site folder exists, copying built firmware files to site/dev..." + + # Copy firmware files for the current environment + for file in "${artifacts_dir}/${environment}"-*.bin "${artifacts_dir}/${environment}"-*.tgz; do + if [[ -f "$file" ]]; then + cp -v "$file" "$site_dir/" || log_warn "Failed to copy $(basename "$file")" + fi + done + + log_info "✓ Firmware files copied to site/dev (no site rebuild needed)" + else + log_warn "Site folder not found at: $site_dir" + log_info "Run 'ci.sh site ' to generate it" + fi + fi + fi # Print summary print_summary "$environment" "$version" "$start_time" diff --git a/scripts/ci_build_firmware.sh b/scripts/ci_build_firmware.sh index 5731c251..7e4d43f6 100755 --- a/scripts/ci_build_firmware.sh +++ b/scripts/ci_build_firmware.sh @@ -6,8 +6,10 @@ set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT # Load shared configuration (colors, logging functions, paths) if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -41,18 +43,7 @@ validate_environment() { log_info "Building environment: $env" } -# Function to setup build environment variables -setup_build_env() { - local enable_dev_ota="${1:-false}" - - export PYTHONIOENCODING=utf-8 - export PYTHONUTF8=1 - - if [[ "$enable_dev_ota" == "true" ]]; then - export PLATFORMIO_BUILD_FLAGS='"-DDEVELOPMENTOTA=true"' - log_info "Development OTA enabled" - fi -} + # Function to check PlatformIO availability check_platformio() { @@ -78,6 +69,18 @@ run_build() { local env="$1" local clean="${2:-false}" local verbose="${3:-false}" + local enable_dev_ota="${4:-false}" + local version="${5:-edge}" + + if [[ "$enable_dev_ota" == "true" ]]; then + log_info "Development OTA enabled" + export PLATFORMIO_BUILD_FLAGS='"-DDEVELOPMENTOTA=true"' + fi + + if [[ -n "$version" ]]; then + log_info "Setting firmware version to: $version" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOMG_VERSION=\\\"${version}\\\"" + fi log_build "Starting build for environment: $env" @@ -95,6 +98,9 @@ run_build() { local start_time start_time=$(date +%s) + log_info "PlatformIO Build Flags: $PLATFORMIO_BUILD_FLAGS" + log_info "Executing: $build_cmd" + if eval "$build_cmd"; then local end_time end_time=$(date +%s) @@ -184,6 +190,7 @@ Options: --clean Clean build artifacts before building --verbose Enable verbose build output --no-verify Skip artifact verification + --version Set firmware version (default: edge) --help Show this help message Examples: @@ -201,10 +208,15 @@ main() { local clean_build_flag=false local verbose=false local verify=true + local version="edge" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in + --version) + version="$2" + shift 2 + ;; --dev-ota) enable_dev_ota=true shift @@ -254,7 +266,8 @@ main() { validate_environment "$environment" || exit 1 # Setup build environment - setup_build_env "$enable_dev_ota" + export PYTHONIOENCODING=utf-8 + export PYTHONUTF8=1 # Clean if requested if [[ "$clean_build_flag" == "true" ]]; then @@ -262,7 +275,7 @@ main() { fi # Run build - run_build "$environment" "$clean_build_flag" "$verbose" || exit 1 + run_build "$environment" "$clean_build_flag" "$verbose" "$enable_dev_ota" "$version" || exit 1 # Verify artifacts if [[ "$verify" == "true" ]]; then diff --git a/scripts/ci_list-env.sh b/scripts/ci_list-env.sh new file mode 100755 index 00000000..052f74d6 --- /dev/null +++ b/scripts/ci_list-env.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Lists all available PlatformIO environments for OpenMQTTGateway +# Usage: ./scripts/ci_list.sh [--full] + +set -euo pipefail + +# Constants +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT + +# Load shared configuration (colors, logging functions, paths) +if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then + source "${SCRIPT_DIR}/ci_00_config.sh" +else + echo "ERROR: ci_00_config.sh not found" >&2 + exit 1 +fi + + +## use .github/workflows/environments.json to list environments +list_environments_from_Json() { + local json_file="${PROJECT_ROOT}/.github/workflows/environments.json" + if [[ ! -f "$json_file" ]]; then + log_error "JSON file not found: $json_file" + return 1 + fi + + if ! command -v jq >/dev/null 2>&1; then + log_error "jq is required to read $json_file" + return 1 + fi + + log_info "Available PlatformIO environments from JSON:" + echo "" + local envs=() + while IFS= read -r env; do + # Skip test environments + if [[ ! "$env" =~ -test$ && ! "$env" =~ -all- ]]; then + envs+=("$env") + fi + done < <(jq -r '.environments.all[]? // empty' "$json_file") + + # Sort and display unique environments + if [[ ${#envs[@]} -gt 0 ]]; then + printf '%s\n' "${envs[@]}" | sort -u | column -c 80 + else + log_warn "No environments found in JSON file" + fi + echo "" + log_info "Total: $(printf '%s\n' "${envs[@]}" | sort -u | wc -l) environments" + +} + + +list_environments() { + log_info "Available PlatformIO environments:" + echo "" + local env_files=("${PROJECT_ROOT}/platformio.ini" "${PROJECT_ROOT}/environments.ini") + local envs=() + for file in "${env_files[@]}"; do + if [[ -f "$file" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ ^\[env:([^\]]+)\] ]]; then + local env_name="${BASH_REMATCH[1]}" + envs+=("$env_name") + fi + done < "$file" + fi + done + # Sort and display unique environments + if [[ ${#envs[@]} -gt 0 ]]; then + printf '%s\n' "${envs[@]}" | sort -u | column -c 80 + else + log_warn "No environments found in configuration files" + fi + echo "" + log_info "Total: $(printf '%s\n' "${envs[@]}" | sort -u | wc -l) environments" +} + +# Main execution +usage() { + cat < [OPTIONS] @@ -6,8 +7,10 @@ set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT # Load shared configuration (colors, logging functions, paths) if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -22,17 +25,22 @@ BUILD_DIR="${PROJECT_ROOT}/${BUILD_DIR}" DEFAULT_OUTPUT_DIR="${PROJECT_ROOT}/${ARTIFACTS_DIR}" # Function to create output directory -create_output_dir() { +prepare_output_dir() { local output_dir="$1" + local clean_flag="${2:-false}" if [[ -d "$output_dir" ]]; then - log_warn "Output directory already exists: $output_dir" - log_info "Cleaning existing artifacts..." - rm -rf "$output_dir" + if [[ "$clean_flag" == "true" ]]; then + log_warn "Cleaning and recreating output directory: $output_dir" + rm -rf "$output_dir" + mkdir -p "$output_dir" + else + log_warn "Output directory already exists and will be reused: $output_dir" + fi + else + mkdir -p "$output_dir" + log_info "Created output directory: $output_dir" fi - - mkdir -p "$output_dir" - log_info "Created output directory: $output_dir" } # Function to copy artifact with optional renaming @@ -58,42 +66,14 @@ copy_artifact() { fi } -# Function to prepare standard artifacts (no renaming) -prepare_standard_artifacts() { - local env="$1" - local output_dir="$2" - local env_dir="${BUILD_DIR}/${env}" - - log_info "Preparing STANDARD artifacts for: $env" - - local copied=0 - - # Copy firmware.bin (required) - if copy_artifact "${env_dir}/firmware.bin" "${output_dir}/firmware.bin" "firmware"; then - ((copied++)) - fi - - # Copy partitions.bin (optional) - copy_artifact "${env_dir}/partitions.bin" "${output_dir}/partitions.bin" "partitions" && ((copied++)) || true - - # Note: bootloader.bin is NOT copied in standard mode (only needed for deployment) - - if [[ $copied -eq 0 ]]; then - log_error "No artifacts were copied" - return 1 - fi - - log_info "Copied ${copied} artifact(s) in standard mode" -} # Function to prepare deployment artifacts (with renaming) -prepare_deployment_artifacts() { +prepare_artifacts() { local env="$1" local output_dir="$2" local env_dir="${BUILD_DIR}/${env}" - log_info "Preparing DEPLOYMENT artifacts for: $env" - + log_info "Preparing firmware directory for: $env" local copied=0 # Copy and rename firmware.bin @@ -118,54 +98,57 @@ prepare_deployment_artifacts() { log_info "Copied ${copied} artifact(s) in deployment mode" } -# Function to create manifest file -create_manifest() { +prepare_libraries() { local env="$1" local output_dir="$2" - local manifest="${output_dir}/manifest.txt" + local env_dir="${BUILD_DIR}/${env}" + + # Process libraries: create temp copy with renamed folders, zip, preserve originals + log_info "Processing libraries for environment: $env" + TEMP_LIBDEPS=$(mktemp -p "$output_dir" -d) || { echo "Failed to create temp directory"; return 1; } + + cp -r .pio/libdeps/"$env" "$TEMP_LIBDEPS/" || { log_error "Failed to copy libdeps for $env"; return 1; } + + ( + cd "$TEMP_LIBDEPS" + log_step "Replace space by _ in folder names (temp copy only)" + find . -type d -name "* *" | while read -r FNAME; do + mv "$FNAME" "${FNAME// /_}" + done - log_info "Creating artifact manifest..." + log_step "Zipping libraries per board" + for i in */; do + tar -czf "${i%/}-libraries.tgz" "$i" > /dev/null + done - { - echo "Environment: $env" - echo "Build Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" - echo "Build Host: $(hostname)" - echo "" - echo "Artifacts:" - - find "$output_dir" -type f -name "*.bin" | sort | while read -r file; do - local size - size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null) - local size_kb=$((size / 1024)) - local md5sum_val - md5sum_val=$(md5sum "$file" 2>/dev/null | cut -d' ' -f1 || md5 -q "$file" 2>/dev/null) - echo " - $(basename "$file"): ${size_kb} KB (MD5: ${md5sum_val})" - done - } > "$manifest" - - log_info "Manifest created: $manifest" + mv ./*.tgz "${output_dir}" + ) + + rm -rf "$TEMP_LIBDEPS" + log_info "✓ Created library archives in: $output_dir" } -# Function to compress artifacts (optional) -compress_artifacts() { +prepare_sources() { local output_dir="$1" - local archive_name="$2" - log_info "Compressing artifacts..." + log_info "Preparing source code archive" - local archive="${output_dir}/${archive_name}.tar.gz" - - if tar -czf "$archive" -C "$output_dir" .; then - local size - size=$(stat -f%z "$archive" 2>/dev/null || stat -c%s "$archive" 2>/dev/null) - local size_kb=$((size / 1024)) - log_info "Archive created: ${archive_name}.tar.gz (${size_kb} KB)" + # Create and move sources tar.gz (newly generated, safe to move) + if tar -czf "${output_dir}/OpenMQTTGateway_sources.tgz" main LICENSE.txt > /dev/null; then + log_info "✓ Created source archive: OpenMQTTGateway_sources.tgz" else - log_error "Failed to create archive" + log_error "Failed to create source archive" return 1 fi } + + + + + + + # Function to list artifacts list_artifacts() { local output_dir="$1" @@ -194,17 +177,14 @@ usage() { cat << EOF Usage: $0 [OPTIONS] -Prepare firmware artifacts for upload or deployment. +Prepare artifacts for upload or deployment. Arguments: - environment PlatformIO environment name + environment PlatformIO environment name, if omitted will be created source archive only. Options: - --deploy Prepare for deployment (rename with environment prefix) - --standard Prepare standard artifacts (no renaming) [default] + --clean Clean existing output directory before preparing artifacts --output DIR Output directory [default: generated/artifacts/] - --manifest Create manifest file with artifact metadata - --compress Compress artifacts into tar.gz archive --help Show this help message Examples: @@ -218,32 +198,27 @@ EOF # Main execution main() { local environment="" - local mode="standard" local output_dir="$DEFAULT_OUTPUT_DIR" - local create_manifest_flag=false - local compress_flag=false + local clean_flag=false + #local version="" ## WILL BE USED WHEN THE VERSION ITSELF AFFECTS THE ARTIFACTS NAMING # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in - --deploy) - mode="deploy" - shift - ;; - --standard) - mode="standard" - shift - ;; --output) output_dir="$2" shift 2 ;; - --manifest) - create_manifest_flag=true - shift + -v|--version) + if [[ -z "${2:-}" ]]; then + log_error "-v|--version requires a version string" + return 1 + fi + #version="$2" + shift 2 ;; - --compress) - compress_flag=true + --clean) + clean_flag=true shift ;; --help|-h) @@ -262,48 +237,47 @@ main() { esac done - # Validate inputs - if [[ -z "$environment" ]]; then - log_error "Environment name is required" - usage - exit 1 - fi - # Change to project root cd "$PROJECT_ROOT" - - # Check if build directory exists - if [[ ! -d "${BUILD_DIR}/${environment}" ]]; then - log_error "Build directory not found for environment: $environment" - log_error "Run build_firmware.sh first" - exit 1 - fi - - # Create output directory - create_output_dir "$output_dir" - - # Prepare artifacts based on mode - case "$mode" in - standard) - prepare_standard_artifacts "$environment" "$output_dir" || exit 1 - ;; - deploy) - prepare_deployment_artifacts "$environment" "$output_dir" || exit 1 - ;; - *) - log_error "Unknown mode: $mode" + + # TODO FOR NEXT STEP MULTI RELEASE: TAG, RC, edge + #if [[ -n "$version" ]]; then + # # Sanitize version string for directory name + # safe_version=$(echo "$version" | sed 's/[^a-zA-Z0-9._-]/_/g') + # output_dir="${output_dir}/${safe_version}" + #fi + + + # Validate inputs + if [[ -z "$environment" ]]; then + log_info "No environment specified, only preparing source archive" + + # Create output directory + prepare_output_dir "$output_dir" "$clean_flag" + + # Prepare source code archive + prepare_sources "$output_dir" || exit 1 + else + # Check if build directory exists + if [[ ! -d "${BUILD_DIR}/${environment}" ]]; then + log_error "Build directory not found for environment: $environment" + log_error "Run build_firmware.sh first" exit 1 - ;; - esac - - # Create manifest if requested - if [[ "$create_manifest_flag" == "true" ]]; then - create_manifest "$environment" "$output_dir" - fi - - # Compress if requested - if [[ "$compress_flag" == "true" ]]; then - compress_artifacts "$output_dir" "$environment" + fi + + #normalize output directory path + #output_dir="${output_dir}/firmware-${environment}" + output_dir="${output_dir}/firmware_build" + + + # Create output directory + prepare_output_dir "$output_dir" "$clean_flag" + + # Prepare artifacts based on mode + prepare_artifacts "$environment" "$output_dir" || exit 1 + + # Prepare libraries + prepare_libraries "$environment" "$output_dir" || exit 1 fi # Show summary diff --git a/scripts/ci_qa.sh b/scripts/ci_qa.sh index ba4122c3..267e3df9 100755 --- a/scripts/ci_qa.sh +++ b/scripts/ci_qa.sh @@ -6,8 +6,10 @@ set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT # Load shared configuration if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -18,7 +20,6 @@ else fi # Default values -CHECK_MODE=true FIX_MODE=false FORMAT_ONLY=false SOURCE_DIR="main" @@ -202,6 +203,63 @@ fix_formatting() { fi } +# Function to run shellcheck on scripts +run_shellcheck() { + log_info "Checking shell scripts with ShellCheck..." + + # Check if shellcheck is installed + if ! command -v shellcheck >/dev/null 2>&1; then + log_warn "ShellCheck not found, skipping shell script checks" + log_info "To install ShellCheck:" + log_info " Ubuntu/Debian: sudo apt-get install shellcheck" + log_info " macOS: brew install shellcheck" + return 0 # Don't fail, just skip + fi + + log_info "✓ ShellCheck found: $(shellcheck --version | head -n2 | tail -n1)" + + # Find all .sh files in scripts directory + local scripts_dir="${PROJECT_ROOT}/scripts" + if [[ ! -d "$scripts_dir" ]]; then + log_warn "Scripts directory not found: $scripts_dir" + return 0 + fi + + local shell_files + shell_files=$(find "$scripts_dir" -type f -name "*.sh" 2>/dev/null) + + if [[ -z "$shell_files" ]]; then + log_warn "No shell scripts found in $scripts_dir" + return 0 + fi + + local file_count + file_count=$(echo "$shell_files" | wc -l) + log_info "Found $file_count shell script(s) to check" + + # Run shellcheck on all files at once to allow cross-file analysis + log_info "Running ShellCheck on scripts directory..." + + local shellcheck_output + # shellcheck disable=SC2086 + shellcheck_output=$(shellcheck -f gcc $shell_files 2>&1) + local shellcheck_result=$? + + if [[ $shellcheck_result -ne 0 ]]; then + echo "" + log_warn "⚠ ShellCheck found issues:" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "$shellcheck_output" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + log_error "Please fix the ShellCheck warnings/errors" + return 1 + fi + + log_success "✓ All $file_count shell script(s) passed ShellCheck" + return 0 +} + # Function to run all QA checks run_all_checks() { log_info "Running all quality assurance checks..." @@ -215,6 +273,13 @@ run_all_checks() { fi echo "" + # ShellCheck + log_info "═══ Shell Scripts Check ═══" + if ! run_shellcheck; then + all_passed=false + fi + echo "" + # Future: Add more checks here # - cppcheck static analysis # - code complexity metrics @@ -237,6 +302,7 @@ run_format_check() { local clang_format_cmd clang_format_cmd=$(check_clang_format "$CLANG_FORMAT_VERSION") + # shellcheck disable=SC2181 if [[ $? -ne 0 ]] || [[ -z "$clang_format_cmd" ]]; then log_error "clang-format not found" log_error "Please install clang-format:" @@ -258,6 +324,7 @@ run_format_check() { local files files=$(find_files "$SOURCE_DIR" "$EXTENSIONS") + # shellcheck disable=SC2181 if [[ $? -ne 0 ]] || [[ -z "$files" ]]; then log_error "Source directory not found: ${PROJECT_ROOT}/${SOURCE_DIR}" return 1 @@ -317,13 +384,11 @@ parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --check) - CHECK_MODE=true FIX_MODE=false shift ;; --fix) FIX_MODE=true - CHECK_MODE=false shift ;; --format) diff --git a/scripts/ci_security.sh b/scripts/ci_security.sh new file mode 100755 index 00000000..77e72513 --- /dev/null +++ b/scripts/ci_security.sh @@ -0,0 +1,428 @@ +#!/bin/bash +# OpenMQTTGateway Security Scan Script +# Runs Trivy vulnerability scanner and generates reports +# Based on task-security-scan.yml workflow +# Usage: ./scripts/ci_security.sh [OPTIONS] + +set -euo pipefail + +# Constants +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly SCRIPT_DIR +readonly PROJECT_ROOT + +# Load shared configuration (colors, logging functions, paths) +if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then + source "${SCRIPT_DIR}/ci_00_config.sh" +else + echo "ERROR: ci_00_config.sh not found" >&2 + exit 1 +fi + +# Default values +SCAN_TYPE="fs" +SEVERITY="HIGH,CRITICAL" +EXIT_CODE_ON_VULN="0" +SCAN_PATH="." +EXCLUDE_PATHS="" +GENERATE_SBOM=false # Off by default + +OUTPUT_DIR="${PROJECT_ROOT}/${REPORTS_DIR}" + +# Show usage +usage() { + cat << EOF +Usage: $0 [OPTIONS] + +Security vulnerability scanning using Trivy + +Options: + --scan-type Type of scan: fs (filesystem), config, or image + Default: fs + + --severity Severity levels to report (comma-separated) + Options: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL + Default: HIGH,CRITICAL + + --scan-path Path to scan (default: entire repository) + Default: . + + --exit-code Exit code when vulnerabilities found + 0 = continue (don't fail) + 1 = fail build + Default: 0 + + --exclude Paths to exclude from scan (comma-separated) + Example: node_modules,test,docs + Default: (none) + + --generate-sbom Generate Software Bill of Materials (SBOM) + Creates CycloneDX and SPDX JSON formats + Default: off + + --output-dir Directory to save reports + Default: ./generated/reports + + -h, --help Show this help message + +Examples: + # Scan filesystem for HIGH and CRITICAL issues + $0 + + # Scan config files and fail if vulnerabilities found + $0 --scan-type config --exit-code 1 + + # Scan specific path for all severity levels + $0 --scan-path ./main --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL + + # Scan with excluded paths + $0 --exclude node_modules,test,docs + + # Scan with multiple options + $0 --scan-path ./main --exclude test --severity HIGH,CRITICAL --exit-code 1 + + # Scan with SBOM generation + $0 --generate-sbom + + # Complete scan with SBOM and custom severity + $0 --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL --generate-sbom + +EOF + exit 0 +} + +# Function to check if Trivy is installed +check_trivy() { + if ! command -v trivy &> /dev/null; then + log_error "Trivy is not installed. Install it from: https://github.com/aquasecurity/trivy" + return 1 + fi + log_success "Trivy found: $(trivy --version)" +} + +# Function to check if jq is installed (for JSON parsing) +check_jq() { + if ! command -v jq &> /dev/null; then + log_warn "jq is not installed. Some report features will be limited." + return 1 + fi + return 0 +} + +# Function to run Trivy scan with specified format +# Usage: run_trivy_scan [exit_code] +run_trivy_scan() { + local format="$1" + local filename="$2" + local exit_code="${3:-0}" # Default to 0 for non-critical scans + local output_file="${OUTPUT_DIR}/${filename}" + + log_info "Running Trivy $SCAN_TYPE scan ($format format)..." + + mkdir -p "$OUTPUT_DIR" + + # Build Trivy command with all enabled scanners + local trivy_cmd=( + trivy "$SCAN_TYPE" "$SCAN_PATH" + --format "$format" + --output "$output_file" + --severity "$SEVERITY" + --exit-code "$exit_code" + ) + + # Add exclude paths if provided + if [[ -n "$EXCLUDE_PATHS" ]]; then + IFS=',' read -r -a exclude_dirs <<< "$EXCLUDE_PATHS" + for skip_dir in "${exclude_dirs[@]}"; do + if [[ -n "$skip_dir" ]]; then + trivy_cmd+=(--skip-dirs "$skip_dir") + fi + done + fi + + log_info "Executing Trivy ${SCAN_TYPE} scan" + + if "${trivy_cmd[@]}" 2>&1 | tee -a "${OUTPUT_DIR}/trivy-scan.log"; then + log_success "${format^^} report generated: $output_file" + return 0 + else + local rc=$? + if [[ $rc -eq 1 && "$exit_code" == "1" ]]; then + log_error "Vulnerabilities found (exit code: $rc)" + return $rc + fi + log_success "Scan completed with exit code: $rc" + return 0 + fi +} + +# Function to create summary report +create_summary_report() { + log_info "Creating security summary report..." + + local summary_file="${OUTPUT_DIR}/security-summary.md" + + { + echo "# 🔒 Security Scan Results" + echo "" + echo "**Scan Type**: $SCAN_TYPE" + echo "**Path Scanned**: $SCAN_PATH" + echo "**Severity Filter**: $SEVERITY" + echo "**Scan Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "" + } > "$summary_file" + + # Parse SARIF if it exists + local sarif_file="${OUTPUT_DIR}/trivy-results.sarif" + if [[ -f "$sarif_file" ]]; then + log_info "Parsing SARIF results..." + + if check_jq; then + # Count vulnerabilities by severity + local vuln_count + vuln_count=$(jq '[.runs[].results[]] | length' "$sarif_file" 2>/dev/null || echo "0") + + local critical high medium low + critical=$(jq '[.runs[].results[] | select(.level == "error")] | length' "$sarif_file" 2>/dev/null || echo "0") + high=$(jq '[.runs[].results[] | select(.level == "warning")] | length' "$sarif_file" 2>/dev/null || echo "0") + medium=$(jq '[.runs[].results[] | select(.level == "note")] | length' "$sarif_file" 2>/dev/null || echo "0") + low=$(jq '[.runs[].results[] | select(.level == "none")] | length' "$sarif_file" 2>/dev/null || echo "0") + + { + echo "## Vulnerability Summary" + echo "" + echo "**Total Vulnerabilities**: ${vuln_count}" + echo "" + echo "- 🔴 **Critical**: ${critical}" + echo "- 🟠 **High**: ${high}" + echo "- 🟡 **Medium**: ${medium}" + echo "- 🟢 **Low**: ${low}" + echo "" + } >> "$summary_file" + + # List vulnerability details + if [[ "$vuln_count" -gt 0 ]]; then + { + echo "## Vulnerability Details" + echo "" + } >> "$summary_file" + + jq -r '.runs[].results[] | + "### \(.level | ascii_upcase): \(.ruleId)\n" + + "**Location**: \(.locations[0].physicalLocation.artifactLocation.uri // "N/A")\n" + + "**Message**: \(.message.text)\n" + + (if .properties.CVE then "**CVE**: \(.properties.CVE)\n" else "" end) + + (if .properties.cvss then "**CVSS Score**: \(.properties.cvss)\n" else "" end) + + ""' "$sarif_file" >> "$summary_file" 2>/dev/null || \ + echo "Could not parse vulnerability details" >> "$summary_file" + fi + else + echo "⚠️ jq not available for detailed SARIF parsing" >> "$summary_file" + fi + else + echo "⚠️ SARIF file not found" >> "$summary_file" + fi + + # Add available report formats + { + echo "## Report Formats Available" + echo "" + if [[ -f "${OUTPUT_DIR}/trivy-results.sarif" ]]; then + echo "- ✅ \`trivy-results.sarif\` - SARIF format (for GitHub/IDE integration)" + fi + if [[ -f "${OUTPUT_DIR}/trivy-report.json" ]]; then + echo "- ✅ \`trivy-report.json\` - JSON format (for automation)" + fi + if [[ -f "${OUTPUT_DIR}/trivy-report.txt" ]]; then + echo "- ✅ \`trivy-report.txt\` - Human-readable table" + fi + echo "" + } >> "$summary_file" + + # Add footer + { + echo "---" + echo "" + echo "Generated by OpenMQTTGateway CI/CD Security Scan" + echo "For more information, see: https://github.com/aquasecurity/trivy" + } >> "$summary_file" + + log_success "Summary report generated: $summary_file" +} + +# Function to check for critical vulnerabilities and fail if needed +check_critical_vulnerabilities() { + if [[ "$EXIT_CODE_ON_VULN" != "1" ]]; then + return 0 + fi + + local sarif_file="${OUTPUT_DIR}/trivy-results.sarif" + + if [[ ! -f "$sarif_file" ]]; then + log_warn "SARIF file not found, skipping critical check" + return 0 + fi + + if check_jq; then + local critical + critical=$(jq '[.runs[].results[] | select(.level == "error")] | length' "$sarif_file" 2>/dev/null || echo "0") + + if [[ "$critical" -gt 0 ]]; then + log_error "❌ Found ${critical} critical vulnerabilities!" + log_error "Review the security reports in: $OUTPUT_DIR" + return 1 + fi + fi + + return 0 +} + +# Main execution +main() { + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --scan-type) + if [[ -z "${2:-}" ]]; then + log_error "--scan-type requires an argument" + return 1 + fi + SCAN_TYPE="$2" + shift 2 + ;; + --severity) + if [[ -z "${2:-}" ]]; then + log_error "--severity requires an argument" + return 1 + fi + SEVERITY="$2" + shift 2 + ;; + --scan-path) + if [[ -z "${2:-}" ]]; then + log_error "--scan-path requires an argument" + return 1 + fi + SCAN_PATH="$2" + shift 2 + ;; + --exit-code) + if [[ -z "${2:-}" ]]; then + log_error "--exit-code requires an argument" + return 1 + fi + EXIT_CODE_ON_VULN="$2" + shift 2 + ;; + --exclude) + if [[ -z "${2:-}" ]]; then + log_error "--exclude requires an argument" + return 1 + fi + EXCLUDE_PATHS="$2" + shift 2 + ;; + --generate-sbom) + GENERATE_SBOM=true + shift + ;; + --output-dir) + if [[ -z "${2:-}" ]]; then + log_error "--output-dir requires an argument" + return 1 + fi + OUTPUT_DIR="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + log_error "Unknown option: $1" + echo "" + usage + ;; + esac + done + + # Pre-flight checks + log_info "Running security scan checks..." + if ! check_trivy; then + log_error "Required tool check failed" + return 1 + fi + check_jq || true + + echo "" + log_info "═══ Security Scan Configuration ═══" + echo " Scan Type: $SCAN_TYPE" + echo " Path: $SCAN_PATH" + echo " Severity: $SEVERITY" + echo " Exit on Vulnerabilities: $EXIT_CODE_ON_VULN" + echo " Excluded Paths: ${EXCLUDE_PATHS:-none}" + echo " Generate SBOM: ${GENERATE_SBOM:-false}" + echo " Output Directory: $OUTPUT_DIR" + echo "" + + # Run scans + if ! run_trivy_scan "sarif" "trivy-results.sarif" "$EXIT_CODE_ON_VULN"; then + if [[ "$EXIT_CODE_ON_VULN" == "1" ]]; then + return 1 + fi + fi + + run_trivy_scan "json" "trivy-report.json" "0" + run_trivy_scan "table" "trivy-report.txt" "0" + + # Create summary + create_summary_report + + # Generate SBOM if requested + if [[ "$GENERATE_SBOM" == "true" ]]; then + log_info "Generating Software Bill of Materials (SBOM)..." + + local sbom_dir="${OUTPUT_DIR}/sbom" + mkdir -p "$sbom_dir" + + # Generate CycloneDX SBOM + log_info "Generating CycloneDX SBOM..." + if trivy "$SCAN_TYPE" "$SCAN_PATH" \ + --format cyclonedx \ + --output "$sbom_dir/sbom-cyclonedx.json" \ + --exit-code 0 2>&1 | grep -i "generated\|error" || true; then + if [[ -f "$sbom_dir/sbom-cyclonedx.json" ]]; then + log_success "CycloneDX SBOM generated: $sbom_dir/sbom-cyclonedx.json" + fi + fi + + # Generate SPDX SBOM + log_info "Generating SPDX SBOM..." + if trivy "$SCAN_TYPE" "$SCAN_PATH" \ + --format spdx-json \ + --output "$sbom_dir/sbom-spdx.json" \ + --exit-code 0 2>&1 | grep -i "generated\|error" || true; then + if [[ -f "$sbom_dir/sbom-spdx.json" ]]; then + log_success "SPDX SBOM generated: $sbom_dir/sbom-spdx.json" + fi + fi + + if [[ -f "$sbom_dir/sbom-cyclonedx.json" ]] || [[ -f "$sbom_dir/sbom-spdx.json" ]]; then + log_success "SBOM generation completed" + echo "" + fi + fi + # Check for critical vulnerabilities + if ! check_critical_vulnerabilities; then + return 1 + fi + + echo "" + log_success "Security scan completed successfully!" + log_info "Reports saved to: $OUTPUT_DIR" + + return 0 +} + +# Execute main function +main "$@" diff --git a/scripts/ci_set_version.sh b/scripts/ci_set_version.sh deleted file mode 100755 index 572bb8dc..00000000 --- a/scripts/ci_set_version.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/bash -# Updates version tags in firmware configuration and JSON files -# Used by: CI/CD pipelines for versioning builds -# Usage: ./set_version.sh [--dev] - -set -euo pipefail - -# Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -readonly USER_CONFIG="${PROJECT_ROOT}/main/User_config.h" -readonly LATEST_VERSION_PROD="${SCRIPT_DIR}/latest_version.json" -readonly LATEST_VERSION_DEV="${SCRIPT_DIR}/latest_version_dev.json" - -# Load shared configuration (colors, logging functions, paths) -if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then - source "${SCRIPT_DIR}/ci_00_config.sh" -else - echo "ERROR: ci_00_config.sh not found" >&2 - exit 1 -fi - -# Function to validate version tag -validate_version() { - local version="$1" - - if [[ -z "$version" ]] || [[ "$version" == "version_tag" ]]; then - log_error "Invalid version tag: '$version'" - return 1 - fi - - log_info "Version tag validated: $version" -} - -# Function to backup files -backup_file() { - local file="$1" - - if [[ -f "$file" ]]; then - cp "$file" "${file}.bak" - log_info "Backed up: $file" - fi -} - -# Function to replace version in file -replace_version() { - local file="$1" - local version="$2" - - if [[ ! -f "$file" ]]; then - log_warn "File not found: $file (skipping)" - return 0 - fi - - # Backup before modification - backup_file "$file" - - # Replace version_tag placeholder - if sed -i "s/version_tag/${version}/g" "$file"; then - log_info "Updated version in: $file" - else - log_error "Failed to update version in: $file" - return 1 - fi -} - -# Function to set production version -set_production_version() { - local version="$1" - - log_info "Setting PRODUCTION version: $version" - - replace_version "$USER_CONFIG" "$version" || return 1 - replace_version "$VERSION_JSON" "$version" || return 1 -} - -# Function to set development version -set_development_version() { - local version="$1" - - log_info "Setting DEVELOPMENT version: $version" - - replace_version "$USER_CONFIG" "$version" || return 1 - replace_version "$VERSION_DEV_JSON" "$version" || return 1 -} - -# Function to restore backups -restore_backups() { - log_warn "Restoring backups..." - - for file in "$USER_CONFIG" "$VERSION_JSON" "$VERSION_DEV_JSON"; do - if [[ -f "${file}.bak" ]]; then - mv "${file}.bak" "$file" - log_info "Restored: $file" - fi - done -} - -# Function to clean backups -clean_backups() { - for file in "$USER_CONFIG" "$VERSION_JSON" "$VERSION_DEV_JSON"; do - if [[ -f "${file}.bak" ]]; then - rm "${file}.bak" - fi - done -} - -# Show usage -usage() { - cat << EOF -Usage: $0 [OPTIONS] - -Update version tags in firmware configuration files. - -Arguments: - version_tag Version string to inject (e.g., v1.2.3, abc123, dev-20230101) - -Options: - --dev Use development version files (latest_version_dev.json) - --prod Use production version files (latest_version.json) [default] - --help Show this help message - -Examples: - $0 v1.2.3 # Production release - $0 abc123 --dev # Development build - $0 \${{ github.sha }} --dev # CI/CD with commit SHA - -EOF -} - -# Main execution -main() { - local version="" - local is_dev=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --dev) - is_dev=true - shift - ;; - --prod) - is_dev=false - shift - ;; - --help|-h) - usage - exit 0 - ;; - -*) - log_error "Unknown option: $1" - usage - exit 1 - ;; - *) - version="$1" - shift - ;; - esac - done - - # Validate version - if [[ -z "$version" ]]; then - log_error "Version tag is required" - usage - exit 1 - fi - - validate_version "$version" || exit 1 - - # Change to project root - cd "$PROJECT_ROOT" - - # Set version with error handling - if [[ "$is_dev" == true ]]; then - set_development_version "$version" || { - restore_backups - exit 1 - } - else - set_production_version "$version" || { - restore_backups - exit 1 - } - fi - - # Clean up backups on success - clean_backups - - log_info "Version update completed successfully" -} - -# Run main if executed directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/scripts/ci_site.sh b/scripts/ci_site.sh index 1b664e00..3d072a7e 100755 --- a/scripts/ci_site.sh +++ b/scripts/ci_site.sh @@ -1,16 +1,19 @@ #!/bin/bash # CI/CD Site/Documentation Builder -# Builds and deploys VuePress documentation with version management -# Usage: ./scripts/ci_site.sh [OPTIONS] +# This script builds and deploys VuePress documentation with version management. +# Usage: ./scripts/ci_site.sh [OPTIONS] - Specify options for the script execution. set -euo pipefail # Constants -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -readonly DOCS_DIR="${PROJECT_ROOT}/docs" -readonly VUEPRESS_CONFIG="${DOCS_DIR}/.vuepress/config.js" -readonly VUEPRESS_BUILD_DIR="${DOCS_DIR}/.vuepress/dist" +# Resolve the folder containing this script so relative paths work +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +DOCS_DIR="${PROJECT_ROOT}/docs" +readonly SCRIPT_DIR +readonly PROJECT_ROOT +readonly DOCS_DIR + # Load shared configuration if [[ -f "${SCRIPT_DIR}/ci_00_config.sh" ]]; then @@ -20,29 +23,23 @@ else exit 1 fi -# Final output directory for site +# Final output directory for the site readonly SITE_OUTPUT_DIR="${PROJECT_ROOT}/${SITE_DIR}" # Default values -MODE="prod" -URL_PREFIX="/" -CUSTOM_VERSION="" -VERSION_SOURCE="release" -PREVIEW=false -GENERATE_WEBUPLOADER=true -WEBUPLOADER_ARGS="" -DEPLOY_DIR="." -RUN_PAGESPEED=false -PAGESPEED_URL="https://docs.openmqttgateway.com/" +MODE="prod" # Set the default mode to production +CURL_INSECURE=false # Allow curl to skip TLS verification (use only when needed) +CLEAN=false # Clean generated/site before build +LEGACY_OPENSSL=false # Use --openssl-legacy-provider for older Node.js versions # Function to check required tools check_requirements() { log_info "Checking required tools..." local missing_tools=() - + if ! command -v node >/dev/null 2>&1; then - missing_tools+=("node") + missing_tools+=("node") # Node.js is required else local node_version node_version=$(node --version) @@ -50,27 +47,19 @@ check_requirements() { fi if ! command -v npm >/dev/null 2>&1; then - missing_tools+=("npm") + missing_tools+=("npm") # npm is required else local npm_version npm_version=$(npm --version) log_info "✓ npm ${npm_version} found" fi - if ! command -v python3 >/dev/null 2>&1; then - missing_tools+=("python3") + if ! command -v openssl >/dev/null 2>&1; then + missing_tools+=("openssl") # OpenSSL is required else - local python_version - python_version=$(python3 --version | grep -oP '\d+\.\d+' || echo "unknown") - log_info "✓ Python ${python_version} found" - fi - - if ! command -v pip3 >/dev/null 2>&1; then - missing_tools+=("pip3") - else - local pip_version - pip_version=$(pip3 --version | grep -oP '\d+\.\d+' || echo "unknown") - log_info "✓ pip ${pip_version} found" + local openssl_version + openssl_version=$(openssl version | grep -oP '\d+\.\d+\.\d+' || echo "unknown") + log_info "✓ OpenSSL ${openssl_version} found" fi if [[ ${#missing_tools[@]} -gt 0 ]]; then @@ -86,21 +75,6 @@ check_requirements() { install_dependencies() { log_info "Installing dependencies..." - # Upgrade pip first (if pip module is available) - if python3 -m pip --version >/dev/null 2>&1; then - log_info "Upgrading pip..." - python3 -m pip install --upgrade pip --quiet || { - log_warn "Failed to upgrade pip, continuing with existing version..." - } - fi - - # Install Python dependencies (without --user if in virtualenv) - log_info "Installing Python dependencies..." - pip3 install requests pandas markdown pytablereader tabulate || { - log_error "Failed to install Python dependencies" - return 1 - } - # Install Node dependencies log_info "Installing Node.js dependencies..." cd "${PROJECT_ROOT}" @@ -118,118 +92,49 @@ download_common_config() { local config_url="https://www.theengs.io/commonConfig.js" local config_dest="${DOCS_DIR}/.vuepress/public/commonConfig.js" + local curl_opts="-sSf" + + # Optionally disable TLS verification if explicitly requested + if [[ "${CURL_INSECURE}" == "true" ]]; then + curl_opts+="k" + log_warn "curl is running with --insecure; TLS verification is disabled" + fi mkdir -p "$(dirname "$config_dest")" - if curl -sSf -o "$config_dest" "$config_url"; then + if curl ${curl_opts} -o "$config_dest" "$config_url"; then log_info "✓ Common config downloaded" else log_warn "Failed to download common config, continuing anyway..." fi } -# Function to get version -get_version() { - local version="" - - if [[ "$VERSION_SOURCE" == "custom" && -n "$CUSTOM_VERSION" ]]; then - version="$CUSTOM_VERSION" - log_info "Using custom version: $version" - elif [[ "$VERSION_SOURCE" == "release" ]]; then - # Try to get latest git tag (simulating GitHub release) - if command -v git >/dev/null 2>&1 && [[ -d "${PROJECT_ROOT}/.git" ]]; then - version=$(git describe --tags --abbrev=0 2>/dev/null || echo "development") - log_info "Using release version from git: $version" - else - version="development" - log_warn "Git not available, using version: $version" - fi - else - # Auto-detect from git - if command -v git >/dev/null 2>&1 && [[ -d "${PROJECT_ROOT}/.git" ]]; then - version=$(git describe --tags --abbrev=0 2>/dev/null || echo "development") - log_info "Using git version: $version" - else - version="development" - log_warn "Git not available, using version: $version" - fi - fi - - echo "$version" -} - -# Function to set version in config files -set_version() { - local version="$1" - - log_info "Setting version: $version" - - # Update VuePress config - if [[ -f "$VUEPRESS_CONFIG" ]]; then - sed -i "s|version_tag|${version}|g" "$VUEPRESS_CONFIG" - fi - - # Update version JSON file based on version source - if [[ "$VERSION_SOURCE" == "custom" ]]; then - # Custom version updates dev file - local version_file="${SCRIPT_DIR}/latest_version_dev.json" - if [[ -f "$version_file" ]]; then - sed -i "s|version_tag|${version}|g" "$version_file" - fi - else - # Release version updates production file - local version_file="${SCRIPT_DIR}/latest_version.json" - if [[ -f "$version_file" ]]; then - sed -i "s|version_tag|${version}|g" "$version_file" - fi - fi -} - -# Function to set URL prefix (base path) -set_url_prefix() { +create_configuration_files() { local url_prefix="$1" - - if [[ "$url_prefix" != "/" ]]; then - log_info "Setting URL prefix: $url_prefix" - sed -i "s|base: '/'|base: '${url_prefix}'|g" "$VUEPRESS_CONFIG" - fi -} + local version="$2" + local mode="$3" + local dest_line="" -# Function to generate board documentation -generate_board_docs() { - log_info "Generating board documentation..." - - local generator="${SCRIPT_DIR}/generate_board_docs.py" - - if [[ -f "$generator" ]]; then - cd "${PROJECT_ROOT}" - python3 "$generator" || { - log_warn "Board documentation generation failed, continuing..." - } - else - log_warn "Board documentation generator not found, skipping..." + if [[ "$mode" == "dev" ]]; then + dest_line=" \"dest\": \"generated/site/dev\"," fi -} -# Function to generate WebUploader manifest -generate_webuploader() { - if [[ "$GENERATE_WEBUPLOADER" != true ]]; then - log_info "Skipping WebUploader generation" - return 0 - fi - - log_info "Generating WebUploader manifest..." - - local generator="${SCRIPT_DIR}/gen_wu.py" - - if [[ -f "$generator" ]]; then - cd "${PROJECT_ROOT}" - python3 "$generator" $WEBUPLOADER_ARGS || { - log_warn "WebUploader generation failed, continuing..." - } - else - log_warn "WebUploader generator not found, skipping..." - fi + # download common config + download_common_config + + + ## Create a meta.json file on config folder + local meta_file="${DOCS_DIR}/.vuepress/meta.json" + cat > "$meta_file" < - $mfn - - ''') - -wu_temp_p1 = ''' - -''' - -manif_path = 'docs/.vuepress/public/firmware_build/' -vue_path = 'docs/.vuepress/components/' -cors_proxy = '' # 'https://cors.bridged.cc/' -esp32_boot = 'https://github.com/espressif/arduino-esp32/raw/2.0.7/tools/partitions/boot_app0.bin' diff --git a/scripts/ensure_ssl_certs.js b/scripts/ensure_ssl_certs.js new file mode 100644 index 00000000..42c33382 --- /dev/null +++ b/scripts/ensure_ssl_certs.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +function ensureSSLCerts() { + const sslDir = path.join(process.cwd(), '.ssl'); + const keyFile = path.join(sslDir, 'key.pem'); + const certFile = path.join(sslDir, 'cert.pem'); + + if (fs.existsSync(keyFile) && fs.existsSync(certFile)) { + return; + } + + const { execSync } = require('child_process'); + + fs.mkdirSync(sslDir, { recursive: true }); + + try { + execSync(`openssl req -new -x509 -keyout "${keyFile}" -out "${certFile}" -days 365 -nodes -subj "/C=US/ST=State/L=City/O=OpenMQTTGateway/CN=localhost" 2>/dev/null`, { + stdio: 'inherit' + }); + console.log('✓ SSL certificate generated'); + } catch (err) { + console.error('Failed to generate SSL certificate. Ensure openssl is installed.'); + process.exit(1); + } +} + +ensureSSLCerts(); diff --git a/scripts/gen_wu.py b/scripts/gen_wu.py deleted file mode 100644 index e5465310..00000000 --- a/scripts/gen_wu.py +++ /dev/null @@ -1,149 +0,0 @@ -# Creates web installer manifests for ESP Web Tools firmware installation -# Used by: .github/workflows/task-docs.yml -import os -import requests -import json -import argparse -import shutil - -from common_wu import mf_temp32, mf_temp32c3, mf_temp8266, wu_temp_opt, wu_temp_p1, wu_temp_p2, wu_temp_p3, wu_temp_p4, wu_temp_end, vue_path, manif_path, cors_proxy, esp32_boot, mf_temp32s3 - -parser = argparse.ArgumentParser() -parser.add_argument('--dev', action='store_true') -parser.add_argument('repo', nargs='?', default='1technophile/OpenMQTTGateway') -args = parser.parse_args() -repo = args.repo -dev = args.dev - -bin_path = 'toDeploy/' -manif_folder = "/firmware_build/" - -if not os.path.exists(manif_path): - os.makedirs(manif_path) - -if dev: - print('Generate Web Upload in dev mode') - manif_folder = "/dev" + manif_folder - # copy OTA latest version definition - shutil.copy("scripts/latest_version_dev.json", - manif_path + "latest_version_dev.json") - # copy the binaries frombin_path to manif_path - for name in os.listdir(bin_path): - if '.bin' in name: - shutil.copyfile(bin_path + name, (manif_path + name)) -else: - print('Generate Web Upload in release mode') - # copy OTA latest version definition - shutil.copy("scripts/latest_version.json", manif_path + "latest_version.json") - release = requests.get('https://api.github.com/repos/' + - repo + '/releases/latest') - rel_data = json.loads(release.text) - if 'assets' in rel_data: - assets = rel_data['assets'] - # Download assets into manif_path - for item in range(len(assets)): - name = assets[item]['name'] - if 'firmware.bin' in name: - fw_bin = requests.get(assets[item]['browser_download_url']) - filename = assets[item]['browser_download_url'].split('/')[-1] - with open(manif_path + filename, 'wb') as output_file: - output_file.write(fw_bin.content) - print('Downloaded: ' + filename) - if 'partitions.bin' in name: - part_bin = requests.get(assets[item]['browser_download_url']) - filename = assets[item]['browser_download_url'].split('/')[-1] - with open(manif_path + filename, 'wb') as output_file: - output_file.write(part_bin.content) - print('Downloaded: ' + filename) - if 'bootloader.bin' in name: - bl_bin = requests.get(assets[item]['browser_download_url']) - filename = assets[item]['browser_download_url'].split('/')[-1] - with open(manif_path + filename, 'wb') as output_file: - output_file.write(bl_bin.content) - print('Downloaded: ' + filename) - else: - print('Assets not found') - os._exit(1) - -if not os.path.exists(vue_path): - os.makedirs(vue_path) - -boot_bin = requests.get(esp32_boot) -filename = esp32_boot.split('/')[-1] -with open(manif_path + filename, 'wb') as output_file: - output_file.write(boot_bin.content) - -wu_file = open(vue_path + 'web-uploader.vue', 'w') -wu_file.write(wu_temp_p1) - -for name in sorted(os.listdir(manif_path)): - if 'firmware.bin' in name and ('esp32c3' not in name ) and ('esp32s3' not in name ) and ('esp32' in name or 'ttgo' in name or 'heltec' in name or 'thingpulse' in name or 'theengs' in name or 'lilygo' in name or 'shelly' in name or 'tinypico' in name): - fw = name.split('-firmware')[0] - man_file = fw + '.manifest.json' - fwp_name = name.split('-firmware')[0] + '-partitions.bin' - fwb_name = name.split('-firmware')[0] + '-bootloader.bin' - mani_str = mf_temp32.substitute({'cp': cors_proxy, 'part': manif_folder + fwp_name.split('/')[-1], 'bin': manif_folder + name, 'bl': manif_folder + fwb_name, 'boot': manif_folder + esp32_boot.split('/')[-1]}) - - with open(manif_path + man_file, 'w') as nf: - nf.write(mani_str) - - wu_file.write(wu_temp_opt.substitute( - {'mff': manif_folder + man_file, 'mfn': fw})) - - print('Created: ' + man_file) - -wu_file.write(wu_temp_p2) - -for name in sorted(os.listdir(manif_path)): - if 'firmware.bin' in name and ('esp32c3' in name ): - fw = name.split('-firmware')[0] - man_file = fw + '.manifest.json' - fwp_name = name.split('-firmware')[0] + '-partitions.bin' - fwb_name = name.split('-firmware')[0] + '-bootloader.bin' - mani_str = mf_temp32c3.substitute({'cp': cors_proxy, 'part': manif_folder + fwp_name, 'bin': manif_folder + name, 'bl': manif_folder + fwb_name, 'boot': manif_folder + esp32_boot.split('/')[-1]}) - - with open(manif_path + man_file, 'w') as nf: - nf.write(mani_str) - - wu_file.write(wu_temp_opt.substitute( - {'mff': manif_folder + man_file, 'mfn': fw})) - - print('Created: ' + man_file) - -wu_file.write(wu_temp_p3) - -for name in sorted(os.listdir(manif_path)): - if 'firmware.bin' in name and ('esp32s3' in name ): - fw = name.split('-firmware')[0] - man_file = fw + '.manifest.json' - fwp_name = name.split('-firmware')[0] + '-partitions.bin' - fwb_name = name.split('-firmware')[0] + '-bootloader.bin' - mani_str = mf_temp32s3.substitute({'cp': cors_proxy, 'part': manif_folder + fwp_name, 'bin': manif_folder + name, 'bl': manif_folder + fwb_name, 'boot': manif_folder + esp32_boot.split('/')[-1]}) - - with open(manif_path + man_file, 'w') as nf: - nf.write(mani_str) - - wu_file.write(wu_temp_opt.substitute( - {'mff': manif_folder + man_file, 'mfn': fw})) - - print('Created: ' + man_file) - -wu_file.write(wu_temp_p4) - -for name in sorted(os.listdir(manif_path)): - if 'firmware.bin' in name and ('nodemcu' in name or 'sonoff' in name or 'rf-wifi-gateway' in name or 'manual-wifi-test' in name or 'rfbridge' in name): - fw = name.split('-firmware')[0] - man_file = fw + '.manifest.json' - mani_str = mf_temp8266.substitute( - {'cp': cors_proxy, 'bin': manif_folder + name}) - - with open(manif_path + man_file, 'w') as nf: - nf.write(mani_str) - - wu_file.write( - manif_folder + wu_temp_opt.substitute({'mff': manif_folder + man_file, 'mfn': fw})) - - print('Created: ' + man_file) - -wu_file.write(wu_temp_end) -wu_file.close() diff --git a/scripts/generate_board_docs.py b/scripts/generate_board_docs.py deleted file mode 100644 index c44f044f..00000000 --- a/scripts/generate_board_docs.py +++ /dev/null @@ -1,90 +0,0 @@ -# Generates board documentation table from platformio.ini environments -# Used by: .github/workflows/task-docs.yml -import pytablereader as ptr -import pandas as pd -import os -import re -import configparser -conf = configparser.ConfigParser() - -# Init the table with the columns -table_init = pd.DataFrame(columns=['Environment', 'uC', 'Hardware', 'Description', 'Modules', 'Platform', - 'Partitions', 'Libraries', 'Options']) -table = table_init - -# Parse platformio.ini to retrieve boards information -conf.read('environments.ini') -for each_section in conf.sections(): - if ("env:" in each_section and "-test" not in each_section): - env = each_section.replace("env:", "") - uc = "" - board = "" - hardware = "" - description = "" - modules = "" - platform = "" - partitions = "" - libraries = "" - options = "" - for (k, v) in conf.items(each_section): - v = v.replace('{', '').replace('}', '').replace('$', '').replace( - "env:", '').replace('\'', '').replace("-D", "") - if (k == "board"): - uc = v - if (k == "platform"): - platform = v - if (k == "board_build.partitions"): - partitions = v - if (k == "lib_deps"): - libraries = v - libraries = libraries.replace( - "\ncom-esp.lib_deps\n", "").replace( - "\ncom-arduino.lib_deps\n", "").replace("libraries.", "") - if (k == "build_flags"): - options = v - for o in options.split('\n'): - if ("gateway" in o or "sensor" in o or "actuator" in o): - if (modules != ""): - modules = modules + "\n" - modules = modules + o[1:o.rfind("=\"")] - options = options.replace( - "com-esp.build_flags\n", "") - if (k == "custom_description"): - description = v - if (k == "custom_hardware"): - hardware = v - table.loc[len(table.index)] = [env, uc, hardware, description, modules, platform, - partitions, libraries, options] - -# Sort rows per Environment name -table.sort_values(by=['Environment'], inplace=True, - key=lambda col: col.str.lower()) - -# Produce individual file -for ind in table.index: - table_extract = table.iloc[ind] - print(table_extract) - file = open("docs/prerequisites/boards/" + - table.iloc[ind]["Environment"] + ".md", 'w') - table_extract = table_extract.rename_axis("Board index") - table_md = table_extract.to_markdown() - n = file.write(table_md) - file.close() - -# Produce list file -# Add link to the file from the environment and replace /n with , -for ind in table.index: - table['Environment'][ind] = "[" + table['Environment'][ind] + \ - "](../prerequisites/boards/" + table['Environment'][ind] + ")" - -table = table.replace("\n", ", ", regex=True) -table = table.drop(["Partitions", "Hardware", "Platform", "Options","Modules"], axis=1) -table = table.reset_index(drop=True) -print(table) -# Convert to Markdown and save per Model_Id -table_md = table.to_markdown() -file = open("docs/prerequisites/board.md", 'a') -n = file.write("# Supported\n" + table_md) -file = open("docs/upload/web-install.md", 'a') -n = file.write(table_md) -file.close() diff --git a/scripts/prepare_deploy.sh b/scripts/prepare_deploy.sh deleted file mode 100755 index 90b603cd..00000000 --- a/scripts/prepare_deploy.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# Prepares firmware binaries and libraries for GitHub release deployment -# Used by: .github/workflows/release.yml -set -e -echo "renaming bin files with the environment name" -rename -v 's:/:-:g' .pio/build/*/*.bin -mkdir toDeploy -rename 's/.pio-build-//' .*.bin -( - cd .pio/libdeps - echo "replace space by _ in folder names" - find . -type d -name "* *" | while read FNAME; do mv "$FNAME" "${FNAME// /_}"; done - echo "zipping libraries per board" - for i in */ - do - zip -r "${i%/}-libraries.zip" "$i" - done - ls -lA - mv *.zip ../../toDeploy -) -# remove binaries for *-all*, *-test* env and only zip containing *-test* -rm -f *-all*.bin *-test*.bin *-test*.zip -echo "zipping code and licence" -zip -r OpenMQTTGateway_sources.zip main LICENSE.txt -mv *.zip toDeploy -mv *.bin toDeploy - -ls -lA toDeploy diff --git a/scripts/preview_site.js b/scripts/preview_site.js new file mode 100644 index 00000000..6282b87e --- /dev/null +++ b/scripts/preview_site.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +'use strict'; + +// Minimal HTTPS static server for generated/site only. + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const mime = require('mime-types'); + +const repoRoot = path.resolve(__dirname, '..'); +const siteRoot = path.join(repoRoot, 'generated', 'site'); +const port = parseInt(process.argv[2] || 8443, 10); +const keyFile = process.argv[3] || path.join(repoRoot, '.ssl', 'key.pem'); +const certFile = process.argv[4] || path.join(repoRoot, '.ssl', 'cert.pem'); + +if (!fs.existsSync(siteRoot)) { + console.error(`Error: folder not found: ${siteRoot}`); + process.exit(1); +} + +if (!fs.existsSync(keyFile) || !fs.existsSync(certFile)) { + console.error(`Error: SSL certificates not found at ${keyFile} or ${certFile}`); + console.error('Use scripts/ensure_ssl_certs.js to generate them.'); + process.exit(1); +} + +const server = https.createServer({ + key: fs.readFileSync(keyFile), + cert: fs.readFileSync(certFile) +}, (req, res) => { + const reqPath = decodeURIComponent(req.url.split('?')[0]); + const safePath = reqPath.endsWith('/') ? `${reqPath}index.html` : reqPath; + const filePath = path.join(siteRoot, safePath); + + if (!filePath.startsWith(siteRoot)) { + res.writeHead(403); + return res.end('Forbidden'); + } + + fs.stat(filePath, (err, stats) => { + if (err || !stats.isFile()) { + res.writeHead(404); + return res.end('Not Found'); + } + const type = mime.contentType(path.extname(filePath)) || 'application/octet-stream'; + res.writeHead(200, { 'Content-Type': type }); + fs.createReadStream(filePath).pipe(res); + }); +}); + +server.listen(port, '0.0.0.0', () => { + console.log(`Serving generated/site over HTTPS at https://localhost:${port}/`); + console.log(`Serving generated/site over HTTPS at https://:${port}/`); + console.log(`Note: remember that if you are testing DEV site you should go on https://:${port}/dev/`); +}); + +process.on('SIGINT', () => { + server.close(() => process.exit()); +});