+1
-1
@@ -38,4 +38,4 @@ cp bin/device-install.* $OUTDIR/
|
||||
cp bin/device-update.* $OUTDIR/
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
|
||||
+1
-1
@@ -49,4 +49,4 @@ if (echo $1 | grep -q "rak4631"); then
|
||||
fi
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
|
||||
+1
-1
@@ -30,4 +30,4 @@ echo "Copying uf2 file"
|
||||
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
|
||||
@@ -30,4 +30,4 @@ echo "Copying STM32 bin file"
|
||||
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 2
|
||||
Busy: 4
|
||||
RXen: 1
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
SX126X_MAX_POWER: 22
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
INSTANCE=$1
|
||||
CONF_DIR="/etc/meshtasticd/config.d"
|
||||
VFS_DIR="/var/lib"
|
||||
|
||||
# If no instance ID provided, start bare daemon and exit
|
||||
echo "no instance ID provided, starting bare meshtasticd service"
|
||||
if [ -z "${INSTANCE}" ]; then
|
||||
/usr/bin/meshtasticd
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Make VFS dir if it does not exist
|
||||
if [ ! -d "${VFS_DIR}/meshtasticd-${INSTANCE}" ]; then
|
||||
echo "vfs for ${INSTANCE} does not exist, creating it."
|
||||
mkdir "${VFS_DIR}/meshtasticd-${INSTANCE}"
|
||||
fi
|
||||
|
||||
# Abort if config for $INSTANCE does not exist
|
||||
if [ ! -f "${CONF_DIR}/config-${INSTANCE}.yaml" ]; then
|
||||
echo "no config for ${INSTANCE} found in ${CONF_DIR}. refusing to start" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start meshtasticd with instance parameters
|
||||
printf "starting meshtasticd-%s..., ${INSTANCE}"
|
||||
if /usr/bin/meshtasticd --config="${CONF_DIR}/config-${INSTANCE}.yaml" --fsdir="${VFS_DIR}/meshtasticd-${INSTANCE}"; then
|
||||
echo "ok"
|
||||
else
|
||||
echo "failed"
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Meshtastic Native Daemon
|
||||
Description=Meshtastic %i Daemon
|
||||
After=network-online.target
|
||||
StartLimitInterval=200
|
||||
StartLimitBurst=5
|
||||
@@ -9,7 +9,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
User=meshtasticd
|
||||
Group=meshtasticd
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/meshtasticd
|
||||
ExecStart=/usr/bin/meshtasticd-start.sh %i
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd
|
||||
cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh
|
||||
mkdir -p /etc/meshtasticd
|
||||
if [[ -f "/etc/meshtasticd/config.yaml" ]]; then
|
||||
cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.18" date="2026-01-02">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
|
||||
</release>
|
||||
<release version="2.7.17" date="2025-11-28">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
|
||||
</release>
|
||||
|
||||
+147
-14
@@ -2,11 +2,12 @@
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||
import sys
|
||||
from os.path import join, basename, isfile
|
||||
from os.path import join
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from readprops import readProps
|
||||
|
||||
@@ -14,10 +15,50 @@ Import("env")
|
||||
platform = env.PioPlatform()
|
||||
progname = env.get("PROGNAME")
|
||||
lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
|
||||
manifest_ran = False
|
||||
|
||||
def infer_architecture(board_cfg):
|
||||
try:
|
||||
mcu = board_cfg.get("build.mcu") if board_cfg else None
|
||||
except KeyError:
|
||||
mcu = None
|
||||
except Exception:
|
||||
mcu = None
|
||||
if not mcu:
|
||||
return None
|
||||
mcu_l = str(mcu).lower()
|
||||
if "esp32s3" in mcu_l:
|
||||
return "esp32-s3"
|
||||
if "esp32c6" in mcu_l:
|
||||
return "esp32-c6"
|
||||
if "esp32c3" in mcu_l:
|
||||
return "esp32-c3"
|
||||
if "esp32" in mcu_l:
|
||||
return "esp32"
|
||||
if "rp2040" in mcu_l:
|
||||
return "rp2040"
|
||||
if "rp2350" in mcu_l:
|
||||
return "rp2350"
|
||||
if "nrf52" in mcu_l or "nrf52840" in mcu_l:
|
||||
return "nrf52840"
|
||||
if "stm32" in mcu_l:
|
||||
return "stm32"
|
||||
return None
|
||||
|
||||
def manifest_gather(source, target, env):
|
||||
global manifest_ran
|
||||
if manifest_ran:
|
||||
return
|
||||
# Skip manifest generation if we cannot determine architecture (host/native builds)
|
||||
board_arch = infer_architecture(env.BoardConfig())
|
||||
if not board_arch:
|
||||
print(f"Skipping mtjson generation for unknown architecture (env={env.get('PIOENV')})")
|
||||
manifest_ran = True
|
||||
return
|
||||
manifest_ran = True
|
||||
out = []
|
||||
board_platform = env.BoardConfig().get("platform")
|
||||
board_mcu = env.BoardConfig().get("build.mcu").lower()
|
||||
needs_ota_suffix = board_platform == "nordicnrf52"
|
||||
check_paths = [
|
||||
progname,
|
||||
@@ -29,7 +70,9 @@ def manifest_gather(source, target, env):
|
||||
f"{progname}.uf2",
|
||||
f"{progname}.factory.uf2",
|
||||
f"{progname}.zip",
|
||||
lfsbin
|
||||
lfsbin,
|
||||
f"mt-{board_mcu}-ota.bin",
|
||||
"bleota-c3.bin"
|
||||
]
|
||||
for p in check_paths:
|
||||
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
|
||||
@@ -47,14 +90,39 @@ def manifest_gather(source, target, env):
|
||||
manifest_write(out, env)
|
||||
|
||||
def manifest_write(files, env):
|
||||
# Defensive: also skip manifest writing if we cannot determine architecture
|
||||
def get_project_option(name):
|
||||
try:
|
||||
return env.GetProjectOption(name)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_project_option_any(names):
|
||||
for name in names:
|
||||
val = get_project_option(name)
|
||||
if val is not None:
|
||||
return val
|
||||
return None
|
||||
|
||||
def as_bool(val):
|
||||
return str(val).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
def as_int(val):
|
||||
try:
|
||||
return int(str(val), 10)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def as_list(val):
|
||||
return [item.strip() for item in str(val).split(",") if item.strip()]
|
||||
|
||||
manifest = {
|
||||
"version": verObj["long"],
|
||||
"build_epoch": build_epoch,
|
||||
"board": env.get("PIOENV"),
|
||||
"platformioTarget": env.get("PIOENV"),
|
||||
"mcu": env.get("BOARD_MCU"),
|
||||
"repo": repo_owner,
|
||||
"files": files,
|
||||
"part": None,
|
||||
"has_mui": False,
|
||||
"has_inkhud": False,
|
||||
}
|
||||
@@ -69,6 +137,51 @@ def manifest_write(files, env):
|
||||
if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
|
||||
manifest["has_inkhud"] = True
|
||||
|
||||
pioenv = env.get("PIOENV")
|
||||
device_meta = {}
|
||||
device_meta_fields = [
|
||||
("hwModel", ["custom_meshtastic_hw_model"], as_int),
|
||||
("hwModelSlug", ["custom_meshtastic_hw_model_slug"], str),
|
||||
("architecture", ["custom_meshtastic_architecture"], str),
|
||||
("activelySupported", ["custom_meshtastic_actively_supported"], as_bool),
|
||||
("displayName", ["custom_meshtastic_display_name"], str),
|
||||
("supportLevel", ["custom_meshtastic_support_level"], as_int),
|
||||
("images", ["custom_meshtastic_images"], as_list),
|
||||
("tags", ["custom_meshtastic_tags"], as_list),
|
||||
("requiresDfu", ["custom_meshtastic_requires_dfu"], as_bool),
|
||||
("partitionScheme", ["custom_meshtastic_partition_scheme"], str),
|
||||
("url", ["custom_meshtastic_url"], str),
|
||||
("key", ["custom_meshtastic_key"], str),
|
||||
("variant", ["custom_meshtastic_variant"], str),
|
||||
]
|
||||
|
||||
|
||||
for manifest_key, option_keys, caster in device_meta_fields:
|
||||
raw_val = get_project_option_any(option_keys)
|
||||
if raw_val is None:
|
||||
continue
|
||||
parsed = caster(raw_val) if callable(caster) else raw_val
|
||||
if parsed is not None and parsed != "":
|
||||
device_meta[manifest_key] = parsed
|
||||
|
||||
# Determine architecture once; if we can't infer it, skip manifest generation
|
||||
board_arch = device_meta.get("architecture") or infer_architecture(env.BoardConfig())
|
||||
if not board_arch:
|
||||
print(f"Skipping mtjson write for unknown architecture (env={env.get('PIOENV')})")
|
||||
return
|
||||
|
||||
device_meta["architecture"] = board_arch
|
||||
|
||||
# Always set requiresDfu: true for nrf52840 targets
|
||||
if board_arch == "nrf52840":
|
||||
device_meta["requiresDfu"] = True
|
||||
|
||||
device_meta.setdefault("displayName", pioenv)
|
||||
device_meta.setdefault("activelySupported", False)
|
||||
|
||||
if device_meta:
|
||||
manifest.update(device_meta)
|
||||
|
||||
# Write the manifest to the build directory
|
||||
with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
@@ -166,8 +279,12 @@ def load_boot_logo(source, target, env):
|
||||
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||
env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
|
||||
|
||||
mtjson_deps = ["buildprog"]
|
||||
if platform.name == "espressif32":
|
||||
board_arch = infer_architecture(env.BoardConfig())
|
||||
should_skip_manifest = board_arch is None
|
||||
|
||||
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
|
||||
mtjson_deps = [] if should_skip_manifest else ["buildprog"]
|
||||
if not should_skip_manifest and platform.name == "espressif32":
|
||||
# Build littlefs image as part of mtjson target
|
||||
# Equivalent to `pio run -t buildfs`
|
||||
target_lfs = env.DataToBin(
|
||||
@@ -175,11 +292,27 @@ if platform.name == "espressif32":
|
||||
)
|
||||
mtjson_deps.append(target_lfs)
|
||||
|
||||
env.AddCustomTarget(
|
||||
name="mtjson",
|
||||
dependencies=mtjson_deps,
|
||||
actions=[manifest_gather],
|
||||
title="Meshtastic Manifest",
|
||||
description="Generating Meshtastic manifest JSON + Checksums",
|
||||
always_build=False,
|
||||
)
|
||||
if should_skip_manifest:
|
||||
def skip_manifest(source, target, env):
|
||||
print(f"mtjson: skipped for native environment: {env.get('PIOENV')}")
|
||||
|
||||
env.AddCustomTarget(
|
||||
name="mtjson",
|
||||
dependencies=mtjson_deps,
|
||||
actions=[skip_manifest],
|
||||
title="Meshtastic Manifest (skipped)",
|
||||
description="mtjson generation is skipped for native environments",
|
||||
always_build=True,
|
||||
)
|
||||
else:
|
||||
env.AddCustomTarget(
|
||||
name="mtjson",
|
||||
dependencies=mtjson_deps,
|
||||
actions=[manifest_gather],
|
||||
title="Meshtastic Manifest",
|
||||
description="Generating Meshtastic manifest JSON + Checksums",
|
||||
always_build=True,
|
||||
)
|
||||
|
||||
# Run manifest generation as part of the default build pipeline for non-native builds.
|
||||
env.Default("mtjson")
|
||||
|
||||
+2
-1
@@ -18,8 +18,9 @@ def readProps(prefsLoc):
|
||||
|
||||
# Try to find current build SHA if if the workspace is clean. This could fail if git is not installed
|
||||
try:
|
||||
# Pin abbreviation length to keep local builds and CI matching (avoid auto-shortening)
|
||||
sha = (
|
||||
subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||
subprocess.check_output(["git", "rev-parse", "--short=7", "HEAD"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from github import Github
|
||||
|
||||
def parseFile(path):
|
||||
with open(path, "r") as f:
|
||||
data = json.loads(f)
|
||||
for file in data["files"]:
|
||||
if file["name"].endswith(".bin"):
|
||||
return file["name"], file["bytes"]
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
|
||||
sys.exit(1)
|
||||
|
||||
pr_number = int(sys.argv[1])
|
||||
|
||||
token = os.getenv("GITHUB_TOKEN")
|
||||
if not token:
|
||||
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
|
||||
|
||||
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
|
||||
if not repo_name:
|
||||
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
|
||||
|
||||
oldFiles = sys.argv[2]
|
||||
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
|
||||
newFiles = sys.argv[3]
|
||||
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
|
||||
|
||||
startMarkdown = "# Target Size Changes\n\n"
|
||||
markdown = ""
|
||||
|
||||
newlyIntroduced = new - old
|
||||
if len(newlyIntroduced) > 0:
|
||||
markdown += "## Newly Introduced Targets\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Size |\n"
|
||||
markdown += "| ---- | ---- |\n"
|
||||
for f in newlyIntroduced:
|
||||
name, size = parseFile(f)
|
||||
markdown += f"| `{name}` | {size}b |\n"
|
||||
|
||||
# do not log removed targets
|
||||
# PRs only run a small subset of builds, so removed targets are not meaningful
|
||||
# since they are very likely to just be not ran in PR CI
|
||||
|
||||
both = old & new
|
||||
degradations = []
|
||||
improvements = []
|
||||
for f in both:
|
||||
oldName, oldSize = parseFile(f)
|
||||
_, newSize = parseFile(f)
|
||||
if oldSize != newSize:
|
||||
if newSize < oldSize:
|
||||
improvements.append((oldName, oldSize, newSize))
|
||||
else:
|
||||
degradations.append((oldName, oldSize, newSize))
|
||||
|
||||
if len(degradations) > 0:
|
||||
markdown += "\n## Degradation\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in degradations:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(improvements) > 0:
|
||||
markdown += "\n## Improvement\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in improvements:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(markdown) == 0:
|
||||
markdown = "No changes in target sizes detected."
|
||||
|
||||
g = Github(token)
|
||||
repo = g.get_repo(repo_name)
|
||||
pr = repo.get_pull(pr_number)
|
||||
|
||||
existing_comment = None
|
||||
for comment in pr.get_issue_comments():
|
||||
if comment.body.startswith(startMarkdown):
|
||||
existing_comment = comment
|
||||
break
|
||||
|
||||
final_markdown = startMarkdown + markdown
|
||||
|
||||
if existing_comment:
|
||||
existing_comment.edit(body=final_markdown)
|
||||
else:
|
||||
pr.create_issue_comment(body=final_markdown)
|
||||
Reference in New Issue
Block a user