Sync edgetx to Gitea
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
Executable
+47
@@ -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)
|
||||
@@ -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
|
||||
Executable
+104
@@ -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",
|
||||
},
|
||||
}
|
||||
Executable
+72
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
Executable
+132
@@ -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
|
||||
Executable
+73
@@ -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()
|
||||
Executable
+153
@@ -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()
|
||||
Executable
+147
@@ -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()
|
||||
Executable
+172
@@ -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()
|
||||
Executable
+95
@@ -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
|
||||
@@ -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()
|
||||
Executable
+65
@@ -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()
|
||||
Executable
+100
@@ -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()
|
||||
Executable
+1334
File diff suppressed because it is too large
Load Diff
Executable
+110
@@ -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()
|
||||
|
||||
Executable
+149
@@ -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()
|
||||
Executable
+65
@@ -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()
|
||||
Executable
+290
@@ -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
|
||||
Executable
+152
@@ -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
|
||||
}
|
||||
Executable
+133
@@ -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}"
|
||||
@@ -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
|
||||
Executable
+351
@@ -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()
|
||||
Executable
+26
@@ -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
|
||||
@@ -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
|
||||
|
||||
Executable
+66
@@ -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
|
||||
Executable
+244
@@ -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()
|
||||
@@ -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
|
||||
|
Executable
+1140
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
Executable
+86
@@ -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()
|
||||
Executable
+65
@@ -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()
|
||||
Executable
+48
@@ -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()
|
||||
Executable
+37
@@ -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
|
||||
Executable
+41
@@ -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
|
||||
@@ -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
|
||||
Executable
+231
@@ -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()
|
||||
Executable
+38
@@ -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()
|
||||
Executable
+164
@@ -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"
|
||||
Executable
+161
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Executable
+82
@@ -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
|
||||
Executable
+307
@@ -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()
|
||||
Reference in New Issue
Block a user