Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a8732318 | ||
|
|
b1e2301ea1 | ||
|
|
0d8a8b35f0 | ||
|
|
d686cf71fc | ||
|
|
86ecf4e5be | ||
|
|
a11f3b9990 | ||
|
|
d8ad7e7c2b | ||
|
|
ef7b6ac00a | ||
|
|
1d865aa881 | ||
|
|
4cdce23d2a | ||
|
|
60aa84841d | ||
|
|
7b9d32b765 | ||
|
|
f06b4b1de6 | ||
|
|
3986d9d1f7 | ||
|
|
3ec01a72fd | ||
|
|
eb1e7c71e6 | ||
|
|
9795527b34 | ||
|
|
827c37542a | ||
|
|
4e49049b27 | ||
|
|
050ba3e827 | ||
|
|
3b8496e323 | ||
|
|
5b839aadd3 | ||
|
|
7945a42d06 | ||
|
|
4e3ed19827 | ||
|
|
1dfa2f17fb | ||
|
|
77754a1fe9 | ||
|
|
98ae55cd7b | ||
|
|
47da455246 | ||
|
|
b6b23bd423 | ||
|
|
e30b65ee6f | ||
|
|
d8ffa3f1dd | ||
|
|
7df23d4e3e | ||
|
|
5fdaddadaf | ||
|
|
4462ac94bf | ||
|
|
65d0998278 | ||
|
|
b9e8821b17 | ||
|
|
f5b33104ef | ||
|
|
382aae8353 | ||
|
|
a33010c068 | ||
|
|
f76d04b613 | ||
|
|
3c33431fe5 | ||
|
|
77b588fb60 | ||
|
|
b493dddb97 | ||
|
|
b01240330a | ||
|
|
268b879239 | ||
|
|
0bbc00cfa0 | ||
|
|
4b7d3f6589 | ||
|
|
94aa6ae443 | ||
|
|
0b6780678f | ||
|
|
2459e1c29b | ||
|
|
9f5bf3ec57 | ||
|
|
c027dd225d | ||
|
|
4d945851ce | ||
|
|
cf27d0e3e7 | ||
|
|
6d64dbcd8c | ||
|
|
e7411d2000 | ||
|
|
7904d49dbe | ||
|
|
319c05af5c | ||
|
|
647e437382 | ||
|
|
9e93e37506 | ||
|
|
ddd05ebe50 | ||
|
|
7551ed9e22 | ||
|
|
23fc99f0fe | ||
|
|
9e8098964c | ||
|
|
a5158c503b | ||
|
|
079c987450 | ||
|
|
8273617c17 | ||
|
|
600d48dc48 | ||
|
|
3b2b8a333a | ||
|
|
3a4eb55a79 | ||
|
|
7d6a019699 | ||
|
|
31f949c80b | ||
|
|
8b3aa1d981 | ||
|
|
c58ead0a35 | ||
|
|
883e9318cf | ||
|
|
98481c5145 | ||
|
|
5f0966c2c1 | ||
|
|
cd9d6ecb96 | ||
|
|
5fe77e9c85 | ||
|
|
df1204d92f | ||
|
|
134c03362c | ||
|
|
fd433c220e | ||
|
|
497dc878f5 | ||
|
|
f52bc6c11e | ||
|
|
32664499f3 | ||
|
|
e01b22695a | ||
|
|
0c2884c392 | ||
|
|
07d048f1c3 | ||
|
|
5c401f6d02 | ||
|
|
e91086a9bd | ||
|
|
5f969bf26b | ||
|
|
50ff9f535f | ||
|
|
c6b2aae965 | ||
|
|
401be5f10c | ||
|
|
593f25ff59 | ||
|
|
75f9d30919 | ||
|
|
382fa5e9f2 | ||
|
|
226297ba5b | ||
|
|
f8f96a0383 | ||
|
|
e495b61216 | ||
|
|
32174e37a5 | ||
|
|
6e6b54b05e | ||
|
|
51bdc2d080 | ||
|
|
14ceba5bee | ||
|
|
5add388fbc | ||
|
|
6d7e4fd8f1 | ||
|
|
75b3a3657e | ||
|
|
6d1b7935c4 | ||
|
|
ddb9cc67cc | ||
|
|
2d29ee4524 | ||
|
|
a0d5b7d181 | ||
|
|
4166d2ab68 | ||
|
|
27a6ec592b | ||
|
|
162a4d3ae9 | ||
|
|
b8b427555a | ||
|
|
3f569b9de3 | ||
|
|
fff05ee2e4 | ||
|
|
f634f1f204 | ||
|
|
9b6e835afb | ||
|
|
e47c145bad | ||
|
|
a4ba937553 | ||
|
|
828f0a226e | ||
|
|
301b3eecd8 | ||
|
|
6175e03bc8 | ||
|
|
f0491e188c | ||
|
|
585df9a420 | ||
|
|
6e77efb11b | ||
|
|
bb135aa0bf | ||
|
|
037ee88528 | ||
|
|
7f542c8347 | ||
|
|
e683fbbd18 | ||
|
|
e6443143bb | ||
|
|
e2ada763ec | ||
|
|
d03795d0bf | ||
|
|
4bf43f5695 | ||
|
|
d837dfd32a | ||
|
|
cd35520bb0 | ||
|
|
5b6cf44662 | ||
|
|
bead01347e | ||
|
|
d8c0f0f9c5 | ||
|
|
4d6e7f4afa | ||
|
|
5e1860abd9 | ||
|
|
3db54b4266 | ||
|
|
312b4bcf17 | ||
|
|
7fa00dd593 | ||
|
|
1b50c8ec02 | ||
|
|
2cce06ed4e | ||
|
|
9bb7751b8d | ||
|
|
1f45a7711b | ||
|
|
8bad846d94 | ||
|
|
b5adf1f5ac | ||
|
|
bbea094438 | ||
|
|
18cfbb759a | ||
|
|
3bcfdee712 | ||
|
|
379312a127 | ||
|
|
fdc7435079 | ||
|
|
f5fe3ed223 | ||
|
|
921488359e | ||
|
|
bb05011bdc | ||
|
|
87228b5f29 | ||
|
|
3cd7c92d8f | ||
|
|
742945906d | ||
|
|
e2641f49e7 | ||
|
|
f5bca0054e | ||
|
|
ac1ad10240 | ||
|
|
ba9506c022 | ||
|
|
044182460c | ||
|
|
67e50642dd | ||
|
|
952a65dd0f | ||
|
|
f30960566c | ||
|
|
f86d89db85 | ||
|
|
3b3fe3eabe | ||
|
|
258e3b3f6a | ||
|
|
4f26d39b93 | ||
|
|
5f80fea88d | ||
|
|
c98ac731b4 | ||
|
|
898b5e6213 | ||
|
|
4e090eda3c | ||
|
|
fcc09df02c | ||
|
|
e390aca43b | ||
|
|
c234afa9c8 | ||
|
|
9e5f56580d | ||
|
|
88e09092d6 | ||
|
|
3187ed6154 | ||
|
|
a89ff43900 | ||
|
|
6fa725d633 | ||
|
|
026fb4a337 | ||
|
|
0d338cc5c5 | ||
|
|
0b4c6ef107 | ||
|
|
4b20976a08 | ||
|
|
776ed6568d | ||
|
|
eab1354d7a | ||
|
|
32e01cdc96 | ||
|
|
ed0b683754 | ||
|
|
435b8ca3d9 | ||
|
|
20af9ccf11 | ||
|
|
5d6f735058 | ||
|
|
caea021ca9 | ||
|
|
00fe36f426 | ||
|
|
2b958ae363 | ||
|
|
5e65e807b7 | ||
|
|
0999563095 | ||
|
|
220e2fe164 | ||
|
|
c5b9bcb4cb | ||
|
|
719976b652 | ||
|
|
cb824e2ee4 | ||
|
|
1f42075ec8 | ||
|
|
830d58f98e | ||
|
|
5564ed531c | ||
|
|
f34e549d59 | ||
|
|
5492e334dc | ||
|
|
3d6d9baeaf | ||
|
|
18461b9ef8 | ||
|
|
a07bbe5c63 | ||
|
|
f9a5c6961a | ||
|
|
068c2e457a | ||
|
|
11ea8210a2 | ||
|
|
b2ebf670d6 | ||
|
|
57fbca8f88 | ||
|
|
36945dae42 | ||
|
|
b0cf3c1dc7 | ||
|
|
761e3e34f3 | ||
|
|
a2fa86eef6 | ||
|
|
f655f4fda0 | ||
|
|
1f5e5006c2 | ||
|
|
8fd672de45 | ||
|
|
8a28bb2cf8 | ||
|
|
25c3da7ee9 | ||
|
|
1c942a06cc | ||
|
|
04c3e7ef95 | ||
|
|
4ec577d6af | ||
|
|
d43d93f151 | ||
|
|
6441175262 | ||
|
|
ad74cf9c3c | ||
|
|
c911ec08dc | ||
|
|
8537b7498d | ||
|
|
ada83bf227 | ||
|
|
35124f2af8 | ||
|
|
b67016bd05 | ||
|
|
3c0140f57b | ||
|
|
cd04c5b92a | ||
|
|
9257433a50 | ||
|
|
b20f2c4819 | ||
|
|
77d0d14af3 | ||
|
|
b1b4bddcbc | ||
|
|
2d5b8b403e | ||
|
|
9af5a2aca0 | ||
|
|
7a2dc8b074 | ||
|
|
6a5d3a3da7 | ||
|
|
aa5fd3cb0b | ||
|
|
4bb7c5502d | ||
|
|
57d1105ba8 | ||
|
|
e4e4814c28 | ||
|
|
0a1ee69a72 | ||
|
|
676a62fecb | ||
|
|
a74a6ebc06 | ||
|
|
e321448db0 | ||
|
|
23b3123b97 | ||
|
|
db8e850570 | ||
|
|
d538316e03 | ||
|
|
6e906702ed | ||
|
|
417d28fb20 | ||
|
|
33511b5b25 | ||
|
|
07d9ed9df5 | ||
|
|
0db67f75aa | ||
|
|
819d3c2d91 | ||
|
|
f98474bed0 | ||
|
|
743083cbfe | ||
|
|
bae4364e41 | ||
|
|
b5e0223602 | ||
|
|
f27855527e | ||
|
|
68fd4dc17e | ||
|
|
55d9d87a77 | ||
|
|
d721567adb | ||
|
|
115a17cbcc | ||
|
|
c85f5bec37 | ||
|
|
2645928dbd | ||
|
|
2065d57e11 | ||
|
|
30f8264878 | ||
|
|
0929fa23a8 | ||
|
|
a52296a97d | ||
|
|
a845d772f9 | ||
|
|
b884ee8883 | ||
|
|
bf70508318 | ||
|
|
781f7af90c | ||
|
|
6c62858093 | ||
|
|
369eb0447b | ||
|
|
3a9193497d |
No files matched your search
@@ -10,13 +10,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
# PlatformIO environment to build (see environments.ini)
|
||||
PIO_ENV: heltec-rtl_433
|
||||
# Chip / flash layout used to create the merged (factory) image
|
||||
ESP_CHIP: esp32
|
||||
BOOTLOADER_OFFSET: '0x1000'
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
@@ -43,71 +37,67 @@ jobs:
|
||||
run: |
|
||||
if ! python3 -m venv /tmp/pio-venv >/dev/null 2>&1; then
|
||||
rm -rf /tmp/pio-venv
|
||||
apt-get update && apt-get install -y python3 python3-venv python3-pip curl git
|
||||
apt-get update && apt-get install -y python3 python3-venv python3-pip curl
|
||||
python3 -m venv /tmp/pio-venv
|
||||
fi
|
||||
/tmp/pio-venv/bin/pip install --upgrade pip
|
||||
/tmp/pio-venv/bin/pip install -U platformio setuptools
|
||||
echo "/tmp/pio-venv/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
- name: Determine default environment and version
|
||||
id: prep
|
||||
run: |
|
||||
# Only the first active "default_envs" entry from platformio.ini is built
|
||||
PIO_ENV="$(sed -n 's/^[[:space:]]*default_envs[[:space:]]*=[[:space:]]*//p' platformio.ini \
|
||||
| head -n 1 | cut -d',' -f1 | tr -d '[:space:]')"
|
||||
if [ -z "${PIO_ENV}" ]; then
|
||||
echo "No active default_envs found in platformio.ini" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
TAG_NAME="${GITHUB_REF_NAME}"
|
||||
else
|
||||
BASE_VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo v1.7.0)"
|
||||
BASE_VERSION="$(git describe --tags --abbrev=0 --exclude '*-b*' 2>/dev/null || echo v1.7.0)"
|
||||
TAG_NAME="${BASE_VERSION}-b${GITHUB_RUN_NUMBER}-${SHORT_SHA}"
|
||||
fi
|
||||
|
||||
echo "Environment: ${PIO_ENV}, version: ${TAG_NAME}"
|
||||
echo "pio_env=${PIO_ENV}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set version tag in sources
|
||||
- name: Build OpenMQTTGateway Firmware (${{ steps.prep.outputs.pio_env }})
|
||||
env:
|
||||
TAG_NAME: ${{ steps.version.outputs.tag_name }}
|
||||
run: |
|
||||
sed -i "s/version_tag/${TAG_NAME}/g" main/User_config.h scripts/latest_version.json
|
||||
|
||||
- name: Build OpenMQTTGateway Firmware (${{ env.PIO_ENV }})
|
||||
PIO_ENV: ${{ steps.prep.outputs.pio_env }}
|
||||
TAG_NAME: ${{ steps.prep.outputs.tag_name }}
|
||||
run: |
|
||||
# Inject the firmware version (same mechanism as upstream scripts/ci_build_firmware.sh)
|
||||
export PLATFORMIO_BUILD_FLAGS="-DOMG_VERSION=\\\"${TAG_NAME}\\\""
|
||||
mkdir -p ".pio/build/${PIO_ENV}"
|
||||
pio run -e "${PIO_ENV}"
|
||||
|
||||
- name: Prepare Release Artifacts
|
||||
env:
|
||||
TAG_NAME: ${{ steps.version.outputs.tag_name }}
|
||||
PIO_ENV: ${{ steps.prep.outputs.pio_env }}
|
||||
TAG_NAME: ${{ steps.prep.outputs.tag_name }}
|
||||
run: |
|
||||
BUILD_DIR=".pio/build/${PIO_ENV}"
|
||||
PREFIX="OpenMQTTGateway-${PIO_ENV}-${TAG_NAME}"
|
||||
BOOT_APP0="${HOME}/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin"
|
||||
|
||||
mkdir -p dist
|
||||
# OTA / update image
|
||||
cp "${BUILD_DIR}/firmware.bin" dist/firmware.bin
|
||||
cp "${BUILD_DIR}/firmware.bin" "dist/${PREFIX}-firmware.bin"
|
||||
cp "${BUILD_DIR}/firmware.elf" "dist/${PREFIX}-firmware.elf"
|
||||
cp "${BUILD_DIR}/partitions.bin" "dist/${PREFIX}-partitions.bin"
|
||||
cp "${BUILD_DIR}/bootloader.bin" "dist/${PREFIX}-bootloader.bin"
|
||||
cp "${BOOT_APP0}" "dist/${PREFIX}-boot_app0.bin"
|
||||
|
||||
# Merged factory image (flash at 0x0 on an erased chip)
|
||||
pio pkg exec -p tool-esptoolpy -- esptool.py --chip "${ESP_CHIP}" merge_bin \
|
||||
-o "dist/${PREFIX}-factory.bin" \
|
||||
"${BOOTLOADER_OFFSET}" "${BUILD_DIR}/bootloader.bin" \
|
||||
0x8000 "${BUILD_DIR}/partitions.bin" \
|
||||
0xe000 "${BOOT_APP0}" \
|
||||
0x10000 "${BUILD_DIR}/firmware.bin"
|
||||
|
||||
(cd dist && sha256sum * > SHA256SUMS.txt)
|
||||
# Only the firmware image is published
|
||||
cp ".pio/build/${PIO_ENV}/firmware.bin" "dist/OpenMQTTGateway-${PIO_ENV}-${TAG_NAME}.bin"
|
||||
ls -lA dist
|
||||
|
||||
- name: Create Gitea Release and Upload Binaries
|
||||
- name: Create Gitea Release and Publish Package
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ steps.version.outputs.tag_name }}
|
||||
SHORT_SHA: ${{ steps.version.outputs.short_sha }}
|
||||
GITEA_PKG_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||
PIO_ENV: ${{ steps.prep.outputs.pio_env }}
|
||||
TAG_NAME: ${{ steps.prep.outputs.tag_name }}
|
||||
SHORT_SHA: ${{ steps.prep.outputs.short_sha }}
|
||||
run: |
|
||||
python3 - << 'EOF'
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
@@ -116,12 +106,14 @@ jobs:
|
||||
|
||||
server_url = os.environ["GITHUB_SERVER_URL"].rstrip("/")
|
||||
repo = os.environ["GITHUB_REPOSITORY"]
|
||||
owner, repo_name = repo.split("/", 1)
|
||||
actor = os.environ.get("GITHUB_ACTOR") or owner
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
pkg_token = os.environ.get("GITEA_PKG_TOKEN") or token
|
||||
tag_name = os.environ["TAG_NAME"]
|
||||
sha = os.environ["GITHUB_SHA"]
|
||||
short_sha = os.environ["SHORT_SHA"]
|
||||
pio_env = os.environ["PIO_ENV"]
|
||||
bootloader_offset = os.environ["BOOTLOADER_OFFSET"]
|
||||
|
||||
api_base = f"{server_url}/api/v1/repos/{repo}"
|
||||
headers = {
|
||||
@@ -134,10 +126,7 @@ jobs:
|
||||
f"Automated OpenMQTTGateway firmware build.\n\n"
|
||||
f"- **Environment:** `{pio_env}`\n"
|
||||
f"- **Commit:** `{short_sha}`\n"
|
||||
f"- **OTA update:** Upload `firmware.bin` via the WebUI / OTA.\n"
|
||||
f"- **Fresh install:** Flash `*-factory.bin` at offset `0x0`, or flash "
|
||||
f"`*-bootloader.bin` @ `{bootloader_offset}`, `*-partitions.bin` @ `0x8000`, "
|
||||
f"`*-boot_app0.bin` @ `0xe000` and `*-firmware.bin` @ `0x10000`."
|
||||
f"- **OTA update:** Upload the `.bin` file via the WebUI / OTA."
|
||||
)
|
||||
|
||||
# Check if a release for this tag already exists
|
||||
@@ -162,7 +151,7 @@ jobs:
|
||||
"name": f"OpenMQTTGateway {tag_name} ({pio_env})",
|
||||
"body": release_notes,
|
||||
"draft": False,
|
||||
"prerelease": os.environ.get("GITHUB_REF_TYPE") != "tag",
|
||||
"prerelease": False,
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(f"{api_base}/releases", data=payload, headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
@@ -170,7 +159,12 @@ jobs:
|
||||
release_id = data["id"]
|
||||
print(f"Created release ID {release_id} for tag {tag_name}")
|
||||
|
||||
# Upload firmware binaries in dist/
|
||||
# Upload firmware binary to Release and Gitea Package Registry
|
||||
# Note: Gitea's Generic Package Registry upload router is at /api/packages/ (not /api/v1/packages/)
|
||||
package_name = f"{repo_name}-{pio_env}"
|
||||
pkg_base = f"{server_url}/api/packages/{owner}/generic/{package_name}/{tag_name}"
|
||||
basic_auth = base64.b64encode(f"{actor}:{pkg_token}".encode("utf-8")).decode("ascii")
|
||||
|
||||
for filename in sorted(os.listdir("dist")):
|
||||
if filename in existing_assets:
|
||||
del_req = urllib.request.Request(
|
||||
@@ -180,13 +174,33 @@ jobs:
|
||||
)
|
||||
urllib.request.urlopen(del_req).close()
|
||||
filepath = os.path.join("dist", filename)
|
||||
print(f"Uploading {filename}...")
|
||||
print(f"Uploading {filename} to Release...")
|
||||
subprocess.run([
|
||||
"curl", "--fail", "-sS", "-X", "POST",
|
||||
"curl", "--fail", "-sS", "-o", "/dev/null", "-X", "POST",
|
||||
"-H", f"Authorization: token {token}",
|
||||
"-F", f"attachment=@{filepath}",
|
||||
f"{api_base}/releases/{release_id}/assets?name={filename}"
|
||||
], check=True)
|
||||
print(f"Uploaded {filename} successfully.")
|
||||
EOF
|
||||
print(f"Uploaded {filename} to Release successfully.")
|
||||
|
||||
# Publish to Gitea Generic Package Registry (delete existing file first if re-running)
|
||||
pkg_file_url = f"{pkg_base}/{filename}"
|
||||
try:
|
||||
del_pkg_req = urllib.request.Request(
|
||||
pkg_file_url,
|
||||
headers={"Authorization": f"Basic {basic_auth}"},
|
||||
method="DELETE",
|
||||
)
|
||||
urllib.request.urlopen(del_pkg_req).close()
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
raise
|
||||
print(f"Publishing {filename} to Gitea Package {package_name}/{tag_name}...")
|
||||
subprocess.run([
|
||||
"curl", "--fail", "-sS", "-o", "/dev/null", "-X", "PUT",
|
||||
"--user", f"{actor}:{pkg_token}",
|
||||
"--upload-file", filepath,
|
||||
pkg_file_url,
|
||||
], check=True)
|
||||
print(f"Published {filename} to Gitea Package Registry successfully.")
|
||||
EOF
|
||||
@@ -1,13 +1,50 @@
|
||||
.pio
|
||||
.vscode
|
||||
*_env.ini
|
||||
*.pem
|
||||
**/.DS_Store
|
||||
node_modules
|
||||
*.ps1
|
||||
main/certs/private*
|
||||
lib/*
|
||||
!lib/esp32-bt-lib
|
||||
!lib/TheengsUtils
|
||||
!lib/LEDManager
|
||||
CMakeLists.txt
|
||||
dependencies.lock
|
||||
sdkconfig.*
|
||||
!sdkconfig.defaults
|
||||
__pycache__
|
||||
*.pem
|
||||
managed_components
|
||||
|
||||
|
||||
.github/chatmodes
|
||||
.github/prompts
|
||||
.github/*instructions.md
|
||||
.github/workflows/_docs
|
||||
# CI/CD Generated outputs
|
||||
docs/.vitepress/dist/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/public/boards-info.json
|
||||
docs/.vitepress/public/commonConfig.js
|
||||
docs/.vitepress/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/.vitepress/meta.json
|
||||
|
||||
# Generated full documentation pages
|
||||
docs/prerequisites/board-full.md
|
||||
docs/upload/web-install-full.md
|
||||
.superpowers/
|
||||
docs/superpowers/
|
||||
@@ -0,0 +1 @@
|
||||
disable=SC1091
|
||||
@@ -0,0 +1,319 @@
|
||||
# OpenMQTTGateway - Claude Code Context
|
||||
|
||||
This document provides essential context about the OpenMQTTGateway project for AI assistants working on the codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
OpenMQTTGateway is a unified firmware that bridges various wireless technologies (RF 433MHz, IR, BLE, LoRa, etc.) to MQTT protocol, enabling home automation integration with platforms like Home Assistant, OpenHAB, and Node-RED.
|
||||
|
||||
**Key Features:**
|
||||
- Multi-protocol gateway (BLE, RF 433MHz, IR, LoRa, RTL_433, Pilight, etc.)
|
||||
- ESP8266/ESP32 and Arduino support
|
||||
- MQTT-based communication
|
||||
- Web UI for configuration
|
||||
- Home Assistant MQTT Discovery support
|
||||
- Over 100 supported BLE sensors via Theengs Decoder
|
||||
|
||||
**Main Branch:** `development` (not master/main)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Modular Design
|
||||
|
||||
The firmware uses a modular architecture where each protocol/sensor/actuator is implemented as a separate "gateway" or "sensor" module:
|
||||
|
||||
- **Gateways**: Receive and transmit signals (e.g., `gatewayBT.cpp`, `gatewayRF.cpp`, `gatewayIR.cpp`)
|
||||
- **Sensors**: Read physical sensor data (e.g., `sensorBME280.cpp`, `sensorDHT.cpp`)
|
||||
- **Actuators**: Control outputs (e.g., `actuatorONOFF.cpp`, `actuatorPWM.cpp`)
|
||||
|
||||
Each module has:
|
||||
- A config file (e.g., `config_BT.h`, `config_RF.h`)
|
||||
- An implementation file (e.g., `gatewayBT.cpp`, `gatewayRF.cpp`)
|
||||
|
||||
### Configuration System
|
||||
|
||||
Configuration is managed through:
|
||||
1. **User_config.h** - Main configuration file with defaults
|
||||
2. **config_*.h** - Module-specific configurations
|
||||
3. **platformio.ini & environments.ini** - Build-time configurations
|
||||
4. **prod_env.ini** - User-specific environments (gitignored)
|
||||
|
||||
Build flags in `environments.ini` can override User_config.h parameters.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
OpenMQTTGateway/
|
||||
├── main/ # Main source code (Arduino convention)
|
||||
│ ├── main.cpp # Main application entry point
|
||||
│ ├── User_config.h # Global configuration
|
||||
│ ├── config_*.h # Module configurations
|
||||
│ ├── gateway*.cpp # Gateway implementations
|
||||
│ ├── sensor*.cpp # Sensor implementations
|
||||
│ ├── actuator*.cpp # Actuator implementations
|
||||
│ ├── webUI.cpp # Web interface
|
||||
│ ├── mqttDiscovery.cpp # Home Assistant discovery
|
||||
│ └── rf/ # RF-specific modules
|
||||
├── lib/ # Custom libraries
|
||||
├── scripts/ # Build and automation scripts
|
||||
├── docs/ # VuePress documentation
|
||||
├── tests/ # Test configurations
|
||||
├── environments.ini # PlatformIO environment definitions
|
||||
├── platformio.ini # PlatformIO configuration
|
||||
└── prod_env.ini.example # Example production environment
|
||||
|
||||
Note: src_dir is set to "main" in platformio.ini
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Environment-Based Builds
|
||||
|
||||
The project supports 80+ hardware configurations defined in `environments.ini`. Examples:
|
||||
- `esp32dev-ble` - ESP32 with BLE gateway
|
||||
- `esp32dev-pilight` - ESP32 with Pilight RF
|
||||
- `esp32dev-rtl_433` - ESP32 with RTL_433 decoder
|
||||
- `nodemcuv2-ir` - NodeMCU with IR
|
||||
- `rfbridge` - Sonoff RF Bridge
|
||||
|
||||
### MQTT Topics
|
||||
|
||||
Standard topic structure:
|
||||
- Base: `home/` (configurable via Base_Topic)
|
||||
- Gateway name: `OpenMQTTGateway` or `OMG` (configurable)
|
||||
- Example: `home/OpenMQTTGateway/BTtoMQTT`
|
||||
|
||||
### Module Activation
|
||||
|
||||
Modules are activated via build flags in environments.ini:
|
||||
```ini
|
||||
build_flags =
|
||||
'-DZgatewayBT="BT"' # Enable BLE gateway
|
||||
'-DZgatewayRF="RF"' # Enable RF gateway
|
||||
'-DZsensorBME280="BME280"' # Enable BME280 sensor
|
||||
```
|
||||
|
||||
### Web UI & WiFi Manager
|
||||
|
||||
- ESP32/ESP8266 boards include a WiFiManager for initial setup
|
||||
- Access point: SSID = `WifiManager_ssid`, Password = `WifiManager_password`
|
||||
- Web UI provides runtime configuration and control
|
||||
- Can disable WiFiManager with `ESPWifiManualSetup` flag
|
||||
|
||||
## Important Files
|
||||
|
||||
### Core Files
|
||||
|
||||
- **main/main.cpp** - Main application loop, MQTT handling, module coordination
|
||||
- **main/User_config.h** - Global configuration defaults
|
||||
- **main/webUI.cpp** - Web interface implementation
|
||||
- **main/mqttDiscovery.cpp** - Home Assistant MQTT discovery
|
||||
|
||||
### Gateway Files
|
||||
|
||||
- **main/gatewayBT.cpp** - BLE gateway (uses Theengs Decoder)
|
||||
- **main/gatewayRF.cpp** - RF 433MHz gateway (RCSwitch library)
|
||||
- **main/gatewayRF2.cpp** - Alternative RF implementation
|
||||
- **main/gatewayPilight.cpp** - Pilight protocol gateway
|
||||
- **main/gatewayRTL_433.cpp** - RTL_433 decoder integration
|
||||
- **main/gatewayIR.cpp** - Infrared gateway
|
||||
- **main/gatewayLORA.cpp** - LoRa gateway
|
||||
|
||||
### Configuration Files
|
||||
|
||||
All in `main/` directory:
|
||||
- **config_RF.h** - RF gateway configuration
|
||||
- **config_BT.h** - BLE gateway configuration
|
||||
- **config_IR.h** - IR gateway configuration
|
||||
- etc.
|
||||
|
||||
### Build System
|
||||
|
||||
- **platformio.ini** - Main PlatformIO config
|
||||
- **environments.ini** - All environment definitions (~3000+ lines)
|
||||
- **scripts/** - Python scripts for build automation
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a New Gateway Module
|
||||
|
||||
1. Create `gateway[NAME].cpp` in `main/`
|
||||
2. Create `config_[NAME].h` in `main/`
|
||||
3. Add build flag in `environments.ini`: `-DZgateway[NAME]="[NAME]"`
|
||||
4. Include conditional compilation: `#ifdef Zgateway[NAME]`
|
||||
5. Implement setup function: `setup[NAME]()`
|
||||
6. Implement loop function: `[NAME]toMQTT()`
|
||||
7. Optionally implement MQTT callback: `MQTTto[NAME]()`
|
||||
|
||||
### Adding a New Sensor Module
|
||||
|
||||
Similar to gateways, but prefix with `sensor` instead of `gateway` and `Z` becomes `Zsensor`.
|
||||
|
||||
### Configuration Override Pattern
|
||||
|
||||
```cpp
|
||||
#ifndef PARAM_NAME
|
||||
# define PARAM_NAME default_value
|
||||
#endif
|
||||
```
|
||||
|
||||
This allows platformio.ini build flags to override defaults.
|
||||
|
||||
### MQTT Publishing Pattern
|
||||
|
||||
```cpp
|
||||
pub(subjectToMQTT, data); // Basic publish
|
||||
pub_custom_topic(topic, data, retain); // Custom topic with retain
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# List all environments
|
||||
pio run --list-targets
|
||||
|
||||
# Build specific environment
|
||||
pio run -e esp32dev-ble
|
||||
|
||||
# Upload to device
|
||||
pio run -e esp32dev-ble -t upload
|
||||
|
||||
# Monitor serial
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
|
||||
- Uses clang-format with config in `.clang-format`
|
||||
- CI checks formatting automatically
|
||||
- Format before committing
|
||||
|
||||
### Testing
|
||||
|
||||
- Unit tests in `tests/` directory
|
||||
- CI runs builds for all environments
|
||||
- Nightly builds test comprehensive environment matrix
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation uses VuePress, located in `docs/`
|
||||
- Hosted at https://docs.openmqttgateway.com
|
||||
|
||||
## CI/CD
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
Located in `.github/workflows/`:
|
||||
- **build.yml** - Build validation on PRs
|
||||
- **nightly.yml** - Nightly builds of all environments
|
||||
- **format-check.yml** - Code formatting validation
|
||||
|
||||
### Build Matrix
|
||||
|
||||
The nightly build tests configurations:
|
||||
- ESP32 variants (ESP32, ESP32-S3, ESP32-C3)
|
||||
- ESP8266 variants (NodeMCU, Sonoff)
|
||||
- Different gateway combinations
|
||||
- Board-specific builds (M5Stack, Heltec, TTGO, etc.)
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Memory Constraints
|
||||
|
||||
- ESP8266 has limited RAM (~40KB free)
|
||||
- Use `PROGMEM` for constants
|
||||
- Minimize module combinations on ESP8266
|
||||
- ESP32 has more headroom (~200KB+ free)
|
||||
|
||||
### RF Module Conflicts
|
||||
|
||||
- Some RF implementations (RF, RF2, Pilight, RTL_433) may conflict
|
||||
- Use separate environments for different RF approaches
|
||||
- Check GPIO pin assignments in config files
|
||||
|
||||
### BLE on ESP32
|
||||
|
||||
- BLE stack uses significant RAM
|
||||
- Limit simultaneous BLE connections
|
||||
|
||||
## Related Projects
|
||||
|
||||
- **Theengs Decoder** - BLE device decoder library
|
||||
- **Theengs Gateway** - Python version for Raspberry Pi/PC
|
||||
- **RTL_433_ESP** - ESP32 port of RTL_433
|
||||
|
||||
## Resources
|
||||
|
||||
- **Documentation**: https://docs.openmqttgateway.com
|
||||
- **Community Forum**: https://community.openmqttgateway.com
|
||||
- **GitHub**: https://github.com/1technophile/OpenMQTTGateway
|
||||
- **Web Installer**: https://docs.openmqttgateway.com/upload/web-install.html
|
||||
|
||||
## Products
|
||||
|
||||
- **Theengs Bridge** - ESP32 BLE gateway with Ethernet
|
||||
- **Theengs Plug** - Smart plug with BLE gateway
|
||||
|
||||
## Conventions
|
||||
|
||||
### Naming
|
||||
|
||||
- Gateways: `gateway[NAME].cpp` with `Zgateway[NAME]` flag
|
||||
- Sensors: `sensor[NAME].cpp` with `Zsensor[NAME]` flag
|
||||
- Actuators: `actuator[NAME].cpp` with `Zactuator[NAME]` flag
|
||||
- Config files: `config_[NAME].h`
|
||||
|
||||
### Code Style
|
||||
|
||||
- 2-space indentation
|
||||
- Follow existing patterns in similar modules
|
||||
- Use preprocessor directives for module isolation
|
||||
- Comment complex RF protocols or BLE parsing
|
||||
|
||||
### Git Workflow
|
||||
|
||||
- Main branch: `development`
|
||||
- Feature branches from `development`
|
||||
- PRs target `development`
|
||||
- Squash commits when appropriate
|
||||
- Descriptive commit messages
|
||||
|
||||
## Quick Start for Contributors
|
||||
|
||||
1. Fork the repository
|
||||
2. Create `prod_env.ini` for your local environments
|
||||
3. Choose an environment from `environments.ini`
|
||||
4. Build: `pio run -e [environment-name]`
|
||||
5. Make changes following existing patterns
|
||||
6. Test build with your environment
|
||||
7. Run format check
|
||||
8. Submit PR to `development` branch
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### ESP32
|
||||
- Preferred platform for new development
|
||||
- More RAM and Flash
|
||||
- Supports BLE natively
|
||||
- Can use Ethernet via add-on boards
|
||||
|
||||
### ESP8266
|
||||
- Legacy support
|
||||
- Memory constrained
|
||||
- No native BLE
|
||||
- Good for simple RF/IR gateways
|
||||
|
||||
## Tips for AI Assistants
|
||||
|
||||
1. **Check environments.ini** when modifying build configurations
|
||||
2. **Follow existing module patterns** when adding new features
|
||||
3. **Test on relevant environments** - changes may affect multiple boards
|
||||
4. **Memory is precious** especially on ESP8266
|
||||
5. **MQTT topic structure** is critical for Home Assistant integration
|
||||
6. **Config headers** allow user customization without code changes
|
||||
7. **Use conditional compilation** to keep builds modular
|
||||
8. **WebUI changes** need both backend (webUI.cpp) and frontend (config_WebUI.h)
|
||||
9. **RF protocols** often need precise timing - be careful with changes
|
||||
10. **The main branch is `development`** - always target PRs there
|
||||
@@ -1,8 +1,5 @@
|
||||
[](https://community.openmqttgateway.com)
|
||||
|
||||

|
||||

|
||||
|
||||
[](https://community.openmqttgateway.com)
|
||||
|
||||
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.
|
||||
@@ -15,49 +12,39 @@ OpenMQTTGateway aims to unify various technologies and protocols into a single f
|
||||
|
||||
The documentation is [here](https://docs.openmqttgateway.com)
|
||||
|
||||
New to OpenMQTTGateway? Follow the [Getting Started guide](https://docs.openmqttgateway.com/getting-started.html) to flash a board from your browser and see your first sensor data in about 15 minutes.
|
||||
|
||||
The reference sheet, with the list of all functions, pinouts is [here](https://docs.google.com/spreadsheets/d/1_5fQjAixzRtepkykmL-3uN3G5bLfQ0zMajM9OBZ1bx0/edit#gid=0)
|
||||
|
||||
## Upload
|
||||
|
||||
Upload OpenMQTTGateway directly from the [upload page](https://docs.openmqttgateway.com/upload/web-install.html) (no additional software required) or [build your own configuration](https://docs.openmqttgateway.com/upload/builds.html) with [PlatformIO](https://platformio.org/) or [Arduino IDE](https://www.arduino.cc/en/software)
|
||||
Upload OpenMQTTGateway directly from the [upload page](https://docs.openmqttgateway.com/upload/web-install.html) (no additional software required) or [build your own configuration](https://docs.openmqttgateway.com/upload/builds.html) with [PlatformIO](https://platformio.org/).
|
||||
|
||||
## 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.
|
||||
Support open-source development through sponsorship, it helps keep the project healthy and evolving.
|
||||
|
||||
[](https://github.com/sponsors/theengs)
|
||||
[](https://github.com/sponsors/1technophile)
|
||||
|
||||
## Products powered by OpenMQTTGateway
|
||||
|
||||
### Theengs Bridge, Bluetooth gateway (BLE) with external antenna
|
||||
|
||||
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [90 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
|
||||
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [100 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
|
||||
|
||||
[](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
|
||||
|
||||
### Theengs Plug, Bluetooth gateway (BLE) gateway and Smart Plug
|
||||
|
||||
[Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption) brings the following features:
|
||||
* BLE to MQTT gateway, tens of [Bluetooth devices](https://compatible.openmqttgateway.com/index.php/devices/ble-devices/) supported thanks to Theengs Decoder library. The plug uses an ESP32 acting as a BLE to Wifi gateway to scan, decode and forward the data of the nearby sensors,
|
||||
* Smart plug that can be controlled remotely,
|
||||
* Energy consumption monitoring,
|
||||
* Device tracker,
|
||||
* Presence detection (beta),
|
||||
* Local connectivity first.
|
||||
|
||||
[](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
|
||||
|
||||
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) or the [Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
|
||||
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
|
||||
|
||||
## Compatible items
|
||||
|
||||
* [List of supported devices](https://compatible.openmqttgateway.com/index.php/devices/), door/window sensors, PIR sensors, smoke detectors, weather stations...
|
||||
* [List of supported BLE devices](https://decoder.theengs.io/devices/devices.html), thermometers, scales, plant sensors, beacons...
|
||||
|
||||
* [List of compatible boards (Off the shelf or DIY) is available](https://compatible.openmqttgateway.com/index.php/boards/), RF Bridge, IR, BLE gateways...
|
||||
* [Boards guide](https://docs.openmqttgateway.com/prerequisites/board.html), RF Bridge, IR, BLE gateways...
|
||||
|
||||
*Running on a computer*
|
||||
If you want to use the BLE decoding capabilities of OpenMQTTGateway with a Raspberry Pi, Windows or Unix PC you can now leverage [Theengs Gateway](https://theengs.github.io/gateway/).
|
||||
|
||||
* [List of compatible components to build your gateway](https://compatible.openmqttgateway.com/index.php/parts/), DHT, RF, IR emitters and receivers...
|
||||
* [Parts to build your gateway](https://docs.openmqttgateway.com/prerequisites/parts.html), DHT, RF, IR emitters and receivers...
|
||||
|
||||
## Compatible controllers, saas or software
|
||||
|
||||
@@ -82,7 +69,6 @@ For Questions or Support please don't open an issue, first go to the [docs](http
|
||||
If you like the project and/or used it please consider supporting it! It can be done in different ways:
|
||||
* 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 to the [community](https://community.openmqttgateway.com)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// docs/.vitepress/commonConfig.mts
|
||||
// Adapter that loads the shared Theengs commonConfig.js (CommonJS) downloaded
|
||||
// by CI from https://www.theengs.io/commonConfig.js, and re-exports the nav
|
||||
// and head arrays in a format VitePress can consume.
|
||||
//
|
||||
// This keeps theengs.io as the single source of truth for nav/head until all
|
||||
// Theengs docs sites are migrated to VitePress.
|
||||
|
||||
import { createRequire } from 'module'
|
||||
import { existsSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
const commonConfigPath = resolve(__dirname, 'public/commonConfig.js')
|
||||
|
||||
let sharedConfig: any = null
|
||||
|
||||
if (existsSync(commonConfigPath)) {
|
||||
sharedConfig = require(commonConfigPath)
|
||||
} else {
|
||||
console.warn(
|
||||
'commonConfig.js not found in .vitepress/public/. ' +
|
||||
'Using fallback nav/head. Run CI or download from https://www.theengs.io/commonConfig.js'
|
||||
)
|
||||
}
|
||||
|
||||
// Re-export nav from shared config, or use a minimal fallback
|
||||
export const commonNav = sharedConfig?.themeConfig?.nav ?? [
|
||||
{ text: 'Home', link: 'https://www.theengs.io' },
|
||||
{ text: 'Community', link: 'https://community.openmqttgateway.com' },
|
||||
{ text: 'Shop', link: 'https://shop.theengs.io/' }
|
||||
]
|
||||
|
||||
// Re-export head from shared config, or use a minimal fallback
|
||||
export const commonHead = sharedConfig?.head ?? [
|
||||
['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
|
||||
['meta', { name: 'theme-color', content: '#3eaf7c' }]
|
||||
]
|
||||
@@ -0,0 +1,926 @@
|
||||
<template>
|
||||
<div class="board-environment-list">
|
||||
<div v-if="loading" class="custom-block tip">
|
||||
<p class="custom-block-title">
|
||||
<span class="loading-spinner" aria-label="Loading"></span>
|
||||
Loading supported environments...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="custom-block danger">
|
||||
<p class="custom-block-title">Error</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && !error">
|
||||
<!-- Guided picker -->
|
||||
<div class="wizard">
|
||||
<span class="wizard-title">Help me choose</span>
|
||||
<div class="wizard-row">
|
||||
<span class="filter-group__label">I want to bridge:</span>
|
||||
<div class="filter-chips">
|
||||
<button
|
||||
v-for="b in bridgeOptions"
|
||||
:key="b.key"
|
||||
:class="['filter-chip', { 'filter-chip--active': wizardBridge === b.key }]"
|
||||
@click="wizardBridge = wizardBridge === b.key ? null : b.key">
|
||||
{{ b.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-row">
|
||||
<span class="filter-group__label">My hardware:</span>
|
||||
<div class="filter-chips">
|
||||
<button
|
||||
v-for="h in hardwareOptions"
|
||||
:key="h.key"
|
||||
:class="['filter-chip', { 'filter-chip--active': wizardHardware === h.key }]"
|
||||
@click="wizardHardware = wizardHardware === h.key ? null : h.key">
|
||||
{{ h.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="wizardBridge && recommendedBoard" class="wizard-result">
|
||||
<div class="wizard-result__text">
|
||||
<span>Recommended: <code>{{ recommendedBoard.environment }}</code></span>
|
||||
<span v-if="recommendedBoard.description" class="wizard-result__desc" v-html="recommendedBoard.description"></span>
|
||||
<span v-if="wizardHardware === 'none' && shoppingHint" class="wizard-result__desc">🛒 {{ shoppingHint }}</span>
|
||||
</div>
|
||||
<button class="wizard-result__cta" @click="openSelector(recommendedBoard.environment)">Flash it →</button>
|
||||
</div>
|
||||
<div v-else-if="wizardBridge && !recommendedBoard" class="wizard-result wizard-result--none">
|
||||
No pre-built environment matches this combination<template v-if="wizardBridge === 'ble' && wizardHardware === 'esp8266'"> — Bluetooth needs an ESP32</template>. Try another hardware choice, or <a :href="buildsUrl">build from source</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="filter-bar">
|
||||
<div class="search-box">
|
||||
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search by name, description, module..."
|
||||
class="search-input"
|
||||
aria-label="Search boards">
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
class="search-clear"
|
||||
@click="searchQuery = ''"
|
||||
aria-label="Clear search">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasActiveFilters" class="filter-status">
|
||||
<span>Showing {{ filteredBoards.length }} of {{ boards.length }} boards</span>
|
||||
<button class="filter-clear" @click="clearFilters">Clear all filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div v-if="filteredBoards.length === 0" class="no-results">
|
||||
<p>No boards match your filters.</p>
|
||||
<button class="filter-clear" @click="clearFilters">Clear all filters</button>
|
||||
</div>
|
||||
|
||||
<div class="boards-grid">
|
||||
<article
|
||||
v-for="board in filteredBoards"
|
||||
:key="board.environment"
|
||||
class="board-card"
|
||||
@click="openSelector(board.environment)">
|
||||
|
||||
<div class="board-card__image">
|
||||
<span v-if="popularEnvironments.includes(board.environment)" class="popular-badge">Popular</span>
|
||||
<img
|
||||
:src="getBoardImageUrl(board)"
|
||||
:alt="board.environment"
|
||||
loading="lazy">
|
||||
</div>
|
||||
|
||||
<div class="board-card__content">
|
||||
<div class="board-card__header">
|
||||
<h3 class="board-card__title">
|
||||
<code>{{ board.environment }}</code>
|
||||
</h3>
|
||||
<span v-if="board.microcontroller" class="board-card__chip">
|
||||
{{ board.microcontroller }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="board.description" class="board-card__description" v-html="board.description">
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Modules Section with expand/collapse -->
|
||||
<div v-if="Array.isArray(board.modules) && board.modules.length" class="board-card__libraries">
|
||||
<div class="modules-label-row">
|
||||
<span class="libraries-label">Modules:</span>
|
||||
<span
|
||||
v-if="board.modules.length > 2"
|
||||
class="expand-icon-btn"
|
||||
@click.stop="toggleModules(board.environment)"
|
||||
:title="expandedModules[board.environment] ? 'Hide' : 'Show all'">
|
||||
<svg :class="{rotated: expandedModules[board.environment]}" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<Transition name="expand-fade">
|
||||
<div class="libraries-badges"
|
||||
:style="expandedModules[board.environment] ? '' : 'max-height: 2.2em; overflow: hidden;'">
|
||||
<span
|
||||
v-for="(mod, index) in board.modules"
|
||||
:key="index"
|
||||
class="lib-badge">
|
||||
{{ mod }}
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Libraries Section with expand/collapse -->
|
||||
<div v-if="Array.isArray(board.libraries) && board.libraries.length" class="board-card__libraries">
|
||||
<div class="modules-label-row">
|
||||
<span class="libraries-label">Libraries:</span>
|
||||
<span
|
||||
v-if="board.libraries.length > 2"
|
||||
class="expand-icon-btn"
|
||||
@click.stop="toggleLibraries(board.environment)"
|
||||
:title="expandedLibraries[board.environment] ? 'Hide' : 'Show all'">
|
||||
<svg :class="{rotated: expandedLibraries[board.environment]}" width="19" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<Transition name="expand-fade">
|
||||
<div class="libraries-badges"
|
||||
:style="expandedLibraries[board.environment] ? '' : 'max-height: 2.2em; overflow: hidden;'">
|
||||
<span
|
||||
v-for="(lib, index) in board.libraries"
|
||||
:key="index"
|
||||
class="lib-badge">
|
||||
{{ lib }}
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="board-card__action">
|
||||
<span class="action-text">More info →</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
|
||||
interface Board {
|
||||
environment: string
|
||||
description?: string
|
||||
microcontroller?: string
|
||||
modules?: string[]
|
||||
libraries?: string[]
|
||||
customImg?: string
|
||||
CustomImg?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
boardsUrl?: string
|
||||
selectorPath?: string
|
||||
}>(), {
|
||||
boardsUrl: '/boards-info.json',
|
||||
selectorPath: '/upload/board-selector.html'
|
||||
})
|
||||
|
||||
const boards = ref<Board[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const expandedModules = reactive<Record<string, boolean>>({})
|
||||
const expandedLibraries = reactive<Record<string, boolean>>({})
|
||||
const searchQuery = ref('')
|
||||
const wizardBridge = ref<string | null>(null)
|
||||
const wizardHardware = ref<string | null>(null)
|
||||
|
||||
const bridgeOptions = [
|
||||
{ key: 'ble', label: 'Bluetooth devices' },
|
||||
{ key: 'rf433', label: 'RF devices (433/868/915MHz)' },
|
||||
{ key: 'ir', label: 'Infrared' },
|
||||
{ key: 'lora', label: 'LoRa' }
|
||||
]
|
||||
|
||||
const hardwareOptions = [
|
||||
{ key: 'esp32dev', label: 'Generic ESP32' },
|
||||
{ key: 'esp32c3', label: 'ESP32-C3' },
|
||||
{ key: 'esp32s3', label: 'ESP32-S3' },
|
||||
{ key: 'lilygo', label: 'LilyGo / Heltec / TTGO' },
|
||||
{ key: 'm5', label: 'M5Stack' },
|
||||
{ key: 'esp8266', label: 'ESP8266 / NodeMCU' },
|
||||
{ key: 'none', label: "I don't have hardware yet" }
|
||||
]
|
||||
|
||||
const popularEnvironments = [
|
||||
'esp32dev-ble',
|
||||
'theengs-bridge-v11',
|
||||
'lilygo-rtl_433',
|
||||
'lilygo-rtl_433-fsk',
|
||||
'esp32dev-ir'
|
||||
]
|
||||
|
||||
function buildUrl(path: string): string {
|
||||
const base = import.meta.env.BASE_URL || '/'
|
||||
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${cleanBase}${cleanPath}`
|
||||
}
|
||||
|
||||
const resolvedBoardsUrl = computed(() => buildUrl(props.boardsUrl))
|
||||
const resolvedSelectorUrl = computed(() => buildUrl(props.selectorPath))
|
||||
|
||||
function getMcuFamily(mcu: string | undefined): string | null {
|
||||
if (!mcu) return null
|
||||
const m = mcu.toLowerCase()
|
||||
if (m.includes('esp32-s3') || m.includes('esp32s3') || m.includes('atoms3') || m.includes('lilygo-t3-s3')) return 'ESP32-S3'
|
||||
if (m.includes('esp32-c3') || m.includes('esp32c3') || m.includes('lolin_c3') || m.includes('airm2m')) return 'ESP32-C3'
|
||||
if (m.includes('esp32') || m.includes('m5st') || m.includes('heltec') || m.includes('ttgo') || m.includes('lolin32') || m.includes('pico32') || m.includes('tinypico') || m.includes('feather')) return 'ESP32'
|
||||
if (m.includes('esp8') || m.includes('nodemcu')) return 'ESP8266'
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
function matchesBridge(board: Board, bridge: string): boolean {
|
||||
const mods = (board.modules || []).map(m => m.toLowerCase())
|
||||
switch (bridge) {
|
||||
case 'ble': return mods.some(m => m.includes('bt'))
|
||||
case 'rf433': return mods.some(m => m.includes('rf') || m.includes('rtl_433') || m.includes('pilight'))
|
||||
case 'ir': return mods.some(m => m.includes('ir'))
|
||||
case 'lora': return mods.some(m => m.includes('lora'))
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
function matchesHardware(board: Board, hardware: string): boolean {
|
||||
const haystack = `${board.environment} ${board.microcontroller || ''}`.toLowerCase()
|
||||
switch (hardware) {
|
||||
case 'esp32dev': return board.environment.toLowerCase().includes('esp32dev')
|
||||
case 'esp32c3': return getMcuFamily(board.microcontroller) === 'ESP32-C3'
|
||||
case 'esp32s3': return getMcuFamily(board.microcontroller) === 'ESP32-S3'
|
||||
case 'lilygo': return /lilygo|heltec|ttgo/.test(haystack)
|
||||
case 'm5': return haystack.includes('m5')
|
||||
case 'esp8266': return getMcuFamily(board.microcontroller) === 'ESP8266'
|
||||
default: return true // 'none': no hardware constraint
|
||||
}
|
||||
}
|
||||
|
||||
// Preferred defaults per bridge choice, consulted before the generic
|
||||
// popular-first order. All entries must be web-flashable environments.
|
||||
const preferredByBridge: Record<string, string[]> = {
|
||||
ble: ['esp32dev-ble', 'esp32c3-dev-c2-ble', 'esp32s3-dev-c1-ble', 'esp32-m5atom-lite', 'heltec-ble', 'lilygo-ble'],
|
||||
rf433: ['lilygo-rtl_433', 'esp32dev-rtl_433', 'heltec-rtl_433', 'nodemcuv2-rf'],
|
||||
ir: ['esp32dev-ir', 'nodemcuv2-ir', 'esp32-m5atom-lite'],
|
||||
lora: ['ttgo-lora32-v21', 'heltec-wifi-lora-32', 'ttgo-lora32-v1', 'ttgo-t-beam']
|
||||
}
|
||||
|
||||
const recommendedBoard = ref<Board | null>(null)
|
||||
const manifestCache = new Map<string, boolean>()
|
||||
|
||||
// Not every environment in boards-info.json has a pre-built firmware
|
||||
// manifest; only recommend ones that can actually be web-flashed.
|
||||
async function hasManifest(env: string): Promise<boolean> {
|
||||
const cached = manifestCache.get(env)
|
||||
if (cached !== undefined) return cached
|
||||
let ok = false
|
||||
try {
|
||||
const response = await fetch(buildUrl(`/firmware_build/${env}.manifest.json`), { method: 'HEAD' })
|
||||
ok = response.ok
|
||||
} catch {
|
||||
ok = false
|
||||
}
|
||||
manifestCache.set(env, ok)
|
||||
return ok
|
||||
}
|
||||
|
||||
let recommendationToken = 0
|
||||
watch([wizardBridge, wizardHardware, boards], async () => {
|
||||
const token = ++recommendationToken
|
||||
const bridge = wizardBridge.value
|
||||
if (!bridge) {
|
||||
recommendedBoard.value = null
|
||||
return
|
||||
}
|
||||
const candidates = boards.value.filter(b =>
|
||||
matchesBridge(b, bridge) &&
|
||||
(!wizardHardware.value || matchesHardware(b, wizardHardware.value))
|
||||
)
|
||||
const preferred = preferredByBridge[bridge] || []
|
||||
const ordered = [...candidates].sort((a, b) => {
|
||||
const ai = preferred.indexOf(a.environment)
|
||||
const bi = preferred.indexOf(b.environment)
|
||||
return (ai === -1 ? preferred.length : ai) - (bi === -1 ? preferred.length : bi)
|
||||
})
|
||||
for (const candidate of ordered.slice(0, 8)) {
|
||||
if (await hasManifest(candidate.environment)) {
|
||||
if (token === recommendationToken) recommendedBoard.value = candidate
|
||||
return
|
||||
}
|
||||
}
|
||||
if (token === recommendationToken) recommendedBoard.value = null
|
||||
})
|
||||
|
||||
const shoppingHint = computed<string>(() => {
|
||||
switch (wizardBridge.value) {
|
||||
case 'ble':
|
||||
case 'ir': return 'Any ESP32 development board will do.'
|
||||
case 'rf433': return 'A LILYGO LoRa32 or Heltec LoRa V2 board matching your devices\' frequency (433/868/915MHz) needs no soldering.'
|
||||
case 'lora': return 'Get a LILYGO LoRa32 or Heltec LoRa board matching the frequency used in your region.'
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
|
||||
const buildsUrl = computed(() => buildUrl('/upload/builds.html'))
|
||||
|
||||
const hasActiveFilters = computed(() =>
|
||||
searchQuery.value || wizardBridge.value || wizardHardware.value
|
||||
)
|
||||
|
||||
const filteredBoards = computed<Board[]>(() =>
|
||||
boards.value.filter(board => {
|
||||
if (wizardBridge.value && !matchesBridge(board, wizardBridge.value)) return false
|
||||
if (wizardHardware.value && wizardHardware.value !== 'none' && !matchesHardware(board, wizardHardware.value)) return false
|
||||
if (searchQuery.value) {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
const haystack = [
|
||||
board.environment,
|
||||
board.description || '',
|
||||
board.microcontroller || '',
|
||||
(board.modules || []).join(' ')
|
||||
].join(' ').toLowerCase()
|
||||
if (!haystack.includes(q)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
function clearFilters() {
|
||||
searchQuery.value = ''
|
||||
wizardBridge.value = null
|
||||
wizardHardware.value = null
|
||||
}
|
||||
|
||||
async function loadBoards() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch(resolvedBoardsUrl.value)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('boards-info.json must be an array')
|
||||
}
|
||||
boards.value = data
|
||||
.filter((board: Board) => board && typeof board.environment === 'string')
|
||||
.sort((a: Board, b: Board) => {
|
||||
const aPopular = popularEnvironments.includes(a.environment)
|
||||
const bPopular = popularEnvironments.includes(b.environment)
|
||||
if (aPopular && !bPopular) return -1
|
||||
if (!aPopular && bPopular) return 1
|
||||
return a.environment.localeCompare(b.environment)
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to load boards-info:', err)
|
||||
error.value = err instanceof Error ? err.message : 'Unable to load board information'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModules(env: string) {
|
||||
expandedModules[env] = !expandedModules[env]
|
||||
}
|
||||
|
||||
function toggleLibraries(env: string) {
|
||||
expandedLibraries[env] = !expandedLibraries[env]
|
||||
}
|
||||
|
||||
function openSelector(environment: string) {
|
||||
if (!environment) return
|
||||
const url = `${resolvedSelectorUrl.value}?env=${encodeURIComponent(environment)}`
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
function getBoardImageUrl(board: Board): string {
|
||||
const customImg = board.customImg || board.CustomImg
|
||||
if (customImg) {
|
||||
if (customImg.startsWith('http')) {
|
||||
return customImg
|
||||
}
|
||||
return buildUrl(customImg)
|
||||
}
|
||||
return buildUrl('/img/microcontroller.gif')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Preset the guided picker from query parameters, e.g. ?bridge=ble&hardware=esp32dev
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const bridgeParam = params.get('bridge')
|
||||
if (bridgeParam && bridgeOptions.some(b => b.key === bridgeParam)) {
|
||||
wizardBridge.value = bridgeParam
|
||||
}
|
||||
const hardwareParam = params.get('hardware')
|
||||
if (hardwareParam && hardwareOptions.some(h => h.key === hardwareParam)) {
|
||||
wizardHardware.value = hardwareParam
|
||||
}
|
||||
loadBoards()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.board-environment-list {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Guided picker */
|
||||
.wizard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.wizard-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.wizard-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wizard-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
padding-top: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wizard-result__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wizard-result__desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wizard-result__cta {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-white, #fff);
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 8px 18px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.wizard-result__cta:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.wizard-result--none {
|
||||
display: block;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.wizard-result--none a {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/* Search and Filter Bar */
|
||||
.filter-bar {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 2.2rem 0.6rem 2.4rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.15);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: var(--vp-c-text-2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.filter-groups {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group__label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.filter-chip--active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #ffffff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.filter-chip--active:hover {
|
||||
color: #ffffff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.filter-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vp-c-brand-1);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.boards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Card Design */
|
||||
.board-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.board-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/* Image Section */
|
||||
.board-card__image {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: #ff9800;
|
||||
color: #ffffff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.board-card__image img {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.board-card__content {
|
||||
padding: 1.25rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.board-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.board-card__title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.board-card__title code {
|
||||
background: transparent;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-size: 0.9em;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.board-card__chip {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.board-card__description {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
|
||||
}
|
||||
|
||||
/* Libraries Section */
|
||||
.board-card__libraries {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.modules-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.libraries-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.libraries-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.lib-badge {
|
||||
display: inline-block;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* Action Footer */
|
||||
.board-card__action {
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.board-card:hover .action-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
border: 2px solid #cfd8dc;
|
||||
border-top-color: var(--vp-c-brand-1);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.filter-groups {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.boards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.board-card__image {
|
||||
min-height: 100px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.board-card__image img {
|
||||
max-width: 80px;
|
||||
max-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.board-card__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.board-card__chip {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
/* Espansione animata per badge */
|
||||
.expand-fade-enter-active, .expand-fade-leave-active {
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
|
||||
}
|
||||
.expand-fade-enter-from, .expand-fade-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
.expand-fade-enter-to, .expand-fade-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.expand-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vp-c-brand-1);
|
||||
cursor: pointer;
|
||||
margin-top: 0.3em;
|
||||
margin-left: 0.2em;
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.expand-icon-btn:hover {
|
||||
background: rgba(62, 175, 124, 0.08);
|
||||
}
|
||||
.expand-icon-btn svg {
|
||||
transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.expand-icon-btn svg.rotated {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,730 @@
|
||||
<template>
|
||||
<div class="flash-selector">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="custom-block tip">
|
||||
<p class="custom-block-title">
|
||||
<span class="loading-spinner"></span>
|
||||
Loading environments...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-if="error" class="custom-block danger">
|
||||
<p class="custom-block-title">Error</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Board Details (when selected) -->
|
||||
<Transition name="fade">
|
||||
<div v-if="currentEnvironment && !loading" class="board-details">
|
||||
|
||||
<!-- Hero Section - Clean and Focused -->
|
||||
<div class="board-hero">
|
||||
<div class="board-hero__content">
|
||||
<!-- Inline Board Switcher -->
|
||||
<h2>
|
||||
<div class="board-switcher">
|
||||
<label for="envSelect" class="switcher-label">Environment:</label>
|
||||
<div class="switcher-select-wrapper">
|
||||
<select
|
||||
id="envSelect"
|
||||
v-model="selectedEnvironment"
|
||||
class="switcher-select">
|
||||
<option
|
||||
v-for="env in environments"
|
||||
:key="env.environment"
|
||||
:value="env.environment">
|
||||
{{ env.environment }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="switcher-icon">⚙️</span>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div v-if="currentEnvironment.microcontroller" class="board-hero__meta">
|
||||
<span class="meta-label">MCU:</span>
|
||||
<span class="meta-value">{{ currentEnvironment.microcontroller }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="currentEnvironment.description" class="board-hero__description">
|
||||
<span class="meta-label">Description:</span> <span v-html="currentEnvironment.description"></span>
|
||||
</p>
|
||||
|
||||
<!-- Primary Action -->
|
||||
<div class="hero-action">
|
||||
<esp-web-install-button
|
||||
v-if="manifestUrl"
|
||||
:manifest="manifestUrl"
|
||||
aria-label="Flash firmware to device">
|
||||
</esp-web-install-button>
|
||||
<button v-else class="connect-button connect-button--disabled" disabled>
|
||||
CONNECT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Optional -->
|
||||
<div v-if="boardImageUrl && !imageError" class="board-hero__image">
|
||||
<img
|
||||
:src="boardImageUrl"
|
||||
:alt="currentEnvironment.environment"
|
||||
loading="lazy"
|
||||
@error="handleImageError">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Details - Collapsible -->
|
||||
<details class="tech-details" open>
|
||||
<summary class="tech-details__summary">
|
||||
<span class="tech-details__icon">📋</span>
|
||||
<span class="tech-details__title">Technical Information</span>
|
||||
<span class="tech-details__toggle">▼</span>
|
||||
</summary>
|
||||
|
||||
<div class="tech-details__content">
|
||||
<div class="tech-grid">
|
||||
|
||||
<!-- Libraries -->
|
||||
<div v-if="currentEnvironment.libraries && currentEnvironment.libraries.length" class="tech-section">
|
||||
<h4 class="tech-section__title">Libraries</h4>
|
||||
<div class="tech-section__content">
|
||||
<div class="chip-group">
|
||||
<span v-for="(lib, index) in currentEnvironment.libraries" :key="index" class="chip">
|
||||
{{ lib }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Modules -->
|
||||
<div v-if="currentEnvironment.modules && currentEnvironment.modules.length" class="tech-section">
|
||||
<h4 class="tech-section__title">Gateway Modules</h4>
|
||||
<div class="tech-section__content">
|
||||
<div class="chip-group">
|
||||
<span v-for="(mod, index) in currentEnvironment.modules" :key="index" class="chip">
|
||||
{{ mod }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hardware -->
|
||||
<div v-if="currentEnvironment.hardware" class="tech-section">
|
||||
<h4 class="tech-section__title">Hardware</h4>
|
||||
<div class="tech-section__content" >
|
||||
<span class="chip" v-html="currentEnvironment.hardware"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Partitions -->
|
||||
<div v-if="currentEnvironment.partitions" class="tech-section">
|
||||
<h4 class="tech-section__title">Partitions</h4>
|
||||
<div class="tech-section__content">
|
||||
<code class="code-inline">{{ currentEnvironment.partitions }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Build Options -->
|
||||
<div v-if="currentEnvironment.options" class="tech-section tech-section--full">
|
||||
<h4 class="tech-section__title">Build Options</h4>
|
||||
<div class="tech-section__content">
|
||||
<pre class="code-block">{{ formatOptions(currentEnvironment.options) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Post-flash next steps -->
|
||||
<div class="next-steps">
|
||||
<h3 class="next-steps__title">✅ After flashing</h3>
|
||||
<ol class="next-steps__list">
|
||||
<li>Keep the board powered — it starts a WiFi access point named <code>OpenMQTTGateway</code> or starting with <code>OMG_</code>.</li>
|
||||
<li>From your phone or computer, connect to that WiFi network and follow the <a :href="portalUrl">configuration portal guide</a> to enter your WiFi and MQTT broker credentials.</li>
|
||||
<li>The gateway restarts, joins your network and connects to your broker — <a :href="verifyUrl">check that it works</a>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Transition>
|
||||
<!-- Board Detail not selected message -->
|
||||
<Transition name="fade">
|
||||
<div v-if="!currentEnvironment && !loading && !error" class="custom-block info">
|
||||
<p class="custom-block-title">Need to select one environment...</p>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
interface Environment {
|
||||
environment: string
|
||||
description?: string
|
||||
microcontroller?: string
|
||||
modules?: string[]
|
||||
libraries?: string[]
|
||||
hardware?: string
|
||||
partitions?: string
|
||||
options?: string | string[]
|
||||
customImg?: string
|
||||
CustomImg?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
boardsUrl?: string
|
||||
firmwareBaseUrl?: string
|
||||
}>(), {
|
||||
boardsUrl: '/boards-info.json',
|
||||
firmwareBaseUrl: '/firmware_build/'
|
||||
})
|
||||
|
||||
const environments = ref<Environment[]>([])
|
||||
const selectedEnvironment = ref<string>('')
|
||||
const loading = ref<boolean>(true)
|
||||
const error = ref<string | null>(null)
|
||||
const imageError = ref<boolean>(false)
|
||||
|
||||
const currentEnvironment = computed<Environment | undefined>(() => {
|
||||
return environments.value.find(env => env.environment === selectedEnvironment.value)
|
||||
})
|
||||
|
||||
const resolvedBoardsUrl = computed<string>(() => buildUrl(props.boardsUrl))
|
||||
|
||||
const resolvedFirmwareBaseUrl = computed<string>(() => buildUrl(props.firmwareBaseUrl))
|
||||
|
||||
const manifestUrl = computed<string | null>(() => {
|
||||
if (!selectedEnvironment.value) return null
|
||||
return `${resolvedFirmwareBaseUrl.value}${selectedEnvironment.value}.manifest.json`
|
||||
})
|
||||
|
||||
const portalUrl = computed<string>(() => buildUrl('/upload/portal.html'))
|
||||
const verifyUrl = computed<string>(() => buildUrl('/getting-started.html#step-4-check-that-it-works'))
|
||||
|
||||
const boardImageUrl = computed<string | null>(() => {
|
||||
if (!currentEnvironment.value) return null
|
||||
const customImg = currentEnvironment.value.customImg || currentEnvironment.value.CustomImg
|
||||
if (customImg && !imageError.value) {
|
||||
if (customImg.startsWith('http')) {
|
||||
return customImg
|
||||
}
|
||||
return buildUrl(customImg)
|
||||
}
|
||||
return buildUrl('/img/microcontroller.gif')
|
||||
})
|
||||
|
||||
watch(selectedEnvironment, () => {
|
||||
imageError.value = false
|
||||
error.value = null
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'module'
|
||||
script.src = 'https://unpkg.com/esp-web-tools@10/dist/web/install-button.js?module'
|
||||
document.head.appendChild(script)
|
||||
loadEnvironments()
|
||||
})
|
||||
|
||||
function buildUrl(path: string): string {
|
||||
const base = import.meta.env.BASE_URL || '/'
|
||||
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${cleanBase}${cleanPath}`
|
||||
}
|
||||
|
||||
function getQueryParam(param: string): string | null {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
return params.get(param)
|
||||
}
|
||||
|
||||
async function loadEnvironments(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(resolvedBoardsUrl.value)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid data format: expected an array')
|
||||
}
|
||||
|
||||
const validEnvironments = data.filter((env: unknown) =>
|
||||
env &&
|
||||
typeof env === 'object' &&
|
||||
(env as Environment).environment &&
|
||||
typeof (env as Environment).environment === 'string'
|
||||
) as Environment[]
|
||||
|
||||
if (validEnvironments.length === 0) {
|
||||
throw new Error('No valid environments found in data')
|
||||
}
|
||||
|
||||
environments.value = validEnvironments
|
||||
|
||||
const envParam = getQueryParam('env')
|
||||
if (envParam) {
|
||||
const matchingEnv = validEnvironments.find(env => env.environment === envParam)
|
||||
if (matchingEnv) {
|
||||
selectedEnvironment.value = envParam
|
||||
} else {
|
||||
console.warn(`Environment '${envParam}' not found in available environments`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load environments:', err)
|
||||
error.value = (err instanceof Error ? err.message : null) || 'Failed to load environments'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatOptions(optionsText: string | string[]): string {
|
||||
if (!optionsText) return ''
|
||||
let optionsTextMap: string[] = []
|
||||
if (typeof optionsText === 'string') {
|
||||
optionsTextMap = optionsText.split('\n')
|
||||
} else if (Array.isArray(optionsText)) {
|
||||
optionsTextMap = optionsText
|
||||
}
|
||||
|
||||
return optionsTextMap.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function handleImageError(): void {
|
||||
imageError.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Post-flash next steps */
|
||||
.next-steps {
|
||||
margin-top: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.next-steps__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.next-steps__list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.next-steps__list li {
|
||||
margin: 6px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.next-steps__list a {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.flash-selector {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Board Details Container */
|
||||
.board-details {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Hero Section - Material Design 3 */
|
||||
.board-hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 24px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px 0 var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.board-hero__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.board-hero__title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Board Switcher - Material Chip Style */
|
||||
.board-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.switcher-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.switcher-select-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.switcher-select {
|
||||
appearance: none;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: none;
|
||||
padding: 6px 32px 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
min-height: 32px;
|
||||
min-width: 280px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.switcher-select:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.switcher-select:focus {
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.switcher-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
pointer-events: none;
|
||||
font-size: 1rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.board-hero__meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.board-hero__description {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.board-hero__image {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.board-hero__image img {
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Primary Action - Material Filled Button */
|
||||
.hero-action {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.connect-button {
|
||||
background: #1976d2;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.125em;
|
||||
height: 40px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.connect-button:hover:not(:disabled) {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.connect-button:active:not(:disabled) {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.connect-button--disabled {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-3);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Technical Details - Material Surface */
|
||||
.tech-details {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px 0 var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.tech-details__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--vp-c-bg-alt);
|
||||
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tech-details__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tech-details__summary:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.tech-details__icon {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tech-details__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.tech-details__toggle {
|
||||
font-size: 0.875rem;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tech-details[open] .tech-details__toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.tech-details__content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Tech Grid */
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.tech-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tech-section--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.tech-section__title {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tech-section__content {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* Chips - Material Design */
|
||||
.chip-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
border: none;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Code Styles - Material Surface */
|
||||
.code-inline {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(62, 175, 124, 0.3);
|
||||
border-top-color: var(--vp-c-brand-1);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Fade Transition */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 968px) {
|
||||
.board-hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.board-hero__image {
|
||||
order: -1;
|
||||
max-width: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.selector-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.board-hero {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.board-hero__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.connect-button {
|
||||
width: 100%;
|
||||
padding: 0.9rem 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="omg-pipeline-wrap">
|
||||
<div class="omg-pipeline">
|
||||
<div class="omg-node">
|
||||
<span class="omg-node-icon">🌡️</span>
|
||||
<strong>Your devices</strong>
|
||||
<span class="omg-node-sub">BLE · RF · IR · LoRa</span>
|
||||
</div>
|
||||
<span class="omg-arrow">⇄</span>
|
||||
<div class="omg-node omg-node-brand">
|
||||
<span class="omg-node-icon">📟</span>
|
||||
<strong>OpenMQTTGateway</strong>
|
||||
<span class="omg-node-sub">ESP32 / ESP8266</span>
|
||||
</div>
|
||||
<span class="omg-arrow">⇄</span>
|
||||
<div class="omg-node">
|
||||
<span class="omg-node-icon">🔀</span>
|
||||
<strong>MQTT broker</strong>
|
||||
<span class="omg-node-sub">Mosquitto...</span>
|
||||
</div>
|
||||
<span class="omg-arrow">⇄</span>
|
||||
<div class="omg-node">
|
||||
<span class="omg-node-icon">🏠</span>
|
||||
<strong>Your controller</strong>
|
||||
<span class="omg-node-sub">Home Assistant, OpenHAB...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.omg-pipeline-wrap {
|
||||
container-type: inline-size;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.omg-pipeline {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.omg-node {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.omg-node-brand {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 1px var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.omg-node-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.omg-node strong {
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.omg-node-sub {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omg-arrow {
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Narrow containers (doc column, tablets): tighten the nodes, keep one row */
|
||||
@container (max-width: 760px) {
|
||||
.omg-pipeline {
|
||||
gap: 6px;
|
||||
}
|
||||
.omg-node {
|
||||
padding: 12px 6px;
|
||||
}
|
||||
.omg-node strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.omg-node-sub {
|
||||
font-size: 11px;
|
||||
}
|
||||
.omg-node-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
.omg-arrow {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Narrow (phones, small tablets): stack vertically. Below ~680px the four nodes
|
||||
no longer leave room for the longest label ("OpenMQTTGateway"), which spilled
|
||||
out of its box and over the arrows. */
|
||||
@container (max-width: 680px) {
|
||||
.omg-pipeline {
|
||||
flex-direction: column;
|
||||
}
|
||||
.omg-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.omg-node strong {
|
||||
font-size: 15px;
|
||||
}
|
||||
.omg-node-sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
// docs/.vitepress/config.mts
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { commonNav, commonHead } from './commonConfig.mts'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// Load metadata with override support
|
||||
import defaults from './defaults.json'
|
||||
let meta = { ...defaults }
|
||||
try {
|
||||
const overrides = await import('./meta.json')
|
||||
meta = { ...meta, ...overrides.default }
|
||||
} catch {
|
||||
console.warn('meta.json not found or not valid. Using default configuration.')
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
title: `${meta.title} - ${meta.version}`,
|
||||
description: 'One gateway, many technologies: MQTT gateway for ESP8266 or ESP32 with bidirectional 433mhz/315mhz/868mhz/915mhz RF, Infrared communications, BLE, LoRa, beacons detection, mi flora / mi jia / LYWSD02/ Mi Scale compatibility, SMS & LORA.',
|
||||
base: meta.url_prefix as '/' | `/${string}/`,
|
||||
outDir: resolve(__dirname, '../../', meta.dest),
|
||||
// Internal working documents, not part of the published site
|
||||
srcExclude: ['superpowers/**'],
|
||||
head: commonHead,
|
||||
ignoreDeadLinks: [
|
||||
/localhost/,
|
||||
/LoraTemperature/
|
||||
],
|
||||
lastUpdated: true,
|
||||
markdown: {
|
||||
lineNumbers: true
|
||||
},
|
||||
themeConfig: {
|
||||
siteTitle: 'OpenMQTTGateway',
|
||||
// @ts-expect-error custom fields consumed by index.md to render the dev-version banner
|
||||
mode: meta.mode,
|
||||
// @ts-expect-error
|
||||
version: meta.version,
|
||||
// @ts-expect-error
|
||||
repo: meta.theme_config_repo,
|
||||
nav: commonNav,
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: `https://github.com/${meta.theme_config_repo}` }
|
||||
],
|
||||
editLink: {
|
||||
pattern: `https://github.com/${meta.theme_config_repo}/edit/development/docs/:path`
|
||||
},
|
||||
sidebar: [
|
||||
{ text: 'What is it for', link: '/' },
|
||||
{ text: 'Getting Started', link: '/getting-started' },
|
||||
{
|
||||
text: '1 - Prerequisites',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Devices', link: '/prerequisites/devices' },
|
||||
{ text: 'Board', link: '/prerequisites/board' },
|
||||
{ text: 'Parts', link: '/prerequisites/parts' },
|
||||
{ text: 'Broker', link: '/prerequisites/broker' },
|
||||
{ text: 'Controller', link: '/prerequisites/controller' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '2 - Upload',
|
||||
collapsed: true,
|
||||
link: '/upload/',
|
||||
items: [
|
||||
{ text: '1. Web installer (easiest)', link: '/upload/web-install' },
|
||||
{ text: '2. Ready-to-go binaries', link: '/upload/binaries' },
|
||||
{ text: '3. Build from source', link: '/upload/builds' },
|
||||
{ text: 'Configuration portal', link: '/upload/portal' },
|
||||
{ text: 'Portal HTTP API', link: '/upload/portal-api' },
|
||||
{ text: 'Advanced Configuration', link: '/upload/advanced-configuration' },
|
||||
{ text: 'Troubleshoot', link: '/upload/troubleshoot' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '3 - Hardware setup',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'RF', link: '/setitup/rf' },
|
||||
{ text: 'BLE', link: '/setitup/ble' },
|
||||
{ text: 'IR', link: '/setitup/ir' },
|
||||
{ text: 'LoRa', link: '/setitup/lora' },
|
||||
{ text: 'GSM', link: '/setitup/gsm' },
|
||||
{ text: 'Serial', link: '/setitup/serial' },
|
||||
{ text: 'Sensors', link: '/setitup/sensors' },
|
||||
{ text: 'Actuators', link: '/setitup/actuators' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '4 - Use',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'RF', link: '/use/rf' },
|
||||
{ text: 'BLE', link: '/use/ble' },
|
||||
{ text: 'IR', link: '/use/ir' },
|
||||
{ text: 'LoRa', link: '/use/lora' },
|
||||
{ text: 'GSM', link: '/use/gsm' },
|
||||
{ text: 'Serial', link: '/use/serial' },
|
||||
{ text: 'RFM69', link: '/use/rfm69' },
|
||||
{ text: 'Sensors', link: '/use/sensors' },
|
||||
{ text: 'Actuators', link: '/use/actuators' },
|
||||
{ text: 'Boards', link: '/use/boards' },
|
||||
{ text: 'Displays', link: '/use/displays' },
|
||||
{ text: 'Gateway', link: '/use/gateway' },
|
||||
{ text: 'WebUI', link: '/use/webui' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '5 - Integrate',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'OpenHAB 2', link: '/integrate/openhab2' },
|
||||
{ text: 'OpenHAB 3', link: '/integrate/openhab3' },
|
||||
{ text: 'Home Assistant', link: '/integrate/home_assistant' },
|
||||
{ text: 'Node-RED', link: '/integrate/node_red' },
|
||||
{ text: 'AWS IoT', link: '/integrate/aws_iot' },
|
||||
{ text: 'Jeedom', link: '/integrate/jeedom' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '6 - Participate',
|
||||
collapsed: true,
|
||||
link: '/participate/',
|
||||
items: [
|
||||
{ text: 'Contributor Quick Start', link: '/participate/quick_start' },
|
||||
{ text: 'Support', link: '/participate/support' },
|
||||
{ text: 'Development', link: '/participate/development' },
|
||||
{ text: 'Adding Protocols', link: '/participate/adding-protocols' },
|
||||
{ text: 'Community', link: '/participate/community' },
|
||||
{ text: 'License', link: meta.url_license_file }
|
||||
]
|
||||
}
|
||||
],
|
||||
search: {
|
||||
provider: 'local'
|
||||
}
|
||||
},
|
||||
sitemap: {
|
||||
hostname: meta.hostname
|
||||
},
|
||||
vite: {
|
||||
publicDir: '.vitepress/public'
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 324 KiB |
@@ -0,0 +1,123 @@
|
||||
/* docs/.vitepress/theme/custom.css */
|
||||
|
||||
/* Prevent long site title from overlapping sidebar/nav */
|
||||
.VPNavBarTitle .title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The shared Theengs nav has seven top-level entries, and the default theme
|
||||
switches from the hamburger to the full nav bar at 768px -- far too early for
|
||||
this menu. Between 768px and ~1180px the entries overflowed: Shop, Sponsor,
|
||||
the appearance switch and the GitHub link ended up clipped or off-screen, and
|
||||
the page picked up a horizontal scrollbar. Keep the compact nav (hamburger +
|
||||
full-screen menu, which already lists every entry plus the appearance switch
|
||||
and social links) until the full menu genuinely fits. */
|
||||
@media (min-width: 768px) and (max-width: 1199px) {
|
||||
.VPNav .VPNavBar .VPNavBarMenu.menu,
|
||||
.VPNav .VPNavBar .VPNavBarExtra.extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .VPNavBarHamburger.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
header.VPNav .VPNavScreen {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Widen the entire content column on pages without aside (board/flash pages) */
|
||||
.VPDoc:not(.has-aside) .content-container,
|
||||
.VPDoc:not(.has-aside) .container {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-aside) .vp-doc {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Enlarge the home hero image beyond the theme's ~320px default cap */
|
||||
.VPHome .VPHero .image-container {
|
||||
transform: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.VPHome .VPHero .image-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPHome .VPHero .image-src {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
/* Below the desktop breakpoint the default theme stacks the image on top of the
|
||||
hero text and pulls it with negative margins (-76px -24px -48px), which only
|
||||
works because the image is normally absolutely positioned inside a fixed
|
||||
320px box. Since we let it flow, those margins made it bleed off-screen and
|
||||
collide with the hero title -- reset them. */
|
||||
@media (max-width: 959px) {
|
||||
.VPHome .VPHero .image {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* On phones the stacked image is a wall of unreadable logos that pushes the
|
||||
title, tagline and call-to-action buttons below the fold. Drop it and let the
|
||||
hero lead with the text. */
|
||||
@media (max-width: 639px) {
|
||||
.VPHome .VPHero .image {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHome .VPHero .image-src {
|
||||
max-width: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Indented (non-fenced) markdown code blocks render as a bare <pre>; the default
|
||||
theme only makes code scroll inside its language-* wrapper, so long lines such
|
||||
as the esptool.py commands pushed the whole page sideways on phones. */
|
||||
.vp-doc pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Embeds are written as raw HTML in markdown with fixed width/height attributes
|
||||
(e.g. <iframe width="560" height="315"> for YouTube), which pushes the whole
|
||||
page sideways on phones. */
|
||||
.vp-doc iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.vp-doc iframe[src*="youtube.com/embed"],
|
||||
.vp-doc iframe[src*="youtube-nocookie.com/embed"],
|
||||
.vp-doc iframe[src*="player.vimeo.com"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* Gray backdrop when ESP Web Tools install dialog is open */
|
||||
ewt-install-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// docs/.vitepress/theme/index.ts
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
import { onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
import './custom.css'
|
||||
import BoardEnvironmentTable from '../components/BoardEnvironmentTable.vue'
|
||||
import FlashEnvironmentSelector from '../components/FlashEnvironmentSelector.vue'
|
||||
import OmgPipeline from '../components/OmgPipeline.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('BoardEnvironmentTable', BoardEnvironmentTable)
|
||||
app.component('FlashEnvironmentSelector', FlashEnvironmentSelector)
|
||||
app.component('OmgPipeline', OmgPipeline)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
const initZoom = () => {
|
||||
mediumZoom('.vp-doc img:not(.no-zoom)', {
|
||||
background: 'var(--vp-c-bg)'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initZoom()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => nextTick(() => initZoom())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
const commonConfig = require('./public/commonConfig');
|
||||
|
||||
module.exports = {
|
||||
...commonConfig,
|
||||
title: 'Theengs OpenMQTTGateway version_tag',
|
||||
base: '/',
|
||||
description: 'One gateway, many technologies: MQTT gateway for ESP8266, ESP32, Sonoff RF Bridge or Arduino with bidirectional 433mhz/315mhz/868mhz, Infrared communications, BLE, 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'}]
|
||||
],
|
||||
themeConfig: {
|
||||
repo: '1technophile/OpenMQTTGateway',
|
||||
docsDir: 'docs',
|
||||
...commonConfig.themeConfig,
|
||||
sidebar: [
|
||||
['/','0 - What is it for 🏠'],
|
||||
{
|
||||
title: '1 - Prerequisites🧭', // required
|
||||
//collapsable: true, // optional, defaults to true
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'prerequisites/devices',
|
||||
'prerequisites/board',
|
||||
'prerequisites/parts',
|
||||
'prerequisites/broker',
|
||||
'prerequisites/controller']
|
||||
},
|
||||
{
|
||||
title: '2 - Set it up 🔨', // required
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'setitup/rf',
|
||||
'setitup/ble',
|
||||
'setitup/ir',
|
||||
'setitup/lora',
|
||||
'setitup/gsm',
|
||||
'setitup/rs232',
|
||||
'setitup/sensors',
|
||||
'setitup/actuators'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '3 - Upload ➡️', // required
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'upload/web-install',
|
||||
'upload/binaries',
|
||||
'upload/builds',
|
||||
'upload/gitpod',
|
||||
'upload/portal',
|
||||
'upload/advanced-configuration',
|
||||
'upload/troubleshoot'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '4 - Use ✈️', // required
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'use/rf',
|
||||
'use/ble',
|
||||
'use/ir',
|
||||
'use/lora',
|
||||
'use/gsm',
|
||||
'use/rs232',
|
||||
'use/rfm69',
|
||||
'use/sensors',
|
||||
'use/actuators',
|
||||
'use/boards',
|
||||
'use/displays',
|
||||
'use/gateway',
|
||||
'use/webui'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '5 - Integrate 🎉', // required
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'integrate/openhab2',
|
||||
'integrate/openhab3',
|
||||
'integrate/home_assistant',
|
||||
'integrate/node_red',
|
||||
'integrate/aws_iot',
|
||||
'integrate/jeedom'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '6 - Participate 💻', // required
|
||||
sidebarDepth: 1, // optional, defaults to 1
|
||||
children: [
|
||||
'participate/support',
|
||||
'participate/development',
|
||||
'participate/adding-protocols',
|
||||
'participate/community',
|
||||
['https://github.com/1technophile/OpenMQTTGateway/blob/development/LICENSE.txt','License']
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: {
|
||||
'sitemap': {
|
||||
hostname: 'https://docs.openmqttgateway.com',
|
||||
urls: [
|
||||
'https://decoder.theengs.io/devices/devices.html',
|
||||
'https://community.openmqttgateway.com/',
|
||||
'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',
|
||||
'https://app.theengs.io/',
|
||||
'https://gateway.theengs.io/',
|
||||
'https://decoder.theengs.io/',
|
||||
'https://parser.theengs.io/',
|
||||
'https://www.theengs.io/'
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
div.table-generated-page
|
||||
main
|
||||
div.content__default,
|
||||
footer.page-edit,
|
||||
div.page-nav
|
||||
// Display full with the API pages
|
||||
max-width: 65%
|
||||
|
||||
.sidebar
|
||||
z-index 6
|
||||
|
||||
.navbar
|
||||
z-index 6
|
||||
@@ -1,113 +0,0 @@
|
||||
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.
|
||||
|
||||

|
||||
|
||||
# What is an MQTT gateway or bridge ?
|
||||
|
||||
MQTT, short for Message Queuing Telemetry Transport, is a lightweight messaging protocol ideal for IoT devices.
|
||||
|
||||
An MQTT gateway or bridge plays a pivotal role in the MQTT ecosystem:
|
||||
* Protocol Translation: It converts non-MQTT protocols (like LoRa or Bluetooth) into MQTT, enabling broader network communication.
|
||||
* Data Aggregation: Combines data from multiple devices into single messages, optimizing network use.
|
||||
* Security: Incorporates features like SSL/TLS encryption to safeguard data during transmission.
|
||||
* Device Management: Handles tasks like firmware updates and remote configuration changes.
|
||||
|
||||
In essence, an MQTT gateway ensures smooth communication between devices and MQTT brokers, enhancing the efficiency and security of IoT systems.
|
||||
|
||||
# What OpenMQTTGateway can do ?
|
||||
|
||||
OpenMQTTGateway integrates with established technologies, such as 433mhz/315mhz protocols and infrared (IR), allowing you to upgrade and repurpose older devices. Additionally, OMG is compatible with modern technologies like Bluetooth Low Energy (BLE) and LoRa.
|
||||
|
||||
To have an overview of the supported PIR, door, water, temperature, smoke sensors, sirens, rings, beacons, switches & weather stations you can take a look to the
|
||||
[compatible devices list](https://compatible.openmqttgateway.com/index.php/devices)
|
||||
|
||||
You can run OpenMQTTGateway on a wide variety of [boards](https://compatible.openmqttgateway.com/index.php/boards/), ESP32, ESP8266, Arduino MEGA, UNO (with limitation).
|
||||
BLE to MQTT gateway can also run on Raspberry Pi, Windows or Unix computers thanks to [Theengs Gateway](https://theengs.github.io/gateway/).
|
||||
|
||||
Using MQTT, you can seamlessly integrate with home automation platforms such as OpenHAB, Home Assistant, and others, or with IoT software like Node-Red.
|
||||
|
||||
# Use cases
|
||||
Leveraging OpenMQTTGateway with a controller allows you to:
|
||||
* Monitor a garden with a Mi Flora BLE sensor and control an irrigation valve depending on the soil moisture,
|
||||
* Trigger a fan depending on the temperature and humidity thanks to a Mi Jia/LYWSD03MMC BLE sensor,
|
||||
* Follow your meat temperature when cooking with an Inkbird IBBQ
|
||||
* Alert yourself by a controller notification if the temperature of a fridge or freezer is too high,
|
||||
* Detect a beacon/smartwatch so as to trigger a special scenario when you come home,
|
||||
* [Lose weight with the help of a complete log system](https://www.youtube.com/watch?v=noUROhtf0E0&t=18s), video from [@Andreas Spiess](https://www.youtube.com/channel/UCu7_D0o48KbfhpEohoP7YSQ)
|
||||
* Detect opened door or windows through 433mhz or BLE and alert yourself when leaving
|
||||
* Detect water leakage or smoke remotely
|
||||
* Actionate a siren if something is going wrong
|
||||
* Detect if your far mailbox has been opened by the postman with LoRa
|
||||
* Make smart your old TV or AC system through infrared control
|
||||
* Monitor vehicle tire pressure
|
||||
|
||||
The limit is your imagination 😀
|
||||
|
||||
# Functions
|
||||
Under the hood, OpenMQTTGateway offers features such as:
|
||||
* Deduplication
|
||||
* Simple and lite API
|
||||
* Strong integrations with libraries used
|
||||
* Signal forward/repeat
|
||||
* Wifi web portal onboarding
|
||||
* Web portal configuration
|
||||
* Whitelist & Blacklist management
|
||||
* Secure connections
|
||||
* Over the air updates
|
||||
* Local or cloud, your choice
|
||||
|
||||
## 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.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<iframe src="https://github.com/sponsors/theengs/button" title="Sponsor Theengs" height="32" width="228" style="border: 0; border-radius: 6px;"></iframe>
|
||||
</div>
|
||||
|
||||
## Products powered by OpenMQTTGateway
|
||||
|
||||
### Theengs Bridge, BLE gateway with external antenna
|
||||
|
||||
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [90 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
|
||||
|
||||
[](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
|
||||
|
||||
### Theengs Plug, BLE gateway and Smart Plug
|
||||
|
||||
[Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption) brings the following features:
|
||||
* BLE to MQTT gateway, tens of [Bluetooth devices](https://compatible.openmqttgateway.com/index.php/devices/ble-devices/) supported thanks to Theengs Decoder library. The plug uses an ESP32 acting as a BLE to Wifi gateway to scan, decode and forward the data of the nearby sensors,
|
||||
* Smart plug that can be controlled remotely,
|
||||
* Energy consumption monitoring,
|
||||
* Device tracker,
|
||||
* Presence detection (beta),
|
||||
* Local connectivity first.
|
||||
|
||||
[](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
|
||||
|
||||
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) or the [Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
|
||||
|
||||
## Media
|
||||
|
||||
* [Hackaday - ARDUINO LIBRARY BRINGS RTL_433 TO THE ESP32](https://hackaday.com/2023/01/13/arduino-library-brings-rtl_433-to-the-esp32)
|
||||
* [CNX Software - 433 MHz is not dead! Using an ESP32 board with LoRa module to talk to 433 MHz sensors](https://www.cnx-software.com/2023/01/14/esp32-board-with-lora-433-mhz-sensors/)
|
||||
* [RTL_433 PORTED TO ESP32 MICROCONTROLLERS WITH CC1101 OR SX127X TRANSCEIVER CHIPS](https://www.rtl-sdr.com/rtl_433-ported-to-esp32-microcontrollers-with-cc1101-or-sx127x-transceiver-chips/)
|
||||
* [Using low-cost wireless sensors in the unlicensed bands](https://lwn.net/Articles/921497/)
|
||||
* [SMART PLUG ESP32 OPENMQTTGATEWAY SERVING AS AN BLE MQTT GATEWAY AND A POWER METER](https://www.electronics-lab.com/smart-plug-esp32-openmqttgateway-serving-as-an-ble-mqtt-gateway-and-a-power-meter/)
|
||||
|
||||
### Theengs Plug
|
||||
[](https://www.youtube.com/watch?v=nUwMt9p2U7o&t=427s)
|
||||
|
||||
### 433Mhz and BLE
|
||||
[](https://www.youtube.com/watch?v=_gdXR1uklaY)
|
||||
|
||||
### BLE
|
||||
[](https://www.youtube.com/watch?v=noUROhtf0E0)
|
||||
|
||||
### 433Mhz
|
||||
[](https://www.youtube.com/watch?v=H-JXWbWjJYE)
|
||||
|
||||
### LORA
|
||||
[](https://www.youtube.com/watch?v=6DftaHxDawM)
|
||||
|
||||
::: warning Note
|
||||
The material and information contained in this documentation is for general information purposes only. You should not rely upon the material or information on this documentation as a basis for making any business, legal or any other decisions. There is no warranty given on this documentation content. If you decide to follow the information and materials given it is at your own risk. I will not be liable for any false, inaccurate, inappropriate or incomplete information presented on this website.
|
||||
:::
|
||||
@@ -0,0 +1,73 @@
|
||||
# Getting Started
|
||||
|
||||
This guide walks you through the two most popular setups from start to finish, flashed **from your browser** and integrated with **Home Assistant**. No soldering, no development environment, about 15 minutes:
|
||||
|
||||
* **Bluetooth sensors** — any ESP32 board reading thermometers, plant sensors, scales...
|
||||
* **RF devices (433/315/868/915MHz)** — a LILYGO LoRa32 board receiving weather stations, door sensors, PIR sensors...
|
||||
|
||||
Using another controller (OpenHAB, Node-RED...) or another technology (IR, LoRa)? Follow the same steps and take the detours linked along the way.
|
||||
|
||||
## How the pieces fit together
|
||||
|
||||
OpenMQTTGateway (OMG) is a firmware for your board that translates signals from your devices (Bluetooth sensors, RF remotes, weather stations...) into MQTT messages, and the other way around:
|
||||
|
||||
<OmgPipeline />
|
||||
|
||||
The **broker** is the message hub; the **controller** is the home automation software where you see and use the data.
|
||||
|
||||
## What you need
|
||||
|
||||
* **A board** — for **Bluetooth**, any ESP32 development board. For **RF**, a LILYGO® LoRa32 V2.1 or a Heltec LoRa V2 matching your devices' frequency — 433MHz for most European devices, 915MHz in North America (no soldering needed). See the [board guide](prerequisites/board.md) for other cases, or get a pre-flashed [Theengs Bridge](https://shop.theengs.io/) and skip the flashing step.
|
||||
* **A data USB cable** — some cables are charge-only and won't work.
|
||||
* **An MQTT broker** — see Step 1.
|
||||
* **A device to observe** — e.g. a [compatible Bluetooth sensor](https://decoder.theengs.io/devices/devices.html) such as a Mi Flora, LYWSD03MMC, Govee or Inkbird thermometer, or any RF sensor around you (weather station, door or PIR sensor...).
|
||||
|
||||
## Step 1 — Set up the MQTT broker
|
||||
|
||||
If you use **Home Assistant**, the simplest option is the Mosquitto add-on:
|
||||
|
||||
1. In Home Assistant go to **Settings → Add-ons → Add-on store**, install **Mosquitto broker** and start it.
|
||||
2. Install the **MQTT integration** (**Settings → Devices & services → Add integration → MQTT**) if it isn't set up already.
|
||||
3. Create a dedicated user for the gateway in **Settings → People → Users** (e.g. `omg` — it doesn't need to be an administrator). The gateway will log in with it.
|
||||
|
||||
No Home Assistant? Install [Mosquitto](https://mosquitto.org/) on any always-on machine, or see the [broker page](prerequisites/broker.md) for alternatives.
|
||||
|
||||
## Step 2 — Flash the firmware from your browser
|
||||
|
||||
1. Open the [web installer](upload/web-install.md) in **Chrome, Edge or Opera** (Firefox and Safari don't support web flashing).
|
||||
2. Connect the board to your computer with the USB cable.
|
||||
3. Pick the firmware matching your board and use case — the two most flashed:
|
||||
* **Bluetooth** on a generic ESP32: <a href="./upload/board-selector.html?env=esp32dev-ble"><strong>esp32dev-ble</strong></a>
|
||||
* **RF** on a LILYGO LoRa32: <a href="./upload/board-selector.html?env=lilygo-rtl_433"><strong>lilygo-rtl_433</strong></a> (the radio frequency is configurable afterwards)
|
||||
4. Click **Connect**, select the board's USB port, and let the installer finish.
|
||||
|
||||
Prefer a desktop tool, or need a custom configuration? See the other [upload options](upload/index.md).
|
||||
|
||||
## Step 3 — Connect the gateway to your WiFi and broker
|
||||
|
||||
After flashing, the gateway starts its own WiFi access point:
|
||||
|
||||
1. From your phone or computer, connect to the WiFi network named **OpenMQTTGateway** or starting with **OMG_** (no password, except for Theengs devices — see the [portal page](upload/portal.md)).
|
||||
2. A configuration page opens (on Android, accept "stay connected without internet"). Click **Configure WiFi**.
|
||||
3. Select your WiFi network and enter its password.
|
||||
4. Enter your broker's address as MQTT server (for the Home Assistant add-on, `homeassistant.local` or the server IP), keep port `1883`, and fill in the MQTT username and password from Step 1.
|
||||
5. Click **Save**. The gateway restarts and joins your network.
|
||||
|
||||
The full list of portal fields is described on the [configuration portal page](upload/portal.md).
|
||||
|
||||
## Step 4 — Check that it works
|
||||
|
||||
* In Home Assistant, go to **Settings → Devices & services → MQTT**: after a minute you should see a device named **OpenMQTTGateway**, created automatically through MQTT discovery.
|
||||
* **Bluetooth**: bring a compatible sensor nearby — it appears as its own device with its measurements as entities. Some devices only advertise on an event: press a button or trigger a reading if nothing shows up.
|
||||
* **RF**: supported devices appear automatically as soon as they transmit — weather stations send every minute or two, door and PIR sensors on an event, so trigger one to see it arrive.
|
||||
* You can also watch the raw messages with [MQTT Explorer](https://mqtt-explorer.com/): connect to your broker and look for `home/OpenMQTTGateway/LWT` = `online` and messages under `home/OpenMQTTGateway/`.
|
||||
|
||||
Something not right? Head to the [troubleshooting page](upload/troubleshoot.md) or ask on the [community forum](https://community.openmqttgateway.com).
|
||||
|
||||
## Where to go next
|
||||
|
||||
* **More RF options** — other <a href="./upload/web-install.html?bridge=rf433">compatible boards and firmwares</a>, transmitting, frequency tuning, and protocol details in [RF hardware setup](setitup/rf.md) and [RF usage](use/rf.md).
|
||||
* **Infrared** — wire an IR emitter/receiver following the [IR hardware setup](setitup/ir.md).
|
||||
* **Tune the gateway** — the [WebUI](use/webui.md) and [gateway commands](use/gateway.md) let you configure it at runtime over MQTT.
|
||||
* **Other controllers** — [OpenHAB](integrate/openhab3.md), [Node-RED](integrate/node_red.md), [Jeedom](integrate/jeedom.md), [AWS IoT](integrate/aws_iot.md).
|
||||
* **Full Home Assistant details** — [Home Assistant integration](integrate/home_assistant.md).
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |