Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
config.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
# Set spidev ownership to 'spi' group
|
||||
SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660"
|
||||
# Allow access to USB CH341 devices
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666"
|
||||
# Set gpio ownership to 'gpio' group
|
||||
SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660"
|
||||
SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarise linker map output to highlight heavy object files and libraries.
|
||||
|
||||
Usage:
|
||||
python bin/analyze_map.py --map .pio/build/rak4631/output.map --top 20
|
||||
|
||||
The script parses GNU ld map files and aggregates section sizes per object file
|
||||
and per archive/library, then prints sortable tables that make it easy to spot
|
||||
modules worth trimming or hiding behind feature flags.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import DefaultDict, Dict, Tuple
|
||||
|
||||
|
||||
SECTION_LINE_RE = re.compile(r"^\s+(?P<section>\S+)\s+0x[0-9A-Fa-f]+\s+0x(?P<size>[0-9A-Fa-f]+)\s+(?P<object>.+)$")
|
||||
ARCHIVE_MEMBER_RE = re.compile(r"^(?P<archive>.+)\((?P<object>[^)]+)\)$")
|
||||
|
||||
|
||||
def human_size(num_bytes: int) -> str:
|
||||
"""Return a friendly size string with one decimal place."""
|
||||
if num_bytes < 1024:
|
||||
return f"{num_bytes:,} B"
|
||||
num = float(num_bytes)
|
||||
for unit in ("KB", "MB", "GB"):
|
||||
num /= 1024.0
|
||||
if num < 1024.0:
|
||||
return f"{num:.1f} {unit}"
|
||||
return f"{num:.1f} TB"
|
||||
|
||||
|
||||
def shorten_path(path: str, root: str) -> str:
|
||||
"""Prefer repository-relative paths for readability."""
|
||||
path = path.strip()
|
||||
if not path:
|
||||
return path
|
||||
|
||||
# Normalise Windows archives (backslashes) to POSIX style for consistency.
|
||||
path = path.replace("\\", "/")
|
||||
|
||||
# Attempt to strip the root when an absolute path lives inside the repo.
|
||||
if os.path.isabs(path):
|
||||
try:
|
||||
rel = os.path.relpath(path, root)
|
||||
if not rel.startswith(".."):
|
||||
return rel
|
||||
except ValueError:
|
||||
# relpath can fail on mixed drives on Windows; fall back to basename.
|
||||
pass
|
||||
return path
|
||||
|
||||
|
||||
def describe_object(raw_object: str, root: str) -> Tuple[str, str]:
|
||||
"""Return a human friendly object label and the library it belongs to."""
|
||||
raw_object = raw_object.strip()
|
||||
lib_label = "[app]"
|
||||
match = ARCHIVE_MEMBER_RE.match(raw_object)
|
||||
if match:
|
||||
archive = shorten_path(match.group("archive"), root)
|
||||
obj = match.group("object")
|
||||
lib_label = os.path.basename(archive) or archive
|
||||
label = f"{archive}:{obj}"
|
||||
else:
|
||||
label = shorten_path(raw_object, root)
|
||||
# If the object lives under libs, hint at the containing directory.
|
||||
parent = os.path.basename(os.path.dirname(label))
|
||||
if parent:
|
||||
lib_label = parent
|
||||
return label, lib_label
|
||||
|
||||
|
||||
def parse_map(map_path: str, repo_root: str) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, int]]]:
|
||||
per_object: DefaultDict[str, int] = collections.defaultdict(int)
|
||||
per_library: DefaultDict[str, int] = collections.defaultdict(int)
|
||||
per_object_sections: DefaultDict[str, DefaultDict[str, int]] = collections.defaultdict(lambda: collections.defaultdict(int))
|
||||
|
||||
try:
|
||||
with open(map_path, "r", encoding="utf-8", errors="ignore") as handle:
|
||||
for line in handle:
|
||||
match = SECTION_LINE_RE.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
section = match.group("section")
|
||||
if section.startswith("*") or section in {"LOAD", "ORIGIN"}:
|
||||
continue
|
||||
|
||||
size = int(match.group("size"), 16)
|
||||
if size == 0:
|
||||
continue
|
||||
|
||||
obj_token = match.group("object").strip()
|
||||
if not obj_token or obj_token.startswith("*") or "load address" in obj_token:
|
||||
continue
|
||||
|
||||
label, lib_label = describe_object(obj_token, repo_root)
|
||||
per_object[label] += size
|
||||
per_library[lib_label] += size
|
||||
per_object_sections[label][section] += size
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"error: map file '{map_path}' not found. Run a build first.")
|
||||
|
||||
return per_object, per_library, per_object_sections
|
||||
|
||||
|
||||
def format_section_breakdown(section_sizes: Dict[str, int], total: int, limit: int = 3) -> str:
|
||||
items = sorted(section_sizes.items(), key=lambda kv: kv[1], reverse=True)
|
||||
parts = []
|
||||
for section, size in items[:limit]:
|
||||
pct = (size / total) * 100 if total else 0
|
||||
parts.append(f"{section} {pct:.1f}%")
|
||||
if len(items) > limit:
|
||||
remainder = total - sum(size for _, size in items[:limit])
|
||||
pct = (remainder / total) * 100 if total else 0
|
||||
parts.append(f"other {pct:.1f}%")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def print_report(map_path: str, top_n: int, per_object: Dict[str, int], per_library: Dict[str, int], per_object_sections: Dict[str, Dict[str, int]]):
|
||||
total_bytes = sum(per_object.values())
|
||||
if total_bytes == 0:
|
||||
print("No section data found in map file.")
|
||||
return
|
||||
|
||||
print(f"Map file: {map_path}")
|
||||
print(f"Accounted size: {human_size(total_bytes)} across {len(per_object)} object files\n")
|
||||
|
||||
sorted_objects = sorted(per_object.items(), key=lambda kv: kv[1], reverse=True)
|
||||
print(f"Top {min(top_n, len(sorted_objects))} object files by linked size:")
|
||||
for idx, (obj, size) in enumerate(sorted_objects[:top_n], 1):
|
||||
pct = (size / total_bytes) * 100
|
||||
breakdown = format_section_breakdown(per_object_sections[obj], size)
|
||||
print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size)")
|
||||
print(f" {obj}")
|
||||
if breakdown:
|
||||
print(f" sections: {breakdown}")
|
||||
print()
|
||||
|
||||
sorted_libs = sorted(per_library.items(), key=lambda kv: kv[1], reverse=True)
|
||||
print(f"Top {min(top_n, len(sorted_libs))} libraries or source roots:")
|
||||
for idx, (lib, size) in enumerate(sorted_libs[:top_n], 1):
|
||||
pct = (size / total_bytes) * 100
|
||||
print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size) {lib}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Highlight heavy object files from a GNU ld map file.")
|
||||
parser.add_argument("--map", default=".pio/build/rak4631/output.map", help="Path to the map file (default: %(default)s)")
|
||||
parser.add_argument("--top", type=int, default=20, help="Number of entries to display per table (default: %(default)s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
map_path = os.path.abspath(args.map)
|
||||
repo_root = os.path.abspath(os.getcwd())
|
||||
|
||||
per_object, per_library, per_object_sections = parse_map(map_path, repo_root)
|
||||
print_report(os.path.relpath(map_path, repo_root), args.top, per_object, per_library, per_object_sections)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
import base64
|
||||
|
||||
def base64_to_hex_string(b64_string):
|
||||
try:
|
||||
# Decode the Base64 string to raw bytes
|
||||
decoded_bytes = base64.b64decode(b64_string)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid Base64 input: {e}")
|
||||
|
||||
# Check if the decoded result is exactly 32 bytes
|
||||
if len(decoded_bytes) != 32:
|
||||
raise ValueError("Decoded Base64 input must be exactly 32 bytes.")
|
||||
|
||||
# Convert each byte to its hex representation
|
||||
hex_values = [f"0x{byte:02x}" for byte in decoded_bytes]
|
||||
|
||||
# Join the formatted hex values with commas
|
||||
formatted_output = "{ " + ", ".join(hex_values) + " };"
|
||||
return formatted_output
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if a Base64 string was provided in command line arguments
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python script.py <base64-string>")
|
||||
sys.exit(1)
|
||||
|
||||
b64_string = sys.argv[1]
|
||||
try:
|
||||
formatted_hex = base64_to_hex_string(b64_string)
|
||||
print(formatted_hex)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=`bin/buildinfo.py long`
|
||||
SHORT_VERSION=`bin/buildinfo.py short`
|
||||
|
||||
BUILDDIR=.pio/build/$1
|
||||
OUTDIR=release
|
||||
|
||||
rm -f $OUTDIR/firmware*
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg install -e $1
|
||||
|
||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||
rm -f $BUILDDIR/firmware*
|
||||
|
||||
# The shell vars the build tool expects to find
|
||||
export APP_VERSION=$VERSION
|
||||
|
||||
basename=firmware-$1-$VERSION
|
||||
|
||||
pio run --environment $1 -t mtjson # -v
|
||||
|
||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||
|
||||
echo "Copying ESP32 bin file"
|
||||
cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
|
||||
|
||||
echo "Copying ESP32 update bin file"
|
||||
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
||||
|
||||
echo "Copying Filesystem for ESP32 targets"
|
||||
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||
cp bin/device-install.* $OUTDIR/
|
||||
cp bin/device-update.* $OUTDIR/
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
if (echo $2 | grep -q "esp32"); then
|
||||
bin/build-esp32.sh $1
|
||||
elif (echo $2 | grep -q "nrf52"); then
|
||||
bin/build-nrf52.sh $1
|
||||
elif (echo $2 | grep -q "stm32"); then
|
||||
bin/build-stm32.sh $1
|
||||
elif (echo $2 | grep -q "rpi2040"); then
|
||||
bin/build-rp2xx0.sh $1
|
||||
else
|
||||
echo "Unknown target $2"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
platformioFailed() {
|
||||
[[ $VIRTUAL_ENV != "" ]] && exit 1 # don't hint at virtualenv if it's already in use
|
||||
echo -e "\nThere were issues running platformio and you are not using a virtual environment." \
|
||||
"\nYou may try setting up virtualenv and downloading the latest platformio from pip:" \
|
||||
"\n\tvirtualenv venv" \
|
||||
"\n\tsource venv/bin/activate" \
|
||||
"\n\tpip install platformio" \
|
||||
"\n\t./bin/build-native.sh # retry building"
|
||||
exit 1
|
||||
}
|
||||
|
||||
VERSION=$(bin/buildinfo.py long)
|
||||
SHORT_VERSION=$(bin/buildinfo.py short)
|
||||
PIO_ENV=${1:-native}
|
||||
|
||||
BUILDDIR=.pio/build/$PIO_ENV
|
||||
OUTDIR=release
|
||||
|
||||
rm -f $OUTDIR/meshtasticd*
|
||||
|
||||
mkdir -p $OUTDIR/
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
basename=meshtasticd-$1-$VERSION
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
||||
pio run --environment "$PIO_ENV" || platformioFailed
|
||||
|
||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||
cp bin/native-install.* $OUTDIR/
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$(bin/buildinfo.py long)
|
||||
SHORT_VERSION=$(bin/buildinfo.py short)
|
||||
|
||||
BUILDDIR=.pio/build/$1
|
||||
OUTDIR=release
|
||||
|
||||
rm -f $OUTDIR/firmware*
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg install -e $1
|
||||
|
||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||
rm -f $BUILDDIR/firmware*
|
||||
|
||||
# The shell vars the build tool expects to find
|
||||
export APP_VERSION=$VERSION
|
||||
|
||||
basename=firmware-$1-$VERSION
|
||||
ota_basename=${basename}-ota
|
||||
|
||||
pio run --environment $1 -t mtjson # -v
|
||||
|
||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||
|
||||
echo "Copying NRF52 dfu (OTA) file"
|
||||
cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip
|
||||
|
||||
echo "Copying NRF52 UF2 file"
|
||||
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
||||
cp bin/*.uf2 $OUTDIR/
|
||||
|
||||
SRCHEX=$BUILDDIR/$basename.hex
|
||||
|
||||
# if WM1110 target, copy the merged.hex
|
||||
if (echo $1 | grep -q "wio-sdk-wm1110"); then
|
||||
echo "Copying .merged.hex file"
|
||||
SRCHEX=$BUILDDIR/$basename.merged.hex
|
||||
cp $SRCHEX $OUTDIR/
|
||||
fi
|
||||
|
||||
if (echo $1 | grep -q "rak4631"); then
|
||||
echo "Copying .hex file"
|
||||
cp $SRCHEX $OUTDIR/
|
||||
fi
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=`bin/buildinfo.py long`
|
||||
SHORT_VERSION=`bin/buildinfo.py short`
|
||||
|
||||
BUILDDIR=.pio/build/$1
|
||||
OUTDIR=release
|
||||
|
||||
rm -f $OUTDIR/firmware*
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg install -e $1
|
||||
|
||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||
rm -f $BUILDDIR/firmware*
|
||||
|
||||
# The shell vars the build tool expects to find
|
||||
export APP_VERSION=$VERSION
|
||||
|
||||
basename=firmware-$1-$VERSION
|
||||
|
||||
pio run --environment $1 -t mtjson # -v
|
||||
|
||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||
|
||||
echo "Copying uf2 file"
|
||||
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$(bin/buildinfo.py long)
|
||||
SHORT_VERSION=$(bin/buildinfo.py short)
|
||||
|
||||
BUILDDIR=.pio/build/$1
|
||||
OUTDIR=release
|
||||
|
||||
rm -f $OUTDIR/firmware*
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg install -e $1
|
||||
|
||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||
rm -f $BUILDDIR/firmware*
|
||||
|
||||
# The shell vars the build tool expects to find
|
||||
export APP_VERSION=$VERSION
|
||||
|
||||
basename=firmware-$1-$VERSION
|
||||
|
||||
pio run --environment $1 -t mtjson # -v
|
||||
|
||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||
|
||||
echo "Copying STM32 bin file"
|
||||
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
def get_macros_from_header(header_file):
|
||||
# Run clang to preprocess the header file and capture the output
|
||||
result = subprocess.run(['clang', '-E', '-dM', header_file], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Clang preprocessing failed: {result.stderr}")
|
||||
|
||||
# Extract macros from the output
|
||||
macros = {}
|
||||
macro_pattern = re.compile(r'#define\s+(\w+)\s+(.*)')
|
||||
for line in result.stdout.splitlines():
|
||||
match = macro_pattern.match(line)
|
||||
if match and 'USERPREFS_' in line and '_USERPREFS_' not in line:
|
||||
macros[match.group(1)] = match.group(2)
|
||||
|
||||
return macros
|
||||
|
||||
def write_macros_to_json(macros, output_file):
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(macros, f, indent=4)
|
||||
|
||||
def main():
|
||||
header_file = 'userPrefs.h'
|
||||
output_file = 'userPrefs.jsonc'
|
||||
# Uncomment all macros in the header file
|
||||
with open(header_file, 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
uncommented_lines = []
|
||||
for line in lines:
|
||||
stripped_line = line.strip().replace('/*', '').replace('*/', '')
|
||||
if stripped_line.startswith('//') and 'USERPREFS_' in stripped_line:
|
||||
# Replace "//"
|
||||
stripped_line = stripped_line.replace('//', '')
|
||||
uncommented_lines.append(stripped_line + '\n')
|
||||
|
||||
with open(header_file, 'w') as file:
|
||||
for line in uncommented_lines:
|
||||
file.write(line)
|
||||
macros = get_macros_from_header(header_file)
|
||||
write_macros_to_json(macros, output_file)
|
||||
print(f"Macros have been written to {output_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
from readprops import readProps
|
||||
|
||||
verObj = readProps("version.properties")
|
||||
propName = sys.argv[1]
|
||||
print(f"{verObj[propName]}")
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from defusedxml.ElementTree import parse
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
# Indent by 2 spaces to align with xml formatting.
|
||||
def indent(elem, level=0):
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
for child in elem:
|
||||
indent(child, level + 1)
|
||||
if not child.tail or not child.tail.strip():
|
||||
child.tail = i
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prepend new release entry to metainfo.xml file.")
|
||||
parser.add_argument("--file", help="Path to the metainfo.xml file",
|
||||
default="org.meshtastic.meshtasticd.metainfo.xml")
|
||||
parser.add_argument("version", help="Version string (e.g. 2.6.4)")
|
||||
parser.add_argument("--date", help="Release date (YYYY-MM-DD), defaults to today",
|
||||
default=datetime.now(timezone.utc).date().isoformat())
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tree = parse(args.file)
|
||||
root = tree.getroot()
|
||||
|
||||
releases = root.find('releases')
|
||||
if releases is None:
|
||||
raise RuntimeError("<releases> element not found in XML.")
|
||||
|
||||
existing_versions = {
|
||||
release.get('version'): release
|
||||
for release in releases.findall('release')
|
||||
}
|
||||
existing_release = existing_versions.get(args.version)
|
||||
|
||||
if existing_release is not None:
|
||||
if not existing_release.get('date'):
|
||||
print(f"Version {args.version} found without date. Adding date...")
|
||||
existing_release.set('date', args.date)
|
||||
else:
|
||||
print(
|
||||
f"Version {args.version} is already present with date, skipping insertion.")
|
||||
else:
|
||||
new_release = ET.Element('release', {
|
||||
'version': args.version,
|
||||
'date': args.date
|
||||
})
|
||||
url = ET.SubElement(new_release, 'url', {'type': 'details'})
|
||||
url.text = f"https://github.com/meshtastic/firmware/releases?q=tag%3Av{args.version}"
|
||||
|
||||
releases.insert(0, new_release)
|
||||
|
||||
indent(releases, level=1)
|
||||
releases.tail = "\n"
|
||||
|
||||
print(f"Inserted new release: {args.version}")
|
||||
|
||||
tree.write(args.file, encoding='UTF-8', xml_declaration=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
defusedxml==0.7.1
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
"""Bump the version number"""
|
||||
|
||||
lines = None
|
||||
|
||||
with open('version.properties', 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
with open('version.properties', 'w', encoding='utf-8') as f:
|
||||
for line in lines:
|
||||
if line.lstrip().startswith("build = "):
|
||||
words = line.split(" = ")
|
||||
ver = f'build = {int(words[1]) + 1}'
|
||||
f.write(f'{ver}\n')
|
||||
else:
|
||||
f.write(line)
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Note: This is a prototype for how we could add static code analysis to the CI.
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$(bin/buildinfo.py long)
|
||||
|
||||
# The shell vars the build tool expects to find
|
||||
export APP_VERSION=$VERSION
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
# can override which environment by passing arg
|
||||
BOARDS="$@"
|
||||
else
|
||||
BOARDS="tlora-v2 tlora-v1 tlora_v1_3 tlora-v2-1-1.6 tbeam heltec-v2.0 heltec-v2.1 tbeam0.7 meshtastic-diy-v1 rak4631 rak4631_eink rak11200 t-echo canaryone pca10059_diy_eink"
|
||||
fi
|
||||
|
||||
echo "BOARDS:${BOARDS}"
|
||||
|
||||
CHECK=""
|
||||
for BOARD in $BOARDS; do
|
||||
CHECK="${CHECK} -e ${BOARD}"
|
||||
done
|
||||
|
||||
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Note: This is a prototype for how we could add static code analysis to the CI.
|
||||
|
||||
set -e
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
# can override which environment by passing arg
|
||||
BOARDS="$@"
|
||||
else
|
||||
BOARDS="rak4631 rak4631_eink t-echo canaryone pca10059_diy_eink pico rak11200 tlora-v2 tlora-v1 tlora_v1_3 tlora-v2-1-1.6 tbeam heltec-v2.0 heltec-v2.1 tbeam0.7 meshtastic-diy-v1 nano-g1 station-g1 m5stack-core m5stack-coreink tbeam-s3-core"
|
||||
fi
|
||||
|
||||
echo "BOARDS:${BOARDS}"
|
||||
|
||||
CHECK=""
|
||||
for BOARD in $BOARDS; do
|
||||
CHECK="${CHECK} -e ${BOARD}"
|
||||
done
|
||||
|
||||
echo $CHECK
|
||||
|
||||
pio pkg outdated -e $CHECK
|
||||
@@ -0,0 +1,216 @@
|
||||
### Many device configs have been moved to /etc/meshtasticd/available.d
|
||||
### To activate, simply copy or link the appropriate file into /etc/meshtasticd/config.d
|
||||
|
||||
### Define your devices here using Broadcom pin numbering
|
||||
### Uncomment the block that corresponds to your hardware
|
||||
### Including the "Module:" line!
|
||||
---
|
||||
Lora:
|
||||
# Default to auto-detecting the module type
|
||||
# This will be overridden by configs from config.d
|
||||
Module: auto
|
||||
|
||||
# # Uncomment to enable Simulation mode, or use --sim
|
||||
# Module: sim
|
||||
|
||||
# Module: sx1262 # Waveshare SX1302 LISTEN ONLY AT THIS TIME!
|
||||
# CS: 7
|
||||
# IRQ: 17
|
||||
# Reset: 22
|
||||
|
||||
# Module: RF95 # Elecrow Lora RFM95 IOT https://www.elecrow.com/lora-rfm95-iot-board-for-rpi.html
|
||||
# Reset: 22
|
||||
# CS: 7
|
||||
# IRQ: 25
|
||||
|
||||
# Module: sx1280 # SX1280
|
||||
# CS: 21
|
||||
# IRQ: 16
|
||||
# Busy: 20
|
||||
# Reset: 18
|
||||
|
||||
### The Radxa Zero 3E/W employs multiple gpio chips.
|
||||
### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line.
|
||||
### In case solely a no. is given, the default gpio chip and pin == line will be employed.
|
||||
###
|
||||
# Module: sx1262 # Radxa Zero 3E/W + Ebyte E22-900M30S
|
||||
# DIO2_AS_RF_SWITCH: true
|
||||
# DIO3_TCXO_VOLTAGE: 1.8
|
||||
# CS: # NSS PIN_24 -> chip 4, line 22
|
||||
# pin: 24
|
||||
# gpiochip: 4
|
||||
# line: 22
|
||||
# SCK: # SCK PIN_23 -> chip 4, line 18
|
||||
# pin: 23
|
||||
# gpiochip: 4
|
||||
# line: 18
|
||||
# Busy: # BUSY PIN_29 -> chip 3!, line 11
|
||||
# pin: 29
|
||||
# gpiochip: 3
|
||||
# line: 11
|
||||
# MOSI: # MOSI PIN_19 -> chip 4, line 19
|
||||
# pin: 19
|
||||
# gpiochip: 4
|
||||
# line: 19
|
||||
# MISO: # MISO PIN_21 -> chip 4, line 21
|
||||
# pin: 21
|
||||
# gpiochip: 4
|
||||
# line: 21
|
||||
# Reset: # NRST PIN_27 -> chip 4, line 10
|
||||
# pin: 27
|
||||
# gpiochip: 4
|
||||
# line: 10
|
||||
# IRQ: # DIO1 PIN_28 -> chip 4, line 11
|
||||
# pin: 28
|
||||
# gpiochip: 4
|
||||
# line: 11
|
||||
# RXen: # RXEN PIN_22 -> chip 3!, line 17
|
||||
# pin: 22
|
||||
# gpiochip: 3
|
||||
# line: 17
|
||||
# TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip
|
||||
|
||||
# Module: sx1268 # SX1268-based modules, tested with Ebyte E22 400M33S
|
||||
# CS: 21
|
||||
# IRQ: 16
|
||||
# Busy: 20
|
||||
# Reset: 18
|
||||
# TXen: 6
|
||||
# RXen: 12
|
||||
# DIO3_TCXO_VOLTAGE: true
|
||||
|
||||
# DIO3_TCXO_VOLTAGE: true # the Waveshare Core1262 and others are known to need this setting
|
||||
|
||||
# TXen: x # TX and RX enable pins
|
||||
# RXen: x
|
||||
|
||||
# SX126X_MAX_POWER: 8 # Limit the output power to 8 dBm, useful for amped nodes
|
||||
|
||||
# spiSpeed: 2000000
|
||||
|
||||
### Set default/fallback gpio chip to use in /dev/. Defaults to 0.
|
||||
### Notably the Raspberry Pi 5 puts the GPIO header on gpiochip4
|
||||
# gpiochip: 4
|
||||
|
||||
### Specify the SPI device to use in /dev/. Defaults to spidev0.0
|
||||
### Some devices, like the pinedio, may require spidev0.1 as a workaround.
|
||||
# spidev: spidev0.0
|
||||
|
||||
### Deprecated location for User Button:
|
||||
|
||||
#GPIO:
|
||||
# User: 6
|
||||
|
||||
### Define GPS
|
||||
|
||||
GPS:
|
||||
# SerialPath: /dev/ttyS0
|
||||
# ExtraPins:
|
||||
# - 22
|
||||
|
||||
### Specify I2C device, or leave blank for none
|
||||
|
||||
I2C:
|
||||
# I2CDevice: /dev/i2c-1
|
||||
|
||||
### Set up SPI displays here. Note that I2C displays are generally auto-detected.
|
||||
|
||||
Display:
|
||||
|
||||
### Adafruit PiTFT 2.8 TFT+Touchscreen
|
||||
# Panel: ILI9341
|
||||
# CS: 8
|
||||
# DC: 25
|
||||
# Width: 240
|
||||
# Height: 320
|
||||
# Rotate: true
|
||||
|
||||
### SHCHV 3.5 RPi TFT+Touchscreen
|
||||
# Panel: ILI9486
|
||||
# spidev: spidev0.0
|
||||
# BusFrequency: 30000000
|
||||
# DC: 24
|
||||
# Reset: 25
|
||||
# Width: 320
|
||||
# Height: 480
|
||||
# OffsetRotate: 2
|
||||
|
||||
### TZT 2.0 Inch TFT Display ST7789V 240RGBx320
|
||||
# Panel: ST7789
|
||||
# spidev: spidev0.0
|
||||
# # CS: 8 # can be freely chosen
|
||||
# BusFrequency: 80000000
|
||||
# DC: 24 # can be freely chosen
|
||||
# Width: 320
|
||||
# Height: 240
|
||||
# Reset: 25 # can be freely chosen
|
||||
# Rotate: true
|
||||
# OffsetRotate: 1
|
||||
# Invert: true
|
||||
|
||||
### You can also specify the spi device for the display to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
Touchscreen:
|
||||
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
||||
|
||||
# Module: STMPE610 # Option 1 for Adafruit PiTFT 2.8
|
||||
# CS: 7
|
||||
# IRQ: 24
|
||||
|
||||
# Module: FT5x06 # Option 2 for Adafruit PiTFT 2.8
|
||||
# IRQ: 24
|
||||
# I2CAddr: 0x38
|
||||
|
||||
### You can also specify the spi device for the touchscreen to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
|
||||
Input:
|
||||
### Configure device for direct keyboard input
|
||||
|
||||
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
||||
|
||||
### Standard User Button Config
|
||||
# UserButton: 6
|
||||
|
||||
### Trackball/Joystick input
|
||||
# TrackballUp: 6
|
||||
# TrackballDown: 19
|
||||
# TrackballLeft: 5
|
||||
# TrackballRight: 26
|
||||
# TrackballPress: 13
|
||||
|
||||
###
|
||||
|
||||
Logging:
|
||||
LogLevel: info # debug, info, warn, error
|
||||
# TraceFile: /var/log/meshtasticd.json
|
||||
# JSONFile: /packets.json # File location for JSON output of decoded packets
|
||||
# JSONFilter: position # filter for packets to save to JSON file
|
||||
# AsciiLogs: true # default if not specified is !isatty() on stdout
|
||||
|
||||
Webserver:
|
||||
# Port: 9443 # Port for Webserver & Webservices
|
||||
# RootPath: /usr/share/meshtasticd/web # Root Dir of WebServer
|
||||
# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present
|
||||
# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present
|
||||
|
||||
|
||||
HostMetrics:
|
||||
# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled
|
||||
# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel.
|
||||
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
||||
|
||||
|
||||
Config:
|
||||
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
|
||||
# DisplayMode: COLOR # uncomment to force MUI
|
||||
|
||||
General:
|
||||
MaxNodes: 200
|
||||
MaxMessageQueue: 100
|
||||
ConfigDirectory: /etc/meshtasticd/config.d/
|
||||
AvailableDirectory: /etc/meshtasticd/available.d/
|
||||
# MACAddress: AA:BB:CC:DD:EE:FF
|
||||
# MACAddressSource: eth0
|
||||
@@ -0,0 +1,4 @@
|
||||
Display:
|
||||
Panel: X11
|
||||
Width: 480
|
||||
Height: 480
|
||||
@@ -0,0 +1,9 @@
|
||||
Lora:
|
||||
Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header
|
||||
## CS: 28
|
||||
IRQ: 50
|
||||
Busy: 62
|
||||
Reset: 51
|
||||
spidev: spidev1.0
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
@@ -0,0 +1,9 @@
|
||||
## https://www.mikroe.com/lr-iot-click
|
||||
Lora:
|
||||
Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK
|
||||
# CS: 25
|
||||
IRQ: 10
|
||||
Busy: 12
|
||||
# Reset: 2
|
||||
spidev: spidev2.0
|
||||
DIO3_TCXO_VOLTAGE: 1.6
|
||||
@@ -0,0 +1,8 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: 10
|
||||
Busy: 12
|
||||
# Reset: 2
|
||||
spidev: spidev2.0
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
@@ -0,0 +1,26 @@
|
||||
### Waveshare 1.44inch LCD HAT
|
||||
Display:
|
||||
Panel: ST7735S
|
||||
spidev: spidev0.0 # Specify either the spidev here, or the CS below
|
||||
# CS: 8 #Chip Select # Optional, as this is the default pin for spidev0.0
|
||||
DC: 25 # Data/Command pin
|
||||
Backlight: 24
|
||||
Width: 128
|
||||
Height: 128
|
||||
Reset: 27
|
||||
OffsetX: 2
|
||||
OffsetY: 1
|
||||
|
||||
|
||||
# OffsetY: 31 # These two options are used to properly flip the screen 180 degrees
|
||||
# OffsetRotate: 3
|
||||
|
||||
|
||||
Input:
|
||||
TrackballUp: 6
|
||||
TrackballDown: 19
|
||||
TrackballLeft: 5
|
||||
TrackballRight: 26
|
||||
TrackballPress: 13
|
||||
TrackballDirection: FALLING
|
||||
# User: 21
|
||||
@@ -0,0 +1,18 @@
|
||||
Display:
|
||||
|
||||
### Waveshare 2.8inch RPi LCD
|
||||
Panel: ST7789
|
||||
CS: 8
|
||||
DC: 22 # Data/Command pin
|
||||
Backlight: 18
|
||||
Width: 240
|
||||
Height: 320
|
||||
Reset: 27
|
||||
Rotate: true
|
||||
Invert: true
|
||||
|
||||
Touchscreen:
|
||||
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
||||
Module: XPT2046 # Waveshare 2.8inch
|
||||
CS: 7
|
||||
IRQ: 17
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E80-900M22S
|
||||
## This is a bit experimental
|
||||
##
|
||||
##
|
||||
Module: lr1121
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup
|
||||
## HT-RA62 (Has internal switching, but whatever)
|
||||
## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN)
|
||||
## Will work with any module with or without RF switching, and with TCXO
|
||||
Module: sx1262
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
|
||||
# TXen: bridge to DIO2 on E22 module
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E22-900MM22S with no external RF switching setup
|
||||
## Waveshare SX126X XXXM, AI Thinker RA-01SH
|
||||
## Will work with any module with or without RF switching and no TCXO
|
||||
|
||||
Module: sx1262
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: false
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
|
||||
# TXen: bridge to DIO2 on E22 module
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
@@ -0,0 +1,6 @@
|
||||
Lora:
|
||||
Module: RF95 # Adafruit RFM9x
|
||||
Reset: 25
|
||||
CS: 7
|
||||
IRQ: 22
|
||||
# Busy: 23
|
||||
@@ -0,0 +1,14 @@
|
||||
# MeshAdv-Pi E22-900M30S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 21
|
||||
IRQ: 16
|
||||
Busy: 20
|
||||
Reset: 18
|
||||
TXen: 13
|
||||
RXen: 12
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# Only for E22-900M33S:
|
||||
# Limit the output power to 8 dBm
|
||||
# SX126X_MAX_POWER: 8
|
||||
@@ -0,0 +1,11 @@
|
||||
# MeshAdv Mini E22-900M22S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Mini
|
||||
Lora:
|
||||
Module: sx1262 # Ebyte E22-900M22S
|
||||
CS: 8
|
||||
IRQ: 16
|
||||
Busy: 20
|
||||
Reset: 24
|
||||
RXen: 12
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
@@ -0,0 +1,12 @@
|
||||
Lora:
|
||||
|
||||
### RAK13300in Slot 1
|
||||
Module: sx1262
|
||||
IRQ: 22 #IO6
|
||||
Reset: 16 # IO4
|
||||
Busy: 24 # IO5
|
||||
# Ant_sw: 13 # IO3
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: 8
|
||||
@@ -0,0 +1,8 @@
|
||||
Lora:
|
||||
### RAK13300in Slot 2 pins
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
@@ -0,0 +1,13 @@
|
||||
Lora:
|
||||
|
||||
### RAK13300in Slot 1
|
||||
Module: sx1262
|
||||
IRQ: 22 #IO6
|
||||
Reset: 16 # IO4
|
||||
Busy: 24 # IO5
|
||||
# Ant_sw: 13 # IO3
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
GPIO_DETECT_PA: 13
|
||||
TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
|
||||
@@ -0,0 +1,18 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
gpiochip: 0
|
||||
MOSI: 12
|
||||
MISO: 13
|
||||
IRQ: 1
|
||||
Busy: 23
|
||||
Reset: 22
|
||||
RXen: 0
|
||||
gpiochip: 1
|
||||
CS: 9
|
||||
SCK: 11
|
||||
# TXen: bridge to DIO2 on E22 module
|
||||
SX126X_MAX_POWER: 22
|
||||
spidev: spidev1.0
|
||||
spiSpeed: 2000000
|
||||
@@ -0,0 +1,16 @@
|
||||
# For use with Armbian luckfox-lyra-ultra-w
|
||||
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
|
||||
# 1 Watt Lyra Ultra hat
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 10
|
||||
IRQ: 5
|
||||
Busy: 11
|
||||
Reset: 9
|
||||
RXen: 14
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
|
||||
spiSpeed: 2000000
|
||||
@@ -0,0 +1,17 @@
|
||||
# For use with Armbian luckfox-lyra-ultra-w
|
||||
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
|
||||
# 2 Watt Lyra Ultra hat
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 8
|
||||
CS: 10
|
||||
IRQ: 5
|
||||
Busy: 11
|
||||
Reset: 9
|
||||
RXen: 14
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
|
||||
spiSpeed: 2000000
|
||||
@@ -0,0 +1,25 @@
|
||||
# For use with Armbian luckfox-lyra // luckfox-lyra-plus
|
||||
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
|
||||
# Waveshare LoRa HAT for Raspberry Pi Pico
|
||||
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
CS: # GPIO0_B5
|
||||
pin: 13
|
||||
gpiochip: 0
|
||||
line: 13
|
||||
IRQ: # GPIO1_C2
|
||||
pin: 50
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
Busy: # GPIO0_B4
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
Reset: # GPIO0_A2
|
||||
pin: 2
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
@@ -0,0 +1,11 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 2
|
||||
Busy: 4
|
||||
spidev: ch341
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
@@ -0,0 +1,11 @@
|
||||
Lora:
|
||||
Module: lr1121
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 2
|
||||
Busy: 4
|
||||
spidev: ch341
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
# USB_Serialnum: 12345678
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
@@ -0,0 +1,5 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 10
|
||||
spidev: ch341
|
||||
@@ -0,0 +1,49 @@
|
||||
Lora:
|
||||
|
||||
### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx
|
||||
### https://github.com/markbirss/rock-2f
|
||||
### https://github.com/markbirss/lora-starter-edition-sx1262-i2c
|
||||
### https://github.com/radxa-pkg/radxa-overlays/blob/main/arch/arm64/boot/dts/rockchip/overlays/rk3528-spi0-cs1-spidev.dts
|
||||
### Require install of https://github.com/radxa-pkg/radxa-overlays and rk3528-spi0-cs1-spidev.dtbo copied to /boot/dtb/rockchip/overlay and enabled
|
||||
### in /boot/armbianEnv.txt - overlays=rk3528-spi0-cs1-spidev
|
||||
### The Radxa Rock 2F employs multiple gpio chips.
|
||||
### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line.
|
||||
### In case solely a no. is given, the default gpio chip and pin == line will be employed.
|
||||
###
|
||||
Module: sx1262 # Radxa Rock 2F + Starter Edition SX1262 HAT by Mark Birss
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
spidev: spidev0.1
|
||||
CS: # NSS PIN_24 -> chip 4, line 14
|
||||
pin: 24
|
||||
gpiochip: 4
|
||||
line: 14
|
||||
SCK: # SCK PIN_23 -> chip 4, line 12
|
||||
pin: 23
|
||||
gpiochip: 4
|
||||
line: 12
|
||||
Busy: # BUSY PIN_7 -> chip 4, line 6
|
||||
pin: 7
|
||||
gpiochip: 4
|
||||
line: 6
|
||||
MOSI: # MOSI PIN_19 -> chip 4, line 10
|
||||
pin: 19
|
||||
gpiochip: 4
|
||||
line: 10
|
||||
MISO: # MISO PIN_21 -> chip 4, line 11
|
||||
pin: 21
|
||||
gpiochip: 4
|
||||
line: 11
|
||||
Reset: # NRST PIN_12 -> chip 1, line 13
|
||||
pin: 12
|
||||
gpiochip: 1
|
||||
line: 13
|
||||
IRQ: # DIO1 PIN_15 -> chip 4, line 22
|
||||
pin: 15
|
||||
gpiochip: 4
|
||||
line: 22
|
||||
# RXen: # RXEN PIN_22 -> chip 3!, line 17
|
||||
# pin: 22
|
||||
# gpiochip: 3
|
||||
# line: 17
|
||||
# TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip
|
||||
@@ -0,0 +1,10 @@
|
||||
# https://www.waveshare.com/core1262-868m.htm
|
||||
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
|
||||
Lora:
|
||||
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 8
|
||||
IRQ: 22
|
||||
Busy: 4
|
||||
Reset: 18
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 2
|
||||
Busy: 4
|
||||
RXen: 1
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
# Optional: Reduce power to 10 dBm to
|
||||
# avoid over-drawing the USB port
|
||||
# SX126X_MAX_POWER: 10
|
||||
# Optional: Set the serial number for multi-radio support
|
||||
# USB_Serialnum: 13374201
|
||||
@@ -0,0 +1,22 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -0,0 +1,22 @@
|
||||
Lora:
|
||||
Module: sx1268
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -0,0 +1,8 @@
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare SX126X XXXM
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
CS: 21
|
||||
IRQ: 16
|
||||
Busy: 20
|
||||
Reset: 18
|
||||
SX126X_ANT_SW: 6
|
||||
@@ -0,0 +1,10 @@
|
||||
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
|
||||
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 21
|
||||
IRQ: 16
|
||||
Busy: 20
|
||||
Reset: 18
|
||||
@@ -0,0 +1,52 @@
|
||||
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
|
||||
# http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-Zero-3.html
|
||||
#
|
||||
# See Orange Pi Zero3 manual, chapter 3.16, page 124 for 26-pin header pinout
|
||||
#
|
||||
# Pin Connection
|
||||
# Waveshare Orange Pi Zero3
|
||||
# 36 3.3V 17
|
||||
# 15 MOSI 19
|
||||
# 16 MISO 21
|
||||
# 14 CLK 23
|
||||
# 38 GND 25
|
||||
# 4 BUSY 18
|
||||
# 20 RESET 22
|
||||
# 5 CS 24
|
||||
# 26 DIO1/IRQ 26
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare Raspberry Pico Lora module
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# Specify either the spidev1_1 or the CS below, not both!
|
||||
# On DietPi Linux, when using the user overlay dietpi-spi1_1.dtbo, CS will be configured with spidev1.1
|
||||
spidev: spidev1.1 # See Orange Pi Zero3 manual, chapter 3.18.3, page 130
|
||||
# CS: # CS PIN_24 -> chip 1, line 233
|
||||
# pin: 24
|
||||
# gpiochip: 1
|
||||
# line: 233
|
||||
SCK: # SCK PIN_23 -> chip 1, line 230
|
||||
pin: 23
|
||||
gpiochip: 1
|
||||
line: 230
|
||||
Busy: # BUSY PIN_18 -> chip 1, line 78
|
||||
pin: 18
|
||||
gpiochip: 1
|
||||
line: 78
|
||||
MOSI: # MOSI PIN_19 -> chip 1, line 231
|
||||
pin: 19
|
||||
gpiochip: 1
|
||||
line: 231
|
||||
MISO: # MISO PIN_21 -> chip 1, line 232
|
||||
pin: 21
|
||||
gpiochip: 1
|
||||
line: 232
|
||||
Reset: # NRST PIN_22 -> chip 1, line 71
|
||||
pin: 22
|
||||
gpiochip: 1
|
||||
line: 71
|
||||
IRQ: # DIO1 PIN_26 -> chip 1, line 74
|
||||
pin: 26
|
||||
gpiochip: 1
|
||||
line: 74
|
||||
@@ -0,0 +1,241 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic device-install
|
||||
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "ESPTOOL_CMD="
|
||||
SET "LOGCOUNTER=0"
|
||||
SET "BPS_RESET=0"
|
||||
@REM Default offsets.
|
||||
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
|
||||
SET "OTA_OFFSET=0x260000"
|
||||
SET "SPIFFS_OFFSET=0x300000"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO Flash image file to device, but first erasing and writing system information.
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset]
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required)
|
||||
ECHO The file must be located in this current directory.
|
||||
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
|
||||
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||
ECHO If supplied the script will use python.
|
||||
ECHO If not supplied the script will try to find esptool in Path.
|
||||
ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset)
|
||||
ECHO Some hardware requires this twice.
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11
|
||||
GOTO eof
|
||||
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.7.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||
IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1"
|
||||
SHIFT
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
IF %BPS_RESET% EQU 1 GOTO skip-filename
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||
IF "__!FILENAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||
GOTO help
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
|
||||
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
|
||||
GOTO help
|
||||
)
|
||||
IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file."
|
||||
GOTO help
|
||||
)
|
||||
@REM Remove ".\" or "./" file prefix if present.
|
||||
SET "FILENAME=!FILENAME:.\=!"
|
||||
SET "FILENAME=!FILENAME:./=!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
|
||||
IF NOT EXIST !FILENAME! (
|
||||
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking for metadata..."
|
||||
@REM Derive metadata filename from firmware filename.
|
||||
SET "METAFILE=!FILENAME:.factory.bin=!.mt.json"
|
||||
IF EXIST !METAFILE! (
|
||||
@REM Print parsed json with powershell
|
||||
CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!"
|
||||
powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()"
|
||||
|
||||
@REM Save metadata values to variables for later use.
|
||||
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
|
||||
"(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A"
|
||||
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
|
||||
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"`
|
||||
) DO SET "OTA_OFFSET=%%A"
|
||||
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
|
||||
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"`
|
||||
) DO SET "SPIFFS_OFFSET=%%A"
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!"
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
:skip-filename
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
|
||||
WHERE esptool >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 (
|
||||
@REM WHERE exits with code 0 if esptool is found.
|
||||
SET "ESPTOOL_CMD=esptool"
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=python -m esptool"
|
||||
CALL :RESET_ERROR
|
||||
)
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
|
||||
IF "__!ESPTOOL_PORT!__" == "____" (
|
||||
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=!ESPTOOL_CMD! --port !ESPTOOL_PORT!"
|
||||
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
|
||||
)
|
||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %BPS_RESET% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@REM Extract PROGNAME from %FILENAME% for later use.
|
||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||
|
||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||
|
||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||
SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin"
|
||||
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
|
||||
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"
|
||||
|
||||
@REM Ensure target files exist before flashing operations.
|
||||
IF NOT EXIST !FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
IF NOT EXIST !OTA_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!OTA_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!SPIFFS_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_ESPTOOL
|
||||
@REM Subroutine used to run ESPTOOL_CMD with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
IF %BPS_RESET% EQU 1 GOTO :eof
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
||||
BPS_RESET=false
|
||||
MCU=""
|
||||
|
||||
# Constants
|
||||
RESET_BAUD=1200
|
||||
FIRMWARE_OFFSET=0x00
|
||||
# Default littlefs* offset.
|
||||
OFFSET=0x300000
|
||||
# Default OTA Offset
|
||||
OTA_OFFSET=0x260000
|
||||
|
||||
# Determine the correct esptool command to use
|
||||
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="$PYTHON -m esptool"
|
||||
elif command -v esptool >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool"
|
||||
elif command -v esptool.py >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool.py"
|
||||
else
|
||||
echo "Error: esptool not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for jq
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: jq not found" >&2
|
||||
echo "Install jq with your package manager." >&2
|
||||
echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# esptool v5 supports commands with dashes and deprecates commands with
|
||||
# underscores. Prior versions only support commands with underscores
|
||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||
then
|
||||
ESPTOOL_WRITE_FLASH=write-flash
|
||||
ESPTOOL_ERASE_FLASH=erase-flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||
else
|
||||
ESPTOOL_WRITE_FLASH=write_flash
|
||||
ESPTOOL_ERASE_FLASH=erase_flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
# Usage info
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--1200bps-reset]
|
||||
Flash image file to device, but first erasing and writing system information.
|
||||
|
||||
-h Display this help and exit.
|
||||
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||
-f FILENAME The firmware *.factory.bin file to flash. Custom to your device type and region.
|
||||
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
||||
|
||||
EOF
|
||||
}
|
||||
# Parse arguments using a single while loop
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-p)
|
||||
ESPTOOL_CMD="$ESPTOOL_CMD --port $2"
|
||||
shift
|
||||
;;
|
||||
-P)
|
||||
PYTHON="$2"
|
||||
shift
|
||||
;;
|
||||
-f)
|
||||
FILENAME="$2"
|
||||
shift
|
||||
;;
|
||||
--1200bps-reset)
|
||||
BPS_RESET=true
|
||||
;;
|
||||
--) # Stop parsing options
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift # Move to the next argument
|
||||
done
|
||||
|
||||
if [[ $BPS_RESET == true ]]; then
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||
FILENAME="$1"
|
||||
shift
|
||||
}
|
||||
|
||||
if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then
|
||||
echo "Filename must be a firmware-*.factory.bin file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract PROGNAME from %FILENAME% for later use.
|
||||
PROGNAME="${FILENAME/.factory.bin/}"
|
||||
# Derive metadata filename from %PROGNAME%.
|
||||
METAFILE="${PROGNAME}.mt.json"
|
||||
|
||||
if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
# Display metadata if it exists
|
||||
if [[ -f "$METAFILE" ]]; then
|
||||
echo "Firmware metadata: ${METAFILE}"
|
||||
jq . "$METAFILE"
|
||||
# Extract relevant fields from metadata
|
||||
if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then
|
||||
OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE")
|
||||
SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE")
|
||||
fi
|
||||
MCU=$(jq -r '.mcu' "$METAFILE")
|
||||
else
|
||||
echo "ERROR: No metadata file found at ${METAFILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine OTA filename based on MCU type (unified OTA format)
|
||||
OTAFILE="mt-${MCU}-ota.bin"
|
||||
|
||||
# Set SPIFFS filename with "littlefs-" prefix.
|
||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||
|
||||
if [[ ! -f "$FILENAME" ]]; then
|
||||
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$OTAFILE" ]]; then
|
||||
echo "Error: file ${OTAFILE} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$SPIFFSFILE" ]]; then
|
||||
echo "Error: file ${SPIFFSFILE} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
||||
|
||||
else
|
||||
show_help
|
||||
echo "Invalid file: ${FILENAME}"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,112 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Unit-test for .\device-install.bat.
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs a positive unit-test on .\device-install.bat by creating the expected .bin
|
||||
files for a device followed by running the .bat script without flashing the firmware (--debug).
|
||||
If any errors are hit they are presented in the standard output. Investigate accordingly.
|
||||
|
||||
This script needs to be placed in the same directory as .\device-install.bat.
|
||||
|
||||
.EXAMPLE
|
||||
.\device-install_test.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\device-install_test.ps1 -Verbose
|
||||
|
||||
.LINK
|
||||
.\device-install.bat --help
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
function New-EmptyFile() {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
# Specifies the file name.
|
||||
[string]$FileName,
|
||||
[Parameter(Position = 1)]
|
||||
# Specifies the target path. (Get-Location).Path is the default.
|
||||
[string]$Directory = (Get-Location).Path
|
||||
)
|
||||
|
||||
$filePath = Join-Path -Path $Directory -ChildPath $FileName
|
||||
|
||||
Write-Verbose -Message "Create empty test file if it doesn't exist: $($FileName)"
|
||||
New-Item -Path "$filePath" -ItemType File -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
|
||||
function Remove-EmptyFile() {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
# Specifies the file name.
|
||||
[string]$FileName,
|
||||
[Parameter(Position = 1)]
|
||||
# Specifies the target path. (Get-Location).Path is the default.
|
||||
[string]$Directory = (Get-Location).Path
|
||||
)
|
||||
|
||||
$filePath = Join-Path -Path $Directory -ChildPath $FileName
|
||||
|
||||
Write-Verbose -Message "Deleted empty test file: $($FileName)"
|
||||
Remove-Item -Path "$filePath" | Out-Null
|
||||
}
|
||||
|
||||
|
||||
$TestCases = New-Object -TypeName PSObject -Property @{
|
||||
# Use this PSObject to define testcases according to this syntax:
|
||||
# "testname" = @("firmware-testname","bleota","littlefs-testname","args")
|
||||
"t-deck" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-2.6.0.0b106d4.bin", "")
|
||||
"t-deck_web" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-t-deck-2.6.0.0b106d4.bin", "--web")
|
||||
"t-deck-tft" = @("firmware-t-deck-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-tft-2.6.0.0b106d4.bin", "")
|
||||
"heltec-ht62-esp32c3" = @("firmware-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "bleota-c3.bin", "littlefs-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "")
|
||||
"tlora-c6" = @("firmware-tlora-c6-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-tlora-c6-2.6.0.0b106d4.bin", "")
|
||||
"heltec-v3_web" = @("firmware-heltec-v3-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-heltec-v3-2.6.0.0b106d4.bin", "--web")
|
||||
"seeed-sensecap-indicator-tft" = @("firmware-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "")
|
||||
"picomputer-s3-tft" = @("firmware-picomputer-s3-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-picomputer-s3-tft-2.6.0.0b106d4.bin", "")
|
||||
}
|
||||
|
||||
foreach ($TestCase in $TestCases.PSObject.Properties) {
|
||||
$Name = $TestCase.Name
|
||||
$Files = $TestCase.Value
|
||||
$Errors = $null
|
||||
$Counter = 0
|
||||
|
||||
Write-Host -Object "Testcase: $Name`:" -ForegroundColor Green
|
||||
foreach ($File in $Files) {
|
||||
if ($File.EndsWith(".bin")) {
|
||||
New-EmptyFile -FileName $File
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host -Object "Performing test on $Name..." -ForegroundColor Blue
|
||||
$Test = Invoke-Expression -Command "cmd /c .\device-install.bat --debug -f $($TestCases."$Name"[0]) $($TestCases."$Name"[3])"
|
||||
|
||||
foreach ($Line in $Test) {
|
||||
if ($Line -match "Set OTA_OFFSET to" -or `
|
||||
$Line -match "Set SPIFFS_OFFSET to") {
|
||||
Write-Host -Object "$($Line -replace "^.*?Set","Set")" -ForegroundColor Blue
|
||||
}
|
||||
elseif ($VerbosePreference -eq "Continue") {
|
||||
Write-Host -Object $Line
|
||||
}
|
||||
if ($Line -match "ERROR") {
|
||||
$Errors += $Line
|
||||
$Counter++
|
||||
}
|
||||
}
|
||||
if ($null -ne $Errors) {
|
||||
Write-Host -Object "$Counter ERROR(s) detected!" -ForegroundColor Red
|
||||
if (-not ($VerbosePreference -eq "Continue")) { Write-Host -Object $Errors }
|
||||
}
|
||||
|
||||
foreach ($File in $Files) {
|
||||
if ($File.EndsWith(".bin")) {
|
||||
Remove-EmptyFile -FileName $File
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic device-update
|
||||
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "RESET_BAUD=1200"
|
||||
SET "UPDATE_OFFSET=0x10000"
|
||||
SET "ESPTOOL_CMD="
|
||||
SET "LOGCOUNTER=0"
|
||||
SET "CHANGE_MODE=0"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO Flash image file to device, but leave existing system intact.
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--change-mode]
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required)
|
||||
ECHO The file must be located in this current directory.
|
||||
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
|
||||
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||
ECHO If supplied the script will use python.
|
||||
ECHO If not supplied the script will try to find esptool in Path.
|
||||
ECHO --change-mode Attempt to place the device in correct mode. (1200bps Reset)
|
||||
ECHO Some hardware requires this twice.
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
|
||||
GOTO eof
|
||||
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.7.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||
IF /I "%~1"=="--change-mode" SET "CHANGE_MODE=1"
|
||||
SHIFT
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
IF %CHANGE_MODE% EQU 1 GOTO skip-filename
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||
IF "__!FILENAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||
GOTO help
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
|
||||
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
|
||||
GOTO help
|
||||
)
|
||||
@REM Remove ".\" or "./" file prefix if present.
|
||||
SET "FILENAME=!FILENAME:.\=!"
|
||||
SET "FILENAME=!FILENAME:./=!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
|
||||
IF NOT EXIST !FILENAME! (
|
||||
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!"
|
||||
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!."
|
||||
GOTO eof
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!"
|
||||
)
|
||||
|
||||
:skip-filename
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
|
||||
WHERE esptool >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 (
|
||||
@REM WHERE exits with code 0 if esptool is found.
|
||||
SET "ESPTOOL_CMD=esptool"
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=python -m esptool"
|
||||
CALL :RESET_ERROR
|
||||
)
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%"
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
|
||||
IF "__!ESPTOOL_PORT!__" == "____" (
|
||||
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=!ESPTOOL_CMD! --port !ESPTOOL_PORT!"
|
||||
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
|
||||
)
|
||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %CHANGE_MODE% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_ESPTOOL
|
||||
@REM Subroutine used to run ESPTOOL_CMD with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
IF %CHANGE_MODE% EQU 1 GOTO :eof
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
||||
CHANGE_MODE=false
|
||||
|
||||
# Constants
|
||||
FLASH_BAUD=115200
|
||||
RESET_BAUD=1200
|
||||
UPDATE_OFFSET=0x10000
|
||||
|
||||
# Determine the correct esptool command to use
|
||||
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="$PYTHON -m esptool"
|
||||
elif command -v esptool >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool"
|
||||
elif command -v esptool.py >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool.py"
|
||||
else
|
||||
echo "Error: esptool not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# esptool v5 supports commands with dashes and deprecates commands with
|
||||
# underscores. Prior versions only support commands with underscores
|
||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||
then
|
||||
ESPTOOL_WRITE_FLASH=write-flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||
else
|
||||
ESPTOOL_WRITE_FLASH=write_flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||
fi
|
||||
|
||||
# Usage info
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode]
|
||||
Flash image file to device, leave existing system intact."
|
||||
|
||||
-h Display this help and exit
|
||||
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||
-f FILENAME The *.bin file to flash. Custom to your device type.
|
||||
--change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Check for --change-mode and remove it from arguments
|
||||
NEW_ARGS=()
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--change-mode" ]; then
|
||||
CHANGE_MODE=true
|
||||
else
|
||||
NEW_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
set -- "${NEW_ARGS[@]}"
|
||||
|
||||
while getopts ":hp:P:f:" opt; do
|
||||
case "${opt}" in
|
||||
h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
p) ESPTOOL_CMD="$ESPTOOL_CMD --port ${OPTARG}"
|
||||
;;
|
||||
P) PYTHON=${OPTARG}
|
||||
;;
|
||||
f) FILENAME=${OPTARG}
|
||||
;;
|
||||
*)
|
||||
echo "Invalid flag."
|
||||
show_help >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
if [ "$CHANGE_MODE" = true ]; then
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||
FILENAME="$1"
|
||||
shift
|
||||
}
|
||||
|
||||
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
||||
echo "Trying to flash update ${FILENAME}"
|
||||
$ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}"
|
||||
else
|
||||
show_help
|
||||
echo "Invalid file: ${FILENAME}"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
arm-none-eabi-readelf -s -e .pio/build/nrf52dk/firmware.elf | head -80
|
||||
|
||||
nm -CSr --size-sort .pio/build/nrf52dk/firmware.elf | grep '^200'
|
||||
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""ESP Exception Decoder
|
||||
|
||||
github: https://github.com/janLo/EspArduinoExceptionDecoder
|
||||
license: GPL v3
|
||||
author: Jan Losinski
|
||||
|
||||
Meshtastic notes:
|
||||
* original version is at: https://github.com/janLo/EspArduinoExceptionDecoder
|
||||
* version that's checked into meshtastic repo is based on: https://github.com/me21/EspArduinoExceptionDecoder
|
||||
which adds in ESP32 Backtrace decoding.
|
||||
* this also updates the defaults to use ESP32, instead of ESP8266 and defaults to the built firmware.bin
|
||||
* also updated the toolchain name, which will be set according to the platform
|
||||
|
||||
To use, copy the "Backtrace: 0x...." line to a file, e.g., backtrace.txt, then run:
|
||||
$ bin/exception_decoder.py backtrace.txt
|
||||
For a platform other than ESP32, use the -p option, e.g.:
|
||||
$ bin/exception_decoder.py -p ESP32S3 backtrace.txt
|
||||
To specify a specific .elf file, use the -e option, e.g.:
|
||||
$ bin/exception_decoder.py -e firmware.elf backtrace.txt
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
EXCEPTIONS = [
|
||||
"Illegal instruction",
|
||||
"SYSCALL instruction",
|
||||
"InstructionFetchError: Processor internal physical address or data error during instruction fetch",
|
||||
"LoadStoreError: Processor internal physical address or data error during load or store",
|
||||
"Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register",
|
||||
"Alloca: MOVSP instruction, if caller's registers are not in the register file",
|
||||
"IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero",
|
||||
"reserved",
|
||||
"Privileged: Attempt to execute a privileged operation when CRING ? 0",
|
||||
"LoadStoreAlignmentCause: Load or store to an unaligned address",
|
||||
"reserved",
|
||||
"reserved",
|
||||
"InstrPIFDataError: PIF data error during instruction fetch",
|
||||
"LoadStorePIFDataError: Synchronous PIF data error during LoadStore access",
|
||||
"InstrPIFAddrError: PIF address error during instruction fetch",
|
||||
"LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access",
|
||||
"InstTLBMiss: Error during Instruction TLB refill",
|
||||
"InstTLBMultiHit: Multiple instruction TLB entries matched",
|
||||
"InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level less than CRING",
|
||||
"reserved",
|
||||
"InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch",
|
||||
"reserved",
|
||||
"reserved",
|
||||
"reserved",
|
||||
"LoadStoreTLBMiss: Error during TLB refill for a load or store",
|
||||
"LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store",
|
||||
"LoadStorePrivilege: A load or store referenced a virtual address at a ring level less than CRING",
|
||||
"reserved",
|
||||
"LoadProhibited: A load referenced a page mapped with an attribute that does not permit loads",
|
||||
"StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores",
|
||||
]
|
||||
|
||||
PLATFORMS = {
|
||||
"ESP8266": "xtensa-lx106",
|
||||
"ESP32": "xtensa-esp32",
|
||||
"ESP32S3": "xtensa-esp32s3",
|
||||
"ESP32C3": "riscv32-esp",
|
||||
}
|
||||
TOOLS = {
|
||||
"ESP8266": "xtensa",
|
||||
"ESP32": "xtensa-esp32",
|
||||
"ESP32S3": "xtensa-esp32s3",
|
||||
"ESP32C3": "riscv32-esp",
|
||||
}
|
||||
|
||||
BACKTRACE_REGEX = re.compile(
|
||||
r"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-F]{8}\b"
|
||||
)
|
||||
EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$")
|
||||
COUNTER_REGEX = re.compile(
|
||||
"^epc1=(?P<epc1>0x[0-9a-f]+) epc2=(?P<epc2>0x[0-9a-f]+) epc3=(?P<epc3>0x[0-9a-f]+) "
|
||||
"excvaddr=(?P<excvaddr>0x[0-9a-f]+) depc=(?P<depc>0x[0-9a-f]+)$"
|
||||
)
|
||||
CTX_REGEX = re.compile("^ctx: (?P<ctx>.+)$")
|
||||
POINTER_REGEX = re.compile(
|
||||
"^sp: (?P<sp>[0-9a-f]+) end: (?P<end>[0-9a-f]+) offset: (?P<offset>[0-9a-f]+)$"
|
||||
)
|
||||
STACK_BEGIN = ">>>stack>>>"
|
||||
STACK_END = "<<<stack<<<"
|
||||
STACK_REGEX = re.compile(
|
||||
r"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$"
|
||||
)
|
||||
|
||||
StackLine = namedtuple("StackLine", ["offset", "content"])
|
||||
|
||||
|
||||
class ExceptionDataParser(object):
|
||||
def __init__(self):
|
||||
self.exception = None
|
||||
|
||||
self.epc1 = None
|
||||
self.epc2 = None
|
||||
self.epc3 = None
|
||||
self.excvaddr = None
|
||||
self.depc = None
|
||||
|
||||
self.ctx = None
|
||||
|
||||
self.sp = None
|
||||
self.end = None
|
||||
self.offset = None
|
||||
|
||||
self.stack = []
|
||||
|
||||
def _parse_backtrace(self, line):
|
||||
if line.startswith("Backtrace:"):
|
||||
self.stack = [
|
||||
StackLine(offset=0, content=(addr,))
|
||||
for addr in BACKTRACE_REGEX.findall(line)
|
||||
]
|
||||
return None
|
||||
return self._parse_backtrace
|
||||
|
||||
def _parse_exception(self, line):
|
||||
match = EXCEPTION_REGEX.match(line)
|
||||
if match is not None:
|
||||
self.exception = int(match.group("exc"))
|
||||
return self._parse_counters
|
||||
return self._parse_exception
|
||||
|
||||
def _parse_counters(self, line):
|
||||
match = COUNTER_REGEX.match(line)
|
||||
if match is not None:
|
||||
self.epc1 = match.group("epc1")
|
||||
self.epc2 = match.group("epc2")
|
||||
self.epc3 = match.group("epc3")
|
||||
self.excvaddr = match.group("excvaddr")
|
||||
self.depc = match.group("depc")
|
||||
return self._parse_ctx
|
||||
return self._parse_counters
|
||||
|
||||
def _parse_ctx(self, line):
|
||||
match = CTX_REGEX.match(line)
|
||||
if match is not None:
|
||||
self.ctx = match.group("ctx")
|
||||
return self._parse_pointers
|
||||
return self._parse_ctx
|
||||
|
||||
def _parse_pointers(self, line):
|
||||
match = POINTER_REGEX.match(line)
|
||||
if match is not None:
|
||||
self.sp = match.group("sp")
|
||||
self.end = match.group("end")
|
||||
self.offset = match.group("offset")
|
||||
return self._parse_stack_begin
|
||||
return self._parse_pointers
|
||||
|
||||
def _parse_stack_begin(self, line):
|
||||
if line == STACK_BEGIN:
|
||||
return self._parse_stack_line
|
||||
return self._parse_stack_begin
|
||||
|
||||
def _parse_stack_line(self, line):
|
||||
if line != STACK_END:
|
||||
match = STACK_REGEX.match(line)
|
||||
if match is not None:
|
||||
self.stack.append(
|
||||
StackLine(
|
||||
offset=match.group("off"),
|
||||
content=(
|
||||
match.group("c1"),
|
||||
match.group("c2"),
|
||||
match.group("c3"),
|
||||
match.group("c4"),
|
||||
),
|
||||
)
|
||||
)
|
||||
return self._parse_stack_line
|
||||
return None
|
||||
|
||||
def parse_file(self, file, platform, stack_only=False):
|
||||
if platform != "ESP8266":
|
||||
func = self._parse_backtrace
|
||||
else:
|
||||
func = self._parse_exception
|
||||
if stack_only:
|
||||
func = self._parse_stack_begin
|
||||
|
||||
for line in file:
|
||||
func = func(line.strip())
|
||||
if func is None:
|
||||
break
|
||||
|
||||
if func is not None:
|
||||
print("ERROR: Parser not complete!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class AddressResolver(object):
|
||||
def __init__(self, tool_path, elf_path):
|
||||
self._tool = tool_path
|
||||
self._elf = elf_path
|
||||
self._address_map = {}
|
||||
|
||||
def _lookup(self, addresses):
|
||||
cmd = [self._tool, "-aipfC", "-e", self._elf] + [
|
||||
addr for addr in addresses if addr is not None
|
||||
]
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
output = subprocess.check_output(cmd)
|
||||
else:
|
||||
output = subprocess.check_output(cmd, encoding="utf-8")
|
||||
|
||||
line_regex = re.compile("^(?P<addr>[0-9a-fx]+): (?P<result>.+)$")
|
||||
|
||||
last = None
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
match = line_regex.match(line)
|
||||
|
||||
if match is None:
|
||||
if last is not None and line.startswith("(inlined by)"):
|
||||
line = line[12:].strip()
|
||||
self._address_map[last] += "\n \\-> inlined by: " + line
|
||||
continue
|
||||
|
||||
if match.group("result") == "?? ??:0":
|
||||
continue
|
||||
|
||||
self._address_map[match.group("addr")] = match.group("result")
|
||||
last = match.group("addr")
|
||||
|
||||
def fill(self, parser):
|
||||
addresses = [
|
||||
parser.epc1,
|
||||
parser.epc2,
|
||||
parser.epc3,
|
||||
parser.excvaddr,
|
||||
parser.sp,
|
||||
parser.end,
|
||||
parser.offset,
|
||||
]
|
||||
for line in parser.stack:
|
||||
addresses.extend(line.content)
|
||||
|
||||
self._lookup(addresses)
|
||||
|
||||
def _sanitize_addr(self, addr):
|
||||
if addr.startswith("0x"):
|
||||
addr = addr[2:]
|
||||
|
||||
fill = "0" * (8 - len(addr))
|
||||
return "0x" + fill + addr
|
||||
|
||||
def resolve_addr(self, addr):
|
||||
out = self._sanitize_addr(addr)
|
||||
|
||||
if out in self._address_map:
|
||||
out += ": " + self._address_map[out]
|
||||
|
||||
return out
|
||||
|
||||
def resolve_stack_addr(self, addr, full=True):
|
||||
addr = self._sanitize_addr(addr)
|
||||
if addr in self._address_map:
|
||||
return addr + ": " + self._address_map[addr]
|
||||
|
||||
if full:
|
||||
return "[DATA (0x" + addr + ")]"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def print_addr(name, value, resolver):
|
||||
print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value)))
|
||||
|
||||
|
||||
def print_stack_full(lines, resolver):
|
||||
print("stack:")
|
||||
for line in lines:
|
||||
print(str(line.offset) + ":")
|
||||
for content in line.content:
|
||||
print(" " + resolver.resolve_stack_addr(content))
|
||||
|
||||
|
||||
def print_stack(lines, resolver):
|
||||
print("stack:")
|
||||
for line in lines:
|
||||
for content in line.content:
|
||||
out = resolver.resolve_stack_addr(content, full=False)
|
||||
if out is None:
|
||||
continue
|
||||
print(out)
|
||||
|
||||
|
||||
def print_result(parser, resolver, platform, full=True, stack_only=False):
|
||||
if platform == "ESP8266" and not stack_only:
|
||||
print(
|
||||
"Exception: {} ({})".format(parser.exception, EXCEPTIONS[parser.exception])
|
||||
)
|
||||
|
||||
print("")
|
||||
print_addr("epc1", parser.epc1, resolver)
|
||||
print_addr("epc2", parser.epc2, resolver)
|
||||
print_addr("epc3", parser.epc3, resolver)
|
||||
print_addr("excvaddr", parser.excvaddr, resolver)
|
||||
print_addr("depc", parser.depc, resolver)
|
||||
|
||||
print("")
|
||||
print("ctx: " + parser.ctx)
|
||||
|
||||
print("")
|
||||
print_addr("sp", parser.sp, resolver)
|
||||
print_addr("end", parser.end, resolver)
|
||||
print_addr("offset", parser.offset, resolver)
|
||||
|
||||
print("")
|
||||
if full:
|
||||
print_stack_full(parser.stack, resolver)
|
||||
else:
|
||||
print_stack(parser.stack, resolver)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="decode ESP Stacktraces.")
|
||||
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--platform",
|
||||
help="The platform to decode from",
|
||||
choices=PLATFORMS.keys(),
|
||||
default="ESP32",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--tool",
|
||||
help="Path to the toolchain (without specific platform)",
|
||||
default="~/.platformio/packages/toolchain-",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--elf", help="path to elf file", default=".pio/build/tbeam/firmware.elf"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f", "--full", help="Print full stack dump", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--stack_only", help="Decode only a stractrace", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"file",
|
||||
help="The file to read the exception data from ('-' for STDIN)",
|
||||
default="-",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
|
||||
if args.file == "-":
|
||||
file = sys.stdin
|
||||
else:
|
||||
if not os.path.exists(args.file):
|
||||
print("ERROR: file " + args.file + " not found")
|
||||
sys.exit(1)
|
||||
file = open(args.file, "r")
|
||||
|
||||
addr2line = os.path.join(
|
||||
os.path.abspath(os.path.expanduser(args.tool + TOOLS[args.platform])),
|
||||
"bin/" + PLATFORMS[args.platform] + "-elf-addr2line",
|
||||
)
|
||||
if os.name == "nt":
|
||||
addr2line += ".exe"
|
||||
if not os.path.exists(addr2line):
|
||||
print("ERROR: addr2line not found (" + addr2line + ")")
|
||||
|
||||
elf_file = os.path.abspath(os.path.expanduser(args.elf))
|
||||
if not os.path.exists(elf_file):
|
||||
print("ERROR: elf file not found (" + elf_file + ")")
|
||||
|
||||
parser = ExceptionDataParser()
|
||||
resolver = AddressResolver(addr2line, elf_file)
|
||||
|
||||
parser.parse_file(file, args.platform, args.stack_only)
|
||||
resolver.fill(parser)
|
||||
|
||||
print_result(parser, resolver, args.platform, args.full, args.stack_only)
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# regen the design bins first
|
||||
cd design
|
||||
bin/generate-pngs.sh
|
||||
cd ..
|
||||
|
||||
# assumes 50 wide, 28 high
|
||||
convert design/logo/png/Mesh_Logo_Black_Small.png -background white -alpha Background src/graphics/img/icon.xbm
|
||||
|
||||
inkscape --batch-process -o images/compass.png -w 48 -h 48 images/location_searching-24px.svg
|
||||
convert compass.png -background white -alpha Background src/graphics/img/compass.xbm
|
||||
|
||||
inkscape --batch-process -o images/face.png -w 13 -h 13 images/face-24px.svg
|
||||
|
||||
inkscape --batch-process -o images/pin.png -w 13 -h 13 images/room-24px.svg
|
||||
convert pin.png -background white -alpha Background src/graphics/img/pin.xbm
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Generate the CI matrix."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from platformio.project.config import ProjectConfig
|
||||
|
||||
parser = argparse.ArgumentParser(description="Generate the CI matrix")
|
||||
parser.add_argument("platform", help="Platform to build for")
|
||||
parser.add_argument(
|
||||
"--level",
|
||||
choices=["extra", "pr"],
|
||||
nargs="*",
|
||||
default=[],
|
||||
help="Board level to build for (omit for full release boards)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
outlist = []
|
||||
|
||||
cfg = ProjectConfig.get_instance()
|
||||
pio_envs = cfg.envs()
|
||||
|
||||
# Gather all PlatformIO environments for filtering later
|
||||
all_envs = []
|
||||
for pio_env in pio_envs:
|
||||
env_build_flags = cfg.get(f"env:{pio_env}", "build_flags")
|
||||
env_platform = None
|
||||
for flag in env_build_flags:
|
||||
# Extract the platform from the build flags
|
||||
# Example flag: -I variants/esp32s3/heltec-v3
|
||||
match = re.search(r"-I\s?variants/([^/]+)", flag)
|
||||
if match:
|
||||
env_platform = match.group(1)
|
||||
break
|
||||
# Intentionally fail if platform cannot be determined
|
||||
if not env_platform:
|
||||
print(f"Error: Could not determine platform for environment '{pio_env}'")
|
||||
exit(1)
|
||||
# Store env details as a dictionary, and add to 'all_envs' list
|
||||
env = {
|
||||
"ci": {"board": pio_env, "platform": env_platform},
|
||||
"board_level": cfg.get(f"env:{pio_env}", "board_level", default=None),
|
||||
"board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)),
|
||||
}
|
||||
all_envs.append(env)
|
||||
|
||||
# Filter outputs based on options
|
||||
# Check is mutually exclusive with other options (except 'pr')
|
||||
if "check" in args.platform:
|
||||
for env in all_envs:
|
||||
if env["board_check"]:
|
||||
if "pr" in args.level:
|
||||
if env["board_level"] == "pr":
|
||||
outlist.append(env["ci"])
|
||||
else:
|
||||
outlist.append(env["ci"])
|
||||
# Filter (non-check) builds by platform
|
||||
else:
|
||||
for env in all_envs:
|
||||
if args.platform == env["ci"]["platform"] or args.platform == "all":
|
||||
# Always include board_level = 'pr'
|
||||
if env["board_level"] == "pr":
|
||||
outlist.append(env["ci"])
|
||||
# Include board_level = 'extra' when requested
|
||||
elif "extra" in args.level and env["board_level"] == "extra":
|
||||
outlist.append(env["ci"])
|
||||
# If no board level is specified, include in release builds (not PR)
|
||||
elif "pr" not in args.level and not env["board_level"]:
|
||||
outlist.append(env["ci"])
|
||||
|
||||
# Return as a JSON list
|
||||
print(json.dumps(outlist))
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate release notes from merged PRs on develop and master branches.
|
||||
Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_last_release_tag():
|
||||
"""Get the most recent release tag."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--abbrev=0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_tag_date(tag):
|
||||
"""Get the commit date (ISO 8601) of the tag."""
|
||||
result = subprocess.run(
|
||||
["git", "show", "-s", "--format=%cI", tag],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_merged_prs_since_tag(tag, branch):
|
||||
"""Get all merged PRs since the given tag on the specified branch."""
|
||||
# Get commits since tag on the branch - look for PR numbers in parentheses
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"log",
|
||||
f"{tag}..origin/{branch}",
|
||||
"--oneline",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
prs = []
|
||||
seen_pr_numbers = set()
|
||||
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Extract PR number from commit message - format: "Title (#1234)"
|
||||
pr_match = re.search(r"\(#(\d+)\)", line)
|
||||
if pr_match:
|
||||
pr_number = pr_match.group(1)
|
||||
if pr_number not in seen_pr_numbers:
|
||||
seen_pr_numbers.add(pr_number)
|
||||
prs.append(pr_number)
|
||||
|
||||
return prs
|
||||
|
||||
|
||||
def get_pr_details(pr_number):
|
||||
"""Get PR details from GitHub API via gh CLI."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"view",
|
||||
pr_number,
|
||||
"--json",
|
||||
"title,author,labels,url",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
|
||||
def should_exclude_pr(pr_details):
|
||||
"""Check if PR should be excluded from release notes."""
|
||||
if not pr_details:
|
||||
return True
|
||||
|
||||
title = pr_details.get("title", "").lower()
|
||||
|
||||
# Exclude trunk update PRs
|
||||
if "upgrade trunk" in title or "update trunk" in title or "trunk update" in title:
|
||||
return True
|
||||
|
||||
# Exclude protobuf update PRs
|
||||
if "update protobufs" in title or "update protobuf" in title:
|
||||
return True
|
||||
|
||||
# Exclude automated version bump PRs
|
||||
if "bump release version" in title or "bump version" in title:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_dependency_update(pr_details):
|
||||
"""Check if PR is a dependency/chore update."""
|
||||
if not pr_details:
|
||||
return False
|
||||
|
||||
title = pr_details.get("title", "").lower()
|
||||
author = pr_details.get("author", {}).get("login", "").lower()
|
||||
labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])]
|
||||
|
||||
# Check for renovate or dependabot authors
|
||||
if "renovate" in author or "dependabot" in author:
|
||||
return True
|
||||
|
||||
# Check for chore(deps) pattern
|
||||
if re.match(r"^chore\(deps\):", title):
|
||||
return True
|
||||
|
||||
# Check for digest update patterns
|
||||
if re.match(r".*digest to [a-f0-9]+", title, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check for dependency-related labels
|
||||
dependency_labels = ["dependencies", "deps", "renovate"]
|
||||
if any(dep in label for label in labels for dep in dependency_labels):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_enhancement(pr_details):
|
||||
"""Determine if PR is an enhancement based on labels and title."""
|
||||
labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])]
|
||||
|
||||
# Check labels first
|
||||
enhancement_labels = ["enhancement", "feature", "feat", "new feature"]
|
||||
for label in labels:
|
||||
if any(enh in label for enh in enhancement_labels):
|
||||
return True
|
||||
|
||||
# Check title prefixes
|
||||
title = pr_details.get("title", "")
|
||||
enhancement_prefixes = ["feat:", "feature:", "add:"]
|
||||
title_lower = title.lower()
|
||||
for prefix in enhancement_prefixes:
|
||||
if title_lower.startswith(prefix) or f" {prefix}" in title_lower:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def clean_title(title):
|
||||
"""Clean up PR title for release notes."""
|
||||
# Remove common prefixes
|
||||
prefixes_to_remove = [
|
||||
r"^fix:\s*",
|
||||
r"^feat:\s*",
|
||||
r"^feature:\s*",
|
||||
r"^bug:\s*",
|
||||
r"^bugfix:\s*",
|
||||
r"^chore:\s*",
|
||||
r"^chore\([^)]+\):\s*",
|
||||
r"^refactor:\s*",
|
||||
r"^docs:\s*",
|
||||
r"^ci:\s*",
|
||||
r"^build:\s*",
|
||||
r"^perf:\s*",
|
||||
r"^style:\s*",
|
||||
r"^test:\s*",
|
||||
]
|
||||
|
||||
cleaned = title
|
||||
for prefix in prefixes_to_remove:
|
||||
cleaned = re.sub(prefix, "", cleaned, flags=re.IGNORECASE)
|
||||
|
||||
# Ensure first letter is capitalized
|
||||
if cleaned:
|
||||
cleaned = cleaned[0].upper() + cleaned[1:]
|
||||
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def format_pr_line(pr_details):
|
||||
"""Format a PR as a markdown bullet point."""
|
||||
title = clean_title(pr_details.get("title", "Unknown"))
|
||||
author = pr_details.get("author", {}).get("login", "unknown")
|
||||
url = pr_details.get("url", "")
|
||||
|
||||
return f"- {title} by @{author} in {url}"
|
||||
|
||||
|
||||
def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"):
|
||||
"""Find contributors who made their first merged PR before this release.
|
||||
|
||||
GitHub usernames do not necessarily match git commit authors, so we use the
|
||||
GitHub search API via `gh` to see if the user has any merged PRs before the
|
||||
tag date. This mirrors how GitHub's "Generate release notes" feature works.
|
||||
"""
|
||||
|
||||
bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"}
|
||||
|
||||
new_contributors = []
|
||||
seen_authors = set()
|
||||
|
||||
try:
|
||||
tag_date = get_tag_date(tag)
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"Warning: Could not determine tag date for {tag}; skipping new contributor detection", file=sys.stderr)
|
||||
return []
|
||||
|
||||
for pr in pr_details_list:
|
||||
author = pr.get("author", {}).get("login", "")
|
||||
if not author or author in seen_authors:
|
||||
continue
|
||||
|
||||
# Skip bots
|
||||
if author.lower() in bot_authors or author.startswith("app/"):
|
||||
continue
|
||||
|
||||
seen_authors.add(author)
|
||||
|
||||
try:
|
||||
# Search for merged PRs by this author created before the tag date
|
||||
search_query = f"is:pr author:{author} repo:{repo} closed:<=\"{tag_date}\""
|
||||
search = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"search",
|
||||
"issues",
|
||||
"--json",
|
||||
"number,mergedAt,createdAt",
|
||||
"--state",
|
||||
"closed",
|
||||
"--limit",
|
||||
"200",
|
||||
search_query,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if search.returncode != 0:
|
||||
# If gh fails, be conservative and skip adding to new contributors
|
||||
print(f"Warning: gh search failed for author {author}: {search.stderr.strip()}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
results = json.loads(search.stdout or "[]")
|
||||
# If any merged PR exists before or on tag date, not a new contributor
|
||||
had_prior_pr = any(item.get("mergedAt") for item in results)
|
||||
|
||||
if not had_prior_pr:
|
||||
new_contributors.append((author, pr.get("url", "")))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
return new_contributors
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: generate_release_notes.py <new_version>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
new_version = sys.argv[1]
|
||||
|
||||
# Get last release tag
|
||||
try:
|
||||
last_tag = get_last_release_tag()
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: Could not find last release tag", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Collect PRs from both branches
|
||||
all_pr_numbers = set()
|
||||
|
||||
for branch in ["develop", "master"]:
|
||||
try:
|
||||
prs = get_merged_prs_since_tag(last_tag, branch)
|
||||
all_pr_numbers.update(prs)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr)
|
||||
|
||||
# Get details for all PRs
|
||||
enhancements = []
|
||||
bug_fixes = []
|
||||
dependencies = []
|
||||
all_pr_details = []
|
||||
|
||||
for pr_number in sorted(all_pr_numbers, key=int):
|
||||
details = get_pr_details(pr_number)
|
||||
if details and not should_exclude_pr(details):
|
||||
all_pr_details.append(details)
|
||||
if is_dependency_update(details):
|
||||
dependencies.append(details)
|
||||
elif is_enhancement(details):
|
||||
enhancements.append(details)
|
||||
else:
|
||||
bug_fixes.append(details)
|
||||
|
||||
# Generate release notes
|
||||
output = []
|
||||
|
||||
if enhancements:
|
||||
output.append("## 🚀 Enhancements\n")
|
||||
for pr in enhancements:
|
||||
output.append(format_pr_line(pr))
|
||||
output.append("")
|
||||
|
||||
if bug_fixes:
|
||||
output.append("## 🐛 Bug fixes and maintenance\n")
|
||||
for pr in bug_fixes:
|
||||
output.append(format_pr_line(pr))
|
||||
output.append("")
|
||||
|
||||
if dependencies:
|
||||
output.append("## ⚙️ Dependencies\n")
|
||||
for pr in dependencies:
|
||||
output.append(format_pr_line(pr))
|
||||
output.append("")
|
||||
|
||||
# Find new contributors (GitHub-accurate check using merged PRs before tag date)
|
||||
new_contributors = get_new_contributors(all_pr_details, last_tag)
|
||||
if new_contributors:
|
||||
output.append("## New Contributors\n")
|
||||
for author, url in new_contributors:
|
||||
# Find first PR URL for this contributor
|
||||
first_pr_url = url
|
||||
for pr in all_pr_details:
|
||||
if pr.get("author", {}).get("login") == author:
|
||||
first_pr_url = pr.get("url", url)
|
||||
break
|
||||
output.append(f"- @{author} made their first contribution in {first_pr_url}")
|
||||
output.append("")
|
||||
|
||||
# Add full changelog link
|
||||
output.append(
|
||||
f"**Full Changelog**: https://github.com/meshtastic/firmware/compare/{last_tag}...v{new_version}"
|
||||
)
|
||||
|
||||
print("\n".join(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
# This is a layout for 4MB of flash
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# nvs, data, nvs, 0x9000, 0x6000,
|
||||
# otadata, data, ota, , 0x2000,
|
||||
# app0, app, ota_0, , 0x1c0000,
|
||||
# app1, app, ota_1, , 0x1c0000,
|
||||
# spiffs, data, spiffs, , 0x06f000,
|
||||
|
||||
start = 0x9000
|
||||
nvssys = 0x3000
|
||||
nvsuser = 0x2000 # NOTE: ti seems total size of nvssys MUST be 0x5000 or device will bootloop
|
||||
nvs = nvssys + nvsuser
|
||||
ota = 0x2000
|
||||
# app = 0x1c0000
|
||||
spi = 128 * 1024
|
||||
|
||||
# treat sys part sizes + spiffs size as reserved, then calculate what appsize can be
|
||||
reserved = start + nvs + ota + spi
|
||||
maxsize = 0x400000 # 4MB
|
||||
|
||||
app = (maxsize - reserved) / 2
|
||||
|
||||
# total = start + nvs + ota + 2 * app + spi
|
||||
|
||||
nvskb = nvsuser / 1024
|
||||
spikb = spi / 1024
|
||||
appkb = app / 1024
|
||||
|
||||
table = """
|
||||
# This is autogenerated by genpartions.py - change that tool instead!
|
||||
# appsize={appkb} KB, spiffs={spikb} KB, usernvs={nvskb} KB
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x{start:x}, 0x{nvs:x},
|
||||
otadata, data, ota, , 0x{ota:x},
|
||||
app0, app, ota_0, , 0x{app:x},
|
||||
app1, app, ota_1, , 0x{app:x},
|
||||
spiffs, data, spiffs, , 0x{spi:x} """.format(**locals())
|
||||
|
||||
print(table)
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to cancel all running GitHub Actions workflows
|
||||
# Requires GitHub CLI (gh) to be installed and authenticated
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if gh CLI is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
print_error "GitHub CLI (gh) is not installed. Please install it first:"
|
||||
echo " brew install gh"
|
||||
echo " Or visit: https://cli.github.com/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if authenticated
|
||||
if ! gh auth status &> /dev/null; then
|
||||
print_error "GitHub CLI is not authenticated. Please run:"
|
||||
echo " gh auth login"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get repository info
|
||||
REPO=$(gh repo view --json owner,name -q '.owner.login + "/" + .name')
|
||||
if [[ -z "$REPO" ]]; then
|
||||
print_error "Could not determine repository. Make sure you're in a GitHub repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "Working with repository: $REPO"
|
||||
|
||||
# Get all active workflows (both queued and in-progress)
|
||||
print_status "Fetching active workflows (queued and in-progress)..."
|
||||
QUEUED_WORKFLOWS=$(gh run list --status queued --json databaseId,displayTitle,headBranch,status --limit 100)
|
||||
IN_PROGRESS_WORKFLOWS=$(gh run list --status in_progress --json databaseId,displayTitle,headBranch,status --limit 100)
|
||||
|
||||
# Combine both lists
|
||||
ALL_WORKFLOWS=$(echo "$QUEUED_WORKFLOWS $IN_PROGRESS_WORKFLOWS" | jq -s 'add | unique_by(.databaseId)')
|
||||
|
||||
if [[ "$ALL_WORKFLOWS" == "[]" ]]; then
|
||||
print_status "No active workflows found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse and display active workflows
|
||||
echo
|
||||
print_warning "Found active workflows:"
|
||||
echo "$ALL_WORKFLOWS" | jq -r '.[] | " - \(.displayTitle) (Branch: \(.headBranch), Status: \(.status), ID: \(.databaseId))"'
|
||||
|
||||
echo
|
||||
read -p "Do you want to cancel ALL these workflows? (y/N): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_status "Cancelled by user."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Cancel each workflow
|
||||
print_status "Cancelling workflows..."
|
||||
CANCELLED_COUNT=0
|
||||
FAILED_COUNT=0
|
||||
|
||||
while IFS= read -r WORKFLOW_ID; do
|
||||
if [[ -n "$WORKFLOW_ID" ]]; then
|
||||
print_status "Cancelling workflow ID: $WORKFLOW_ID"
|
||||
if gh run cancel "$WORKFLOW_ID" 2>/dev/null; then
|
||||
((CANCELLED_COUNT++))
|
||||
else
|
||||
print_error "Failed to cancel workflow ID: $WORKFLOW_ID"
|
||||
((FAILED_COUNT++))
|
||||
fi
|
||||
fi
|
||||
done < <(echo "$ALL_WORKFLOWS" | jq -r '.[].databaseId')
|
||||
|
||||
echo
|
||||
print_status "Summary:"
|
||||
echo " - Cancelled: $CANCELLED_COUNT workflows"
|
||||
if [[ $FAILED_COUNT -gt 0 ]]; then
|
||||
echo " - Failed: $FAILED_COUNT workflows"
|
||||
fi
|
||||
|
||||
print_status "Done!"
|
||||
|
||||
# Optional: Show remaining active workflows
|
||||
echo
|
||||
print_status "Checking for any remaining active workflows..."
|
||||
REMAINING_QUEUED=$(gh run list --status queued --json databaseId --limit 10)
|
||||
REMAINING_IN_PROGRESS=$(gh run list --status in_progress --json databaseId --limit 10)
|
||||
REMAINING_ALL=$(echo "$REMAINING_QUEUED $REMAINING_IN_PROGRESS" | jq -s 'add | unique_by(.databaseId)')
|
||||
|
||||
if [[ "$REMAINING_ALL" == "[]" ]]; then
|
||||
print_status "All workflows successfully cancelled."
|
||||
else
|
||||
REMAINING_COUNT=$(echo "$REMAINING_ALL" | jq '. | length')
|
||||
print_warning "Still $REMAINING_COUNT workflows active (may take a moment to update status)"
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Meshtastic %i Daemon
|
||||
After=network-online.target
|
||||
StartLimitInterval=200
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
User=meshtasticd
|
||||
Group=meshtasticd
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/meshtasticd-start.sh %i
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
pio run --environment native
|
||||
gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/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
|
||||
else
|
||||
cp bin/config-dist.yaml /etc/meshtasticd/config.yaml
|
||||
fi
|
||||
cp bin/meshtasticd.service /usr/lib/systemd/system/meshtasticd.service
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
pio run --environment native
|
||||
.pio/build/native/meshtasticd "$@"
|
||||
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Name=Meshtastic
|
||||
Comment=Meshtastic App
|
||||
Exec=meshtasticd
|
||||
Icon=org.meshtastic.meshtasticd
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Network;Chat;HamRadio;
|
||||
@@ -0,0 +1,184 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<component type="desktop-application">
|
||||
<id>org.meshtastic.meshtasticd</id>
|
||||
|
||||
<name>Meshtastic</name>
|
||||
<summary>Decentralized mesh communication</summary>
|
||||
|
||||
<metadata_license>CC-BY-4.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
|
||||
<developer id="org.meshtastic">
|
||||
<name>Meshtastic</name>
|
||||
</developer>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
Meshtastic is an open source project for creating off-grid, affordable, and resilient communication with LoRa mesh networks.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">org.meshtastic.meshtasticd.desktop</launchable>
|
||||
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
<category>Chat</category>
|
||||
<category>HamRadio</category>
|
||||
</categories>
|
||||
<keywords>
|
||||
<keyword>mesh</keyword>
|
||||
<keyword>LoRa</keyword>
|
||||
</keywords>
|
||||
|
||||
<recommends>
|
||||
<control>keyboard</control>
|
||||
<control>pointing</control>
|
||||
<control>touch</control>
|
||||
</recommends>
|
||||
<requires>
|
||||
<display_length compare="ge">360</display_length>
|
||||
</requires>
|
||||
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#97be89</color>
|
||||
<color type="primary" scheme_preference="dark">#206538</color>
|
||||
</branding>
|
||||
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
<content_attribute id="social-location">intense</content_attribute>
|
||||
</content_rating>
|
||||
|
||||
<url type="bugtracker">https://github.com/meshtastic/firmware/issues</url>
|
||||
<url type="homepage">https://meshtastic.org/</url>
|
||||
<url type="donation">https://opencollective.com/meshtastic</url>
|
||||
<url type="faq">https://meshtastic.org/docs/software/linux/usage/</url>
|
||||
<url type="vcs-browser">https://github.com/meshtastic/firmware/</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_home_dashboard_dark.webp</image>
|
||||
<caption>Home Dashboard</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_initial_boot.webp</image>
|
||||
<caption>Setup</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_node_list_dark.webp</image>
|
||||
<caption>Nodes List</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_chat_list_dark.webp</image>
|
||||
<caption>Chats List</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_chat_message_dark.webp</image>
|
||||
<caption>Messages</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_map_dark.webp</image>
|
||||
<caption>Map</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://meshtastic.org/img/software/meshtastic-ui/mui_settings_dark.webp</image>
|
||||
<caption>Settings</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.20" date="2026-02-11">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
|
||||
</release>
|
||||
<release version="2.7.19" date="2026-01-22">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
||||
</release>
|
||||
<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>
|
||||
<release version="2.7.16" date="2025-11-19">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16</url>
|
||||
</release>
|
||||
<release version="2.7.15" date="2025-11-13">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.15</url>
|
||||
</release>
|
||||
<release version="2.7.14" date="2025-11-03">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.14</url>
|
||||
</release>
|
||||
<release version="2.7.13" date="2025-10-11">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.13</url>
|
||||
</release>
|
||||
<release version="2.7.12" date="2025-10-01">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12</url>
|
||||
</release>
|
||||
<release version="2.7.11" date="2025-09-24">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11</url>
|
||||
</release>
|
||||
<release version="2.7.10" date="2025-09-18">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10</url>
|
||||
</release>
|
||||
<release version="2.7.9" date="2025-09-03">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
|
||||
</release>
|
||||
<release version="2.7.8" date="2025-08-30">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8</url>
|
||||
</release>
|
||||
<release version="2.7.7" date="2025-08-28">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7</url>
|
||||
</release>
|
||||
<release version="2.7.6" date="2025-08-12">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
|
||||
</release>
|
||||
<release version="2.7.5" date="2025-08-09">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5</url>
|
||||
</release>
|
||||
<release version="2.7.4" date="2025-07-19">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4</url>
|
||||
</release>
|
||||
<release version="2.7.3" date="2025-07-10">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3</url>
|
||||
</release>
|
||||
<release version="2.7.2" date="2025-07-04">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2</url>
|
||||
</release>
|
||||
<release version="2.7.1" date="2025-06-27">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1</url>
|
||||
</release>
|
||||
<release version="2.7.0" date="2025-06-20">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0</url>
|
||||
</release>
|
||||
<release version="2.6.13" date="2025-06-16">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.13</url>
|
||||
</release>
|
||||
<release version="2.6.12" date="2025-06-15">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.12</url>
|
||||
</release>
|
||||
<release version="2.6.11" date="2025-06-02">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.11</url>
|
||||
</release>
|
||||
<release version="2.6.10" date="2025-05-25">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10</url>
|
||||
</release>
|
||||
<release version="2.6.9" date="2025-05-15">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.9</url>
|
||||
</release>
|
||||
<release version="2.6.8" date="2025-05-05">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.8</url>
|
||||
</release>
|
||||
<release version="2.6.7" date="2025-04-28">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.7</url>
|
||||
</release>
|
||||
<release version="2.6.6" date="2025-04-15">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.6</url>
|
||||
</release>
|
||||
<release version="2.6.5" date="2025-03-30">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.5</url>
|
||||
</release>
|
||||
<release version="2.6.4" date="2025-03-28">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.4</url>
|
||||
</release>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="512" height="512" viewBox="0 0 512 512" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 4.6.0</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="matrix(1 0 0 1 256 256)" id="xYQ9Gk9Jwpgj_HMOXB3F_" >
|
||||
<path style="stroke: rgb(213,130,139); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(103,234,148); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-256, -256)" d="M 0 0 L 512 0 L 512 512 L 0 512 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.79 0 0 1.79 313.74 258.36)" id="1xBsk2n9FZp60Rz1O-ceJ" >
|
||||
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: round; stroke-miterlimit: 2; fill: rgb(44,45,60); fill-rule: evenodd; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-250.97, -362.41)" d="M 250.908 330.267 L 193.126 415.005 L 180.938 406.694 L 244.802 313.037 C 246.174 311.024 248.453 309.819 250.889 309.816 C 253.326 309.814 255.606 311.015 256.982 313.026 L 320.994 406.536 L 308.821 414.869 L 250.908 330.267 Z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.81 0 0 1.81 145 256.15)" id="KxN7E9YpbyPgz0S4z4Cl6" >
|
||||
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: round; stroke-miterlimit: 2; fill: rgb(44,45,60); fill-rule: evenodd; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-115.14, -528.06)" d="M 87.642 581.398 L 154.757 482.977 L 142.638 474.713 L 75.523 573.134 L 87.642 581.398 Z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||
import sys
|
||||
from os.path import join
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from readprops import readProps
|
||||
|
||||
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"
|
||||
|
||||
# Mapping of bin files to their target partition names
|
||||
# Maps the filename pattern to the partition name where it should be flashed
|
||||
partition_map = {
|
||||
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
|
||||
lfsbin: "spiffs", # filesystem image flashed to spiffs
|
||||
}
|
||||
|
||||
check_paths = [
|
||||
progname,
|
||||
f"{progname}.elf",
|
||||
f"{progname}.bin",
|
||||
f"{progname}.factory.bin",
|
||||
f"{progname}.hex",
|
||||
f"{progname}.merged.hex",
|
||||
f"{progname}.uf2",
|
||||
f"{progname}.factory.uf2",
|
||||
f"{progname}.zip",
|
||||
lfsbin,
|
||||
f"mt-{board_mcu}-ota.bin",
|
||||
"bleota-c3.bin"
|
||||
]
|
||||
for p in check_paths:
|
||||
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
|
||||
if f.exists():
|
||||
manifest_name = p
|
||||
if needs_ota_suffix and p == f"{progname}.zip":
|
||||
manifest_name = f"{progname}-ota.zip"
|
||||
d = {
|
||||
"name": manifest_name,
|
||||
"md5": f.get_content_hash(), # Returns MD5 hash
|
||||
"bytes": f.get_size() # Returns file size in bytes
|
||||
}
|
||||
# Add part_name if this file represents a partition that should be flashed
|
||||
if p in partition_map:
|
||||
d["part_name"] = partition_map[p]
|
||||
out.append(d)
|
||||
print(d)
|
||||
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,
|
||||
"platformioTarget": env.get("PIOENV"),
|
||||
"mcu": env.get("BOARD_MCU"),
|
||||
"repo": repo_owner,
|
||||
"files": files,
|
||||
"has_mui": False,
|
||||
"has_inkhud": False,
|
||||
}
|
||||
# Get partition table (generated in esp32_pre.py) if it exists
|
||||
if env.get("custom_mtjson_part"):
|
||||
# custom_mtjson_part is a JSON string, convert it back to a dict
|
||||
pj = json.loads(env.get("custom_mtjson_part"))
|
||||
manifest["part"] = pj
|
||||
# Enable has_mui for TFT builds
|
||||
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||
manifest["has_mui"] = True
|
||||
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)
|
||||
|
||||
Import("projenv")
|
||||
|
||||
prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
|
||||
verObj = readProps(prefsLoc)
|
||||
print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}")
|
||||
|
||||
# get repository owner if git is installed
|
||||
try:
|
||||
r_owner = (
|
||||
subprocess.check_output(["git", "config", "--get", "remote.origin.url"])
|
||||
.decode("utf-8")
|
||||
.strip().split("/")
|
||||
)
|
||||
repo_owner = r_owner[-2] + "/" + r_owner[-1].replace(".git", "")
|
||||
except subprocess.CalledProcessError:
|
||||
repo_owner = "unknown"
|
||||
|
||||
jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc"
|
||||
with open(jsonLoc) as f:
|
||||
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
|
||||
userPrefs = json.loads(jsonStr)
|
||||
|
||||
pref_flags = []
|
||||
# Pre-process the userPrefs
|
||||
for pref in userPrefs:
|
||||
if userPrefs[pref].startswith("{"):
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref].lstrip("-").replace(".", "").isdigit():
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref] == "true" or userPrefs[pref] == "false":
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref].startswith("meshtastic_"):
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
# If the value is a string, we need to wrap it in quotes
|
||||
else:
|
||||
pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
|
||||
|
||||
# General options that are passed to the C and C++ compilers
|
||||
# Calculate unix epoch for current day (midnight)
|
||||
current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
build_epoch = int(current_date.timestamp())
|
||||
|
||||
flags = [
|
||||
"-DAPP_VERSION=" + verObj["long"],
|
||||
"-DAPP_VERSION_SHORT=" + verObj["short"],
|
||||
"-DAPP_ENV=" + env.get("PIOENV"),
|
||||
"-DAPP_REPO=" + repo_owner,
|
||||
"-DBUILD_EPOCH=" + str(build_epoch),
|
||||
] + pref_flags
|
||||
|
||||
print("Using flags:")
|
||||
for flag in flags:
|
||||
print(flag)
|
||||
|
||||
projenv.Append(
|
||||
CCFLAGS=flags,
|
||||
)
|
||||
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name == "meshtastic-device-ui":
|
||||
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
||||
break
|
||||
|
||||
# Get the display resolution from macros
|
||||
def get_display_resolution(build_flags):
|
||||
# Check "DISPLAY_SIZE" to determine the screen resolution
|
||||
for flag in build_flags:
|
||||
if isinstance(flag, tuple) and flag[0] == "DISPLAY_SIZE":
|
||||
screen_width, screen_height = map(int, flag[1].split("x"))
|
||||
return screen_width, screen_height
|
||||
print("No screen resolution defined in build_flags. Please define DISPLAY_SIZE.")
|
||||
exit(1)
|
||||
|
||||
def load_boot_logo(source, target, env):
|
||||
build_flags = env.get("CPPDEFINES", [])
|
||||
logo_w, logo_h = get_display_resolution(build_flags)
|
||||
print(f"TFT build with {logo_w}x{logo_h} resolution detected")
|
||||
|
||||
# Load the boot logo from `branding/logo_<width>x<height>.png` if it exists
|
||||
source_path = join(env["PROJECT_DIR"], "branding", f"logo_{logo_w}x{logo_h}.png")
|
||||
dest_dir = join(env["PROJECT_DIR"], "data", "boot")
|
||||
dest_path = join(dest_dir, "logo.png")
|
||||
if env.File(source_path).exists():
|
||||
print(f"Loading boot logo from {source_path}")
|
||||
# Prepare the destination
|
||||
env.Execute(f"mkdir -p {dest_dir} && rm -f {dest_path}")
|
||||
# Copy the logo to the `data/boot` directory
|
||||
env.Execute(f"cp {source_path} {dest_path}")
|
||||
|
||||
# Load the boot logo on TFT builds
|
||||
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||
env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
|
||||
|
||||
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(
|
||||
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
|
||||
)
|
||||
mtjson_deps.append(target_lfs)
|
||||
|
||||
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")
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||
Import("env")
|
||||
platform = env.PioPlatform()
|
||||
|
||||
if platform.name == "native":
|
||||
env.Replace(PROGNAME="meshtasticd")
|
||||
else:
|
||||
from readprops import readProps
|
||||
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
|
||||
verObj = readProps(prefsLoc)
|
||||
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
|
||||
env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
|
||||
|
||||
# Print the new program name for verification
|
||||
print(f"PROGNAME: {env.get('PROGNAME')}")
|
||||
if platform.name == "espressif32":
|
||||
print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "This script is only for developers who are publishing new builds on github. Most users don't need it"
|
||||
|
||||
VERSION=`bin/buildinfo.py long`
|
||||
|
||||
# Must have a V prefix to trigger github
|
||||
git tag "v${VERSION}"
|
||||
|
||||
git push origin "v${VERSION}" # push the tag
|
||||
|
||||
echo "Tag ${VERSION} pushed to github, github actions should now be building the draft release. If it seems good, click to publish it"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
esptool.py --baud 115200 read_flash 0x1000 0xf000 system-info.img
|
||||
@@ -0,0 +1,46 @@
|
||||
import configparser
|
||||
import subprocess
|
||||
import os
|
||||
run_number = os.getenv('GITHUB_RUN_NUMBER', '0')
|
||||
build_location = os.getenv('BUILD_LOCATION', 'local')
|
||||
|
||||
def readProps(prefsLoc):
|
||||
"""Read the version of our project as a string"""
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(prefsLoc)
|
||||
version = dict(config.items("VERSION"))
|
||||
verObj = dict(
|
||||
short="{}.{}.{}".format(version["major"], version["minor"], version["build"]),
|
||||
long="unset",
|
||||
deb="unset",
|
||||
)
|
||||
|
||||
# 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=7", "HEAD"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
isDirty = (
|
||||
subprocess.check_output(["git", "diff", "HEAD"]).decode("utf-8").strip()
|
||||
)
|
||||
suffix = sha
|
||||
# if isDirty:
|
||||
# # short for 'dirty', we want to keep our verstrings source for protobuf reasons
|
||||
# suffix = sha + "-d"
|
||||
verObj["long"] = "{}.{}".format(verObj["short"], suffix)
|
||||
verObj["deb"] = "{}.{}~{}{}".format(verObj["short"], run_number, build_location, sha)
|
||||
except:
|
||||
# print("Unexpected error:", sys.exc_info()[0])
|
||||
# traceback.print_exc()
|
||||
verObj["long"] = verObj["short"]
|
||||
verObj["deb"] = "{}.{}~{}".format(verObj["short"], run_number, build_location)
|
||||
|
||||
# print("firmware version " + verStr)
|
||||
return verObj
|
||||
|
||||
|
||||
# print("path is" + ','.join(sys.path))
|
||||
@@ -0,0 +1,10 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
cd protobufs
|
||||
..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto
|
||||
GOTO eof
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "This script requires https://jpa.kapsi.fi/nanopb/download/ version 0.4.9 to be located in the"
|
||||
echo "firmware root directory if the following step fails, you should download the correct"
|
||||
echo "prebuilt binaries for your computer into nanopb-0.4.9"
|
||||
|
||||
# the nanopb tool seems to require that the .options file be in the current directory!
|
||||
cd protobufs
|
||||
../nanopb-0.4.9/generator-bin/protoc --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:../src/mesh/generated/" -I=../protobufs meshtastic/*.proto
|
||||
@@ -0,0 +1,16 @@
|
||||
function meshtastic_version {
|
||||
meshtastic_version=$(python3 bin/buildinfo.py short)
|
||||
echo -n "$meshtastic_version"
|
||||
}
|
||||
function web_version {
|
||||
web_version=$(cat bin/web.version)
|
||||
echo -n "$web_version"
|
||||
}
|
||||
function git_commits_num {
|
||||
total_commits=$(git rev-list --all --count)
|
||||
echo -n "$total_commits"
|
||||
}
|
||||
function git_commit_sha {
|
||||
commit_sha=$(git rev-parse --short HEAD)
|
||||
echo -n "$commit_sha"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
# shellcheck shell=bash
|
||||
# (this minor script is actually shell agnostic, and is intended to be sourced rather than run in a subshell)
|
||||
|
||||
# This is a little script you can source if you want to make ESP debugging work on a modern (24.04) ubuntu machine
|
||||
# It assumes you have built and installed python 2.7 from source with:
|
||||
# ./configure --enable-optimizations --enable-shared --enable-unicode=ucs4
|
||||
# sudo make clean
|
||||
# make
|
||||
# sudo make altinstall
|
||||
|
||||
export LD_LIBRARY_PATH=$HOME/packages/python-2.7.18/
|
||||
export PYTHON_HOME=/usr/local/lib/python2.7/
|
||||
@@ -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)
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts).
|
||||
#
|
||||
# Usage:
|
||||
# ./bin/test-native-docker.sh # run all native tests
|
||||
# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter
|
||||
# ./bin/test-native-docker.sh --rebuild # force rebuild the image
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
IMAGE_NAME="meshtastic-native-test"
|
||||
|
||||
REBUILD=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--rebuild" ]]; then
|
||||
REBUILD=true
|
||||
else
|
||||
EXTRA_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
|
||||
echo "Building test image (first run may take a few minutes)..."
|
||||
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR"
|
||||
fi
|
||||
|
||||
# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI)
|
||||
sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/'
|
||||
|
||||
# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through.
|
||||
if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then
|
||||
CMD=("platformio" "test" "-e" "coverage" "-v")
|
||||
else
|
||||
CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}")
|
||||
fi
|
||||
|
||||
exec docker run --rm \
|
||||
-v "$ROOT_DIR:/src:ro" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Starting simulator"
|
||||
.pio/build/native/meshtasticd -s &
|
||||
sleep 20 # 5 seconds was not enough
|
||||
|
||||
echo "Simulator started, launching python test..."
|
||||
python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic uf2-convert
|
||||
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "NRF=0"
|
||||
SET "UF2CONV_CMD=python3 .\bin\uf2conv.py"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -t [t-echo^|rak4631^|nano-g2-ultra^|wio-tracker-wm1110^|canaryone^|
|
||||
ECHO heltec-mesh-node-t114^|tracker-t1000-e^|rak_wismeshtap^|rak2560^|
|
||||
ECHO nrf52_promicro_diy_tcxo]
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -t target Specify a platformio NRF target to build for. (required)
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -t rak4631
|
||||
GOTO eof
|
||||
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.6.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-t" SET "TARGETNAME=%~2" & SHIFT
|
||||
IF /I "%~1"=="--target" SET "TARGETNAME=%~2" & SHIFT
|
||||
SHIFT
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking TARGETNAME parameter..."
|
||||
IF "__!TARGETNAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -t target input."
|
||||
GOTO help
|
||||
)
|
||||
|
||||
IF %DEBUG% EQU 1 SET "UF2CONV_CMD=REM python3 .\bin\uf2conv.py"
|
||||
|
||||
SET "NRFTARGETS=t-echo rak4631 nano-g2-ultra wio-tracker-wm1110 canaryone heltec-mesh-node-t114 tracker-t1000-e rak_wismeshtap rak2560 nrf52_promicro_diy_tcxo"
|
||||
FOR %%a IN (%NRFTARGETS%) DO (
|
||||
IF /I "%%a"=="!TARGETNAME!" (
|
||||
@REM We are working with any of %NRFTARGETS%.
|
||||
SET "NRF=1"
|
||||
GOTO end_loop_nrf
|
||||
)
|
||||
)
|
||||
:end_loop_nrf
|
||||
|
||||
@REM Building operations.
|
||||
IF !NRF! EQU 1 (
|
||||
CALL :LOG_MESSAGE INFO "Trying to build for !TARGETNAME!..."
|
||||
CALL :RUN_UF2CONV !TARGETNAME! || GOTO eof
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE WARN "!TARGETNAME! is not supported..."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_UF2CONV
|
||||
@REM Subroutine used to run .\bin\uf2conv.py with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_UF2CONV [target]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_UF2CONV rak4631
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
|
||||
CALL :RESET_ERROR
|
||||
!UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
||||
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
|
||||
UF2_MAGIC_END = 0x0AB16F30 # Ditto
|
||||
|
||||
families = {
|
||||
"SAMD21": 0x68ED2B88,
|
||||
"SAML21": 0x1851780A,
|
||||
"SAMD51": 0x55114460,
|
||||
"NRF52": 0x1B57745F,
|
||||
"STM32F0": 0x647824B6,
|
||||
"STM32F1": 0x5EE21072,
|
||||
"STM32F2": 0x5D1A0A2E,
|
||||
"STM32F3": 0x6B846188,
|
||||
"STM32F4": 0x57755A57,
|
||||
"STM32F7": 0x53B80F00,
|
||||
"STM32G0": 0x300F5633,
|
||||
"STM32G4": 0x4C71240A,
|
||||
"STM32H7": 0x6DB66082,
|
||||
"STM32L0": 0x202E3A91,
|
||||
"STM32L1": 0x1E1F432D,
|
||||
"STM32L4": 0x00FF6919,
|
||||
"STM32L5": 0x04240BDF,
|
||||
"STM32WB": 0x70D16653,
|
||||
"STM32WL": 0x21460FF0,
|
||||
"ATMEGA32": 0x16573617,
|
||||
"MIMXRT10XX": 0x4FB2D5BD,
|
||||
}
|
||||
|
||||
INFO_FILE = "/INFO_UF2.TXT"
|
||||
|
||||
appstartaddr = 0x2000
|
||||
familyid = 0x0
|
||||
|
||||
|
||||
def is_uf2(buf):
|
||||
w = struct.unpack("<II", buf[0:8])
|
||||
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
|
||||
|
||||
|
||||
def is_hex(buf):
|
||||
try:
|
||||
w = buf[0:30].decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
if w[0] == ":" and re.match(b"^[:0-9a-fA-F\r\n]+$", buf):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def convert_from_uf2(buf):
|
||||
global appstartaddr
|
||||
numblocks = len(buf) // 512
|
||||
curraddr = None
|
||||
outp = b""
|
||||
for blockno in range(numblocks):
|
||||
ptr = blockno * 512
|
||||
block = buf[ptr : ptr + 512]
|
||||
hd = struct.unpack(b"<IIIIIIII", block[0:32])
|
||||
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
|
||||
print("Skipping block at " + ptr + "; bad magic")
|
||||
continue
|
||||
if hd[2] & 1:
|
||||
# NO-flash flag set; skip block
|
||||
continue
|
||||
datalen = hd[4]
|
||||
if datalen > 476:
|
||||
assert False, "Invalid UF2 data size at " + ptr
|
||||
newaddr = hd[3]
|
||||
if curraddr == None:
|
||||
appstartaddr = newaddr
|
||||
curraddr = newaddr
|
||||
padding = newaddr - curraddr
|
||||
if padding < 0:
|
||||
assert False, "Block out of order at " + ptr
|
||||
if padding > 10 * 1024 * 1024:
|
||||
assert False, "More than 10M of padding needed at " + ptr
|
||||
if padding % 4 != 0:
|
||||
assert False, "Non-word padding size at " + ptr
|
||||
while padding > 0:
|
||||
padding -= 4
|
||||
outp += b"\x00\x00\x00\x00"
|
||||
outp += block[32 : 32 + datalen]
|
||||
curraddr = newaddr + datalen
|
||||
return outp
|
||||
|
||||
|
||||
def convert_to_carray(file_content):
|
||||
outp = "const unsigned char bindata[] __attribute__((aligned(16))) = {"
|
||||
for i in range(len(file_content)):
|
||||
if i % 16 == 0:
|
||||
outp += "\n"
|
||||
outp += "0x%02x, " % ord(file_content[i])
|
||||
outp += "\n};\n"
|
||||
return outp
|
||||
|
||||
|
||||
def convert_to_uf2(file_content):
|
||||
global familyid
|
||||
datapadding = b""
|
||||
while len(datapadding) < 512 - 256 - 32 - 4:
|
||||
datapadding += b"\x00\x00\x00\x00"
|
||||
numblocks = (len(file_content) + 255) // 256
|
||||
outp = b""
|
||||
for blockno in range(numblocks):
|
||||
ptr = 256 * blockno
|
||||
chunk = file_content[ptr : ptr + 256]
|
||||
flags = 0x0
|
||||
if familyid:
|
||||
flags |= 0x2000
|
||||
hd = struct.pack(
|
||||
b"<IIIIIIII",
|
||||
UF2_MAGIC_START0,
|
||||
UF2_MAGIC_START1,
|
||||
flags,
|
||||
ptr + appstartaddr,
|
||||
256,
|
||||
blockno,
|
||||
numblocks,
|
||||
familyid,
|
||||
)
|
||||
while len(chunk) < 256:
|
||||
chunk += b"\x00"
|
||||
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
|
||||
assert len(block) == 512
|
||||
outp += block
|
||||
return outp
|
||||
|
||||
|
||||
class Block:
|
||||
def __init__(self, addr):
|
||||
self.addr = addr
|
||||
self.bytes = bytearray(256)
|
||||
|
||||
def encode(self, blockno, numblocks):
|
||||
global familyid
|
||||
flags = 0x0
|
||||
if familyid:
|
||||
flags |= 0x2000
|
||||
hd = struct.pack(
|
||||
"<IIIIIIII",
|
||||
UF2_MAGIC_START0,
|
||||
UF2_MAGIC_START1,
|
||||
flags,
|
||||
self.addr,
|
||||
256,
|
||||
blockno,
|
||||
numblocks,
|
||||
familyid,
|
||||
)
|
||||
hd += self.bytes[0:256]
|
||||
while len(hd) < 512 - 4:
|
||||
hd += b"\x00"
|
||||
hd += struct.pack("<I", UF2_MAGIC_END)
|
||||
return hd
|
||||
|
||||
|
||||
def convert_from_hex_to_uf2(buf):
|
||||
global appstartaddr
|
||||
appstartaddr = None
|
||||
upper = 0
|
||||
currblock = None
|
||||
blocks = []
|
||||
for line in buf.split("\n"):
|
||||
if line[0] != ":":
|
||||
continue
|
||||
i = 1
|
||||
rec = []
|
||||
while i < len(line) - 1:
|
||||
rec.append(int(line[i : i + 2], 16))
|
||||
i += 2
|
||||
tp = rec[3]
|
||||
if tp == 4:
|
||||
upper = ((rec[4] << 8) | rec[5]) << 16
|
||||
elif tp == 2:
|
||||
upper = ((rec[4] << 8) | rec[5]) << 4
|
||||
assert (upper & 0xFFFF) == 0
|
||||
elif tp == 1:
|
||||
break
|
||||
elif tp == 0:
|
||||
addr = upper | (rec[1] << 8) | rec[2]
|
||||
if appstartaddr == None:
|
||||
appstartaddr = addr
|
||||
i = 4
|
||||
while i < len(rec) - 1:
|
||||
if not currblock or currblock.addr & ~0xFF != addr & ~0xFF:
|
||||
currblock = Block(addr & ~0xFF)
|
||||
blocks.append(currblock)
|
||||
currblock.bytes[addr & 0xFF] = rec[i]
|
||||
addr += 1
|
||||
i += 1
|
||||
numblocks = len(blocks)
|
||||
resfile = b""
|
||||
for i in range(0, numblocks):
|
||||
resfile += blocks[i].encode(i, numblocks)
|
||||
return resfile
|
||||
|
||||
|
||||
def to_str(b):
|
||||
return b.decode("utf-8")
|
||||
|
||||
|
||||
def get_drives():
|
||||
drives = []
|
||||
if sys.platform == "win32":
|
||||
r = subprocess.check_output(
|
||||
[
|
||||
"wmic",
|
||||
"PATH",
|
||||
"Win32_LogicalDisk",
|
||||
"get",
|
||||
"DeviceID,",
|
||||
"VolumeName,",
|
||||
"FileSystem,",
|
||||
"DriveType",
|
||||
]
|
||||
)
|
||||
for line in to_str(r).split("\n"):
|
||||
words = re.split("\\s+", line)
|
||||
if len(words) >= 3 and words[1] == "2" and words[2] == "FAT":
|
||||
drives.append(words[0])
|
||||
else:
|
||||
rootpath = "/media"
|
||||
if sys.platform == "darwin":
|
||||
rootpath = "/Volumes"
|
||||
elif sys.platform == "linux":
|
||||
tmp = rootpath + "/" + os.environ["USER"]
|
||||
if os.path.isdir(tmp):
|
||||
rootpath = tmp
|
||||
for d in os.listdir(rootpath):
|
||||
drives.append(os.path.join(rootpath, d))
|
||||
|
||||
def has_info(d):
|
||||
try:
|
||||
return os.path.isfile(d + INFO_FILE)
|
||||
except:
|
||||
return False
|
||||
|
||||
return list(filter(has_info, drives))
|
||||
|
||||
|
||||
def board_id(path):
|
||||
with open(path + INFO_FILE, mode="r") as file:
|
||||
file_content = file.read()
|
||||
return re.search("Board-ID: ([^\r\n]*)", file_content).group(1)
|
||||
|
||||
|
||||
def list_drives():
|
||||
for d in get_drives():
|
||||
print(d, board_id(d))
|
||||
|
||||
|
||||
def write_file(name, buf):
|
||||
with open(name, "wb") as f:
|
||||
f.write(buf)
|
||||
print("Wrote %d bytes to %s" % (len(buf), name))
|
||||
|
||||
|
||||
def main():
|
||||
global appstartaddr, familyid
|
||||
|
||||
def error(msg):
|
||||
print(msg)
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Convert to UF2 or flash directly.")
|
||||
parser.add_argument(
|
||||
"input",
|
||||
metavar="INPUT",
|
||||
type=str,
|
||||
nargs="?",
|
||||
help="input file (HEX, BIN or UF2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--base",
|
||||
dest="base",
|
||||
type=str,
|
||||
default="0x2000",
|
||||
help="set base address of application for BIN format (default: 0x2000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
metavar="FILE",
|
||||
dest="output",
|
||||
type=str,
|
||||
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--device", dest="device_path", help="select a device path to flash"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", "--list", action="store_true", help="list connected devices"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--convert", action="store_true", help="do not flash, just convert"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-D", "--deploy", action="store_true", help="just flash, do not convert"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--family",
|
||||
dest="family",
|
||||
type=str,
|
||||
default="0x0",
|
||||
help="specify familyID - number or name (default: 0x0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C",
|
||||
"--carray",
|
||||
action="store_true",
|
||||
help="convert binary file to a C array, not UF2",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
appstartaddr = int(args.base, 0)
|
||||
|
||||
if args.family.upper() in families:
|
||||
familyid = families[args.family.upper()]
|
||||
else:
|
||||
try:
|
||||
familyid = int(args.family, 0)
|
||||
except ValueError:
|
||||
error(
|
||||
"Family ID needs to be a number or one of: "
|
||||
+ ", ".join(families.keys())
|
||||
)
|
||||
|
||||
if args.list:
|
||||
list_drives()
|
||||
else:
|
||||
if not args.input:
|
||||
error("Need input file")
|
||||
with open(args.input, mode="rb") as f:
|
||||
inpbuf = f.read()
|
||||
from_uf2 = is_uf2(inpbuf)
|
||||
ext = "uf2"
|
||||
if args.deploy:
|
||||
outbuf = inpbuf
|
||||
elif from_uf2:
|
||||
outbuf = convert_from_uf2(inpbuf)
|
||||
ext = "bin"
|
||||
elif is_hex(inpbuf):
|
||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
|
||||
elif args.carray:
|
||||
outbuf = convert_to_carray(inpbuf)
|
||||
ext = "h"
|
||||
else:
|
||||
outbuf = convert_to_uf2(inpbuf)
|
||||
print(
|
||||
"Converting to %s, output size: %d, start address: 0x%x"
|
||||
% (ext, len(outbuf), appstartaddr)
|
||||
)
|
||||
if args.convert or ext != "uf2":
|
||||
drives = []
|
||||
if args.output == None:
|
||||
args.output = "flash." + ext
|
||||
else:
|
||||
drives = get_drives()
|
||||
|
||||
if args.output:
|
||||
write_file(args.output, outbuf)
|
||||
else:
|
||||
if len(drives) == 0:
|
||||
error("No drive to deploy.")
|
||||
for d in drives:
|
||||
print("Flashing %s (%s)" % (d, board_id(d)))
|
||||
write_file(d + "/NEW.UF2", outbuf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo using amap tool to display memory map
|
||||
amap .pio/build/output.map
|
||||
@@ -0,0 +1 @@
|
||||
2.6.7
|
||||
@@ -0,0 +1,30 @@
|
||||
# tips from mark on how to replace the (broken) bootloader on the red wio_tracker_1110 boards.
|
||||
|
||||
~/.platformio/penv/bin/adafruit-nrfutil --verbose dfu serial --package wio_tracker_1110_bootloader-0.9.1_s140_7.3.0.zip -p /dev/ttyACM1 -b 115200 --singlebank --touch 1200
|
||||
|
||||
exit
|
||||
|
||||
# Output should look like
|
||||
|
||||
Upgrading target on /dev/ttyACM1 with DFU package /home/kevinh/development/meshtastic/WioWM1110/wio_tracker_1110_bootloader-0.9.1_s140_7.3.0.zip. Flow control is disabled, Single bank, Touch 1200
|
||||
Touched serial port /dev/ttyACM1
|
||||
Opened serial port /dev/ttyACM1
|
||||
Starting DFU upgrade of type 3, SoftDevice size: 152728, bootloader size: 39000, application size: 0
|
||||
Sending DFU start packet
|
||||
Sending DFU init packet
|
||||
Sending firmware file
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
########################################
|
||||
###############
|
||||
Activating new firmware
|
||||
|
||||
DFU upgrade took 20.242434978485107s
|
||||
Device programmed.
|
||||
|
||||
Reference in New Issue
Block a user