Sync edgetx to Gitea

This commit is contained in:
2026-08-03 16:37:20 +08:00
commit 14eb5a3fb9
5364 changed files with 2835009 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import re
if len(sys.argv) > 1:
inputFile = sys.argv[1]
inp = open(inputFile, "r")
else:
inp = sys.stdin
pattern = re.compile("#\d+")
while True:
skip = False
line = inp.readline()
if len(line) == 0:
break
line = line.strip('\r\n')
if len(line) == 0:
skip = True
if line.startswith("<"):
skip = True
if line.startswith("["):
skip = True
if not skip:
# line = line.strip()
# print("line: %s" % line)
found = re.findall(pattern, line)
if len(found) > 0:
for issue in found:
line = line.replace(issue, '')
# add issues
issue_links = ["<a href=https://github.com/opentx/opentx/issues/%d>#%d</a>" % (int(issue[1:]), int(issue[1:])) for issue in found]
links = "(" + ", ".join(issue_links) + ")"
line = "<li>" + line + " " + links + "</li>"
print(line)
inp.close()
sys.exit(0)
+17
View File
@@ -0,0 +1,17 @@
[aqt]
# Using this mirror instead of download.qt.io because of timeouts in CI
# Below is the default URL of the mirror
# baseurl: https://download.qt.io
baseurl: https://qt.mirror.constant.com
[requests]
# Mirrors require sha1 instead of sha256
hash_algorithm: sha1
[mirrors]
trusted_mirrors:
https://qt.mirror.constant.com
fallbacks:
https://qt.mirror.constant.com
https://mirrors.ocf.berkeley.edu
https://download.qt.io
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/python3
boards = {
"XLITE": {
"PCB": "XLITE",
},
"XLITES": {
"PCB": "XLITES",
},
"X9LITE": {
"PCB": "X9LITE",
},
"X9LITES": {
"PCB": "X9LITES",
},
"X9D": {
"PCB": "X9D",
},
"X9D+": {
"PCB": "X9D+",
},
"X9D+2019": {
"PCB": "X9D+",
"PCBREV": "2019",
},
"X9E": {
"PCB": "X9E",
},
"X7": {
"PCB": "X7",
},
"X7ACCESS": {
"PCB": "X7",
"PCBREV": "ACCESS",
},
"X10": {
"PCB": "X10",
},
"X10EXPRESS": {
"PCB": "X10",
"PCBREV": "EXPRESS",
},
"X12S": {
"PCB": "X12S",
},
"T15": {
"PCB": "X10",
"PCBREV": "T15",
"INTERNAL_MODULE_CRSF": "YES"
},
"T16": {
"PCB": "X10",
"PCBREV": "T16",
"INTERNAL_MODULE_MULTI": "YES"
},
"T18": {
"PCB": "X10",
"PCBREV": "T18",
"INTERNAL_MODULE_MULTI": "YES"
},
"TX16S": {
"PCB": "X10",
"PCBREV": "TX16S",
"INTERNAL_MODULE_MULTI": "YES"
},
"F16": {
"PCB": "X10",
"PCBREV": "F16",
"INTERNAL_MODULE_MULTI": "YES"
},
"V12": {
"PCB": "X7",
"PCBREV": "V12",
},
"V14": {
"PCB": "X7",
"PCBREV": "V14",
},
"V16": {
"PCB": "X10",
"PCBREV": "V16",
"INTERNAL_MODULE_MULTI": "YES"
},
"T12": {
"PCB": "X7",
"PCBREV": "T12",
},
"TX12": {
"PCB": "X7",
"PCBREV": "TX12",
},
"COMMANDO8": {
"PCB": "X7",
"PCBREV": "COMMANDO8",
},
"T20": {
"PCB": "X7",
"PCBREV": "T20",
},
"T14": {
"PCB": "X7",
"PCBREV": "T14",
},
}
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"LR3PRO": {
"PCB": "X7",
"PCBREV": "LR3PRO",
"DEFAULT_MODE": "2",
},
}
translations = [
"EN",
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DBETAFPV_RELEASE=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j16")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build BETAFPV firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+210
View File
@@ -0,0 +1,210 @@
get_target_build_options() {
local target_name=$1
case $target_name in
x9lite)
BUILD_OPTIONS+="-DPCB=X9LITE"
;;
x9lites)
BUILD_OPTIONS+="-DPCB=X9LITES"
;;
x7)
BUILD_OPTIONS+="-DPCB=X7"
;;
x7access)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ACCESS -DPXX1=YES"
;;
t12)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T12 -DINTERNAL_MODULE_MULTI=ON"
;;
tx12)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TX12"
;;
tx12mk2)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TX12MK2"
;;
gx12)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=GX12"
;;
boxer)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=BOXER"
;;
t8)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T8"
;;
zorro)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ZORRO"
;;
pocket)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=POCKET"
;;
mt12)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=MT12"
;;
tlite)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TLITE"
;;
tpro)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPRO"
;;
tprov2)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPROV2"
;;
tpros)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPROS"
;;
bumblebee)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=BUMBLEBEE"
;;
t20)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T20"
;;
t12max)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T12MAX"
;;
t14)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T14"
;;
t20v2)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T20V2"
;;
lr3pro)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=LR3PRO"
;;
commando8)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=COMMANDO8"
;;
xlite)
BUILD_OPTIONS+="-DPCB=XLITE"
;;
xlites)
BUILD_OPTIONS+="-DPCB=XLITES"
;;
x9d)
BUILD_OPTIONS+="-DPCB=X9D"
;;
x9dp)
BUILD_OPTIONS+="-DPCB=X9D+"
;;
x9dp2019)
BUILD_OPTIONS+="-DPCB=X9D+ -DPCBREV=2019 -DUSE_FW_LTO=y"
;;
x9e)
BUILD_OPTIONS+="-DPCB=X9E"
;;
x9e-hall)
BUILD_OPTIONS+="-DPCB=X9E -DSTICKS=HORUS"
;;
x10)
BUILD_OPTIONS+="-DPCB=X10"
;;
x10express)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=EXPRESS -DPXX1=YES"
;;
x12s)
BUILD_OPTIONS+="-DPCB=X12S"
;;
t15)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=T15 -DINTERNAL_MODULE_CRSF=ON"
;;
t16)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=T16 -DINTERNAL_MODULE_MULTI=ON"
;;
t18)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=T18"
;;
t15pro)
BUILD_OPTIONS+="-DPCB=T15PRO"
;;
tx15)
BUILD_OPTIONS+="-DPCB=TX15"
;;
tx16s)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=TX16S"
;;
tx16smk3)
BUILD_OPTIONS+="-DPCB=TX16SMK3"
;;
f16)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=F16"
;;
v12)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=V12"
;;
v14)
BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=V14"
;;
v16)
BUILD_OPTIONS+="-DPCB=X10 -DPCBREV=V16"
;;
nv14)
BUILD_OPTIONS+="-DPCB=PL18 -DPCBREV=NV14"
;;
el18)
BUILD_OPTIONS+="-DPCB=PL18 -DPCBREV=EL18"
;;
pl18)
BUILD_OPTIONS+="-DPCB=PL18"
;;
pl18ev)
BUILD_OPTIONS+="-DPCB=PL18 -DPCBREV=PL18EV"
;;
pl18u)
BUILD_OPTIONS+="-DPCB=PL18 -DPCBREV=PL18U"
;;
nb4p)
BUILD_OPTIONS+="-DPCB=PL18 -DPCBREV=NB4P"
;;
st16)
BUILD_OPTIONS+="-DPCB=ST16"
;;
pa01)
BUILD_OPTIONS+="-DPCB=PA01"
;;
*)
echo "Unknown target: $target_name"
return 1
;;
esac
}
# Determine parallel job limit based on environment
determine_max_jobs() {
if [[ -n ${CMAKE_BUILD_PARALLEL_LEVEL} ]]; then
MAX_JOBS=${CMAKE_BUILD_PARALLEL_LEVEL}
elif [ -n "$GITHUB_ACTIONS" ]; then
# Limit jobs in GitHub Actions to n-1 to avoid resource contention
if [ "$(uname)" = "Darwin" ]; then
MAX_JOBS=2 # macOS runners have 3 cores
else
MAX_JOBS=3 # Linux and Windows runners have 4 cores
fi
else
MAX_JOBS="" # Let CMake/build system decide
fi
export MAX_JOBS
}
# Helper function to run cmake build with appropriate parallelism
cmake_build_parallel() {
local args=()
local native_flags=""
# Separate cmake args from native flags (anything after --)
for arg in "$@"; do
if [[ "$arg" == "--" ]]; then
# Capture everything after -- as native flags
shift
native_flags="-- $*"
break
fi
args+=("$arg")
shift
done
if [[ -n ${MAX_JOBS} ]]; then
cmake --build "${args[@]}" --parallel ${MAX_JOBS} ${native_flags}
else
cmake --build "${args[@]}" --parallel ${native_flags}
fi
}
+132
View File
@@ -0,0 +1,132 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
SRCDIR=$1
OUTDIR=$2
if [[ -z ${SRCDIR} ]]; then
SRCDIR="$(pwd)"
fi
if [[ -z ${OUTDIR} ]]; then
OUTDIR="$(pwd)/output"
fi
if [[ ! -d "${OUTDIR}" ]]; then
mkdir -p "${OUTDIR}"
fi
# Determine parallel jobs
determine_max_jobs
QUIET_FLAGS=""
if [[ "$CMAKE_GENERATOR" == "Ninja" ]]; then
QUIET_FLAGS="-- --quiet"
else
# Assume Makefile generator for non-Ninja builds
COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_RULE_MESSAGES=OFF"
fi
COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_BUILD_TYPE=Release -DCMAKE_MESSAGE_LOG_LEVEL=WARNING -Wno-dev"
if [ "$(uname)" = "Darwin" ]; then
COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_OSX_DEPLOYMENT_TARGET='11.0'"
fi
# Generate EDGETX_VERSION_SUFFIX if not already set
if [[ -z ${EDGETX_VERSION_SUFFIX} ]]; then
gh_type=$(echo "$GITHUB_REF" | awk -F / '{print $2}') #heads|tags|pull
if [[ $gh_type = "tags" ]]; then
# tags: refs/tags/<tag_name>
gh_tag=${GITHUB_REF##*/}
export EDGETX_VERSION_TAG=$gh_tag
elif [[ $gh_type = "pull" ]]; then
# pull: refs/pull/<pr_number>/merge
gh_pull_number=PR$(echo "$GITHUB_REF" | awk -F / '{print $3}')
export EDGETX_VERSION_SUFFIX=$gh_pull_number
elif [[ $gh_type = "heads" ]]; then
# heads: refs/heads/<branch_name>
gh_branch=${GITHUB_REF##*/}
export EDGETX_VERSION_SUFFIX=$gh_branch
fi
fi
rm -rf build && mkdir build && cd build
get_platform_config() {
local platform=$(uname)
case "$platform" in
"Darwin")
PACKAGE_TARGET="package"
PACKAGE_FILES="*.dmg"
PACKAGE_NAME="macOS DMG"
PACKAGE_EMOJI="🍎"
;;
"Linux")
PACKAGE_TARGET="package"
PACKAGE_FILES="*.AppImage"
PACKAGE_NAME="Linux AppImage"
PACKAGE_EMOJI="🐧"
;;
*)
PACKAGE_TARGET="installer"
PACKAGE_FILES="companion/*.exe"
PACKAGE_NAME="Windows installer"
PACKAGE_EMOJI="🪟"
;;
esac
}
get_platform_config
BUILD_OPTIONS="${COMMON_OPTIONS} -DEdgeTX_SUPERBUILD:BOOL=0 -DNATIVE_BUILD:BOOL=1"
LOG_FILE="build-companion.log"
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::group::📦 Building $PACKAGE_NAME"
else
echo "📦 Building $PACKAGE_NAME"
echo "=========================================="
fi
clean_build() {
rm -f CMakeCache.txt native/CMakeCache.txt
}
clean_build && mkdir -p native/plugins
# Copy WASM simulator modules if available
if [[ -d "${SRCDIR}/wasm-modules" ]]; then
cp "${SRCDIR}"/wasm-modules/*.wasm native/plugins/ 2>/dev/null && \
echo " 🔌 Copied WASM modules to plugins/" || true
fi
error_status=0
if ! cmake -B native -S "${SRCDIR}" --toolchain cmake/toolchain/native.cmake ${BUILD_OPTIONS} >> "$LOG_FILE" 2>&1; then
echo " ❌ CMake configuration failed"
cat "$LOG_FILE"
error_status=1
elif ! cmake_build_parallel native --target companion ${QUIET_FLAGS} >> "$LOG_FILE" 2>&1; then
echo " ❌ Companion build failed"
cat "$LOG_FILE"
error_status=1
elif ! cmake_build_parallel native --target ${PACKAGE_TARGET} >> "$LOG_FILE" 2>&1; then
echo " ❌ Packaging failed"
cat "$LOG_FILE"
error_status=1
elif cp native/$PACKAGE_FILES "${OUTDIR}" 2>/dev/null; then
echo " ✅ Build completed successfully!"
echo " 📁 Package saved to: ${OUTDIR}"
else
echo " ❌ Failed to copy package files to output directory"
ls -la native/ || echo "native/ directory not found"
error_status=1
fi
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::endgroup::"
fi
exit $error_status
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"NV14": { "PCB": "PL18", "PCBREV": "NV14" },
"EL18": { "PCB": "PL18", "PCBREV": "EL18" },
"PL18": { "PCB": "PL18" },
"PL18EV": { "PCB": "PL18", "PCBREV": "PL18EV" },
"PL18U": { "PCB": "PL18", "PCBREV": "PL18U" },
"ST16": { "PCB": "ST16" , "NANO": "NO" },
}
translations = [
"EN",
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j16")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build Flysky firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import os
import sys
from PIL import Image, ImageDraw, ImageFont
from charset import get_chars, special_chars, extra_chars, standard_chars
EXTRA_BITMAP_MAX_WIDTH = 297
class FontBitmap:
def __init__(self, language, font_size, font_name, foreground, background):
self.language = language
self.chars = get_chars(language)
self.font_size = font_size
self.foreground = foreground
self.background = background
self.font = self.load_font(font_name)
self.extra_bitmap = self.load_extra_bitmap()
self.extra_bitmap_added = False
self.extra_bitmap_width = EXTRA_BITMAP_MAX_WIDTH
if self.extra_bitmap is not None:
self.extra_bitmap_width = self.extra_bitmap.width
def load_extra_bitmap(self):
try:
tools_path = os.path.dirname(os.path.realpath(__file__))
filename = "extra_%dpx.png" % self.font_size
path = os.path.join(tools_path, "../radio/src/fonts", filename)
extra_image = Image.open(path)
return extra_image.convert('RGB')
except IOError:
return None
def load_font(self, font_name):
# print(font_name)
for ext in (".ttf", ".otf"):
tools_path = os.path.dirname(os.path.realpath(__file__))
path = os.path.join(tools_path, "../radio/src/fonts", font_name + ext)
if os.path.exists(path):
return ImageFont.truetype(path, self.font_size)
print("Font file %s not found" % font_name)
def get_text_dimensions(self, text_string):
# https://stackoverflow.com/a/46220683/9263761
_, descent = self.font.getmetrics()
text_width = self.font.getmask(text_string).getbbox()[2]
text_height = self.font.getmask(text_string).getbbox()[3] + descent
return (text_width, text_height)
def draw_char(self, draw, x, y, c):
# default width for space
width = 4
if c != ' ':
if "0123456789".find(c) >= 0 :
width = self.font.getmask("4").getbbox()[2]
_, (offset_x, offset_y) = self.font.font.getsize(c)
_, (offset_x2, offset_y2) = self.font.font.getsize("4")
offset_x = (offset_x + offset_x2) / 2
else :
width = self.font.getmask(c).getbbox()[2]
_, (offset_x, offset_y) = self.font.font.getsize(c)
draw.text((x - offset_x, y), c, fill=self.foreground, font=self.font)
return width
def generate(self, filename, generate_coords_file=True):
coords = []
(_,baseline), (offset_x, offset_y) = self.font.font.getsize(self.chars)
(text_width, text_height) = self.get_text_dimensions(self.chars)
y_shifts = 0
if offset_y < 0:
y_shifts = -offset_y
text_height += y_shifts
offset_y = 0
img_width = text_width + self.extra_bitmap_width
image = Image.new("RGB", (img_width, text_height), self.background)
draw = ImageDraw.Draw(image)
width = 0
for c in self.chars:
if c in extra_chars:
if not self.extra_bitmap_added:
# append same width for non-existing characters
for i in range(128 - 32 - len(standard_chars)):
coords.append(width)
if self.extra_bitmap:
# copy extra_bitmap at once
image.paste(self.extra_bitmap, (width, offset_y + y_shifts))
# append width for extra_bitmap symbols
for coord in [14, 14, 12, 12, 13, 13, 13, 13, 13] + [15] * 12:
coords.append(width)
width += coord
else:
for coord in range(21):
coords.append(width)
# once inserted, disable insert again
self.extra_bitmap_added = True
# skip
continue
# normal characters + CJK
w = self.draw_char(draw, width, y_shifts, c)
coords.append(width)
width += w
# append the last computed width
coords.append(width)
# trim image to correct size
image = image.crop((0, offset_y, width, baseline + offset_y))
# insert the size of the image
coords.insert(0, image.height)
# convert to 8-bit and save
image = image.convert("L")
image.save(filename + ".png")
if generate_coords_file:
with open(filename + ".specs", "w") as f:
f.write(",".join(str(tmp) for tmp in coords))
def main():
if sys.version_info < (3, 0, 0):
print("%s requires Python 3. Terminating." % __file__)
sys.exit(1)
parser = argparse.ArgumentParser(description="Builder for OpenTX font files")
parser.add_argument('--output', help="Output file name")
parser.add_argument('--subset', help="Subset")
parser.add_argument('--size', type=int, help="Font size")
parser.add_argument('--font', help="Font name")
args = parser.parse_args()
font = FontBitmap(args.subset, args.size, args.font, (0, 0, 0), (255, 255, 255))
font.generate(args.output)
if __name__ == "__main__":
main()
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python
import argparse
import os
import struct
class CrcCCITT:
msb_table = (
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0)
@classmethod
def calc_crc(cls, msg):
crc = 0x00
for byte in msg:
crc = cls.msb_table[(crc >> 8 ^ byte) & 0xff] ^ ((crc << 8) & 0xFF00)
return crc
class FrSkyFirmwareInformation:
fourcc = "FRSK"
header_version = 1
product_family_list = {
"INTERNAL_MODULE": 0,
"EXTERNAL_MODULE": 1,
"RECEIVER": 2,
"SENSOR": 3,
"BLUETOOTH_CHIP": 4,
"POWER_CONTROL_CHIP": 5
}
product_id_list = {
# None
"None": 0x00,
# Modules
"XJT": 0x01,
"ISRM": 0x02,
# TODO missing modules
# Receivers
"X8R": 0x01,
"RX8R": 0x02,
"RX8R-PRO": 0x03,
"RX6R": 0x04,
"RX4R": 0x05,
"G-RX8": 0x06,
'G-RX6': 0x07,
"X6R": 0x08,
"X4R": 0x09,
"X4R-SB": 0x0A,
"XSR": 0x0B,
"XSR-M": 0x0C,
"RXSR": 0x0D,
"S6R": 0x0E,
"S8R": 0x0F,
"XM": 0x10,
"XM+": 0x11,
"XMR": 0x12,
"R9": 0x13,
"R9-SLIM": 0x14,
"R9-SLIM+": 0x15,
"R9MINI": 0x16,
"R9MM": 0x17,
"R9-STAB": 0x18
}
def __init__(self, data, args):
self.data = data
self.args = args
@staticmethod
def parse_version(s):
try:
return [int(part) for part in s.split(".")]
except Exception:
raise argparse.ArgumentTypeError("%r is not a valid version" % s)
def write(self, filename):
with open(filename, "wb") as f:
f.write(self.fourcc.encode())
f.write(struct.pack('B', self.header_version))
for i in range(3):
f.write(struct.pack('B', self.args.version[i]))
f.write(struct.pack('I', len(self.data)))
f.write(struct.pack('B', self.product_family_list[self.args.family]))
f.write(struct.pack('B', self.product_id_list[self.args.product]))
f.write(struct.pack('H', CrcCCITT.calc_crc(self.data)))
f.write(self.data)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--family', help="Product family", choices=FrSkyFirmwareInformation.product_family_list.keys(), required=True)
parser.add_argument('-p', '--product', help="Product ID", choices=FrSkyFirmwareInformation.product_id_list.keys(), required=True)
parser.add_argument('-v', '--version', help="Firmware version", type=FrSkyFirmwareInformation.parse_version, required=True)
parser.add_argument("input", type=argparse.FileType("rb"))
args = parser.parse_args()
print("Product family:", args.family)
print("Product ID:", args.product)
print("Product version:", ".".join(str(part) for part in args.version))
s = input("Do you confirm?")
if s.upper() == 'Y':
frsk = FrSkyFirmwareInformation(args.input.read(), args)
output = os.path.splitext(args.input.name)[0] + ".frsk"
frsk.write(output)
print("File %s created!" % output)
if __name__ == '__main__':
main()
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"XLITE_FCC": {
"PCB": "XLITE",
"MODULE_SIZE_STD": "NO",
"PPM": "NO",
"DSM2": "NO",
"SBUS": "NO",
},
"XLITE_LBT": {
"PCB": "XLITE",
"MODULE_PROTOCOL_D8": "NO",
"MODULE_SIZE_STD": "NO",
"PPM": "NO",
"DSM2": "NO",
"SBUS": "NO",
},
"XLITES": {
"PCB": "XLITES",
"AUTOUPDATE": "YES",
"PXX1": "YES",
"XJT": "NO",
"MODULE_SIZE_STD": "NO",
"PPM": "NO",
"DSM2": "NO",
"SBUS": "NO",
},
"X9LITE": {
"PCB": "X9LITE",
"AUTOUPDATE": "YES",
"PXX1": "YES",
"XJT": "NO",
"MODULE_SIZE_STD": "NO",
"PPM": "NO",
"DSM2": "NO",
"SBUS": "NO",
"DEFAULT_MODE": "2",
},
"X9LITES": {
"PCB": "X9LITES",
"AUTOUPDATE": "YES",
"PXX1": "YES",
"XJT": "NO",
"MODULE_SIZE_STD": "NO",
"PPM": "NO",
"DSM2": "NO",
"SBUS": "NO",
"DEFAULT_MODE": "2",
},
"X9D+2019": {
"PCB": "X9D+",
"PCBREV": "2019",
"AUTOUPDATE": "YES",
"PXX1": "YES",
"DEFAULT_MODE": "2",
},
"X9D+": {
"PCB": "X9D+",
"DEFAULT_MODE": "2",
},
"X9E": {
"PCB": "X9E",
"DEFAULT_MODE": "2",
},
"X9EHall": {
"PCB": "X9E",
"STICKS": "HORUS",
"DEFAULT_MODE": "2",
},
"X7_FCC": {
"PCB": "X7",
"DEFAULT_MODE": "2",
},
"X7_LBT": {
"PCB": "X7",
"MODULE_PROTOCOL_D8": "NO",
"DEFAULT_MODE": "2",
},
"X7ACCESS": {
"PCB": "X7",
"PCBREV": "ACCESS",
"AUTOUPDATE": "YES",
"PXX1": "YES",
"DEFAULT_MODE": "2",
},
"X10S": {
"PCB": "X10",
"DEFAULT_MODE": "2",
},
"X10SExpress": {
"PCB": "X10",
"PCBREV": "EXPRESS",
"DEFAULT_MODE": "2",
},
"X12S": {
"PCB": "X12S",
"DEFAULT_MODE": "2",
},
}
translations = [
"EN",
"CZ"
]
common_options = {
"MULTIMODULE": "NO",
"CROSSFIRE": "NO",
"AFHDS3": "NO",
"GVARS": "YES",
"LUA": "NO_MODEL_SCRIPTS",
}
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in list(boards[board].items()) + list(common_options.items())])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DFRSKY_RELEASE=YES -DDEFAULT_TEMPLATE_SETUP=17 %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j6")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build FrSky firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Stop on first error, echo on
set -e
set -x
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
for i in "$@"
do
case $i in
-Wno-error)
WERROR=0
shift
;;
-b*)
FLAVOR="${i#*b}"
shift
;;
esac
done
# Add GCC_ARM to PATH
if [[ -n ${GCC_ARM} ]] ; then
export PATH=${GCC_ARM}:$PATH
fi
: "${SRCDIR:=$(dirname "$(pwd)/$0")/..}"
# Generate EDGETX_VERSION_SUFFIX if not already set
if [[ -z ${EDGETX_VERSION_SUFFIX} ]]; then
gh_type=$(echo "$GITHUB_REF" | awk -F / '{print $2}') #heads|tags|pull
if [[ $gh_type = "tags" ]]; then
# tags: refs/tags/<tag_name>
gh_tag=${GITHUB_REF##*/}
export EDGETX_VERSION_TAG=$gh_tag
elif [[ $gh_type = "pull" ]]; then
# pull: refs/pull/<pr_number>/merge
gh_pull_number=PR$(echo "$GITHUB_REF" | awk -F / '{print $3}')
export EDGETX_VERSION_SUFFIX=$gh_pull_number
elif [[ $gh_type = "heads" ]]; then
# heads: refs/heads/<branch_name>
gh_branch=${GITHUB_REF##*/}
export EDGETX_VERSION_SUFFIX=$gh_branch
fi
fi
: "${BUILD_TYPE:=Release}"
: "${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_RULE_MESSAGES=OFF -Wno-dev "}"
: "${EXTRA_OPTIONS:="$EXTRA_OPTIONS"}"
COMMON_OPTIONS+=${EXTRA_OPTIONS}" "
: "${FIRMARE_TARGET:="firmware-size"}"
# Determine parallel jobs
determine_max_jobs
# workaround for GH repo owner
git config --global --add safe.directory "$(pwd)"
# wipe build directory clean
rm -rf build && mkdir -p build && cd build
GIT_SHA_SHORT=$(git rev-parse --short HEAD)
target_names=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]' | tr ';' '\n')
for target_name in $target_names
do
fw_name="${target_name}-${GIT_SHA_SHORT}"
BUILD_OPTIONS=${COMMON_OPTIONS}
echo "Building ${fw_name}"
if ! get_target_build_options "$target_name"; then
echo "Error: Failed to find a match for target '$target_name'"
exit 1
fi
cmake ${BUILD_OPTIONS} "${SRCDIR}"
cmake_build_parallel . --target arm-none-eabi-configure
cmake_build_parallel arm-none-eabi --target ${FIRMARE_TARGET}
rm -f CMakeCache.txt arm-none-eabi/CMakeCache.txt
if [ -f "arm-none-eabi/firmware.uf2" ]; then
mv arm-none-eabi/firmware.uf2 "../${fw_name}.uf2"
else
mv arm-none-eabi/firmware.bin "../${fw_name}.bin"
fi
done
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"COMMANDO8": {
"PCB": "X7",
"PCBREV": "COMMANDO8",
"DEFAULT_MODE": "1",
"IFLIGHT_RELEASE": "YES",
}
}
translations = [
"EN",
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DIFLIGHT_RELEASE=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j6")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build iFlight firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
from boards import *
translations = [
"EN"
]
def timestamp():
return datetime.datetime.now().strftime("%Y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DIMRC_RELEASE=YES -DGHOST=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j16")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/ghost_firm_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build Ghost firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"TLITE": {
"PCB": "X7",
"PCBREV": "TLITE",
"DEFAULT_MODE": "2",
},
"TPRO": {
"PCB": "X7",
"PCBREV": "TPRO",
"DEFAULT_MODE": "2",
},
"T12": {
"PCB": "X7",
"PCBREV": "T12",
"DEFAULT_MODE": "2",
},
"T12PRO": {
"PCB": "X7",
"PCBREV": "T12",
"INTERNAL_MODULE_MULTI": "YES",
"DEFAULT_MODE": "2",
},
"T16": {
"PCB": "X10",
"PCBREV": "T16",
"INTERNAL_MODULE_MULTI": "YES",
"DEFAULT_MODE": "2",
},
"T18": {
"PCB": "X10",
"PCBREV": "T18",
"INTERNAL_MODULE_MULTI": "YES",
"DEFAULT_MODE": "2",
},
}
translations = [
"EN",
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DJUMPER_RELEASE=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j16")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build JumperRC firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+1334
View File
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
#! /usr/bin/python
import cgitb
#cgitb.enable()
from pprint import pprint
from time import gmtime, strftime
from os.path import expanduser
import os
import os.path
import cgi
import sys
import re
import fcntl
import subprocess
gitdir="~/cgibuild/opentx"
pidfile="~/cgibuild/opentxbuild.pid"
outdir="~/Sites/builds/"
builddir="~/cgibuild/"
def main():
print ("Content-Type: text/html\n")
print ("""<!DOCTYPE html>
<html>
<body>
<h1>OpenTX 2.3 Companion build...</h1>
""")
form = cgi.FieldStorage()
suffix = None
buildscript = "build-companion-release.sh"
if 'suffix' in form:
suffix = form.getfirst("suffix")
if not re.match("^[a-zA-Z0-9]*$", suffix):
status("Invalid suffix specified", True)
if suffix and not suffix.startswith("rc"):
buildscript = "build-companion-nightly.sh"
if "branch" not in form or not re.match("^[a-z_\\-.A-Z0-9/]*$",form.getfirst("branch")):
status ("No branch specified or invalid branch\n", True)
branch = form.getfirst("branch")
status ("Branch %s - Suffix %s" %(branch, suffix))
status ("Trying to get lock")
getlock_or_exit()
logfile = open(os.path.join(expanduser(outdir),strftime("build-%Y-%m-%d %H:%M:%S.log", gmtime())),"w")
run_cmd(["git", "fetch"], gitdir, logfile)
run_cmd(["git", "checkout", "origin/%s" % branch], gitdir, logfile)
run_cmd(["git", "reset", "--hard"], gitdir, logfile)
buildcmd = [os.path.join(expanduser(gitdir), "tools", buildscript), \
"-j4",
expanduser(gitdir), \
expanduser(outdir)]
if suffix:
buildcmd.append(suffix)
run_cmd(buildcmd, builddir, logfile)
status("Finished")
def run_cmd(cmd, wd, logfile):
env = os.environ.copy()
env['PATH'] = env['PATH'] + ":/usr/local/bin/"
env['PYTHONPATH']="/usr/local/lib/python2.7/site-packages"
env['HOME']=expanduser("~")
status("Running '[%s]'" % ", ".join(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=expanduser(wd),env=env, bufsize=00)
print("<pre>\n")
sys.stdout.flush()
for line in p.stdout:
sys.stdout.write(line)
logfile.write(line)
if '\n' in line:
sys.stdout.flush()
print("</pre>\n")
p.wait()
if(p.returncode != 0):
status( "failed with status %d" % p.returncode, True)
def getlock_or_exit():
global fp
pid_file = expanduser(pidfile)
fp = open(pid_file, 'w')
try:
fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
fp.write("%s" % (os.getpid()))
except IOError as e:
# another instance is running
status("could get lock, another instance running? %s " % str(e), True)
def status(msg, exit=False):
print ("<p><b>%s - %s</b></p>\n" % (strftime("%Y-%m-%d %H:%M:%S", gmtime()),msg))
if exit:
sys.exit(0)
if __name__=="__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
boards = {
"TX16S_1": {
"PCB": "X10",
"PCBREV": "TX16S",
"DEFAULT_MODE": "1",
},
"TX16S_2": {
"PCB": "X10",
"PCBREV": "TX16S",
"DEFAULT_MODE": "2",
},
"TX12_1": {
"PCB": "X7",
"PCBREV": "TX12",
"DEFAULT_MODE": "1",
},
"TX12_2": {
"PCB": "X7",
"PCBREV": "TX12",
"DEFAULT_MODE": "2",
},
"TX12MK2_1": {
"PCB": "X7",
"PCBREV": "TX12MK2",
"DEFAULT_MODE": "1",
},
"TX12MK2_2": {
"PCB": "X7",
"PCBREV": "TX12MK2",
"DEFAULT_MODE": "2",
},
"ZORRO_1": {
"PCB": "X7",
"PCBREV": "ZORRO",
"DEFAULT_MODE": "1",
},
"ZORRO_2": {
"PCB": "X7",
"PCBREV": "ZORRO",
"DEFAULT_MODE": "2",
},
"BOXER_1": {
"PCB": "X7",
"PCBREV": "BOXER",
"DEFAULT_MODE": "1",
},
"BOXER_2": {
"PCB": "X7",
"PCBREV": "BOXER",
"DEFAULT_MODE": "2",
},
"POCKET_1": {
"PCB": "X7",
"PCBREV": "POCKET",
"DEFAULT_MODE": "1",
},
"POCKET_2": {
"PCB": "X7",
"PCBREV": "POCKET",
"DEFAULT_MODE": "2",
},
"MT12_1": {
"PCB": "X7",
"PCBREV": "MT12",
"DEFAULT_MODE": "1",
},
"MT12_2": {
"PCB": "X7",
"PCBREV": "MT12",
"DEFAULT_MODE": "2",
},
"T8_1": {
"PCB": "X7",
"PCBREV": "T8",
"DEFAULT_MODE": "1",
"RADIOMASTER_RTF_RELEASE": "YES",
},
"T8_2": {
"PCB": "X7",
"PCBREV": "T8",
"DEFAULT_MODE": "2",
"RADIOMASTER_RTF_RELEASE": "YES",
}
}
translations = [
"EN",
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DRADIOMASTER_RELEASE=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j6")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/firmware_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build Radiomaster firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/python3
import argparse
import datetime
import os
from builtins import NotADirectoryError
import shutil
import tempfile
from boards import *
translations = [
"EN"
]
def timestamp():
return datetime.datetime.now().strftime("%y%m%d")
def build(board, translation, srcdir):
cmake_options = " ".join(["-D%s=%s" % (key, value) for key, value in boards[board].items()])
cwd = os.getcwd()
if not os.path.exists("output"):
os.mkdir("output")
path = tempfile.mkdtemp()
os.chdir(path)
command = "cmake %s -DTRANSLATIONS=%s -DTBS_RELEASE=YES %s" % (cmake_options, translation, srcdir)
print(command)
os.system(command)
os.system("make firmware -j16")
os.chdir(cwd)
index = 0
while 1:
suffix = "" if index == 0 else "_%d" % index
filename = "output/tbs_firm_%s_%s_%s%s.bin" % (board.lower(), translation.lower(), timestamp(), suffix)
if not os.path.exists(filename):
shutil.copy("%s/arm-none-eabi/firmware.bin" % path, filename)
break
index += 1
shutil.rmtree(path)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser(description="Build FrSky firmware")
parser.add_argument("-b", "--boards", action="append", help="Destination boards", required=True)
parser.add_argument("-t", "--translations", action="append", help="Translations", required=True)
parser.add_argument("srcdir", type=dir_path)
args = parser.parse_args()
for board in (boards.keys() if "ALL" in args.boards else args.boards):
for translation in (translations if "ALL" in args.translations else args.translations):
build(board, translation, args.srcdir)
if __name__ == "__main__":
main()
+290
View File
@@ -0,0 +1,290 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
SRCDIR=$1
OUTDIR=$2
if [[ -z ${SRCDIR} ]]; then
SRCDIR="$(pwd)"
fi
if [[ -z ${OUTDIR} ]]; then
OUTDIR="$(pwd)/output"
fi
if [[ ! -d "${OUTDIR}" ]]; then
mkdir -p "${OUTDIR}"
fi
QUIET_FLAGS=""
if [[ "$CMAKE_GENERATOR" == "Ninja" ]]; then
QUIET_FLAGS="-- --quiet"
else
# Assume Makefile generator for non-Ninja builds
COMMON_OPTIONS="-DCMAKE_RULE_MESSAGES=OFF"
fi
COMMON_OPTIONS+=" -DCMAKE_BUILD_TYPE=Release -DCMAKE_MESSAGE_LOG_LEVEL=WARNING -Wno-dev"
COMMON_OPTIONS+=" -DEdgeTX_SUPERBUILD:BOOL=0 -DNATIVE_BUILD:BOOL=1"
# Generate EDGETX_VERSION_SUFFIX if not already set
if [[ -z ${EDGETX_VERSION_SUFFIX} ]]; then
gh_type=$(echo "$GITHUB_REF" | awk -F / '{print $2}') #heads|tags|pull
if [[ $gh_type = "tags" ]]; then
# tags: refs/tags/<tag_name>
gh_tag=${GITHUB_REF##*/}
export EDGETX_VERSION_TAG=$gh_tag
elif [[ $gh_type = "pull" ]]; then
# pull: refs/pull/<pr_number>/merge
gh_pull_number=PR$(echo "$GITHUB_REF" | awk -F / '{print $3}')
export EDGETX_VERSION_SUFFIX=$gh_pull_number
elif [[ $gh_type = "heads" ]]; then
# heads: refs/heads/<branch_name>
gh_branch=${GITHUB_REF##*/}
export EDGETX_VERSION_SUFFIX=$gh_branch
fi
fi
if [[ -n "$GITHUB_ACTIONS" ]]; then
MAX_JOBS=${MAX_JOBS:-3}
fi
BUILD_DIR="build/wasm"
# Resolve WASI SDK: use WASI_SDK_PATH env, /opt/wasi-sdk, or auto-fetch
resolve_wasi_sdk() {
if [[ -n "$WASI_SDK_PATH" ]] && [[ -d "$WASI_SDK_PATH" ]]; then
echo "Using WASI SDK from WASI_SDK_PATH=$WASI_SDK_PATH"
return 0
fi
if [[ -d "/opt/wasi-sdk" ]]; then
WASI_SDK_PATH="/opt/wasi-sdk"
echo "Using WASI SDK from /opt/wasi-sdk"
return 0
fi
echo "WASI SDK not found, fetching via cmake/FetchWasiSDK.cmake..."
# Run a minimal cmake project that reuses FetchWasiSDK.cmake to download
# the SDK. Downloads go into _deps/ which persists across plugin builds.
local fetch_dir="_wasi_sdk_fetch"
mkdir -p "$fetch_dir"
cat > "$fetch_dir/CMakeLists.txt" << CMAKEOF
cmake_minimum_required(VERSION 3.14)
project(wasi_sdk_fetch NONE)
list(APPEND CMAKE_MODULE_PATH "${SRCDIR}/cmake")
include(FetchWasiSDK)
file(WRITE "\${CMAKE_CURRENT_BINARY_DIR}/wasi_sdk_path.txt" "\${WASI_SDK_PATH}")
CMAKEOF
if ! cmake -S "$fetch_dir" -B "$fetch_dir/build" \
-DFETCHCONTENT_BASE_DIR="$PWD/_deps" 2>&1; then
echo "❌ Failed to fetch WASI SDK"
return 1
fi
WASI_SDK_PATH=$(cat "$fetch_dir/build/wasi_sdk_path.txt")
if [[ -z "$WASI_SDK_PATH" ]] || [[ ! -d "$WASI_SDK_PATH" ]]; then
echo "❌ WASI SDK path not resolved"
return 1
fi
echo "Using auto-fetched WASI SDK at $WASI_SDK_PATH"
return 0
}
if ! resolve_wasi_sdk; then
echo "❌ Cannot proceed without WASI SDK"
exit 1
fi
export WASI_SDK_PATH
COMMON_OPTIONS+=" -DCMAKE_MODULE_PATH=${WASI_SDK_PATH}/share/cmake/"
COMMON_OPTIONS+=" -DWASI_SDK_PREFIX=${WASI_SDK_PATH}"
# Function to output error logs (works in both GitHub Actions and terminal)
output_error_log() {
local log_file="$1"
local context="$2"
if [[ -f "$log_file" ]]; then
echo "------------------------------------------"
echo " Full error output from $log_file:"
echo "------------------------------------------"
cat "$log_file"
else
echo "⚠️ Warning: Log file $log_file not found for $context"
fi
}
# Function to show last N lines of a log file for warnings
show_log_summary() {
local log_file="$1"
local lines="${2:-50}"
local context="$3"
if [[ -f "$log_file" ]]; then
echo "------------------------------------------"
echo "Last $lines lines from $log_file:"
echo "------------------------------------------"
tail -n "$lines" "$log_file"
fi
}
run_pipeline() {
local log_file="${1:-/dev/null}"
local context="$2"
local show_details="${3:-false}"
local cmake_opts="--parallel ${MAX_JOBS} ${QUIET_FLAGS}"
local wasi_toolchain="${SRCDIR}/cmake/toolchain/wasi-threads.cmake"
if ! execute_with_output "🔧 CMake config" "cmake --fresh -S ${SRCDIR} -B ${BUILD_DIR} --toolchain ${wasi_toolchain} ${BUILD_OPTIONS}" "$log_file" "$show_details"; then
output_error_log "$log_file" "$context (Configuration)"
return 1
fi
if ! execute_with_output "📦 Building WASM module" "cmake --build ${BUILD_DIR} --target wasi-module ${cmake_opts}" "$log_file" "$show_details"; then
output_error_log "$log_file" "$context (Library Build)"
return 1
fi
return 0
}
execute_with_output() {
local description="$1"
local command="$2"
local log_file="$3"
local show_output="${4:-false}"
if [[ "$show_output" == "true" && "$log_file" != "/dev/null" ]]; then
echo " $description..."
fi
rm -f "$log_file"
eval "$command" >> "$log_file" 2>&1
}
# Enhanced plugin builder with better error handling
build_plugin() {
local plugin="$1"
local log_file="${OUTDIR}/build_${plugin}.log"
local verbose="${2:-false}"
BUILD_OPTIONS="${COMMON_OPTIONS} "
if ! get_target_build_options "$plugin" >> "$log_file" 2>&1; then
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::error::Failed to get build options for $plugin"
fi
output_error_log "$log_file" "$plugin (Build Options)"
return 1
fi
# Only show detailed output in GitHub Actions or if verbose is requested
local show_details="false"
if [[ -n "$GITHUB_ACTIONS" || "$verbose" == "true" ]]; then
show_details="true"
fi
if ! run_pipeline "$log_file" "$plugin" "$show_details"; then
return 1
fi
# FLAVOUR may differ from target name (e.g. x9dp2019 -> x9d+2019),
# so copy whatever .wasm was produced.
cp ${BUILD_DIR}/edgetx-*-simulator.wasm "${OUTDIR}/" 2>/dev/null
# Check for warnings and show summary if found
if grep -q -i "warning" "$log_file"; then
echo " ⚠️ $plugin completed with warnings"
if [[ "$show_details" == "true" ]]; then
show_log_summary "$log_file" 50 "$plugin (Warnings)"
fi
return 0
fi
echo "$plugin completed successfully"
return 0
}
if [[ -n "$FLAVOR" ]]; then
# Convert semicolon-separated string to array
IFS=';' read -ra temp_array <<< "$FLAVOR"
plugins=()
for item in "${temp_array[@]}"; do
plugins+=($(echo "$item" | tr '[:upper:]' '[:lower:]'))
done
else
declare -a plugins=(
# monochrome
boxer bumblebee commando8
gx12 mt12 pocket t12max
t14 t20 t20v2 tpros tprov2
tx12mk2 zorro v12 v14
x7access x9dp2019 x9e
# colour
el18 nb4p nv14 st16 pa01
pl18 pl18ev pl18u
t15 t15pro t16 t18
tx15 tx16s tx16smk3 f16 v16
x10 x10express x12s
)
fi
TOTAL=${#plugins[@]}
FAILED_PLUGINS=()
echo "🔨 Building $TOTAL Plugins"
for i in "${!plugins[@]}"; do
plugin="${plugins[$i]}"
current=$((i + 1))
percent=$((current * 100 / TOTAL))
# For terminal output, put each plugin on its own line for cleaner display
if [[ -n "$GITHUB_ACTIONS" ]]; then
printf "::group::📦 %-12s [%2d/%2d] %3d%%\n" "$plugin" "$current" "$TOTAL" "$percent"
else
echo "🔨 [$current/$TOTAL] ($percent%) Building $plugin..."
fi
error_status=0
if ! build_plugin "$plugin"; then
FAILED_PLUGINS+=("$plugin")
error_status=1
fi
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::endgroup::"
fi
if [ $error_status -ne 0 ]; then
# TODO: if not '--build-all' used
exit 1
fi
done
# Show summary of any failures (if using continue-on-error approach)
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::group::❌ Build Failures Summary"
else
echo "❌ Build Failures Summary"
echo "=========================================="
fi
echo "The following plugins failed to build:"
for failed_plugin in "${FAILED_PLUGINS[@]}"; do
echo "$failed_plugin"
done
if [[ -n "$GITHUB_ACTIONS" ]]; then
echo "::endgroup::"
else
echo "=========================================="
fi
exit 1
fi
+152
View File
@@ -0,0 +1,152 @@
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
#include <locale>
#include <string.h>
#define CFN_ONLY
#define SKIP
#include "../radio/src/dataconstants.h"
#define LCD_W 480
#define TR(a,b) b
#define TR3(a, b, c) c
#define TR_BW_COL(a, b) b
#if defined(LNG_CN)
#include "../radio/src/translations/i18n/cn.h"
#define LOC "zh_CN.UTF-8"
#elif defined(LNG_CZ)
#include "../radio/src/translations/i18n/cz.h"
#define LOC "cs_CZ.UTF-8"
#elif defined(LNG_DA)
#include "../radio/src/translations/i18n/da.h"
#define LOC "da_DK.UTF-8"
#elif defined(LNG_DE)
#include "../radio/src/translations/i18n/de.h"
#define LOC "de_DE.UTF-8"
#elif defined(LNG_EN)
#include "../radio/src/translations/i18n/en.h"
#define LOC "en_US.UTF-8"
#elif defined(LNG_ES)
#include "../radio/src/translations/i18n/es.h"
#define LOC "es_ES.UTF-8"
#elif defined(LNG_FI)
#include "../radio/src/translations/i18n/fi.h"
#define LOC "fi_FI.UTF-8"
#elif defined(LNG_FR)
#include "../radio/src/translations/i18n/fr.h"
#define LOC "fr_FR.UTF-8"
#elif defined(LNG_HE)
#include "../radio/src/translations/i18n/he.h"
#define LOC "he_IL.UTF-8"
#elif defined(LNG_IT)
#include "../radio/src/translations/i18n/it.h"
#define LOC "it_IT.UTF-8"
#elif defined(LNG_JP)
#include "../radio/src/translations/i18n/jp.h"
#define LOC "ja_JP.UTF-8"
#elif defined(LNG_KO)
#include "../radio/src/translations/i18n/ko.h"
#define LOC "ko_KR.UTF-8"
#elif defined(LNG_NL)
#include "../radio/src/translations/i18n/nl.h"
#define LOC "nl_NL.UTF-8"
#elif defined(LNG_PL)
#include "../radio/src/translations/i18n/pl.h"
#define LOC "pl_PL.UTF-8"
#elif defined(LNG_PT)
#include "../radio/src/translations/i18n/pt.h"
#define LOC "pt_PT.UTF-8"
#elif defined(LNG_RU)
#include "../radio/src/translations/i18n/ru.h"
#define LOC "ru_RU.UTF-8"
#elif defined(LNG_SE)
#include "../radio/src/translations/i18n/se.h"
#define LOC "sv_SE.UTF-8"
#elif defined(LNG_TW)
#include "../radio/src/translations/i18n/tw.h"
#define LOC "zh_TW.UTF-8"
#elif defined(LNG_UA)
#include "../radio/src/translations/i18n/ua.h"
#define LOC "uk_UA.UTF-8"
#else
#error "Unknown language"
#endif
struct cfn {
std::string str;
std::string nam;
Functions func;
std::string cond;
};
struct LocaleComparator {
std::locale locale;
LocaleComparator(const std::locale& loc) : locale(loc) {}
bool operator()(const struct cfn& lhs, const struct cfn& rhs) const {
return std::use_facet< std::collate<char> >(locale).compare(
lhs.str.data(), lhs.str.data() + lhs.str.size(),
rhs.str.data(), rhs.str.data() + rhs.str.size()) < 0;
}
};
int main()
{
#if defined(LOC)
std::vector<struct cfn> list = {
{ TR_SF_SAFETY, "FUNC_OVERRIDE_CHANNEL", FUNC_OVERRIDE_CHANNEL, "" },
{ TR_SF_TRAINER, "FUNC_TRAINER", FUNC_TRAINER, "" },
{ TR_SF_INST_TRIM, "FUNC_INSTANT_TRIM", FUNC_INSTANT_TRIM, "" },
{ TR_SF_RESET, "FUNC_RESET", FUNC_RESET, "" },
{ TR_SF_SET_TIMER, "FUNC_SET_TIMER", FUNC_SET_TIMER, "" },
{ TR_ADJUST_GVAR, "FUNC_ADJUST_GVAR", FUNC_ADJUST_GVAR, "" },
{ TR_SF_VOLUME, "FUNC_VOLUME", FUNC_VOLUME, "" },
{ TR_SF_FAILSAFE, "FUNC_SET_FAILSAFE", FUNC_SET_FAILSAFE, "" },
{ TR_SF_RANGE_CHECK, "FUNC_RANGECHECK", FUNC_RANGECHECK, "" },
{ TR_SF_MOD_BIND, "FUNC_BIND", FUNC_BIND, "" },
{ TR_SOUND, "FUNC_PLAY_SOUND", FUNC_PLAY_SOUND, "" },
{ TR_PLAY_TRACK, "FUNC_PLAY_TRACK", FUNC_PLAY_TRACK, "" },
{ TR_PLAY_VALUE, "FUNC_PLAY_VALUE", FUNC_PLAY_VALUE, "" },
{ TR_SF_PLAY_SCRIPT, "FUNC_PLAY_SCRIPT", FUNC_PLAY_SCRIPT, "" },
{ TR_SF_BG_MUSIC, "FUNC_BACKGND_MUSIC", FUNC_BACKGND_MUSIC, "" },
{ TR_SF_BG_MUSIC_PAUSE, "FUNC_BACKGND_MUSIC_PAUSE", FUNC_BACKGND_MUSIC_PAUSE, "" },
{ TR_SF_VARIO, "FUNC_VARIO", FUNC_VARIO, "" },
{ TR_SF_HAPTIC, "FUNC_HAPTIC", FUNC_HAPTIC, "" },
{ TR_SF_LOGS, "FUNC_LOGS", FUNC_LOGS, "" },
{ TR_BRIGHTNESS, "FUNC_BACKLIGHT", FUNC_BACKLIGHT, "defined(OLED_SCREEN)" },
{ TR_SF_BACKLIGHT, "FUNC_BACKLIGHT", FUNC_BACKLIGHT, "!defined(OLED_SCREEN)" },
{ TR_SF_SCREENSHOT, "FUNC_SCREENSHOT", FUNC_SCREENSHOT, "" },
{ TR_SF_RACING_MODE, "FUNC_RACING_MODE", FUNC_RACING_MODE, "" },
{ TR_SF_DISABLE_TOUCH, "FUNC_DISABLE_TOUCH", FUNC_DISABLE_TOUCH, "defined(COLORLCD)" },
{ TR_SF_SET_SCREEN, "FUNC_SET_SCREEN", FUNC_SET_SCREEN, "" },
{ TR_SF_DISABLE_AUDIO_AMP, "FUNC_DISABLE_AUDIO_AMP", FUNC_DISABLE_AUDIO_AMP, "" },
{ TR_SF_RGBLEDS, "FUNC_RGB_LED", FUNC_RGB_LED, "" },
{ TR_SF_LCD_TO_VIDEO, "FUNC_LCD_TO_VIDEO", FUNC_LCD_TO_VIDEO, "defined(VIDEO_SWITCH)" },
{ TR_SF_PUSH_CUST_SWITCH, "FUNC_PUSH_CUST_SWITCH", FUNC_PUSH_CUST_SWITCH, "defined(FUNCTION_SWITCHES)" },
{ TR_SF_TEST, "FUNC_TEST", FUNC_TEST, "defined(DEBUG)" },
};
std::locale locale(LOC);
std::sort(list.begin(), list.end(), LocaleComparator(locale));
for (int i = 0; i < list.size(); i += 1) {
bool addEndif = false;
if (!list[i].cond.empty()) {
printf("#if %s\n", list[i].cond.c_str());
addEndif = true;
}
printf(" /* %s */ %s,\n", list[i].str.c_str(), list[i].nam.c_str());
if (addEndif)
printf("#endif\n");
}
return 0;
#else
static_assert(false,"No valid language defined!");
#endif
}
+133
View File
@@ -0,0 +1,133 @@
#!/bin/sh
set -e
# set -x
# Required locales (check your system and install if needed):
# Linux:
# apt update && apt install locales
# locale-gen zh_CN.UTF-8 cs_CZ.UTF-8 da_DK.UTF-8 de_DE.UTF-8 es_ES.UTF-8 en_US.UTF-8 fi_FI.UTF-8 fr_FR.UTF-8 he_IL.UTF-8 it_IT.UTF-8 ja_JP.UTF-8 ko_KR.UTF-8 nl_NL.UTF-8 pl_PL.UTF-8 pt_PT.UTF-8 ru_RU.UTF-8 sv_SE.UTF-8 zh_TW.UTF-8 uk_UA.UTF-8
# zh_CN.UTF-8 / zh_CN.utf8
# cs_CZ.UTF-8 / cs_CZ.utf8
# da_DK.UTF-8 / da_DK.utf8
# de_DE.UTF-8 / de_DE.utf8
# es_ES.UTF-8 / es_ES.utf8
# en_US.UTF-8 / en_US.utf8
# fi_FI.UTF-8 / fi_FI.utf8
# fr_FR.UTF-8 / fr_FR.utf8
# he_IL.UTF-8 / he_IL.utf8
# it_IT.UTF-8 / it_IT.utf8
# ja_JP.UTF-8 / ja_JP.utf8
# ko_KR.UTF-8 / ko_KR.utf8
# nl_NL.UTF-8 / nl_NL.utf8
# pl_PL.UTF-8 / pl_PL.utf8
# pt_PT.UTF-8 / pt_PT.utf8
# ru_RU.UTF-8 / ru_RU.utf8
# sv_SE.UTF-8 / sv_SE.utf8
# zh_TW.UTF-8 / zh_TW.utf8
# uk_UA.UTF-8 / uk_UA.utf8
# Get the directory where this script is located and the project root
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Define paths relative to project root
TOOLS_DIR="${PROJECT_ROOT}/tools"
RADIO_SRC_DIR="${PROJECT_ROOT}/radio/src"
CFN_SORTER_CPP="${TOOLS_DIR}/cfn_sorter.cpp"
COPYRIGHT_HEADER="${TOOLS_DIR}/copyright-header.txt"
OUTPUT_FILE="${RADIO_SRC_DIR}/cfn_sort.cpp"
EXECUTABLE="${SCRIPT_DIR}/a.out"
# Determine the compiler to use
if command -v g++ >/dev/null 2>&1; then
CXX=g++
elif command -v gcc >/dev/null 2>&1; then
CXX=gcc
elif command -v clang++ >/dev/null 2>&1; then
CXX=clang++
else
echo "Neither g++, gcc nor clang++ found. Please install a C++ compiler."
exit 1
fi
# Compile specified translation macro, append to cfn_sort.cpp
compile_and_append() {
local lng_macro=$1
local translation_macro=$2
local condition=$3
echo "Compiling ${translation_macro} (${lng_macro}) ..."
# Change to script directory for compilation
cd "${SCRIPT_DIR}"
$CXX -std=c++11 -lstdc++ -Wfatal-errors -D${lng_macro} "${CFN_SORTER_CPP}"
{
if [ "$condition" = "else" ]; then
echo "#${condition}"
else
echo "#${condition} defined(${translation_macro})"
fi
"${EXECUTABLE}"
} >> "${OUTPUT_FILE}"
}
# Ensure output directory exists
mkdir -p "${RADIO_SRC_DIR}"
# Remove existing output file
rm -f "${OUTPUT_FILE}"
# Start the file with boilerplate copyright header
cat "${COPYRIGHT_HEADER}" > "${OUTPUT_FILE}"
# Add the rest of the file header
cat <<EOF >> "${OUTPUT_FILE}"
// This file is auto-generated via cfn_sorter.sh. Do not edit.
#include "dataconstants.h"
Functions cfn_sorted[] = {
EOF
# Languages - compile and append each translation
compile_and_append "LNG_CN" "TRANSLATIONS_CN" "if"
compile_and_append "LNG_CZ" "TRANSLATIONS_CZ" "elif"
compile_and_append "LNG_DA" "TRANSLATIONS_DA" "elif"
compile_and_append "LNG_DE" "TRANSLATIONS_DE" "elif"
compile_and_append "LNG_ES" "TRANSLATIONS_ES" "elif"
compile_and_append "LNG_FI" "TRANSLATIONS_FI" "elif"
compile_and_append "LNG_FR" "TRANSLATIONS_FR" "elif"
compile_and_append "LNG_HE" "TRANSLATIONS_HE" "elif"
compile_and_append "LNG_IT" "TRANSLATIONS_IT" "elif"
compile_and_append "LNG_JP" "TRANSLATIONS_JP" "elif"
compile_and_append "LNG_KO" "TRANSLATIONS_KO" "elif"
compile_and_append "LNG_NL" "TRANSLATIONS_NL" "elif"
compile_and_append "LNG_PL" "TRANSLATIONS_PL" "elif"
compile_and_append "LNG_PT" "TRANSLATIONS_PT" "elif"
compile_and_append "LNG_RU" "TRANSLATIONS_RU" "elif"
compile_and_append "LNG_SE" "TRANSLATIONS_SE" "elif"
compile_and_append "LNG_TW" "TRANSLATIONS_TW" "elif"
compile_and_append "LNG_UA" "TRANSLATIONS_UA" "elif"
compile_and_append "LNG_EN" "TRANSLATIONS_EN" "else"
# End of cfn_sort.cpp
cat <<EOF >> "${OUTPUT_FILE}"
#endif
};
uint8_t getFuncSortIdx(uint8_t func)
{
for (uint8_t i = 0; i < FUNC_MAX; i += 1)
if (cfn_sorted[i] == func)
return i;
return 0;
}
EOF
# Clean up
[ -f "${EXECUTABLE}" ] && rm "${EXECUTABLE}"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# used ? Δ~\n\t
import os
standard_chars = """ !"#$%&'()*+,-./0123456789:;<=>?°ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz~|≥"""
extra_chars = "".join([chr(0x10000+i) for i in range(21)])
def is_special_char(c):
# only 'our' special chars and CJK Unified Ideographs
return 192 <= ord(c) <= 383 or 0x4E00 <= ord(c) <= 0x9FFF
def get_special_chars():
result = {}
for lang in["cn", "cz", "da", "de", "en", "es", "fi", "fr", "he", "it", "jp", "nl", "pl", "pt", "ru", "se", "tw"]:
charset = set()
tools_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(tools_path, "../radio/src/translations/%s.h" % lang), encoding='utf-8') as f:
data = f.read()
for c in data:
if is_special_char(c):
charset.add(c)
data = list(charset)
data.sort()
result[lang] = data
return result
special_chars = get_special_chars()
def get_chars(subset):
result = standard_chars + extra_chars
result += "".join([char for char in special_chars[subset]])
return result
def get_chars_encoding(subset):
result = {}
if subset in ("cn", "tw"):
chars = get_chars(subset)
for char in chars:
if char in special_chars[subset]:
index = special_chars[subset].index(char) + 1
if index >= 0x100:
index += 1
result[char] = "\\%03o\\%03o" % (0xFE + ((index >> 8) & 0x01), index & 0xFF)
elif char not in standard_chars and char not in extra_chars:
result[char] = "\\%03o" % (0xC0 + chars.index(char) - len(standard_chars))
else:
offset = 128 - len(standard_chars)
chars = get_chars(subset)
for char in chars:
if char not in standard_chars:
result[char] = "\\%03o" % (offset + chars.index(char))
return result
special_chars_BW = {
"en": "",
"fr": "éèàîç",
"da": "åæøÅÆØ",
"de": "ÄäÖöÜüß",
"cz": "áčéěíóřšúůýÁÍŘÝžÉ",
"nl": "",
"es": "ÑñÁáÉéÍíÓóÚú",
"fi": "åäöÅÄÖ",
"it": "àù",
"pl": "ąćęłńóśżźĄĆĘŁŃÓŚŻŹ",
"pt": "ÁáÂâÃãÀàÇçÉéÊêÍíÓóÔôÕõÚú",
"ru": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя",
"se": "åäöÅÄÖ",
"cn": "",
"tw": "",
}
subset_lowercase_BW = {
"Č": "č",
"Ě": "ě",
"Š": "š",
"Ú": "ú",
"Ů": "ů",
"Ž": "ž"
}
def get_chars_BW(subset):
result = standard_chars + extra_chars
if subset in special_chars_BW:
if (subset == "cz"):
result += "".join([char for char in special_chars_BW[subset] if char not in subset_lowercase_BW])
else:
result += "".join([char for char in special_chars_BW[subset]])
return result
def get_chars_encoding_BW(subset):
result = {}
offset = 128 - len(standard_chars)
chars = get_chars_BW(subset)
for char in chars:
if char not in standard_chars:
result[char] = "\\%03o" % (offset + chars.index(char))
if (subset == "cz"):
for upper, lower in subset_lowercase_BW.items():
if lower in result:
result[upper] = result[lower]
return result
+351
View File
@@ -0,0 +1,351 @@
#!/usr/bin/env python3
"""
Translation Checker for EdgeTX Translations
This script can check:
1. Bootloader translations (bl_translations.h) - unified file with multiple languages
2. Individual language translation files (*.h) - separate files per language
It ensures that all translation languages have the same set of translation strings defined.
"""
import re
import sys
import os
from pathlib import Path
from collections import defaultdict
from typing import Dict, Set, List, Tuple, Optional
import argparse
import glob
class TranslationChecker:
def __init__(self):
self.bootloader_translations = defaultdict(set) # language -> set of bootloader translation keys
self.language_translations = defaultdict(set) # language -> set of language translation keys
self.bootloader_keys = set()
self.language_keys = set()
self.checked_files = []
def find_translations_directory(self, start_path: str) -> Optional[Path]:
"""Find the translations directory by searching up from the given path."""
current_path = Path(start_path).resolve()
# If the provided path is already the translations directory
if current_path.name == "translations" and current_path.is_dir():
return current_path
# If the provided path is a file in translations directory
if current_path.parent.name == "translations":
return current_path.parent
# Search up the directory tree
while current_path != current_path.parent:
translations_path = current_path / "radio" / "src" / "translations"
if translations_path.exists() and translations_path.is_dir():
return translations_path
current_path = current_path.parent
return None
def parse_bootloader_file(self, file_path: Path):
"""Parse the bootloader translation file (bl_translations.h)."""
if not file_path.exists():
print(f"Warning: File {file_path} does not exist")
return False
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Split content into lines for easier processing
lines = content.split('\n')
current_language = None
in_translation_block = False
conditional_depth = 0
for i, line in enumerate(lines):
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith('//'):
continue
# Check for translation language blocks
translation_match = re.match(r'#(?:if|elif)\s+defined\(TRANSLATIONS_([A-Z]+)\)', line)
if translation_match:
current_language = translation_match.group(1)
in_translation_block = True
conditional_depth = 0
continue
# Check for else block (default/English)
if re.match(r'#else', line) and in_translation_block and conditional_depth == 0:
current_language = "EN" # Default language
continue
# Track conditional compilation depth
if re.match(r'#if', line) and in_translation_block:
conditional_depth += 1
continue
elif re.match(r'#endif', line) and in_translation_block:
if conditional_depth > 0:
conditional_depth -= 1
else:
# This is the end of the translation block
in_translation_block = False
current_language = None
continue
# Skip non-translation lines
if not in_translation_block or not current_language:
continue
# Parse #define statements
define_match = re.match(r'#define\s+(TR_BL_\w+)', line)
if define_match:
key = define_match.group(1)
self.bootloader_translations[current_language].add(key)
self.bootloader_keys.add(key)
self.checked_files.append(str(file_path))
return True
def parse_language_file(self, file_path: Path) -> Optional[str]:
"""Parse an individual language translation file (e.g., en.h, fr.h)."""
if not file_path.exists():
return None
# Extract language code from filename
language = file_path.stem.upper()
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Find all #define TR_ statements
define_matches = re.findall(r'#define\s+(TR_\w+)', content)
for key in define_matches:
self.language_translations[language].add(key)
self.language_keys.add(key)
self.checked_files.append(str(file_path))
return language
def check_bootloader_translations(self, translations_dir: Path) -> bool:
"""Check bootloader translations."""
bl_file = translations_dir / "bl_translations.h"
return self.parse_bootloader_file(bl_file)
def check_language_translations(self, translations_dir: Path) -> List[str]:
"""Check individual language translation files."""
languages_found = []
# Look for .h files that are language files (excluding special files)
exclude_files = {"bl_translations.h", "untranslated.h"}
for h_file in translations_dir.glob("*.h"):
if h_file.name in exclude_files:
continue
# Skip files that are clearly not language files
if h_file.name.startswith("tts_"):
continue
language = self.parse_language_file(h_file)
if language:
languages_found.append(language)
return languages_found
def analyze(self) -> Dict[str, any]:
"""Analyze translations and return results."""
bootloader_languages = list(self.bootloader_translations.keys())
language_languages = list(self.language_translations.keys())
results = {
"bootloader": {
"languages": bootloader_languages,
"total_keys": len(self.bootloader_keys),
"missing_keys": defaultdict(set),
"extra_keys": defaultdict(set),
"summary": {}
},
"language_files": {
"languages": language_languages,
"total_keys": len(self.language_keys),
"missing_keys": defaultdict(set),
"extra_keys": defaultdict(set),
"summary": {}
},
"checked_files": self.checked_files
}
# Analyze bootloader translations
for lang in bootloader_languages:
lang_keys = self.bootloader_translations[lang]
results["bootloader"]["missing_keys"][lang] = self.bootloader_keys - lang_keys
results["bootloader"]["extra_keys"][lang] = lang_keys - self.bootloader_keys
results["bootloader"]["summary"][lang] = {
"total": len(lang_keys),
"missing": len(results["bootloader"]["missing_keys"][lang]),
"extra": len(results["bootloader"]["extra_keys"][lang])
}
# Analyze language file translations
for lang in language_languages:
lang_keys = self.language_translations[lang]
results["language_files"]["missing_keys"][lang] = self.language_keys - lang_keys
results["language_files"]["extra_keys"][lang] = lang_keys - self.language_keys
results["language_files"]["summary"][lang] = {
"total": len(lang_keys),
"missing": len(results["language_files"]["missing_keys"][lang]),
"extra": len(results["language_files"]["extra_keys"][lang])
}
return results
def print_report(self, verbose=False):
"""Print a detailed report of the translation analysis."""
results = self.analyze()
has_bootloader = bool(results["bootloader"]["languages"])
has_language_files = bool(results["language_files"]["languages"])
if not has_bootloader and not has_language_files:
print("No translation files found or processed.")
return
print("EdgeTX Translation Analysis")
print("=" * 50)
if verbose:
print(f"Checked files: {len(results['checked_files'])}")
for file_path in results['checked_files']:
print(f" - {file_path}")
print()
# Report bootloader translations
if has_bootloader:
self._print_section_report("Bootloader Translations (bl_translations.h)",
results["bootloader"], self.bootloader_keys, verbose)
# Report language file translations
if has_language_files:
self._print_section_report("Language File Translations (*.h)",
results["language_files"], self.language_keys, verbose)
def _print_section_report(self, title: str, section_results: Dict, all_keys: Set, verbose: bool):
"""Print report for a specific section (bootloader or language files)."""
print(f"\n{title}")
print("-" * len(title))
print(f"Total unique translation keys: {section_results['total_keys']}")
print(f"Languages found: {', '.join(sorted(section_results['languages']))}")
print()
# Summary table
print("Summary by Language:")
print("-" * 60)
print(f"{'Language':<12} {'Total':<8} {'Missing':<10} {'Extra':<8}")
print("-" * 60)
for lang in sorted(section_results['languages']):
summary = section_results['summary'][lang]
print(f"{lang:<12} {summary['total']:<8} {summary['missing']:<10} {summary['extra']:<8}")
print()
# Detailed missing keys report
has_issues = False
for lang in sorted(section_results['languages']):
missing = section_results['missing_keys'][lang]
extra = section_results['extra_keys'][lang]
if missing or extra:
has_issues = True
print(f"Issues for {lang}:")
if missing:
print(f" Missing keys ({len(missing)}):")
for key in sorted(missing):
print(f" - {key}")
if extra:
print(f" Extra keys ({len(extra)}):")
for key in sorted(extra):
print(f" + {key}")
print()
if not has_issues:
print("✅ All languages have consistent translation keys!")
else:
print("❌ Translation inconsistencies found!")
# Show all translation keys for reference (only if verbose)
if verbose:
print(f"\nAll Translation Keys ({len(all_keys)}):")
print("-" * 40)
for key in sorted(all_keys):
print(f" {key}")
print()
def main():
parser = argparse.ArgumentParser(
description="Check EdgeTX translation consistency",
epilog="""
Examples:
# Check bootloader translations only
python3 check_translations.py --bootloader
# Check individual language files only
python3 check_translations.py --languages
# Check both (default)
python3 check_translations.py
# Check with verbose output
python3 check_translations.py -v
# Specify path to translations directory or EdgeTX root
python3 check_translations.py /path/to/edgetx/radio/src/translations
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("path", nargs="?", default=".",
help="Path to translations directory, EdgeTX root, or current directory")
parser.add_argument("-v", "--verbose", action="store_true",
help="Show all translation keys")
parser.add_argument("--bootloader", action="store_true",
help="Check only bootloader translations (bl_translations.h)")
parser.add_argument("--languages", action="store_true",
help="Check only individual language files (*.h)")
args = parser.parse_args()
checker = TranslationChecker()
# Find translations directory
translations_dir = checker.find_translations_directory(args.path)
if not translations_dir:
print(f"Error: Could not find translations directory from path: {args.path}")
print("Please specify a path to the EdgeTX repository root or translations directory.")
sys.exit(1)
print(f"Using translations directory: {translations_dir}")
print()
# Determine what to check
check_bootloader = args.bootloader or not args.languages
check_languages = args.languages or not args.bootloader
if check_bootloader:
if not checker.check_bootloader_translations(translations_dir):
print("Warning: Could not process bootloader translations")
if check_languages:
languages_found = checker.check_language_translations(translations_dir)
if not languages_found:
print("Warning: No individual language translation files found")
checker.print_report(verbose=args.verbose)
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# Automatic indentation of all .c, .cpp and .h-files using clang-format
# based on https://github.com/opentx/opentx/wiki/OpenTX-Code-Style-Guide
# clang-format version 10.0.0
for i in $(find ./ -name "*.c" -or -name "*.cpp" -or -name "*.h")
do
echo $i
clang-format -i -style="{BasedOnStyle: LLVM,
IndentWidth: 2,
UseTab: Never,
AllowShortBlocksOnASingleLine: false,
AllowShortIfStatementsOnASingleLine: false,
AllowShortFunctionsOnASingleLine: false,
BreakBeforeBraces: Custom,
BraceWrapping: { AfterFunction: true,
BeforeElse: true },
IndentCaseLabels: true,
PenaltyReturnTypeOnItsOwnLine: 0,
ColumnLimit: 120}" $i
done
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
for f in `find ../src -regex '.*\.\(c\|cpp\|h\)$' -print`
do
if [[ $f != *"thirdparty"* ]]
then
dos2unix $f $f
uncrustify -c ./uncrustify.cfg --no-backup $f
./copyright.py ./copyright-header.txt $f
./include-guard.py $f
fi
done
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Stop on first error, echo on
set -e
set -x
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
for i in "$@"
do
case $i in
-Wno-error)
WERROR=0
shift
;;
-b*)
FLAVOR="${i#*b}"
shift
;;
esac
done
# Add GCC_ARM to PATH
if [[ -n ${GCC_ARM} ]] ; then
export PATH=${GCC_ARM}:$PATH
fi
: ${SRCDIR:=$(dirname "$(pwd)/$0")/..}
: ${BUILD_TYPE:=Debug}
: ${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -Wno-dev "}
if (( $WERROR )); then COMMON_OPTIONS+=" -DWARNINGS_AS_ERRORS=YES "; fi
: ${EXTRA_OPTIONS:="$EXTRA_OPTIONS"}
COMMON_OPTIONS+=${EXTRA_OPTIONS}" "
: ${FIRMARE_TARGET:="firmware-size"}
# Determine parallel jobs
determine_max_jobs
# wipe build directory clean
rm -rf build && mkdir -p build && cd build
target_names=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]' | tr ';' '\n')
for target_name in $target_names
do
BUILD_OPTIONS=${COMMON_OPTIONS}
echo "Testing ${target_name}"
if ! get_target_build_options "$target_name"; then
echo "Error: Failed to find a match for target '$target_name'"
exit 1
fi
cmake ${BUILD_OPTIONS} "${SRCDIR}"
cmake_build_parallel . --target native-configure
cmake_build_parallel native --target tests-radio
rm -f CMakeCache.txt native/CMakeCache.txt
done
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
# certifi==2021.5.30
# charset-normalizer==2.0.4
# idna==3.2
# Pillow==8.3.1
# requests==2.26.0
# urllib3==1.26.6
import requests
import os
import shutil
import subprocess
import platform
import tarfile
import pathlib
from PIL import Image
# if False, then sips will be used instead of ImageMagick
useMagick = True
LOGO_FILENAME = "edgetx-logo.png"
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
TOOLS_DIR = os.path.dirname(__file__)
IMAGES_DIR = os.path.join(PROJECT_ROOT, "companion", "src", "images")
WIN_ICONS_DIR = os.path.join(PROJECT_ROOT, "companion", "src", "images", "winicons")
LINUX_ICONS_DIR = os.path.join(PROJECT_ROOT, "companion", "src", "images", "linuxicons")
MAC_ICONS_DIR = os.path.join(PROJECT_ROOT, "companion", "src", "images", "macicons")
START_DIR = os.getcwd()
ABS_LOGO_FILENAME = os.path.abspath(os.path.join(TOOLS_DIR,LOGO_FILENAME))
def cleanup():
os.chdir(TOOLS_DIR)
os.remove(ABS_LOGO_FILENAME)
# Remove temporary mac icons folder
if os.path.exists(MAC_ICONS_DIR):
shutil.rmtree(MAC_ICONS_DIR)
if os.path.exists("icns.tar.gz"):
os.remove("icns.tar.gz")
if os.path.exists("icnsify"):
os.remove("icnsify")
os.chdir(START_DIR)
quit()
def downloadFile(url: str, outFile: str):
try:
r = requests.get(url, allow_redirects=True)
except requests.exceptions.RequestException as e: # This is the correct syntax
print("Unable to download!")
raise SystemExit(e)
open(outFile, 'wb').write(r.content)
# Startup checks
if not os.path.exists(IMAGES_DIR) or not os.path.exists(WIN_ICONS_DIR) or not os.path.exists(LINUX_ICONS_DIR):
print("Couldn't find a required directory!")
print("Images => " + IMAGES_DIR)
print("Windows => " + WIN_ICONS_DIR)
print("Linux => " + LINUX_ICONS_DIR)
quit()
# Download and save logo if we don't have it already
if not os.path.exists(LOGO_FILENAME):
print("Downloading logo...")
downloadFile('https://edgetx.org/assets/logo.png', LOGO_FILENAME)
print("Generate 96x96 icon.png... ")
os.chdir(IMAGES_DIR)
icon_png = os.path.join(IMAGES_DIR,'icon.png')
if os.path.exists(icon_png):
os.remove(icon_png)
img = Image.open(ABS_LOGO_FILENAME).resize((96, 96))
img.save(icon_png)
print("Generate Linux Icons... ", end="")
os.chdir(LINUX_ICONS_DIR)
linux_resolutions = [16, 22, 24, 32, 48, 64, 128, 256, 512]
for size in linux_resolutions:
new_image_folder = os.path.join(LINUX_ICONS_DIR, str(size) + 'x' + str(size))
companion_png = os.path.join(new_image_folder, 'companion.png')
if os.path.exists(companion_png):
os.remove(companion_png)
elif not os.path.exists(new_image_folder):
os.mkdir(new_image_folder)
print(str(size) + " ", end="")
img = Image.open(ABS_LOGO_FILENAME).resize((size, size))
img.save(companion_png)
print("\nGenerate Windows Icons... ", end="")
os.chdir(WIN_ICONS_DIR)
windows_resolutions = [16, 20, 24, 30, 32, 36, 40, 48, 60, 64, 72, 80, 96, 128, 256]
for size in windows_resolutions:
if os.path.exists('edgetx_' + str(size) + '.ico'):
os.remove('edgetx_' + str(size) + '.ico')
img = Image.open(ABS_LOGO_FILENAME).resize((size, size))
print(str(size) + " ", end="")
img.save('edgetx_' + str(size) + '.ico')
print("\nWindows All in One...")
# Since Pillow only supports [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
# setting any other size will use the nearest valid setting instead, don't bother with the full list
# icon_sizes = [(16,16), (20,20), (24,24), (30,30), (32, 32), (36,36), (40,40), (48, 48), (60,60), (64,64), (72,72), (80,80), (96,96), (128,128), (256,256)]
icon_sizes = [(16, 16), (24, 24), (32, 32), (48, 48),
(64, 64), (128, 128), (256, 256)]
img = Image.open(ABS_LOGO_FILENAME)
if os.path.exists('edgetx.ico'):
os.remove('edgetx.ico')
img.save('edgetx.ico', sizes=icon_sizes)
alternative_icns_tools = '''\
icns (Go) @ https://github.com/JackMordaunt/icns
make-icns (NodeJS) @ https://www.npmjs.com/package/make-icns
libicns @ https://icns.sourceforge.io\
'''
print("Generate Mac icon set...")
if platform.system() == "Linux":
icns_linux_gz = os.path.join(TOOLS_DIR, "icns.tar.gz")
print("Downloading icns (Go) for Linux amd64...")
downloadFile('https://github.com/JackMordaunt/icns/releases/download/v2.1.2/icns_2.1.2_Linux_x86_64.tar.gz',
icns_linux_gz)
print("Extract icnsify...")
tar = tarfile.open(icns_linux_gz, "r:gz")
for member in tar.getmembers():
if "icnsify" in member.name:
tar.extract(member, TOOLS_DIR)
tar.close()
if not os.path.exists(os.path.join(TOOLS_DIR, "icnsify")):
print("Something went wrong setting up icns! Some alternatives are")
print(alternative_icns_tools)
else:
print("Creating MacOS icon set...")
subprocess.call(
[
os.path.join(TOOLS_DIR, "icnsify"),
"-i",
ABS_LOGO_FILENAME,
"-o",
os.path.join(IMAGES_DIR, "iconmac.icns")
]
)
cleanup()
elif platform.system == "Windows":
print("Windows is not supported yet. Some options are:")
print(alternative_icns_tools)
cleanup()
elif platform.system == "Darwin":
if useMagick:
if shutil.which("magick") is None or shutil.which("iconutil") is None:
print("Required tool ImageMagick or Mac built in iconutil missing")
print("useMagick flag at top of file can be disabled to use sips instead,")
print("but it apparently has poorer image quality than ImageMagick.")
print()
print("Alternatives for icon generation are:")
print(alternative_icns_tools)
cleanup()
# Following was based on MIT licensed work from retifrav
# https://github.com/retifrav/python-scripts/blob/master/generate-iconset/generate-iconset.py
ext = pathlib.Path(ABS_LOGO_FILENAME).suffix
class IconParameters():
width = 0
scale = 1
def __init__(self, width, scale):
self.width = width
self.scale = scale
def getIconName(self):
if self.scale != 1:
return f"icon_{self.width}x{self.width}{ext}"
else:
return f"icon_{self.width//2}x{self.width//2}@2x{ext}"
ListOfIconParameters = [
IconParameters(16, 1),
IconParameters(16, 2),
IconParameters(32, 1),
IconParameters(32, 2),
IconParameters(64, 1),
IconParameters(64, 2),
IconParameters(128, 1),
IconParameters(128, 2),
IconParameters(256, 1),
IconParameters(256, 2),
IconParameters(512, 1),
IconParameters(512, 2),
IconParameters(1024, 1),
IconParameters(1024, 2)
]
# generate iconset
for ip in ListOfIconParameters:
if useMagick:
subprocess.call(
[
"magick",
"convert",
ABS_LOGO_FILENAME,
"-resize",
str(ip.width),
MAC_ICONS_DIR / ip.getIconName()
]
)
else:
subprocess.call(
[
"sips",
"-z",
str(ip.width),
str(ip.width),
ABS_LOGO_FILENAME,
"--out",
MAC_ICONS_DIR / ip.getIconName()
]
)
# convert iconset to icns file
subprocess.call(
[
"iconutil",
"-c",
"icns",
MAC_ICONS_DIR,
"-o",
IMAGES_DIR / "iconmac.icns"
]
)
cleanup()
+97
View File
@@ -0,0 +1,97 @@
file,width,height,modified
bmp_bootloader_plug_usb,111,59,2026-01-13 11:36:16
bmp_bootloader_usb_plugged,52,54,2026-01-13 11:36:16
bmp_logo_edgetx_splash,271,257,2026-01-13 11:36:16
bmp_radio_stick_background,90,90,2026-01-13 12:46:22
bmp_radio_stick_pointer,20,20,2026-01-13 11:36:16
mask_icon_edgetx,30,30,2026-01-13 11:36:16
mask_icon_menu_favs,30,30,2026-01-13 11:36:16
mask_icon_menu_manage_models,30,30,2026-01-13 11:36:16
mask_icon_menu_model_setup,30,30,2026-01-13 11:36:16
mask_icon_menu_radio_setup,30,30,2026-01-13 11:36:16
mask_icon_menu_tools,30,30,2026-01-13 11:36:16
mask_icon_menu_ui_setup,30,30,2026-01-13 11:36:16
mask_icon_model_curves,30,30,2026-01-13 11:36:16
mask_icon_model_flight_modes,30,30,2026-01-13 11:36:16
mask_icon_model_general,30,30,2026-01-13 11:36:16
mask_icon_model_gvars,30,30,2026-01-13 11:36:16
mask_icon_model_heli,30,30,2026-01-13 11:36:16
mask_icon_model_inputs,30,30,2026-01-13 11:36:16
mask_icon_model_logical_switches,30,30,2026-01-13 11:36:16
mask_icon_model_mixer,30,30,2026-01-13 11:36:16
mask_icon_model_mixer_scripts,30,30,2026-01-13 11:36:16
mask_icon_model_notes,30,30,2026-01-13 11:36:16
mask_icon_model_outputs,30,30,2026-01-13 11:36:16
mask_icon_model_special_functions,30,30,2026-01-13 11:36:16
mask_icon_model_telemetry,30,30,2026-01-13 11:36:16
mask_icon_model_timers,30,30,2026-01-13 11:36:16
mask_icon_model_usb,30,30,2026-01-13 11:36:16
mask_icon_radio_about,30,30,2026-01-13 11:36:16
mask_icon_radio_analogs,30,30,2026-01-13 11:36:16
mask_icon_radio_calibration,30,30,2026-01-13 11:36:16
mask_icon_radio_general,30,30,2026-01-13 11:36:16
mask_icon_radio_global_functions,30,30,2026-01-13 11:36:16
mask_icon_radio_hardware,30,30,2026-01-13 11:36:16
mask_icon_radio_trainer,30,30,2026-01-13 11:36:16
mask_icon_tools_apps,30,30,2026-01-13 11:36:16
mask_icon_tools_debug,30,30,2026-01-13 11:36:16
mask_icon_tools_monitor_ch,30,30,2026-01-13 11:36:16
mask_icon_tools_monitor_ls,30,30,2026-01-13 11:36:16
mask_icon_tools_reset,30,30,2026-01-13 11:36:16
mask_icon_tools_stats,30,30,2026-01-13 11:36:16
mask_icon_tools_storage,30,30,2026-01-13 11:36:16
mask_icon_ui_themes,30,30,2026-01-13 11:36:16
mask_icon_ui_topbar_setup,30,30,2026-01-13 11:36:16
mask_icon_ui_view1,30,30,2026-01-13 11:36:16
mask_icon_ui_view10,30,30,2026-01-13 11:36:16
mask_icon_ui_view2,30,30,2026-01-13 11:36:16
mask_icon_ui_view3,30,30,2026-01-13 11:36:16
mask_icon_ui_view4,30,30,2026-01-13 11:36:16
mask_icon_ui_view5,30,30,2026-01-13 11:36:16
mask_icon_ui_view6,30,30,2026-01-13 11:36:16
mask_icon_ui_view7,30,30,2026-01-13 11:36:16
mask_icon_ui_view8,30,30,2026-01-13 11:36:16
mask_icon_ui_view9,30,30,2026-01-13 11:36:16
mask_icon_ui_view_add,30,30,2026-01-13 11:36:16
mask_info_busy,96,96,2026-01-13 13:10:04
mask_info_error,96,96,2026-01-13 11:36:16
mask_info_shutdown,96,96,2026-01-13 11:36:16
mask_info_shutdown_circle0,75,75,2026-01-13 11:36:16
mask_info_shutdown_circle1,75,75,2026-01-13 11:36:16
mask_info_shutdown_circle2,75,75,2026-01-13 11:36:16
mask_info_shutdown_circle3,75,75,2026-01-13 11:36:16
mask_info_usb_plugged,211,110,2026-01-13 11:36:16
mask_inline_add,17,17,2026-01-13 11:36:16
mask_inline_curve,17,17,2026-01-13 11:36:16
mask_inline_dot,13,13,2026-01-13 11:36:16
mask_inline_fm,17,17,2026-01-13 11:36:16
mask_inline_inverted,11,16,2026-01-13 11:36:16
mask_inline_locked,11,16,2026-01-13 11:36:16
mask_inline_multiply,17,17,2026-01-13 11:36:16
mask_inline_replace,17,17,2026-01-13 11:36:16
mask_menu_edgetx,122,25,2026-01-13 11:36:16
mask_menu_favs,30,30,2026-01-13 11:36:16
mask_ui_bg_topbar_left,45,45,2026-01-13 11:36:16
mask_ui_bg_topbar_right,45,45,2026-01-13 11:36:16
mask_ui_btn_close,30,30,2026-01-13 11:36:16
mask_ui_btn_grid_large,26,26,2026-01-13 11:36:16
mask_ui_btn_grid_small,26,26,2026-01-13 11:36:16
mask_ui_btn_list_one,26,26,2026-01-13 11:36:16
mask_ui_btn_list_two,26,26,2026-01-13 11:36:16
mask_ui_btn_next,26,26,2026-01-13 11:36:16
mask_ui_btn_prev,26,26,2026-01-13 11:36:16
mask_widget_antenna,18,17,2026-01-13 11:36:16
mask_widget_gps,18,18,2026-01-13 11:36:16
mask_widget_timer,62,62,2026-01-13 11:36:16
mask_widget_timer_bg,180,70,2026-01-13 11:36:16
mask_widget_trim,15,15,2026-01-13 11:36:16
mask_widget_trim_shadow,17,17,2026-01-13 11:36:16
mask_widget_txbat,24,12,2026-01-13 11:36:16
mask_widget_txbat_charging,5,15,2026-01-13 11:36:16
mask_widget_usb,22,10,2026-01-13 11:36:16
mask_widget_volume0,18,16,2026-01-13 11:36:16
mask_widget_volume1,15,16,2026-01-13 11:36:16
mask_widget_volume2,19,16,2026-01-13 11:36:16
mask_widget_volume3,24,16,2026-01-13 11:36:16
mask_widget_volume4,30,16,2026-01-13 11:36:16
mask_widget_volume_scale,15,16,2026-01-13 11:36:16
1 file width height modified
2 bmp_bootloader_plug_usb 111 59 2026-01-13 11:36:16
3 bmp_bootloader_usb_plugged 52 54 2026-01-13 11:36:16
4 bmp_logo_edgetx_splash 271 257 2026-01-13 11:36:16
5 bmp_radio_stick_background 90 90 2026-01-13 12:46:22
6 bmp_radio_stick_pointer 20 20 2026-01-13 11:36:16
7 mask_icon_edgetx 30 30 2026-01-13 11:36:16
8 mask_icon_menu_favs 30 30 2026-01-13 11:36:16
9 mask_icon_menu_manage_models 30 30 2026-01-13 11:36:16
10 mask_icon_menu_model_setup 30 30 2026-01-13 11:36:16
11 mask_icon_menu_radio_setup 30 30 2026-01-13 11:36:16
12 mask_icon_menu_tools 30 30 2026-01-13 11:36:16
13 mask_icon_menu_ui_setup 30 30 2026-01-13 11:36:16
14 mask_icon_model_curves 30 30 2026-01-13 11:36:16
15 mask_icon_model_flight_modes 30 30 2026-01-13 11:36:16
16 mask_icon_model_general 30 30 2026-01-13 11:36:16
17 mask_icon_model_gvars 30 30 2026-01-13 11:36:16
18 mask_icon_model_heli 30 30 2026-01-13 11:36:16
19 mask_icon_model_inputs 30 30 2026-01-13 11:36:16
20 mask_icon_model_logical_switches 30 30 2026-01-13 11:36:16
21 mask_icon_model_mixer 30 30 2026-01-13 11:36:16
22 mask_icon_model_mixer_scripts 30 30 2026-01-13 11:36:16
23 mask_icon_model_notes 30 30 2026-01-13 11:36:16
24 mask_icon_model_outputs 30 30 2026-01-13 11:36:16
25 mask_icon_model_special_functions 30 30 2026-01-13 11:36:16
26 mask_icon_model_telemetry 30 30 2026-01-13 11:36:16
27 mask_icon_model_timers 30 30 2026-01-13 11:36:16
28 mask_icon_model_usb 30 30 2026-01-13 11:36:16
29 mask_icon_radio_about 30 30 2026-01-13 11:36:16
30 mask_icon_radio_analogs 30 30 2026-01-13 11:36:16
31 mask_icon_radio_calibration 30 30 2026-01-13 11:36:16
32 mask_icon_radio_general 30 30 2026-01-13 11:36:16
33 mask_icon_radio_global_functions 30 30 2026-01-13 11:36:16
34 mask_icon_radio_hardware 30 30 2026-01-13 11:36:16
35 mask_icon_radio_trainer 30 30 2026-01-13 11:36:16
36 mask_icon_tools_apps 30 30 2026-01-13 11:36:16
37 mask_icon_tools_debug 30 30 2026-01-13 11:36:16
38 mask_icon_tools_monitor_ch 30 30 2026-01-13 11:36:16
39 mask_icon_tools_monitor_ls 30 30 2026-01-13 11:36:16
40 mask_icon_tools_reset 30 30 2026-01-13 11:36:16
41 mask_icon_tools_stats 30 30 2026-01-13 11:36:16
42 mask_icon_tools_storage 30 30 2026-01-13 11:36:16
43 mask_icon_ui_themes 30 30 2026-01-13 11:36:16
44 mask_icon_ui_topbar_setup 30 30 2026-01-13 11:36:16
45 mask_icon_ui_view1 30 30 2026-01-13 11:36:16
46 mask_icon_ui_view10 30 30 2026-01-13 11:36:16
47 mask_icon_ui_view2 30 30 2026-01-13 11:36:16
48 mask_icon_ui_view3 30 30 2026-01-13 11:36:16
49 mask_icon_ui_view4 30 30 2026-01-13 11:36:16
50 mask_icon_ui_view5 30 30 2026-01-13 11:36:16
51 mask_icon_ui_view6 30 30 2026-01-13 11:36:16
52 mask_icon_ui_view7 30 30 2026-01-13 11:36:16
53 mask_icon_ui_view8 30 30 2026-01-13 11:36:16
54 mask_icon_ui_view9 30 30 2026-01-13 11:36:16
55 mask_icon_ui_view_add 30 30 2026-01-13 11:36:16
56 mask_info_busy 96 96 2026-01-13 13:10:04
57 mask_info_error 96 96 2026-01-13 11:36:16
58 mask_info_shutdown 96 96 2026-01-13 11:36:16
59 mask_info_shutdown_circle0 75 75 2026-01-13 11:36:16
60 mask_info_shutdown_circle1 75 75 2026-01-13 11:36:16
61 mask_info_shutdown_circle2 75 75 2026-01-13 11:36:16
62 mask_info_shutdown_circle3 75 75 2026-01-13 11:36:16
63 mask_info_usb_plugged 211 110 2026-01-13 11:36:16
64 mask_inline_add 17 17 2026-01-13 11:36:16
65 mask_inline_curve 17 17 2026-01-13 11:36:16
66 mask_inline_dot 13 13 2026-01-13 11:36:16
67 mask_inline_fm 17 17 2026-01-13 11:36:16
68 mask_inline_inverted 11 16 2026-01-13 11:36:16
69 mask_inline_locked 11 16 2026-01-13 11:36:16
70 mask_inline_multiply 17 17 2026-01-13 11:36:16
71 mask_inline_replace 17 17 2026-01-13 11:36:16
72 mask_menu_edgetx 122 25 2026-01-13 11:36:16
73 mask_menu_favs 30 30 2026-01-13 11:36:16
74 mask_ui_bg_topbar_left 45 45 2026-01-13 11:36:16
75 mask_ui_bg_topbar_right 45 45 2026-01-13 11:36:16
76 mask_ui_btn_close 30 30 2026-01-13 11:36:16
77 mask_ui_btn_grid_large 26 26 2026-01-13 11:36:16
78 mask_ui_btn_grid_small 26 26 2026-01-13 11:36:16
79 mask_ui_btn_list_one 26 26 2026-01-13 11:36:16
80 mask_ui_btn_list_two 26 26 2026-01-13 11:36:16
81 mask_ui_btn_next 26 26 2026-01-13 11:36:16
82 mask_ui_btn_prev 26 26 2026-01-13 11:36:16
83 mask_widget_antenna 18 17 2026-01-13 11:36:16
84 mask_widget_gps 18 18 2026-01-13 11:36:16
85 mask_widget_timer 62 62 2026-01-13 11:36:16
86 mask_widget_timer_bg 180 70 2026-01-13 11:36:16
87 mask_widget_trim 15 15 2026-01-13 11:36:16
88 mask_widget_trim_shadow 17 17 2026-01-13 11:36:16
89 mask_widget_txbat 24 12 2026-01-13 11:36:16
90 mask_widget_txbat_charging 5 15 2026-01-13 11:36:16
91 mask_widget_usb 22 10 2026-01-13 11:36:16
92 mask_widget_volume0 18 16 2026-01-13 11:36:16
93 mask_widget_volume1 15 16 2026-01-13 11:36:16
94 mask_widget_volume2 19 16 2026-01-13 11:36:16
95 mask_widget_volume3 24 16 2026-01-13 11:36:16
96 mask_widget_volume4 30 16 2026-01-13 11:36:16
97 mask_widget_volume_scale 15 16 2026-01-13 11:36:16
+1140
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python
from __future__ import print_function
import os
import sys
def writeheader(filename, header):
"""
write a header to filename,
skip files where first line after optional shebang matches the skip regex
filename should be the name of the file to write to
header should be a list of strings
skip should be a regex
"""
with open(filename, "r") as f:
inpt = f.readlines()
output = []
# comment out the next 3 lines if you don't wish to preserve shebangs
if len(inpt) > 0 and inpt[0].startswith("#!"):
output.append(inpt[0])
inpt = inpt[1:]
if inpt[0].strip().startswith("/*"):
for i, line in enumerate(inpt):
if line.strip().endswith("*/"):
inpt = inpt[i + 1:]
break
while not inpt[0].strip():
inpt = inpt[1:]
output.extend(header) # add the header
for line in inpt:
output.append(line)
try:
with open(filename, 'w') as f:
f.writelines(output)
print("added header to %s" % filename)
except IOError as err:
print("something went wrong trying to add header to %s: %s" % (filename, err))
def main(args=sys.argv):
with open(os.path.dirname(os.path.realpath(__file__)) + "/copyright-header.txt") as headerfile:
header = headerfile.readlines()
for filename in args[1:]:
writeheader(filename, header)
if __name__ == '__main__':
# call the main method
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Program to remove #define statements from C/C++ files based on a list of names.
"""
import re
import argparse
import sys
from pathlib import Path
def load_names_to_remove(names_file):
"""Load the list of names to remove from the specified file."""
try:
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
return set(names) # Use set for faster lookup
except FileNotFoundError:
print(f"Error: Names file '{names_file}' not found.")
sys.exit(1)
except Exception as e:
print(f"Error reading names file: {e}")
sys.exit(1)
def remove_defines_from_file(file_path, names_to_remove, dry_run=False):
"""Remove #define statements from a file based on the names list."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception as e:
print(f"Error reading file '{file_path}': {e}")
return False
modified_lines = []
removed_count = 0
i = 0
# Pattern to match #define statements
# Matches: #define NAME or #define NAME value
define_pattern = re.compile(r'^\s*#define\s+(\w+)')
while i < len(lines):
line = lines[i]
line_num = i + 1
match = define_pattern.match(line)
if match:
define_name = match.group(1)
if define_name in names_to_remove:
# Found a #define to remove, collect all continuation lines
lines_to_remove = []
current_line = line
current_line_num = line_num
# Keep collecting lines while they end with backslash
while current_line.rstrip().endswith('\\'):
lines_to_remove.append((current_line_num, current_line))
i += 1
if i < len(lines):
current_line = lines[i]
current_line_num = i + 1
else:
break
# Add the final line (without backslash or last line of file)
lines_to_remove.append((current_line_num, current_line))
# Print what we're removing
if not dry_run:
print(f"Removing multi-line #define from {file_path}:")
for ln, l in lines_to_remove:
print(f" Line {ln}: {l.rstrip()}")
else:
print(f"Would remove multi-line #define from {file_path}:")
for ln, l in lines_to_remove:
print(f" Line {ln}: {l.rstrip()}")
removed_count += 1
i += 1 # Move to next line after the #define block
continue
# Line is not a #define to remove, keep it
modified_lines.append(line)
i += 1
# Write back to file if not dry run and changes were made
if not dry_run and removed_count > 0:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(modified_lines)
print(f"Successfully processed '{file_path}' - removed {removed_count} #define(s)")
except Exception as e:
print(f"Error writing file '{file_path}': {e}")
return False
elif dry_run and removed_count > 0:
print(f"Would process '{file_path}' - {removed_count} #define(s) to remove")
elif removed_count == 0:
print(f"No matching #define statements found in '{file_path}'")
return True
def main():
parser = argparse.ArgumentParser(
description="Remove #define statements from C/C++ files based on a list of names"
)
parser.add_argument(
"names_file",
help="File containing names to remove (one name per line)"
)
parser.add_argument(
"source_files",
nargs="+",
help="Source files to process"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be removed without actually modifying files"
)
args = parser.parse_args()
# Load names to remove
names_to_remove = load_names_to_remove(args.names_file)
print(f"Loaded {len(names_to_remove)} names to remove")
if args.dry_run:
print("DRY RUN MODE - No files will be modified")
# Process each source file
success_count = 0
for source_file in args.source_files:
file_path = Path(source_file)
if not file_path.exists():
print(f"Warning: File '{source_file}' not found, skipping")
continue
if remove_defines_from_file(file_path, names_to_remove, args.dry_run):
success_count += 1
print(f"\nProcessed {success_count} out of {len(args.source_files)} files successfully")
if __name__ == "__main__":
main()
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
Program to check if #define names are used anywhere in C++ code files.
Use this to verify that defines are safe to remove.
"""
import re
import argparse
import sys
from pathlib import Path
from collections import defaultdict
def load_names_to_check(names_file):
"""Load the list of names to check from the specified file."""
try:
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
return names
except FileNotFoundError:
print(f"Error: Names file '{names_file}' not found.")
sys.exit(1)
except Exception as e:
print(f"Error reading names file: {e}")
sys.exit(1)
def check_usage_in_file(file_path, names_to_check, exclude_defines=True):
"""Check if any of the names are used in the given file."""
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
except Exception as e:
print(f"Error reading file '{file_path}': {e}")
return {}
lines = content.splitlines()
usage_found = defaultdict(list)
# Pattern to match #define statements (to potentially exclude them)
define_pattern = re.compile(r'^\s*#define\s+(\w+)')
# Pre-compile regex patterns for better performance
name_patterns = {name: re.compile(r'\b' + re.escape(name) + r'\b') for name in names_to_check}
for line_num, line in enumerate(lines, 1):
# Skip #define lines if exclude_defines is True
if exclude_defines and define_pattern.match(line):
continue
# Skip comments (basic comment detection)
# Remove single-line comments
if '//' in line:
line = line.split('//')[0]
# Skip lines that are mostly comments (basic /* */ detection)
if line.strip().startswith('/*') or line.strip().startswith('*'):
continue
# Check each name using pre-compiled patterns
for name, pattern in name_patterns.items():
if pattern.search(line):
usage_found[name].append({
'line_num': line_num,
'line': line.strip(),
'file': str(file_path)
})
return usage_found
def find_cpp_files(directories, extensions=None):
"""Find all C++ files in the given directories."""
if extensions is None:
extensions = {'.cpp', '.cxx', '.cc', '.c', '.hpp', '.hxx', '.h', '.hh'}
cpp_files = []
for directory in directories:
dir_path = Path(directory)
if dir_path.is_file():
if dir_path.suffix.lower() in extensions:
cpp_files.append(dir_path)
elif dir_path.is_dir():
for ext in extensions:
cpp_files.extend(dir_path.rglob(f'*{ext}'))
else:
print(f"Warning: '{directory}' is not a valid file or directory")
return cpp_files
def main():
parser = argparse.ArgumentParser(
description="Check if #define names are used anywhere in C++ code"
)
parser.add_argument(
"names_file",
help="File containing names to check (one name per line)"
)
parser.add_argument(
"paths",
nargs="+",
help="Files or directories to search (directories are searched recursively)"
)
parser.add_argument(
"--include-defines",
action="store_true",
help="Include #define statements in the search (default: exclude them)"
)
parser.add_argument(
"--extensions",
default=".cpp,.cxx,.cc,.c,.hpp,.hxx,.h,.hh",
help="Comma-separated list of file extensions to search (default: .cpp,.cxx,.cc,.c,.hpp,.hxx,.h,.hh)"
)
parser.add_argument(
"--summary-only",
action="store_true",
help="Show only summary of used/unused names"
)
args = parser.parse_args()
# Parse extensions
extensions = {ext.strip() for ext in args.extensions.split(',') if ext.strip()}
# Load names to check
names_to_check = load_names_to_check(args.names_file)
print(f"Checking usage of {len(names_to_check)} names")
# Find C++ files
cpp_files = find_cpp_files(args.paths, extensions)
print(f"Found {len(cpp_files)} C++ files to search")
if not cpp_files:
print("No C++ files found to search!")
sys.exit(1)
# Check usage in all files
all_usage = defaultdict(list)
files_processed = 0
for cpp_file in cpp_files:
usage = check_usage_in_file(cpp_file, names_to_check, exclude_defines=not args.include_defines)
files_processed += 1
for name, occurrences in usage.items():
all_usage[name].extend(occurrences)
# Progress indicator for large codebases
if files_processed % 100 == 0:
print(f"Processed {files_processed} files...")
# Analyze results
used_names = set()
unused_names = set()
for name in names_to_check:
if name in all_usage:
used_names.add(name)
else:
unused_names.add(name)
# Display results
print(f"\n{'='*60}")
print("USAGE ANALYSIS RESULTS")
print(f"{'='*60}")
if used_names:
print(f"\n❌ USED NAMES ({len(used_names)}) - DO NOT REMOVE:")
for name in sorted(used_names):
print(f"{name}")
if not args.summary_only:
print(f" Used in {len(set(occ['file'] for occ in all_usage[name]))} file(s):")
for occurrence in all_usage[name][:5]: # Show first 5 occurrences
print(f" {occurrence['file']}:{occurrence['line_num']}: {occurrence['line']}")
if len(all_usage[name]) > 5:
print(f" ... and {len(all_usage[name]) - 5} more occurrences")
print()
if unused_names:
print(f"\n✅ UNUSED NAMES ({len(unused_names)}) - SAFE TO REMOVE:")
for name in sorted(unused_names):
print(f"{name}")
# Summary
print(f"\n{'='*60}")
print("SUMMARY:")
print(f" Total names checked: {len(names_to_check)}")
print(f" Names in use: {len(used_names)}")
print(f" Names unused: {len(unused_names)}")
print(f" Files searched: {files_processed}")
if used_names:
print(f"\n⚠️ WARNING: {len(used_names)} names are still in use!")
print(" Review the usage before removing these #define statements.")
else:
print(f"\n✅ All {len(names_to_check)} names appear to be unused and safe to remove.")
if __name__ == "__main__":
main()
+29
View File
@@ -0,0 +1,29 @@
local function checktable(actual, reference)
for k, v in pairs( actual ) do
if v == reference[k] then
print("OK ", k, v, reference[k])
else
print("ISSUE ", k, v, reference[k])
end
end
end
local function run(event)
inputt= {name = "In1", source = 1, weight = 50, offset = 10, switch = 3}
mixt= {name = "Mix1", source = 1, weight = 50, offset = 10, curveType = 1, curveValue = 50, flightModes = 3, delayUp = 5, speedDown = 5, speedUp = 2, delayDown = 5, mixWarn = 1 , carryTrim = true , multiplex = 1, switch = 3}
outputt = {name = "Out1", min = -800, max = 800, offset = 10, ppmCenter = 10, symetrical = 1, revert = 1, switch = 3}
timert = {mode = 4, start = 0, countdownBeep = 2, persistent = 1, minuteBeep = true, value = 10 }
lst = {func = 2, v1 = 2, v2 = 2, v3 = 2, duration = 3, delay = 2, ["and"] = 2}
input = model.getInput(4,0)
mix = model.getMix(4,0)
output = model.getOutput(4)
ls = model.getLogicalSwitch(0)
timer = model.getTimer(0)
checktable(input,inputt)
checktable(mix, mixt)
checktable(output, outputt)
checktable(ls, lst)
checktable(timer, timert)
return 2
end
return {run=run}
+14
View File
@@ -0,0 +1,14 @@
local function run(event)
inputt= {name = "In1", source = 1, weight = 50, offset = 10, switch = 3}
mixt= {name = "Mix1", source = 1, weight = 50, offset = 10, curveType = 1, curveValue = 50, flightModes = 3, delayUp = 5, speedDown = 5, speedUp = 2, delayDown = 5, mixWarn = 1 , carryTrim = true , multiplex = 1, switch = 3}
outputt = {name = "Out1", min = -800, max = 800, offset = 10, ppmCenter = 10, symetrical = 1, revert = 1, switch = 3}
timert = {mode = 4, start = 0, countdownBeep = 2, persistent = 1, minuteBeep = true, value = 10 }
lst = {func = 2, v1 = 2, v2 = 2, v3 = 2, duration = 3, delay = 2, ["and"] = 2}
model.insertInput(4,0,inputt)
model.insertMix(4,0,mixt)
model.setOutput(4, outputt)
model.setLogicalSwitch(0, lst)
model.setTimer(0, timert)
return 2
end
return {run=run}
+46
View File
@@ -0,0 +1,46 @@
Use this two scripts to check eeprom upgrade on lua capale radio.
Using create.lua on source version will create a predefined set of input, mixer, switche, timer and logical swictch
Perform the eeprom upgrade
On the target version, run check.lua, the debug output will give something like :
OK source 1 1
OK switch 3 3
OK offset 10 10
OK weight 50 50
OK name In1 In1
OK weight 50 50
OK speedDown 5 5
OK curveValue 50 50
OK speedUp 2 2
OK delayDown 5 5
OK carryTrim true true
OK mixWarn 1 1
OK flightModes 3 3
OK offset 10 10
OK switch 3 3
OK source 1 1
OK curveType 1 1
OK delayUp 5 5
OK name Mix1 Mix1
OK multiplex 1 1
OK min -800 -800
OK ppmCenter 10 10
OK symetrical 1 1
OK offset 10 10
OK revert 1 1
OK max 800 800
OK name Out1 Out1
OK v1 2 2
OK delay 2 2
OK func 2 2
OK v3 2 2
OK and 2 2
OK v2 2 2
OK duration 3 3
OK minuteBeep true true
ISSUE value 43 10
OK persistent 1 1
OK start 0 0
OK countdownBeep 2 2
OK mode 4 4
The issue on 'value' for timer is normal and should be expected, but it should be the only one if eeprom upgrade was ok
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python
import sys
import argparse
from PIL import Image
class FontEncoder:
def __init__(self, filename):
self.f = open(filename, "w")
def write(self, value):
self.f.write("0x%02x," % value)
#
# def write_size(self, width, height):
# self.f.write("%d,%d,\n" % (width, height))
def encode(self, image, step):
image = image.convert(mode='L')
width, height = image.size
for y in range(0, height, step):
for x in range(width):
for l in range(0, step, 8):
value = 0
for z in range(min(8, step)):
# print(y + l + z)
if image.getpixel((x, y + l + z)) == 0:
value += 1 << z
if step < 8 and value == 0x7f:
value = 0xff
self.write(value)
# self.f.write("\n")
def encode_special(self, image, step):
image = image.convert(mode='L')
width, height = image.size
for y in range(0, height, step):
for x in range(width):
skip = True
for l in range(0, step, 8):
value = 0
for z in range(min(8, step)):
if l + z < 12:
if image.getpixel((x, y + l + z)) == 0:
value += 1 << z
else:
skip = False
if skip and l == 8:
value = 0xff
self.write(value)
def main():
parser = argparse.ArgumentParser(description='Fonts encoder')
parser.add_argument('input', action="store", help="Input file name")
parser.add_argument('output', action="store", help="Output file name")
args = parser.parse_args()
image = Image.open(args.input)
output = args.output
for s in ("03x05", "04x06", "05x07", "08x10", "10x14", "22x38"):
if s in args.input:
size = s
break
encoder = FontEncoder(output)
if size == "03x05":
encoder.encode(image, 5)
elif size == "04x06":
encoder.encode(image, 7)
elif size == "05x07":
encoder.encode(image, 8)
elif size == "08x10":
encoder.encode_special(image, 12)
elif size == "10x14":
encoder.encode(image, 16)
elif size == "22x38":
encoder.encode(image, 40)
else:
print("Unknown size", sys.argv[4])
if __name__ == "__main__":
main()
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import codecs
import sys
from charset import special_chars, get_chars_encoding, special_chars_BW, get_chars_encoding_BW
def main():
parser = argparse.ArgumentParser(description='Encoder for OpenTX translations')
parser.add_argument('input', action="store", help="Input file name")
parser.add_argument('output', action="store", help="Output file name")
parser.add_argument('language', action="store", help="Two letter language identifier", default=None)
parser.add_argument('bwlcd', action="store", help="Black White LCD", default="F")
parser.add_argument("--reverse", help="Reversed char conversion (from number to char)", action="store_true")
args = parser.parse_args()
if args.bwlcd == "F":
if args.language not in special_chars:
parser.error(args.language + ' is not a supported language. Try one of the supported ones: %s' % list(special_chars.keys()))
sys.exit()
else:
if args.language not in special_chars_BW:
parser.error(args.language + ' is not a supported language. Try one of the supported ones: %s' % list(special_chars_BW.keys()))
sys.exit()
# if args.reverse:
# for translation in special_chars:
# translations[translation] = [(after, before) for (before, after) in translations[translation]]
# Read the input file into a buffer
in_file = codecs.open(args.input, "r", "utf-8")
# Write the result to a temporary file
out_file = codecs.open(args.output, 'w', 'utf-8')
for line in in_file.readlines():
# # Do the special chars replacements
# if args.bwlcd == "F":
# for before, after in get_chars_encoding(args.language).items():
# line = line.replace(before, after)
# else:
# for before, after in get_chars_encoding_BW(args.language).items():
# line = line.replace(before, after)
# if line.startswith("#define ZSTR_"):
# before = line[32:-2]
# after = ""
# for c in before:
# if ord('A') <= ord(c) <= ord('Z'):
# c = "\\%03o" % (ord(c) - ord('A') + 1)
# elif ord('a') <= ord(c) <= ord('z'):
# c = "\\%03o" % (-ord(c) + ord('a') + 255)
# elif ord('0') <= ord(c) <= ord('9'):
# c = "\\%03o" % (ord(c) - ord('0') + 27)
# after = after + c
# line = line[:32] + after + line[-2:]
out_file.write(line)
out_file.close()
in_file.close()
if __name__ == "__main__":
main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/python3
import argparse
def line_index(lines, start):
for i, line in enumerate(lines):
if line.startswith(start):
return i
def extract_vars(lines):
result = []
lines = lines[line_index(lines, ".data"):line_index(lines, ".memory")]
i = 0
while i < len(lines):
line = lines[i]
i += 1
if line.startswith("*"):
continue
if line.startswith(" .data.") or line.startswith(" .bss."):
fields = (line + lines[i]).split()
# print(fields)
i += 1
var = fields[0].split(".")[-1]
offset = int(fields[1], 16)
size = int(fields[2], 16)
result.append((var, offset, size))
return result
def main():
parser = argparse.ArgumentParser(description="Extract firmware.map")
parser.add_argument("file", type=argparse.FileType("r"))
args = parser.parse_args()
f = args.file
lines = f.readlines()
vars = extract_vars(lines)
vars.sort(key=lambda var: "%08d %s" % (var[2], var[0]))
for var, offset, size in vars:
print("%s\t %d" % (var, size))
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# Stops on first error, echo on
set -e
# set -x
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
: "${SRCDIR:=$(dirname "$(pwd)/$0")/..}"
: ${FLAVOR:="nv14;el18;pl18;pl18ev;pl18u;nb4p;st16;pa01;t12;t12max;t15;t15pro;t16;t18;t8;zorro;pocket;commando8;tlite;tpro;tprov2;tpros;bumblebee;t20;t20v2;t14;lr3pro;mt12;gx12;tx12;tx12mk2;boxer;tx16s;tx16smk3;x10;x10express;x12s;x7;x7access;x9d;x9dp;x9dp2019;x9e;x9lite;x9lites;xlite;xlites;f16;v16;v14;v12;tx15"}
: ${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_RULE_MESSAGES=OFF -Wno-dev -DCMAKE_MESSAGE_LOG_LEVEL=WARNING"}
# wipe build directory clean
rm -rf build && mkdir -p build && cd build
target_names=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]' | tr ';' '\n')
for target_name in $target_names
do
BUILD_OPTIONS=${COMMON_OPTIONS}
BUILD_OPTIONS+=" $EXTRA_OPTIONS "
echo "Processing ${target_name}"
if ! get_target_build_options "$target_name"; then
echo "Error: Failed to find a match for target '$target_name'"
exit 1
fi
cmake ${BUILD_OPTIONS} "${SRCDIR}"
cmake --build . --target arm-none-eabi-configure
cmake --build arm-none-eabi --target hardware_defs
rm -f CMakeCache.txt arm-none-eabi/CMakeCache.txt
done
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Stops on first error
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
# Add GCC_ARM to PATH
if [[ -n ${GCC_ARM} ]] ; then
export PATH=${GCC_ARM}:$PATH
fi
: ${FLAVOR:="t15;tx16s;pl18;nv14;pl18u;nb4p;x9d;x9dp2019;x9e;xlite;xlites;x7;tpro;t20;f16;gx12;st16;pa01;tx15;t15pro;tx16smk3"}
: ${SRCDIR:=$(dirname "$(pwd)/$0")/..}
: ${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_RULE_MESSAGES=OFF -Wno-dev -DDISABLE_COMPANION=YES -DCMAKE_MESSAGE_LOG_LEVEL=WARNING"}
# wipe build directory clean
rm -rf build && mkdir -p build && cd build
target_names=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]' | tr ';' '\n')
for target_name in $target_names
do
BUILD_OPTIONS=${COMMON_OPTIONS}
BUILD_OPTIONS+=" $EXTRA_OPTIONS "
echo "Generating YAML structures for ${target_name}"
if ! get_target_build_options "$target_name"; then
echo "Error: Failed to find a match for target '$target_name'"
exit 1
fi
cmake ${BUILD_OPTIONS} "${SRCDIR}"
make native-configure
make -C native yaml_data
rm -f CMakeCache.txt arm-none-eabi/CMakeCache.txt
done
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
import os
for filename in sys.argv[1:]:
with open(filename, "r") as f:
lines = f.readlines()
newguard = "_" + os.path.basename(filename).upper().replace(".", "_") + "_"
for i, line in enumerate(lines):
line = line.strip()
if line.startswith("#ifndef "):
guard = line[8:]
if lines[i + 1].strip() == "#define %s" % guard:
print(filename, ":", guard, "=>", newguard)
lines[i] = "#ifndef %s\n" % newguard
lines[i + 1] = "#define %s\n" % newguard
end = -1
while not lines[end].strip().startswith("#endif"):
end -= 1
lines[end] = "#endif // %s\n" % newguard
with open(filename, "w") as f:
f.write("".join(lines))
break
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) EdgeTX
#
# Based on code named
# opentx - https://github.com/opentx/opentx
# th9x - http://code.google.com/p/th9x
# er9x - http://code.google.com/p/er9x
# gruvin9x - http://code.google.com/p/gruvin9x
#
# License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import argparse
import csv
import os
def build_transitions_array(f, column):
f.seek(0)
reader = csv.reader(f, delimiter=',')
last = None
transitions = []
for i, row in enumerate(reader):
if i == 0:
continue
value = row[column]
if last is None or value != last:
transitions.append((float(row[0]) * 1000, int(value)))
last = value
debounced = []
i = 0
while i < len(transitions):
t, val = transitions[i]
if i < len(transitions) - 1 and transitions[i+1][0] - t < 0.002:
i += 2
continue
debounced.append((t, val))
i += 1
return debounced
class Frame:
def __init__(self):
self.transitions = []
def push(self, t, value):
self.transitions.append((t, value))
def start(self):
return self.transitions[0][0]
def end(self):
return self.transitions[-1][0]
def is_after(self, t):
return self.start() >= t
def output(self, time):
value = 0
for t, v in self.transitions:
if t > time:
return value
value = v
return value
@classmethod
def get_frames(cls, transitions):
result = []
current_frame = None
last = 0
for t, value in transitions:
if t - last > 3:
if current_frame:
result.append(current_frame)
current_frame = cls()
if current_frame is not None:
current_frame.push(t, value)
last = t
return result
class SBusFrame(Frame):
def byte(self, index):
t = self.start() + 0.12 * index + 0.015
value = 0
for bit in range(8):
value += (1 - self.output(t)) << bit
t += 0.010
return value
def is_lost(self):
return self.byte(23) & 0x04
def value(self, channel):
bits_available = 0
bits = 0
byte = 0
value = None
for i in range(channel + 1):
while bits_available < 11:
byte += 1
bits |= self.byte(byte) << bits_available
bits_available += 8
value = ((bits & 0b11111111111) - 0x3E0) * 5 / 8
bits_available -= 11
bits >>= 11
return round((value * 100) / 512)
def __str__(self):
return "%.03fms " % self.start() + " ".join(["%02X" % self.byte(i) for i in range(25)])
class PwmFrame(Frame):
def duration(self):
return self.end() - self.start()
def value(self, channel):
return round((self.duration() * 1000 - 1500) * 100 / 512)
def __str__(self):
return "%.03fms %d" % (self.start(), self.value(0))
class LatencyStatistics:
def __init__(self, trigger_transitions, frames, channel, highval, lowval):
self.trigger_transitions = trigger_transitions
self.frames = frames
self.channel = channel
self.highval = highval
self.lowval = lowval
def iter(self):
for t0, val in self.trigger_transitions[1:]:
value = self.highval if val == 1 else self.lowval
for frame in self.frames:
if frame.is_after(t0) and frame.value(self.channel) == value:
delay = frame.end() - t0
yield (t0, val, delay)
break
@staticmethod
def append_to_line(f, s, index, lines, columns):
if columns == 0:
f.write(s)
elif index < len(lines):
f.write(lines[index].strip() + ";" + s)
else:
f.write(";" * columns + s)
f.write("\n")
def export(self, path, title, append):
lines = []
columns = 0
if append and os.path.exists(path):
with open(path, 'r') as f:
lines = f.readlines()
columns = len(lines[0].split(";"))
with open(path, 'w') as f:
self.append_to_line(f, title, 0, lines, columns)
index = 1
for t0, val, delay in self.iter():
self.append_to_line(f, str(delay), index, lines, columns)
index += 1
def print(self):
mini, maxi = None, None
count = 0
total = 0
for t0, val, delay in self.iter():
count += 1
total += delay
if mini is None or delay < mini[0]:
mini = (delay, t0)
if maxi is None or delay > maxi[0]:
maxi = (delay, t0)
print("Delay between the switch toggle and the end of the SBUS frame:")
print(" Count = %d transitions" % count)
print(" Average = %.1fms" % (total / count))
print(" Mini = %.1fms @ %fs" % (mini[0], mini[1] / 1000))
print(" Maxi = %.1fms @ %fs" % (maxi[0], maxi[1] / 1000))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('file', help='file to parse', type=argparse.FileType('r'))
parser.add_argument('--trigger', help='column in the CSV file where is the trigger', type=int, required=True)
parser.add_argument('--pwm', help='column in the CSV file where is the PWM output', type=int)
parser.add_argument('--sbus', help='column in the CSV file where is the SBUS output', type=int)
parser.add_argument('--channel', help='channel to check', type=int, default=0)
parser.add_argument('--highval', help='value of channel when trigger=HIGH', type=int, default=+100)
parser.add_argument('--lowval', help='value of channel when trigger=LOW', type=int, default=-100)
parser.add_argument('--export', help='CSV file to export latency values')
parser.add_argument('--title', help='CSV column title', default="Unknown")
parser.add_argument('--append', help='export CSV file in append mode', action='store_true')
args = parser.parse_args()
trigger_transitions = build_transitions_array(args.file, args.trigger)
if args.sbus:
sbus_transitions = build_transitions_array(args.file, args.sbus)
frames = SBusFrame.get_frames(sbus_transitions)
for frame in frames:
if frame.is_lost():
print("Frame lost bit @ %fs" % (frame.start() / 1000))
elif args.pwm:
pwm_transitions = build_transitions_array(args.file, args.pwm)
frames = PwmFrame.get_frames(pwm_transitions)
else:
print("Either a PWM or SBUS column in CSV must be specified")
exit()
statistics = LatencyStatistics(trigger_transitions, frames, args.channel, args.highval, args.lowval)
if args.export:
statistics.export(args.export, args.title, args.append)
statistics.print()
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import codecs
import sys
from charset import special_chars, get_chars_encoding, special_chars_BW, get_chars_encoding_BW
all_languages = special_chars.keys()
def main():
languages = ""
char_list = []
lang_args = all_languages
if len(sys.argv) > 1:
lang_args = sys.argv[1:]
for lang in lang_args:
if lang not in special_chars:
print(lang + ' is not a supported language. Try one of the supported ones: %s' % list(special_chars.keys()))
sys.exit()
languages += ' ' + lang
char_list.extend(c for c in get_chars_encoding(lang).keys())
char_list = sorted(set(char_list))
chars = ""
for c in char_list:
if ord(c) < 0x10000 or ord(c) > 0x10014:
chars = chars + hex(ord(c)) + ','
print(f"{languages}: {chars}")
if __name__ == "__main__":
main()
+164
View File
@@ -0,0 +1,164 @@
#! /usr/bin/env bash
set -e
## Bash script to setup EdgeTX development environment on Ubuntu 22.04 running on bare-metal or in a virtual machine.
## Let it run as normal user and when asked, give sudo credentials
QT_VERSION="6.9.3"
GCC_ARM_VERSION="14.2.rel1"
PAUSEAFTEREACHLINE="false"
STEP=1
# Parse argument(s)
for arg in "$@"
do
if [[ $arg == "--pause" ]]; then
PAUSEAFTEREACHLINE="true"
fi
done
if [[ $(lsb_release -rs) != "22.04" ]]; then
echo "ERROR: Not running on Ubuntu 22.04!"
echo "Terminating the script now."
exit 1
fi
echo "=== Step $((STEP++)): Setting up package repositories ==="
sudo apt-get -y install --no-install-recommends software-properties-common gpg gpg-agent wget ca-certificates
sudo mkdir -p /etc/apt/keyrings
# Set up Kitware repository for newer cmake
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | \
gpg --dearmor - | sudo tee /etc/apt/keyrings/kitware-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main" | \
sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null
# Set up NodeSource repository for Node.js 20.x
wget -O - https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key 2>/dev/null | \
gpg --dearmor - | sudo tee /etc/apt/keyrings/nodesource.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \
sudo tee /etc/apt/sources.list.d/nodesource.list >/dev/null
# Set up Git PPA for newer git
sudo add-apt-repository ppa:git-core/ppa --yes
# Set up Rob Savoury's PPA for newer SDL2
sudo add-apt-repository ppa:savoury1/multimedia --yes
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Updating Ubuntu package lists. Please provide sudo credentials, when asked ==="
sudo apt-get -y update
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing packages ==="
sudo apt-get -y install --no-install-recommends \
build-essential \
cmake \
kitware-archive-keyring \
git \
zip \
unzip \
file \
gawk \
libsdl2-dev \
python3-dev \
python3-pip \
python3-setuptools \
python3-tk \
libcups2 \
libssl-dev \
libgtest-dev \
clang \
libclang-dev \
python-is-python3 \
dfu-util \
nodejs \
stlink-tools \
openocd \
pv
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing lv_font_conv ==="
sudo npm i lv_font_conv -g
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing Python packages ==="
sudo python3 -m pip install \
asciitree \
jinja2 \
pillow \
clang \
lz4 \
aqtinstall \
pyelftools \
pydantic
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing Qt ==="
sudo mkdir -p /opt/qt
sudo chown "${USER}:${USER}" /opt/qt
aqt install-qt --outputdir /opt/qt linux desktop ${QT_VERSION} linux_gcc_64 -m qtmultimedia qtserialport
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Fetching GNU Arm Embedded Toolchains ==="
wget -q --show-progress --progress=bar:force:noscroll https://developer.arm.com/-/media/Files/downloads/gnu/${GCC_ARM_VERSION}/binrel/arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Unpacking GNU Arm Embedded Toolchains ==="
pv arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz | tar xJf -
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Removing the downloaded archives ==="
rm arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Moving GNU Arm Embedded Toolchains to /opt ==="
sudo mv arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi /opt/gcc-arm-none-eabi
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Adding GNU Arm Embedded Toolchain and Qt to PATH of current user ==="
cat >> ~/.bashrc << EOF
export PATH="/opt/gcc-arm-none-eabi/bin:\$PATH"
export PATH="/opt/qt/${QT_VERSION}/gcc_64/bin:\$PATH"
EOF
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Removing modemmanager (conflicts with DFU) ==="
sudo apt-get -y remove modemmanager
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "Finished setting up EdgeTX development environment."
echo "Please execute: source ~/.bashrc"
+161
View File
@@ -0,0 +1,161 @@
#! /usr/bin/env bash
set -e
## Bash script to setup EdgeTX development environment on Ubuntu 24.04 running on bare-metal or in a virtual machine.
## Let it run as normal user and when asked, give sudo credentials
QT_VERSION="6.9.3"
GCC_ARM_VERSION="14.2.rel1"
PAUSEAFTEREACHLINE="false"
STEP=1
# Parse argument(s)
for arg in "$@"
do
if [[ $arg == "--pause" ]]; then
PAUSEAFTEREACHLINE="true"
fi
done
if [[ $(lsb_release -rs) != "24.04" ]]; then
echo "ERROR: Not running on Ubuntu 24.04!"
echo "Terminating the script now."
exit 1
fi
echo "=== Step $((STEP++)): Setting up package repositories ==="
sudo apt-get -y install --no-install-recommends software-properties-common gpg gpg-agent wget ca-certificates
sudo mkdir -p /etc/apt/keyrings
# Set up Kitware repository for newer cmake
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | \
gpg --dearmor - | sudo tee /etc/apt/keyrings/kitware-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main" | \
sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null
# Set up NodeSource repository for Node.js 20.x
wget -O - https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key 2>/dev/null | \
gpg --dearmor - | sudo tee /etc/apt/keyrings/nodesource.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \
sudo tee /etc/apt/sources.list.d/nodesource.list >/dev/null
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Updating Ubuntu package lists. Please provide sudo credentials, when asked ==="
sudo apt-get -y update
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing packages ==="
sudo apt-get -y install --no-install-recommends \
build-essential \
cmake \
kitware-archive-keyring \
git \
zip \
unzip \
file \
gawk \
libsdl2-dev \
python3-dev \
python3-pip \
python3-setuptools \
python3-tk \
libcups2 \
libssl-dev \
libgtest-dev \
clang \
libclang-dev \
python-is-python3 \
dfu-util \
nodejs \
stlink-tools \
openocd \
pv
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing lv_font_conv ==="
sudo npm i lv_font_conv -g
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing Python packages ==="
# Python 3.12+ enforces PEP 668 (managed environment); --break-system-packages allows pip installs outside a venv
sudo python3 -m pip install --break-system-packages --ignore-installed \
asciitree \
jinja2 \
pillow \
clang \
lz4 \
aqtinstall \
pyelftools \
pydantic
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Installing Qt ==="
sudo mkdir -p /opt/qt
sudo chown "${USER}:${USER}" /opt/qt
aqt install-qt --outputdir /opt/qt linux desktop ${QT_VERSION} linux_gcc_64 -m qtmultimedia qtserialport
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Fetching GNU Arm Embedded Toolchains ==="
wget -q --show-progress --progress=bar:force:noscroll https://developer.arm.com/-/media/Files/downloads/gnu/${GCC_ARM_VERSION}/binrel/arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Unpacking GNU Arm Embedded Toolchains ==="
pv arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz | tar xJf -
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Removing the downloaded archives ==="
rm arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi.tar.xz
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Moving GNU Arm Embedded Toolchains to /opt ==="
sudo mv arm-gnu-toolchain-${GCC_ARM_VERSION}-x86_64-arm-none-eabi /opt/gcc-arm-none-eabi
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Adding GNU Arm Embedded Toolchain and Qt to PATH of current user ==="
cat >> ~/.bashrc << EOF
export PATH="/opt/gcc-arm-none-eabi/bin:\$PATH"
export PATH="/opt/qt/${QT_VERSION}/gcc_64/bin:\$PATH"
EOF
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please press Enter to continue or Ctrl+C to stop."
read
fi
echo "=== Step $((STEP++)): Removing modemmanager (conflicts with DFU) ==="
sudo apt-get -y remove modemmanager
if [[ $PAUSEAFTEREACHLINE == "true" ]]; then
echo "Step finished. Please check the output above and press Enter to continue or Ctrl+C to stop."
read
fi
echo "Finished setting up EdgeTX development environment."
echo "Please execute: source ~/.bashrc"
+74
View File
@@ -0,0 +1,74 @@
#
# K&R (sort-of)
#
indent_with_tabs = 0 # 1=indent to level only, 2=indent with tabs
input_tab_size = 8 # original tab size
output_tab_size = 2 # new tab size
indent_columns = output_tab_size
indent_label = 2 # pos: absolute col, neg: relative column
nl_enum_brace = remove # "enum {" vs "enum \n {"
nl_union_brace = remove # "union {" vs "union \n {"
nl_struct_brace = remove # "struct {" vs "struct \n {"
nl_do_brace = remove # "do {" vs "do \n {"
nl_if_brace = remove # "if () {" vs "if () \n {"
nl_for_brace = remove # "for () {" vs "for () \n {"
nl_else_brace = remove # "else {" vs "else \n {"
nl_while_brace = remove # "while () {" vs "while () \n {"
nl_switch_brace = remove # "switch () {" vs "switch () \n {"
nl_fcall_brace = add # "foo() {" vs "foo()\n{"
nl_fdef_brace = add # "int foo() {" vs "int foo()\n{"
nl_brace_while = remove
nl_brace_else = add
nl_squeeze_ifdef = true
mod_case_brace = remove
sp_before_semi = remove
sp_paren_paren = remove # space between (( and ))
sp_return_paren = force # "return (1);" vs "return(1);"
sp_sizeof_paren = remove # "sizeof (int)" vs "sizeof(int)"
sp_before_sparen = force # "if (" vs "if("
sp_after_sparen = force # "if () {" vs "if (){"
sp_after_cast = remove # "(int) a" vs "(int)a"
sp_inside_braces = force # "{ 1 }" vs "{1}"
sp_inside_braces_struct = force # "{ 1 }" vs "{1}"
sp_inside_braces_enum = force # "{ 1 }" vs "{1}"
sp_inside_paren = remove # "( 1 )" vs "(1)"
sp_inside_fparen = remove # "( 1 )" vs "(1)" - functions
sp_inside_sparen = remove # "( 1 )" vs "(1)" - if/for/etc
sp_assign = force
sp_assign_default = remove
sp_arith = force
sp_bool = force
sp_compare = force
sp_after_comma = force
sp_after_ptr_star = force
sp_before_ptr_star = force
sp_before_byref = force
sp_after_byref = force
sp_func_def_paren = remove # "int foo (){" vs "int foo(){"
sp_func_call_paren = remove # "foo (" vs "foo("
sp_func_proto_paren = remove # "int foo ();" vs "int foo();"
sp_func_class_paren = remove
sp_inside_angle = remove
sp_before_angle = remove
sp_after_angle = force
indent_class = true
indent_access_spec_body = true
indent_switch_case = 2
eat_blanks_before_close_brace = true
eat_blanks_after_open_brace = true
pp_indent = remove
pp_indent_at_level = false
pp_indent_count = 2
# pp_indent_if = 2
# pp_if_indent_code = false
# pp_define_at_level = true
set FOR foreach
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# Stops on first error, echo on
set -e
set -x
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/build-common.sh"
# Determine number of CPU cores for parallel builds
if [ -f /usr/bin/nproc ]; then
num_cpus=$(nproc)
elif [ "$(uname)" = "Darwin" ]; then
num_cpus=$(sysctl -n hw.ncpu)
elif [ -f /usr/sbin/sysctl ]; then
num_cpus=$(sysctl -n hw.logicalcpu)
else
num_cpus=2
fi
: "${JOBS:=$num_cpus}"
# Parse command line arguments
while [ $# -gt 0 ]
do
case "$1" in
--jobs=*)
JOBS="${1#*=}";;
-j*)
JOBS="${1#*j}";;
-h|--help)
echo "Usage: $0 [-j<jobs>|--jobs=<jobs>]"
echo "Build companion translations using tx16s target configuration"
echo ""
echo "Options:"
echo " -j<jobs>, --jobs=<jobs> Number of parallel jobs (default: $num_cpus)"
echo " -h, --help Show this help message"
exit 0;;
-*)
echo >&2 "usage: $0 [-j<jobs>|--jobs=<jobs>]"
echo >&2 "Use -h or --help for more information"
exit 1;;
*)
echo >&2 "usage: $0 [-j<jobs>|--jobs=<jobs>]"
echo >&2 "Use -h or --help for more information"
exit 1;;
esac
shift
done
# Project root directory (one level up from tools)
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Set common build options
: "${BUILD_TYPE:=Release}"
: "${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_RULE_MESSAGES=OFF -Wno-dev"}"
BUILD_OPTIONS="${COMMON_OPTIONS} "
echo "Building companion translations with tx16s target configuration..."
# Get tx16s target build options from build-common.sh
if ! get_target_build_options "tx16s"; then
echo "Error: Failed to find a match for target 'tx16s'"
exit 1
fi
# Ensure we're in the project root
cd "${PROJECT_ROOT}"
# Create and enter build directory
rm -rf build
mkdir build
cd build
echo "Configuring build with options: ${BUILD_OPTIONS}"
cmake ${BUILD_OPTIONS} "${PROJECT_ROOT}"
echo "Configuring native build..."
cmake --build . --target native-configure
echo "Building companion translations with ${JOBS} parallel jobs..."
make -j"${JOBS}" -C native companion_translations
+307
View File
@@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""
EdgeTX JSON validation script
This script validates JSON files with optional schema validation:
1. Generic JSON syntax validation for any JSON file
2. EdgeTX fw.json schema validation (targets array, changelog string)
3. EdgeTX fw.json alphabetical order validation
Usage:
python3 tools/validate-json.py [options] [file.json]
Options:
--fw-schema, -f Validate against EdgeTX fw.json schema
--syntax-only, -s Only validate JSON syntax (and schema if --fw-schema)
--order-only, -o Only validate alphabetical order (requires --fw-schema)
--help, -h Show this help message
Auto-detection:
- If filename is 'fw.json', automatically applies --fw-schema
- If no file specified and 'fw.json' exists, uses it with --fw-schema
Examples:
python3 validate-json.py data.json # Generic JSON validation
python3 validate-json.py fw.json # Auto-detects fw.json schema
python3 validate-json.py --fw-schema config.json # Force fw.json schema on other file
python3 validate-json.py --syntax-only fw.json # Only syntax + fw schema
python3 validate-json.py --order-only fw.json # Only order validation
Exit codes:
0: All validations passed
1: Validation failed
2: File not found or other error
"""
import json
import sys
import os
import argparse
from pathlib import Path
def validate_json_syntax(file_path, show_message=True):
"""Validate JSON syntax and load the data."""
if show_message:
print(f'🔍 Validating JSON syntax: {file_path}')
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if show_message:
print('✅ JSON syntax is valid')
return data
except json.JSONDecodeError as e:
print(f'❌ JSON syntax error: {e}')
return None
except FileNotFoundError:
print(f'❌ File not found: {file_path}')
return None
except Exception as e:
print(f'❌ Error reading file: {e}')
return None
def validate_fw_schema(data):
"""Validate the EdgeTX fw.json schema structure."""
print('🔍 Validating EdgeTX fw.json schema structure...')
# Check required fields
if 'targets' not in data:
print('❌ Missing required field: "targets"')
return False
if 'changelog' not in data:
print('❌ Missing required field: "changelog"')
return False
# Check field types
if not isinstance(data['targets'], list):
print('❌ Field "targets" must be an array')
return False
if not isinstance(data['changelog'], str):
print('❌ Field "changelog" must be a string')
return False
# Validate each target structure
for i, target in enumerate(data['targets']):
target_num = i + 1
if not isinstance(target, list):
print(f'❌ Target {target_num} must be an array')
return False
if len(target) != 2:
print(f'❌ Target {target_num} must have exactly 2 elements, found {len(target)}')
return False
if not isinstance(target[0], str):
print(f'❌ Target {target_num} first element (name) must be a string')
return False
if not isinstance(target[1], str):
print(f'❌ Target {target_num} second element (prefix) must be a string')
return False
# Check for empty strings
if not target[0].strip():
print(f'❌ Target {target_num} name cannot be empty')
return False
if not target[1].strip():
print(f'❌ Target {target_num} prefix cannot be empty')
return False
# Check for extra fields
allowed_fields = {'targets', 'changelog'}
extra_fields = set(data.keys()) - allowed_fields
if extra_fields:
print(f'❌ Unexpected fields found: {sorted(extra_fields)}')
return False
print(f'✅ EdgeTX fw.json schema validation passed - found {len(data["targets"])} targets')
return True
def validate_fw_alphabetical_order(data):
"""Validate that fw.json targets are in case-insensitive alphabetical order."""
print('🔍 Validating fw.json targets alphabetical order (case-insensitive)...')
# Extract target names (first element of each target pair)
target_names = [target[0] for target in data['targets']]
# Sort case-insensitively
sorted_names = sorted(target_names, key=str.lower)
# Check if they're in order
if target_names != sorted_names:
print('❌ ERROR: Targets are not in alphabetical order (case-insensitive)!')
print()
print('Current order:')
for i, name in enumerate(target_names):
print(f' {i+1:2d}. {name}')
print()
print('Expected alphabetical order (case-insensitive):')
for i, name in enumerate(sorted_names):
print(f' {i+1:2d}. {name}')
print()
# Show misplaced targets
misplaced = []
for i, (current, expected) in enumerate(zip(target_names, sorted_names)):
if current != expected:
misplaced.append(f'Position {i+1}: found "{current}", expected "{expected}"')
if misplaced:
print('Misplaced targets:')
for item in misplaced[:10]: # Show first 10
print(f' - {item}')
if len(misplaced) > 10:
print(f' - ... and {len(misplaced) - 10} more')
return False
else:
print('✅ All targets are in alphabetical order (case-insensitive)!')
print(f'Found {len(target_names)} targets, all properly sorted.')
return True
def find_json_file(filename=None):
"""Find JSON file in current directory or repository root."""
if filename:
return filename
# Default to fw.json for backwards compatibility
# Check current directory first
if os.path.exists('fw.json'):
return 'fw.json'
elif os.path.exists('../fw.json'):
return '../fw.json'
else:
# Try to find repository root
current_dir = Path.cwd()
for parent in [current_dir] + list(current_dir.parents):
fw_json = parent / 'fw.json'
if fw_json.exists():
return str(fw_json)
return None
def is_fw_json_file(file_path):
"""Check if the file is fw.json based on filename."""
return os.path.basename(file_path).lower() == 'fw.json'
def main():
"""Main validation function."""
parser = argparse.ArgumentParser(
description='EdgeTX JSON validator',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 validate-json.py data.json # Generic JSON validation
python3 validate-json.py fw.json # Auto-detects fw.json schema
python3 validate-json.py --fw-schema config.json # Force fw.json schema validation
python3 validate-json.py --syntax-only fw.json # Only syntax + fw schema
python3 validate-json.py --order-only fw.json # Only order validation
"""
)
parser.add_argument('file', nargs='?', help='Path to JSON file (default: auto-detect fw.json)')
parser.add_argument('-f', '--fw-schema', action='store_true',
help='Validate against EdgeTX fw.json schema')
parser.add_argument('-s', '--syntax-only', action='store_true',
help='Only validate JSON syntax (and schema if --fw-schema)')
parser.add_argument('-o', '--order-only', action='store_true',
help='Only validate alphabetical order (requires --fw-schema)')
args = parser.parse_args()
# Validate argument combinations
if args.order_only and not args.fw_schema:
# Auto-enable fw-schema if doing order validation and file looks like fw.json
file_to_check = args.file or find_json_file()
if file_to_check and is_fw_json_file(file_to_check):
args.fw_schema = True
else:
print('❌ --order-only requires --fw-schema or fw.json file')
sys.exit(2)
# Determine file path
file_path = find_json_file(args.file)
if not file_path:
if args.file:
file_path = args.file # Use provided file even if not found (will error later)
else:
print('❌ No JSON file found. Please specify the file path.')
sys.exit(2)
# Auto-detect fw.json schema
if not args.fw_schema and is_fw_json_file(file_path):
args.fw_schema = True
# Determine what to validate
check_syntax = not args.order_only # Always check syntax unless order-only
check_schema = args.fw_schema and not args.order_only # Check schema if fw-schema and not order-only
check_order = args.fw_schema and not args.syntax_only # Check order if fw-schema and not syntax-only
print(f'EdgeTX JSON Validator')
print(f'====================')
print(f'File: {file_path}')
if args.fw_schema:
if args.syntax_only:
print('Mode: EdgeTX fw.json syntax and schema validation only')
elif args.order_only:
print('Mode: EdgeTX fw.json alphabetical order validation only')
else:
print('Mode: Full EdgeTX fw.json validation (syntax + schema + order)')
else:
print('Mode: Generic JSON syntax validation')
print()
# Load JSON data (required for any validation)
if args.order_only:
# For order-only mode, silently load the JSON (we still need to parse it)
print('🔍 Loading JSON for alphabetical order validation...')
data = validate_json_syntax(file_path, show_message=False)
if data is None:
sys.exit(1)
print('✅ JSON loaded successfully')
else:
# For syntax validation, show the full syntax validation
data = validate_json_syntax(file_path, show_message=True)
if data is None:
sys.exit(1)
validation_passed = True
if check_schema:
print()
# Validate EdgeTX fw.json schema structure
if not validate_fw_schema(data):
validation_passed = False
if args.syntax_only: # If syntax-only mode, exit immediately on failure
sys.exit(1)
# Don't continue to order validation if schema failed
check_order = False
if check_order:
if not args.order_only: # Only add spacing if we're not in order-only mode
print()
# Validate alphabetical order
if not validate_fw_alphabetical_order(data):
validation_passed = False
if not validation_passed:
sys.exit(1)
print()
if args.fw_schema:
if args.syntax_only:
print('🎉 EdgeTX fw.json syntax and schema validation passed!')
elif args.order_only:
print('🎉 EdgeTX fw.json alphabetical order validation passed!')
else:
print('🎉 All EdgeTX fw.json validations passed!')
else:
print('🎉 JSON syntax validation passed!')
if __name__ == '__main__':
main()