diff --git a/bin/format_lua.py b/bin/format_lua.py new file mode 100644 index 0000000..df8d65c --- /dev/null +++ b/bin/format_lua.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import sys +import shutil +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +import tempfile +from typing import Optional, Tuple, List +from datetime import datetime + +DEFAULT_EXCLUDES = {"vendor", "third_party", "build", "dist", ".git", "node_modules"} + +# ---------- Encoding helpers (preserve originals; avoid losing characters like '°') ---------- +PREFERRED_FALLBACKS = ("cp1252", "latin-1") + +def read_text_preserve_encoding(path: Path) -> Tuple[str, str]: + """Read file, returning (text, encoding). Try utf-8 strict, then cp1252, latin-1, then raw latin-1 decode.""" + try: + return path.read_text(encoding="utf-8", errors="strict"), "utf-8" + except UnicodeDecodeError: + pass + for enc in PREFERRED_FALLBACKS: + try: + return path.read_text(encoding=enc, errors="strict"), enc + except UnicodeDecodeError: + continue + # Last resort: binary -> latin-1 decode to avoid data loss + raw = path.read_bytes() + return raw.decode("latin-1"), "latin-1" + +def write_text_with_encoding(path: Path, text: str, encoding: str) -> None: + path.write_text(text, encoding=encoding, newline=None) + +# ---------- Robust Lua comment stripper (respects ' " and long strings [=[ ]=]) ---------- +def _match_long_bracket(s: str, i: int) -> Optional[int]: + if i >= len(s) or s[i] != "[": + return None + j = i + 1 + eqs = 0 + while j < len(s) and s[j] == "=": + eqs += 1 + j += 1 + if j < len(s) and s[j] == "[": + return eqs + return None + + +def strip_lua_comments(code: str) -> str: + """Remove real Lua comments while preserving strings (', ", [=[ ]=]).""" + i, n = 0, len(code) + out: List[str] = [] + NORMAL, SQ, DQ, LONG_STR, LINE_COM, BLOCK_COM = range(6) + state = NORMAL + long_eqs = 0 + + while i < n: + ch = code[i] + + if state == NORMAL: + if ch == "-" and i + 1 < n and code[i + 1] == "-": + j = i + 2 + if j < n and code[j] == "[": + eqs = _match_long_bracket(code, j) + if eqs is not None: + state = BLOCK_COM + long_eqs = eqs + i = j + 2 + eqs + continue + state = LINE_COM + i += 2 + continue + + if ch == "[": + eqs = _match_long_bracket(code, i) + if eqs is not None: + state = LONG_STR + long_eqs = eqs + out.append(code[i : i + 2 + eqs]) + i += 2 + eqs + continue + + if ch == "'": + state = SQ + out.append(ch) + i += 1 + continue + if ch == '"': + state = DQ + out.append(ch) + i += 1 + continue + + out.append(ch) + i += 1 + continue + + if state == LINE_COM: + if ch == "\n": + out.append("\n") + state = NORMAL + i += 1 + continue + + if state == BLOCK_COM: + if ch == "]": + j = i + 1 + eqs = 0 + while j < n and code[j] == "=": + eqs += 1 + j += 1 + if eqs == long_eqs and j < n and code[j] == "]": + i = j + 1 + state = NORMAL + continue + if ch == "\n": + out.append("\n") + i += 1 + continue + + if state == SQ: + out.append(ch) + if ch == "\\" and i + 1 < n: + out.append(code[i + 1]) + i += 2 + continue + if ch == "'": + state = NORMAL + i += 1 + continue + + if state == DQ: + out.append(ch) + if ch == "\\" and i + 1 < n: + out.append(code[i + 1]) + i += 2 + continue + if ch == '"': + state = NORMAL + i += 1 + continue + + if state == LONG_STR: + out.append(ch) + if ch == "]": + j = i + 1 + eqs = 0 + while j < n and code[j] == "=": + eqs += 1 + j += 1 + if eqs == long_eqs and j < n and code[j] == "]": + out.append(code[i + 1 : j + 1]) + i = j + 1 + state = NORMAL + continue + i += 1 + continue + + return "".join(out) + + +# ---------- Header injection ---------- +def make_header(year: int, holder: str) -> str: + """Compact 3–4 line GPLv3 header.""" + return ( + "--[[\n" + f" Copyright (C) {year} {holder}\n" + " GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html\n" + "]] --\n" + ) + + +def split_bom_shebang(text: str): + """Return (prefix, rest) where prefix keeps BOM and/or shebang if present.""" + prefix = "" + i = 0 + if text.startswith("\ufeff"): + prefix += "\ufeff" + i = 1 + if text[i:].startswith("#!"): + nl = text.find("\n", i) + if nl == -1: + prefix += text[i:] + return prefix, "" + prefix += text[i : nl + 1] + i = nl + 1 + return prefix, text[i:] + + +def inject_header_final(formatted_text: str, header: str, enable: bool) -> str: + """Prepend header after BOM/shebang.""" + if not enable: + return formatted_text + prefix, body = split_bom_shebang(formatted_text) + # Since comments are stripped earlier, duplication isn’t possible. + header_out = header if (not body or body.startswith("\n")) else header + "\n" + return prefix + header_out + body + + +# ---------- File discovery ---------- +def find_lua_files(root: Path, excludes: set[str]) -> list[Path]: + files: list[Path] = [] + root = root.resolve() + for dirpath, dirnames, filenames in os.walk(root): + rel = Path(dirpath).relative_to(root) + dirnames[:] = [d for d in dirnames if (rel / d).parts[0] not in excludes] + for name in filenames: + if name.lower().endswith(".lua"): + files.append(Path(dirpath) / name) + return files + + +# ---------- Format pipeline ---------- +def run_lua_format_on_text(text: str, column_limit: int, extra_args: list[str]) -> Tuple[int, str, str]: + """Write text to a temp file, run lua-format (no -i) and return (exit_code, stdout, stderr).""" + with tempfile.NamedTemporaryFile("w+", suffix=".lua", delete=False, encoding="utf-8") as tf: + temp_path = Path(tf.name) + tf.write(text) + tf.flush() + try: + cmd = [ + "lua-format", + f"--column-limit={column_limit}", + "--keep-simple-function-one-line", + "--keep-simple-control-block-one-line", + "--no-align-args", + "--no-align-table-field", + ] + extra_args + [str(temp_path)] + proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="strict") + return proc.returncode, (proc.stdout or ""), (proc.stderr or "") + finally: + try: + temp_path.unlink(missing_ok=True) + except Exception: + pass + + +def process_file( + path: Path, + column_limit: int, + extra_args: list[str], + dry_run: bool, + make_backup: bool, + header_enable: bool, + header_holder: str, + header_year: int, +) -> Tuple[bool, Optional[str]]: + """Returns (changed, error_message).""" + # Read preserving original encoding + original, orig_enc = read_text_preserve_encoding(path) + + stripped = strip_lua_comments(original) + + code, out_stdout, out_stderr = run_lua_format_on_text(stripped, column_limit, extra_args) + if code != 0: + return (False, f"lua-format failed: {out_stderr.strip() or 'non-zero exit'}") + + formatted_text = out_stdout # only stdout contains the formatted code + + # Final: inject header + header_text = make_header(header_year, header_holder) + final_text = inject_header_final(formatted_text, header_text, enable=header_enable) + + if dry_run: + return (final_text != original, None) + + if final_text == original: + return (False, None) + + if make_backup: + bak = path.with_suffix(path.suffix + ".bak") + try: + shutil.copy2(path, bak) + except Exception as e: + return (False, f"failed to create backup: {e}") + + # Write back using the same encoding the file was originally in + write_text_with_encoding(path, final_text, orig_enc) + return (True, None) + + +# ---------- CLI ---------- +def main(): + parser = argparse.ArgumentParser( + description="Strip Lua comments, format via lua-format, then inject a compact GPL header (encoding-safe)." + ) + parser.add_argument("root", nargs="?", default=".", help="Root directory to scan (default: current dir)") + parser.add_argument("-x", "--exclude", action="append", default=[], help=f"Directory to exclude (default: {', '.join(sorted(DEFAULT_EXCLUDES))})") + parser.add_argument("--column-limit", type=int, default=300, help="Column limit (default: 300)") + parser.add_argument("--jobs", type=int, default=os.cpu_count() or 4, help="Parallelism (default: CPU count)") + parser.add_argument("--extra", nargs=argparse.REMAINDER, default=[], help="Extra args passed to lua-format (put them after --extra)") + parser.add_argument("--dry-run", action="store_true", help="Do not modify files; just report which would change") + parser.add_argument("--backup", action="store_true", help="Create .bak backups before writing") + + # Header options + parser.add_argument("--no-header", action="store_true", help="Disable final header injection") + parser.add_argument("--header-holder", default="Rob Thomson", help="Header holder name (default: Rob Thomson)") + parser.add_argument("--header-year", type=int, default=datetime.now().year, help="Header year (default: current year)") + + args = parser.parse_args() + + if shutil.which("lua-format") is None: + print("Error: lua-format not found in PATH.", file=sys.stderr) + sys.exit(127) + + root = Path(args.root) + excludes = set(DEFAULT_EXCLUDES) | set(args.exclude) + files = find_lua_files(root, excludes) + if not files: + print("No .lua files found.") + return + + print(f"Processing {len(files)} Lua files…") + changed = 0 + failed = 0 + failures: list[tuple[Path, str]] = [] + + with ThreadPoolExecutor(max_workers=max(1, args.jobs)) as ex: + futures = { + ex.submit( + process_file, + f, + args.column_limit, + args.extra, + args.dry_run, + args.backup, + not args.no_header, + args.header_holder, + args.header_year, + ): f + for f in files + } + for fut in as_completed(futures): + f = futures[fut] + try: + did_change, err = fut.result() + if err: + failed += 1 + failures.append((f, err)) + elif did_change: + changed += 1 + except Exception as e: + failed += 1 + failures.append((f, str(e))) + + if args.dry_run: + print(f"[dry-run] Would change: {changed} file(s); Failures: {failed}") + else: + print(f"Changed: {changed} file(s); Failures: {failed}") + + if failures: + print("\nFailures:") + for f, msg in failures: + print(f"- {f}: {msg}") + sys.exit(1 if failed else 0) + + +if __name__ == "__main__": + main() diff --git a/docs/dashboard-objects.md b/docs/dashboard-objects.md new file mode 100644 index 0000000..777f564 --- /dev/null +++ b/docs/dashboard-objects.md @@ -0,0 +1,923 @@ +# Dashboard Objects + +--- + +### API Version Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if not present + novalue : string -- (Optional) Text shown if telemetry value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Arc Gauge Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +Title parameters + title : string -- (Optional) Title text + titlepos : string -- (Optional) If `title` is present but `titlepos` is not set, title is placed at the top by default. + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) + titlespacing : number -- (Optional) Vertical gap between title and value + titlecolor : color -- (Optional) Title text color (theme/text fallback) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title +Value/Source parameters + value : any -- (Optional) Static value to display if telemetry is not present + source : string -- Telemetry sensor source name (e.g., "voltage", "current") + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value +Maxval parameters + arcmax : bool -- (Optional) Draw arcmac gauge within the outer arc (false by default) + maxfont : font -- (Optional) Font for max value label (e.g., FONT_XS, FONT_S, FONT_M, default: FONT_S) + maxtextcolor : color -- (Optional) Max text color (theme/text fallback) + maxpadding : number -- (Optional) Padding (Y-offset) below arc center for max value label (default: 0) + maxpaddingleft : number -- (Optional) Additional X-offset for max label (default: 0) + maxpaddingtop : number -- (Optional) Additional Y-offset for max label (default: 0) +Appearance/Theming + bgcolor : color -- (Optional) Widget background color (theme fallback) + fillbgcolor : color -- (Optional) Arc background color (theme fallback) + fillcolor : color -- (Optional) Arc foreground color (theme fallback) + maxprefix : string -- (Optional) Prefix for max value label (default: "+") +Arc Geometry/Advanced + min : number -- (Optional) Minimum value of the arc (default: 0) + max : number -- (Optional) Maximum value of the arc (default: 100) + thickness : number -- (Optional) Arc thickness in pixels + gaugepadding : number -- (Optional) Horizontal-only padding applied to arc radius (shrinks arc from left/right only) + gaugepaddingbottom : number -- (Optional) Extra space added below arc region, pushing arc upward (vertical only) +``` +--- + +### Arm Flags Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if not present + thresholds : table -- (Optional) List of thresholds: {value=..., textcolor=...} for coloring ARMED/DISARMED states. + novalue : string -- (Optional) Text shown if telemetry value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value (not used here) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +Example thresholds: +thresholds = { + { value = "ARMED", textcolor = "green" }, + { value = "DISARMED", textcolor = "red" }, + { value = "Throttle high", textcolor = "orange" }, + { value = "Failsafe", textcolor = "orange" }, +} +``` +--- + +### Attitude Horizon Widget (AH) + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (default: 0.2) + pixelsperdeg : number -- Pixels per degree for pitch & compass (default: 2.0) + dynamicscalemin : number -- Minimum scale factor (default: 1.05) + dynamicscalemax : number -- Maximum scale factor (default: 1.95) + showarc : bool -- Show arc markers (default: true) + showladder : bool -- Show pitch ladder (default: true) + showcompass : bool -- Show compass ribbon (default: true) + showaltitude : bool -- Show altitude bar on right (default: false) + showgroundspeed : bool -- Show groundspeed bar on left (default: false) + arccolor : color -- Color for arc markings (default: white) + laddercolor : color -- Color for pitch ladder (default: white) + compasscolor : color -- Color for compass (default: white) + crosshaircolor : color -- Color for central cross marker (default: white) + altitudecolor : color -- Color for altitude bar (default: white) + groundspeedcolor : color -- Color for groundspeed bar (default: white) + altitudemin : number -- Minimum displayed altitude (default: 0) + altitudemax : number -- Maximum displayed altitude (default: 200) + groundspeedmin : number -- Minimum displayed groundspeed (default: 0) + groundspeedmax : number -- Maximum displayed groundspeed (default: 100) +``` +--- + +### Bar Gauge Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +Title/label + title : string -- (Optional) Title text + titlepos : string -- (Optional) "top" or "bottom" + titlealign : string -- (Optional) "center", "left", "right" + titlefont : font -- (Optional) Title font (e.g., FONT_L) + titlespacing : number -- (Optional) Vertical gap below title + titlecolor : color -- (Optional) Title text color (theme/text fallback) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) + titlepaddingright : number -- (Optional) + titlepaddingtop : number -- (Optional) + titlepaddingbottom : number -- (Optional) +Value/source + value : any -- (Optional) Static value to display if no telemetry + hidevalue : bool -- (Optional) If true, do not display the value text (default: false; value is shown) + source : string -- (Optional) Telemetry sensor source name + transform : string|function|number -- (Optional) Value transformation + decimals : number -- (Optional) Number of decimal places for display + thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} + novalue : string -- (Optional) Text shown if value missing (default: "-") + unit : string -- (Optional) Unit label, "" to hide, or nil to auto-resolve + font : font -- (Optional) Value font (e.g., FONT_L) + valuealign : string -- (Optional) "center", "left", "right" + textcolor : color -- (Optional) Value text color (theme/text fallback) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) + valuepaddingright : number -- (Optional) + valuepaddingtop : number -- (Optional) + valuepaddingbottom : number -- (Optional) +Bar geometry/appearance + min : number -- (Optional) Min value (alias for gaugemin) + max : number -- (Optional) Max value (alias for gaugemax) + gaugeorientation : string -- (Optional) "vertical" or "horizontal" + gaugepaddingleft : number -- (Optional) + gaugepaddingright : number -- (Optional) + gaugepaddingtop : number -- (Optional) + gaugepaddingbottom : number -- (Optional) + roundradius : number -- (Optional) Corner radius to apply rounding on edges of the bar +Appearance/Theming + bgcolor : color -- (Optional) Widget background color (theme fallback) + fillbgcolor : color -- (Optional) Bar background color (theme fallback) + fillcolor : color -- (Optional) Bar fill color (theme fallback) +Battery-style bar options + batteryframe : bool -- (Optional) Draw battery frame & cap around the bar (applies to both standard and segmented bars) + battery : bool -- (Optional) If true, draw a segmented battery bar instead of a standard fill bar + batteryframethickness : number -- (Optional) Battery frame outline thickness (default: 2) + batterysegments : number -- (Optional) Number of segments for segmented battery bar (default: 6) + batteryspacing : number -- (Optional) Spacing (pixels) between battery segments (default: 2) + batterysegmentpaddingtop : number -- (Optional) Padding (pixels) from the top of each horizontal segment (default: 0) + batterysegmentpaddingbottom : number -- (Optional) Padding (pixels) from the bottom of each horizontal segment (default: 0) + accentcolor : color -- (Optional) Color for the battery frame and cap (theme fallback) + cappaddingleft : number -- (Optional) Padding from the left edge of the cap (default: 0) + cappaddingright : number -- (Optional) Padding from the right edge of the cap (default: 0) + cappaddingtop : number -- (Optional) Padding from the top edge of the cap (default: 0) + cappaddingbottom : number -- (Optional) Padding from the bottom edge of the cap (default: 0) +Battery Advanced Info (Optional overlay for battery/fuel bar) + battadv : bool -- (Optional) If true, shows advanced battery/fuel telemetry info lines (voltage, per-cell voltage, consumption, cell count) + battadvfont : font -- Font for advanced info lines (e.g., "FONT_XS", "FONT_M"). Defaults to FONT_XS if unset + battadvblockalign : string -- Horizontal alignment of the entire info block: "left", "center", or "right" (default: "right") + battadvvaluealign : string -- Text alignment within each info line: "left", "center", or "right" (default: "left") + battadvpadding : number -- Padding (pixels) applied to all sides unless overridden by individual paddings (default: 4) + battadvpaddingleft : number -- Padding (pixels) on the left side of the info block (overrides battadvpadding) + battadvpaddingright : number -- Padding (pixels) on the right side of the info block (overrides battadvpadding) + battadvpaddingtop : number -- Padding (pixels) above the first info line (overrides battadvpadding) + battadvpaddingbottom : number -- Padding (pixels) below the last info line (overrides battadvpadding) + battadvgap : number -- Vertical gap (pixels) between info lines (default: 5) +Subtext + subtext : string -- (Optional) A line of subtext to draw inside the bar (usually below value) + subtextfont : font -- (Optional) Font for subtext (default: FONT_XS) + subtextalign : string -- (Optional) "center", "left", or "right" (default: "left") + subtextpaddingleft : number -- (Optional) Padding from left edge of bar (default: 0) + subtextpaddingright : number -- (Optional) Padding from right edge of bar (default: 0) + subtextpaddingtop : number -- (Optional) Extra offset from top of bar (default: 0) + subtextpaddingbottom : number -- (Optional) Padding above bottom of bar (default: 0) +``` +--- + +### Blackbox Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if not present + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) on used MB + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Clock Widget + +``` + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value or configure as "" to omit the unit from being displayed. + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Craft Name Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if not present + novalue : string -- (Optional) Text shown if craft name is missing (default: "-") + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Custom Function Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + wakeup : function -- Custom wakeup function, called with (box, telemetry), should return a table to cache + paint : function -- Custom paint function, called with (x, y, w, h, box, cache, telemetry) +Note: This widget does not process colors, layout, or padding. All rendering and caching logic must be handled in the user's custom functions. +``` +--- + +### Dial Image Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +title parameters + title : string -- (Optional) Title text + titlealign : string -- (Optional) "center", "left", "right" + titlefont : font -- (Optional) Title font (e.g., font_l, font_xl) + titlespacing : number -- (Optional) Gap below title + titlecolor : color -- (Optional) Title text color + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) + titlepaddingright : number -- (Optional) + titlepaddingtop : number -- (Optional) + titlepaddingbottom : number -- (Optional) +value / source parameters + value : any -- (Optional) Static value to display if telemetry is not present + source : string -- Telemetry sensor name + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", etc.) + decimals : number -- (Optional) Decimal precision + novalue : string -- (Optional) Text if telemetry is missing (default: "-") + unit : string -- (Optional) Unit label ("" hides unit) + font : font -- (Optional) Value font (e.g. font_l) + valuealign : string -- (Optional) "center", "left", "right" + textcolor : color -- (Optional) Text color + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) + valuepaddingright : number -- (Optional) + valuepaddingtop : number -- (Optional) + valuepaddingbottom : number -- (Optional) +dial image & needle styling + dial : string|number|function -- Dial image selector (used for asset path) + scalefactor : number -- (Optional) Image scale multiplier (default: 0.4) + needlecolor : color -- (Optional) Needle color (default: theme) + needlehubcolor : color -- (Optional) Hub color (default: theme) + needlethickness : number -- (Optional) Needle width in pixels (default: 3) + needlehubsize : number -- (Optional) Hub circle radius in pixels (default: needle thickness + 2) + needlestartangle : number -- (Optional) Needle starting angle in degrees (default: 135) + needlesweepangle : number -- (Optional) Needle sweep angle in degrees (default: 270) + + bgcolor : color -- Widget background color (default: theme fallback) +``` +--- + +### Dynamic Power (Watts) Display Widget + +``` + title : string -- (Optional) Title text displayed above or below the value + titlepos : string -- "top" or "bottom" (default) + titlealign : string -- "center", "left", or "right" + titlefont : font -- Font for title (e.g., FONT_L) + titlespacing : number -- Vertical gap between title and value (pixels) + titlecolor : color -- Title text color + titlepadding : number -- Padding for title (all sides) + font : font -- Font for value (e.g., FONT_XL) + valuealign : string -- "center", "left", or "right" + textcolor : color -- Value text color + valuepadding : number -- Padding for value (all sides) + bgcolor : color -- Widget background color + novalue : string -- Text to show if sensors unavailable (default: "-") + source : string -- "current", "min", "max", or "avg" (default: "current") +``` +--- + +### Flight Count Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if not present + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Flight Time Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Governor State Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Vertical gap between title and value text + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + displayValue : any -- (Optional) Value to display (processed governor state) + unit : string -- (Not used) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) + thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") +Example thresholds: +thresholds = { + { value = "DISARMED", textcolor = "red" }, + { value = "ACTIVE", textcolor = "green" }, + ... +} +``` +--- + +### Image Box Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + image : string -- (Optional) Path to image file (no extension needed; .png is tried first, then .bmp) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Gap between title and image + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + valuepadding : number -- (Optional) Padding for image (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for image + valuepaddingright : number -- (Optional) Right padding for image + valuepaddingtop : number -- (Optional) Top padding for image + valuepaddingbottom : number -- (Optional) Bottom padding for image + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) + imagewidth : number -- (Optional) Image width (px) + imageheight : number -- (Optional) Image height (px) + imagealign : string -- (Optional) Image alignment ("center", "left", "right", "top", "bottom") +``` +--- + +### Model Image Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Gap between title and image + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + font : font -- (Unused, for consistency) + valuealign : string -- (Unused, for consistency) + textcolor : color -- (Unused, for consistency) + valuepadding : number -- (Optional) Padding for image (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for image + valuepaddingright : number -- (Optional) Right padding for image + valuepaddingtop : number -- (Optional) Top padding for image + valuepaddingbottom : number -- (Optional) Bottom padding for image + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) + image : string -- (Auto) Image path, auto-resolved from model name or ID + imagewidth : number -- (Optional) Image width (px) + imageheight : number -- (Optional) Image height (px) + imagealign : string -- (Optional) Image alignment ("center", "left", "right", "top", "bottom") +``` +--- + +### PID/Rates Profile Display Object + +``` +Profile Source Selection + object : string -- Required: must be "pid" or "rates"; maps to telemetry source "pid_profile" or "rate_profile" + profilecount : number -- (Optional) How many profile numbers to draw (1 to 6, default 6) +Telemetry and Value Handling + value : number -- (Optional) Static fallback value if telemetry is unavailable + transform : string|function|number -- (Optional) Value transform logic (e.g., "floor", multiplier, or custom function) + decimals : number -- (Optional) Decimal precision for transformed value + thresholds : table -- (Optional) Value threshold list: { value=..., textcolor=... } + novalue : string -- (Optional) Fallback text if no telemetry or static value is available + unit : string -- (Optional) Placeholder only; not used in this object +Value Styling and Alignment + font : font -- (Optional) Font for profile number text + textcolor : color -- (Optional) Text color for inactive profile / rates + fillcolor : color -- (Optional) Text color for active profile / rates + valuealign : string -- (Optional) Ignored; profile numbers are always centered + valuepadding : number -- (Optional) General padding around value area (overridden by sides) + valuepaddingleft : number + valuepaddingright : number + valuepaddingtop : number + valuepaddingbottom : number +Title Styling + title : string -- (Optional) Title label (e.g., "Active Profile") + titlepos : string -- (Optional) "top" or "bottom" + titlealign : string -- (Optional) Title alignment: "center", "left", or "right" + titlefont : font -- (Optional) Title font (e.g., FONT_L) + titlespacing : number -- (Optional) Gap between title and profile number row + titlecolor : color -- (Optional) Title text color + titlepadding : number -- (Optional) General padding around title (overridden by sides) + titlepaddingleft : number + titlepaddingright : number + titlepaddingtop : number + titlepaddingbottom : number +Row Layout and Font Options + rowalign : string -- (Optional) Alignment for number row: "left", "center", or "right" + rowspacing : number -- (Optional) Spacing between profile numbers (default: width / profilecount) + rowfont : font -- (Optional) Font for profile numbers (fallbacks to `font`) + rowpadding : number -- (Optional) General padding for number row (overridden by sides) + rowpaddingleft : number + rowpaddingright : number + rowpaddingtop : number + rowpaddingbottom : number + highlightlarger : boolean -- (Optional) If true, enlarges the active index using the next font in the list +Background + bgcolor : color -- (Optional) Widget background color +``` + +--- + +### Rainbow Gauge Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +title parameters + title : string -- (Optional) Title text + titlealign : string -- (Optional) "center", "left", "right" + titlefont : font -- (Optional) Title font (e.g., font_l, font_xl) + titlespacing : number -- (Optional) Gap below title + titlecolor : color -- (Optional) Title text color + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) + titlepaddingright : number -- (Optional) + titlepaddingtop : number -- (Optional) + titlepaddingbottom : number -- (Optional) +value / source parameters + value : any -- (Optional) Static value to display if telemetry is not present + showvalue : bool -- (Optional) If false, hides the main value text (default true) + source : string -- Telemetry sensor name + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", etc.) + decimals : number -- (Optional) Decimal precision + novalue : string -- (Optional) Text if telemetry is missing (default: "-") + unit : string -- (Optional) Unit label ("" hides unit) + font : font -- (Optional) Value font (e.g., font_l) + valuealign : string -- (Optional) "center", "left", "right" + textcolor : color -- (Optional) Text color + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) + valuepaddingright : number -- (Optional) + valuepaddingtop : number -- (Optional) + valuepaddingbottom : number -- (Optional) +arc band parameters + bandlabels : table -- List of labels for each band (e.g. {"Low", "Med", "High"}) + bandcolors : table -- List of band colors (e.g. {lcd.RGB(180,50,50), lcd.RGB(...)}) + bandlabeloffset : number -- (Optional) Outward for left/right labels (default 18) + bandlabeloffsettop : number -- (Optional) Down from the arc edge for the top label (default 8) + bandlabelfont : font -- (Optional) Font for band labels (e.g. FONT_XS, FONT_S). Defaults to FONT_XS +appearance / theming + bgcolor : color -- (Optional) Widget background color + fillbgcolor : color -- (Optional) Arc background color (optional) + titlecolor : color -- (Optional) Title text color fallback +needle styling + accentcolor : color -- (Optional) Needle and hub color + needlethickness : number -- (Optional) Needle width (default: 5) + needlehubsize : number -- (Optional) Needle hub circle radius (default: 7) +``` + +--- + +### Rainbow Gauge Widget + +``` +Timing + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +Title parameters + title : string -- (Optional) Title text + titlepos : string -- (Optional) If `title` is present but `titlepos` is not set, title is placed at the top by default + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) + titlespacing : number -- (Optional) Vertical gap between title and value + titlecolor : color -- (Optional) Title text color (theme/text fallback) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title +Value/Source parameters + value : any -- (Optional) Static value to display if telemetry is not present + source : string -- Telemetry sensor source name (e.g., "temp_esc") + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value +Appearance/Theming + bgcolor : color -- (Optional) Widget background color (theme fallback) + fillbgcolor : color -- (Optional) Ring background color (theme fallback) + fillcolor : color -- (Optional) Ring foreground color (theme fallback) +Geometry + thickness : number -- (Optional) Ring thickness in pixels (default is proportional to radius) +Battery Ring Mode (Optional fuel-based battery style) + ringbatt : bool -- If true, draws 360° fill ring based on fuel (%) and shows mAh consumption + ringbattsubfont : font -- (Optional) Font for subtext in ringbatt mode (e.g., FONT_XS, FONT_S, FONT_M; default: FONT_XS) + innerringcolor : color -- Color of the inner decorative ring in ringbatt mode (default: white) + ringbattsubtext : string|bool -- (Optional) Overrides subtext below value in ringbatt mode (set "" or false to hide) + innerringthickness : number -- (Optional) Thickness of inner decorative ring in ringbatt mode (default: 8) + ringbattsubalign : string -- (Optional) "left", "center", or "right" alignment of subtext (default: center under value) + ringbattsubpadding : number -- (Optional) General padding (px) for subtext (applies if per-side not set) + ringbattsubpaddingleft : number -- (Optional) Left padding override for subtext + ringbattsubpaddingright : number -- (Optional) Right padding override for subtext + ringbattsubpaddingtop : number -- (Optional) Top padding override for subtext + ringbattsubpaddingbottom : number -- (Optional) Bottom padding override for subtext +``` + +--- + +### Session Value Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + source : string -- Session variable to display + unit : string -- (Optional) Unit label to append to value + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Stats Display Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) +Title & Layout + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) + titlespacing : number -- (Optional) Vertical gap between title and value + titlecolor : color -- (Optional) Title text color (theme fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title +Stat Source & Value + source : string -- (Required for stat mode) Telemetry sensor name used to fetch stats (e.g., "rpm", "current") + stattype : string -- (Optional) Which stat to show ("max", "min", "avg", etc; default: "max") + value : any -- (Optional, advanced) Static value. If omitted, widget shows the selected stat for 'source' +Value Display + unit : string -- (Optional) Dynamic localized unit displayed by default, you can use override this or "" to hide unit + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value +General + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + + Notes: +The widget only displays stat values (not live telemetry). "source" and "stattype" select which telemetry stat to show. +"unit" always overrides; if not set, unit is resolved from telemetry.sensorTable[source] if available. +To display min stats, set stattype = "min"; for max, omit or set stattype = "max". +``` +--- + +### Step Bar Widget + +``` + wakeupinterval : number -- (Optional) Wakeup interval in seconds for the widget (set in wrapper) +Title parameters + title : string -- (Optional) Title text (e.g., "2.4G", "Lora") + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Vertical gap between title and bar/value + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Title padding (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title +Value/telemetry parameters + value : number -- (Optional) Static value to display if no telemetry + hidevalue : bool -- (Optional) If true, value/unit will NOT be displayed (default: false) + source : string -- (Optional) Telemetry sensor source name (e.g., "rssi", "voltage", "current") + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Value padding (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value +Step bar parameters + stepcount : number -- (Optional) Number of steps/bars to draw (default: 4) + stepgap : number -- (Optional) Pixel gap between each step/bar (default: 1) + fillcolor : color -- (Optional) Color for active steps (theme fallback, or resolved by thresholds) + fillbgcolor : color -- (Optional) Color for inactive steps (theme fallback) + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +Bar padding parameters + barpadding : number -- (Optional) Bar padding (all sides unless overridden) + barpaddingleft : number -- (Optional) Left padding for bar + barpaddingright : number -- (Optional) Right padding for bar + barpaddingtop : number -- (Optional) Top padding for bar + barpaddingbottom : number -- (Optional) Bottom padding for bar +``` +--- + +### Telemetry Value Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default + titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if telemetry is not present + source : string -- Telemetry sensor source name (e.g., "voltage", "current") + transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) + decimals : number -- (Optional) Number of decimal places for numeric display + thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} + novalue : string -- (Optional) Text shown if value is missing (default: "-") + unit : string -- (Optional) Unit label to append to value or configure as "" to omit the unit from being displayed. If not specified, the widget attempts to resolve a dynamic unit + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- + +### Text Display Widget (Static/Label) + + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text displayed above or below the value + titlepos : string -- (Optional) Title position: "top" or "bottom" + titlealign : string -- (Optional) Title alignment: "center", "left", or "right" + titlefont : font -- (Optional) Font for title (e.g., FONT_L, FONT_XL). Uses theme or default if unset. + titlespacing : number -- (Optional) Vertical gap between title and value (pixels) + titlecolor : color -- (Optional) Title text color (theme fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : string|number -- (Optional) **Static** value to display (required for this widget) + font : font -- (Optional) Font for value (e.g., FONT_L, FONT_XL). Uses theme or default if unset. + valuealign : string -- (Optional) Value alignment: "center", "left", or "right" + textcolor : color -- (Optional) Value text color (theme fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) + novalue : string -- (Optional) Text to show if value is nil (default: "-") +Note: +This widget is for **static or label text only**. It does not support live telemetry or stats. +If you need dynamic stats or telemetry (min/max/live), use `stats.lua` or other appropriate widgets. +``` +--- + +### Total Flight Time Widget + +Total Flight Time Widget + +``` + wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) + title : string -- (Optional) Title text + titlepos : string -- (Optional) Title position ("top" or "bottom") + titlealign : string -- (Optional) Title alignment ("center", "left", "right") + titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) + titlespacing : number -- (Optional) Controls the vertical gap between title text and value text + titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) + titlepadding : number -- (Optional) Padding for title (all sides unless overridden) + titlepaddingleft : number -- (Optional) Left padding for title + titlepaddingright : number -- (Optional) Right padding for title + titlepaddingtop : number -- (Optional) Top padding for title + titlepaddingbottom : number -- (Optional) Bottom padding for title + value : any -- (Optional) Static value to display if telemetry is not present + unit : string -- (Optional) Unit label to append to value ("" to omit) + font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) + valuealign : string -- (Optional) Value alignment ("center", "left", "right") + textcolor : color -- (Optional) Value text color (theme/text fallback if nil) + valuepadding : number -- (Optional) Padding for value (all sides unless overridden) + valuepaddingleft : number -- (Optional) Left padding for value + valuepaddingright : number -- (Optional) Right padding for value + valuepaddingtop : number -- (Optional) Top padding for value + valuepaddingbottom : number -- (Optional) Bottom padding for value + bgcolor : color -- (Optional) Widget background color (theme fallback if nil) +``` +--- diff --git a/scripts/dashx/app/app.lua b/scripts/dashx/app/app.lua index 507b610..02a5b14 100644 --- a/scripts/dashx/app/app.lua +++ b/scripts/dashx/app/app.lua @@ -1,24 +1,10 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local app = {} app.initialized = false @@ -31,10 +17,6 @@ local arg = {...} local config = arg[1] --- Function to invalidate the pages variable. --- Typically called after writing MSP data. --- Resets the app.Page to nil, sets app.pageState to app.pageStatus.display, --- and initializes app.saveTS to 0. local function invalidatePages() app.Page = nil app.pageState = app.pageStatus.display @@ -42,16 +24,6 @@ local function invalidatePages() collectgarbage() end - ---[[ - Updates the current telemetry state. This function is called frequently to check the telemetry status. - - - If the system is not in simulation mode: - - Sets telemetry state to `noSensor` if the RSSI sensor is not available. - - Sets telemetry state to `noTelemetry` if the RSSI value is 0. - - Sets telemetry state to `ok` if the RSSI value is valid. - - If the system is in simulation mode, sets telemetry state to `ok`. -]] function app.updateTelemetryState() if system:getVersion().simulation ~= true then @@ -66,285 +38,230 @@ function app.updateTelemetryState() app.triggers.telemetryState = app.telemetryStatus.noTelemetry end - end --- Function: app.paint --- Description: Calls the paint method of the current page if it exists. --- Note: This function is triggered by lcd.refresh and is not a wakeup function. -function app.paint() - if app.Page and app.Page.paint then - app.Page.paint(app.Page) - end -end +function app.paint() if app.Page and app.Page.paint then app.Page.paint(app.Page) end end ---[[ -app._uiTasks - -A table containing a sequence of functions, each representing a logical UI update or task for the dashx Ethos Suite application. These tasks are executed in order to manage UI state, handle dialogs, process telemetry, and respond to user or system triggers. - -Each function in the table is responsible for a specific aspect of the application's UI logic, including but not limited to: - -1. Exiting the application and cleaning up resources. -2. Managing the progress loader dialog and its closure. -3. Handling the save loader dialog and its closure. -4. Simulating save progress in a simulator environment. -5. Detecting profile or rate changes and triggering UI reloads as needed. -6. Enabling or disabling main menu icons based on connection state and API version. -7. Displaying a "no-link" dialog when telemetry is lost or not established. -8. Updating the "no-link" progress and message based on connection diagnostics. -9. Monitoring save operation timeouts and handling failures. -10. Monitoring progress operation timeouts and handling failures. -11. Triggering save dialogs and handling user confirmation. -12. Triggering reload dialogs and handling user confirmation. -13. Displaying saving progress and managing save state transitions. -14. Warning the user if attempting to save while the system is armed. -15. Updating telemetry state and page readiness. -16. Triggering page retrieval when required. -17. Performing reload actions for the current or full page. -18. Playing pending audio alerts for various events. -19. Invoking page-specific wakeup functions if defined. - -Each task function typically checks relevant triggers or state variables before performing its logic, ensuring that UI updates occur only when necessary. This modular approach allows for clear separation of concerns and easier maintenance of the UI update logic. -]] app._uiTasks = { - -- 1. Exit App - function() - if app.triggers.exitAPP then - app.triggers.exitAPP = false - form.invalidate() - system.exit() - utils.reportMemoryUsage("Exit App") - end - end, - - -- 4. No-Link Initial Trigger - function() - if app.triggers.telemetryState == 1 or app.triggers.disableRssiTimeout then return end - if not app.dialogs.nolinkDisplay and not app.triggers.wasConnected then - if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end - if app.dialogs.saveDisplay then app.ui.progressDisplaySaveClose() end - if app.ui then app.ui.progressNolinkDisplay() end - app.dialogs.nolinkDisplay = true - end - end, - - -- 5. Save Timeout Watchdog - function() - if not app.dialogs.saveDisplay or not app.dialogs.saveWatchDog then return end - local timeout = tonumber(5) - if (os.clock() - app.dialogs.saveWatchDog) > timeout - or (app.dialogs.saveProgressCounter > 120 ) then - app.audio.playTimeout = true - app.ui.progressDisplaySaveMessage("@i18n(app.error_timed_out)@") - app.ui.progressDisplaySaveCloseAllowed(true) - app.dialogs.save:value(100) - app.dialogs.saveProgressCounter = 0 - app.dialogs.saveDisplay = false - app.triggers.isSaving = false - app.Page = app.PageTmp - app.PageTmp = nil - end - end, - - -- 6. Progress Timeout Watchdog - function() - if not app.dialogs.progressDisplay or not app.dialogs.progressWatchDog then return end - app.dialogs.progressCounter = app.dialogs.progressCounter + (app.Page and app.Page.progressCounter or 1.5) - app.ui.progressDisplayValue(app.dialogs.progressCounter) - if (os.clock() - app.dialogs.progressWatchDog) > 5 then - app.audio.playTimeout = true - app.ui.progressDisplayMessage("@i18n(app.error_timed_out)@") - app.ui.progressDisplayCloseAllowed(true) - app.Page = app.PageTmp - app.PageTmp = nil - app.dialogs.progressCounter = 0 - app.dialogs.progressDisplay = false - end - end, - - -- 7. Trigger Save Dialogs - function() - if app.triggers.triggerSave then - app.triggers.triggerSave = false - form.openDialog({ - width = nil, - title = "@i18n(app.msg_save_settings)@", - message = (app.Page.extraMsgOnSave and - "@i18n(app.msg_save_current_page)@".."\n\n"..app.Page.extraMsgOnSave or - "@i18n(app.msg_save_current_page)@"), - buttons = {{ label="@i18n(app.btn_ok)@", action=function() - app.PageTmp = app.Page - - app.triggers.isSaving = true - saveSettings() - return true - end },{ label="@i18n(app.btn_cancel)@",action=function() return true end }}, - wakeup = function() end, - paint = function() end, - options= TEXT_LEFT - }) - elseif app.triggers.triggerSaveNoProgress then - app.triggers.triggerSaveNoProgress = false - app.PageTmp = app.Page - saveSettings() - end - end, - - -- 8. Trigger Reload Dialogs - function() - if app.triggers.triggerReloadNoPrompt then - app.triggers.triggerReloadNoPrompt = false - app.triggers.reload = true - return - end - if app.triggers.triggerReload then - app.triggers.triggerReload = false - form.openDialog({ - title = "@i18n(reload)@", - message = "@i18n(app.msg_reload_settings)@", - buttons = {{ label="@i18n(app.btn_ok)@", action=function() app.triggers.reload = true; return true end }, - { label="@i18n(app.btn_cancel)@", action=function() return true end }}, - options = TEXT_LEFT - }) - elseif app.triggers.triggerReloadFull then - app.triggers.triggerReloadFull = false - form.openDialog({ - title = "@i18n(reload)@", - message = "@i18n(app.msg_reload_settings)@", - buttons = {{ label="@i18n(app.btn_ok)@", action=function() app.triggers.reloadFull = true; return true end }, - { label="@i18n(app.btn_cancel)@", action=function() return true end }}, - options = TEXT_LEFT - }) - end - end, - - -- 9. Saving Progress Display - function() - if app.triggers.isSaving then - app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 10 - if app.pageState >= app.pageStatus.saving then - if not app.dialogs.saveDisplay then - app.triggers.saveFailed = false - app.dialogs.saveProgressCounter = 0 - app.ui.progressDisplaySave() - dashx.tasks.msp.mspQueue.retryCount = 0 + function() + if app.triggers.exitAPP then + app.triggers.exitAPP = false + form.invalidate() + system.exit() + utils.reportMemoryUsage("Exit App") end - - app.ui.progressDisplaySaveValue(app.dialogs.saveProgressCounter, "@i18n(app.pageStatus.saving)") - else - app.triggers.isSaving = false - app.dialogs.saveDisplay = false - app.dialogs.saveWatchDog = nil - end - elseif app.triggers.isSavingFake then - app.triggers.isSavingFake = false - app.triggers.closeSaveFake = true - end - end, - - - -- 11. Telemetry & Page State Updates - function() - app.updateTelemetryState() - if app.uiState == app.uiStatus.mainMenu then - invalidatePages() - elseif app.triggers.isReady and dashx.tasks.msp.mspQueue:isProcessed() - and app.Page and app.Page.values then - app.triggers.isReady = false - app.triggers.closeProgressLoader = true - end - end, - - -- 12. Page Retrieval Trigger - function() - if app.uiState == app.uiStatus.pages then - if not app.Page and app.PageTmp then app.Page = app.PageTmp end - if app.Page and app.Page.apidata and app.pageState == app.pageStatus.display - and not app.triggers.isReady then - requestPage() - end - end - end, - - -- 13. Perform Reload Actions - function() - if app.triggers.reload then - app.triggers.reload = false - app.ui.progressDisplay() - app.ui.openPageRefresh(app.lastIdx, app.lastTitle, app.lastScript) - end - if app.triggers.reloadFull then - app.triggers.reloadFull = false - app.ui.progressDisplay() - app.ui.openPage(app.lastIdx, app.lastTitle, app.lastScript) - end - end, - - -- 14. Play Pending Audio Alerts - function() - local a = app.audio - if a.playEraseFlash then utils.playFile("app","eraseflash.wav"); a.playEraseFlash = false end - if a.playTimeout then utils.playFile("app","timeout.wav"); a.playTimeout = false end - if a.playEscPowerCycle then utils.playFile("app","powercycleesc.wav"); a.playEscPowerCycle = false end - if a.playServoOverideEnable then utils.playFile("app","soverideen.wav"); a.playServoOverideEnable = false end - if a.playServoOverideDisable then utils.playFile("app","soveridedis.wav"); a.playServoOverideDisable = false end - if a.playMixerOverideEnable then utils.playFile("app","moverideen.wav"); a.playMixerOverideEnable = false end - if a.playMixerOverideDisable then utils.playFile("app","moveridedis.wav"); a.playMixerOverideDisable = false end - if a.playSaveArmed then utils.playFileCommon("warn.wav"); a.playSaveArmed = false end - if a.playBufferWarn then utils.playFileCommon("warn.wav"); a.playBufferWarn = false end - end, - - -- 15. Wakeup UI Tasks - function() - if app.Page and app.uiState == app.uiStatus.pages and app.Page.wakeup then - -- run the pages wakeup function if it exists - app.Page.wakeup(app.Page) + end, function() + if app.triggers.telemetryState == 1 or app.triggers.disableRssiTimeout then return end + if not app.dialogs.nolinkDisplay and not app.triggers.wasConnected then + if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end + if app.dialogs.saveDisplay then app.ui.progressDisplaySaveClose() end + if app.ui then app.ui.progressNolinkDisplay() end + app.dialogs.nolinkDisplay = true + end + end, function() + if not app.dialogs.saveDisplay or not app.dialogs.saveWatchDog then return end + local timeout = tonumber(5) + if (os.clock() - app.dialogs.saveWatchDog) > timeout or (app.dialogs.saveProgressCounter > 120) then + app.audio.playTimeout = true + app.ui.progressDisplaySaveMessage("@i18n(app.error_timed_out)@") + app.ui.progressDisplaySaveCloseAllowed(true) + app.dialogs.save:value(100) + app.dialogs.saveProgressCounter = 0 + app.dialogs.saveDisplay = false + app.triggers.isSaving = false + app.Page = app.PageTmp + app.PageTmp = nil + end + end, function() + if not app.dialogs.progressDisplay or not app.dialogs.progressWatchDog then return end + app.dialogs.progressCounter = app.dialogs.progressCounter + (app.Page and app.Page.progressCounter or 1.5) + app.ui.progressDisplayValue(app.dialogs.progressCounter) + if (os.clock() - app.dialogs.progressWatchDog) > 5 then + app.audio.playTimeout = true + app.ui.progressDisplayMessage("@i18n(app.error_timed_out)@") + app.ui.progressDisplayCloseAllowed(true) + app.Page = app.PageTmp + app.PageTmp = nil + app.dialogs.progressCounter = 0 + app.dialogs.progressDisplay = false + end + end, function() + if app.triggers.triggerSave then + app.triggers.triggerSave = false + form.openDialog({ + width = nil, + title = "@i18n(app.msg_save_settings)@", + message = (app.Page.extraMsgOnSave and "@i18n(app.msg_save_current_page)@" .. "\n\n" .. app.Page.extraMsgOnSave or "@i18n(app.msg_save_current_page)@"), + buttons = { + { + label = "@i18n(app.btn_ok)@", + action = function() + app.PageTmp = app.Page + + app.triggers.isSaving = true + saveSettings() + return true + end + }, {label = "@i18n(app.btn_cancel)@", action = function() return true end} + }, + wakeup = function() end, + paint = function() end, + options = TEXT_LEFT + }) + elseif app.triggers.triggerSaveNoProgress then + app.triggers.triggerSaveNoProgress = false + app.PageTmp = app.Page + saveSettings() + end + end, function() + if app.triggers.triggerReloadNoPrompt then + app.triggers.triggerReloadNoPrompt = false + app.triggers.reload = true + return + end + if app.triggers.triggerReload then + app.triggers.triggerReload = false + form.openDialog({ + title = "@i18n(reload)@", + message = "@i18n(app.msg_reload_settings)@", + buttons = { + { + label = "@i18n(app.btn_ok)@", + action = function() + app.triggers.reload = true; + return true + end + }, {label = "@i18n(app.btn_cancel)@", action = function() return true end} + }, + options = TEXT_LEFT + }) + elseif app.triggers.triggerReloadFull then + app.triggers.triggerReloadFull = false + form.openDialog({ + title = "@i18n(reload)@", + message = "@i18n(app.msg_reload_settings)@", + buttons = { + { + label = "@i18n(app.btn_ok)@", + action = function() + app.triggers.reloadFull = true; + return true + end + }, {label = "@i18n(app.btn_cancel)@", action = function() return true end} + }, + options = TEXT_LEFT + }) end - end, + end, function() + if app.triggers.isSaving then + app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 10 + if app.pageState >= app.pageStatus.saving then + if not app.dialogs.saveDisplay then + app.triggers.saveFailed = false + app.dialogs.saveProgressCounter = 0 + app.ui.progressDisplaySave() + dashx.tasks.msp.mspQueue.retryCount = 0 + end + + app.ui.progressDisplaySaveValue(app.dialogs.saveProgressCounter, "@i18n(app.pageStatus.saving)") + else + app.triggers.isSaving = false + app.dialogs.saveDisplay = false + app.dialogs.saveWatchDog = nil + end + elseif app.triggers.isSavingFake then + app.triggers.isSavingFake = false + app.triggers.closeSaveFake = true + end + end, function() + app.updateTelemetryState() + if app.uiState == app.uiStatus.mainMenu then + invalidatePages() + elseif app.triggers.isReady and dashx.tasks.msp.mspQueue:isProcessed() and app.Page and app.Page.values then + app.triggers.isReady = false + app.triggers.closeProgressLoader = true + end + end, function() + if app.uiState == app.uiStatus.pages then + if not app.Page and app.PageTmp then app.Page = app.PageTmp end + if app.Page and app.Page.apidata and app.pageState == app.pageStatus.display and not app.triggers.isReady then requestPage() end + end + end, function() + if app.triggers.reload then + app.triggers.reload = false + app.ui.progressDisplay() + app.ui.openPageRefresh(app.lastIdx, app.lastTitle, app.lastScript) + end + if app.triggers.reloadFull then + app.triggers.reloadFull = false + app.ui.progressDisplay() + app.ui.openPage(app.lastIdx, app.lastTitle, app.lastScript) + end + end, function() + local a = app.audio + if a.playEraseFlash then + utils.playFile("app", "eraseflash.wav"); + a.playEraseFlash = false + end + if a.playTimeout then + utils.playFile("app", "timeout.wav"); + a.playTimeout = false + end + if a.playEscPowerCycle then + utils.playFile("app", "powercycleesc.wav"); + a.playEscPowerCycle = false + end + if a.playServoOverideEnable then + utils.playFile("app", "soverideen.wav"); + a.playServoOverideEnable = false + end + if a.playServoOverideDisable then + utils.playFile("app", "soveridedis.wav"); + a.playServoOverideDisable = false + end + if a.playMixerOverideEnable then + utils.playFile("app", "moverideen.wav"); + a.playMixerOverideEnable = false + end + if a.playMixerOverideDisable then + utils.playFile("app", "moveridedis.wav"); + a.playMixerOverideDisable = false + end + if a.playSaveArmed then + utils.playFileCommon("warn.wav"); + a.playSaveArmed = false + end + if a.playBufferWarn then + utils.playFileCommon("warn.wav"); + a.playBufferWarn = false + end + end, function() if app.Page and app.uiState == app.uiStatus.pages and app.Page.wakeup then app.Page.wakeup(app.Page) end end } ---- Handles periodic execution of UI tasks in a round-robin fashion. --- This function is intended to be called regularly (e.g., on each system wakeup or tick). --- It distributes the execution of tasks in `app._uiTasks` based on the configured percentage (`app._uiTaskPercent`), --- ensuring that at least one task is executed per tick. The function uses an accumulator to handle fractional --- task execution rates and maintains the index of the next task to execute in `app._nextUiTask`. --- Tasks are executed in order, wrapping around to the beginning of the list as needed. -app._nextUiTask = 1 -- accumulator for fractional tasks per tick -app._taskAccumulator = 0 -- desired throughput percentage of total tasks per tick (0-100) -app._uiTaskPercent = 100 -- e.g., 100% of tasks each tick +app._nextUiTask = 1 +app._taskAccumulator = 0 +app._uiTaskPercent = 100 function app.wakeup() + local total = #app._uiTasks + local tasksThisTick = math.max(1, (total * app._uiTaskPercent) / 100) - local total = #app._uiTasks - local tasksThisTick = math.max(1, (total * app._uiTaskPercent) / 100) + app._taskAccumulator = app._taskAccumulator + tasksThisTick - app._taskAccumulator = app._taskAccumulator + tasksThisTick - - while app._taskAccumulator >= 1 do - local idx = app._nextUiTask - app._uiTasks[idx]() - app._nextUiTask = (idx % total) + 1 - app._taskAccumulator = app._taskAccumulator - 1 - end + while app._taskAccumulator >= 1 do + local idx = app._nextUiTask + app._uiTasks[idx]() + app._nextUiTask = (idx % total) + 1 + app._taskAccumulator = app._taskAccumulator - 1 + end end - ---[[ - Creates the log tool for the application. - This function initializes various configurations and settings for the log tool, - including disabling buffer warnings, setting the environment version, - determining the LCD dimensions, loading the radio configuration, - and setting the initial UI state. It also checks for developer mode, - updates the menu selection, displays the progress, and opens the logs page in offline mode. -]] function app.create_logtool() triggers.showUnderUsedBufferWarning = false triggers.showOverUsedBufferWarning = false - -- dashx.session.apiVersion = nil config.environment = system.getVersion() config.ethosRunningVersion = {config.environment.major, config.environment.minor, config.environment.revision} @@ -357,169 +274,133 @@ function app.create_logtool() dashx.app.ui.progressDisplay() dashx.app.offlineMode = true - dashx.app.ui.openPage(1, "Logs", "logs/logs.lua", 1) -- final param says to load in standalone mode + dashx.app.ui.openPage(1, "Logs", "logs/logs.lua", 1) end ---[[ - Function: app.create - - Initializes the application by setting up the environment configuration, - determining the LCD dimensions, loading the radio configuration, - setting the initial UI state, and checking for developer mode. - - Steps: - 1. Sets the environment configuration using the system version. - 2. Retrieves and sets the LCD width and height. - 3. Loads the radio configuration from "app/radios.lua". - 4. Sets the initial UI state to 'init'. - 5. Checks for the existence of a developer mode file and enables developer mode if found. - 6. Opens the main menu UI. -]] function app.create() if not app.initialized then - -- dashx.session.apiVersion = nil - config.environment = system.getVersion() - config.ethosRunningVersion = {config.environment.major, config.environment.minor, config.environment.revision} - - dashx.session.lcdWidth, dashx.session.lcdHeight = utils.getWindowSize() - - app.triggers = {} - app.triggers.exitAPP = false - app.triggers.noRFMsg = false - app.triggers.triggerSave = false - app.triggers.triggerSaveNoProgress = false - app.triggers.triggerReload = false - app.triggers.triggerReloadFull = false - app.triggers.triggerReloadNoPrompt = false - app.triggers.reloadFull = false - app.triggers.isReady = false - app.triggers.isSaving = false - app.triggers.isSavingFake = false - app.triggers.saveFailed = false - app.triggers.telemetryState = nil - app.triggers.profileswitchLast = nil - app.triggers.rateswitchLast = nil - app.triggers.closeSave = false - app.triggers.closeSaveFake = false - app.triggers.badMspVersion = false - app.triggers.badMspVersionDisplay = false - app.triggers.closeProgressLoader = false - app.triggers.mspBusy = false - app.triggers.disableRssiTimeout = false - app.triggers.timeIsSet = false - app.triggers.invalidConnectionSetup = false - app.triggers.wasConnected = false - app.triggers.isArmed = false - app.triggers.showSaveArmedWarning = false - - app.sensors = {} - app.formFields = {} - app.formNavigationFields = {} - app.PageTmp = {} - app.Page = {} - app.saveTS = 0 - app.lastPage = nil - app.lastSection = nil - app.lastIdx = nil - app.lastTitle = nil - app.lastScript = nil - app.gfx_buttons = {} - app.uiStatus = {init = 1, mainMenu = 2, pages = 3, confirm = 4} - app.pageStatus = {display = 1, editing = 2, saving = 3, eepromWrite = 4, rebooting = 5} - app.telemetryStatus = {ok = 1, noSensor = 2, noTelemetry = 3} - app.uiState = app.uiStatus.init - app.pageState = app.pageStatus.display - app.lastLabel = nil - app.NewRateTable = nil - app.RateTable = nil - app.fieldHelpTxt = nil - app.radio = {} - app.sensor = {} - app.init = nil - app.guiIsRunning = false - app.adjfunctions = nil - app.profileCheckScheduler = os.clock() - app.offlineMode = false - - - app.audio = {} - app.audio.playTimeout = false - app.audio.playEscPowerCycle = false - app.audio.playServoOverideDisable = false - app.audio.playServoOverideEnable = false - app.audio.playMixerOverideDisable = false - app.audio.playMixerOverideEnable = false - app.audio.playEraseFlash = false - - app.dialogs = {} - app.dialogs.progress = false - app.dialogs.progressDisplay = false - app.dialogs.progressWatchDog = nil - app.dialogs.progressCounter = 0 - app.dialogs.progressRateLimit = os.clock() - app.dialogs.progressRate = 0.25 - - - app.dialogs.progressESC = false - app.dialogs.progressDisplayEsc = false - app.dialogs.progressWatchDogESC = nil - app.dialogs.progressCounterESC = 0 - app.dialogs.progressESCRateLimit = os.clock() - app.dialogs.progressESCRate = 2.5 - - app.dialogs.save = false - app.dialogs.saveDisplay = false - app.dialogs.saveWatchDog = nil - app.dialogs.saveProgressCounter = 0 - app.dialogs.saveRateLimit = os.clock() - app.dialogs.saveRate = 0.25 - - app.dialogs.nolink = false - app.dialogs.nolinkDisplay = false - app.dialogs.nolinkValueCounter = 0 - app.dialogs.nolinkRateLimit = os.clock() - app.dialogs.nolinkRate = 0.25 - - - app.dialogs.badversion = false - app.dialogs.badversionDisplay = false - - app.radio = assert(compile("app/radios.lua"))() - - app.MainMenu = assert(compile("app/modules/init.lua"))() - - app.ui = assert(compile("app/lib/ui.lua"))(config) - - app.utils = assert(compile("app/lib/utils.lua"))(config) - - app.initialized = true - end + config.environment = system.getVersion() + config.ethosRunningVersion = {config.environment.major, config.environment.minor, config.environment.revision} + + dashx.session.lcdWidth, dashx.session.lcdHeight = utils.getWindowSize() + + app.triggers = {} + app.triggers.exitAPP = false + app.triggers.noRFMsg = false + app.triggers.triggerSave = false + app.triggers.triggerSaveNoProgress = false + app.triggers.triggerReload = false + app.triggers.triggerReloadFull = false + app.triggers.triggerReloadNoPrompt = false + app.triggers.reloadFull = false + app.triggers.isReady = false + app.triggers.isSaving = false + app.triggers.isSavingFake = false + app.triggers.saveFailed = false + app.triggers.telemetryState = nil + app.triggers.profileswitchLast = nil + app.triggers.rateswitchLast = nil + app.triggers.closeSave = false + app.triggers.closeSaveFake = false + app.triggers.badMspVersion = false + app.triggers.badMspVersionDisplay = false + app.triggers.closeProgressLoader = false + app.triggers.mspBusy = false + app.triggers.disableRssiTimeout = false + app.triggers.timeIsSet = false + app.triggers.invalidConnectionSetup = false + app.triggers.wasConnected = false + app.triggers.isArmed = false + app.triggers.showSaveArmedWarning = false + + app.sensors = {} + app.formFields = {} + app.formNavigationFields = {} + app.PageTmp = {} + app.Page = {} + app.saveTS = 0 + app.lastPage = nil + app.lastSection = nil + app.lastIdx = nil + app.lastTitle = nil + app.lastScript = nil + app.gfx_buttons = {} + app.uiStatus = {init = 1, mainMenu = 2, pages = 3, confirm = 4} + app.pageStatus = {display = 1, editing = 2, saving = 3, eepromWrite = 4, rebooting = 5} + app.telemetryStatus = {ok = 1, noSensor = 2, noTelemetry = 3} + app.uiState = app.uiStatus.init + app.pageState = app.pageStatus.display + app.lastLabel = nil + app.NewRateTable = nil + app.RateTable = nil + app.fieldHelpTxt = nil + app.radio = {} + app.sensor = {} + app.init = nil + app.guiIsRunning = false + app.adjfunctions = nil + app.profileCheckScheduler = os.clock() + app.offlineMode = false + + app.audio = {} + app.audio.playTimeout = false + app.audio.playEscPowerCycle = false + app.audio.playServoOverideDisable = false + app.audio.playServoOverideEnable = false + app.audio.playMixerOverideDisable = false + app.audio.playMixerOverideEnable = false + app.audio.playEraseFlash = false + + app.dialogs = {} + app.dialogs.progress = false + app.dialogs.progressDisplay = false + app.dialogs.progressWatchDog = nil + app.dialogs.progressCounter = 0 + app.dialogs.progressRateLimit = os.clock() + app.dialogs.progressRate = 0.25 + + app.dialogs.progressESC = false + app.dialogs.progressDisplayEsc = false + app.dialogs.progressWatchDogESC = nil + app.dialogs.progressCounterESC = 0 + app.dialogs.progressESCRateLimit = os.clock() + app.dialogs.progressESCRate = 2.5 + + app.dialogs.save = false + app.dialogs.saveDisplay = false + app.dialogs.saveWatchDog = nil + app.dialogs.saveProgressCounter = 0 + app.dialogs.saveRateLimit = os.clock() + app.dialogs.saveRate = 0.25 + + app.dialogs.nolink = false + app.dialogs.nolinkDisplay = false + app.dialogs.nolinkValueCounter = 0 + app.dialogs.nolinkRateLimit = os.clock() + app.dialogs.nolinkRate = 0.25 + + app.dialogs.badversion = false + app.dialogs.badversionDisplay = false + + app.radio = assert(compile("app/radios.lua"))() + + app.MainMenu = assert(compile("app/modules/init.lua"))() + + app.ui = assert(compile("app/lib/ui.lua"))(config) + + app.utils = assert(compile("app/lib/utils.lua"))(config) + + app.initialized = true + end app.ui.openMainMenu() end ---[[ -Handles various events for the app, including key presses and page events. - -Parameters: -- widget: The widget triggering the event. -- category: The category of the event. -- value: The value associated with the event. -- x: The x-coordinate of the event. -- y: The y-coordinate of the event. - -Returns: -- 0 if a rapid exit is triggered. -- The return value of the page event handler if it handles the event. -- true if the event is handled by the generic event handler. -- false if the event is not handled. -]] function app.event(widget, category, value, x, y) - -- long press on return at any point will force an rapid exit if value == KEY_RTN_LONG then log("KEY_RTN_LONG", "info") invalidatePages() @@ -527,21 +408,16 @@ function app.event(widget, category, value, x, y) return 0 end - -- the page has its own event system. we should use it. if app.Page and (app.uiState == app.uiStatus.pages or app.uiState == app.uiStatus.mainMenu) then if app.Page.event then log("USING PAGES EVENTS", "debug") local ret = app.Page.event(widget, category, value, x, y) - if ret ~= nil then - return ret - end + if ret ~= nil then return ret end end end - -- generic events handler for most pages if app.uiState == app.uiStatus.pages then - -- close button (top menu) should go back to main menu if category == EVT_CLOSE and value == 0 or value == 35 then log("EVT_CLOSE", "info") if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end @@ -551,11 +427,8 @@ function app.event(widget, category, value, x, y) return true end - -- long press on enter should result in a save dialog box if value == KEY_ENTER_LONG then - if dashx.app.Page.navButtons and dashx.app.Page.navButtons.save == false then - return true - end + if dashx.app.Page.navButtons and dashx.app.Page.navButtons.save == false then return true end log("EVT_ENTER_LONG (PAGES)", "info") if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end if app.dialogs.saveDisplay then app.ui.progressDisplaySaveClose() end @@ -569,37 +442,21 @@ function app.event(widget, category, value, x, y) end end - -- catch all to stop lock press on main menu doing anything if app.uiState == app.uiStatus.mainMenu and value == KEY_ENTER_LONG then - log("EVT_ENTER_LONG (MAIN MENU)", "info") - if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end - if app.dialogs.saveDisplay then app.ui.progressDisplaySaveClose() end - system.killEvents(KEY_ENTER_BREAK) - return true + log("EVT_ENTER_LONG (MAIN MENU)", "info") + if app.dialogs.progressDisplay then app.ui.progressDisplayClose() end + if app.dialogs.saveDisplay then app.ui.progressDisplaySaveClose() end + system.killEvents(KEY_ENTER_BREAK) + return true end return false end ---[[ -Closes the application and performs necessary cleanup operations. - -This function sets the application state to indicate that the GUI is no longer running -and that the application is not in offline mode. It then checks if there is an active -page and if the current UI state is either in pages or main menu, and if so, it calls -the close method of the active page. - -Additionally, it closes any open progress, save, or no-link dialogs. It then invalidates -the pages, resets the application state, and exits the system. - -Returns: - true: Always returns true to indicate successful closure. -]] function app.close() dashx.utils.reportMemoryUsage("closing application: start") - -- save user preferences local userpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini" dashx.ini.save_ini_file(userpref_file, dashx.preferences) @@ -607,22 +464,17 @@ function app.close() app.offlineMode = false app.uiState = app.uiStatus.init - if app.Page and (app.uiState == app.uiStatus.pages or app.uiState == app.uiStatus.mainMenu) and app.Page.close then - app.Page.close() - end + if app.Page and (app.uiState == app.uiStatus.pages or app.uiState == app.uiStatus.mainMenu) and app.Page.close then app.Page.close() end if app.ui then - if app.dialogs.progress then app.ui.progressDisplayClose() end - if app.dialogs.save then app.ui.progressDisplaySaveClose() end - if app.dialogs.noLink then app.ui.progressNolinkDisplayClose() end + if app.dialogs.progress then app.ui.progressDisplayClose() end + if app.dialogs.save then app.ui.progressDisplaySaveClose() end + if app.dialogs.noLink then app.ui.progressNolinkDisplayClose() end end - - -- Reset configuration and compiler flags config.useCompiler = true dashx.config.useCompiler = true - -- Reset page and navigation state app.Page = {} app.formFields = {} app.formNavigationFields = {} @@ -632,8 +484,6 @@ function app.close() app.PageTmp = nil app.moduleList = nil - - -- Reset triggers app.triggers.exitAPP = false app.triggers.noRFMsg = false app.triggers.telemetryState = nil @@ -641,16 +491,12 @@ function app.close() app.triggers.invalidConnectionSetup = false app.triggers.disableRssiTimeout = false - -- Reset dialogs app.dialogs.nolinkDisplay = false app.dialogs.nolinkValueCounter = 0 app.dialogs.progressDisplayEsc = false - -- Reset audio app.audio = {} - - -- Reset profile/rate state dashx.app.triggers.profileswitchLast = nil dashx.session.activeProfileLast = nil dashx.session.activeProfile = nil @@ -658,11 +504,10 @@ function app.close() dashx.session.activeRateProfileLast = nil dashx.session.activeRateTable = nil - -- Cleanup collectgarbage() invalidatePages() - dashx.utils.reportMemoryUsage("closing application: end") + dashx.utils.reportMemoryUsage("closing application: end") system.exit() return true diff --git a/scripts/dashx/app/lib/ui.lua b/scripts/dashx/app/lib/ui.lua index dd4fe97..237c965 100644 --- a/scripts/dashx/app/lib/ui.lua +++ b/scripts/dashx/app/lib/ui.lua @@ -1,33 +1,15 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local ui = {} local arg = {...} local config = arg[1] - --- Displays a progress dialog with a title and message. --- @param title The title of the progress dialog (optional, default is "Loading"). --- @param message The message of the progress dialog (optional, default is "Loading data from flight controller..."). function ui.progressDisplay(title, message) if dashx.app.dialogs.progressDisplay then return end @@ -36,29 +18,26 @@ function ui.progressDisplay(title, message) dashx.app.dialogs.progressDisplay = true dashx.app.dialogs.progressWatchDog = os.clock() - dashx.app.dialogs.progress = form.openProgressDialog( - { - title = title, - message = message, - close = function() - end, - wakeup = function() - local app = dashx.app - app.dialogs.progress:value(app.dialogs.progressCounter) - if not app.triggers.closeProgressLoader then - app.dialogs.progressCounter = app.dialogs.progressCounter + 10 - else - app.dialogs.progressCounter = app.dialogs.progressCounter + 20 - if app.dialogs.progressCounter >= 100 then - app.dialogs.progress:close() - app.dialogs.progressDisplay = false - app.dialogs.progressCounter = 0 - app.triggers.closeProgressLoader = false - end - end - end - } - ) + dashx.app.dialogs.progress = form.openProgressDialog({ + title = title, + message = message, + close = function() end, + wakeup = function() + local app = dashx.app + app.dialogs.progress:value(app.dialogs.progressCounter) + if not app.triggers.closeProgressLoader then + app.dialogs.progressCounter = app.dialogs.progressCounter + 10 + else + app.dialogs.progressCounter = app.dialogs.progressCounter + 20 + if app.dialogs.progressCounter >= 100 then + app.dialogs.progress:close() + app.dialogs.progressDisplay = false + app.dialogs.progressCounter = 0 + app.triggers.closeProgressLoader = false + end + end + end + }) dashx.app.dialogs.progressCounter = 0 @@ -69,124 +48,89 @@ function ui.progressDisplay(title, message) end end ---[[ - Function: ui.progressNolinkDisplay - Description: Displays a progress dialog indicating a connection attempt. - Sets the nolinkDisplay flag to true and opens a progress dialog with the title "Connecting" and message "Connecting...". - The dialog is configured to disallow closing and initializes the progress value to 0. -]] function ui.progressNolinkDisplay() dashx.app.dialogs.nolinkDisplay = true - --dashx.app.dialogs.noLink = form.openProgressDialog(, ) - - dashx.app.dialogs.noLink = form.openProgressDialog( - { - title = "@i18n(app.msg_connecting)@", - message = "@i18n(app.msg_connecting_to_fbl)@", - close = function() - end, - wakeup = function() - local app = dashx.app - local utils = dashx.utils - - local apiStr = tostring(dashx.session.apiVersion) - local moduleEnabled = model.getModule(0):enable() or model.getModule(1):enable() - local sensorSport = system.getSource({appId=0xF101}) - local sensorElrs = system.getSource({crsfId=0x14, subIdStart=0, subIdEnd=1}) - local curRssi = app.utils.getRSSI() - local invalid, abort = false, false - local msg = "@i18n(app.msg_connecting_to_fbl)@" - if not utils.ethosVersionAtLeast() then - msg = string.format("%s < V%d.%d.%d", string.upper("@i18n(ethos)@"), table.unpack(dashx.config.ethosVersion)) - elseif not dashx.tasks.active() then - msg, invalid, abort = "@i18n(app.check_bg_task)@", true, false - elseif not moduleEnabled and not app.offlineMode then - msg, invalid = "@i18n(app.check_rf_module_on)@", true - elseif not (sensorSport or sensorElrs) and not app.offlineMode then - msg, invalid = "@i18n(app.check_discovered_sensors)@", true - end - app.triggers.invalidConnectionSetup = invalid - local step = invalid and 10 or 15 - app.dialogs.nolinkValueCounter = app.dialogs.nolinkValueCounter + step - dashx.app.dialogs.noLink:value(app.dialogs.nolinkValueCounter) - dashx.app.dialogs.noLink:message(msg) - if invalid and app.dialogs.nolinkValueCounter == 15 then app.audio.playBufferWarn = true end - if app.dialogs.nolinkValueCounter >= 100 then - app.dialogs.nolinkDisplay = false - app.triggers.wasConnected = true - dashx.app.dialogs.noLink:close() - if abort then app.close() end - end - end - } - ) + dashx.app.dialogs.noLink = form.openProgressDialog({ + title = "@i18n(app.msg_connecting)@", + message = "@i18n(app.msg_connecting_to_fbl)@", + close = function() end, + wakeup = function() + local app = dashx.app + local utils = dashx.utils + + local apiStr = tostring(dashx.session.apiVersion) + local moduleEnabled = model.getModule(0):enable() or model.getModule(1):enable() + local sensorSport = system.getSource({appId = 0xF101}) + local sensorElrs = system.getSource({crsfId = 0x14, subIdStart = 0, subIdEnd = 1}) + local curRssi = app.utils.getRSSI() + local invalid, abort = false, false + local msg = "@i18n(app.msg_connecting_to_fbl)@" + if not utils.ethosVersionAtLeast() then + msg = string.format("%s < V%d.%d.%d", string.upper("@i18n(ethos)@"), table.unpack(dashx.config.ethosVersion)) + elseif not dashx.tasks.active() then + msg, invalid, abort = "@i18n(app.check_bg_task)@", true, false + elseif not moduleEnabled and not app.offlineMode then + msg, invalid = "@i18n(app.check_rf_module_on)@", true + elseif not (sensorSport or sensorElrs) and not app.offlineMode then + msg, invalid = "@i18n(app.check_discovered_sensors)@", true + end + app.triggers.invalidConnectionSetup = invalid + local step = invalid and 10 or 15 + app.dialogs.nolinkValueCounter = app.dialogs.nolinkValueCounter + step + dashx.app.dialogs.noLink:value(app.dialogs.nolinkValueCounter) + dashx.app.dialogs.noLink:message(msg) + if invalid and app.dialogs.nolinkValueCounter == 15 then app.audio.playBufferWarn = true end + if app.dialogs.nolinkValueCounter >= 100 then + app.dialogs.nolinkDisplay = false + app.triggers.wasConnected = true + dashx.app.dialogs.noLink:close() + if abort then app.close() end + end + end + }) dashx.app.dialogs.noLink:closeAllowed(false) dashx.app.dialogs.noLink:value(0) end - ---- Closes the "No Link" progress dialog if it is currently open. --- This function checks if the `noLink` dialog exists in `dashx.app.dialogs`. --- If it does, it calls the `close` method on the dialog to close it. function ui.progressNolinkDisplayClose() if not dashx.app.dialogs.noLink then return end dashx.app.dialogs.noLink:close() end ---[[ - Function: ui.progressDisplaySave - Description: Opens a progress dialog indicating that data is being saved. - Sets the save display flag, initializes the save watchdog timer, - and configures the progress dialog with initial values. -]] function ui.progressDisplaySave(message) dashx.app.dialogs.saveDisplay = true dashx.app.dialogs.saveWatchDog = os.clock() local title = "@i18n(app.msg_saving)@" - if not message then - message = "@i18n(app.msg_saving_to_fbl)@" - end - - dashx.app.dialogs.save = form.openProgressDialog( - { - title = title, - message = message, - close = function() - end, - wakeup = function() - local app = dashx.app - app.dialogs.save:value(app.dialogs.saveProgressCounter) - if not app.dialogs.saveProgressCounter then - app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 5 - else - app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 15 - if app.dialogs.saveProgressCounter >= 100 then - app.dialogs.save:close() - app.dialogs.saveDisplay = false - app.dialogs.saveProgressCounter = 0 - app.triggers.closeSave = false - app.triggers.isSaving = false - end - end - end - } - ) + if not message then message = "@i18n(app.msg_saving_to_fbl)@" end + + dashx.app.dialogs.save = form.openProgressDialog({ + title = title, + message = message, + close = function() end, + wakeup = function() + local app = dashx.app + app.dialogs.save:value(app.dialogs.saveProgressCounter) + if not app.dialogs.saveProgressCounter then + app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 5 + else + app.dialogs.saveProgressCounter = app.dialogs.saveProgressCounter + 15 + if app.dialogs.saveProgressCounter >= 100 then + app.dialogs.save:close() + app.dialogs.saveDisplay = false + app.dialogs.saveProgressCounter = 0 + app.triggers.closeSave = false + app.triggers.isSaving = false + end + end + end + }) dashx.app.dialogs.save:value(0) dashx.app.dialogs.save:closeAllowed(false) end - ---[[ - Updates the progress display with the given value and optional message. - - @param value (number) - The progress value to display. If the value is 100 or more, the progress is updated immediately. - @param message (string, optional) - An optional message to display along with the progress value. - - The function ensures that the progress display is updated at a rate limited by `dashx.app.dialogs.progressRate`. -]] function ui.progressDisplayValue(value, message) if value >= 100 then dashx.app.dialogs.progress:value(value) @@ -202,18 +146,9 @@ function ui.progressDisplayValue(value, message) end end - ---[[ - Updates the progress display with a given value and optional message. - - @param value number: The progress value to display. If the value is 100 or more, the display is updated immediately. - @param message string (optional): An optional message to display along with the progress value. -]] function ui.progressDisplaySaveValue(value, message) if value >= 100 then - if dashx.app.dialogs.save then - dashx.app.dialogs.save:value(value) - end + if dashx.app.dialogs.save then dashx.app.dialogs.save:value(value) end if message then dashx.app.dialogs.save:message(message) end return end @@ -221,16 +156,11 @@ function ui.progressDisplaySaveValue(value, message) local now = os.clock() if (now - dashx.app.dialogs.saveRateLimit) >= dashx.app.dialogs.saveRate then dashx.app.dialogs.saveRateLimit = now - if dashx.app.dialogs.save then - dashx.app.dialogs.save:value(value) - end + if dashx.app.dialogs.save then dashx.app.dialogs.save:value(value) end if message then dashx.app.dialogs.save:message(message) end end end --- Closes the progress display dialog if it is currently open. --- This function checks if the progress dialog exists, closes it, --- and updates the progress display status to false. function ui.progressDisplayClose() local progress = dashx.app.dialogs.progress if progress then @@ -239,166 +169,70 @@ function ui.progressDisplayClose() end end --- Closes the progress display if allowed by the given status. --- @param status A boolean indicating whether closing the progress display is allowed. function ui.progressDisplayCloseAllowed(status) local progress = dashx.app.dialogs.progress - if progress then - progress:closeAllowed(status) - end + if progress then progress:closeAllowed(status) end end --- Displays a progress message in the UI. --- @param message The message to be displayed in the progress dialog. function ui.progressDisplayMessage(message) local progress = dashx.app.dialogs.progress - if progress then - progress:message(message) - end + if progress then progress:message(message) end end --- Closes the save dialog if it is open and updates the save display status. --- This function checks if the save dialog exists, closes it if it does, --- and then sets the save display status to false. function ui.progressDisplaySaveClose() local saveDialog = dashx.app.dialogs.save if saveDialog then saveDialog:close() end dashx.app.dialogs.saveDisplay = false end ---- Displays a save message in the progress dialog. --- @param message The message to be displayed in the save dialog. function ui.progressDisplaySaveMessage(message) local saveDialog = dashx.app.dialogs.save if saveDialog then saveDialog:message(message) end end ---[[ - Function: ui.progressDisplaySaveCloseAllowed - - Description: - This function updates the closeAllowed status of the save dialog in the dashx application. - - Parameters: - status (boolean) - The status to set for allowing the save dialog to close. - - Usage: - ui.progressDisplaySaveCloseAllowed(true) -- Allows the save dialog to close. - ui.progressDisplaySaveCloseAllowed(false) -- Prevents the save dialog from closing. -]] function ui.progressDisplaySaveCloseAllowed(status) local saveDialog = dashx.app.dialogs.save if saveDialog then saveDialog:closeAllowed(status) end end --- Disables all form fields in the dashx application. --- Iterates through the formFields array and disables each field if it is of type "userdata". function ui.disableAllFields() - for i = 1, #dashx.app.formFields do + for i = 1, #dashx.app.formFields do local field = dashx.app.formFields[i] - if type(field) == "userdata" then - field:enable(false) - end + if type(field) == "userdata" then field:enable(false) end end end --- Enables all form fields in the dashx application. --- Iterates through the formFields table and enables each field if it is of type "userdata". -function ui.enableAllFields() - for _, field in ipairs(dashx.app.formFields) do - if type(field) == "userdata" then - field:enable(true) - end - end -end +function ui.enableAllFields() for _, field in ipairs(dashx.app.formFields) do if type(field) == "userdata" then field:enable(true) end end end --- Disables all navigation fields in the form except the currently active one. --- Iterates through the `formNavigationFields` table in the `dashx.app` namespace --- and disables each field by calling its `enable` method with `false` as the argument. -function ui.disableAllNavigationFields() - for i, v in pairs(dashx.app.formNavigationFields) do - if x ~= v then - v:enable(false) - end - end -end +function ui.disableAllNavigationFields() for i, v in pairs(dashx.app.formNavigationFields) do if x ~= v then v:enable(false) end end end --- Enables all navigation fields in the form except the one specified by 'x'. --- Iterates through 'dashx.app.formNavigationFields' and calls 'enable(true)' on each field. -function ui.enableAllNavigationFields() - for i, v in pairs(dashx.app.formNavigationFields) do - if x ~= v then - v:enable(true) - end - end -end +function ui.enableAllNavigationFields() for i, v in pairs(dashx.app.formNavigationFields) do if x ~= v then v:enable(true) end end end --- Enables a navigation field based on the given index. --- @param x The index of the navigation field to enable. function ui.enableNavigationField(x) local field = dashx.app.formNavigationFields[x] if field then field:enable(true) end end --- Disables the navigation field at the specified index. --- @param x The index of the navigation field to disable. function ui.disableNavigationField(x) local field = dashx.app.formNavigationFields[x] if field then field:enable(false) end end ---[[ - Checks if any progress-related display is active. - - @return boolean True if any of the progress, save, no link, or bad version displays are active; otherwise, false. -]] -function ui.progressDisplayIsActive() - return dashx.app.dialogs.progressDisplay or - dashx.app.dialogs.saveDisplay or - dashx.app.dialogs.progressDisplayEsc or - dashx.app.dialogs.nolinkDisplay or - dashx.app.dialogs.badversionDisplay -end +function ui.progressDisplayIsActive() return dashx.app.dialogs.progressDisplay or dashx.app.dialogs.saveDisplay or dashx.app.dialogs.progressDisplayEsc or dashx.app.dialogs.nolinkDisplay or dashx.app.dialogs.badversionDisplay end ---[[ - Function: ui.openMainMenu - - Description: - Initializes and opens the main menu of the application. This function clears previous form fields, form lines, and graphics buttons, - checks for the required Ethos version, and loads the main menu configuration from a specified file. It then sets up the main menu - interface based on user preferences for icon size, and dynamically creates buttons for each section and page defined in the main menu - configuration. The function also handles hiding sections and pages based on Ethos version, MSP version, and developer mode settings. - - Parameters: - None - - Returns: - None -]] function ui.openMainMenu() - - dashx.app.formFields = {} dashx.app.formLines = {} dashx.session.lastLabel = nil dashx.app.isOfflinePage = false - -- clear old icons - for i in pairs(dashx.app.gfx_buttons) do - if i ~= "mainmenu" then - dashx.app.gfx_buttons[i] = nil - end - end + for i in pairs(dashx.app.gfx_buttons) do if i ~= "mainmenu" then dashx.app.gfx_buttons[i] = nil end end - -- hard exit on error - if not dashx.utils.ethosVersionAtLeast(config.ethosVersion) then - return - end + if not dashx.utils.ethosVersionAtLeast(config.ethosVersion) then return end local MainMenu = dashx.app.MainMenu - -- Clear all navigation variables dashx.app.lastIdx = nil dashx.app.lastTitle = nil dashx.app.lastScript = nil @@ -407,25 +241,24 @@ function ui.openMainMenu() dashx.app.uiState = dashx.app.uiStatus.mainMenu dashx.app.triggers.disableRssiTimeout = false - -- Determine button size based on preferences dashx.preferences.general.iconsize = tonumber(dashx.preferences.general.iconsize) or 1 local buttonW, buttonH, padding, numPerRow if dashx.preferences.general.iconsize == 0 then - -- Text icons + padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow elseif dashx.preferences.general.iconsize == 1 then - -- Small icons + padding = dashx.app.radio.buttonPaddingSmall buttonW = dashx.app.radio.buttonWidthSmall buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall elseif dashx.preferences.general.iconsize == 2 then - -- Large icons + padding = dashx.app.radio.buttonPadding buttonW = dashx.app.radio.buttonWidth buttonH = dashx.app.radio.buttonHeight @@ -441,9 +274,7 @@ function ui.openMainMenu() dashx.preferences.menulastselected["mainmenu"] = dashx.preferences.menulastselected["mainmenu"] or 1 for idx, section in ipairs(MainMenu.sections) do - local hideSection = (section.ethosversion and dashx.session.ethosRunningVersion < section.ethosversion) or - (section.mspversion and (dashx.session.apiVersion or 1) < section.mspversion) or - (section.developer and not dashx.preferences.developer.devtools) + local hideSection = (section.ethosversion and dashx.session.ethosRunningVersion < section.ethosversion) or (section.mspversion and (dashx.session.apiVersion or 1) < section.mspversion) or (section.developer and not dashx.preferences.developer.devtools) if not hideSection then form.addLine(section.title) @@ -451,16 +282,12 @@ function ui.openMainMenu() for pidx, page in ipairs(MainMenu.pages) do if page.section == idx then - local hideEntry = (page.ethosversion and not dashx.utils.ethosVersionAtLeast(page.ethosversion)) or - (page.mspversion and (dashx.session.apiVersion or 1) < page.mspversion) or - (page.developer and not dashx.preferences.developer.devtools) + local hideEntry = (page.ethosversion and not dashx.utils.ethosVersionAtLeast(page.ethosversion)) or (page.mspversion and (dashx.session.apiVersion or 1) < page.mspversion) or (page.developer and not dashx.preferences.developer.devtools) local offline = page.offline if not hideEntry then - if lc == 0 then - y = form.height() + (dashx.preferences.general.iconsize == 2 and dashx.app.radio.buttonPadding or dashx.app.radio.buttonPaddingSmall) - end + if lc == 0 then y = form.height() + (dashx.preferences.general.iconsize == 2 and dashx.app.radio.buttonPadding or dashx.app.radio.buttonPaddingSmall) end local x = (buttonW + padding) * lc if dashx.preferences.general.iconsize ~= 0 then @@ -478,13 +305,11 @@ function ui.openMainMenu() dashx.preferences.menulastselected["mainmenu"] = pidx dashx.app.ui.progressDisplay() dashx.app.isOfflinePage = offline - dashx.app.ui.openPage(pidx, page.title, page.folder .. "/" .. page.script) + dashx.app.ui.openPage(pidx, page.title, page.folder .. "/" .. page.script) end }) - if dashx.preferences.menulastselected["mainmenu"] == pidx then - dashx.app.formFields[pidx]:focus() - end + if dashx.preferences.menulastselected["mainmenu"] == pidx then dashx.app.formFields[pidx]:focus() end lc = (lc + 1) % numPerRow end @@ -497,66 +322,32 @@ function ui.openMainMenu() dashx.utils.reportMemoryUsage("MainMenu") end - ---[[ - Retrieves a label from a given page by its ID. - - @param id The ID of the label to retrieve. - @param page The page containing the labels. - @return The label with the specified ID, or nil if not found. -]] function ui.getLabel(id, page) if id == nil then return nil end - for i = 1, #page do - if page[i].label == id then - return page[i] - end - end + for i = 1, #page do if page[i].label == id then return page[i] end end return nil end ---[[ - ui.fieldChoice(i) - - This function creates a choice field in the UI form based on the provided index `i`. - - Parameters: - - i: The index of the field in the `fields` table. - - The function performs the following steps: - 1. Retrieves the application, page, fields, form lines, form fields, and radio text. - 2. Determines the position of the text and field based on whether the field is inline. - 3. Adds static text or a new form line based on the field's properties. - 4. Converts the field's table data if available. - 5. Adds a choice field to the form and sets up its get and set value functions. - 6. Disables the field if specified. -]] function ui.fieldChoice(i) - local app = dashx.app - local page = app.Page - local fields = page.fields - local f = fields[i] - local formLines = app.formLines - local formFields = app.formFields + local app = dashx.app + local page = app.Page + local fields = page.fields + local f = fields[i] + local formLines = app.formLines + local formFields = app.formFields local radioText = app.radio.text local posText, posField if f.inline and f.inline >= 1 and f.label then - if radioText == 2 and f.t2 then - f.t = f.t2 - end + if radioText == 2 and f.t2 then f.t = f.t2 end local p = dashx.app.utils.getInlinePositions(f, page) - posText = p.posText + posText = p.posText posField = p.posField form.addStaticText(formLines[dashx.session.formLineCnt], posText, f.t) else if f.t then - if radioText == 2 and f.t2 then - f.t = f.t2 - end - if f.label then - f.t = " " .. f.t - end + if radioText == 2 and f.t2 then f.t = f.t2 end + if f.label then f.t = " " .. f.t end end dashx.session.formLineCnt = dashx.session.formLineCnt + 1 formLines[dashx.session.formLineCnt] = form.addLine(f.t) @@ -564,68 +355,41 @@ function ui.fieldChoice(i) end local tbldata = f.table and dashx.app.utils.convertPageValueTable(f.table, f.tableIdxInc) or {} - formFields[i] = form.addChoiceField(formLines[dashx.session.formLineCnt], posField, tbldata, - function() - if not fields or not fields[i] then - ui.disableAllFields() - ui.disableAllNavigationFields() - ui.enableNavigationField('menu') - return nil - end - return dashx.app.utils.getFieldValue(fields[i]) - end, - function(value) - if f.postEdit then f.postEdit(page, value) end - if f.onChange then f.onChange(page, value) end - f.value = dashx.app.utils.saveFieldValue(fields[i], value) + formFields[i] = form.addChoiceField(formLines[dashx.session.formLineCnt], posField, tbldata, function() + if not fields or not fields[i] then + ui.disableAllFields() + ui.disableAllNavigationFields() + ui.enableNavigationField('menu') + return nil end - ) + return dashx.app.utils.getFieldValue(fields[i]) + end, function(value) + if f.postEdit then f.postEdit(page, value) end + if f.onChange then f.onChange(page, value) end + f.value = dashx.app.utils.saveFieldValue(fields[i], value) + end) - if f.disable then - formFields[i]:enable(false) - end + if f.disable then formFields[i]:enable(false) end end ---[[ - Function: ui.fieldNumber - - Description: - This function creates and configures a number input field in the form. It handles various configurations - such as inline positioning, text overrides, value scaling, and field-specific behaviors like focus, - default values, and help text. - - Parameters: - - i (number): The index of the field in the fields table. - - Behavior: - - Applies radio text override if applicable. - - Determines the position of the field and text based on inline settings. - - Adjusts min and max values based on offset and scaling factors. - - Adds the number field to the form with specified configurations. - - Sets up callbacks for getting and setting the field value. - - Configures additional properties like focus behavior, default value, decimals, unit, step, and help text. - - Enables or disables instant change based on the field configuration. -]] function ui.fieldNumber(i) - local app = dashx.app - local page = app.Page + local app = dashx.app + local page = app.Page local fields = page.fields - local f = fields[i] - local formLines = app.formLines + local f = fields[i] + local formLines = app.formLines local formFields = app.formFields local posField, posText if f.inline and f.inline >= 1 and f.label then local p = dashx.app.utils.getInlinePositions(f, page) - posText = p.posText + posText = p.posText posField = p.posField form.addStaticText(formLines[dashx.session.formLineCnt], posText, f.t) else if f.t then - if f.label then - f.t = " " .. f.t - end + if f.label then f.t = " " .. f.t end else f.t = "" end @@ -651,28 +415,23 @@ function ui.fieldNumber(i) minValue = minValue or 0 maxValue = maxValue or 0 - formFields[i] = form.addNumberField(formLines[dashx.session.formLineCnt], posField, minValue, maxValue, - function() - if not (page.fields and page.fields[i]) then - ui.disableAllFields() - ui.disableAllNavigationFields() - ui.enableNavigationField('menu') - return nil - end - return dashx.app.utils.getFieldValue(page.fields[i]) - end, - function(value) - if f.postEdit then f.postEdit(page) end - if f.onChange then f.onChange(page) end - f.value = dashx.app.utils.saveFieldValue(page.fields[i], value) + formFields[i] = form.addNumberField(formLines[dashx.session.formLineCnt], posField, minValue, maxValue, function() + if not (page.fields and page.fields[i]) then + ui.disableAllFields() + ui.disableAllNavigationFields() + ui.enableNavigationField('menu') + return nil end - ) + return dashx.app.utils.getFieldValue(page.fields[i]) + end, function(value) + if f.postEdit then f.postEdit(page) end + if f.onChange then f.onChange(page) end + f.value = dashx.app.utils.saveFieldValue(page.fields[i], value) + end) local currentField = formFields[i] - if f.onFocus then - currentField:onFocus(function() f.onFocus(page) end) - end + if f.onFocus then currentField:onFocus(function() f.onFocus(page) end) end if f.default then if f.offset then f.default = f.default + f.offset end @@ -686,15 +445,13 @@ function ui.fieldNumber(i) end if f.decimals then currentField:decimals(f.decimals) end - if f.unit then currentField:suffix(f.unit) end - if f.step then currentField:step(f.step) end - if f.disable then currentField:enable(false) end + if f.unit then currentField:suffix(f.unit) end + if f.step then currentField:step(f.step) end + if f.disable then currentField:enable(false) end if f.help or f.apikey then if not f.help and f.apikey then f.help = f.apikey end - if app.fieldHelpTxt and app.fieldHelpTxt[f.help] and app.fieldHelpTxt[f.help].t then - currentField:help(app.fieldHelpTxt[f.help].t) - end + if app.fieldHelpTxt and app.fieldHelpTxt[f.help] and app.fieldHelpTxt[f.help].t then currentField:help(app.fieldHelpTxt[f.help].t) end end if f.instantChange == false then @@ -704,50 +461,26 @@ function ui.fieldNumber(i) end end - ---[[ - Function: ui.fieldStaticText - - This function adds a static text field to the form based on the provided index. - - Parameters: - i (number) - The index of the field in the fields table. - - Behavior: - - Retrieves the application, page, fields, form lines, form fields, and radio text. - - Determines the position and text for the static text field based on the field's properties. - - Adds the static text to the form. - - Increments the form line counter. - - Optionally hides the field if `HideMe` is true. - - Adds the static text field to the form fields table. - - Sets up focus, decimals, unit, and step properties if they are defined for the field. ---]] function ui.fieldStaticText(i) - local app = dashx.app - local page = app.Page - local fields = page.fields - local f = fields[i] + local app = dashx.app + local page = app.Page + local fields = page.fields + local f = fields[i] local formLines = app.formLines local formFields = app.formFields local radioText = app.radio.text local posText, posField if f.inline and f.inline >= 1 and f.label then - if radioText == 2 and f.t2 then - f.t = f.t2 - end + if radioText == 2 and f.t2 then f.t = f.t2 end local p = dashx.app.utils.getInlinePositions(f, page) - posText = p.posText + posText = p.posText posField = p.posField form.addStaticText(formLines[dashx.session.formLineCnt], posText, f.t) else - if radioText == 2 and f.t2 then - f.t = f.t2 - end + if radioText == 2 and f.t2 then f.t = f.t2 end if f.t then - if f.label then - f.t = " " .. f.t - end + if f.label then f.t = " " .. f.t end else f.t = "" end @@ -756,64 +489,39 @@ function ui.fieldStaticText(i) posField = f.position or nil end - if HideMe == true then - -- posField = {x = 2000, y = 0, w = 20, h = 20} - end + if HideMe == true then end formFields[i] = form.addStaticText(formLines[dashx.session.formLineCnt], posField, dashx.app.utils.getFieldValue(fields[i])) local currentField = formFields[i] - if f.onFocus then - currentField:onFocus(function() f.onFocus(page) end) - end + if f.onFocus then currentField:onFocus(function() f.onFocus(page) end) end if f.decimals then currentField:decimals(f.decimals) end - if f.unit then currentField:suffix(f.unit) end - if f.step then currentField:step(f.step) end + if f.unit then currentField:suffix(f.unit) end + if f.step then currentField:step(f.step) end end - ---[[ - Function: ui.fieldText - - Description: - This function is responsible for creating and configuring a text field in the UI. It handles the display of static text, - inline text positioning, and the creation of text fields with various properties such as focus, disable, help text, - and instant change behavior. - - Parameters: - - i (number): The index of the field in the fields table. - - Returns: - None -]] function ui.fieldText(i) - local app = dashx.app - local page = app.Page - local fields = page.fields - local f = fields[i] - local formLines = app.formLines - local formFields = app.formFields - local radioText = app.radio.text + local app = dashx.app + local page = app.Page + local fields = page.fields + local f = fields[i] + local formLines = app.formLines + local formFields = app.formFields + local radioText = app.radio.text local posText, posField if f.inline and f.inline >= 1 and f.label then - if radioText == 2 and f.t2 then - f.t = f.t2 - end + if radioText == 2 and f.t2 then f.t = f.t2 end local p = dashx.app.utils.getInlinePositions(f, page) - posText = p.posText + posText = p.posText posField = p.posField form.addStaticText(formLines[dashx.session.formLineCnt], posText, f.t) else - if radioText == 2 and f.t2 then - f.t = f.t2 - end + if radioText == 2 and f.t2 then f.t = f.t2 end if f.t then - if f.label then - f.t = " " .. f.t - end + if f.label then f.t = " " .. f.t end else f.t = "" end @@ -823,37 +531,28 @@ function ui.fieldText(i) posField = f.position or nil end - formFields[i] = form.addTextField(formLines[dashx.session.formLineCnt], posField, - function() - if not fields or not fields[i] then - ui.disableAllFields() - ui.disableAllNavigationFields() - ui.enableNavigationField('menu') - return nil - end - return dashx.app.utils.getFieldValue(fields[i]) - end, - function(value) - if f.postEdit then f.postEdit(page) end - if f.onChange then f.onChange(page) end - - f.value = dashx.app.utils.saveFieldValue(fields[i], value) + formFields[i] = form.addTextField(formLines[dashx.session.formLineCnt], posField, function() + if not fields or not fields[i] then + ui.disableAllFields() + ui.disableAllNavigationFields() + ui.enableNavigationField('menu') + return nil end - ) + return dashx.app.utils.getFieldValue(fields[i]) + end, function(value) + if f.postEdit then f.postEdit(page) end + if f.onChange then f.onChange(page) end + + f.value = dashx.app.utils.saveFieldValue(fields[i], value) + end) local currentField = formFields[i] - if f.onFocus then - currentField:onFocus(function() f.onFocus(page) end) - end + if f.onFocus then currentField:onFocus(function() f.onFocus(page) end) end - if f.disable then - currentField:enable(false) - end + if f.disable then currentField:enable(false) end - if f.help and app.fieldHelpTxt and app.fieldHelpTxt[f.help] and app.fieldHelpTxt[f.help].t then - currentField:help(app.fieldHelpTxt[f.help].t) - end + if f.help and app.fieldHelpTxt and app.fieldHelpTxt[f.help] and app.fieldHelpTxt[f.help].t then currentField:help(app.fieldHelpTxt[f.help].t) end if f.instantChange == false then currentField:enableInstantChange(false) @@ -862,42 +561,18 @@ function ui.fieldText(i) end end - ---[[ - Function: ui.fieldLabel - - Parameters: - - f (table): A table containing field properties. - - t (string, optional): A text value. - - t2 (string, optional): A secondary text value. - - label (string, optional): A label identifier. - - i (number): An index value (not used in the function). - - l (number): A length value (not used in the function). - - Description: - This function handles the creation and management of field labels within the UI. - It updates the text value based on the presence of secondary text and label properties. - If a label is provided and it is different from the last processed label, - it adds a new line to the form and updates the session's lastLabel and formLineCnt. -]] function ui.fieldLabel(f, i, l) local app = dashx.app if f.t then - if f.t2 then - f.t = f.t2 - end - if f.label then - f.t = " " .. f.t - end + if f.t2 then f.t = f.t2 end + if f.label then f.t = " " .. f.t end end if f.label then local label = app.ui.getLabel(f.label, l) local labelValue = label.t - if label.t2 then - labelValue = label.t2 - end + if label.t2 then labelValue = label.t2 end local labelName = f.t and labelValue or "unknown" if f.label ~= dashx.session.lastLabel then @@ -910,27 +585,17 @@ function ui.fieldLabel(f, i, l) end end - ---[[ - Function: ui.fieldHeader - Description: Creates a header field in the UI with a title and navigation buttons. - Parameters: - title (string) - The title text to be displayed in the header. - Returns: None -]] function ui.fieldHeader(title) - local app = dashx.app - local utils = dashx.utils - local radio = app.radio + local app = dashx.app + local utils = dashx.utils + local radio = app.radio local formFields = app.formFields - local lcdWidth = dashx.session.lcdWidth + local lcdWidth = dashx.session.lcdWidth local w, h = utils.getWindowSize() local padding = 5 local colStart = math.floor(w * 59.4 / 100) - if radio.navButtonOffset then - colStart = colStart - radio.navButtonOffset - end + if radio.navButtonOffset then colStart = colStart - radio.navButtonOffset end local buttonW = radio.buttonWidth and radio.menuButtonWidth or ((w - colStart) / 3 - padding) local buttonH = radio.navbuttonHeight @@ -941,87 +606,44 @@ function ui.fieldHeader(title) app.ui.navigationButtons(w - 5, radio.linePaddingTop, buttonW, buttonH) end +function ui.openPageRefresh(idx, title, script, extra1, extra2, extra3, extra5, extra6) dashx.app.triggers.isReady = false end ---- Opens a page and refreshes the UI. --- @param idx The index of the page. --- @param title The title of the page. --- @param script The script associated with the page. --- @param extra1 Additional parameter 1. --- @param extra2 Additional parameter 2. --- @param extra3 Additional parameter 3. --- @param extra5 Additional parameter 5. --- @param extra6 Additional parameter 6. -function ui.openPageRefresh(idx, title, script, extra1, extra2, extra3, extra5, extra6) - dashx.app.triggers.isReady = false -end - --- Cache for help modules to avoid repeated file_exists and loadfile ui._helpCache = ui._helpCache or {} local function getHelpData(section) - if ui._helpCache[section] == nil then - local helpPath = "app/modules/" .. section .. "/help.lua" - if dashx.utils.file_exists(helpPath) then - local ok, helpData = pcall(function() - return assert(loadfile(helpPath))() - end) - ui._helpCache[section] = (ok and type(helpData)=="table") and helpData or false - else - ui._helpCache[section] = false + if ui._helpCache[section] == nil then + local helpPath = "app/modules/" .. section .. "/help.lua" + if dashx.utils.file_exists(helpPath) then + local ok, helpData = pcall(function() return assert(loadfile(helpPath))() end) + ui._helpCache[section] = (ok and type(helpData) == "table") and helpData or false + else + ui._helpCache[section] = false + end end - end - return ui._helpCache[section] or nil + return ui._helpCache[section] or nil end - ---[[ - Function: ui.openPage - - Description: - Opens a new page in the UI, initializing the global UI state, loading the specified module, - and setting up form data and help text if available. If the loaded module has its own - openPage function, it will be called with the provided arguments. - - Parameters: - - idx (number): Index of the page to open. - - title (string): Title of the page. - - script (string): Script name of the module to load. - - extra1 (any): Additional parameter 1. - - extra2 (any): Additional parameter 2. - - extra3 (any): Additional parameter 3. - - extra5 (any): Additional parameter 5. - - extra6 (any): Additional parameter 6. - - Returns: - None -]] function ui.openPage(idx, title, script, extra1, extra2, extra3, extra5, extra6) - -- Initialize global UI state and clear form data dashx.app.uiState = dashx.app.uiStatus.pages dashx.app.triggers.isReady = false dashx.app.formFields = {} dashx.app.formLines = {} dashx.session.lastLabel = nil - -- Load the module local modulePath = "app/modules/" .. script dashx.app.Page = assert(loadfile(modulePath))(idx) - -- Load the help file if it exists local section = script:match("([^/]+)") local helpData = getHelpData(section) dashx.app.fieldHelpTxt = helpData and helpData.fields or nil - -- dashx.app.Page = assert(loadfile(modulePath))(idx) - -- If the Page has its own openPage function, use it and return early if dashx.app.Page.openPage then dashx.app.Page.openPage(idx, title, script, extra1, extra2, extra3, extra5, extra6) dashx.utils.reportMemoryUsage(title) return end - -- Fallback behavior if no custom openPage exists dashx.app.lastIdx = idx dashx.app.lastTitle = title dashx.app.lastScript = script @@ -1034,12 +656,7 @@ function ui.openPage(idx, title, script, extra1, extra2, extra3, extra5, extra6) if dashx.app.Page.headerLine then local headerLine = form.addLine("") - form.addStaticText(headerLine, { - x = 0, - y = dashx.app.radio.linePaddingTop, - w = dashx.session.lcdWidth, - h = dashx.app.radio.navbuttonHeight - }, dashx.app.Page.headerLine) + form.addStaticText(headerLine, {x = 0, y = dashx.app.radio.linePaddingTop, w = dashx.session.lcdWidth, h = dashx.app.radio.navbuttonHeight}, dashx.app.Page.headerLine) end dashx.session.formLineCnt = 0 @@ -1052,14 +669,10 @@ function ui.openPage(idx, title, script, extra1, extra2, extra3, extra5, extra6) for i, field in ipairs(dashx.app.Page.fields) do local label = dashx.app.Page.labels - local version = dashx.utils.round(dashx.session.apiVersion,2) + local version = dashx.utils.round(dashx.session.apiVersion, 2) if version == nil then return end - local valid = (field.apiversion == nil or dashx.utils.round(field.apiversion,2) <= version) and - (field.apiversionlt == nil or dashx.utils.round(field.apiversionlt,2) > version) and - (field.apiversiongt == nil or dashx.utils.round(field.apiversiongt,2) < version) and - (field.apiversionlte == nil or dashx.utils.round(field.apiversionlte,2) >= version) and - (field.apiversiongte == nil or dashx.utils.round(field.apiversiongte,2) <= version) and - (field.enablefunction == nil or field.enablefunction()) + local valid = (field.apiversion == nil or dashx.utils.round(field.apiversion, 2) <= version) and (field.apiversionlt == nil or dashx.utils.round(field.apiversionlt, 2) > version) and (field.apiversiongt == nil or dashx.utils.round(field.apiversiongt, 2) < version) and + (field.apiversionlte == nil or dashx.utils.round(field.apiversionlte, 2) >= version) and (field.apiversiongte == nil or dashx.utils.round(field.apiversiongte, 2) <= version) and (field.enablefunction == nil or field.enablefunction()) if field.hidden ~= true and valid then dashx.app.ui.fieldLabel(field, i, label) @@ -1083,7 +696,7 @@ function ui.openPage(idx, title, script, extra1, extra2, extra3, extra5, extra6) end function ui.openPageDashboard(idx, title, script, source, folder) - -- Initialize global UI state and clear form data + dashx.app.uiState = dashx.app.uiStatus.pages dashx.app.triggers.isReady = false dashx.app.formFields = {} @@ -1092,24 +705,21 @@ function ui.openPageDashboard(idx, title, script, source, folder) dashx.session.dashboardEditingTheme = source .. "/" .. folder - -- Load the module - local modulePath = script + local modulePath = script dashx.app.Page = assert(loadfile(modulePath))(idx) - -- load up the menu local w, h = dashx.utils.getWindowSize() local windowWidth = w local windowHeight = h local padding = dashx.app.radio.buttonPadding local sc - local panel + local panel form.clear() - --form.addLine("../ " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.name)@" .. " / " .. title) - form.addLine( "@i18n(app.modules.settings.name)@" .. " / " .. title) + form.addLine("@i18n(app.modules.settings.name)@" .. " / " .. title) buttonW = 100 local x = windowWidth - (buttonW * 2) - 15 @@ -1117,74 +727,47 @@ function ui.openPageDashboard(idx, title, script, source, folder) text = "@i18n(app.navigation_menu)@", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.app.lastIdx = nil dashx.session.lastPage = nil if dashx.app.Page and dashx.app.Page.onNavMenu then dashx.app.Page.onNavMenu(dashx.app.Page) end - - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.dashboard)@", - "settings/tools/dashboard_settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.dashboard)@", "settings/tools/dashboard_settings.lua") end }) dashx.app.formNavigationFields['menu']:focus() - local x = windowWidth - buttonW - 10 dashx.app.formNavigationFields['save'] = form.addButton(line, {x = x, y = dashx.app.radio.linePaddingTop, w = buttonW, h = dashx.app.radio.navbuttonHeight}, { text = "SAVE", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() - local buttons = { - { - label = "@i18n(app.btn_ok_long)@", - action = function() - local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" - dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - if dashx.app.Page.write then - dashx.app.Page.write() - end - -- update dashboard theme - dashx.widgets.dashboard.reload_themes() - dashx.app.triggers.closeSave = true - return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, - } - - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + local buttons = { + { + label = "@i18n(app.btn_ok_long)@", + action = function() + local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" + dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) + if dashx.app.Page.write then dashx.app.Page.write() end + + dashx.widgets.dashboard.reload_themes() + dashx.app.triggers.closeSave = true + return true + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} + } + + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end }) dashx.app.formNavigationFields['menu']:focus() - - - -- If the Page has its own openPage function, use it and return early if dashx.app.Page.configure then dashx.app.Page.configure(idx, title, script, extra1, extra2, extra3, extra5, extra6) dashx.utils.reportMemoryUsage(title) @@ -1194,26 +777,6 @@ function ui.openPageDashboard(idx, title, script, source, folder) end - ---[[ - Function: ui.navigationButtons - - Description: - This function creates and positions navigation buttons (Menu, Save, Reload, Tool, Help) on the UI. - It calculates the offsets for each button based on their visibility and positions them accordingly. - - Parameters: - - x (number): The x-coordinate for the button placement. - - y (number): The y-coordinate for the button placement. - - w (number): The width of the buttons. - - h (number): The height of the buttons. - - Notes: - - The function checks the visibility of each button from `dashx.app.Page.navButtons`. - - If a button is visible, it calculates its offset and adds it to the form. - - Each button has a specific action defined in its `press` function. - - The Help button attempts to load a help file and displays relevant help content. ---]] function ui.navigationButtons(x, y, w, h) local xOffset = 0 @@ -1232,10 +795,6 @@ function ui.navigationButtons(x, y, w, h) navButtons = dashx.app.Page.navButtons end - -- calc all offsets - -- these are done 'early' to enable the actual placement of the buttons on - -- display to be rendered by ethos in the right order - for scrolling via - -- keypad to work. if navButtons.help ~= nil and navButtons.help == true then xOffset = xOffset + wS + padding end helpOffset = x - xOffset @@ -1251,15 +810,13 @@ function ui.navigationButtons(x, y, w, h) if navButtons.menu ~= nil and navButtons.menu == true then xOffset = xOffset + w + padding end menuOffset = x - xOffset - -- MENU BTN if navButtons.menu ~= nil and navButtons.menu == true then dashx.app.formNavigationFields['menu'] = form.addButton(line, {x = menuOffset, y = y, w = w, h = h}, { text = "@i18n(app.navigation_menu)@", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() if dashx.app.Page and dashx.app.Page.onNavMenu then dashx.app.Page.onNavMenu(dashx.app.Page) @@ -1271,15 +828,13 @@ function ui.navigationButtons(x, y, w, h) dashx.app.formNavigationFields['menu']:focus() end - -- SAVE BTN if navButtons.save ~= nil and navButtons.save == true then dashx.app.formNavigationFields['save'] = form.addButton(line, {x = saveOffset, y = y, w = w, h = h}, { text = "@i18n(app.navigation_save)@", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() if dashx.app.Page and dashx.app.Page.onSaveMenu then dashx.app.Page.onSaveMenu(dashx.app.Page) @@ -1290,61 +845,46 @@ function ui.navigationButtons(x, y, w, h) }) end - -- RELOAD BTN if navButtons.reload ~= nil and navButtons.reload == true then dashx.app.formNavigationFields['reload'] = form.addButton(line, {x = reloadOffset, y = y, w = w, h = h}, { text = "@i18n(app.navigation_reload)@", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() if dashx.app.Page and dashx.app.Page.onReloadMenu then dashx.app.Page.onReloadMenu(dashx.app.Page) else - dashx.app.triggers.triggerReload = true + dashx.app.triggers.triggerReload = true end return true end }) end - -- TOOL BUTTON if navButtons.tool ~= nil and navButtons.tool == true then - dashx.app.formNavigationFields['tool'] = form.addButton(line, {x = toolOffset, y = y, w = wS, h = h}, { - text = "@i18n(app.navigation_tools)@", - icon = nil, - options = FONT_S, - paint = function() - end, - press = function() - dashx.app.Page.onToolMenu() - end - }) + dashx.app.formNavigationFields['tool'] = form.addButton(line, {x = toolOffset, y = y, w = wS, h = h}, {text = "@i18n(app.navigation_tools)@", icon = nil, options = FONT_S, paint = function() end, press = function() dashx.app.Page.onToolMenu() end}) end - -- HELP BUTTON if navButtons.help ~= nil and navButtons.help == true then local section = dashx.app.lastScript:match("([^/]+)") local script = dashx.app.lastScript:match("/([^/]+)%.lua$") - -- Load help module with caching + local help = getHelpData(section) if help then - -- Execution of the file succeeded dashx.app.formNavigationFields['help'] = form.addButton(line, {x = helpOffset, y = y, w = wS, h = h}, { text = "@i18n(app.navigation_help)@", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() if dashx.app.Page and dashx.app.Page.onHelpMenu then dashx.app.Page.onHelpMenu(dashx.app.Page) else - -- choose default or custom + if help.help[script] then dashx.app.ui.openPageHelp(help.help[script], section) else @@ -1355,61 +895,20 @@ function ui.navigationButtons(x, y, w, h) }) else - -- No help available - dashx.app.formNavigationFields['help'] = form.addButton(line, {x = helpOffset, y = y, w = wS, h = h}, { - text = "@i18n(app.navigation_help)@", - icon = nil, options = FONT_S, paint = function() end, press = function() end - }) + + dashx.app.formNavigationFields['help'] = form.addButton(line, {x = helpOffset, y = y, w = wS, h = h}, {text = "@i18n(app.navigation_help)@", icon = nil, options = FONT_S, paint = function() end, press = function() end}) dashx.app.formNavigationFields['help']:enable(false) end end end ---[[ - Opens a help dialog with the provided text data and section. - - @param txtData (table) - A table containing lines of text to be displayed in the help dialog. - @param section (string) - The section of the help content to be displayed (currently unused). - - @return (boolean) - Always returns true when the close button is pressed. -]] function ui.openPageHelp(txtData, section) local message = table.concat(txtData, "\r\n\r\n") - form.openDialog({ - width = dashx.session.lcdWidth, - title = "Help - " .. dashx.app.lastTitle, - message = message, - buttons = {{ - label = "@i18n(app.btn_close)@", - action = function() return true end - }}, - options = TEXT_LEFT - }) + form.openDialog({width = dashx.session.lcdWidth, title = "Help - " .. dashx.app.lastTitle, message = message, buttons = {{label = "@i18n(app.btn_close)@", action = function() return true end}}, options = TEXT_LEFT}) end - ---[[ - Injects API attributes into a form field. - - @param formField (table) - The form field to inject attributes into. - @param f (table) - The form field's current attributes. - @param v (table) - The new attributes to inject. - - Attributes injected: - - decimals: Number of decimal places. - - scale: Scale factor. - - mult: Multiplication factor. - - offset: Offset value. - - unit: Unit suffix. - - step: Step value. - - min: Minimum value. - - max: Maximum value. - - default: Default value. - - table: Table of values. - - help: Help text. -]] function ui.injectApiAttributes(formField, f, v) local utils = dashx.utils local log = utils.log @@ -1421,23 +920,23 @@ function ui.injectApiAttributes(formField, f, v) formField:decimals(v.decimals) end end - if v.scale and not f.scale then + if v.scale and not f.scale then log("Injecting scale: " .. v.scale, "debug") - f.scale = v.scale + f.scale = v.scale end - if v.mult and not f.mult then + if v.mult and not f.mult then log("Injecting mult: " .. v.mult, "debug") - f.mult = v.mult + f.mult = v.mult end - if v.offset and not f.offset then + if v.offset and not f.offset then log("Injecting offset: " .. v.offset, "debug") - f.offset = v.offset + f.offset = v.offset end - if v.unit and not f.unit then + if v.unit and not f.unit then if f.type ~= 1 then log("Injecting unit: " .. v.unit, "debug") formField:suffix(v.unit) - end + end end if v.step and not f.step then if f.type ~= 1 then @@ -1448,9 +947,7 @@ function ui.injectApiAttributes(formField, f, v) end if v.min and not f.min then f.min = v.min - if f.offset then - f.min = f.min + f.offset - end + if f.offset then f.min = f.min + f.offset end if f.type ~= 1 then log("Injecting min: " .. f.min, "debug") formField:minimum(f.min) @@ -1458,9 +955,7 @@ function ui.injectApiAttributes(formField, f, v) end if v.max and not f.max then f.max = v.max - if f.offset then - f.max = f.max + f.offset - end + if f.offset then f.max = f.max + f.offset end if f.type ~= 1 then log("Injecting max: " .. f.max, "debug") formField:maximum(f.max) @@ -1468,47 +963,37 @@ function ui.injectApiAttributes(formField, f, v) end if v.default and not f.default then f.default = v.default - - -- Factor in all possible scaling. - if f.offset then - f.default = f.default + f.offset - end + + if f.offset then f.default = f.default + f.offset end local default = f.default * dashx.app.utils.decimalInc(f.decimals) - if f.mult then - default = default * f.mult - end + if f.mult then default = default * f.mult end - -- Work around ethos peculiarity on default boxes if trailing .0. local str = tostring(default) - if str:match("%.0$") then - default = math.ceil(default) - end + if str:match("%.0$") then default = math.ceil(default) end - if f.type ~= 1 then + if f.type ~= 1 then log("Injecting default: " .. default, "debug") formField:default(default) end - end - if v.table and not f.table then - f.table = v.table + end + if v.table and not f.table then + f.table = v.table local idxInc = f.tableIdxInc or v.tableIdxInc - local tbldata = dashx.app.utils.convertPageValueTable(v.table, idxInc) - if f.type == 1 then - log("Injecting table: {}", "debug") + local tbldata = dashx.app.utils.convertPageValueTable(v.table, idxInc) + if f.type == 1 then + log("Injecting table: {}", "debug") formField:values(tbldata) end - end + end if v.help then f.help = v.help log("Injecting help: {}", "debug") formField:help(v.help) - end + end - -- force focus to ensure field updates formField:focus(true) end - return ui diff --git a/scripts/dashx/app/lib/utils.lua b/scripts/dashx/app/lib/utils.lua index 8028af3..00044f3 100644 --- a/scripts/dashx/app/lib/utils.lua +++ b/scripts/dashx/app/lib/utils.lua @@ -1,48 +1,21 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local utils = {} - local arg = {...} local config = arg[1] - ---[[ - Function: app.utils.getRSSI - Description: Retrieves the RSSI (Received Signal Strength Indicator) value. - Returns 100 if the system is in simulation mode, the RSSI sensor check is skipped, or the app is in offline mode. - Otherwise, returns 100 if telemetry is active, and 0 if it is not. - Returns: - number - The RSSI value (100 or 0). -]] function utils.getRSSI() - if dashx.simevent.rflink == 1 then - return 0 - end + if dashx.simevent.rflink == 1 then return 0 end if dashx.app.offlineMode == true then return 100 end - if dashx.session.telemetryState then return 100 else @@ -50,20 +23,8 @@ function utils.getRSSI() end end --- Retrieves the current window size from the LCD. --- @return The window size as provided by lcd.getWindowSize(). -function utils.getWindowSize() - return lcd.getWindowSize() -end - ---[[ - Converts a table of values into a table of tables, where each inner table contains the original value and an incremented index. - - @param tbl (table) The input table of values. - @param inc (number) Optional increment to add to each index. Defaults to 0 if not provided. +function utils.getWindowSize() return lcd.getWindowSize() end - @return (table) A new table where each entry is a table containing the original value and the incremented index. -]] function utils.convertPageValueTable(tbl, inc) local thetable = {} @@ -83,48 +44,18 @@ function utils.convertPageValueTable(tbl, inc) return thetable end - ---[[ - Retrieves the value of a field, applying optional transformations. - - @param f (table) The field table containing the value and optional transformation parameters: - - value (number) The base value of the field. - - decimals (number, optional) The number of decimal places to consider. - - offset (number, optional) A value to add to the base value. - - mult (number, optional) A multiplier to apply to the value. - - @return (number) The transformed field value. -]] function utils.getFieldValue(f) local v = f.value or 0 - if f.decimals then - v = dashx.utils.round(v * dashx.app.utils.decimalInc(f.decimals),2) - end + if f.decimals then v = dashx.utils.round(v * dashx.app.utils.decimalInc(f.decimals), 2) end - if f.offset then - v = v + f.offset - end + if f.offset then v = v + f.offset end - if f.mult then - v = math.floor(v * f.mult + 0.5) - end + if f.mult then v = math.floor(v * f.mult + 0.5) end return v end ---[[ - Saves the given value to the specified field after applying necessary transformations. - - @param f (table) The field to save the value to. Expected to have the following optional properties: - - offset (number): A value to subtract from the input value before saving. - - decimals (number): The number of decimal places to consider for the value. - - postEdit (function): A function to call after the value is saved. - - mult (number): A multiplier to divide the final value by before returning. - @param value (number) The value to save to the field. - - @return (number) The final value saved to the field. -]] function utils.saveFieldValue(f, value) if value then if f.offset then value = value - f.offset end @@ -141,12 +72,6 @@ function utils.saveFieldValue(f, value) return f.value end --- Scales a given value based on the provided factor. --- @param value The value to be scaled. --- @param f A table containing scaling parameters: --- - decimals: The number of decimal places to consider. --- - scale: (optional) A scaling factor to divide the value by. --- @return The scaled value, rounded to the nearest integer, or nil if the input value is nil. function utils.scaleValue(value, f) if not value then return nil end local v = value * dashx.app.utils.decimalInc(f.decimals) @@ -154,47 +79,20 @@ function utils.scaleValue(value, f) return dashx.utils.round(v) end - --- Increments the decimal place value. --- @param dec The current decimal place value (1 for 10, 2 for 100, etc.). --- @return The next decimal place value or 1 if the input is nil or 0. function utils.decimalInc(dec) if dec == nil then return 1 elseif dec > 0 and dec <= 10 then - return 10 ^ dec -- Use dynamic exponentiation + return 10 ^ dec else - return nil -- Return nil for invalid inputs (optional, you can adjust behavior) + return nil end end - ---[[ - Computes the positions for inline elements on the LCD screen. - - @param f (table) - A table containing the label and inline properties. - - label (string) - The label text. - - inline (number) - The inline multiplier (1 to 5). - - t (string) - Optional text to display. - @param lPage (number) - The page number for inline size calculation. - - @return (table) - A table containing the positions for text and field elements. - - posText (table) - Position and size of the text element. - - x (number) - X-coordinate of the text. - - y (number) - Y-coordinate of the text. - - w (number) - Width of the text. - - h (number) - Height of the text. - - posField (table) - Position and size of the field element. - - x (number) - X-coordinate of the field. - - y (number) - Y-coordinate of the field. - - w (number) - Width of the field. - - h (number) - Height of the field. -]] function utils.getInlinePositions(f, lPage) - -- Compute inline size in one step. + local inline_size = utils.getInlineSize(f.label, lPage) * dashx.app.radio.inlinesize_mult - -- Get LCD dimensions. local w, h = dashx.utils.getWindowSize() local padding = 5 @@ -203,45 +101,28 @@ function utils.getInlinePositions(f, lPage) local eH = dashx.app.radio.navbuttonHeight local eY = dashx.app.radio.linePaddingTop - -- Set default text and compute its dimensions. f.t = f.t or "" lcd.font(FONT_M) local tsizeW, tsizeH = lcd.getTextSize(f.t) - -- Map inline values to multipliers. - local multipliers = { [1] = 1, [2] = 3, [3] = 5, [4] = 7, [5] = 9 } + local multipliers = {[1] = 1, [2] = 3, [3] = 5, [4] = 7, [5] = 9} local m = multipliers[f.inline] or 1 - -- For inline==1, extra padding is applied to the text. local textPadding = (f.inline == 1) and (2 * padding) or padding local posTextX = w - fieldW * m - tsizeW - textPadding local posFieldX = w - fieldW * m - ((f.inline == 1) and padding or 0) - local posText = { x = posTextX, y = eY, w = tsizeW, h = eH } - local posField = { x = posFieldX, y = eY, w = eW, h = eH } + local posText = {x = posTextX, y = eY, w = tsizeW, h = eH} + local posField = {x = posFieldX, y = eY, w = eW, h = eH} - return { posText = posText, posField = posField } + return {posText = posText, posField = posField} end - - ---[[ - Retrieves the inline size for a given label ID from the provided page. - - @param id (string|nil) The ID of the label to find the inline size for. If nil, a default size is returned. - @param lPage (table) The page object containing labels with their respective inline sizes. - - @return (number) The inline size of the label if found, otherwise returns a default size of 13.6. -]] function utils.getInlineSize(id, lPage) - if not id then return 13.6 end -- Prevent nil size issues - for i = 1, #lPage.labels do - if lPage.labels[i].label == id then - return lPage.labels[i].inline_size or 13.6 - end - end - return 13.6 -- Use default if label is missing + if not id then return 13.6 end + for i = 1, #lPage.labels do if lPage.labels[i].label == id then return lPage.labels[i].inline_size or 13.6 end end + return 13.6 end return utils diff --git a/scripts/dashx/app/modules/init.lua b/scripts/dashx/app/modules/init.lua index 8d3af18..b99f479 100644 --- a/scripts/dashx/app/modules/init.lua +++ b/scripts/dashx/app/modules/init.lua @@ -1,49 +1,31 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local pages = {} local sections = loadfile("app/modules/sections.lua")() --- find the modules (this should already have been done in the tasks/tasks.lua script but we catch and retry on the offchance it hasn't) if dashx.app.moduleList == nil then dashx.app.moduleList = dashx.utils.findModules() end --- Helper function to find section index local function findSectionIndex(sectionTitle) for index, section in ipairs(sections) do if section.id == sectionTitle then return index end end - return nil -- Section not found + return nil end --- Populate pages with mapped modules for _, module in ipairs(dashx.app.moduleList) do local sectionIndex = findSectionIndex(module.section) if sectionIndex then pages[#pages + 1] = {title = module.title, section = sectionIndex, script = module.script, order = module.order or 0, image = module.image, folder = module.folder, ethosversion = module.ethosversion, mspversion = module.mspversion, apiform = module.apiform, offline = module.offline or false} else - dashx.utils.log("Warning: Section '" .. module.section .. "' not found for module '" .. module.title .. "'","debug") + dashx.utils.log("Warning: Section '" .. module.section .. "' not found for module '" .. module.title .. "'", "debug") end end --- Function to sort pages by order within each section local function sortPagesBySectionAndOrder(pages) - -- Group pages by section + local groupedPages = {} for _, page in ipairs(pages) do @@ -51,21 +33,14 @@ local function sortPagesBySectionAndOrder(pages) table.insert(groupedPages[page.section], page) end - -- Sort each group by order - for section, pagesGroup in pairs(groupedPages) do - table.sort(pagesGroup, function(a, b) - return a.order < b.order - end) - end + for section, pagesGroup in pairs(groupedPages) do table.sort(pagesGroup, function(a, b) return a.order < b.order end) end - -- Reconstruct the pages table in the correct order local sortedPages = {} for section = 1, #sections do if groupedPages[section] then for _, page in ipairs(groupedPages[section]) do sortedPages[#sortedPages + 1] = page end end end return sortedPages end --- Sort the pages pages = sortPagesBySectionAndOrder(pages) return {pages = pages, sections = sections} diff --git a/scripts/dashx/app/modules/logs/help.lua b/scripts/dashx/app/modules/logs/help.lua index 534004e..1264308 100644 --- a/scripts/dashx/app/modules/logs/help.lua +++ b/scripts/dashx/app/modules/logs/help.lua @@ -1,36 +1,17 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- + +local dashx = require("dashx") + local data = {} data['help'] = {} -data['help']['default'] = { - "@i18n(app.modules.logs.help_logs_p1)@", - "@i18n(app.modules.logs.help_logs_p2)@", - "@i18n(app.modules.logs.help_logs_p3)@", -} +data['help']['default'] = {"@i18n(app.modules.logs.help_logs_p1)@", "@i18n(app.modules.logs.help_logs_p2)@", "@i18n(app.modules.logs.help_logs_p3)@"} -data['help']['logs_tool'] = { - "@i18n(app.modules.logs.help_logs_tool_p1)@", -} +data['help']['logs_tool'] = {"@i18n(app.modules.logs.help_logs_tool_p1)@"} data['fields'] = {} diff --git a/scripts/dashx/app/modules/logs/init.lua b/scripts/dashx/app/modules/logs/init.lua index 9e5269d..225d56c 100644 --- a/scripts/dashx/app/modules/logs/init.lua +++ b/scripts/dashx/app/modules/logs/init.lua @@ -1,31 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - title = "@i18n(app.modules.logs.name)@", -- title of the page - section = "tools", -- do not run if busy with msp - script = "logs_logs.lua", -- run this script - image = "gfx/logs.png", -- image for the page - order = 15, -- order in the section - offline = true, -- run this script offline - ethosversion = {1, 6, 2} -- disable button if ethos version is less than this, -} + +local dashx = require("dashx") + +local init = {title = "@i18n(app.modules.logs.name)@", section = "tools", script = "logs_logs.lua", image = "gfx/logs.png", order = 15, offline = true, ethosversion = {1, 6, 2}} return init diff --git a/scripts/dashx/app/modules/logs/lib/utils.lua b/scripts/dashx/app/modules/logs/lib/utils.lua index 9a80b40..d440f57 100644 --- a/scripts/dashx/app/modules/logs/lib/utils.lua +++ b/scripts/dashx/app/modules/logs/lib/utils.lua @@ -1,135 +1,81 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local utils = {} ---- Resolves model name from telemetry folder's configuration file --- @param foldername string: Name of the telemetry folder (nil returns "Unknown") --- @return string: Model name if found in logs.ini, otherwise "Unknown" function utils.resolveModelName(foldername) - if foldername == nil then - return "Unknown" - end + if foldername == nil then return "Unknown" end local iniName = "LOGS:dashx/telemetry/" .. foldername .. "/logs.ini" local iniData = dashx.ini.load_ini_file(iniName) or {} - if iniData["model"] and iniData["model"].name then - return iniData["model"].name - end + if iniData["model"] and iniData["model"].name then return iniData["model"].name end return "Unknown" end - function utils.hasModelName(foldername) - if foldername == nil then - return false - end + if foldername == nil then return false end local iniName = "LOGS:dashx/telemetry/" .. foldername .. "/logs.ini" local iniData = dashx.ini.load_ini_file(iniName) or {} - if iniData["model"] and iniData["model"].name then - return true - end + if iniData["model"] and iniData["model"].name then return true end return false end ---- Retrieves and manages CSV log files in a directory --- 1. Lists all CSV files in directory --- 2. Extracts timestamps from filenames (YYYY-MM-DD_HH-MM-SS_ format) --- 3. Sorts entries by timestamp (newest first) --- 4. Keeps only 50 most recent files, deletes older ones --- 5. Returns list of recent filenames --- @param logDir string: Path to log directory --- @return table: List of recent log filenames function utils.getLogs(logDir) local files = system.listFiles(logDir) local entries = {} - - -- Process CSV files with valid timestamps + for _, fname in ipairs(files) do if fname:match("%.csv$") then local date, time = fname:match("(%d%d%d%d%-%d%d%-%d%d)_(%d%d%-%d%d%-%d%d)_") - if date and time then - table.insert(entries, { - name = fname, - ts = date .. 'T' .. time -- ISO 8601 format for sorting - }) - end + if date and time then table.insert(entries, {name = fname, ts = date .. 'T' .. time}) end end end - -- Sort by timestamp (newest first) - table.sort(entries, function(a, b) - return a.ts > b.ts - end) - - -- Cleanup old files (keep max 50) + table.sort(entries, function(a, b) return a.ts > b.ts end) + local maxEntries = 50 - for i = maxEntries + 1, #entries do - os.remove(logDir .. "/" .. entries[i].name) - end - - -- Prepare result list + for i = maxEntries + 1, #entries do os.remove(logDir .. "/" .. entries[i].name) end + local result = {} - for i = 1, math.min(#entries, maxEntries) do - table.insert(result, entries[i].name) - end + for i = 1, math.min(#entries, maxEntries) do table.insert(result, entries[i].name) end return result end ---- Ensures base log directory structure exists --- @return string: Path to active log directory (if set) or base telemetry directory function utils.getLogPath() - -- Create directory hierarchy + os.mkdir("LOGS:") os.mkdir("LOGS:/dashx") os.mkdir("LOGS:/dashx/telemetry") - - -- Return active directory if available - --if dashx.session.activeLogDir then - -- return string.format("LOGS:/dashx/telemetry/%s/", dashx.session.activeLogDir) - --end + return "LOGS:/dashx/telemetry/" end ---- Gets or creates a specific log directory --- @param dirname string|nil: Optional subdirectory name --- @return string: Full path to requested directory function utils.getLogDir(dirname) - -- Ensure base directories exist + os.mkdir("LOGS:") os.mkdir("LOGS:/dashx") os.mkdir("LOGS:/dashx/telemetry") - - -- Handle default case (MCU ID directory) + if not dirname then - local defaultDir = "LOGS:/dashx/telemetry/" + local defaultDir = "LOGS:/dashx/telemetry/" os.mkdir(defaultDir) return defaultDir end - -- Return requested directory return "LOGS:/dashx/telemetry/" end ---- Lists non-hidden subdirectories in a directory --- @param logDir string: Path to scan --- @return table: List of directory entries { foldername = "name" } function utils.getLogsDir(logDir) local files = system.listFiles(logDir) local dirs = {} - for _, name in ipairs(files) do - -- Exclude ".", "..", names like ".log", and names ending in ".xyz" - if not (name == "." or name == ".." or - name:match("^%.%w%w%w$") or - name:match("%.%w%w%w$")) then - - if utils.hasModelName(name) then - dirs[#dirs + 1] = {foldername = name} - end - end - end + for _, name in ipairs(files) do if not (name == "." or name == ".." or name:match("^%.%w%w%w$") or name:match("%.%w%w%w$")) then if utils.hasModelName(name) then dirs[#dirs + 1] = {foldername = name} end end end return dirs end -return utils \ No newline at end of file +return utils diff --git a/scripts/dashx/app/modules/logs/logs_logs.lua b/scripts/dashx/app/modules/logs/logs_logs.lua index 26bbbc6..f560b65 100644 --- a/scripts/dashx/app/modules/logs/logs_logs.lua +++ b/scripts/dashx/app/modules/logs/logs_logs.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local utils = assert(loadfile("SCRIPTS:/" .. dashx.config.baseDir .. "/app/modules/logs/lib/utils.lua"))() @@ -15,36 +20,24 @@ local function getCleanModelName() return logdir end - local function extractHourMinute(filename) - -- Capture hour and minute from the time-portion (HH-MM-SS) after the underscore + local hour, minute = filename:match(".-%d%d%d%d%-%d%d%-%d%d_(%d%d)%-(%d%d)%-%d%d") - if hour and minute then - return hour .. ":" .. minute - end + if hour and minute then return hour .. ":" .. minute end return nil end local function format_date(iso_date) - local y, m, d = iso_date:match("^(%d+)%-(%d+)%-(%d+)$") - return os.date("%d %B %Y", os.time{ - year = tonumber(y), - month = tonumber(m), - day = tonumber(d), - }) + local y, m, d = iso_date:match("^(%d+)%-(%d+)%-(%d+)$") + return os.date("%d %B %Y", os.time {year = tonumber(y), month = tonumber(m), day = tonumber(d)}) end local function openPage(pidx, title, script, displaymode) - -- hard exit on error - if not dashx.utils.ethosVersionAtLeast() then - return - end - + if not dashx.utils.ethosVersionAtLeast() then return end currentDisplayMode = displaymode - dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.pages @@ -62,27 +55,25 @@ local function openPage(pidx, title, script, displaymode) local sc local panel - local logDir = utils.getLogPath() - - local logs = utils.getLogs(logDir) - + local logDir = utils.getLogPath() + local logs = utils.getLogs(logDir) local name = utils.resolveModelName(dashx.session.mcu_id or dashx.session.activeLogDir) - dashx.app.ui.fieldHeader("Logs" ) + dashx.app.ui.fieldHeader("Logs") local buttonW local buttonH local padding local numPerRow - if dashx.preferences.general.iconsize == 0 then + if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow end - -- SMALL ICONS + if dashx.preferences.general.iconsize == 1 then padding = dashx.app.radio.buttonPaddingSmall @@ -90,7 +81,7 @@ local function openPage(pidx, title, script, displaymode) buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall end - -- LARGE ICONS + if dashx.preferences.general.iconsize == 2 then padding = dashx.app.radio.buttonPadding @@ -99,7 +90,6 @@ local function openPage(pidx, title, script, displaymode) numPerRow = dashx.app.radio.buttonsPerRow end - local x = windowWidth - buttonW + 10 local lc = 0 @@ -111,7 +101,6 @@ local function openPage(pidx, title, script, displaymode) if dashx.app.gfx_buttons["logs"] == nil then dashx.app.gfx_buttons["logs"] = {} end if dashx.preferences.menulastselected["logs_logs"] == nil then dashx.preferences.menulastselected["logs_logs"] = 1 end - -- Group logs by date local groupedLogs = {} for _, filename in ipairs(logs) do local datePart = filename:match("(%d%d%d%d%-%d%d%-%d%d)_") @@ -121,11 +110,9 @@ local function openPage(pidx, title, script, displaymode) end end - -- Sort dates descending local dates = {} - for date,_ in pairs(groupedLogs) do table.insert(dates, date) end - table.sort(dates, function(a,b) return a > b end) - + for date, _ in pairs(groupedLogs) do table.insert(dates, date) end + table.sort(dates, function(a, b) return a > b end) if #dates == 0 then @@ -148,52 +135,44 @@ local function openPage(pidx, title, script, displaymode) for idx, section in ipairs(dates) do - form.addLine(format_date(section)) - local lc, y = 0, 0 + form.addLine(format_date(section)) + local lc, y = 0, 0 - for pidx, page in ipairs(groupedLogs[section]) do + for pidx, page in ipairs(groupedLogs[section]) do - if lc == 0 then - y = form.height() + (dashx.preferences.general.iconsize == 2 and dashx.app.radio.buttonPadding or dashx.app.radio.buttonPaddingSmall) - end + if lc == 0 then y = form.height() + (dashx.preferences.general.iconsize == 2 and dashx.app.radio.buttonPadding or dashx.app.radio.buttonPaddingSmall) end - local x = (buttonW + padding) * lc - if dashx.preferences.general.iconsize ~= 0 then - if dashx.app.gfx_buttons["logs_logs"][pidx] == nil then dashx.app.gfx_buttons["logs_logs"][pidx] = lcd.loadMask("app/modules/logs/gfx/logs.png") end - else - dashx.app.gfx_buttons["logs_logs"][pidx] = nil - end + local x = (buttonW + padding) * lc + if dashx.preferences.general.iconsize ~= 0 then + if dashx.app.gfx_buttons["logs_logs"][pidx] == nil then dashx.app.gfx_buttons["logs_logs"][pidx] = lcd.loadMask("app/modules/logs/gfx/logs.png") end + else + dashx.app.gfx_buttons["logs_logs"][pidx] = nil + end - dashx.app.formFields[pidx] = form.addButton(line, {x = x, y = y, w = buttonW, h = buttonH}, { - text = extractHourMinute(page), - icon = dashx.app.gfx_buttons["logs_logs"][pidx], - options = FONT_S, - paint = function() end, - press = function() - dashx.preferences.menulastselected["logs_logs"] = tostring(idx) .. "_" .. tostring(pidx) - dashx.app.ui.progressDisplay() - dashx.app.ui.openPage(pidx, "Logs", "logs/logs_view.lua", page) - end - }) + dashx.app.formFields[pidx] = form.addButton(line, {x = x, y = y, w = buttonW, h = buttonH}, { + text = extractHourMinute(page), + icon = dashx.app.gfx_buttons["logs_logs"][pidx], + options = FONT_S, + paint = function() end, + press = function() + dashx.preferences.menulastselected["logs_logs"] = tostring(idx) .. "_" .. tostring(pidx) + dashx.app.ui.progressDisplay() + dashx.app.ui.openPage(pidx, "Logs", "logs/logs_view.lua", page) + end + }) - if dashx.preferences.menulastselected["logs_logs"] == tostring(idx) .. "_" .. tostring(pidx) then - dashx.app.formFields[pidx]:focus() - end + if dashx.preferences.menulastselected["logs_logs"] == tostring(idx) .. "_" .. tostring(pidx) then dashx.app.formFields[pidx]:focus() end - if not dashx.tasks or not dashx.tasks.active() then - dashx.app.formFields[pidx]:enable(false) - end + if not dashx.tasks or not dashx.tasks.active() then dashx.app.formFields[pidx]:enable(false) end - lc = (lc + 1) % numPerRow + lc = (lc + 1) % numPerRow - end + end - end + end - end - dashx.app.triggers.closeProgressLoader = true enableWakeup = true @@ -202,39 +181,15 @@ local function openPage(pidx, title, script, displaymode) end local function event(widget, category, value, x, y) - if value == 35 then + if value == 35 then dashx.app.ui.openMainMenu() return true end return false end -local function wakeup() - - if enableWakeup == true then - - end - -end - -local function onNavMenu() - - --dashx.app.ui.openPage(dashx.app.lastIdx, dashx.app.lastTitle, "logs/logs_dir.lua") - dashx.app.ui.openMainMenu() +local function wakeup() if enableWakeup == true then end end -end +local function onNavMenu() dashx.app.ui.openMainMenu() end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - navButtons = { - menu = true, - save = false, - reload = false, - tool = false, - help = true - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, navButtons = {menu = true, save = false, reload = false, tool = false, help = true}, API = {}} diff --git a/scripts/dashx/app/modules/logs/logs_view.lua b/scripts/dashx/app/modules/logs/logs_view.lua index 7758c14..9dad8d1 100644 --- a/scripts/dashx/app/modules/logs/logs_view.lua +++ b/scripts/dashx/app/modules/logs/logs_view.lua @@ -1,5 +1,10 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") --- display vars + local utils = assert(loadfile("SCRIPTS:/" .. dashx.config.baseDir .. "/app/modules/logs/lib/utils.lua"))() local res = system.getVersion() local LCD_W = res.lcdWidth @@ -15,14 +20,12 @@ graphPos['key_width'] = LCD_W - graphPos['width'] graphPos['height'] = LCD_H - graphPos['menu_offset'] - graphPos['menu_offset'] - 40 + graphPos['height_offset'] graphPos['slider_y'] = LCD_H - (graphPos['menu_offset'] + 30) + graphPos['height_offset'] - local zoomLevel = 1 local zoomCount = 5 local enableWakeup = false local activeLogFile local logPadding = 5 - local logFileHandle = nil local logDataRaw = {} local logChunkSize = 1000 @@ -42,44 +45,17 @@ local sliderPositionOld = 1 local processedLogData = false local currentDataIndex = 1 --- Cache for paint data -local paintCache = { - points = {}, - step_size = 0, - position = 1, - graphCount = 0, - laneHeight = 0, - currentLane = 0, - decimationFactor = 1, - needsUpdate = false -} - --- number of samples to skip for each zoom level -local zoomLevelToDecimation = { - [1] = 5, -- Fully zoomed out: - [2] = 4, - [3] = 2, - [4] = 1, - [5] = 1, -- Fully zoomed in: -} - -local zoomLevelToTime = { - [1] = 600, -- 10 minutes - [2] = 300, -- 5 minutes - [3] = 120, -- 2 minutes - [4] = 60, -- 1 minute - [5] = 30, -- 30 seconds -} +local paintCache = {points = {}, step_size = 0, position = 1, graphCount = 0, laneHeight = 0, currentLane = 0, decimationFactor = 1, needsUpdate = false} + +local zoomLevelToDecimation = {[1] = 5, [2] = 4, [3] = 2, [4] = 1, [5] = 1} + +local zoomLevelToTime = {[1] = 600, [2] = 300, [3] = 120, [4] = 60, [5] = 30} local SAMPLE_RATE = 1 -local function secondsToSamples(sec) - return math.floor(sec * SAMPLE_RATE) -end +local function secondsToSamples(sec) return math.floor(sec * SAMPLE_RATE) end local function readNextChunk() - if logDataRawReadComplete then - return - end + if logDataRawReadComplete then return end if not logFileHandle then system.messageBox("Log file handle lost.") @@ -92,113 +68,92 @@ local function readNextChunk() if chunk then table.insert(logDataRaw, chunk) logFileReadOffset = logFileReadOffset + #chunk - dashx.utils.log("Read " .. #chunk .. " bytes from log file","debug") + dashx.utils.log("Read " .. #chunk .. " bytes from log file", "debug") else logFileHandle:close() logFileHandle = nil logDataRawReadComplete = true logDataRaw = table.concat(logDataRaw) - dashx.utils.log("Read complete, total size: " .. #logDataRaw .. " bytes","debug") + dashx.utils.log("Read complete, total size: " .. #logDataRaw .. " bytes", "debug") end end local function format_time(seconds) - -- Calculate minutes and remaining seconds + local minutes = math.floor(seconds / 60) local seconds_remainder = seconds % 60 - -- Format the time string return string.format("%02d:%02d", minutes, seconds_remainder) end local function calculateZoomSteps(logLineCount) - -- Calculate total log duration in seconds (assuming 1 sample/second) + local logDurationSec = logLineCount / SAMPLE_RATE - - -- Determine which zoom levels are feasible + local maxZoomLevel = 1 for level = 5, 1, -1 do local desiredTime = zoomLevelToTime[level] - -- Require at least 1.5x the desired time window to enable a zoom level - -- (so you have some room to pan around) + if logDurationSec >= desiredTime * 1.5 then maxZoomLevel = level break end end - + return maxZoomLevel end - - local function calculateSeconds(totalSeconds, sliderValue) - -- Ensure sliderValue is within the range 1-100 + if sliderValue < 1 or sliderValue > 100 then error("Slider value must be between 1 and 100") end - - local secondsPassed = math.floor(((sliderValue-1) / 100) * totalSeconds) + + local secondsPassed = math.floor(((sliderValue - 1) / 100) * totalSeconds) return secondsPassed end --- Enhanced paginate_table() to support decimation local function paginate_table(data, step_size, position, decimationFactor) - decimationFactor = decimationFactor or 1 + decimationFactor = decimationFactor or 1 - local start_index = math.max(1, position) - local end_index = math.min(start_index + step_size - 1, #data) + local start_index = math.max(1, position) + local end_index = math.min(start_index + step_size - 1, #data) - local page = {} - for i = start_index, end_index, decimationFactor do - table.insert(page, data[i]) - end + local page = {} + for i = start_index, end_index, decimationFactor do table.insert(page, data[i]) end - return page + return page end local function padTable(tbl, padCount) - -- Get the first and last values of the table + local first = tbl[1] local last = tbl[#tbl] - -- Create a new table for the padded result local paddedTable = {} - -- Add the padding elements at the beginning for i = 1, padCount do table.insert(paddedTable, first) end - -- Add the original table elements for _, value in ipairs(tbl) do table.insert(paddedTable, value) end - -- Add the padding elements at the end for i = 1, padCount do table.insert(paddedTable, last) end return paddedTable end +local function map(x, in_min, in_max, out_min, out_max) return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min end -local function map(x, in_min, in_max, out_min, out_max) - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min -end - - --- Efficient function to get a specific column from CSV local function getColumn(csvData, colIndex) local column = {} local start = 1 local len = #csvData while start <= len do - -- Find the position of the next newline + local newlinePos = csvData:find("\n", start) - if not newlinePos then - newlinePos = len + 1 -- End of string - end + if not newlinePos then newlinePos = len + 1 end - -- Extract row data local row = csvData:sub(start, newlinePos - 1) - -- Extract the column by scanning through the row local colStart = 1 local colEnd = 1 local colCount = 0 @@ -216,7 +171,6 @@ local function getColumn(csvData, colIndex) if colEnd == #row + 1 then break end end - -- Move the start position to the next row start = newlinePos + 1 end @@ -225,52 +179,39 @@ end local function cleanColumn(data) local out = {} - for i, v in ipairs(data) do - if i ~= 1 then -- skip the header - out[i - 1] = tonumber(v) - end - end + for i, v in ipairs(data) do if i ~= 1 then out[i - 1] = tonumber(v) end end return out end local function getValueAtPercentage(array, percentage) - -- Ensure percentage is between 0 and 100 + if percentage < 0 or percentage > 100 then error("Percentage must be between 0 and 100") end - -- Calculate the index based on the percentage local arraySize = #array if arraySize == 0 then error("Array cannot be empty") end - -- Calculate the 1-based index local index = math.ceil((percentage / 100) * arraySize) return array[index] end local function extractShortTimestamp(filename) - -- Match the date and time components in the filename, ignoring the prefix + local date, time = filename:match(".-(%d%d%d%d%-%d%d%-%d%d)_(%d%d%-%d%d%-%d%d)") - if date and time then - -- Replace dashes with slashes or colons for a compact format - return date:gsub("%-", "/") .. " " .. time:gsub("%-", ":") - end - return nil -- Return nil if the pattern doesn't match + if date and time then return date:gsub("%-", "/") .. " " .. time:gsub("%-", ":") end + return nil end local function drawGraph(points, color, pen, x_start, y_start, width, height, min_val, max_val) - -- Create a little buffer to prevent graphs coliding with each other - local padding = math.max(5, math.floor(height * 0.1)) -- 5% of height, at least 2 pixel - y_start = y_start + (padding/2) - height = height - padding + local padding = math.max(5, math.floor(height * 0.1)) + y_start = y_start + (padding / 2) + height = height - padding - -- Sanity check: Ensure all points are numbers for i, v in ipairs(points) do if type(v) ~= "number" then error("Point at index " .. i .. " is not a number") end end - -- Use provided min and max values, or calculate from points min_val = min_val or math.min(table.unpack(points)) max_val = max_val or math.max(table.unpack(points)) - -- Handle edge case: If max_val equals min_val, avoid divide-by-zero error if max_val == min_val then max_val = max_val + 1 min_val = min_val - 1 @@ -287,19 +228,16 @@ local function drawGraph(points, color, pen, x_start, y_start, width, height, mi lcd.pen(DOTTED) end - -- Calculate scales to fit the graph within the display area - local x_scale = width / (#points - 1) -- Width spread across the number of points - local y_scale = height / (max_val - min_val) -- Height scaled to the value range + local x_scale = width / (#points - 1) + local y_scale = height / (max_val - min_val) - -- Draw lines between consecutive points for i = 1, #points - 1 do - -- Calculate coordinates for two consecutive points + local x1 = x_start + (i - 1) * x_scale local y1 = y_start + height - (points[i] - min_val) * y_scale local x2 = x_start + i * x_scale local y2 = y_start + height - (points[i + 1] - min_val) * y_scale - -- Draw the line lcd.drawLine(x1, y1, x2, y2) end end @@ -313,7 +251,7 @@ local function drawKey(name, keyunit, keyminmax, keyfloor, color, minimum, maxim local boxHeight = th + boxpadding local x = graphPos['width'] - local y = laneY -- No more shifting, this is the real top of the lane + local y = laneY if keyfloor then minimum = math.floor(minimum) @@ -334,39 +272,33 @@ local function drawKey(name, keyunit, keyminmax, keyfloor, color, minimum, maxim lcd.color(COLOR_BLACK) end - -- shrink rpm if desirable - -- 10000rpm is prob never going to be hit - -- but we are safe! local min_trunc if keyunit == "rpm" and (minimum >= 100000 or maximum >= 1000000) then min_trunc = string.format("%.1fK", minimum / 10000) max_trunc = string.format("%.1fK", maximum / 10000) end - + local max_str local min_str if keyminmax == 1 then - min_str = "↓ " .. (min_trunc or minimum) .. keyunit + min_str = "↓ " .. (min_trunc or minimum) .. keyunit max_str = " ↑ " .. (max_trunc or maximum) .. keyunit else min_str = "" max_str = "↑ " .. (max_trunc or maximum) .. keyunit end - -- left align min value local mmY = y + boxHeight + 2 lcd.drawText(x + 5, mmY, min_str, LEFT) - -- right align max value local tw, th = lcd.getTextSize(max_str) lcd.drawText((LCD_W - tw) + boxpadding, mmY, max_str, LEFT) - -- display average (can only do on bigger radios due to space) if dashx.app.radio.logShowAvg == true then local avg_str = "Ø " .. math.floor((minimum + maximum) / 2) .. keyunit - local avgY = mmY + th -2 + local avgY = mmY + th - 2 lcd.drawText(x + 5, avgY, avg_str, LEFT) - end + end end local function drawCurrentIndex(points, position, totalPoints, keyindex, keyunit, keyfloor, name, color, laneY, laneHeight, laneNumber, totalLanes) @@ -399,12 +331,10 @@ local function drawCurrentIndex(points, position, totalPoints, keyindex, keyunit local tw, th = lcd.getTextSize(value) local boxHeight = th + boxpadding - local boxY = laneY -- Top of the lane - no offset needed + local boxY = laneY local textY = boxY + (boxHeight / 2 - th / 2) - if position > 50 then - boxPos = boxPos - tw - (boxpadding * 2) - end + if position > 50 then boxPos = boxPos - tw - (boxpadding * 2) end lcd.color(color) lcd.drawFilledRectangle(boxPos, boxY, tw + (boxpadding * 2), boxHeight) @@ -418,20 +348,18 @@ local function drawCurrentIndex(points, position, totalPoints, keyindex, keyunit if laneNumber == 1 then local current_s = calculateSeconds(totalPoints, position) - local time_str = format_time(math.floor(current_s)) + local time_str = format_time(math.floor(current_s)) - -- 2) look up our zoom‐window span, capped to real log duration - local logDurSec = math.floor(logLineCount / SAMPLE_RATE) + local logDurSec = math.floor(logLineCount / SAMPLE_RATE) local desiredWinSec = zoomLevelToTime[zoomLevel] or zoomLevelToTime[1] - local windowSec = math.min(desiredWinSec, logDurSec) + local windowSec = math.min(desiredWinSec, logDurSec) local win_label if windowSec < 60 then win_label = string.format("%ds", windowSec) else - win_label = string.format("%d:%02d", math.floor(windowSec/60), windowSec % 60) + win_label = string.format("%d:%02d", math.floor(windowSec / 60), windowSec % 60) end - -- 3) combine into "HH:MM [+SSs]" or "HH:MM [+M:SS]" local full_label = string.format("%s [+%s]", time_str, win_label) lcd.font(dashx.app.radio.logKeyFont) @@ -447,26 +375,22 @@ local function drawCurrentIndex(points, position, totalPoints, keyindex, keyunit end lcd.drawLine(linePos, graphPos['menu_offset'] - 5, linePos, graphPos['menu_offset'] + graphPos['height']) - -- draw zoom level indicator if lcd.darkMode() then lcd.color(lcd.RGB(40, 40, 40)) else lcd.color(lcd.RGB(240, 240, 240)) - end + end local z_x = (LCD_W - 25) local z_y = graphPos['slider_y'] local z_w = 20 local z_h = 40 - local z_lh = z_h/zoomCount - - -- calculate line offset (inverted direction) + local z_lh = z_h / zoomCount + local lineOffsetY = (zoomCount - zoomLevel) * z_lh - - -- draw background + lcd.drawFilledRectangle(z_x, z_y, z_w, z_h) - - -- draw line + if zoomCount > 1 then if lcd.darkMode() then lcd.color(COLOR_WHITE) @@ -474,39 +398,33 @@ local function drawCurrentIndex(points, position, totalPoints, keyindex, keyunit lcd.color(COLOR_BLACK) end else - lcd.color(COLOR_GREY) + lcd.color(COLOR_GREY) end lcd.drawFilledRectangle(z_x, z_y + lineOffsetY, z_w, z_lh) end end local function findMaxNumber(numbers) - local max = numbers[1] -- Assume the first number is the largest initially - for i = 2, #numbers do -- Iterate through the table starting from the second element - if numbers[i] > max then max = numbers[i] end - end + local max = numbers[1] + for i = 2, #numbers do if numbers[i] > max then max = numbers[i] end end return max end local function findMinNumber(numbers) - local min = numbers[1] -- Assume the first number is the smallest initially - for i = 2, #numbers do -- Iterate through the table starting from the second element - if numbers[i] < min then min = numbers[i] end - end + local min = numbers[1] + for i = 2, #numbers do if numbers[i] < min then min = numbers[i] end end return min end local function findAverage(numbers) local sum = 0 - for i = 1, #numbers do -- Iterate through the table - sum = sum + numbers[i] - end - local average = sum / #numbers -- Divide the sum by the number of elements + for i = 1, #numbers do sum = sum + numbers[i] end + local average = sum / #numbers return average end -local function openPage(pidx, title, script, logfile, displaymode,dirname) - +local function openPage(pidx, title, script, logfile, displaymode, dirname) + local err dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.pages @@ -530,16 +448,11 @@ local function openPage(pidx, title, script, logfile, displaymode,dirname) end logFileHandle, err = io.open(filePath, "rb") - -- slider local posField = {x = graphPos['x_start'], y = graphPos['slider_y'], w = graphPos['width'] - 10, h = 40} - dashx.app.formFields[1] = form.addSliderField(nil, posField, 0, 100, function() - return sliderPosition - end, function(newValue) - sliderPosition = newValue - end) + dashx.app.formFields[1] = form.addSliderField(nil, posField, 0, 100, function() return sliderPosition end, function(newValue) sliderPosition = newValue end) local zoomButtonWidth = (graphPos['key_width'] / 2) - 20 - --- zoom - + local posField = {x = graphPos['width'], y = graphPos['slider_y'], w = zoomButtonWidth, h = 40} dashx.app.formFields[2] = form.addButton(line, posField, { text = "-", @@ -552,18 +465,17 @@ local function openPage(pidx, title, script, logfile, displaymode,dirname) lcd.invalidate() dashx.app.formFields[2]:enable(true) dashx.app.formFields[3]:enable(true) - end + end if zoomLevel == 1 then dashx.app.formFields[2]:enable(false) - dashx.app.formFields[3]:focus() + dashx.app.formFields[3]:focus() end end }) - -- disable on start - dashx.app.formFields[2]:enable(false) - --- zoom + - local posField = {x = graphPos['width'] + zoomButtonWidth + 10 , y = graphPos['slider_y'], w = zoomButtonWidth, h = 40} + dashx.app.formFields[2]:enable(false) + + local posField = {x = graphPos['width'] + zoomButtonWidth + 10, y = graphPos['slider_y'], w = zoomButtonWidth, h = 40} dashx.app.formFields[3] = form.addButton(line, posField, { text = "+", icon = nil, @@ -575,14 +487,14 @@ local function openPage(pidx, title, script, logfile, displaymode,dirname) lcd.invalidate() dashx.app.formFields[2]:enable(true) dashx.app.formFields[3]:enable(true) - end + end if zoomLevel == zoomCount then dashx.app.formFields[3]:enable(false) - dashx.app.formFields[2]:focus() - end + dashx.app.formFields[2]:focus() + end end }) - + dashx.app.formFields[1]:step(1) logDataRaw = {} @@ -596,7 +508,7 @@ local function openPage(pidx, title, script, logfile, displaymode,dirname) end local function event(event, category, value, x, y) - if value == 35 then + if value == 35 then dashx.app.ui.openPage(dashx.app.lastIdx, dashx.app.lastTitle, "logs/logs_logs.lua") return true end @@ -609,35 +521,27 @@ local subStepSize = nil local function updatePaintCache() if not logData or not processedLogData then return end - - -- 1) pick window size by time, but cap it to actual log length - local logDurSec = math.floor(logLineCount / SAMPLE_RATE) + + local logDurSec = math.floor(logLineCount / SAMPLE_RATE) local desiredWinSec = zoomLevelToTime[zoomLevel] or zoomLevelToTime[1] - local winSec = math.min(desiredWinSec, logDurSec) + local winSec = math.min(desiredWinSec, logDurSec) paintCache.step_size = secondsToSamples(winSec) - -- 2) slide that window via slider local maxPosition = math.max(1, logLineCount - paintCache.step_size + 1) paintCache.position = math.floor(map(sliderPosition, 1, 100, 1, maxPosition)) if paintCache.position < 1 then paintCache.position = 1 end paintCache.graphCount = 0 - for _, v in ipairs(logData) do - if v.graph then paintCache.graphCount = paintCache.graphCount + 1 end - end + for _, v in ipairs(logData) do if v.graph then paintCache.graphCount = paintCache.graphCount + 1 end end paintCache.laneHeight = graphPos['height'] / paintCache.graphCount paintCache.currentLane = 0 paintCache.decimationFactor = zoomLevelToDecimation[zoomLevel] or 1 - if zoomCount == 1 then - paintCache.decimationFactor = 1 - end + if zoomCount == 1 then paintCache.decimationFactor = 1 end - -- Clear previous points paintCache.points = {} - -- Calculate points for each graph lane for _, v in ipairs(logData) do if v.graph then paintCache.currentLane = paintCache.currentLane + 1 @@ -661,9 +565,7 @@ local function updatePaintCache() end local function wakeup() - if not enableWakeup then - return -- Exit early if wakeup is disabled - end + if not enableWakeup then return end if sliderPosition ~= sliderPositionOld or paintCache.needsUpdate then updatePaintCache() @@ -673,7 +575,7 @@ local function wakeup() if logFileHandle and not logDataRawReadComplete then readNextChunk() - return -- exit early so we don’t start processing until we've got more data + return end if not progressLoader then @@ -688,12 +590,12 @@ local function wakeup() end if logDataRawReadComplete and not processedLogData then - -- Set up carryOver and subStepSize once, when processing starts + if not carriedOver then - -- this needs to be done to set focus or txt radios have issue + dashx.app.formNavigationFields['menu']:focus(true) carriedOver = slowcount - subStepSize = (100 - carriedOver) / (#logColumns * 5) -- 5 subtasks per column + subStepSize = (100 - carriedOver) / (#logColumns * 5) end local function updateProgress(subStep) @@ -712,24 +614,19 @@ local function wakeup() logData[currentDataIndex]['keyfloor'] = logColumns[currentDataIndex].keyfloor logData[currentDataIndex]['graph'] = logColumns[currentDataIndex].graph - -- Step 1: Clean the column data updateProgress(1) local rawColumn = getColumn(logDataRaw, currentDataIndex + 1) local cleanedColumn = cleanColumn(rawColumn) - -- Step 2: Pad the data updateProgress(2) logData[currentDataIndex]['data'] = padTable(cleanedColumn, logPadding) - -- Step 3: Find max value updateProgress(3) logData[currentDataIndex]['maximum'] = findMaxNumber(logData[currentDataIndex]['data']) - -- Step 4: Find min value updateProgress(4) logData[currentDataIndex]['minimum'] = findMinNumber(logData[currentDataIndex]['data']) - -- Step 5: Find average updateProgress(5) logData[currentDataIndex]['average'] = findAverage(logData[currentDataIndex]['data']) @@ -738,18 +635,17 @@ local function wakeup() if currentDataIndex >= #logColumns then logLineCount = #logData[currentDataIndex]['data'] - -- recompute how many zoom‐levels really make sense for this file zoomCount = calculateZoomSteps(logLineCount) if zoomLevel > zoomCount then zoomLevel = zoomCount end - -- update zoom‐button states local btnMinus = dashx.app.formFields[2] - local btnPlus = dashx.app.formFields[3] + local btnPlus = dashx.app.formFields[3] if zoomCount <= 1 then - btnMinus:enable(false); btnPlus:enable(false) + btnMinus:enable(false); + btnPlus:enable(false) else btnMinus:enable(zoomLevel > 1) - btnPlus :enable(zoomLevel < zoomCount) + btnPlus:enable(zoomLevel < zoomCount) end progressLoader:close() @@ -774,7 +670,7 @@ local function paint() if paintCache.points and #paintCache.points > 0 then for laneNumber, laneData in ipairs(paintCache.points) do local laneY = y_start + (laneNumber - 1) * paintCache.laneHeight - + drawGraph(laneData.points, laneData.color, laneData.pen, x_start, laneY, width, paintCache.laneHeight, laneData.minimum, laneData.maximum) drawKey(laneData.keyname, laneData.keyunit, laneData.keyminmax, laneData.keyfloor, laneData.color, laneData.minimum, laneData.maximum, laneY, paintCache.laneHeight) drawCurrentIndex(laneData.points, sliderPosition, logLineCount + logPadding, laneData.keyindex, laneData.keyunit, laneData.keyfloor, laneData.name, laneData.color, laneY, paintCache.laneHeight, laneNumber, paintCache.graphCount) @@ -788,18 +684,4 @@ local function onNavMenu(self) dashx.app.ui.openPage(dashx.app.lastIdx, dashx.app.lastTitle, "logs/logs_logs.lua") end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - paint = paint, - onNavMenu = onNavMenu, - navButtons = { - menu = true, - save = false, - reload = false, - tool = false, - help = true - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, paint = paint, onNavMenu = onNavMenu, navButtons = {menu = true, save = false, reload = false, tool = false, help = true}, API = {}} diff --git a/scripts/dashx/app/modules/model/help.lua b/scripts/dashx/app/modules/model/help.lua index cfada9a..3732cf1 100644 --- a/scripts/dashx/app/modules/model/help.lua +++ b/scripts/dashx/app/modules/model/help.lua @@ -1,30 +1,15 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- + +local dashx = require("dashx") + local data = {} data['help'] = {} -data['help']['default'] = { - --"@i18n(app.modules.accelerometer.help_p1)@" -} +data['help']['default'] = {} data['fields'] = {} diff --git a/scripts/dashx/app/modules/model/init.lua b/scripts/dashx/app/modules/model/init.lua index b465d4a..511b7b2 100644 --- a/scripts/dashx/app/modules/model/init.lua +++ b/scripts/dashx/app/modules/model/init.lua @@ -1,31 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - title = "@i18n(app.modules.model.name)@", -- title of the page - section = "tools", -- do not run if busy with msp - script = "model.lua", -- run this script - image = "model.png", -- image for the page - order = 12, -- order in the section - offline = true, -- run this script offline - ethosversion = {1, 6, 2} -- disable button if ethos version is less than this -} + +local dashx = require("dashx") + +local init = {title = "@i18n(app.modules.model.name)@", section = "tools", script = "model.lua", image = "model.png", order = 12, offline = true, ethosversion = {1, 6, 2}} return init diff --git a/scripts/dashx/app/modules/model/model.lua b/scripts/dashx/app/modules/model/model.lua index b12fb61..60e7f84 100644 --- a/scripts/dashx/app/modules/model/model.lua +++ b/scripts/dashx/app/modules/model/model.lua @@ -1,15 +1,15 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local enableWakeup = false -local S_PAGES = { - {name = "@i18n(app.modules.model.triggers)@", script = "triggers.lua", image = "triggers.png"}, - {name = "@i18n(app.modules.model.battery)@", script = "battery.lua", image = "battery.png"}, -} +local S_PAGES = {{name = "@i18n(app.modules.model.triggers)@", script = "triggers.lua", image = "triggers.png"}, {name = "@i18n(app.modules.model.battery)@", script = "battery.lua", image = "battery.png"}} local function openPage(pidx, title, script) - - dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.mainMenu @@ -19,8 +19,6 @@ local function openPage(pidx, title, script) dashx.app.lastTitle = title dashx.app.lastScript = script - - -- size of buttons if dashx.preferences.general.iconsize == nil or dashx.preferences.general.iconsize == "" then dashx.preferences.general.iconsize = 1 else @@ -44,8 +42,7 @@ local function openPage(pidx, title, script) text = "MENU", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.app.lastIdx = nil dashx.session.lastPage = nil @@ -62,15 +59,13 @@ local function openPage(pidx, title, script) local padding local numPerRow - -- TEXT ICONS - -- TEXT ICONS if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow end - -- SMALL ICONS + if dashx.preferences.general.iconsize == 1 then padding = dashx.app.radio.buttonPaddingSmall @@ -78,7 +73,7 @@ local function openPage(pidx, title, script) buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall end - -- LARGE ICONS + if dashx.preferences.general.iconsize == 2 then padding = dashx.app.radio.buttonPadding @@ -87,19 +82,15 @@ local function openPage(pidx, title, script) numPerRow = dashx.app.radio.buttonsPerRow end - if dashx.app.gfx_buttons["model"] == nil then dashx.app.gfx_buttons["model"] = {} end if dashx.preferences.menulastselected["model"] == nil then dashx.preferences.menulastselected["model"] = 1 end - local Menu = assert(loadfile("app/modules/" .. script))() local pages = S_PAGES local lc = 0 local bx = 0 local y = 0 - - for pidx, pvalue in ipairs(S_PAGES) do if lc == 0 then @@ -120,8 +111,7 @@ local function openPage(pidx, title, script) text = pvalue.name, icon = dashx.app.gfx_buttons["model"][pidx], options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.preferences.menulastselected["model"] = pidx dashx.app.ui.progressDisplay() @@ -132,10 +122,8 @@ local function openPage(pidx, title, script) if pvalue.disabled == true then dashx.app.formFields[pidx]:enable(false) end if dashx.preferences.menulastselected["model"] == pidx then dashx.app.formFields[pidx]:focus() end - - if not dashx.session.isConnected then - dashx.app.formFields[pidx]:enable(false) - end + + if not dashx.session.isConnected then dashx.app.formFields[pidx]:enable(false) end lc = lc + 1 @@ -154,21 +142,11 @@ dashx.app.uiState = dashx.app.uiStatus.pages local function wakeup() if enableWakeup then if dashx.session.isConnected then - for i,v in ipairs(dashx.app.formFields) do - dashx.app.formFields[i]:enable(true) - end + for i, v in ipairs(dashx.app.formFields) do dashx.app.formFields[i]:enable(true) end else - for i,v in ipairs(dashx.app.formFields) do - dashx.app.formFields[i]:enable(false) - end + for i, v in ipairs(dashx.app.formFields) do dashx.app.formFields[i]:enable(false) end end end end - -return { - wakeup = wakeup, - pages = pages, - openPage = openPage, - API = {}, -} +return {wakeup = wakeup, pages = pages, openPage = openPage, API = {}} diff --git a/scripts/dashx/app/modules/model/tools/battery.lua b/scripts/dashx/app/modules/model/tools/battery.lua index fb29b8d..85e88fa 100644 --- a/scripts/dashx/app/modules/model/tools/battery.lua +++ b/scripts/dashx/app/modules/model/tools/battery.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false @@ -7,325 +12,153 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.model.name)@" .. " / " .. "@i18n(app.modules.model.triggers)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.model.name)@" .. " / " .. "@i18n(app.modules.model.triggers)@") local formFieldCount = 0 local formLineCnt = 0 dashx.app.formLines = {} dashx.app.formFields = {} - - -- Fuel sensor formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.calcfuel_using)@") - dashx.app.formFields[formFieldCount] = form.addChoiceField( - dashx.app.formLines[formLineCnt], - nil, - {{"@i18n(app.modules.model.calcfuel_current)@", 0}, {"@i18n(app.modules.model.calcfuel_voltage)@", 1}}, - function() - if dashx.session.modelPreferences then - return dashx.session.modelPreferences.battery.calc_local - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.calc_local = newValue - end - end - ) - + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[formLineCnt], nil, {{"@i18n(app.modules.model.calcfuel_current)@", 0}, {"@i18n(app.modules.model.calcfuel_voltage)@", 1}}, function() + if dashx.session.modelPreferences then return dashx.session.modelPreferences.battery.calc_local end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.calc_local = newValue end end) - -- Battery capacity formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_capacity)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 0, - 10000000, - function() - if dashx.session.modelPreferences and settings then - return dashx.session.modelPreferences.battery.batteryCapacity - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.batteryCapacity = newValue - end - end - ) - dashx.app.formFields[formFieldCount]:suffix("mAh") - dashx.app.formFields[formFieldCount]:default(2200) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 0, 10000000, function() + if dashx.session.modelPreferences and settings then return dashx.session.modelPreferences.battery.batteryCapacity end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.batteryCapacity = newValue end end) + dashx.app.formFields[formFieldCount]:suffix("mAh") + dashx.app.formFields[formFieldCount]:default(2200) - -- Cell count formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_cells)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 1, - 24, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.batteryCellCount - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.batteryCellCount = newValue - end - end - ) - --dashx.app.formFields[formFieldCount]:suffix("mAh") - dashx.app.formFields[formFieldCount]:default(3) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 1, 24, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.batteryCellCount end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.batteryCellCount = newValue end end) + dashx.app.formFields[formFieldCount]:default(3) - -- Warning cell voltage formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_warning_voltage)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 5, - 600, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.vbatwarningcellvoltage - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.vbatwarningcellvoltage = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 5, 600, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.vbatwarningcellvoltage end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.vbatwarningcellvoltage = newValue end end) dashx.app.formFields[formFieldCount]:suffix("v") dashx.app.formFields[formFieldCount]:default(35) dashx.app.formFields[formFieldCount]:decimals(1) - - -- Min cell voltage formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_min_voltage)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 5, - 600, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.vbatmincellvoltage - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.vbatmincellvoltage = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 5, 600, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.vbatmincellvoltage end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.vbatmincellvoltage = newValue end end) dashx.app.formFields[formFieldCount]:suffix("v") dashx.app.formFields[formFieldCount]:default(33) dashx.app.formFields[formFieldCount]:decimals(1) - - -- Min cell voltage formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_max_voltage)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 5, - 600, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.vbatmaxcellvoltage - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.vbatmaxcellvoltage = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 5, 600, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.vbatmaxcellvoltage end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.vbatmaxcellvoltage = newValue end end) dashx.app.formFields[formFieldCount]:suffix("v") dashx.app.formFields[formFieldCount]:default(43) dashx.app.formFields[formFieldCount]:decimals(1) - - -- Full cell voltage formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_full_voltage)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 5, - 600, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.vbatfullcellvoltage - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.vbatfullcellvoltage = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 5, 600, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.vbatfullcellvoltage end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.vbatfullcellvoltage = newValue end end) dashx.app.formFields[formFieldCount]:suffix("v") dashx.app.formFields[formFieldCount]:default(41) dashx.app.formFields[formFieldCount]:decimals(1) - - -- consumptionWarningPercentage formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.battery_consumption_warning_percentage)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 0, - 100, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then - return dashx.session.modelPreferences.battery.consumptionWarningPercentage - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.battery.consumptionWarningPercentage = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 0, 100, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery then return dashx.session.modelPreferences.battery.consumptionWarningPercentage end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.battery.consumptionWarningPercentage = newValue end end) dashx.app.formFields[formFieldCount]:suffix("%") dashx.app.formFields[formFieldCount]:default(30) - - enableWakeup = true - + end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.model.name)@", - "model/model.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.model.name)@", "model/model.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - -- save model dashboard settings if dashx.session.mcu_id and dashx.session.modelPreferencesFile then - for key, value in pairs(settings) do - dashx.session.modelPreferences.battery[key] = value - end - + for key, value in pairs(settings) do dashx.session.modelPreferences.battery[key] = value end - dashx.ini.save_ini_file( - dashx.session.modelPreferencesFile, - dashx.session.modelPreferences - ) + dashx.ini.save_ini_file(dashx.session.modelPreferencesFile, dashx.session.modelPreferences) end dashx.app.triggers.closeSave = true - if dashx.tasks and dashx.tasks.sensors then - dashx.tasks.sensors.reset() - end + if dashx.tasks and dashx.tasks.sensors then dashx.tasks.sensors.reset() end return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if (category == EVT_CLOSE and value == 0) or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.model.name)@", - "model/model.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.model.name)@", "model/model.lua") return true end end - - local function wakeup() if enableWakeup then - if not dashx.tasks.telemetry.getSensorSource("consumption") then - dashx.session.modelPreferences.battery.calc_local = 1 - dashx.app.formFields[1]:enable(false) - end - - if not dashx.session.isConnected then - dashx.app.ui.openMainMenu() - end + if not dashx.tasks.telemetry.getSensorSource("consumption") then + dashx.session.modelPreferences.battery.calc_local = 1 + dashx.app.formFields[1]:enable(false) + end + if not dashx.session.isConnected then dashx.app.ui.openMainMenu() end end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/model/tools/triggers.lua b/scripts/dashx/app/modules/model/tools/triggers.lua index 822ada3..c3f8555 100644 --- a/scripts/dashx/app/modules/model/tools/triggers.lua +++ b/scripts/dashx/app/modules/model/tools/triggers.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local enableWakeup = false @@ -7,192 +12,100 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.model.name)@" .. " / " .. "@i18n(app.modules.model.triggers)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.model.name)@" .. " / " .. "@i18n(app.modules.model.triggers)@") local formFieldCount = 0 local formLineCnt = 0 dashx.app.formLines = {} dashx.app.formFields = {} - - - -- Arm Switch formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.model_armswitch)@") - dashx.app.formFields[formFieldCount] = form.addSwitchField( - dashx.app.formLines[formLineCnt], - nil, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.model.armswitch then - local category, member, options = dashx.session.modelPreferences.model.armswitch:match("([^:]+):([^:]+):([^:]+)") - if category and member then - return system.getSource({category = category, member = member, options = options}) - end - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - local member = newValue:member() - local category = newValue:category() - local options = newValue:options() - dashx.session.modelPreferences.model.armswitch = category .. ":" .. member .. ":" .. options - end + dashx.app.formFields[formFieldCount] = form.addSwitchField(dashx.app.formLines[formLineCnt], nil, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.model.armswitch then + local category, member, options = dashx.session.modelPreferences.model.armswitch:match("([^:]+):([^:]+):([^:]+)") + if category and member then return system.getSource({category = category, member = member, options = options}) end end - ) - + return nil + end, function(newValue) + if dashx.session.modelPreferences then + local member = newValue:member() + local category = newValue:category() + local options = newValue:options() + dashx.session.modelPreferences.model.armswitch = category .. ":" .. member .. ":" .. options + end + end) - -- Inflight Switch formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.model_inflightswitch)@") - dashx.app.formFields[formFieldCount] = form.addSwitchField( - dashx.app.formLines[formLineCnt], - nil, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.model.inflightswitch then - local category, member, options = dashx.session.modelPreferences.model.inflightswitch:match("([^:]+):([^:]+):([^:]+)") - if category and member then - return system.getSource({category = category, member = member, options = options}) - end - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - local member = newValue:member() - local category = newValue:category() - local options = newValue:options() - dashx.session.modelPreferences.model.inflightswitch = category .. ":" .. member .. ":" .. options - end + dashx.app.formFields[formFieldCount] = form.addSwitchField(dashx.app.formLines[formLineCnt], nil, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.model.inflightswitch then + local category, member, options = dashx.session.modelPreferences.model.inflightswitch:match("([^:]+):([^:]+):([^:]+)") + if category and member then return system.getSource({category = category, member = member, options = options}) end end - ) - + return nil + end, function(newValue) + if dashx.session.modelPreferences then + local member = newValue:member() + local category = newValue:category() + local options = newValue:options() + dashx.session.modelPreferences.model.inflightswitch = category .. ":" .. member .. ":" .. options + end + end) - -- Inflight Switch formFieldCount = formFieldCount + 1 formLineCnt = formLineCnt + 1 dashx.app.formLines[formLineCnt] = form.addLine("@i18n(app.modules.model.model_inflightswitch_delay)@") - dashx.app.formFields[formFieldCount] = form.addNumberField( - dashx.app.formLines[formLineCnt], - nil, - 0, - 120, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences.model.inflightswitch_delay then - return dashx.session.modelPreferences.model.inflightswitch_delay - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences then - dashx.session.modelPreferences.model.inflightswitch_delay = newValue - end - end - ) + dashx.app.formFields[formFieldCount] = form.addNumberField(dashx.app.formLines[formLineCnt], nil, 0, 120, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences.model.inflightswitch_delay then return dashx.session.modelPreferences.model.inflightswitch_delay end + return nil + end, function(newValue) if dashx.session.modelPreferences then dashx.session.modelPreferences.model.inflightswitch_delay = newValue end end) dashx.app.formFields[formFieldCount]:suffix("s") dashx.app.formFields[formFieldCount]:default(20) - end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.model.name)@", - "model/model.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.model.name)@", "model/model.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - -- save model dashboard dashx.preferences.model - if dashx.session.mcu_id and dashx.session.modelPreferencesFile then - - dashx.ini.save_ini_file( - dashx.session.modelPreferencesFile, - dashx.session.modelPreferences - ) - end + if dashx.session.mcu_id and dashx.session.modelPreferencesFile then dashx.ini.save_ini_file(dashx.session.modelPreferencesFile, dashx.session.modelPreferences) end dashx.app.triggers.closeSave = true - if dashx.tasks and dashx.tasks.sensors then - dashx.tasks.sensors.reset() - end + if dashx.tasks and dashx.tasks.sensors then dashx.tasks.sensors.reset() end return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_dashx.preferences.model)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_dashx.preferences.model)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if (category == EVT_CLOSE and value == 0) or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.model.name)@", - "model/model.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.model.name)@", "model/model.lua") return true end end +local function wakeup() if enableWakeup then if not dashx.session.isConnected then dashx.app.ui.openMainMenu() end end end -local function wakeup() - if enableWakeup then - - if not dashx.session.isConnected then - dashx.app.ui.openMainMenu() - end - - - end -end - -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/sections.lua b/scripts/dashx/app/modules/sections.lua index 32c8f87..1072c2f 100644 --- a/scripts/dashx/app/modules/sections.lua +++ b/scripts/dashx/app/modules/sections.lua @@ -1,27 +1,12 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local sections = {} - sections[#sections + 1] = {title = "@i18n(app.menu_section_tools)@", id = "tools"} return sections diff --git a/scripts/dashx/app/modules/settings/help.lua b/scripts/dashx/app/modules/settings/help.lua index cfada9a..3732cf1 100644 --- a/scripts/dashx/app/modules/settings/help.lua +++ b/scripts/dashx/app/modules/settings/help.lua @@ -1,30 +1,15 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- + +local dashx = require("dashx") + local data = {} data['help'] = {} -data['help']['default'] = { - --"@i18n(app.modules.accelerometer.help_p1)@" -} +data['help']['default'] = {} data['fields'] = {} diff --git a/scripts/dashx/app/modules/settings/init.lua b/scripts/dashx/app/modules/settings/init.lua index 0a73384..6e5ed40 100644 --- a/scripts/dashx/app/modules/settings/init.lua +++ b/scripts/dashx/app/modules/settings/init.lua @@ -1,31 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - title = "@i18n(app.modules.settings.name)@", -- title of the page - section = "tools", -- do not run if busy with msp - script = "settings.lua", -- run this script - image = "settings.png", -- image for the page - order = 10, -- order in the section - offline = true, -- run this script offline - ethosversion = {1, 6, 2} -- disable button if ethos version is less than this -} + +local dashx = require("dashx") + +local init = {title = "@i18n(app.modules.settings.name)@", section = "tools", script = "settings.lua", image = "settings.png", order = 10, offline = true, ethosversion = {1, 6, 2}} return init diff --git a/scripts/dashx/app/modules/settings/settings.lua b/scripts/dashx/app/modules/settings/settings.lua index 4c8dd40..344126c 100644 --- a/scripts/dashx/app/modules/settings/settings.lua +++ b/scripts/dashx/app/modules/settings/settings.lua @@ -1,18 +1,18 @@ -local dashx = require("dashx") +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local S_PAGES = { - {name = "@i18n(app.modules.settings.txt_general)@", script = "general.lua", image = "general.png"}, - {name = "@i18n(app.modules.settings.dashboard)@", script = "dashboard.lua", image = "dashboard.png"}, - {name = "@i18n(app.modules.settings.localizations)@", script = "localizations.lua", image = "localizations.png"}, - {name = "@i18n(app.modules.settings.audio)@", script = "audio.lua", image = "audio.png"}, - {name = "@i18n(app.modules.settings.txt_development)@", script = "development.lua", image = "development.png"}, + {name = "@i18n(app.modules.settings.txt_general)@", script = "general.lua", image = "general.png"}, {name = "@i18n(app.modules.settings.dashboard)@", script = "dashboard.lua", image = "dashboard.png"}, + {name = "@i18n(app.modules.settings.localizations)@", script = "localizations.lua", image = "localizations.png"}, {name = "@i18n(app.modules.settings.audio)@", script = "audio.lua", image = "audio.png"}, + {name = "@i18n(app.modules.settings.txt_development)@", script = "development.lua", image = "development.png"} } local function openPage(pidx, title, script) - - dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.mainMenu @@ -22,9 +22,6 @@ local function openPage(pidx, title, script) dashx.app.lastTitle = title dashx.app.lastScript = script - - - -- size of buttons if dashx.preferences.general.iconsize == nil or dashx.preferences.general.iconsize == "" then dashx.preferences.general.iconsize = 1 else @@ -48,8 +45,7 @@ local function openPage(pidx, title, script) text = "MENU", icon = nil, options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.app.lastIdx = nil dashx.session.lastPage = nil @@ -66,15 +62,13 @@ local function openPage(pidx, title, script) local padding local numPerRow - -- TEXT ICONS - -- TEXT ICONS if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow end - -- SMALL ICONS + if dashx.preferences.general.iconsize == 1 then padding = dashx.app.radio.buttonPaddingSmall @@ -82,7 +76,7 @@ local function openPage(pidx, title, script) buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall end - -- LARGE ICONS + if dashx.preferences.general.iconsize == 2 then padding = dashx.app.radio.buttonPadding @@ -91,19 +85,15 @@ local function openPage(pidx, title, script) numPerRow = dashx.app.radio.buttonsPerRow end - if dashx.app.gfx_buttons["settings"] == nil then dashx.app.gfx_buttons["settings"] = {} end if dashx.preferences.menulastselected["settings"] == nil then dashx.preferences.menulastselected["settings"] = 1 end - local Menu = assert(loadfile("app/modules/" .. script))() local pages = S_PAGES local lc = 0 local bx = 0 local y = 0 - - for pidx, pvalue in ipairs(S_PAGES) do if lc == 0 then @@ -124,8 +114,7 @@ local function openPage(pidx, title, script) text = pvalue.name, icon = dashx.app.gfx_buttons["settings"][pidx], options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.preferences.menulastselected["settings"] = pidx dashx.app.ui.progressDisplay() @@ -150,8 +139,4 @@ end dashx.app.uiState = dashx.app.uiStatus.pages -return { - pages = pages, - openPage = openPage, - API = {}, -} +return {pages = pages, openPage = openPage, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/audio.lua b/scripts/dashx/app/modules/settings/tools/audio.lua index 35d02a7..db41399 100644 --- a/scripts/dashx/app/modules/settings/tools/audio.lua +++ b/scripts/dashx/app/modules/settings/tools/audio.lua @@ -1,16 +1,14 @@ -local dashx = require("dashx") - - +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") -local S_PAGES = { - {name = "@i18n(app.modules.settings.txt_audio_events)@", script = "audio_events.lua", image = "audio_events.png"}, - {name = "@i18n(app.modules.settings.txt_audio_switches)@", script = "audio_switches.lua", image = "audio_switches.png"}, -} +local S_PAGES = {{name = "@i18n(app.modules.settings.txt_audio_events)@", script = "audio_events.lua", image = "audio_events.png"}, {name = "@i18n(app.modules.settings.txt_audio_switches)@", script = "audio_switches.lua", image = "audio_switches.png"}} local function openPage(pidx, title, script) - dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.mainMenu @@ -20,8 +18,6 @@ local function openPage(pidx, title, script) dashx.app.lastTitle = title dashx.app.lastScript = script - - -- size of buttons if dashx.preferences.general.iconsize == nil or dashx.preferences.general.iconsize == "" then dashx.preferences.general.iconsize = 1 else @@ -36,29 +32,23 @@ local function openPage(pidx, title, script) local sc local panel - - local buttonW = 100 local x = windowWidth - buttonW - 10 - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@") local buttonW local buttonH local padding local numPerRow - -- TEXT ICONS - -- TEXT ICONS if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow end - -- SMALL ICONS + if dashx.preferences.general.iconsize == 1 then padding = dashx.app.radio.buttonPaddingSmall @@ -66,7 +56,7 @@ local function openPage(pidx, title, script) buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall end - -- LARGE ICONS + if dashx.preferences.general.iconsize == 2 then padding = dashx.app.radio.buttonPadding @@ -75,19 +65,15 @@ local function openPage(pidx, title, script) numPerRow = dashx.app.radio.buttonsPerRow end - if dashx.app.gfx_buttons["settings_dashboard"] == nil then dashx.app.gfx_buttons["settings_dashboard"] = {} end if dashx.preferences.menulastselected["settings_dashboard"] == nil then dashx.preferences.menulastselected["settings_dashboard"] = 1 end - local Menu = assert(loadfile("app/modules/" .. script))() local pages = S_PAGES local lc = 0 local bx = 0 local y = 0 - - for pidx, pvalue in ipairs(S_PAGES) do if lc == 0 then @@ -108,8 +94,7 @@ local function openPage(pidx, title, script) text = pvalue.name, icon = dashx.app.gfx_buttons["settings_dashboard"][pidx], options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.preferences.menulastselected["settings_dashboard"] = pidx dashx.app.ui.progressDisplay() @@ -133,41 +118,19 @@ local function openPage(pidx, title, script) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") return true end end - local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) - return true + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") + return true end dashx.app.uiState = dashx.app.uiStatus.pages -return { - pages = pages, - openPage = openPage, - onNavMenu = onNavMenu, - API = {}, - event = event, - navButtons = { - menu = true, - save = false, - reload = false, - tool = false, - help = false, - }, -} +return {pages = pages, openPage = openPage, onNavMenu = onNavMenu, API = {}, event = event, navButtons = {menu = true, save = false, reload = false, tool = false, help = false}} diff --git a/scripts/dashx/app/modules/settings/tools/audio_events.lua b/scripts/dashx/app/modules/settings/tools/audio_events.lua index d30fc17..f44efb2 100644 --- a/scripts/dashx/app/modules/settings/tools/audio_events.lua +++ b/scripts/dashx/app/modules/settings/tools/audio_events.lua @@ -1,12 +1,15 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false local function sensorNameMap(sensorList) local nameMap = {} - for _, sensor in ipairs(sensorList) do - nameMap[sensor.key] = sensor.name - end + for _, sensor in ipairs(sensorList) do nameMap[sensor.key] = sensor.name end return nameMap end @@ -15,13 +18,11 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@" .. " / " .. "@i18n(app.modules.settings.txt_audio_events)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@" .. " / " .. "@i18n(app.modules.settings.txt_audio_events)@") dashx.session.formLineCnt = 0 local formFieldCount = 0 @@ -32,95 +33,44 @@ local function openPage(pageIdx, title, script) settings = dashx.preferences.events for i, v in ipairs(eventList) do - formFieldCount = formFieldCount + 1 - dashx.session.formLineCnt = dashx.session.formLineCnt + 1 - dashx.app.formLines[dashx.session.formLineCnt] = form.addLine(eventNames[v.sensor] or "unknown") - dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], - nil, - function() - if dashx.preferences and dashx.preferences.events then - return settings[v.sensor] - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.events then - settings[v.sensor] = newValue - end - end) + formFieldCount = formFieldCount + 1 + dashx.session.formLineCnt = dashx.session.formLineCnt + 1 + dashx.app.formLines[dashx.session.formLineCnt] = form.addLine(eventNames[v.sensor] or "unknown") + dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], nil, function() if dashx.preferences and dashx.preferences.events then return settings[v.sensor] end end, + function(newValue) if dashx.preferences and dashx.preferences.events then settings[v.sensor] = newValue end end) end - + end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/tools/audio.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/tools/audio.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - for key, value in pairs(settings) do - dashx.preferences.events[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) + for key, value in pairs(settings) do dashx.preferences.events[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/tools/audio.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/tools/audio.lua") return true end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/audio_switches.lua b/scripts/dashx/app/modules/settings/tools/audio_switches.lua index 79077d7..71dbbfc 100644 --- a/scripts/dashx/app/modules/settings/tools/audio_switches.lua +++ b/scripts/dashx/app/modules/settings/tools/audio_switches.lua @@ -1,12 +1,15 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false local function sensorNameMap(sensorList) local nameMap = {} - for _, sensor in ipairs(sensorList) do - nameMap[sensor.key] = sensor.name - end + for _, sensor in ipairs(sensorList) do nameMap[sensor.key] = sensor.name end return nameMap end @@ -15,21 +18,17 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@" .. " / " .. "@i18n(app.modules.settings.txt_audio_switches)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.audio)@" .. " / " .. "@i18n(app.modules.settings.txt_audio_switches)@") dashx.session.formLineCnt = 0 local formFieldCount = 0 local function sortSensorListByName(sensorList) - table.sort(sensorList, function(a, b) - return a.name:lower() < b.name:lower() - end) + table.sort(sensorList, function(a, b) return a.name:lower() < b.name:lower() end) return sensorList end @@ -38,108 +37,63 @@ local function openPage(pageIdx, title, script) settings = dashx.preferences.switches for i, v in ipairs(sensorList) do - formFieldCount = formFieldCount + 1 - dashx.session.formLineCnt = dashx.session.formLineCnt + 1 - dashx.app.formLines[dashx.session.formLineCnt] = form.addLine(v.name or "unknown") - - - dashx.app.formFields[formFieldCount] = form.addSwitchField(dashx.app.formLines[dashx.session.formLineCnt], - nil, - function() - if dashx.preferences and dashx.preferences.switches then - local value = settings[v.key] - if value then - local scategory, smember = value:match("([^,]+),([^,]+)") - if scategory and smember then - local source = system.getSource({ category = tonumber(scategory), member = tonumber(smember) }) - return source - end - end - return nil - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.switches then - local cat_member = newValue:category() .. "," .. newValue:member() - settings[v.key] = cat_member or nil - end - end) + formFieldCount = formFieldCount + 1 + dashx.session.formLineCnt = dashx.session.formLineCnt + 1 + dashx.app.formLines[dashx.session.formLineCnt] = form.addLine(v.name or "unknown") + + dashx.app.formFields[formFieldCount] = form.addSwitchField(dashx.app.formLines[dashx.session.formLineCnt], nil, function() + if dashx.preferences and dashx.preferences.switches then + local value = settings[v.key] + if value then + local scategory, smember = value:match("([^,]+),([^,]+)") + if scategory and smember then + local source = system.getSource({category = tonumber(scategory), member = tonumber(smember)}) + return source + end + end + return nil + end + end, function(newValue) + if dashx.preferences and dashx.preferences.switches then + local cat_member = newValue:category() .. "," .. newValue:member() + settings[v.key] = cat_member or nil + end + end) end - + end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/tools/audio.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/tools/audio.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - for key, value in pairs(settings) do - dashx.preferences.switches[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) + for key, value in pairs(settings) do dashx.preferences.switches[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) dashx.tasks.events.switches.resetSwitchStates() dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/tools/audio.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/tools/audio.lua") return true end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/dashboard.lua b/scripts/dashx/app/modules/settings/tools/dashboard.lua index 016641c..4e2ef2c 100644 --- a/scripts/dashx/app/modules/settings/tools/dashboard.lua +++ b/scripts/dashx/app/modules/settings/tools/dashboard.lua @@ -1,16 +1,14 @@ -local dashx = require("dashx") - +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") -local S_PAGES = { - {name = "@i18n(app.modules.settings.dashboard_theme)@", script = "dashboard_theme.lua", image = "dashboard_theme.png"}, - {name = "@i18n(app.modules.settings.dashboard_settings)@", script = "dashboard_settings.lua", image = "dashboard_settings.png"}, -} +local S_PAGES = {{name = "@i18n(app.modules.settings.dashboard_theme)@", script = "dashboard_theme.lua", image = "dashboard_theme.png"}, {name = "@i18n(app.modules.settings.dashboard_settings)@", script = "dashboard_settings.lua", image = "dashboard_settings.png"}} local function openPage(pidx, title, script) - - dashx.app.triggers.isReady = false dashx.app.uiState = dashx.app.uiStatus.mainMenu @@ -20,8 +18,6 @@ local function openPage(pidx, title, script) dashx.app.lastTitle = title dashx.app.lastScript = script - - -- size of buttons if dashx.preferences.general.iconsize == nil or dashx.preferences.general.iconsize == "" then dashx.preferences.general.iconsize = 1 else @@ -36,29 +32,23 @@ local function openPage(pidx, title, script) local sc local panel - local buttonW = 100 local x = windowWidth - buttonW - 10 - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" - ) - + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@") local buttonW local buttonH local padding local numPerRow - -- TEXT ICONS - -- TEXT ICONS if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall buttonW = (dashx.session.lcdWidth - padding) / dashx.app.radio.buttonsPerRow - padding buttonH = dashx.app.radio.navbuttonHeight numPerRow = dashx.app.radio.buttonsPerRow end - -- SMALL ICONS + if dashx.preferences.general.iconsize == 1 then padding = dashx.app.radio.buttonPaddingSmall @@ -66,7 +56,7 @@ local function openPage(pidx, title, script) buttonH = dashx.app.radio.buttonHeightSmall numPerRow = dashx.app.radio.buttonsPerRowSmall end - -- LARGE ICONS + if dashx.preferences.general.iconsize == 2 then padding = dashx.app.radio.buttonPadding @@ -75,19 +65,15 @@ local function openPage(pidx, title, script) numPerRow = dashx.app.radio.buttonsPerRow end - if dashx.app.gfx_buttons["settings_dashboard"] == nil then dashx.app.gfx_buttons["settings_dashboard"] = {} end if dashx.preferences.menulastselected["settings_dashboard"] == nil then dashx.preferences.menulastselected["settings_dashboard"] = 1 end - local Menu = assert(loadfile("app/modules/" .. script))() local pages = S_PAGES local lc = 0 local bx = 0 local y = 0 - - for pidx, pvalue in ipairs(S_PAGES) do if lc == 0 then @@ -108,8 +94,7 @@ local function openPage(pidx, title, script) text = pvalue.name, icon = dashx.app.gfx_buttons["settings_dashboard"][pidx], options = FONT_S, - paint = function() - end, + paint = function() end, press = function() dashx.preferences.menulastselected["settings_dashboard"] = pidx dashx.app.ui.progressDisplay() @@ -133,41 +118,19 @@ local function openPage(pidx, title, script) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") return true end end - local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) - return true + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") + return true end dashx.app.uiState = dashx.app.uiStatus.pages -return { - pages = pages, - openPage = openPage, - onNavMenu = onNavMenu, - event = event, - API = {}, - navButtons = { - menu = true, - save = false, - reload = false, - tool = false, - help = false, - }, -} +return {pages = pages, openPage = openPage, onNavMenu = onNavMenu, event = event, API = {}, navButtons = {menu = true, save = false, reload = false, tool = false, help = false}} diff --git a/scripts/dashx/app/modules/settings/tools/dashboard_settings.lua b/scripts/dashx/app/modules/settings/tools/dashboard_settings.lua index 85bb082..e4c194e 100644 --- a/scripts/dashx/app/modules/settings/tools/dashboard_settings.lua +++ b/scripts/dashx/app/modules/settings/tools/dashboard_settings.lua @@ -1,12 +1,16 @@ -local dashx = require("dashx") +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local themesBasePath = "SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/themes/" local themesUserPath = "SCRIPTS:/" .. dashx.config.preferences .. "/dashboard/" local enableWakeup = false local function openPage(pidx, title, script) - -- Get the installed themes + local themeList = dashx.widgets.dashboard.listThemes() dashx.session.dashboardEditingTheme = nil @@ -14,16 +18,12 @@ local function openPage(pidx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.dashboard_settings)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.dashboard_settings)@") - -- Icon/button layout settings local buttonW, buttonH, padding, numPerRow if dashx.preferences.general.iconsize == 0 then padding = dashx.app.radio.buttonPaddingSmall @@ -42,16 +42,11 @@ local function openPage(pidx, title, script) numPerRow = dashx.app.radio.buttonsPerRow end - -- Image cache table for theme icons - if dashx.app.gfx_buttons["settings_dashboard_themes"] == nil then - dashx.app.gfx_buttons["settings_dashboard_themes"] = {} - end - if dashx.preferences.menulastselected["settings_dashboard_themes"] == nil then - dashx.preferences.menulastselected["settings_dashboard_themes"] = 1 - end + if dashx.app.gfx_buttons["settings_dashboard_themes"] == nil then dashx.app.gfx_buttons["settings_dashboard_themes"] = {} end + if dashx.preferences.menulastselected["settings_dashboard_themes"] == nil then dashx.preferences.menulastselected["settings_dashboard_themes"] = 1 end local lc, bx, y = 0, 0, 0 - + for idx, theme in ipairs(themeList) do if theme.configure then @@ -63,15 +58,14 @@ local function openPage(pidx, title, script) end if lc >= 0 then bx = (buttonW + padding) * lc end - -- Only load image once per theme index if dashx.app.gfx_buttons["settings_dashboard_themes"][idx] == nil then - local icon + local icon if theme.source == "system" then icon = themesBasePath .. theme.folder .. "/icon.png" - else + else icon = themesUserPath .. theme.folder .. "/icon.png" - end + end dashx.app.gfx_buttons["settings_dashboard_themes"][idx] = lcd.loadMask(icon) end @@ -81,36 +75,31 @@ local function openPage(pidx, title, script) options = FONT_S, paint = function() end, press = function() - -- Optional: your action when pressing a theme - -- Example: dashx.app.ui.loadTheme(theme.folder) + dashx.preferences.menulastselected["settings_dashboard_themes"] = idx - dashx.app.ui.progressDisplay() + dashx.app.ui.progressDisplay() local configure = theme.configure local source = theme.source local folder = theme.folder local themeScript if theme.source == "system" then - themeScript = themesBasePath .. folder .. "/" .. configure - else - themeScript = themesUserPath .. folder .. "/" .. configure - end + themeScript = themesBasePath .. folder .. "/" .. configure + else + themeScript = themesUserPath .. folder .. "/" .. configure + end - dashx.app.ui.openPageDashboard(idx, theme.name,themeScript, source, folder) + dashx.app.ui.openPageDashboard(idx, theme.name, themeScript, source, folder) end }) - if not theme.configure then - dashx.app.formFields[idx]:enable(false) - end + if not theme.configure then dashx.app.formFields[idx]:enable(false) end - if dashx.preferences.menulastselected["settings_dashboard_themes"] == idx then - dashx.app.formFields[idx]:focus() - end + if dashx.preferences.menulastselected["settings_dashboard_themes"] == idx then dashx.app.formFields[idx]:focus() end lc = lc + 1 if lc == numPerRow then lc = 0 end - end + end end if lc == 0 then @@ -120,7 +109,7 @@ local function openPage(pidx, title, script) local x = w / 2 - tw / 2 local y = h / 2 - th / 2 local btnH = dashx.app.radio.navbuttonHeight - form.addStaticText(nil, { x = x, y = y, w = tw, h = btnH }, msg) + form.addStaticText(nil, {x = x, y = y, w = tw, h = btnH}, msg) end dashx.app.triggers.closeProgressLoader = true @@ -128,42 +117,20 @@ local function openPage(pidx, title, script) return end - dashx.app.uiState = dashx.app.uiStatus.pages local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.dashboard)@", - "settings/tools/dashboard.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.dashboard)@", "settings/tools/dashboard.lua") return true end end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.dashboard)@", - "settings/tools/dashboard.lua" - ) - return true + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.dashboard)@", "settings/tools/dashboard.lua") + return true end -return { - pages = pages, - openPage = openPage, - API = {}, - navButtons = { - menu = true, - save = false, - reload = false, - tool = false, - help = false, - }, - event = event, - onNavMenu = onNavMenu, -} +return {pages = pages, openPage = openPage, API = {}, navButtons = {menu = true, save = false, reload = false, tool = false, help = false}, event = event, onNavMenu = onNavMenu} diff --git a/scripts/dashx/app/modules/settings/tools/dashboard_theme.lua b/scripts/dashx/app/modules/settings/tools/dashboard_theme.lua index a256836..acade2f 100644 --- a/scripts/dashx/app/modules/settings/tools/dashboard_theme.lua +++ b/scripts/dashx/app/modules/settings/tools/dashboard_theme.lua @@ -1,22 +1,21 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local settings_model = {} -local themeList = dashx.widgets.dashboard.listThemes() +local themeList = dashx.widgets.dashboard.listThemes() local formattedThemes = {} local formattedThemesModel = {} local enableWakeup = false local prevConnectedState = nil ---- Generates formatted lists of available themes and their models. --- Iterates over the global `themeList` and populates two tables: --- `formattedThemes` with theme names and indices, and --- `formattedThemesModel` with a disabled option followed by theme names and indices. --- Assumes `themeList`, `formattedThemes`, `formattedThemesModel`, and `dashx.i18n` are defined in the surrounding scope. local function generateThemeList() - -- setup environment settings = dashx.preferences.dashboard if dashx.session.modelPreferences then @@ -25,16 +24,10 @@ local function generateThemeList() settings_model = {} end - -- build global table - for i, theme in ipairs(themeList) do - table.insert(formattedThemes, { theme.name, theme.idx }) - end + for i, theme in ipairs(themeList) do table.insert(formattedThemes, {theme.name, theme.idx}) end - -- build model table - table.insert(formattedThemesModel, { "@i18n(app.modules.settings.dashboard_theme_panel_model_disabled)@", 0 }) - for i, theme in ipairs(themeList) do - table.insert(formattedThemesModel, { theme.name, theme.idx }) - end + table.insert(formattedThemesModel, {"@i18n(app.modules.settings.dashboard_theme_panel_model_disabled)@", 0}) + for i, theme in ipairs(themeList) do table.insert(formattedThemesModel, {theme.name, theme.idx}) end end local function openPage(pageIdx, title, script) @@ -42,325 +35,199 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.dashboard_theme)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.dashboard_theme)@") dashx.app.formLineCnt = 0 local formFieldCount = 0 - -- generate the initial list generateThemeList() - -- =========================================================================== - -- create global theme selection panel - -- =========================================================================== local global_panel = form.addExpansionPanel("@i18n(app.modules.settings.dashboard_theme_panel_global)@") - global_panel:open(true) + global_panel:open(true) - -- preflight theme selection formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = global_panel:addLine("@i18n(app.modules.settings.dashboard_theme_preflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemes, - function() - if dashx.preferences and dashx.preferences.dashboard then - local folderName = settings.theme_preflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.preferences and dashx.preferences.dashboard then - local theme = themeList[newValue] - if theme then - settings.theme_preflight = theme.source .. "/" .. theme.folder - end - end - end) - - -- inflight theme selection + + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemes, function() + if dashx.preferences and dashx.preferences.dashboard then + local folderName = settings.theme_preflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.preferences and dashx.preferences.dashboard then + local theme = themeList[newValue] + if theme then settings.theme_preflight = theme.source .. "/" .. theme.folder end + end + end) + formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = global_panel:addLine("@i18n(app.modules.settings.dashboard_theme_inflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemes, - function() - if dashx.preferences and dashx.preferences.dashboard then - local folderName = settings.theme_inflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.preferences and dashx.preferences.dashboard then - local theme = themeList[newValue] - if theme then - settings.theme_inflight = theme.source .. "/" .. theme.folder - end - end - end) - - - -- postflight theme selection + + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemes, function() + if dashx.preferences and dashx.preferences.dashboard then + local folderName = settings.theme_inflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.preferences and dashx.preferences.dashboard then + local theme = themeList[newValue] + if theme then settings.theme_inflight = theme.source .. "/" .. theme.folder end + end + end) + formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = global_panel:addLine("@i18n(app.modules.settings.dashboard_theme_postflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemes, - function() - if dashx.preferences and dashx.preferences.dashboard then - local folderName = settings.theme_postflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.preferences and dashx.preferences.dashboard then - local theme = themeList[newValue] - if theme then - settings.theme_postflight = theme.source .. "/" .. theme.folder - end - end - end) - - -- =========================================================================== - -- create model theme selection panel - -- =========================================================================== - local model_panel = form.addExpansionPanel("@i18n(app.modules.settings.dashboard_theme_panel_model)@") - model_panel:open(false) + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemes, function() + if dashx.preferences and dashx.preferences.dashboard then + local folderName = settings.theme_postflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.preferences and dashx.preferences.dashboard then + local theme = themeList[newValue] + if theme then settings.theme_postflight = theme.source .. "/" .. theme.folder end + end + end) + + local model_panel = form.addExpansionPanel("@i18n(app.modules.settings.dashboard_theme_panel_model)@") + model_panel:open(false) - -- preflight theme selection formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = model_panel:addLine("@i18n(app.modules.settings.dashboard_theme_preflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemesModel, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences then - local folderName = settings_model.theme_preflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences and dashx.session.modelPreferences then - local theme = themeList[newValue] - if theme then - settings_model.theme_preflight = theme.source .. "/" .. theme.folder - else - settings_model.theme_preflight = "nil" - end - end - end) - dashx.app.formFields[formFieldCount]:enable(false) - - -- inflight theme selection + + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemesModel, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences then + local folderName = settings_model.theme_preflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.session.modelPreferences and dashx.session.modelPreferences then + local theme = themeList[newValue] + if theme then + settings_model.theme_preflight = theme.source .. "/" .. theme.folder + else + settings_model.theme_preflight = "nil" + end + end + end) + dashx.app.formFields[formFieldCount]:enable(false) + formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = model_panel:addLine("@i18n(app.modules.settings.dashboard_theme_inflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemesModel, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences then - local folderName = settings_model.theme_inflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.session.modelPreferences and dashx.session.modelPreferences then - local theme = themeList[newValue] - if theme then - settings_model.theme_inflight = theme.source .. "/" .. theme.folder - else - settings_model.theme_inflight = "nil" - end - end - end) - dashx.app.formFields[formFieldCount]:enable(false) - - -- postflight theme selection + + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemesModel, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences then + local folderName = settings_model.theme_inflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.session.modelPreferences and dashx.session.modelPreferences then + local theme = themeList[newValue] + if theme then + settings_model.theme_inflight = theme.source .. "/" .. theme.folder + else + settings_model.theme_inflight = "nil" + end + end + end) + dashx.app.formFields[formFieldCount]:enable(false) + formFieldCount = formFieldCount + 1 dashx.app.formLineCnt = dashx.app.formLineCnt + 1 dashx.app.formLines[dashx.app.formLineCnt] = model_panel:addLine("@i18n(app.modules.settings.dashboard_theme_postflight)@") - - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, - formattedThemesModel, - function() - if dashx.session.modelPreferences and dashx.session.modelPreferences then - local folderName = settings_model.theme_postflight - for _, theme in ipairs(themeList) do - if (theme.source .. "/" .. theme.folder) == folderName then - return theme.idx - end - end - end - return nil - end, - function(newValue) - if dashx.preferences and dashx.preferences.dashboard then - local theme = themeList[newValue] - if theme then - settings_model.theme_postflight = theme.source .. "/" .. theme.folder - else - settings_model.theme_postflight = "nil" - end - end - end) - dashx.app.formFields[formFieldCount]:enable(false) - + + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.app.formLineCnt], nil, formattedThemesModel, function() + if dashx.session.modelPreferences and dashx.session.modelPreferences then + local folderName = settings_model.theme_postflight + for _, theme in ipairs(themeList) do if (theme.source .. "/" .. theme.folder) == folderName then return theme.idx end end + end + return nil + end, function(newValue) + if dashx.preferences and dashx.preferences.dashboard then + local theme = themeList[newValue] + if theme then + settings_model.theme_postflight = theme.source .. "/" .. theme.folder + else + settings_model.theme_postflight = "nil" + end + end + end) + dashx.app.formFields[formFieldCount]:enable(false) + end local function onNavMenu() - dashx.app.ui.progressDisplay(nil,nil,true) - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.dashboard)@", - "settings/tools/dashboard.lua" - ) - return true + dashx.app.ui.progressDisplay(nil, nil, true) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.dashboard)@", "settings/tools/dashboard.lua") + return true end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - -- save global dashboard settings - for key, value in pairs(settings) do - dashx.preferences.dashboard[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) + for key, value in pairs(settings) do dashx.preferences.dashboard[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) - -- save model dashboard settings if dashx.session.isConnected and dashx.session.mcu_id and dashx.session.modelPreferencesFile then - for key, value in pairs(settings_model) do - dashx.session.modelPreferences.dashboard[key] = value - end - dashx.ini.save_ini_file( - dashx.session.modelPreferencesFile, - dashx.session.modelPreferences - ) - end - - - -- update dashboard theme - dashx.widgets.dashboard.reload_themes(true) -- send true to force full reload - -- close save progress + for key, value in pairs(settings_model) do dashx.session.modelPreferences.dashboard[key] = value end + dashx.ini.save_ini_file(dashx.session.modelPreferencesFile, dashx.session.modelPreferences) + end + + dashx.widgets.dashboard.reload_themes(true) + dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.dashboard)@", - "settings/tools/dashboard.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.dashboard)@", "settings/tools/dashboard.lua") return true end end local function wakeup() - if not enableWakeup then - return - end + if not enableWakeup then return end - -- current combined state: true only if both are truthy local currState = (dashx.session.isConnected and dashx.session.mcu_id) and true or false - -- only update if state has changed if currState ~= prevConnectedState then - -- if we're now connected, you can do any repopulation here if currState then - generateThemeList() - for i = 4, 6 do - dashx.app.formFields[i]:values(formattedThemesModel) - end + generateThemeList() + for i = 4, 6 do dashx.app.formFields[i]:values(formattedThemesModel) end end - -- toggle all three fields together - for i = 4, 6 do - dashx.app.formFields[i]:enable(currState) - end + for i = 4, 6 do dashx.app.formFields[i]:enable(currState) end - -- remember for next time prevConnectedState = currState end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/development.lua b/scripts/dashx/app/modules/settings/tools/development.lua index f2d4b78..8ebb330 100644 --- a/scripts/dashx/app/modules/settings/tools/development.lua +++ b/scripts/dashx/app/modules/settings/tools/development.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false @@ -7,187 +12,114 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.txt_development)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.txt_development)@") dashx.session.formLineCnt = 0 local formFieldCount = 0 settings = dashx.preferences.developer -formFieldCount = formFieldCount + 1 + formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = form.addLine("@i18n(app.modules.settings.txt_devtools)@") - dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], - nil, - function() - if dashx.preferences and dashx.preferences.developer then - return settings['devtools'] - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.developer then - settings.devtools = newValue - end - end) - - + dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], nil, function() if dashx.preferences and dashx.preferences.developer then return settings['devtools'] end end, + function(newValue) if dashx.preferences and dashx.preferences.developer then settings.devtools = newValue end end) local logpanel = form.addExpansionPanel("@i18n(app.modules.settings.txt_logging)@") - logpanel:open(false) + logpanel:open(false) formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = logpanel:addLine("@i18n(app.modules.settings.txt_loglocation)@") - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, - {{"@i18n(app.modules.settings.txt_console)@", 0}, {"@i18n(app.modules.settings.txt_consolefile)@", 1}}, - function() - if dashx.preferences and dashx.preferences.developer then - if dashx.preferences.developer.logtofile == false then - return 0 - else - return 1 - end - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.developer then - local value - if newValue == 0 then - value = false - else - value = true - end - settings.logtofile = value - end - end) + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, {{"@i18n(app.modules.settings.txt_console)@", 0}, {"@i18n(app.modules.settings.txt_consolefile)@", 1}}, function() + if dashx.preferences and dashx.preferences.developer then + if dashx.preferences.developer.logtofile == false then + return 0 + else + return 1 + end + end + end, function(newValue) + if dashx.preferences and dashx.preferences.developer then + local value + if newValue == 0 then + value = false + else + value = true + end + settings.logtofile = value + end + end) formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = logpanel:addLine("@i18n(app.modules.settings.txt_loglevel)@") - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, - {{"@i18n(app.modules.settings.txt_off)@", 0}, {"@i18n(app.modules.settings.txt_info)@", 1}, {"@i18n(app.modules.settings.txt_debug)@", 2}}, - function() - if dashx.preferences and dashx.preferences.developer then - if settings['loglevel'] == "off" then - return 0 - elseif settings['loglevel'] == "info" then - return 1 - else - return 2 - end - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.developer then - local value - if newValue == 0 then - value = "off" - elseif newValue == 1 then - value = "info" - else - value = "debug" - end - settings['loglevel'] = value - end - end) - + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, {{"@i18n(app.modules.settings.txt_off)@", 0}, {"@i18n(app.modules.settings.txt_info)@", 1}, {"@i18n(app.modules.settings.txt_debug)@", 2}}, function() + if dashx.preferences and dashx.preferences.developer then + if settings['loglevel'] == "off" then + return 0 + elseif settings['loglevel'] == "info" then + return 1 + else + return 2 + end + end + end, function(newValue) + if dashx.preferences and dashx.preferences.developer then + local value + if newValue == 0 then + value = "off" + elseif newValue == 1 then + value = "info" + else + value = "debug" + end + settings['loglevel'] = value + end + end) formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = logpanel:addLine("@i18n(app.modules.settings.txt_memusage)@") - dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], - nil, - function() - if dashx.preferences and dashx.preferences.developer then - return settings['memstats'] - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.developer then - settings.memstats = newValue - end - end) - - + dashx.app.formFields[formFieldCount] = form.addBooleanField(dashx.app.formLines[dashx.session.formLineCnt], nil, function() if dashx.preferences and dashx.preferences.developer then return settings['memstats'] end end, + function(newValue) if dashx.preferences and dashx.preferences.developer then settings.memstats = newValue end end) + end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - for key, value in pairs(settings) do - dashx.preferences.developer[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) - + for key, value in pairs(settings) do dashx.preferences.developer[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) + dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") return true end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/general.lua b/scripts/dashx/app/modules/settings/tools/general.lua index 6232a5e..f4ae985 100644 --- a/scripts/dashx/app/modules/settings/tools/general.lua +++ b/scripts/dashx/app/modules/settings/tools/general.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false @@ -7,120 +12,59 @@ local function openPage(pageIdx, title, script) dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.txt_general)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.txt_general)@") dashx.session.formLineCnt = 0 local formFieldCount = 0 settings = dashx.preferences.general - -- Icon size choice field formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 - dashx.app.formLines[dashx.session.formLineCnt] = form.addLine( - "@i18n(app.modules.settings.txt_iconsize)@" - ) - dashx.app.formFields[formFieldCount] = form.addChoiceField( - dashx.app.formLines[dashx.session.formLineCnt], - nil, - { - { "@i18n(app.modules.settings.txt_text)@", 0 }, - { "@i18n(app.modules.settings.txt_small)@", 1 }, - { "@i18n(app.modules.settings.txt_large)@", 2 }, - }, - function() - if dashx.preferences and dashx.preferences.general and dashx.preferences.general.iconsize then - return settings.iconsize - else - return 1 - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.general then - settings.iconsize = newValue - end + dashx.app.formLines[dashx.session.formLineCnt] = form.addLine("@i18n(app.modules.settings.txt_iconsize)@") + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, {{"@i18n(app.modules.settings.txt_text)@", 0}, {"@i18n(app.modules.settings.txt_small)@", 1}, {"@i18n(app.modules.settings.txt_large)@", 2}}, function() + if dashx.preferences and dashx.preferences.general and dashx.preferences.general.iconsize then + return settings.iconsize + else + return 1 end - ) - + end, function(newValue) if dashx.preferences and dashx.preferences.general then settings.iconsize = newValue end end) end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - for key, value in pairs(settings) do - dashx.preferences.general[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) + for key, value in pairs(settings) do dashx.preferences.general[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") return true end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/modules/settings/tools/localizations.lua b/scripts/dashx/app/modules/settings/tools/localizations.lua index d4d33e4..012f7d7 100644 --- a/scripts/dashx/app/modules/settings/tools/localizations.lua +++ b/scripts/dashx/app/modules/settings/tools/localizations.lua @@ -1,20 +1,22 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local settings = {} local enableWakeup = false - local function openPage(pageIdx, title, script) enableWakeup = true dashx.app.triggers.closeProgressLoader = true form.clear() - dashx.app.lastIdx = pageIdx - dashx.app.lastTitle = title + dashx.app.lastIdx = pageIdx + dashx.app.lastTitle = title dashx.app.lastScript = script - dashx.app.ui.fieldHeader( - "@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.localizations)@" - ) + dashx.app.ui.fieldHeader("@i18n(app.modules.settings.name)@" .. " / " .. "@i18n(app.modules.settings.dashboard)@" .. " / " .. "@i18n(app.modules.settings.localizations)@") dashx.session.formLineCnt = 0 local formFieldCount = 0 @@ -24,112 +26,50 @@ local function openPage(pageIdx, title, script) formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = form.addLine("@i18n(app.modules.settings.temperature_unit)@") - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, - {{"@i18n(app.modules.settings.celcius)@", 0}, {"@i18n(app.modules.settings.fahrenheit)@", 1}}, - function() - if dashx.preferences and dashx.preferences.localizations then - return settings.temperature_unit or 0 - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.localizations then - settings.temperature_unit = newValue - end - end) - + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, {{"@i18n(app.modules.settings.celcius)@", 0}, {"@i18n(app.modules.settings.fahrenheit)@", 1}}, + function() if dashx.preferences and dashx.preferences.localizations then return settings.temperature_unit or 0 end end, function(newValue) if dashx.preferences and dashx.preferences.localizations then settings.temperature_unit = newValue end end) + formFieldCount = formFieldCount + 1 dashx.session.formLineCnt = dashx.session.formLineCnt + 1 dashx.app.formLines[dashx.session.formLineCnt] = form.addLine("@i18n(app.modules.settings.altitude_unit)@") - dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, - {{"@i18n(app.modules.settings.meters)@", 0}, {"@i18n(app.modules.settings.feet)@", 1}}, - function() - if dashx.preferences and dashx.preferences.localizations then - return settings.altitude_unit or 0 - end - end, - function(newValue) - if dashx.preferences and dashx.preferences.localizations then - settings.altitude_unit = newValue - end - end) - - + dashx.app.formFields[formFieldCount] = form.addChoiceField(dashx.app.formLines[dashx.session.formLineCnt], nil, {{"@i18n(app.modules.settings.meters)@", 0}, {"@i18n(app.modules.settings.feet)@", 1}}, + function() if dashx.preferences and dashx.preferences.localizations then return settings.altitude_unit or 0 end end, function(newValue) if dashx.preferences and dashx.preferences.localizations then settings.altitude_unit = newValue end end) + end local function onNavMenu() dashx.app.ui.progressDisplay() - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) - return true + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") + return true end local function onSaveMenu() local buttons = { { - label = "@i18n(app.btn_ok_long)@", + label = "@i18n(app.btn_ok_long)@", action = function() local msg = "@i18n(app.modules.profile_select.save_prompt_local)@" dashx.app.ui.progressDisplaySave(msg:gsub("%?$", ".")) - for key, value in pairs(settings) do - dashx.preferences.dashboard[key] = value - end - dashx.ini.save_ini_file( - "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", - dashx.preferences - ) - -- update dashboard theme + for key, value in pairs(settings) do dashx.preferences.dashboard[key] = value end + dashx.ini.save_ini_file("SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini", dashx.preferences) + dashx.widgets.dashboard.reload_themes() - -- close save progress + dashx.app.triggers.closeSave = true return true - end, - }, - { - label = "@i18n(app.modules.profile_select.cancel)@", - action = function() - return true - end, - }, + end + }, {label = "@i18n(app.modules.profile_select.cancel)@", action = function() return true end} } - form.openDialog({ - width = nil, - title = "@i18n(app.modules.profile_select.save_settings)@", - message = "@i18n(app.modules.profile_select.save_prompt_local)@", - buttons = buttons, - wakeup = function() end, - paint = function() end, - options = TEXT_LEFT, - }) + form.openDialog({width = nil, title = "@i18n(app.modules.profile_select.save_settings)@", message = "@i18n(app.modules.profile_select.save_prompt_local)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) end local function event(widget, category, value, x, y) - -- if close event detected go to section home page + if category == EVT_CLOSE and value == 0 or value == 35 then - dashx.app.ui.openPage( - pageIdx, - "@i18n(app.modules.settings.name)@", - "settings/settings.lua" - ) + dashx.app.ui.openPage(pageIdx, "@i18n(app.modules.settings.name)@", "settings/settings.lua") return true end end -return { - event = event, - openPage = openPage, - wakeup = wakeup, - onNavMenu = onNavMenu, - onSaveMenu = onSaveMenu, - navButtons = { - menu = true, - save = true, - reload = false, - tool = false, - help = false, - }, - API = {}, -} +return {event = event, openPage = openPage, wakeup = wakeup, onNavMenu = onNavMenu, onSaveMenu = onSaveMenu, navButtons = {menu = true, save = true, reload = false, tool = false, help = false}, API = {}} diff --git a/scripts/dashx/app/radios.lua b/scripts/dashx/app/radios.lua index da769ad..0e4020e 100644 --- a/scripts/dashx/app/radios.lua +++ b/scripts/dashx/app/radios.lua @@ -1,136 +1,110 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local LCD_W, LCD_H = dashx.utils.getWindowSize() local resolution = LCD_W .. "x" .. LCD_H ---[[ - This script defines a table `supportedRadios` that contains configuration settings for different radio models. - Each key in the table represents a screen resolution, and the value is a table of settings specific to that resolution. - - Supported Radios: - - TANDEM X20, TANDEM XE (800x480) - - TANDEM X18, TWIN X Lite (480x320) - - Horus X10, Horus X12 (480x272) - - Twin X14 (632x314) - - Configuration settings include: - - `inlinesize_mult`: Multiplier for inline size. - - `menuButtonWidth`: Width of menu buttons. - - `navbuttonHeight`: Height of navigation buttons. - - `buttonsPerRow`: Number of buttons per row. - - `buttonsPerRowSmall`: Number of small buttons per row. - - `buttonWidth`: Width of buttons. - - `buttonHeight`: Height of buttons. - - `buttonPadding`: Padding between buttons. - - `buttonWidthSmall`: Width of small buttons. - - `buttonHeightSmall`: Height of small buttons. - - `buttonPaddingSmall`: Padding between small buttons. - - `linePaddingTop`: Padding at the top of lines. - - `logGraphMenuOffset`: Offset for log graph menu. - - `logGraphWidthPercentage`: Width percentage for log graph. - - `logGraphButtonsPerRow`: Number of buttons per row in log graph. - - `logGraphKeyHeight`: Height of log graph key. - - `logGraphHeightOffset`: Height offset for log graph. - - `logKeyFont`: Font for log key. - - `logSliderPaddingLeft`: Left padding for sliders. -]] local supportedRadios = { - -- TANDEM X20, TANDEM XE (800x480) + ["784x406"] = { - inlinesize_mult = 1, - menuButtonWidth = 100, - navbuttonHeight = 40, - buttonsPerRow = 6, - buttonsPerRowSmall = 7, - buttonWidth = 120, - buttonHeight = 120, - buttonPadding = 10, - buttonWidthSmall = 105, - buttonHeightSmall = 110, - buttonPaddingSmall = 6, - linePaddingTop = 8, - logGraphMenuOffset = 70, - logGraphWidthPercentage = 0.79, - logGraphButtonsPerRow = 5, - logGraphKeyHeight = 65, - logGraphHeightOffset = -15, - logKeyFont = FONT_S, - logKeyFontSmall = FONT_XS, - logSliderPaddingLeft = 42, - logShowAvg = true, + inlinesize_mult = 1, + menuButtonWidth = 100, + navbuttonHeight = 40, + buttonsPerRow = 6, + buttonsPerRowSmall = 7, + buttonWidth = 120, + buttonHeight = 120, + buttonPadding = 10, + buttonWidthSmall = 105, + buttonHeightSmall = 110, + buttonPaddingSmall = 6, + linePaddingTop = 8, + logGraphMenuOffset = 70, + logGraphWidthPercentage = 0.79, + logGraphButtonsPerRow = 5, + logGraphKeyHeight = 65, + logGraphHeightOffset = -15, + logKeyFont = FONT_S, + logKeyFontSmall = FONT_XS, + logSliderPaddingLeft = 42, + logShowAvg = true }, - -- TANDEM X18, TWIN X Lite (480x320) + ["472x288"] = { - inlinesize_mult = 1.28, - menuButtonWidth = 60, - navbuttonHeight = 30, - navButtonOffset = 47, - buttonsPerRow = 4, - buttonsPerRowSmall = 5, - buttonWidth = 110, - buttonHeight = 110, - buttonPadding = 8, - buttonWidthSmall = 89, - buttonHeightSmall = 95, - buttonPaddingSmall = 5, - linePaddingTop = 6, - logGraphMenuOffset = 55, - logGraphWidthPercentage = 0.72, - logGraphButtonsPerRow = 4, - logGraphKeyHeight = 45, - logGraphHeightOffset = 10, - logKeyFont = FONT_XS, - logKeyFontSmall = FONT_XXS, - logSliderPaddingLeft = 30, - logShowAvg = false, + inlinesize_mult = 1.28, + menuButtonWidth = 60, + navbuttonHeight = 30, + navButtonOffset = 47, + buttonsPerRow = 4, + buttonsPerRowSmall = 5, + buttonWidth = 110, + buttonHeight = 110, + buttonPadding = 8, + buttonWidthSmall = 89, + buttonHeightSmall = 95, + buttonPaddingSmall = 5, + linePaddingTop = 6, + logGraphMenuOffset = 55, + logGraphWidthPercentage = 0.72, + logGraphButtonsPerRow = 4, + logGraphKeyHeight = 45, + logGraphHeightOffset = 10, + logKeyFont = FONT_XS, + logKeyFontSmall = FONT_XXS, + logSliderPaddingLeft = 30, + logShowAvg = false }, - -- Horus X10, Horus X12 (480x272) + ["472x240"] = { - inlinesize_mult = 1.0715, - menuButtonWidth = 60, - navbuttonHeight = 30, - buttonsPerRow = 4, - buttonsPerRowSmall = 5, - buttonWidth = 110, - buttonHeight = 110, - buttonPadding = 8, - buttonWidthSmall = 87, - buttonHeightSmall = 97, - buttonPaddingSmall = 7, - linePaddingTop = 6, - logGraphMenuOffset = 50, - logGraphWidthPercentage = 0.65, - logGraphButtonsPerRow = 4, - logGraphKeyHeight = 38, - logGraphHeightOffset = 0, - logKeyFont = FONT_XS, - logKeyFontSmall = FONT_XXS, - logSliderPaddingLeft = 30, - logShowAvg = false, + inlinesize_mult = 1.0715, + menuButtonWidth = 60, + navbuttonHeight = 30, + buttonsPerRow = 4, + buttonsPerRowSmall = 5, + buttonWidth = 110, + buttonHeight = 110, + buttonPadding = 8, + buttonWidthSmall = 87, + buttonHeightSmall = 97, + buttonPaddingSmall = 7, + linePaddingTop = 6, + logGraphMenuOffset = 50, + logGraphWidthPercentage = 0.65, + logGraphButtonsPerRow = 4, + logGraphKeyHeight = 38, + logGraphHeightOffset = 0, + logKeyFont = FONT_XS, + logKeyFontSmall = FONT_XXS, + logSliderPaddingLeft = 30, + logShowAvg = false }, - -- Twin X14 (632x314) + ["632x314"] = { - menuButtonWidth = 80, - inlinesize_mult = 1.11, - navbuttonHeight = 35, - navButtonOffset = 47, - buttonsPerRow = 5, - buttonsPerRowSmall = 6, - buttonWidth = 118, - buttonHeight = 120, - buttonPadding = 7, - buttonWidthSmall = 97, - buttonHeightSmall = 115, - buttonPaddingSmall = 8, - linePaddingTop = 6, - logGraphMenuOffset = 60, - logGraphWidthPercentage = 0.76, - logGraphButtonsPerRow = 4, - logGraphKeyHeight = 50, - logGraphHeightOffset = 0, - logKeyFont = FONT_XXS, - logKeyFontSmall = FONT_XXS, - logSliderPaddingLeft = 30, - logShowAvg = false, + menuButtonWidth = 80, + inlinesize_mult = 1.11, + navbuttonHeight = 35, + navButtonOffset = 47, + buttonsPerRow = 5, + buttonsPerRowSmall = 6, + buttonWidth = 118, + buttonHeight = 120, + buttonPadding = 7, + buttonWidthSmall = 97, + buttonHeightSmall = 115, + buttonPaddingSmall = 8, + linePaddingTop = 6, + logGraphMenuOffset = 60, + logGraphWidthPercentage = 0.76, + logGraphButtonsPerRow = 4, + logGraphKeyHeight = 50, + logGraphHeightOffset = 0, + logKeyFont = FONT_XXS, + logKeyFontSmall = FONT_XXS, + logSliderPaddingLeft = 30, + logShowAvg = false } } diff --git a/scripts/dashx/lib/ini.lua b/scripts/dashx/lib/ini.lua index aa14322..e525f6d 100644 --- a/scripts/dashx/lib/ini.lua +++ b/scripts/dashx/lib/ini.lua @@ -1,20 +1,20 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local ini = {} --- Reads a file's full contents into a string, compatible with limited Lua function ini.load_file_as_string(path) local f = io.open(path, "rb") - if not f then - return nil, "Cannot open file: " .. path - end + if not f then return nil, "Cannot open file: " .. path end local content = "" local chunk repeat - chunk = io.read(f, "L") -- "L" = read a line, including newline (fallback chunk method) - if chunk then - content = content .. chunk - end + chunk = io.read(f, "L") + if chunk then content = content .. chunk end until not chunk io.close(f) @@ -25,22 +25,20 @@ function ini.load_ini_file(fileName) assert(type(fileName) == 'string', 'Parameter "fileName" must be a string.') local content, err = ini.load_file_as_string(fileName) - if not content then - return nil - end + if not content then return nil end local data = {} local section = nil for line in string.gmatch(content, "[^\r\n]+") do - line = line:match("^%s*(.-)%s*$") -- Trim line + line = line:match("^%s*(.-)%s*$") if line == "" or line:sub(1, 1) == ";" then - -- Skip comments and empty lines + elseif line:match("^%[.+%]$") then section = line:match("^%[(.+)%]$") if section then - section = section:match("^%s*(.-)%s*$") -- Trim spaces inside brackets + section = section:match("^%s*(.-)%s*$") section = tonumber(section) or section data[section] = data[section] or {} end @@ -49,7 +47,6 @@ function ini.load_ini_file(fileName) if param and value then param = tonumber(param) or param - -- Convert value types if value == "true" then value = true elseif value == "false" then @@ -58,9 +55,7 @@ function ini.load_ini_file(fileName) value = tonumber(value) end - if section then - data[section][param] = value - end + if section then data[section][param] = value end end end end @@ -73,16 +68,12 @@ function ini.save_ini_file(fileName, data) assert(type(data) == 'table', 'Parameter "data" must be a table.') local file, err = io.open(fileName, 'w') - if not file then - return false - end + if not file then return false end for section, params in pairs(data) do - file:write(("[" .. tostring(section) .. "]\n")) -- Removed extra spaces + file:write(("[" .. tostring(section) .. "]\n")) for key, value in pairs(params) do - if type(value) == "boolean" then - value = value and "true" or "false" - end + if type(value) == "boolean" then value = value and "true" or "false" end file:write(("%s=%s\n"):format(tostring(key), tostring(value))) end file:write("\n") @@ -92,7 +83,6 @@ function ini.save_ini_file(fileName, data) return true end --- Merges two INI-like tables, with values from the master table overwriting those in the slave table function ini.merge_ini_tables(master, slave) assert(type(master) == "table", "master must be a table") assert(type(slave) == "table", "slave must be a table") @@ -102,65 +92,40 @@ function ini.merge_ini_tables(master, slave) for section, slaveSection in pairs(slave) do merged[section] = {} - -- Copy slave defaults first - for key, value in pairs(slaveSection) do - merged[section][key] = value - end + for key, value in pairs(slaveSection) do merged[section][key] = value end - -- Overwrite or add values from master - if master[section] then - for key, value in pairs(master[section]) do - merged[section][key] = value - end - end + if master[section] then for key, value in pairs(master[section]) do merged[section][key] = value end end end - -- If master has additional sections, include them too for section, masterSection in pairs(master) do if not merged[section] then merged[section] = {} - for key, value in pairs(masterSection) do - merged[section][key] = value - end + for key, value in pairs(masterSection) do merged[section][key] = value end end end return merged end - --- Check if update is needed (i.e., merged table has new keys not present in original file) function ini.ini_tables_equal(a, b) for section, b_vals in pairs(b) do local a_vals = a[section] or {} - for k, v in pairs(b_vals) do - if a_vals[k] == nil then return false end - end + for k, v in pairs(b_vals) do if a_vals[k] == nil then return false end end end return true end function ini.getvalue(data, section, key) - if data and section and key then - if data[section] and data[section][key] ~= nil then - return data[section][key] - end - end + if data and section and key then if data[section] and data[section][key] ~= nil then return data[section][key] end end return nil end -function ini.section_exists(data, section) - return data and data[section] ~= nil -end +function ini.section_exists(data, section) return data and data[section] ~= nil end function ini.setvalue(data, section, key, value) - if not data then - return - end - if not data[section] then - data[section] = {} - end + if not data then return end + if not data[section] then data[section] = {} end data[section][key] = value end -return ini \ No newline at end of file +return ini diff --git a/scripts/dashx/lib/utils.lua b/scripts/dashx/lib/utils.lua index f04836c..be54fd0 100644 --- a/scripts/dashx/lib/utils.lua +++ b/scripts/dashx/lib/utils.lua @@ -1,35 +1,15 @@ -local dashx = require("dashx") --[[ - - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local utils = {} +local dashx = require("dashx") +local utils = {} local arg = {...} local config = arg[1] - --- sets up the initial session var state. --- function is called on startup of the script and --- whenever the tasks.lua detects the heli has been disconnected function utils.session() dashx.session = {} dashx.session.tailMode = nil @@ -73,195 +53,130 @@ function utils.session() dashx.session.bblSize = nil dashx.session.bblUsed = nil dashx.session.batteryConfig = nil - -- keep dashx.session.batteryConfig nil as it is used to determine if the battery config has been loaded - -- dashx.session.batteryConfig will end up containing the following: - -- batteryCapacity = nil - -- batteryCellCount = nil - -- vbatwarningcellvoltage = nil - -- vbatmincellvoltage = nil - -- vbatmaxcellvoltage = nil - -- vbatfullcellvoltage = nil - -- lvcPercentage = nil - -- consumptionWarningPercentage = nil - dashx.session.modelPreferences = nil -- this is used to store the model preferences - dashx.session.modelPreferencesFile = nil -- this is used to store the model preferences file path - dashx.session.dashboardEditingTheme = nil -- this is used to store the dashboard theme being edited in settings + + dashx.session.modelPreferences = nil + dashx.session.modelPreferencesFile = nil + dashx.session.dashboardEditingTheme = nil dashx.session.timer = {} - dashx.session.timer.start = nil -- this is used to store the start time of the timer - dashx.session.timer.live = nil -- this is used to store the live timer value while inflight - dashx.session.timer.lifetime = nil -- this is used to store the total flight time of a model and store it in the user ini file - dashx.session.timer.session = 0 -- this is used to track flight time for the session + dashx.session.timer.start = nil + dashx.session.timer.live = nil + dashx.session.timer.lifetime = nil + dashx.session.timer.session = 0 dashx.session.flightCounted = false - dashx.session.onConnect = {} -- this is used to store the onConnect tasks that need to be run + dashx.session.onConnect = {} dashx.session.onConnect.high = false dashx.session.onConnect.low = false dashx.session.onConnect.medium = false dashx.session.rx = {} dashx.session.rx.map = {} - dashx.session.rx.values = {} -- this is used to store the rx values for the rxmap task + dashx.session.rx.values = {} end ---- Checks if the RX map is ready by verifying the presence of required channel mappings. --- The function returns true if the `dashx.session.rxmap` table exists and at least one of the following fields is present: --- `collective`, `elevator`, `throttle`, or `rudder`. --- @return boolean True if the RX map is ready, false otherwise. function utils.rxmapReady() - -- Check if the RX map is ready - if dashx.session.rx and dashx.session.rx.map and (dashx.session.rx.map.collective or dashx.session.rx.map.elevator or dashx.session.rx.map.throttle or dashx.session.rx.map.rudder) then - return true - end + + if dashx.session.rx and dashx.session.rx.map and (dashx.session.rx.map.collective or dashx.session.rx.map.elevator or dashx.session.rx.map.throttle or dashx.session.rx.map.rudder) then return true end return false end ---- Checks if the current flight mode is "inflight". --- @return boolean Returns true if the flight mode is "inflight", false otherwise. function utils.inFlight() - if dashx.flightmode.current == "inflight" then - return true - end + if dashx.flightmode.current == "inflight" then return true end return false end ---- Converts a version array into an indexed array of tables. --- Each element in the input table is paired with its zero-based index in a new table. --- @param tbl Table containing version elements. --- @return arr Array of tables, each containing the value and its zero-based index: {value, index}. function utils.msp_version_array_to_indexed() local arr = {} - local tbl = dashx.config.supportedMspApiVersion or {"12.06", "12.07","12.08"} - for i, v in ipairs(tbl) do - arr[#arr+1] = {v, i} - end + local tbl = dashx.config.supportedMspApiVersion or {"12.06", "12.07", "12.08"} + for i, v in ipairs(tbl) do arr[#arr + 1] = {v, i} end return arr end ---- Converts arming disable flags into a human-readable string representation. ---- ---- This function iterates through the bits of the provided `flags` integer and checks ---- which flags are set. For each set flag, it appends the corresponding localized ---- string to the result. If no flags are set, it returns a localized "OK" message. ---- ---- @param flags number The bitfield representing arming disable flags. ---- @return string A comma-separated string of human-readable flag descriptions, or "OK" if no flags are set. function utils.armingDisableFlagsToString(flags) local ARMING_DISABLE_FLAG_TAG = { - [0] = "@i18n(app.modules.fblstatus.arming_disable_flag_0):upper()@", - [1] = "@i18n(app.modules.fblstatus.arming_disable_flag_1):upper()@", - [2] = "@i18n(app.modules.fblstatus.arming_disable_flag_2):upper()@", - [3] = "@i18n(app.modules.fblstatus.arming_disable_flag_3):upper()@", - [4] = "@i18n(app.modules.fblstatus.arming_disable_flag_4):upper()@", - [5] = "@i18n(app.modules.fblstatus.arming_disable_flag_5):upper()@", - [6] = "@i18n(app.modules.fblstatus.arming_disable_flag_6):upper()@", - [7] = "@i18n(app.modules.fblstatus.arming_disable_flag_7):upper()@", - [8] = "@i18n(app.modules.fblstatus.arming_disable_flag_8):upper()@", - [9] = "@i18n(app.modules.fblstatus.arming_disable_flag_9):upper()@", - [10] = "@i18n(app.modules.fblstatus.arming_disable_flag_10):upper()@", - [11] = "@i18n(app.modules.fblstatus.arming_disable_flag_11):upper()@", - [12] = "@i18n(app.modules.fblstatus.arming_disable_flag_12):upper()@", - [13] = "@i18n(app.modules.fblstatus.arming_disable_flag_13):upper()@", - [14] = "@i18n(app.modules.fblstatus.arming_disable_flag_14):upper()@", - [15] = "@i18n(app.modules.fblstatus.arming_disable_flag_15):upper()@", - [16] = "@i18n(app.modules.fblstatus.arming_disable_flag_16):upper()@", - [17] = "@i18n(app.modules.fblstatus.arming_disable_flag_17):upper()@", - [18] = "@i18n(app.modules.fblstatus.arming_disable_flag_18):upper()@", - [19] = "@i18n(app.modules.fblstatus.arming_disable_flag_19):upper()@", - [20] = "@i18n(app.modules.fblstatus.arming_disable_flag_20):upper()@", - [21] = "@i18n(app.modules.fblstatus.arming_disable_flag_21):upper()@", - [22] = "@i18n(app.modules.fblstatus.arming_disable_flag_22):upper()@", - [23] = "@i18n(app.modules.fblstatus.arming_disable_flag_23):upper()@", - [24] = "@i18n(app.modules.fblstatus.arming_disable_flag_24):upper()@", - [25] = "@i18n(app.modules.fblstatus.arming_disable_flag_25):upper()@", + [0] = "@i18n(app.modules.fblstatus.arming_disable_flag_0):upper()@", + [1] = "@i18n(app.modules.fblstatus.arming_disable_flag_1):upper()@", + [2] = "@i18n(app.modules.fblstatus.arming_disable_flag_2):upper()@", + [3] = "@i18n(app.modules.fblstatus.arming_disable_flag_3):upper()@", + [4] = "@i18n(app.modules.fblstatus.arming_disable_flag_4):upper()@", + [5] = "@i18n(app.modules.fblstatus.arming_disable_flag_5):upper()@", + [6] = "@i18n(app.modules.fblstatus.arming_disable_flag_6):upper()@", + [7] = "@i18n(app.modules.fblstatus.arming_disable_flag_7):upper()@", + [8] = "@i18n(app.modules.fblstatus.arming_disable_flag_8):upper()@", + [9] = "@i18n(app.modules.fblstatus.arming_disable_flag_9):upper()@", + [10] = "@i18n(app.modules.fblstatus.arming_disable_flag_10):upper()@", + [11] = "@i18n(app.modules.fblstatus.arming_disable_flag_11):upper()@", + [12] = "@i18n(app.modules.fblstatus.arming_disable_flag_12):upper()@", + [13] = "@i18n(app.modules.fblstatus.arming_disable_flag_13):upper()@", + [14] = "@i18n(app.modules.fblstatus.arming_disable_flag_14):upper()@", + [15] = "@i18n(app.modules.fblstatus.arming_disable_flag_15):upper()@", + [16] = "@i18n(app.modules.fblstatus.arming_disable_flag_16):upper()@", + [17] = "@i18n(app.modules.fblstatus.arming_disable_flag_17):upper()@", + [18] = "@i18n(app.modules.fblstatus.arming_disable_flag_18):upper()@", + [19] = "@i18n(app.modules.fblstatus.arming_disable_flag_19):upper()@", + [20] = "@i18n(app.modules.fblstatus.arming_disable_flag_20):upper()@", + [21] = "@i18n(app.modules.fblstatus.arming_disable_flag_21):upper()@", + [22] = "@i18n(app.modules.fblstatus.arming_disable_flag_22):upper()@", + [23] = "@i18n(app.modules.fblstatus.arming_disable_flag_23):upper()@", + [24] = "@i18n(app.modules.fblstatus.arming_disable_flag_24):upper()@", + [25] = "@i18n(app.modules.fblstatus.arming_disable_flag_25):upper()@" } - - -- No flags: localized OK (already uppercase via tag transform) - if flags == nil or flags == 0 then - return "@i18n(app.modules.fblstatus.ok):upper()@" - end + if flags == nil or flags == 0 then return "@i18n(app.modules.fblstatus.ok):upper()@" end local names = {} for i = 0, 25 do if (flags & (1 << i)) ~= 0 then local name = ARMING_DISABLE_FLAG_TAG[i] - if name and name ~= "" then - names[#names+1] = name - end + if name and name ~= "" then names[#names + 1] = name end end end - if #names == 0 then - return "@i18n(app.modules.fblstatus.ok):upper()@" - end + if #names == 0 then return "@i18n(app.modules.fblstatus.ok):upper()@" end - -- NOTE: We avoid forcing uppercase here to preserve non-ASCII locales. - -- If you really want uppercase, you can do: - -- return (table.concat(names, ", ")):upper() return table.concat(names, ", ") end --- get the governor text from the value function utils.getGovernorState(value) local returnvalue - if not dashx.tasks.telemetry then - return "@i18n(widgets.governor.UNKNOWN)@" - end - - --[[ - Checks if the provided value exists as a key in the 'map' table. - If the key exists, assigns the corresponding value from 'map' to 'returnvalue'. - If the key does not exist, assigns a localized "UNKNOWN" string to 'returnvalue' using 'i18n'. - ]] - local map = { - [0] = "@i18n(widgets.governor.OFF)@", - [1] = "@i18n(widgets.governor.IDLE)@", - [2] = "@i18n(widgets.governor.SPOOLUP)@", - [3] = "@i18n(widgets.governor.RECOVERY)@", - [4] = "@i18n(widgets.governor.ACTIVE)@", - [5] = "@i18n(widgets.governor.THROFF)@", - [6] = "@i18n(widgets.governor.LOSTHS)@", - [7] = "@i18n(widgets.governor.AUTOROT)@", - [8] = "@i18n(widgets.governor.BAILOUT)@", + if not dashx.tasks.telemetry then return "@i18n(widgets.governor.UNKNOWN)@" end + + local map = { + [0] = "@i18n(widgets.governor.OFF)@", + [1] = "@i18n(widgets.governor.IDLE)@", + [2] = "@i18n(widgets.governor.SPOOLUP)@", + [3] = "@i18n(widgets.governor.RECOVERY)@", + [4] = "@i18n(widgets.governor.ACTIVE)@", + [5] = "@i18n(widgets.governor.THROFF)@", + [6] = "@i18n(widgets.governor.LOSTHS)@", + [7] = "@i18n(widgets.governor.AUTOROT)@", + [8] = "@i18n(widgets.governor.BAILOUT)@", [100] = "@i18n(widgets.governor.DISABLED)@", [101] = "@i18n(widgets.governor.DISARMED)@" } if dashx.session and dashx.session.apiVersion and dashx.session.apiVersion > 12.07 then local armflags = dashx.tasks.telemetry.getSensor("armflags") - if armflags == 0 or armflags == 2 then - value = 101 - end + if armflags == 0 or armflags == 2 then value = 101 end end if map[value] then returnvalue = map[value] else returnvalue = "@i18n(widgets.governor.UNKNOWN)@" - end - - --[[ - Checks the value of the "armdisableflags" telemetry sensor. If the sensor value is available, - it is floored to the nearest integer and converted to a human-readable string using - utils.armingDisableFlagsToString(). If the resulting string is not "OK", - the function sets 'returnvalue' to this string, indicating a reason why arming is disabled. - --]] + end + local armdisableflags = dashx.tasks.telemetry.getSensor("armdisableflags") if armdisableflags ~= nil then armdisableflags = math.floor(armdisableflags) - local armstring = utils.armingDisableFlagsToString(armdisableflags ) - if armstring ~= "OK" then - returnvalue = armstring - end - end - - --- Returns the value stored in `returnvalue`. - -- @return The value of `returnvalue`. + local armstring = utils.armingDisableFlagsToString(armdisableflags) + if armstring ~= "OK" then returnvalue = armstring end + end + return returnvalue end - function utils.createCacheFile(tbl, path, options) os.mkdir("cache") @@ -304,7 +219,6 @@ function utils.createCacheFile(tbl, path, options) f:close() end - function utils.sanitize_filename(str) if not str then return nil end return str:match("^%s*(.-)%s*$"):gsub('[\\/:"*?<>|]', '') @@ -314,11 +228,7 @@ function utils.dir_exists(base, name) base = base or "./" local list = system.listFiles(base) if list == nil then return false end - for i = 1, #list do - if list[i] == name then - return true - end - end + for i = 1, #list do if list[i] == name then return true end end return false end @@ -332,20 +242,15 @@ function utils.file_exists(name) end function utils.playFile(pkg, file) - -- Get and clean audio voice path + local av = system.getAudioVoice():gsub("SD:", ""):gsub("RADIO:", ""):gsub("AUDIO:", ""):gsub("VOICE[1-4]:", ""):gsub("audio/", "") - - -- Ensure av does not start with a slash - if av:sub(1, 1) == "/" then - av = av:sub(2) - end - -- Construct file paths - local wavUser = "SCRIPTS:/dashx.user/audio/user/" .. pkg .. "/" .. file + if av:sub(1, 1) == "/" then av = av:sub(2) end + + local wavUser = "SCRIPTS:/dashx.user/audio/user/" .. pkg .. "/" .. file local wavLocale = "SCRIPTS:/dashx.user/audio/" .. av .. "/" .. pkg .. "/" .. file - local wavDefault= "SCRIPTS:/dashx/audio/en/default/" .. pkg .. "/" .. file + local wavDefault = "SCRIPTS:/dashx/audio/en/default/" .. pkg .. "/" .. file - -- Determine which file to play: user → locale → default local path if dashx.utils.file_exists(wavUser) then path = wavUser @@ -358,18 +263,10 @@ function utils.playFile(pkg, file) system.playFile(path) end +function utils.playFileCommon(file) system.playFile("audio/" .. file) end -function utils.playFileCommon(file) - system.playFile("audio/" .. file) -end - - --- this is used in multiple places - just gives easy way --- to grab activeProfile or activeRateProfile in tmp var --- you MUST set it to nil after you get it! function utils.getCurrentProfile() - local pidProfile = dashx.tasks.telemetry.getSensor("pid_profile") local rateProfile = dashx.tasks.telemetry.getSensor("rate_profile") @@ -394,47 +291,36 @@ function utils.getCurrentProfile() end end --- Function to compare the current system version with a target version --- Function to compare the current system version with a target version function utils.ethosVersionAtLeast(targetVersion) local env = system.getVersion() local currentVersion = {env.major, env.minor, env.revision} - -- Fallback to default config if targetVersion is not provided - if targetVersion == nil then + if targetVersion == nil then if dashx and dashx.config and dashx.config.ethosVersion then targetVersion = dashx.config.ethosVersion else - -- Fail-safe: if no targetVersion is provided and config is missing + return false end elseif type(targetVersion) == "number" then - dashx.utils.log("WARNING: utils.ethosVersionAtLeast() called with a number instead of a table (" .. targetVersion .. ")",2) - return false + dashx.utils.log("WARNING: utils.ethosVersionAtLeast() called with a number instead of a table (" .. targetVersion .. ")", 2) + return false end - -- Ensure the targetVersion has three components (major, minor, revision) - for i = 1, 3 do - targetVersion[i] = targetVersion[i] or 0 -- Default to 0 if not provided - end + for i = 1, 3 do targetVersion[i] = targetVersion[i] or 0 end - -- Compare major, minor, and revision explicitly for i = 1, 3 do if currentVersion[i] > targetVersion[i] then - return true -- Current version is higher + return true elseif currentVersion[i] < targetVersion[i] then - return false -- Current version is lower + return false end end - return true -- Versions are equal (>= condition met) + return true end -function utils.titleCase(str) - return str:gsub("(%a)([%w_']*)", function(first, rest) - return first:upper() .. rest:lower() - end) -end +function utils.titleCase(str) return str:gsub("(%a)([%w_']*)", function(first, rest) return first:upper() .. rest:lower() end) end function utils.stringInArray(array, s) for i, value in ipairs(array) do if value == s then return true end end @@ -442,28 +328,20 @@ function utils.stringInArray(array, s) end function utils.round(num, places) - if num == nil then - return nil - end + if num == nil then return nil end local places = places or 2 if places == 0 then - return math.floor(num + 0.5) -- return integer (no .0) + return math.floor(num + 0.5) else - local mult = 10^places + local mult = 10 ^ places return math.floor(num * mult + 0.5) / mult end end +function utils.roughlyEqual(a, b, tolerance) return math.abs(a - b) < (tolerance or 0.0001) end -function utils.roughlyEqual(a, b, tolerance) - return math.abs(a - b) < (tolerance or 0.0001) -- Allows a tiny margin of error -end - --- return current window size -function utils.getWindowSize() - return lcd.getWindowSize() -end +function utils.getWindowSize() return lcd.getWindowSize() end function utils.joinTableItems(tbl, delimiter) if not tbl or #tbl == 0 then return "" end @@ -471,45 +349,21 @@ function utils.joinTableItems(tbl, delimiter) delimiter = delimiter or "" local startIndex = tbl[0] and 0 or 1 - -- Pre-pad all fields once before joining local paddedTable = {} - for i = startIndex, #tbl do - paddedTable[i] = tostring(tbl[i]) .. string.rep(" ", math.max(0, 3 - #tostring(tbl[i]))) - end + for i = startIndex, #tbl do paddedTable[i] = tostring(tbl[i]) .. string.rep(" ", math.max(0, 3 - #tostring(tbl[i]))) end - -- Join the padded table items return table.concat(paddedTable, delimiter, startIndex, #tbl) end ---[[ - Logs a message with a specified log level. - - @param msg string: The message to log. - @param level string (optional): The log level (e.g., "debug", "info", "warn", "error"). Defaults to "debug". -]] -function utils.log(msg, level) - if dashx.tasks and dashx.tasks.logger then - dashx.tasks.logger.add(msg, level or "debug") - end -end +function utils.log(msg, level) if dashx.tasks and dashx.tasks.logger then dashx.tasks.logger.add(msg, level or "debug") end end --- Function to print a table to the debug console in a readable format. --- @param node The table to be printed. --- @param maxDepth (optional) The maximum depth to traverse the table. Default is 5. --- @param currentDepth (optional) The current depth of traversal. Default is 0. --- @return A string representation of the table. --- print a table out to debug console function utils.print_r(node, maxDepth, currentDepth) - maxDepth = maxDepth or 5 -- Reasonable depth limit to avoid runaway recursion + maxDepth = maxDepth or 5 currentDepth = currentDepth or 0 - if currentDepth > maxDepth then - return "{...} -- Max Depth Reached" - end + if currentDepth > maxDepth then return "{...} -- Max Depth Reached" end - if type(node) ~= "table" then - return tostring(node) .. " (" .. type(node) .. ")" - end + if type(node) ~= "table" then return tostring(node) .. " (" .. type(node) .. ")" end local result = {} @@ -523,9 +377,7 @@ function utils.print_r(node, maxDepth, currentDepth) value = utils.print_r(v, maxDepth, currentDepth + 1) else value = tostring(v) - if type(v) == "string" then - value = '"' .. value .. '"' - end + if type(v) == "string" then value = '"' .. value .. '"' end end table.insert(result, key .. " = " .. value .. ",") @@ -536,16 +388,6 @@ function utils.print_r(node, maxDepth, currentDepth) return print(table.concat(result, " ")) end - ---[[ - Finds and loads modules from the specified directory. - - This function scans the "app/modules/" directory for subdirectories containing an "init.lua" file. - It attempts to load each "init.lua" file as a Lua chunk and expects it to return a table with a "script" field. - If the "init.lua" file is successfully loaded and returns a valid configuration table, the module is added to the modules list. - - @return table A list of loaded module configurations. Each configuration is a table containing the module's details. -]] function utils.findModules() local modulesList = {} @@ -572,23 +414,13 @@ function utils.findModules() table.insert(modulesList, mconfig) end end - - end + + end end return modulesList end ---[[ - Finds and loads widget configurations from the "widgets/" directory. - - This function scans the "widgets/" directory for subdirectories containing an "init.lua" file. - It attempts to load each "init.lua" file as a Lua chunk and expects it to return a table with widget configuration. - The configuration table must contain a "key" field to be considered valid. - If valid, the configuration table is added to the widgets list with an additional "folder" field indicating the widget's directory. - - @return table A list of valid widget configuration tables. -]] function utils.findWidgets() local widgetsList = {} @@ -599,76 +431,37 @@ function utils.findWidgets() if v ~= ".." and v ~= "." and not v:match("%.%a+$") then local init_path = widgets_path .. v .. '/init.lua' - -- try loading directly + local func, err = loadfile(init_path) if not func then - dashx.utils.log( - "Failed to load widget init " .. init_path .. ": " .. err, - "debug" - ) + dashx.utils.log("Failed to load widget init " .. init_path .. ": " .. err, "debug") else local ok, wconfig = pcall(func) if not ok then - dashx.utils.log( - "Error executing widget init " .. init_path .. ": " .. wconfig, - "debug" - ) + dashx.utils.log("Error executing widget init " .. init_path .. ": " .. wconfig, "debug") elseif type(wconfig) ~= "table" or not wconfig.key then - dashx.utils.log( - "Invalid configuration in " .. init_path, - "debug" - ) + dashx.utils.log("Invalid configuration in " .. init_path, "debug") else wconfig.folder = v table.insert(widgetsList, wconfig) end - end - end + end + end end return widgetsList end ---[[ - utils.loadImage(image1, image2, image3) - - This function attempts to load an image from a list of provided image paths or Bitmap objects. - It checks for the existence of the image in multiple directories and supports both PNG and BMP formats. - - Parameters: - image1 (string|Bitmap): The primary image path or Bitmap object to load. - image2 (string|Bitmap): The secondary image path or Bitmap object to load if the primary is not found. - image3 (string|Bitmap): The tertiary image path or Bitmap object to load if neither the primary nor secondary are found. - - Returns: - Bitmap: The loaded Bitmap object if an image path is found and successfully loaded. - Bitmap: The first existing Bitmap object from the provided parameters if no image path is found. - nil: If no valid image path or Bitmap object is found. - - Helper Functions: - find_image_in_directories(img): - Checks if the image file exists in different directories and returns the valid path if found. - - resolve_image(image): - Resolves the image path by checking its existence and attempting to switch between PNG and BMP formats if necessary. ---]] --- caches for loadImage -utils._imagePathCache = {} +utils._imagePathCache = {} utils._imageBitmapCache = {} function utils.loadImage(image1, image2, image3) - -- Resolve & cache bitmaps to avoid repeated fs checks + local function getCachedBitmap(key, tryPaths) - -- already loaded? - -- nothing to do if no key - if not key then - return nil - end - -- already loaded? - if utils._imageBitmapCache[key] then - return utils._imageBitmapCache[key] - end - -- find or reuse resolved path + if not key then return nil end + + if utils._imageBitmapCache[key] then return utils._imageBitmapCache[key] end + local path = utils._imagePathCache[key] if not path then for _, p in ipairs(tryPaths) do @@ -686,45 +479,21 @@ function utils.loadImage(image1, image2, image3) return bmp end - -- build candidate paths for each image string local function candidates(img) if type(img) ~= "string" then return {} end - local out = { img, "BITMAPS:"..img, "SYSTEM:"..img } + local out = {img, "BITMAPS:" .. img, "SYSTEM:" .. img} if img:match("%.png$") then - -- direct array-style append instead of table.insert - out[#out+1] = img:gsub("%.png$",".bmp") + + out[#out + 1] = img:gsub("%.png$", ".bmp") elseif img:match("%.bmp$") then - out[#out+1] = img:gsub("%.bmp$",".png") + out[#out + 1] = img:gsub("%.bmp$", ".png") end return out end - -- try in order - return getCachedBitmap(image1, candidates(image1)) - or getCachedBitmap(image2, candidates(image2)) - or getCachedBitmap(image3, candidates(image3)) + return getCachedBitmap(image1, candidates(image1)) or getCachedBitmap(image2, candidates(image2)) or getCachedBitmap(image3, candidates(image3)) end ---[[ - Function: utils.simSensors - - Loads and executes a telemetry Lua script based on the provided ID. - - Parameters: - id (string): The identifier for the telemetry script to load. - - Returns: - number: The result of the executed telemetry script, or 0 if an error occurs. - - Description: - This function attempts to load a telemetry Lua script from two possible paths: - 1. "LOGS:/dashx/sensors/.lua" - 2. "lib/sim/sensors/.lua" - - It first checks if the file exists at the local path. If not, it checks the fallback path. - If the file is found, it attempts to load and execute the script. If any error occurs - during loading or execution, it prints an error message and returns 0. ---]] function utils.simSensors(id) os.mkdir("LOGS:") os.mkdir("LOGS:/dashx") @@ -734,7 +503,6 @@ function utils.simSensors(id) local filepath = "sim/sensors/" .. id .. ".lua" - -- loadfile will fail gracefully if file doesn't exist or has errors local chunk, err = loadfile(filepath) if not chunk then print("Error loading telemetry file: " .. err) @@ -750,50 +518,28 @@ function utils.simSensors(id) return result end - --- Splits a given string into a table of substrings based on a specified separator. --- @param input The string to be split. --- @param sep The separator used to split the string. --- @return A table containing the substrings. function utils.splitString(input, sep) local result = {} - -- Lua's gmatch needs plain `sep`, so if you want to handle "%s*" or patterns, use this - for item in input:gmatch("([^" .. sep .. "]+)") do - table.insert(result, item) - end + for item in input:gmatch("([^" .. sep .. "]+)") do table.insert(result, item) end return result end - ---- Logs MSP (Multiwii Serial Protocol) commands if logging is enabled in the configuration. --- @param cmd The MSP command to log. --- @param rwState The read/write state of the command. --- @param buf The buffer containing the command data. --- @param err Any error associated with the command. --- @usage --- utils.logMsp("MSP_STATUS", "read", {0x01, 0x02, 0x03}, nil) function utils.logMsp(cmd, rwState, buf, err) if dashx.preferences.developer.logmsp then local payload = dashx.utils.joinTableItems(buf, ", ") dashx.utils.log(rwState .. " [" .. cmd .. "]" .. " {" .. payload .. "}", "info") - if err then - dashx.utils.log("Error: " .. err, "info") - end + if err then dashx.utils.log("Error: " .. err, "info") end end end - function utils.truncateText(str, maxWidth) lcd.font(bestFont) local tsizeW, _ = lcd.getTextSize(str) - if tsizeW <= maxWidth then - return str -- Fits, no need to truncate - end + if tsizeW <= maxWidth then return str end - -- Start truncating local ellipsis = "..." local truncatedStr = str while tsizeW > maxWidth and #truncatedStr > 1 do @@ -805,24 +551,18 @@ end function utils.reportMemoryUsage(location) - if dashx.preferences.developer.memstats == false then - return - end + if dashx.preferences.developer.memstats == false then return end - -- Get current memory usage in bytes and convert to KB local currentMemoryUsage = system.getMemoryUsage().luaRamAvailable / 1024 - -- Retrieve the last memory usage from the session (convert it to KB if it exists) local lastMemoryUsage = dashx.session.lastMemoryUsage - -- Ensure location is not nil or empty location = location or "Unknown" - -- Construct the log message local logMessage if lastMemoryUsage then - lastMemoryUsage = lastMemoryUsage / 1024 -- Convert last recorded memory to KB + lastMemoryUsage = lastMemoryUsage / 1024 local difference = currentMemoryUsage - lastMemoryUsage if difference > 0 then logMessage = string.format("[%s] Memory usage decreased by %.2f KB (Current: %.2f KB)", location, difference, currentMemoryUsage) @@ -835,14 +575,11 @@ function utils.reportMemoryUsage(location) logMessage = string.format("[%s] Initial memory usage: %.2f KB", location, currentMemoryUsage) end - -- Log the message dashx.utils.log(logMessage, "info") - -- Store the current memory usage in bytes for future calls (convert back to bytes) dashx.session.lastMemoryUsage = system.getMemoryUsage().luaRamAvailable end - function utils.onReboot() dashx.session.resetSensors = true dashx.session.resetTelemetry = true @@ -850,26 +587,17 @@ function utils.onReboot() dashx.session.resetMSPSensors = true end ---- Parses a version string into a table of numbers. --- The function splits the input version string by numeric components and returns a table --- where each element is a number from the version string. The table starts with 0 as the first element. --- @param versionString string: The version string to parse (e.g., "1.2.3"). --- @return table|nil: A table of numbers representing the version, or nil if the input is nil. function utils.splitVersionStringToNumbers(versionString) if not versionString then return nil end - local parts = {0} -- start with 0 - for num in versionString:gmatch("%d+") do - table.insert(parts, tonumber(num)) - end + local parts = {0} + for num in versionString:gmatch("%d+") do table.insert(parts, tonumber(num)) end return parts end function utils.keys(tbl) local keys = {} - for k in pairs(tbl) do - table.insert(keys, k) - end + for k in pairs(tbl) do table.insert(keys, k) end return keys end diff --git a/scripts/dashx/main.lua b/scripts/dashx/main.lua index ea89af5..f9d3b5f 100644 --- a/scripts/dashx/main.lua +++ b/scripts/dashx/main.lua @@ -1,36 +1,27 @@ --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- --- Keep the namespace local (no globals) local dashx = {} dashx.session = {} --- Namespace for the suite (kept local; not global) local dashx = {} package.loaded.dashx = dashx --- Print warning if accidental global created -local _ENV = setmetatable({ dashx = dashx }, { - __index = _G, - __newindex = function(_, k) print("attempt to create global '"..tostring(k).."'", 2) end -}) --- initialise legacy font if not already set (ethos 1.6 vs 1.7) +local _ENV = setmetatable({dashx = dashx}, {__index = _G, __newindex = function(_, k) print("attempt to create global '" .. tostring(k) .. "'", 2) end}) + if not FONT_M then FONT_M = FONT_STD end --- RotorFlight + ETHOS LUA configuration local config = {} --- Configuration settings for the dashx Lua Ethos Suite config.toolName = "DashX" config.icon = lcd.loadMask("app/gfx/icon.png") config.icon_logtool = lcd.loadMask("app/gfx/icon_logtool.png") config.icon_unsupported = lcd.loadMask("app/gfx/unsupported.png") config.version = {major = 2, minor = 3, revision = 0, suffix = "DEV"} config.ethosVersion = {1, 6, 2} -config.supportedMspApiVersion = {"12.07","12.08","12.09"} +config.supportedMspApiVersion = {"12.07", "12.08", "12.09"} config.baseDir = "dashx" config.preferences = config.baseDir .. ".user" config.defaultRateProfile = 4 @@ -38,48 +29,18 @@ config.watchdogParam = 10 dashx.config = config --- INI utilities (never compiled) loaded inside ENV dashx.ini = assert(loadfile("lib/ini.lua", "t", _ENV))(config) --- set defaults for user preferences -local userpref_defaults ={ - general ={ - iconsize = 2, - syncname = false, - gimbalsupression = 0.85 - }, - localizations = { - temperature_unit = 0, -- 0 = Celsius, 1 = Fahrenheit - altitude_unit = 0, -- 0 = meters, 1 = feet - }, - dashboard = { - theme_preflight = "system/default", - theme_inflight = "system/default", - theme_postflight = "system/default", - }, - events = { - armed = true, - voltage = true, - fuel = true, - profile = true, - inflight = true, - }, - switches = {}, - developer = { - compile = true, - devtools = false, - logtofile = false, - loglevel = "off", - logmsp = false, - logmspQueue = false, - memstats = false, - mspexpbytes = 8, - apiversion = 2, - }, - menulastselected = {} +local userpref_defaults = { + general = {iconsize = 2, syncname = false, gimbalsupression = 0.85}, + localizations = {temperature_unit = 0, altitude_unit = 0}, + dashboard = {theme_preflight = "system/default", theme_inflight = "system/default", theme_postflight = "system/default"}, + events = {armed = true, voltage = true, fuel = true, profile = true, inflight = true}, + switches = {}, + developer = {compile = true, devtools = false, logtofile = false, loglevel = "off", logmsp = false, logmspQueue = false, memstats = false, mspexpbytes = 8, apiversion = 2}, + menulastselected = {} } --- Preferences path os.mkdir("SCRIPTS:/" .. dashx.config.preferences) local userpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/preferences.ini" local slave_ini = userpref_defaults @@ -88,143 +49,112 @@ local master_ini = dashx.ini.load_ini_file(userpref_file) or {} local updated_ini = dashx.ini.merge_ini_tables(master_ini, slave_ini) dashx.preferences = updated_ini -if not dashx.ini.ini_tables_equal(master_ini, slave_ini) then - dashx.ini.save_ini_file(userpref_file, updated_ini) -end +if not dashx.ini.ini_tables_equal(master_ini, slave_ini) then dashx.ini.save_ini_file(userpref_file, updated_ini) end --- tasks dashx.config.bgTaskName = dashx.config.toolName .. " [Background]" dashx.config.bgTaskKey = "dshxbg" --- library with utility functions used throughout the suite dashx.utils = assert(loadfile("lib/utils.lua"))(dashx.config) --- main app dashx.app = assert(loadfile("app/app.lua"))(dashx.config) --- tasks dashx.tasks = assert(loadfile("tasks/tasks.lua"))(dashx.config) --- configure the flight mode -dashx.flightmode = { current = "preflight" } +dashx.flightmode = {current = "preflight"} --- Reset the session state dashx.utils.session() --- simulator hooks -dashx.simevent = { telemetry_state = true } +dashx.simevent = {telemetry_state = true} ---- Retrieves the version information of the dashx module. function dashx.version() - local v = dashx.config.version - return { - version = string.format("%d.%d.%d-%s", v.major, v.minor, v.revision, v.suffix), - major = v.major, - minor = v.minor, - revision = v.revision, - suffix = v.suffix - } + local v = dashx.config.version + return {version = string.format("%d.%d.%d-%s", v.major, v.minor, v.revision, v.suffix), major = v.major, minor = v.minor, revision = v.revision, suffix = v.suffix} end local function init() - -- prevent this running if version is not supported - if not dashx.utils.ethosVersionAtLeast() then - system.registerSystemTool({ - name = dashx.config.toolName, - icon = dashx.config.icon_unsupported , - create = function () end, - wakeup = function () lcd.invalidate(); return end, - paint = function () - local w, h = lcd.getWindowSize() - local textColor = lcd.RGB(255, 255, 255, 1) - lcd.color(textColor) - lcd.font(FONT_M) - local badVersionMsg = string.format("ETHOS < V%d.%d.%d", table.unpack(config.ethosVersion)) - local textWidth, textHeight = lcd.getTextSize(badVersionMsg) - local x = (w - textWidth) / 2 - local y = (h - textHeight) / 2 - lcd.drawText(x, y, badVersionMsg) + + if not dashx.utils.ethosVersionAtLeast() then + system.registerSystemTool({ + name = dashx.config.toolName, + icon = dashx.config.icon_unsupported, + create = function() end, + wakeup = function() + lcd.invalidate(); + return + end, + paint = function() + local w, h = lcd.getWindowSize() + local textColor = lcd.RGB(255, 255, 255, 1) + lcd.color(textColor) + lcd.font(FONT_M) + local badVersionMsg = string.format("ETHOS < V%d.%d.%d", table.unpack(config.ethosVersion)) + local textWidth, textHeight = lcd.getTextSize(badVersionMsg) + local x = (w - textWidth) / 2 + local y = (h - textHeight) / 2 + lcd.drawText(x, y, badVersionMsg) + return + end, + close = function() end + }) return - end, - close = function () end, - }) - return - end - - -- main system tool - system.registerSystemTool({ - event = dashx.app.event, - name = dashx.config.toolName, - icon = dashx.config.icon, - create = dashx.app.create, - wakeup = dashx.app.wakeup, - paint = dashx.app.paint, - close = dashx.app.close - }) - - -- background task - system.registerTask({ - name = dashx.config.bgTaskName, - key = dashx.config.bgTaskKey, - wakeup = dashx.tasks.wakeup, - event = dashx.tasks.event, - init = dashx.tasks.init - }) - - -- widgets: use cache if valid; else rebuild - local cacheFile = "widgets.lua" - local cachePath = "cache/" .. cacheFile - local widgetList - - local loadf, loadErr = loadfile(cachePath) - if loadf then - local ok, cached = pcall(loadf) - if ok and type(cached) == "table" then - widgetList = cached - dashx.utils.log("[cache] Loaded widget list from cache","info") - else - dashx.utils.log("[cache] Bad cache, rebuilding: "..tostring(cached),"info") end - end - - if not widgetList then - widgetList = dashx.utils.findWidgets() - dashx.utils.createCacheFile(widgetList, cacheFile, true) - dashx.utils.log("[cache] Created new widgets cache file","info") - end - - -- load and register widgets - dashx.widgets = {} - for _, v in ipairs(widgetList) do - if v.script then - local scriptModule = assert(loadfile("widgets/" .. v.folder .. "/" .. v.script))(config) - local varname = v.varname or v.script:gsub("%.lua$", "") - if dashx.widgets[varname] then - math.randomseed(os.time()) - local rand = math.random() - dashx.widgets[varname .. rand] = scriptModule - else - dashx.widgets[varname] = scriptModule - end - - system.registerWidget({ - name = v.name, - key = v.key, - event = scriptModule.event, - create = scriptModule.create, - paint = scriptModule.paint, - wakeup = scriptModule.wakeup, - build = scriptModule.build, - close = scriptModule.close, - configure = scriptModule.configure, - read = scriptModule.read, - write = scriptModule.write, - persistent = scriptModule.persistent or false, - menu = scriptModule.menu, - title = scriptModule.title - }) + + system.registerSystemTool({event = dashx.app.event, name = dashx.config.toolName, icon = dashx.config.icon, create = dashx.app.create, wakeup = dashx.app.wakeup, paint = dashx.app.paint, close = dashx.app.close}) + + system.registerTask({name = dashx.config.bgTaskName, key = dashx.config.bgTaskKey, wakeup = dashx.tasks.wakeup, event = dashx.tasks.event, init = dashx.tasks.init}) + + local cacheFile = "widgets.lua" + local cachePath = "cache/" .. cacheFile + local widgetList + + local loadf, loadErr = loadfile(cachePath) + if loadf then + local ok, cached = pcall(loadf) + if ok and type(cached) == "table" then + widgetList = cached + dashx.utils.log("[cache] Loaded widget list from cache", "info") + else + dashx.utils.log("[cache] Bad cache, rebuilding: " .. tostring(cached), "info") + end + end + + if not widgetList then + widgetList = dashx.utils.findWidgets() + dashx.utils.createCacheFile(widgetList, cacheFile, true) + dashx.utils.log("[cache] Created new widgets cache file", "info") + end + + dashx.widgets = {} + for _, v in ipairs(widgetList) do + if v.script then + local scriptModule = assert(loadfile("widgets/" .. v.folder .. "/" .. v.script))(config) + local varname = v.varname or v.script:gsub("%.lua$", "") + if dashx.widgets[varname] then + math.randomseed(os.time()) + local rand = math.random() + dashx.widgets[varname .. rand] = scriptModule + else + dashx.widgets[varname] = scriptModule + end + + system.registerWidget({ + name = v.name, + key = v.key, + event = scriptModule.event, + create = scriptModule.create, + paint = scriptModule.paint, + wakeup = scriptModule.wakeup, + build = scriptModule.build, + close = scriptModule.close, + configure = scriptModule.configure, + read = scriptModule.read, + write = scriptModule.write, + persistent = scriptModule.persistent or false, + menu = scriptModule.menu, + title = scriptModule.title + }) + end end - end end -return { init = init } +return {init = init} diff --git a/scripts/dashx/sim/sensors/accx.lua b/scripts/dashx/sim/sensors/accx.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/accx.lua +++ b/scripts/dashx/sim/sensors/accx.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/accy.lua b/scripts/dashx/sim/sensors/accy.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/accy.lua +++ b/scripts/dashx/sim/sensors/accy.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/accz.lua b/scripts/dashx/sim/sensors/accz.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/accz.lua +++ b/scripts/dashx/sim/sensors/accz.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/adj_f.lua b/scripts/dashx/sim/sensors/adj_f.lua index c784044..d2978b2 100644 --- a/scripts/dashx/sim/sensors/adj_f.lua +++ b/scripts/dashx/sim/sensors/adj_f.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0.0 \ No newline at end of file +return 0.0 diff --git a/scripts/dashx/sim/sensors/adj_v.lua b/scripts/dashx/sim/sensors/adj_v.lua index c784044..d2978b2 100644 --- a/scripts/dashx/sim/sensors/adj_v.lua +++ b/scripts/dashx/sim/sensors/adj_v.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0.0 \ No newline at end of file +return 0.0 diff --git a/scripts/dashx/sim/sensors/altitude.lua b/scripts/dashx/sim/sensors/altitude.lua index 0237864..04d8f4d 100644 --- a/scripts/dashx/sim/sensors/altitude.lua +++ b/scripts/dashx/sim/sensors/altitude.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(31, 34) \ No newline at end of file +return math.random(31, 34) diff --git a/scripts/dashx/sim/sensors/armdisableflags.lua b/scripts/dashx/sim/sensors/armdisableflags.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/armdisableflags.lua +++ b/scripts/dashx/sim/sensors/armdisableflags.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/armed.lua b/scripts/dashx/sim/sensors/armed.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/armed.lua +++ b/scripts/dashx/sim/sensors/armed.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/armflags.lua b/scripts/dashx/sim/sensors/armflags.lua index c784044..d2978b2 100644 --- a/scripts/dashx/sim/sensors/armflags.lua +++ b/scripts/dashx/sim/sensors/armflags.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0.0 \ No newline at end of file +return 0.0 diff --git a/scripts/dashx/sim/sensors/attpitch.lua b/scripts/dashx/sim/sensors/attpitch.lua index 54006cd..30674f3 100644 --- a/scripts/dashx/sim/sensors/attpitch.lua +++ b/scripts/dashx/sim/sensors/attpitch.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(80, 120) \ No newline at end of file +return math.random(80, 120) diff --git a/scripts/dashx/sim/sensors/attroll.lua b/scripts/dashx/sim/sensors/attroll.lua index 54006cd..30674f3 100644 --- a/scripts/dashx/sim/sensors/attroll.lua +++ b/scripts/dashx/sim/sensors/attroll.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(80, 120) \ No newline at end of file +return math.random(80, 120) diff --git a/scripts/dashx/sim/sensors/attyaw.lua b/scripts/dashx/sim/sensors/attyaw.lua index c5953e8..94198b5 100644 --- a/scripts/dashx/sim/sensors/attyaw.lua +++ b/scripts/dashx/sim/sensors/attyaw.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(160, 240) \ No newline at end of file +return math.random(160, 240) diff --git a/scripts/dashx/sim/sensors/bec_voltage.lua b/scripts/dashx/sim/sensors/bec_voltage.lua index b47c1a1..3862325 100644 --- a/scripts/dashx/sim/sensors/bec_voltage.lua +++ b/scripts/dashx/sim/sensors/bec_voltage.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(760, 840) \ No newline at end of file +return math.random(760, 840) diff --git a/scripts/dashx/sim/sensors/cell_count.lua b/scripts/dashx/sim/sensors/cell_count.lua index 74ad81e..a7f7044 100644 --- a/scripts/dashx/sim/sensors/cell_count.lua +++ b/scripts/dashx/sim/sensors/cell_count.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 6.0 \ No newline at end of file +return 6.0 diff --git a/scripts/dashx/sim/sensors/consumption.lua b/scripts/dashx/sim/sensors/consumption.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/consumption.lua +++ b/scripts/dashx/sim/sensors/consumption.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/current.lua b/scripts/dashx/sim/sensors/current.lua index bf061af..69c99c0 100644 --- a/scripts/dashx/sim/sensors/current.lua +++ b/scripts/dashx/sim/sensors/current.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(0, 0) \ No newline at end of file +return math.random(0, 0) diff --git a/scripts/dashx/sim/sensors/flightmode.lua b/scripts/dashx/sim/sensors/flightmode.lua index e69de29..4d314b8 100644 --- a/scripts/dashx/sim/sensors/flightmode.lua +++ b/scripts/dashx/sim/sensors/flightmode.lua @@ -0,0 +1,5 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + diff --git a/scripts/dashx/sim/sensors/fuel.lua b/scripts/dashx/sim/sensors/fuel.lua index 842c2b6..384e61c 100644 --- a/scripts/dashx/sim/sensors/fuel.lua +++ b/scripts/dashx/sim/sensors/fuel.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(84, 85) \ No newline at end of file +return math.random(84, 85) diff --git a/scripts/dashx/sim/sensors/governor.lua b/scripts/dashx/sim/sensors/governor.lua index c784044..d2978b2 100644 --- a/scripts/dashx/sim/sensors/governor.lua +++ b/scripts/dashx/sim/sensors/governor.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0.0 \ No newline at end of file +return 0.0 diff --git a/scripts/dashx/sim/sensors/gps_sats.lua b/scripts/dashx/sim/sensors/gps_sats.lua index f48bc8d..6d5b14f 100644 --- a/scripts/dashx/sim/sensors/gps_sats.lua +++ b/scripts/dashx/sim/sensors/gps_sats.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 6 \ No newline at end of file +return 6 diff --git a/scripts/dashx/sim/sensors/groundspeed.lua b/scripts/dashx/sim/sensors/groundspeed.lua index 2b2876e..1f51c15 100644 --- a/scripts/dashx/sim/sensors/groundspeed.lua +++ b/scripts/dashx/sim/sensors/groundspeed.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(57, 63) \ No newline at end of file +return math.random(57, 63) diff --git a/scripts/dashx/sim/sensors/isconnected.lua b/scripts/dashx/sim/sensors/isconnected.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/isconnected.lua +++ b/scripts/dashx/sim/sensors/isconnected.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/pid_profile.lua b/scripts/dashx/sim/sensors/pid_profile.lua index 7c7433a..a702531 100644 --- a/scripts/dashx/sim/sensors/pid_profile.lua +++ b/scripts/dashx/sim/sensors/pid_profile.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 1.0 \ No newline at end of file +return 1.0 diff --git a/scripts/dashx/sim/sensors/profile.lua b/scripts/dashx/sim/sensors/profile.lua index 84ce43f..0905b17 100644 --- a/scripts/dashx/sim/sensors/profile.lua +++ b/scripts/dashx/sim/sensors/profile.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 1 \ No newline at end of file +return 1 diff --git a/scripts/dashx/sim/sensors/rate_profile.lua b/scripts/dashx/sim/sensors/rate_profile.lua index 7c7433a..a702531 100644 --- a/scripts/dashx/sim/sensors/rate_profile.lua +++ b/scripts/dashx/sim/sensors/rate_profile.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 1.0 \ No newline at end of file +return 1.0 diff --git a/scripts/dashx/sim/sensors/rpm.lua b/scripts/dashx/sim/sensors/rpm.lua index 22c8748..74e9162 100644 --- a/scripts/dashx/sim/sensors/rpm.lua +++ b/scripts/dashx/sim/sensors/rpm.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(1620, 1980) \ No newline at end of file +return math.random(1620, 1980) diff --git a/scripts/dashx/sim/sensors/simevent_telemetry_state.lua b/scripts/dashx/sim/sensors/simevent_telemetry_state.lua index f8af46d..6d22a6a 100644 --- a/scripts/dashx/sim/sensors/simevent_telemetry_state.lua +++ b/scripts/dashx/sim/sensors/simevent_telemetry_state.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return 0 \ No newline at end of file +return 0 diff --git a/scripts/dashx/sim/sensors/temp_esc.lua b/scripts/dashx/sim/sensors/temp_esc.lua index fc5e1c9..c51f54c 100644 --- a/scripts/dashx/sim/sensors/temp_esc.lua +++ b/scripts/dashx/sim/sensors/temp_esc.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(47, 52) \ No newline at end of file +return math.random(47, 52) diff --git a/scripts/dashx/sim/sensors/temp_mcu.lua b/scripts/dashx/sim/sensors/temp_mcu.lua index 4e88d54..770c35d 100644 --- a/scripts/dashx/sim/sensors/temp_mcu.lua +++ b/scripts/dashx/sim/sensors/temp_mcu.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(28, 31) \ No newline at end of file +return math.random(28, 31) diff --git a/scripts/dashx/sim/sensors/throttle_percent.lua b/scripts/dashx/sim/sensors/throttle_percent.lua index 26eabf7..65d9168 100644 --- a/scripts/dashx/sim/sensors/throttle_percent.lua +++ b/scripts/dashx/sim/sensors/throttle_percent.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(76, 84) \ No newline at end of file +return math.random(76, 84) diff --git a/scripts/dashx/sim/sensors/voltage.lua b/scripts/dashx/sim/sensors/voltage.lua index 76f25ba..4967a5d 100644 --- a/scripts/dashx/sim/sensors/voltage.lua +++ b/scripts/dashx/sim/sensors/voltage.lua @@ -1,2 +1,7 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") -return math.random(2520, 2520) \ No newline at end of file +return math.random(2520, 2520) diff --git a/scripts/dashx/tasks/callback/callback.lua b/scripts/dashx/tasks/callback/callback.lua index 2d5513a..add56f1 100644 --- a/scripts/dashx/tasks/callback/callback.lua +++ b/scripts/dashx/tasks/callback/callback.lua @@ -1,25 +1,10 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- --- local arg = {...} local config = arg[1] @@ -28,21 +13,13 @@ local loadedSensorModule = nil callback._queue = {} -local function get_time() - return os.clock() -end +local function get_time() return os.clock() end -function callback.now(callbackParam) - table.insert(callback._queue, {time = nil, func = callbackParam, repeat_interval = nil}) -end +function callback.now(callbackParam) table.insert(callback._queue, {time = nil, func = callbackParam, repeat_interval = nil}) end -function callback.inSeconds(seconds, callbackParam) - table.insert(callback._queue, {time = get_time() + seconds, func = callbackParam, repeat_interval = nil}) -end +function callback.inSeconds(seconds, callbackParam) table.insert(callback._queue, {time = get_time() + seconds, func = callbackParam, repeat_interval = nil}) end -function callback.every(seconds, callbackParam) - table.insert(callback._queue, {time = get_time() + seconds, func = callbackParam, repeat_interval = seconds}) -end +function callback.every(seconds, callbackParam) table.insert(callback._queue, {time = get_time() + seconds, func = callbackParam, repeat_interval = seconds}) end function callback.wakeup() local now = get_time() @@ -63,21 +40,10 @@ function callback.wakeup() end end -function callback.clear(callbackParam) - for i = #callback._queue, 1, -1 do - if callback._queue[i].func == callbackParam then - table.remove(callback._queue, i) - end - end -end +function callback.clear(callbackParam) for i = #callback._queue, 1, -1 do if callback._queue[i].func == callbackParam then table.remove(callback._queue, i) end end end -function callback.clearAll() - callback._queue = {} -end - -function callback.reset() - callback.clearAll() -end +function callback.clearAll() callback._queue = {} end +function callback.reset() callback.clearAll() end return callback diff --git a/scripts/dashx/tasks/callback/init.lua b/scripts/dashx/tasks/callback/init.lua index bdf769e..0919688 100644 --- a/scripts/dashx/tasks/callback/init.lua +++ b/scripts/dashx/tasks/callback/init.lua @@ -1,27 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.1, -- run every 0.025 seconds - script = "callback.lua", -- run this script - spreadschedule = false, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.1, script = "callback.lua", spreadschedule = false, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/developer/developer.lua b/scripts/dashx/tasks/developer/developer.lua index 56550cf..44c322a 100644 --- a/scripts/dashx/tasks/developer/developer.lua +++ b/scripts/dashx/tasks/developer/developer.lua @@ -1,72 +1,26 @@ -local dashx = require("dashx") --[[ - - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - - * This task is a developer debug tool. It simply provides a simple - * point in which to inject api queries for debugging or creating new - * api libraries. Default behavious should always be to not run this - * loop. Its up to the developer to ensure that after debug he flags - * the task to not run. - - * This can be done by setting the ENABLE_TASK flag to true or false. - - * It can be usefull when using the task to enable the preferences.developer.logmsp - * flag in main.lua. This will print out the msp request and response. - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local ENABLE_TASK = false local arg = {...} local developer = {} - - function developer.wakeup() - -- quick exit - this is the normal behaviour - if ENABLE_TASK == false then - return - end - - --[[ - -- This is an example of how to use the api library to query the governor mode - dashx.utils.log("API Debug Task: GOVERNOR_CONFIG", "info") - local API = dashx.tasks.msp.api.load("GOVERNOR_CONFIG") - API.setCompleteHandler(function(self, buf) - local governorMode = API.readValue("gov_mode") - dashx.utils.log("Governor mode: " .. governorMode, "info") - dashx.session.governorMode = governorMode - end) - API.setUUID("123e4567-e89b-12d3-a456-426614174000") - API.read() - ]]-- + if ENABLE_TASK == false then return end dashx.utils.log("API Debug Task: TELEMETRY_CONFIG", "info") local API = dashx.tasks.msp.api.load("TELEMETRY_CONFIG") - API.setCompleteHandler(function(self, buf) - end) + API.setCompleteHandler(function(self, buf) end) API.setUUID("123e4567-e89b-12d3-a456-426614174000") API.read() - end return developer diff --git a/scripts/dashx/tasks/developer/init.lua b/scripts/dashx/tasks/developer/init.lua index 528e82a..10c255e 100644 --- a/scripts/dashx/tasks/developer/init.lua +++ b/scripts/dashx/tasks/developer/init.lua @@ -1,28 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 5, -- run every 5 seconds - script = "developer.lua", -- run this script - linkrequired = false, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = true, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 5, script = "developer.lua", linkrequired = false, spreadschedule = true, simulatoronly = true} return init diff --git a/scripts/dashx/tasks/events/events.lua b/scripts/dashx/tasks/events/events.lua index fd4ef2f..fedfc44 100644 --- a/scripts/dashx/tasks/events/events.lua +++ b/scripts/dashx/tasks/events/events.lua @@ -1,33 +1,20 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") -local arg = { ... } +local arg = {...} local config = arg[1] local events = {} local telemetryStartTime = nil local wakeupStep = 0 local wakeupHandlers = {} --- List of task module names (must match the .lua filenames) -local taskNames = { "telemetry", "switches", "flightmode", "stats", "rxmap", "timer" } -local taskExecutionPercent = 50 -- 50% of tasks will run each cycle +local taskNames = {"telemetry", "switches", "flightmode", "stats", "rxmap", "timer"} +local taskExecutionPercent = 50 --- Dynamically load task modules and populate wakeupHandlers for _, name in ipairs(taskNames) do events[name] = assert(loadfile("tasks/events/tasks/" .. name .. ".lua"))(dashx.config) table.insert(wakeupHandlers, function() events[name].wakeup() end) @@ -37,17 +24,11 @@ function events.wakeup() local currentTime = os.clock() if dashx.session.isConnected and dashx.session.telemetryState then - if telemetryStartTime == nil then - telemetryStartTime = currentTime - end + if telemetryStartTime == nil then telemetryStartTime = currentTime end - -- Wait 2.5 seconds after telemetry becomes active - if (currentTime - telemetryStartTime) < 2.5 then - return - end + if (currentTime - telemetryStartTime) < 2.5 then return end - -- Determine how many tasks to run this cycle based on config - local percent = taskExecutionPercent or 25 -- Default to 25% if not set + local percent = taskExecutionPercent or 25 local tasksPerWakeup = math.max(1, math.floor((percent / 100) * #wakeupHandlers)) for i = 1, tasksPerWakeup do @@ -60,9 +41,7 @@ function events.wakeup() end end -function events.reset() - telemetryStartTime = nil -end +function events.reset() telemetryStartTime = nil end return events diff --git a/scripts/dashx/tasks/events/init.lua b/scripts/dashx/tasks/events/init.lua index 7c56534..44c19b1 100644 --- a/scripts/dashx/tasks/events/init.lua +++ b/scripts/dashx/tasks/events/init.lua @@ -1,27 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.1, -- run every 0.1 seconds - script = "events.lua", -- run this script - linkrequired = true, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.1, script = "events.lua", linkrequired = true, spreadschedule = true, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/events/tasks/flightmode.lua b/scripts/dashx/tasks/events/tasks/flightmode.lua index 06f9505..d5eb12d 100644 --- a/scripts/dashx/tasks/events/tasks/flightmode.lua +++ b/scripts/dashx/tasks/events/tasks/flightmode.lua @@ -1,22 +1,11 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- - -local arg = { ... } + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") + +local arg = {...} local config = arg[1] local flightmode = {} @@ -24,59 +13,39 @@ local lastFlightMode = nil local hasBeenInFlight = false local inflight_start_time = nil - ---- Determines if the flight mode is considered "in flight". --- This function checks two main conditions to decide if the model is in flight: --- 1. If the governor sensor is active (highest priority). --- 2. If the throttle has been above zero for a sustained period. --- The function also ensures telemetry is active and the session is armed before proceeding. --- @return boolean True if the model is considered in flight, false otherwise. function flightmode.inFlight() local telemetry = dashx.tasks.telemetry - if not telemetry.active() then - return false - end + if not telemetry.active() then return false end local inflight = telemetry.getSensor("inflight") local armed = telemetry.getSensor("armed") local delay = dashx.session.modelPreferences.model.inflightswitch_delay or 10 - -- Both sensors indicate "not armed" and "not inflight" if armed == 0 and inflight == 0 then if not inflight_start_time then - -- Start the timer + inflight_start_time = os.time() print("Starting inflight timer") elseif os.difftime(os.time(), inflight_start_time) >= delay then - -- Delay has passed + print("In flight confirmed after delay") return true end else - -- Reset timer if condition is broken + inflight_start_time = nil end return false end ---- Resets the flight mode state. --- This function clears the last flight mode, resets the flight status, --- and clears the throttle start time. It is typically used to reinitialize --- the flight mode tracking variables to their default states. function flightmode.reset() lastFlightMode = nil hasBeenInFlight = false - inflight_start_time = nil + inflight_start_time = nil end ---- Determines the current flight mode based on session state and flight status. --- This function checks the current session's flight mode and connection status, --- as well as the result of `flightmode.inFlight()`, to decide whether the mode --- should be "preflight", "inflight", or "postflight". --- It also manages the `hasBeenInFlight` flag to track if the system has ever been in flight. --- @return string The determined flight mode: "preflight", "inflight", or "postflight". local function determineMode() if dashx.flightmode.current == "inflight" and not dashx.session.isConnected then hasBeenInFlight = false @@ -91,9 +60,6 @@ local function determineMode() return hasBeenInFlight and "postflight" or "preflight" end ---- Wakes up the flight mode task and updates the current flight mode if it has changed. --- Determines the current flight mode using `determineMode()`. If the mode has changed since the last check, --- logs the new flight mode, updates the session's flight mode, and stores the new mode as the last known mode. function flightmode.wakeup() local mode = determineMode() diff --git a/scripts/dashx/tasks/events/tasks/rxmap.lua b/scripts/dashx/tasks/events/tasks/rxmap.lua index 120d8f5..05021e0 100644 --- a/scripts/dashx/tasks/events/tasks/rxmap.lua +++ b/scripts/dashx/tasks/events/tasks/rxmap.lua @@ -1,36 +1,16 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") ---[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- -local arg = { ... } +local arg = {...} local config = arg[1] local rxmap = {} -local channelNames = { - "aileron", - "elevator", - "collective", - "rudder", - "arm", - "throttle", - "headspeed", - "mode" -} +local channelNames = {"aileron", "elevator", "collective", "rudder", "arm", "throttle", "headspeed", "mode"} local channelSources = {} local initialized = false @@ -40,10 +20,8 @@ local function initChannelSources() for _, name in ipairs(channelNames) do local member = rxMap[name] if member then - local src = system.getSource({ category = CATEGORY_CHANNEL, member = member, options = 0 }) - if src then - channelSources[name] = src - end + local src = system.getSource({category = CATEGORY_CHANNEL, member = member, options = 0}) + if src then channelSources[name] = src end end end initialized = true @@ -52,16 +30,12 @@ end function rxmap.wakeup() if not dashx.utils.rxmapReady() then return end - if not initialized then - initChannelSources() - end + if not initialized then initChannelSources() end for name, src in pairs(channelSources) do if src then local val = src:value() - if val ~= nil then - dashx.session.rx.values[name] = val - end + if val ~= nil then dashx.session.rx.values[name] = val end end end end diff --git a/scripts/dashx/tasks/events/tasks/stats.lua b/scripts/dashx/tasks/events/tasks/stats.lua index b7d89f8..bee0d65 100644 --- a/scripts/dashx/tasks/events/tasks/stats.lua +++ b/scripts/dashx/tasks/events/tasks/stats.lua @@ -1,24 +1,11 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") ---[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- - --- Optimized stats.lua - -local arg = { ... } + +local arg = {...} local config = arg[1] local stats = {} @@ -40,16 +27,13 @@ local function buildFilteredList() elseif type(mt) == "function" then local ok, result = pcall(mt) - if ok and result then - filteredSensors[sensorKey] = sensorDef - end + if ok and result then filteredSensors[sensorKey] = sensorDef end end end end function stats.wakeup() - -- we start this in wakeup as telemetry may not be set when this tasks starts if not telemetry then telemetry = dashx.tasks.telemetry return @@ -67,40 +51,30 @@ function stats.wakeup() buildFilteredList() end - if not telemetry.sensorStats then - telemetry.sensorStats = {} - end + if not telemetry.sensorStats then telemetry.sensorStats = {} end local statsTable = telemetry.sensorStats for sensorKey, _ in pairs(filteredSensors) do local val = telemetry.getSensor(sensorKey) if val and type(val) == "number" then - if not statsTable[sensorKey] then - statsTable[sensorKey] = { - min = math.huge, - max = -math.huge, - sum = 0, - count = 0, - avg = 0 - } - end + if not statsTable[sensorKey] then statsTable[sensorKey] = {min = math.huge, max = -math.huge, sum = 0, count = 0, avg = 0} end local entry = statsTable[sensorKey] - entry.min = math.min(entry.min, val) - entry.max = math.max(entry.max, val) - entry.sum = entry.sum + val + entry.min = math.min(entry.min, val) + entry.max = math.max(entry.max, val) + entry.sum = entry.sum + val entry.count = entry.count + 1 - entry.avg = entry.sum / entry.count + entry.avg = entry.sum / entry.count end end end function stats.reset() telemetry.sensorStats = {} - fullSensorTable = nil - filteredSensors = nil - lastTrackTime = 0 + fullSensorTable = nil + filteredSensors = nil + lastTrackTime = 0 end return stats diff --git a/scripts/dashx/tasks/events/tasks/switches.lua b/scripts/dashx/tasks/events/tasks/switches.lua index a1d96c6..3e08e9e 100644 --- a/scripts/dashx/tasks/events/tasks/switches.lua +++ b/scripts/dashx/tasks/events/tasks/switches.lua @@ -1,48 +1,21 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- - -local arg = { ... } + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") + +local arg = {...} local config = arg[1] local switches = {} -local switchTable = { - switches = {}, - units = {}, -} +local switchTable = {switches = {}, units = {}} -local lastPlayTime = {} +local lastPlayTime = {} local lastSwitchState = {} local switchStartTime = nil ---- Initializes the switchTable with switch sources and sensor audio units based on user preferences. --- --- This function retrieves the user's switch preferences from `dashx.preferences.switches`. --- For each valid preference entry, it parses the category and member values, converts them to numbers, --- and uses `system.getSource` to obtain the corresponding switch source, which is then stored in `switchTable.switches`. --- Finally, it populates `switchTable.units` with the list of sensor audio units from telemetry. --- --- @function initializeSwitches --- @usage --- initializeSwitches() --- @see dashx.preferences.switches --- @see system.getSource --- @see dashx.tasks.telemetry.listSensorAudioUnits local function initializeSwitches() local prefs = dashx.preferences.switches if not prefs then return end @@ -51,54 +24,20 @@ local function initializeSwitches() if v then local scategory, smember = v:match("([^,]+),([^,]+)") scategory = tonumber(scategory) - smember = tonumber(smember) - if scategory and smember then - switchTable.switches[key] = system.getSource({ - category = scategory, - member = smember - }) - end + smember = tonumber(smember) + if scategory and smember then switchTable.switches[key] = system.getSource({category = scategory, member = smember}) end end end switchTable.units = dashx.tasks.telemetry.listSensorAudioUnits() end ---- Handles the periodic wakeup logic for monitoring and announcing switch states. --- --- This function checks the state of switches defined in `switchTable.switches`. --- It initializes the switches if they are not already set up, and ensures that --- at least 5 seconds have passed since the function was first called before processing. --- --- For each switch: --- - If the switch is active and either was previously inactive or at least 10 seconds --- have passed since the last announcement, it plays the current sensor value using --- `system.playNumber`. --- - The function tracks the last state and last play time for each switch to avoid --- repeated announcements. --- --- Dependencies: --- - `os.clock()`: Current time reference. --- - `switchTable.switches`: Table of switch sensor objects. --- - `dashx.tasks.telemetry.getSensorSource(key)`: Retrieves the sensor source for a switch. --- - `system.playNumber(value, unit, decimals)`: Announces the sensor value. --- --- Globals used: --- - `switchStartTime`: Timestamp of the first wakeup call. --- - `lastSwitchState`: Table storing the last known state of each switch. --- - `lastPlayTime`: Table storing the last announcement time for each switch. --- --- No return value. function switches.wakeup() local now = os.clock() - if next(switchTable.switches) == nil then - initializeSwitches() - end + if next(switchTable.switches) == nil then initializeSwitches() end - if not switchStartTime then - switchStartTime = now - end + if not switchStartTime then switchStartTime = now end if (now - switchStartTime) <= 5 then return end @@ -106,9 +45,9 @@ function switches.wakeup() local currentState = sensor:state() if currentState == nil then goto continue end - local prevState = lastSwitchState[key] or false - local lastTime = lastPlayTime[key] or 0 - local playNow = false + local prevState = lastSwitchState[key] or false + local lastTime = lastPlayTime[key] or 0 + local playNow = false if not currentState then goto skip_play @@ -121,7 +60,7 @@ function switches.wakeup() if sensorSrc then local value = sensorSrc:value() if value and type(value) == "number" then - local unit = switchTable.units[key] + local unit = switchTable.units[key] local decimals = tonumber(sensorSrc:decimals()) system.playNumber(value, unit, decimals) lastPlayTime[key] = now @@ -135,20 +74,13 @@ function switches.wakeup() end end ---- Resets the state of all switches and related tracking variables. --- This function clears the `switchTable.switches` table, resets the `lastPlayTime` --- and `lastSwitchState` tables, and sets `switchStartTime` to nil. --- It is typically used to reinitialize switch states, for example when starting a new task or event. function switches.resetSwitchStates() - switchTable.switches = {} - lastPlayTime = {} - lastSwitchState = {} - switchStartTime = nil + switchTable.switches = {} + lastPlayTime = {} + lastSwitchState = {} + switchStartTime = nil end ---- Assigns the provided `switchTable` to the `switches.switchTable` property. --- This allows access to the table of switch configurations or states via the `switches` module. --- @field switchTable table: A table containing switch definitions or states. switches.switchTable = switchTable -return switches \ No newline at end of file +return switches diff --git a/scripts/dashx/tasks/events/tasks/telemetry.lua b/scripts/dashx/tasks/events/tasks/telemetry.lua index d2e9acc..fa1cfb9 100644 --- a/scripts/dashx/tasks/events/tasks/telemetry.lua +++ b/scripts/dashx/tasks/events/tasks/telemetry.lua @@ -1,29 +1,18 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- - -local arg = { ... } + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") + +local arg = {...} local config = arg[1] local telemetry = {} local lastEventTimes = {} -local lastValues = {} -local lastPlayTime = {} +local lastValues = {} +local lastPlayTime = {} local userpref = dashx.preferences local enabledEvents = (userpref and userpref.events) or {} @@ -35,14 +24,14 @@ local eventTable = { local session = dashx.session if not session.batteryConfig then return end - local cellCount = session.batteryConfig.batteryCellCount + local cellCount = session.batteryConfig.batteryCellCount local warnVoltage = session.batteryConfig.vbatwarningcellvoltage - local minVoltage = session.batteryConfig.vbatmincellvoltage + local minVoltage = session.batteryConfig.vbatmincellvoltage local collective = session.rx.values['collective'] or 0 - local aileron = session.rx.values['aileron'] or 0 - local elevator = session.rx.values['elevator'] or 0 - local rudder = session.rx.values['rudder'] or 0 + local aileron = session.rx.values['aileron'] or 0 + local elevator = session.rx.values['elevator'] or 0 + local rudder = session.rx.values['rudder'] or 0 if not (cellCount and warnVoltage and minVoltage) then return end @@ -52,55 +41,17 @@ local eventTable = { local suppressionPercent = userpref.general.gimbalsupression or 0.85 local suppressionLimit = suppressionPercent * 1024 - --if math.abs(collective) > suppressionLimit or - -- math.abs(aileron) > suppressionLimit or - -- math.abs(elevator) > suppressionLimit or - -- math.abs(rudder) > suppressionLimit then - -- return - --end - - if cellVoltage < warnVoltage then - dashx.utils.playFile("events", "alerts/lowvoltage.wav") - end - end, - interval = 10 - }, - { - sensor = "smartfuel", - event = function(value) - -- Play the alert every interval if fuel is 10% or below - if value and value <= 10 then - dashx.utils.playFile("events", "alerts/lowfuel.wav") - end + if cellVoltage < warnVoltage then dashx.utils.playFile("events", "alerts/lowvoltage.wav") end end, interval = 10 - }, - { + }, {sensor = "smartfuel", event = function(value) if value and value <= 10 then dashx.utils.playFile("events", "alerts/lowfuel.wav") end end, interval = 10}, { sensor = "armed", event = function(value) - if value == 0 then - dashx.utils.playFile("events", "alerts/armed.wav") - end - if value == 1 then - dashx.utils.playFile("events", "alerts/disarmed.wav") - end - end, - debounce = 0.25 - }, - { - sensor = "inflight", - event = function(value) - if dashx.tasks.telemetry.getSensorSource("armed"):value() == 0 then - -- if value == 0 then - -- dashx.utils.playFile("events", "alerts/inflight.wav") - -- end - -- if value == 1 then - -- dashx.utils.playFile("events", "alerts/idledown.wav") - -- end - end + if value == 0 then dashx.utils.playFile("events", "alerts/armed.wav") end + if value == 1 then dashx.utils.playFile("events", "alerts/disarmed.wav") end end, debounce = 0.25 - }, + }, {sensor = "inflight", event = function(value) if dashx.tasks.telemetry.getSensorSource("armed"):value() == 0 then end end, debounce = 0.25} } function telemetry.wakeup() @@ -136,4 +87,4 @@ end telemetry.eventTable = eventTable -return telemetry \ No newline at end of file +return telemetry diff --git a/scripts/dashx/tasks/events/tasks/timer.lua b/scripts/dashx/tasks/events/tasks/timer.lua index b63e19a..0ea29b3 100644 --- a/scripts/dashx/tasks/events/tasks/timer.lua +++ b/scripts/dashx/tasks/events/tasks/timer.lua @@ -1,22 +1,11 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") -local arg = { ... } +local arg = {...} local config = arg[1] local timer = {} @@ -29,21 +18,18 @@ function timer.wakeup() local batteryConfig = session and session.batteryConfig local targetSeconds = batteryConfig and batteryConfig.modelFlightTime or 0 - -- Only trigger if the feature is configured and flight time is available if not targetSeconds or targetSeconds == 0 or not modelFlightTime or modelFlightTime == 0 then triggered = false lastBeepTime = nil return end - -- Only trigger if we are armed / inflight if dashx.flightmode.current ~= "inflight" then triggered = false lastBeepTime = nil return end - -- If flight time exceeds or equals the target, handle beeping if modelFlightTime >= targetSeconds then local now = os.clock() if not triggered then diff --git a/scripts/dashx/tasks/logger/init.lua b/scripts/dashx/tasks/logger/init.lua index c4033df..60715d3 100644 --- a/scripts/dashx/tasks/logger/init.lua +++ b/scripts/dashx/tasks/logger/init.lua @@ -1,28 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.25, -- run every 0.25 seconds - script = "logger.lua", -- run this script - linkrequired = false, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = true, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.25, script = "logger.lua", linkrequired = false, spreadschedule = true, simulatoronly = true} return init diff --git a/scripts/dashx/tasks/logger/lib/log.lua b/scripts/dashx/tasks/logger/lib/log.lua index 51d1a1d..903fe9d 100644 --- a/scripts/dashx/tasks/logger/lib/log.lua +++ b/scripts/dashx/tasks/logger/lib/log.lua @@ -1,117 +1,33 @@ -local dashx = require("dashx") --[[ - - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local logs = {} ---[[ -logs.config: Configuration table for logging settings. - -Fields: -- enabled (boolean): Enable or disable logging. -- log_to_file (boolean): Enable or disable logging to a file. -- print_interval (number): Interval in seconds for printing logs to the console. -- disk_write_interval (number): Interval in seconds for writing logs to disk. -- max_line_length (number): Maximum length of a log line. -- min_print_level (string): Minimum log level to print (e.g., "info"). -- log_file (string): Name of the log file. -- prefix (string): Prefix to add to each log message. -]] -logs.config = { - enabled = true, - log_to_file = true, - print_interval = 0.5, - disk_write_interval = 5.0, - max_line_length = 100, - min_print_level = "info", - log_file = "log.txt", - prefix = "" -} +logs.config = {enabled = true, log_to_file = true, print_interval = 0.5, disk_write_interval = 5.0, max_line_length = 100, min_print_level = "info", log_file = "log.txt", prefix = ""} +if system:getVersion().simulation == true then logs.config.print_interval = 0.025 end ---[[ - Checks if the system is running in simulation mode. - If true, sets the log print interval to 0.025 seconds. -]] -if system:getVersion().simulation == true then - logs.config.print_interval = 0.025 -end - ---[[ - logs.queue: Table to store log messages for console output. - logs.disk_queue: Table to store log messages for batched disk writes. - logs.last_print_time: Timestamp of the last console output. - logs.last_disk_write_time: Timestamp of the last disk write. -]] -logs.queue = {} -logs.disk_queue = {} +logs.queue = {} +logs.disk_queue = {} logs.last_print_time = os.clock() logs.last_disk_write_time = os.clock() +logs.levels = {debug = 0, info = 1, off = 2} ---[[ - Table `logs.levels` defines different logging levels. - The levels are: - - `debug`: Level 0, used for debugging messages. - - `info`: Level 1, used for informational messages. - - `off`: Level 2, used to turn off logging. -]] -logs.levels = { - debug = 0, - info = 1, - off = 2 -} - ---[[ -Splits a message into multiple lines if it exceeds a specified maximum length. - -@param message (string) The message to be split. -@param max_length (number) The maximum length of each line. -@param prefix (string) The prefix to be added to each subsequent line after the first. - -@return (table) A table containing the split lines of the message. -]] local function split_message(message, max_length, prefix) local lines = {} while #message > max_length do table.insert(lines, message:sub(1, max_length)) message = prefix .. message:sub(max_length + 1) end - if #message > 0 then - table.insert(lines, message) - end + if #message > 0 then table.insert(lines, message) end return lines end - ---[[ -Logs a message with a specified log level. - -Parameters: -- message (string): The message to log. -- level (string, optional): The log level (e.g., "info", "error"). Defaults to "info". - -The function checks if logging is enabled and if the specified log level is above the minimum print level. -If the conditions are met, it formats the message with a prefix and splits it into lines if necessary. -The message is then added to the console queue and, if file logging is enabled, to the disk queue. - -Returns: -- None -]] function logs.add(message, level) if not logs.config.enabled or logs.config.min_print_level == "off" then return end @@ -119,11 +35,8 @@ function logs.add(message, level) if logs.levels[level] == nil then return end if logs.levels[level] < logs.levels[logs.config.min_print_level] then return end - -- Truncate extremely long messages (configurable cap) local max_message_length = logs.config.max_line_length * 10 - if #message > max_message_length then - message = message:sub(1, max_message_length) .. " [truncated]" - end + if #message > max_message_length then message = message:sub(1, max_message_length) .. " [truncated]" end local prefix = logs.config.prefix .. " [" .. level .. "] " local log_entry = prefix .. message @@ -135,27 +48,11 @@ function logs.add(message, level) lines = split_message(log_entry, logs.config.max_line_length, string.rep(" ", #prefix)) end - for _, line in ipairs(lines) do - table.insert(logs.queue, line) - end + for _, line in ipairs(lines) do table.insert(logs.queue, line) end - if logs.config.log_to_file then - table.insert(logs.disk_queue, log_entry) - end + if logs.config.log_to_file then table.insert(logs.disk_queue, log_entry) end end - - ---[[ - Function: logs.process_console_queue - Description: Processes the console log queue by printing messages at a specified interval. - If logging is disabled or the minimum print level is set to "off", the function returns immediately. - Otherwise, it prints the next message in the queue if the configured print interval has elapsed. - - Parameters: None - - Returns: None -]] local function process_console_queue() if not logs.config.enabled or logs.config.min_print_level == "off" then return end @@ -171,26 +68,6 @@ local function process_console_queue() end end ---[[ - Function: logs.process_disk_queue - Description: Processes the disk queue by writing log messages to a file if certain conditions are met. - Conditions: - - Logging is enabled. - - Minimum print level is not set to "off". - - Logging to file is enabled. - Behavior: - - Checks the current time and compares it with the last disk write time. - - If the time interval since the last write is greater than or equal to the configured disk write interval and there are messages in the disk queue, it writes the messages to the log file. - - Clears the disk queue after writing. - Dependencies: - - logs.config.enabled: Boolean indicating if logging is enabled. - - logs.config.min_print_level: String indicating the minimum print level. - - logs.config.log_to_file: Boolean indicating if logging to file is enabled. - - logs.config.disk_write_interval: Number indicating the interval between disk writes. - - logs.config.log_file: String indicating the path to the log file. - - logs.last_disk_write_time: Number indicating the last time logs were written to disk. - - logs.disk_queue: Table containing log messages to be written to disk. -]] local function process_disk_queue() if not logs.config.enabled or logs.config.min_print_level == "off" or not logs.config.log_to_file then return end @@ -210,12 +87,6 @@ local function process_disk_queue() end end - ---[[ - Function: logs.process - Description: Processes the console and disk queues by calling the respective functions. - This function is responsible for handling log processing tasks. -]] function logs.process() process_console_queue() process_disk_queue() diff --git a/scripts/dashx/tasks/logger/logger.lua b/scripts/dashx/tasks/logger/logger.lua index 9213e8d..7c45c59 100644 --- a/scripts/dashx/tasks/logger/logger.lua +++ b/scripts/dashx/tasks/logger/logger.lua @@ -1,67 +1,31 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- --- local arg = {...} local config = arg[1] local logger = {} --- --- This script initializes the logging configuration for the dashx module. --- --- The logging configuration is loaded from the "lib/log.lua" file and is --- customized based on the provided configuration (`config`). --- --- The log file is named using the current date and time in the format --- "logs/dashx_YYYY-MM-DD_HH-MM-SS.log". --- --- The minimum print level for logging is set from `dashx.preferences.developer.loglevel`. --- --- The option to log to a file is set from `preferences.developer.logtofile`. --- --- If the system is running in simulation mode, the log print interval is --- set to 0.1 seconds. --- logging os.mkdir("LOGS:") os.mkdir("LOGS:/dashx") os.mkdir("LOGS:/dashx/logs") logger.queue = assert(loadfile("tasks/logger/lib/log.lua"))(config) logger.queue.config.log_file = "LOGS:/dashx/logs/dashx_" .. os.date("%Y-%m-%d_%H-%M-%S") .. ".log" -logger.queue.config.min_print_level = dashx.preferences.developer.loglevel +logger.queue.config.min_print_level = dashx.preferences.developer.loglevel logger.queue.config.log_to_file = tostring(dashx.preferences.developer.logtofile) +function logger.wakeup() logger.queue.process() end -function logger.wakeup() - logger.queue.process() -end - -function logger.reset() - -end +function logger.reset() end function logger.add(message, level) - logger.queue.config.min_print_level = dashx.preferences.developer.loglevel - logger.queue.config.log_to_file = tostring(dashx.preferences.developer.logtofile) - logger.queue.add(message,level) + logger.queue.config.min_print_level = dashx.preferences.developer.loglevel + logger.queue.config.log_to_file = tostring(dashx.preferences.developer.logtofile) + logger.queue.add(message, level) end return logger diff --git a/scripts/dashx/tasks/logging/init.lua b/scripts/dashx/tasks/logging/init.lua index e6a5865..342304e 100644 --- a/scripts/dashx/tasks/logging/init.lua +++ b/scripts/dashx/tasks/logging/init.lua @@ -1,28 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.5, -- run every 0.5 seconds - script = "logging.lua", -- run this script - linkrequired = true, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.5, script = "logging.lua", linkrequired = true, spreadschedule = true, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/logging/logging.lua b/scripts/dashx/tasks/logging/logging.lua index d34f9f9..893440b 100644 --- a/scripts/dashx/tasks/logging/logging.lua +++ b/scripts/dashx/tasks/logging/logging.lua @@ -1,19 +1,18 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) Rotorflight Project - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * Some icons sourced from https://www.flaticon.com/ +local dashx = require("dashx") -]]-- local arg = {...} local config = arg[1] -local MAX_QUEUE = 50 -- If we exceed this many lines queued, force flush -local FLUSH_QUEUE_SIZE = 2 -- number of lines to queue before writing to file +local MAX_QUEUE = 50 +local FLUSH_QUEUE_SIZE = 2 local logging = {} -local logInterval = 1 -- changing this will skew the log analysis - so dont change it +local logInterval = 1 local logFileName local logRateLimit = os.clock() local logHeader @@ -29,16 +28,15 @@ if lcd.darkMode() then colorTable["temp_esc"] = COLOR_CYAN colorTable["throttle_percent"] = COLOR_YELLOW else - colorTable["voltage"] = lcd.RGB(200, 0, 0) -- Bright red - colorTable["current"] = lcd.RGB(220, 100, 0) -- Deep orange - colorTable["rpm"] = lcd.RGB(0, 140, 0) -- Strong green - colorTable["temp_esc"] = lcd.RGB(0, 80, 200) -- Bold blue - colorTable["throttle_percent"] = lcd.RGB(180, 160, 0) -- Deep gold + colorTable["voltage"] = lcd.RGB(200, 0, 0) + colorTable["current"] = lcd.RGB(220, 100, 0) + colorTable["rpm"] = lcd.RGB(0, 140, 0) + colorTable["temp_esc"] = lcd.RGB(0, 80, 200) + colorTable["throttle_percent"] = lcd.RGB(180, 160, 0) end local logTable = { - {name = "voltage", keyindex = 1, keyname = "Voltage", keyunit = "v", keyminmax = 1, color = colorTable['voltage'], pen = SOLID, graph = true}, - {name = "current", keyindex = 2, keyname = "Current", keyunit = "A", keyminmax = 1, color = colorTable['current'], pen = SOLID, graph = true}, + {name = "voltage", keyindex = 1, keyname = "Voltage", keyunit = "v", keyminmax = 1, color = colorTable['voltage'], pen = SOLID, graph = true}, {name = "current", keyindex = 2, keyname = "Current", keyunit = "A", keyminmax = 1, color = colorTable['current'], pen = SOLID, graph = true}, {name = "rpm", keyindex = 3, keyname = "Headspeed", keyunit = "rpm", keyminmax = 1, keyfloor = true, color = colorTable['rpm'], pen = SOLID, graph = true}, {name = "temp_esc", keyindex = 4, keyname = "Esc. Temperature", keyunit = "°", keyminmax = 1, color = colorTable['temp_esc'], pen = SOLID, graph = true}, {name = "throttle_percent", keyindex = 5, keyname = "Throttle %", keyunit = "%", keyminmax = 1, color = colorTable['throttle_percent'], pen = SOLID, graph = false} @@ -47,25 +45,21 @@ local logTable = { local log_queue = {} local logDirChecked = false - local function generateLogFilename() local timestamp = os.date("%Y-%m-%d_%H-%M-%S") local uniquePart = math.floor(os.clock() * 1000) - return timestamp .. "_" .. uniquePart .. ".csv" + return timestamp .. "_" .. uniquePart .. ".csv" end local function checkLogdirExists() - os.mkdir("LOGS:") - os.mkdir("LOGS:/dashx") - os.mkdir("LOGS:/dashx/telemetry") + os.mkdir("LOGS:") + os.mkdir("LOGS:/dashx") + os.mkdir("LOGS:/dashx/telemetry") end function logging.queueLog(msg) table.insert(log_queue, msg) - if #log_queue >= MAX_QUEUE then - -- If something stalls and the queue grows, force a flush to bound memory. - logging.writeLogs(true) - end + if #log_queue >= MAX_QUEUE then logging.writeLogs(true) end end function logging.writeLogs(forcewrite) @@ -73,32 +67,25 @@ function logging.writeLogs(forcewrite) if #log_queue > 0 and logFileName then local filePath = "LOGS:dashx/telemetry/" .. logFileName - dashx.utils.log( - string.format("Write %d (of %d) lines to %s", - math.min(#log_queue, max_lines), #log_queue, logFileName), - "info" - ) + dashx.utils.log(string.format("Write %d (of %d) lines to %s", math.min(#log_queue, max_lines), #log_queue, logFileName), "info") local f = io.open(filePath, 'a') local n = math.min(#log_queue, max_lines) - -- write N lines in one go + io.write(f, table.concat(log_queue, "\n", 1, n), "\n") - -- compact the queue so #log_queue stays accurate (avoid holes) local total = #log_queue if n < total then - table.move(log_queue, n+1, total, 1) + table.move(log_queue, n + 1, total, 1) for i = total - n + 1, total do log_queue[i] = nil end else for i = 1, total do log_queue[i] = nil end end io.close(f) - end end - - +end function logging.getLogHeader() local names = {} @@ -116,31 +103,24 @@ function logging.getLogLine() return ts .. ", " .. dashx.utils.joinTableItems(values, ", ") end -function logging.getLogTable() - return logTable -end - +function logging.getLogTable() return logTable end function logging.flushLogs() if logFileName or logHeader then dashx.utils.log("Flushing logs - " .. tostring(logFileName), "info") - -- Write pending lines before clearing state so they don't carry over + logging.writeLogs(true) logFileName, logHeader = nil, nil logdir = nil collectgarbage() - end + end end -function logging.reset() - -end +function logging.reset() end function logging.wakeup() - if not dashx.session.mcu_id then - return - end + if not dashx.session.mcu_id then return end if not telemetry then telemetry = dashx.tasks.telemetry @@ -157,8 +137,6 @@ function logging.wakeup() return end - - -- SIMPLIFIED logging trigger: if dashx.utils.inFlight() then if not logFileName then logFileName = generateLogFilename() @@ -166,14 +144,12 @@ function logging.wakeup() local iniName = "LOGS:dashx/telemetry/logs.ini" local iniData = dashx.ini.load_ini_file(iniName) or {} - if not iniData.model then - iniData.model = {} - end + if not iniData.model then iniData.model = {} end iniData.model.name = dashx.session.craftName or model.name() or "Unknown" dashx.ini.save_ini_file(iniName, iniData) end if not logHeader then - -- Write the header immediately so it is always the first line + local filePath = "LOGS:dashx/telemetry/" .. logFileName local f = io.open(filePath, 'w') if f then @@ -186,14 +162,11 @@ function logging.wakeup() if os.clock() - logRateLimit >= logInterval then logRateLimit = os.clock() logging.queueLog(logging.getLogLine()) - if #log_queue >= FLUSH_QUEUE_SIZE then - logging.writeLogs() - end + if #log_queue >= FLUSH_QUEUE_SIZE then logging.writeLogs() end end else logging.flushLogs() end end - return logging diff --git a/scripts/dashx/tasks/onconnect/init.lua b/scripts/dashx/tasks/onconnect/init.lua index 3a1aa0a..12a767c 100644 --- a/scripts/dashx/tasks/onconnect/init.lua +++ b/scripts/dashx/tasks/onconnect/init.lua @@ -1,27 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.25, -- run every 0.25 seconds - script = "tasks.lua", -- run this script - linkrequired = false, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.25, script = "tasks.lua", linkrequired = false, spreadschedule = true, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/onconnect/tasks.lua b/scripts/dashx/tasks/onconnect/tasks.lua index 1efb081..fce4286 100644 --- a/scripts/dashx/tasks/onconnect/tasks.lua +++ b/scripts/dashx/tasks/onconnect/tasks.lua @@ -1,11 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. ---]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local tasks = {} local tasksList = {} @@ -13,21 +11,16 @@ local tasksLoaded = false local TASK_TIMEOUT_SECONDS = 10 --- Base path and priority levels local BASE_PATH = "tasks/onconnect/tasks/" local PRIORITY_LEVELS = {"high", "medium", "low"} --- Initialize or reset session flags local function resetSessionFlags() dashx.session.onConnect = dashx.session.onConnect or {} - for _, level in ipairs(PRIORITY_LEVELS) do - dashx.session.onConnect[level] = false - end - -- Ensure isConnected resets until high priority completes + for _, level in ipairs(PRIORITY_LEVELS) do dashx.session.onConnect[level] = false end + dashx.session.isConnected = false end --- Discover task files in fixed priority order function tasks.findTasks() if tasksLoaded then return end @@ -46,13 +39,7 @@ function tasks.findTasks() else local module = assert(chunk()) if type(module) == "table" and type(module.wakeup) == "function" then - tasksList[name] = { - module = module, - priority = level, - initialized = false, - complete = false, - startTime = nil - } + tasksList[name] = {module = module, priority = level, initialized = false, complete = false, startTime = nil} else dashx.utils.log("Invalid task file: " .. fullPath, "info") end @@ -82,7 +69,7 @@ function tasks.wakeup() if dashx.session.telemetryTypeChanged then dashx.utils.logRotorFlightBanner() - --dashx.utils.log("Telemetry type changed, resetting tasks.", "info") + dashx.session.telemetryTypeChanged = false tasks.resetAllTasks() tasksLoaded = false @@ -95,13 +82,10 @@ function tasks.wakeup() return end - if not tasksLoaded then - tasks.findTasks() - end + if not tasksLoaded then tasks.findTasks() end local now = os.clock() - -- Run each task for name, task in pairs(tasksList) do if not task.initialized then task.initialized = true @@ -121,7 +105,6 @@ function tasks.wakeup() end end - -- Update session flags as soon as each priority level completes for _, level in ipairs(PRIORITY_LEVELS) do if not dashx.session.onConnect[level] then local levelDone = true @@ -135,7 +118,6 @@ function tasks.wakeup() dashx.session.onConnect[level] = true dashx.utils.log("All '" .. level .. "' tasks complete.", "info") - -- Signal the session connected immediately when high priority finishes if level == "high" then dashx.utils.playFileCommon("beep.wav") dashx.flightmode.current = "preflight" @@ -145,9 +127,9 @@ function tasks.wakeup() elseif level == "medium" then dashx.session.isConnectedMedium = true return - elseif level == "low" then - dashx.session.isConnectedLow = true - dashx.session.isConnected = true + elseif level == "low" then + dashx.session.isConnectedLow = true + dashx.session.isConnected = true collectgarbage() return end diff --git a/scripts/dashx/tasks/onconnect/tasks/high/apiversion.lua b/scripts/dashx/tasks/onconnect/tasks/high/apiversion.lua index b76d535..160f7d3 100644 --- a/scripts/dashx/tasks/onconnect/tasks/high/apiversion.lua +++ b/scripts/dashx/tasks/onconnect/tasks/high/apiversion.lua @@ -1,37 +1,16 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local apiversion = {} -function apiversion.wakeup() - dashx.session.apiVersion = 12.07 -end +function apiversion.wakeup() dashx.session.apiVersion = 12.07 end -function apiversion.reset() - dashx.session.apiVersion = nil -end +function apiversion.reset() dashx.session.apiVersion = nil end -function apiversion.isComplete() - if dashx.session.apiVersion ~= nil then - return true - end -end +function apiversion.isComplete() if dashx.session.apiVersion ~= nil then return true end end -return apiversion \ No newline at end of file +return apiversion diff --git a/scripts/dashx/tasks/onconnect/tasks/high/sensorstats.lua b/scripts/dashx/tasks/onconnect/tasks/high/sensorstats.lua index b8df5af..ae23142 100644 --- a/scripts/dashx/tasks/onconnect/tasks/high/sensorstats.lua +++ b/scripts/dashx/tasks/onconnect/tasks/high/sensorstats.lua @@ -1,23 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local sensorstats = {} local runOnce = false @@ -29,12 +16,8 @@ function sensorstats.wakeup() end end -function sensorstats.reset() - runOnce = false -end +function sensorstats.reset() runOnce = false end -function sensorstats.isComplete() - return runOnce -end +function sensorstats.isComplete() return runOnce end -return sensorstats \ No newline at end of file +return sensorstats diff --git a/scripts/dashx/tasks/onconnect/tasks/high/timer.lua b/scripts/dashx/tasks/onconnect/tasks/high/timer.lua index 5f42a9e..286889d 100644 --- a/scripts/dashx/tasks/onconnect/tasks/high/timer.lua +++ b/scripts/dashx/tasks/onconnect/tasks/high/timer.lua @@ -1,43 +1,26 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local timer = {} local runOnce = false function timer.wakeup() - dashx.session.timer = {} - dashx.session.timer.start = nil -- this is used to store the start time of the timer - dashx.session.timer.live = nil -- this is used to store the live timer value while inflight - dashx.session.timer.lifetime = nil -- this is used to store the total flight time of a model and store it in the user ini file - dashx.session.timer.session = 0 -- this is used to track flight time for the session - runOnce = true + dashx.session.timer = {} + dashx.session.timer.start = nil + dashx.session.timer.live = nil + dashx.session.timer.lifetime = nil + dashx.session.timer.session = 0 + runOnce = true end -function timer.reset() - runOnce = false -end +function timer.reset() runOnce = false end -function timer.isComplete() - return runOnce -end +function timer.isComplete() return runOnce end -return timer \ No newline at end of file +return timer diff --git a/scripts/dashx/tasks/onconnect/tasks/high/uid.lua b/scripts/dashx/tasks/onconnect/tasks/high/uid.lua index 4a5ecc3..1b6b5d4 100644 --- a/scripts/dashx/tasks/onconnect/tasks/high/uid.lua +++ b/scripts/dashx/tasks/onconnect/tasks/high/uid.lua @@ -1,79 +1,39 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local uid = {} --- FNV-1a 32-bit local function fnv1a32(s) - local hash = 0x811C9DC5 -- 2166136261 + local hash = 0x811C9DC5 for i = 1, #s do hash = (hash ~ s:byte(i)) & 0xffffffff - hash = (hash * 0x01000193) & 0xffffffff -- 16777619 + hash = (hash * 0x01000193) & 0xffffffff end return hash end --- 128-bit UUID-like string derived from the path local function path_to_uuid(path) - -- (Optional) normalize if your paths vary by slashes/case: - -- path = path:gsub("\\", "/") local parts = {} for i = 1, 4 do local h = fnv1a32(path .. "\0" .. i) parts[i] = string.format("%08x", h) end - local full = table.concat(parts) -- 32 hex chars + local full = table.concat(parts) - return string.format("%s-%s-%s-%s-%s", - full:sub(1, 8), - full:sub(9, 12), - full:sub(13, 16), - full:sub(17, 20), - full:sub(21, 32) - ) + return string.format("%s-%s-%s-%s-%s", full:sub(1, 8), full:sub(9, 12), full:sub(13, 16), full:sub(17, 20), full:sub(21, 32)) end --- If you prefer a short stable ID instead: -local function path_to_id32(path) - return string.format("%08x", fnv1a32(path)) -end - -function uid.wakeup() - -- quick exit if no apiVersion - if dashx.session.mcu_id == nil then - - dashx.session.mcu_id = path_to_uuid(model.path()) +local function path_to_id32(path) return string.format("%08x", fnv1a32(path)) end - end - -end +function uid.wakeup() if dashx.session.mcu_id == nil then dashx.session.mcu_id = path_to_uuid(model.path()) end end -function uid.reset() - dashx.session.mcu_id = nil -end +function uid.reset() dashx.session.mcu_id = nil end -function uid.isComplete() - if dashx.session.mcu_id ~= nil then - return true - end -end +function uid.isComplete() if dashx.session.mcu_id ~= nil then return true end end -return uid \ No newline at end of file +return uid diff --git a/scripts/dashx/tasks/onconnect/tasks/low/battery.lua b/scripts/dashx/tasks/onconnect/tasks/low/battery.lua index 9622a96..54bade9 100644 --- a/scripts/dashx/tasks/onconnect/tasks/low/battery.lua +++ b/scripts/dashx/tasks/onconnect/tasks/low/battery.lua @@ -1,67 +1,41 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local battery = {} function battery.wakeup() - -- quick exit if no apiVersion - if dashx.session.apiVersion == nil then return end - - if (dashx.session.batteryConfig == nil and dashx.session.mcu_id ) then - - - - local modelpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/models/" .. dashx.session.mcu_id ..".ini" + if dashx.session.apiVersion == nil then return end - os.mkdir("SCRIPTS:/" .. dashx.config.preferences) - os.mkdir("SCRIPTS:/" .. dashx.config.preferences .. "/models") - local master_ini = dashx.ini.load_ini_file(modelpref_file) or {} - local preferences = master_ini.battery or {} + if (dashx.session.batteryConfig == nil and dashx.session.mcu_id) then + local modelpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/models/" .. dashx.session.mcu_id .. ".ini" + os.mkdir("SCRIPTS:/" .. dashx.config.preferences) + os.mkdir("SCRIPTS:/" .. dashx.config.preferences .. "/models") + local master_ini = dashx.ini.load_ini_file(modelpref_file) or {} + local preferences = master_ini.battery or {} - dashx.session.batteryConfig = {} - dashx.session.batteryConfig.batteryCapacity = preferences.batteryCapacity - dashx.session.batteryConfig.batteryCellCount = preferences.batteryCellCount - dashx.session.batteryConfig.vbatwarningcellvoltage = preferences.vbatwarningcellvoltage/10 - dashx.session.batteryConfig.vbatmincellvoltage = preferences.vbatmincellvoltage/10 - dashx.session.batteryConfig.vbatmaxcellvoltage = preferences.vbatmaxcellvoltage/10 - dashx.session.batteryConfig.vbatfullcellvoltage = preferences.vbatfullcellvoltage/10 - dashx.session.batteryConfig.lvcPercentage = preferences.lvcPercentage - dashx.session.batteryConfig.consumptionWarningPercentage = preferences.consumptionWarningPercentage + dashx.session.batteryConfig = {} + dashx.session.batteryConfig.batteryCapacity = preferences.batteryCapacity + dashx.session.batteryConfig.batteryCellCount = preferences.batteryCellCount + dashx.session.batteryConfig.vbatwarningcellvoltage = preferences.vbatwarningcellvoltage / 10 + dashx.session.batteryConfig.vbatmincellvoltage = preferences.vbatmincellvoltage / 10 + dashx.session.batteryConfig.vbatmaxcellvoltage = preferences.vbatmaxcellvoltage / 10 + dashx.session.batteryConfig.vbatfullcellvoltage = preferences.vbatfullcellvoltage / 10 + dashx.session.batteryConfig.lvcPercentage = preferences.lvcPercentage + dashx.session.batteryConfig.consumptionWarningPercentage = preferences.consumptionWarningPercentage - - - end + end end -function battery.reset() - dashx.session.batteryConfig = nil -end +function battery.reset() dashx.session.batteryConfig = nil end -function battery.isComplete() - if dashx.session.batteryConfig ~= nil then - return true - end -end +function battery.isComplete() if dashx.session.batteryConfig ~= nil then return true end end -return battery \ No newline at end of file +return battery diff --git a/scripts/dashx/tasks/onconnect/tasks/low/rxmap.lua b/scripts/dashx/tasks/onconnect/tasks/low/rxmap.lua index 0171106..74afb69 100644 --- a/scripts/dashx/tasks/onconnect/tasks/low/rxmap.lua +++ b/scripts/dashx/tasks/onconnect/tasks/low/rxmap.lua @@ -1,53 +1,36 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local rxmap = {} function rxmap.wakeup() - - -- quick exit if no apiVersion - if dashx.session.apiVersion == nil then return end - if not dashx.utils.rxmapReady() then + if dashx.session.apiVersion == nil then return end - dashx.session.rx.map.aileron = 0 - dashx.session.rx.map.elevator = 1 - dashx.session.rx.map.collective = 2 - dashx.session.rx.map.rudder = 3 - dashx.session.rx.map.arm = 4 - dashx.session.rx.map.throttle = 5 - dashx.session.rx.map.mode = 6 - dashx.session.rx.map.headspeed = 7 + if not dashx.utils.rxmapReady() then + dashx.session.rx.map.aileron = 0 + dashx.session.rx.map.elevator = 1 + dashx.session.rx.map.collective = 2 + dashx.session.rx.map.rudder = 3 + dashx.session.rx.map.arm = 4 + dashx.session.rx.map.throttle = 5 + dashx.session.rx.map.mode = 6 + dashx.session.rx.map.headspeed = 7 - end + end end function rxmap.reset() dashx.session.rxmap = {} - dashx.session.rxvalues = {} + dashx.session.rxvalues = {} end -function rxmap.isComplete() - return dashx.utils.rxmapReady() -end +function rxmap.isComplete() return dashx.utils.rxmapReady() end -return rxmap \ No newline at end of file +return rxmap diff --git a/scripts/dashx/tasks/onconnect/tasks/medium/modelpreferences.lua b/scripts/dashx/tasks/onconnect/tasks/medium/modelpreferences.lua index 232d86f..520ef2e 100644 --- a/scripts/dashx/tasks/onconnect/tasks/medium/modelpreferences.lua +++ b/scripts/dashx/tasks/onconnect/tasks/medium/modelpreferences.lua @@ -1,103 +1,51 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- +local dashx = require("dashx") + local modelpreferences = {} -local modelpref_defaults ={ - dashboard = { - theme_preflight = "nil", - theme_inflight = "nil", - theme_postflight = "nil", - }, - general ={ - flightcount = 0, - totalflighttime = 0, - lastflighttime = 0, - }, - model = { - armswitch = false, - inflightswitch = false, - inflightswitch_delay = 10, - rateswitch = false, - }, - battery = { - calc_local = 0, - batteryCapacity = 2200, - batteryCellCount = 3, - vbatwarningcellvoltage = 35, - vbatmincellvoltage = 33, - vbatmaxcellvoltage = 43, - vbatfullcellvoltage = 41, - lvcPercentage = 30, - consumptionWarningPercentage = 30 - } +local modelpref_defaults = { + dashboard = {theme_preflight = "nil", theme_inflight = "nil", theme_postflight = "nil"}, + general = {flightcount = 0, totalflighttime = 0, lastflighttime = 0}, + model = {armswitch = false, inflightswitch = false, inflightswitch_delay = 10, rateswitch = false}, + battery = {calc_local = 0, batteryCapacity = 2200, batteryCellCount = 3, vbatwarningcellvoltage = 35, vbatmincellvoltage = 33, vbatmaxcellvoltage = 43, vbatfullcellvoltage = 41, lvcPercentage = 30, consumptionWarningPercentage = 30} } function modelpreferences.wakeup() - -- quick exit if no apiVersion - if dashx.session.apiVersion == nil then return end + if dashx.session.apiVersion == nil then return end - --- check if we have a mcu_id - if not dashx.session.mcu_id then - return - end - + if not dashx.session.mcu_id then return end - if (dashx.session.modelPreferences == nil) then - -- populate the model preferences variable + if (dashx.session.modelPreferences == nil) then if dashx.config.preferences and dashx.session.mcu_id then - local modelpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/models/" .. dashx.session.mcu_id ..".ini" + local modelpref_file = "SCRIPTS:/" .. dashx.config.preferences .. "/models/" .. dashx.session.mcu_id .. ".ini" dashx.utils.log("Preferences file: " .. modelpref_file, "info") os.mkdir("SCRIPTS:/" .. dashx.config.preferences) os.mkdir("SCRIPTS:/" .. dashx.config.preferences .. "/models") - local slave_ini = modelpref_defaults - local master_ini = dashx.ini.load_ini_file(modelpref_file) or {} - + local master_ini = dashx.ini.load_ini_file(modelpref_file) or {} local updated_ini = dashx.ini.merge_ini_tables(master_ini, slave_ini) dashx.session.modelPreferences = updated_ini dashx.session.modelPreferencesFile = modelpref_file - if not dashx.ini.ini_tables_equal(master_ini, slave_ini) then - dashx.ini.save_ini_file(modelpref_file, updated_ini) - end - + if not dashx.ini.ini_tables_equal(master_ini, slave_ini) then dashx.ini.save_ini_file(modelpref_file, updated_ini) end + end end end -function modelpreferences.reset() - dashx.session.modelPreferences = nil -end +function modelpreferences.reset() dashx.session.modelPreferences = nil end -function modelpreferences.isComplete() - if dashx.session.modelPreferences ~= nil then - return true - end -end +function modelpreferences.isComplete() if dashx.session.modelPreferences ~= nil then return true end end -return modelpreferences \ No newline at end of file +return modelpreferences diff --git a/scripts/dashx/tasks/sensors/frsky.lua b/scripts/dashx/tasks/sensors/frsky.lua index 0c9629b..1430b23 100644 --- a/scripts/dashx/tasks/sensors/frsky.lua +++ b/scripts/dashx/tasks/sensors/frsky.lua @@ -1,88 +1,43 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) Inav Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - -* This script is called when using RF2.1 or lower. It is used to create, drop and rename sensors for the legacy frsky protocol +local dashx = require("dashx") -]] -- --- local arg = {...} local config = arg[1] --- local cacheExpireTime = 10 -- Time in seconds to expire the caches (disabled) --- local lastCacheFlushTime = os.clock() -- Store the initial time (disabled) --- (Periodic cache flush disabled; using event-driven clears) -local sensorTlm +local sensorTlm local frsky = {} --- used by sensors.lua to know if module has changed frsky.name = "frsky" --- Bounded drain controls (tune as needed) local MAX_FRAMES_PER_WAKEUP = 32 -local MAX_TIME_BUDGET = 0.004 +local MAX_TIME_BUDGET = 0.004 local telemetryStartTime = os.clock() -local TELEMETRY_TIMEOUT = 20 -- seconds - +local TELEMETRY_TIMEOUT = 20 --- create local createSensorList = {} createSensorList[0x0430] = {name = "Pitch", unit = UNIT_DEGREE, decimals = 1} createSensorList[0x0440] = {name = "Roll", unit = UNIT_DEGREE, decimals = 1} createSensorList[0x0480] = {name = "GPS Sats", unit = UNIT_RAW, decimals = 0} - --- drop local dropSensorList = {} dropSensorList[0x0400] = {name = "Temp1"} dropSensorList[0x0410] = {name = "Temp1"} --- rename local renameSensorList = {} ---renameSensorList[0x0500] = {name = "Headspeed", onlyifname = "RPM"} - frsky.createSensorCache = {} frsky.dropSensorCache = {} frsky.renameSensorCache = {} --- Track once-only ops to avoid repeated work frsky.renamed = {} frsky.dropped = {} - ---[[ - createSensor - Creates a custom sensor if it does not already exist in the cache. - - Parameters: - physId (number) - The physical ID of the sensor. - primId (number) - The primary ID of the sensor. - appId (number) - The application ID of the sensor. - frameValue (number) - The frame value of the sensor. - - This function checks if a custom sensor with the given appId exists in the createSensorList. - If it does, it then checks if the sensor is already cached in frsky.createSensorCache. - If the sensor is not cached, it creates a new sensor, sets its properties, and caches it. -]] --- createSensor: return a status local function createSensor(physId, primId, appId, frameValue) if dashx.session.apiVersion == nil then return "skip" end local v = createSensorList[appId] @@ -98,32 +53,24 @@ local function createSensor(physId, primId, appId, frameValue) s:module(dashx.session.telemetrySensor:module()) s:minimum(min or -1000000000) s:maximum(max or 2147483647) - if v.unit then s:unit(v.unit); s:protocolUnit(v.unit) end - if v.decimals then s:decimals(v.decimals); s:protocolDecimals(v.decimals) end - if v.minimum then s:minimum(v.minimum) end - if v.maximum then s:maximum(v.maximum) end + if v.unit then + s:unit(v.unit); + s:protocolUnit(v.unit) + end + if v.decimals then + s:decimals(v.decimals); + s:protocolDecimals(v.decimals) + end + if v.minimum then s:minimum(v.minimum) end + if v.maximum then s:maximum(v.maximum) end frsky.createSensorCache[appId] = s return "created" end end - return "noop" -- already present + return "noop" end ---[[ - dropSensor - Function to handle the dropping of a sensor based on its application ID. - - Parameters: - physId (number) - The physical ID of the sensor. - primId (number) - The primary ID of the sensor. - appId (number) - The application ID of the sensor. - frameValue (number) - The frame value associated with the sensor. - - This function checks if a custom sensor exists in the dropSensorList using the provided appId. - If the sensor exists and is not already cached in frsky.dropSensorCache, it retrieves the sensor - source using system.getSource and drops it if successfully retrieved. -]] --- dropSensor: return a status (optional, only if you actually use dropSensorList here) local function dropSensor(physId, primId, appId, frameValue) if dashx.session.apiVersion == nil then return "skip" end if not dropSensorList or not dropSensorList[appId] then return "skip" end @@ -144,20 +91,6 @@ local function dropSensor(physId, primId, appId, frameValue) return "skip" end - ---[[ - renameSensor - Renames a telemetry sensor based on provided parameters. - - Parameters: - physId (number) - The physical ID of the sensor. - primId (number) - The primary ID of the sensor. - appId (number) - The application ID of the sensor. - frameValue (number) - The frame value of the sensor. - - This function checks if a custom sensor exists in the renameSensorList using the appId. - If the sensor exists and is not already cached in frsky.renameSensorCache, it retrieves the sensor source. - If the sensor source is found and its name matches the specified condition, it renames the sensor. -]] local function renameSensor(physId, primId, appId, frameValue) if dashx.session.apiVersion == nil then return "skip" end local v = renameSensorList[appId] @@ -180,23 +113,8 @@ local function renameSensor(physId, primId, appId, frameValue) return "skip" end - ---[[ - Function: telemetryPop - Description: Pops a received SPORT packet from the queue and processes it. - Only packets using a data ID within 0x5000 to 0x50FF (frame ID == 0x10), - as well as packets with a frame ID equal to 0x32 (regardless of the data ID) - will be passed to the LUA telemetry receive queue. - Returns: - - true if a frame was processed - - false if no frame was available - Note: - - The function calls createSensor, dropSensor, and renameSensor with the frame's - physical ID, primary ID, application ID, and value. ---]] --- telemetryPop: short-circuit based on status local function telemetryPop() - + if not sensorTlm then return false end local frame = sensorTlm:popFrame() @@ -205,34 +123,24 @@ local function telemetryPop() local physId, primId, appId, value = frame:physId(), frame:primId(), frame:appId(), frame:value() - -- 1) If this appId belongs to create list and we created/found it, we can skip rename/drop local cs = createSensor(physId, primId, appId, value) - if cs ~= "skip" then return true end -- handled or confirmed not needed; nothing else to do + if cs ~= "skip" then return true end - -- 2) If you’re actively dropping legacy sensors, try that next local ds = dropSensor(physId, primId, appId, value) if ds ~= "skip" then return true end - -- 3) Finally, try a conditional rename renameSensor(physId, primId, appId, value) return true end ---[[ - Function: frsky.wakeup - Description: This function is responsible for managing sensor caches and ensuring they are cleared at appropriate times. It checks if the caches need to be expired based on a timer and clears them if necessary. Additionally, it flushes the sensor list if telemetry is inactive or if the RSSI sensor is not available. The function also ensures that certain operations are only performed when the GUI is not running and the MSP queue is processed. - Short: Manages sensor caches and ensures timely clearing. ---]] function frsky.wakeup() - if not sensorTlm then - sensorTlm = sport.getSensor() - end + if not sensorTlm then sensorTlm = sport.getSensor() end local function clearCaches() frsky.createSensorCache = {} frsky.renameSensorCache = {} - frsky.dropSensorCache = {} + frsky.dropSensorCache = {} end if not dashx.session.telemetryState or not dashx.session.telemetrySensor then @@ -240,30 +148,25 @@ function frsky.wakeup() return end - + if not dashx.tasks and dashx.tasks.telemetry then return end - if not dashx.tasks and dashx.tasks.telemetry then - return - end + if os.clock() - telemetryStartTime > TELEMETRY_TIMEOUT then - if os.clock() - telemetryStartTime > TELEMETRY_TIMEOUT then - -- stop trying to pop telemetry after timeout clearCaches() return end - if (dashx.app and dashx.app.guiIsRunning == false) or dashx.tasks.telemetry then + if (dashx.app and dashx.app.guiIsRunning == false) or dashx.tasks.telemetry then - local start = os.clock() - local count = 0 - while count < MAX_FRAMES_PER_WAKEUP and (os.clock() - start) <= MAX_TIME_BUDGET do - if not telemetryPop() then break end - count = count + 1 - end + local start = os.clock() + local count = 0 + while count < MAX_FRAMES_PER_WAKEUP and (os.clock() - start) <= MAX_TIME_BUDGET do + if not telemetryPop() then break end + count = count + 1 + end end end - function frsky.reset() frsky.createSensorCache = {} frsky.renameSensorCache = {} diff --git a/scripts/dashx/tasks/sensors/init.lua b/scripts/dashx/tasks/sensors/init.lua index 7247e19..54c3914 100644 --- a/scripts/dashx/tasks/sensors/init.lua +++ b/scripts/dashx/tasks/sensors/init.lua @@ -1,28 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.1, -- run every 0.1 seconds - script = "sensors.lua", -- run this script - linkrequired = true, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.1, script = "sensors.lua", linkrequired = true, spreadschedule = true, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/sensors/lib/smartfuel.lua b/scripts/dashx/tasks/sensors/lib/smartfuel.lua index 0b31517..bc82f17 100644 --- a/scripts/dashx/tasks/sensors/lib/smartfuel.lua +++ b/scripts/dashx/tasks/sensors/lib/smartfuel.lua @@ -1,44 +1,27 @@ -local dashx = require("dashx") ---[[ - * Copyright (C) Rotorflight Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- --- Persistent vars for the smart fuel logic -local batteryConfigCache = nil -- Cached battery configuration data. -local fuelStartingPercent = nil -- Initial fuel percentage at the start of measurement. -local fuelStartingConsumption = nil -- Initial fuel consumption value at the start. - --- Voltage stabilisation state -local lastVoltages = {} -- Table holding the most recent voltage readings for stability analysis. -local maxVoltageSamples = 5 -- Number of recent voltage samples to retain for stability checks. -local voltageStableTime = nil -- Timestamp when voltage was last considered stable. -local voltageStabilised = false -- Boolean indicating if voltage has stabilised. -local stabilizeNotBefore = nil -- Earliest time at which stabilisation can be considered. -local voltageThreshold = 0.15 -- Maximum allowed voltage variation within the sample window to consider as stable. -local preStabiliseDelay = 1.5 -- Minimum seconds to wait after configuration or telemetry update before checking for stabilisation. - -local telemetry -- Reference to the telemetry task, used to access sensor data. -local lastMode = dashx.flightmode.current or "preflight" -- Last flight mode to detect changes. + +local dashx = require("dashx") + +local batteryConfigCache = nil +local fuelStartingPercent = nil +local fuelStartingConsumption = nil + +local lastVoltages = {} +local maxVoltageSamples = 5 +local voltageStableTime = nil +local voltageStabilised = false +local stabilizeNotBefore = nil +local voltageThreshold = 0.15 +local preStabiliseDelay = 1.5 + +local telemetry +local lastMode = dashx.flightmode.current or "preflight" local currentMode = dashx.flightmode.current or "preflight" local lastSensorMode --- Discharge curve with 0.01V per cell resolution from 3.00V to 4.20V (121 points) --- This curve uses a sigmoid approximation to mimic real LiPo discharge behavior --- Same curve as used in smartfuelvoltage.lua for consistency local dischargeCurveTable = {} for i = 0, 120 do local v = 3.00 + i * 0.01 @@ -47,53 +30,37 @@ for i = 0, 120 do dischargeCurveTable[i + 1] = math.floor(math.min(100, math.max(0, percent)) + 0.5) end --- Calculate fuel percentage using sigmoid discharge curve for accurate LiPo behavior --- This provides much better accuracy than linear voltage mapping local function fuelPercentageFromVoltage(voltage, cellCount, bc) local minV = bc.vbatmincellvoltage or 3.30 local fullV = bc.vbatfullcellvoltage or 4.10 local voltagePerCell = voltage / cellCount - -- Handle edge cases if voltagePerCell >= fullV then return 100 elseif voltagePerCell <= minV then return 0 end - -- Map voltage range [minV, fullV] to discharge curve range [3.00, 4.20] local sigmoidMin, sigmoidMax = 3.00, 4.20 local scaledV = sigmoidMin + (voltagePerCell - minV) / (fullV - minV) * (sigmoidMax - sigmoidMin) - -- Clamp to discharge curve range scaledV = math.max(sigmoidMin, math.min(sigmoidMax, scaledV)) - -- Look up percentage from discharge curve table local index = math.floor((scaledV - sigmoidMin) / 0.01) + 1 index = math.max(1, math.min(#dischargeCurveTable, index)) return dischargeCurveTable[index] end --- Resets the voltage tracking state by clearing the last recorded voltages, --- resetting the voltage stable time, and marking the voltage as not stabilised. --- This function is typically used to reinitialize voltage monitoring logic. local function resetVoltageTracking() lastVoltages = {} voltageStableTime = nil voltageStabilised = false end --- Checks if the voltage readings in `lastVoltages` are stable. --- Stability is determined by ensuring the number of samples in `lastVoltages` --- is at least `maxVoltageSamples`, and the difference between the maximum and --- minimum voltage values does not exceed `voltageThreshold`. --- @return boolean True if voltage is stable, false otherwise. local function isVoltageStable() - if #lastVoltages < maxVoltageSamples then - return false - end + if #lastVoltages < maxVoltageSamples then return false end local vmin, vmax = lastVoltages[1], lastVoltages[1] for _, v in ipairs(lastVoltages) do if v < vmin then vmin = v end @@ -102,50 +69,27 @@ local function isVoltageStable() return (vmax - vmin) <= voltageThreshold end --- Calculates the estimated remaining battery "fuel" percentage based on voltage and consumption telemetry. --- --- This function performs a two-step estimation: --- 1. Determines the initial fuel percentage from the battery voltage, after ensuring voltage readings are stable. --- 2. Tracks the percentage drop using mAh consumption telemetry after the initial value is set. --- --- The function handles battery configuration changes, voltage stabilization, and clamps values to ensure safe operation. --- It uses a ring buffer to stabilize voltage readings and waits for a pre-stabilization delay after configuration changes. --- --- @return number|nil The estimated remaining fuel percentage (0-100), or nil if unavailable or not stabilized. local function smartFuelCalc() - -- Assign this here as it may not be available in the global scope at intialisation - if not telemetry then - telemetry = dashx.tasks.telemetry - end + if not telemetry then telemetry = dashx.tasks.telemetry end - -- quick exit and cleanup - if not dashx.session.isConnected or not dashx.session.batteryConfig then + if not dashx.session.isConnected or not dashx.session.batteryConfig then resetVoltageTracking() - return nil + return nil end local bc = dashx.session.batteryConfig - local configSig = table.concat({ - bc.batteryCellCount, - bc.batteryCapacity, - bc.consumptionWarningPercentage, - bc.vbatmaxcellvoltage, - bc.vbatmincellvoltage, - bc.vbatfullcellvoltage - }, ":") + local configSig = table.concat({bc.batteryCellCount, bc.batteryCapacity, bc.consumptionWarningPercentage, bc.vbatmaxcellvoltage, bc.vbatmincellvoltage, bc.vbatfullcellvoltage}, ":") - -- If config changed, reset voltage stabilization and fuel state if configSig ~= batteryConfigCache then batteryConfigCache = configSig fuelStartingPercent = nil fuelStartingConsumption = nil resetVoltageTracking() - stabilizeNotBefore = os.clock() + preStabiliseDelay -- start pre-stabilisation delay on config change + stabilizeNotBefore = os.clock() + preStabiliseDelay end - -- make sure we reset the method if the sensor mode changes if dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.calc_local then if lastSensorMode ~= dashx.session.modelPreferences.battery.calc_local then resetVoltageTracking() @@ -153,10 +97,8 @@ local function smartFuelCalc() end end - -- Read current voltage local voltage = telemetry and telemetry.getSensor and telemetry.getSensor("voltage") or nil - -- Only track/accept valid voltages (e.g., battery plugged in) if not voltage or voltage < 2 then resetVoltageTracking() stabilizeNotBefore = nil @@ -165,53 +107,40 @@ local function smartFuelCalc() local now = os.clock() - --**Preemptive reset**: bail out _before_ any recalculation if currentMode ~= lastMode then dashx.utils.log("Flight mode changed – resetting voltage & fuel state", "info") - -- clear starting references - fuelStartingPercent = nil + + fuelStartingPercent = nil fuelStartingConsumption = nil - -- clear any stored voltages + resetVoltageTracking() - -- defer next real calc until after your stabilization delay + stabilizeNotBefore = now + preStabiliseDelay - -- update for next time and exit lastMode = currentMode return nil end - -- keep track for next invocation - lastMode = currentMode + lastMode = currentMode - -- Wait for pre-stabilisation delay after config/telemetry is available - if stabilizeNotBefore and now < stabilizeNotBefore then - -- still in pre-stabilization, but don’t toss our samples - return nil - end + if stabilizeNotBefore and now < stabilizeNotBefore then return nil end - -- ring buffer of last N voltage readings table.insert(lastVoltages, voltage) - if #lastVoltages > maxVoltageSamples then - table.remove(lastVoltages, 1) - end + if #lastVoltages > maxVoltageSamples then table.remove(lastVoltages, 1) end - -- wait until we have N consistent readings within threshold if not voltageStabilised then if isVoltageStable() then - dashx.utils.log("Voltage stabilized at: " .. voltage,"info") + dashx.utils.log("Voltage stabilized at: " .. voltage, "info") voltageStabilised = true else - dashx.utils.log("Waiting for voltage to stabilize...","info") + dashx.utils.log("Waiting for voltage to stabilize...", "info") return nil end end - -- Detect voltage increase after stabilization if not yet flying, Only allow this reset whilst in preflight & disarmed. - local isDisarmed = (dashx and dashx.session and dashx.session.isArmed == false) + local isDisarmed = (dashx and dashx.session and dashx.session.isArmed == false) local isPreflight = (dashx and dashx.flightmode and dashx.flightmode.current == "preflight") - -- Need at least 2 samples because we read (#lastVoltages - 1) if lastVoltages and #lastVoltages >= 2 and isPreflight and isDisarmed then local prev = lastVoltages[#lastVoltages - 1] if voltage > prev + voltageThreshold then @@ -220,16 +149,12 @@ local function smartFuelCalc() fuelStartingConsumption = nil resetVoltageTracking() stabilizeNotBefore = os.clock() + preStabiliseDelay - return nil -- Ensure upstream caller knows we are resetting + return nil end - end + end - -- After voltage is stable, proceed as normal - local cellCount, packCapacity, reserve, maxCellV, minCellV, fullCellV = - bc.batteryCellCount, bc.batteryCapacity, bc.consumptionWarningPercentage, - bc.vbatmaxcellvoltage, bc.vbatmincellvoltage, bc.vbatfullcellvoltage + local cellCount, packCapacity, reserve, maxCellV, minCellV, fullCellV = bc.batteryCellCount, bc.batteryCapacity, bc.consumptionWarningPercentage, bc.vbatmaxcellvoltage, bc.vbatmincellvoltage, bc.vbatfullcellvoltage - -- Clamp reserve to allowed range for safety if reserve > 60 then reserve = 35 elseif reserve < 15 then @@ -242,16 +167,14 @@ local function smartFuelCalc() return nil end - -- Clamp usableCapacity once for both steps local usableCapacity = packCapacity * (1 - reserve / 100) if usableCapacity < 10 then usableCapacity = packCapacity end local consumption = telemetry and telemetry.getSensor and telemetry.getSensor("consumption") or nil - -- Step 1: Determine initial fuel % from voltage using accurate discharge curve if not fuelStartingPercent then if voltage and cellCount > 0 then - -- Use sigmoid discharge curve for accurate LiPo percentage calculation + fuelStartingPercent = fuelPercentageFromVoltage(voltage, cellCount, bc) else fuelStartingPercent = 0 @@ -260,14 +183,13 @@ local function smartFuelCalc() fuelStartingConsumption = (consumption or 0) - estimatedUsed end - -- Step 2: Use mAh consumption to track % drop after initial value if consumption and fuelStartingConsumption and packCapacity > 0 then local used = consumption - fuelStartingConsumption local percentUsed = used / usableCapacity * 100 local remaining = math.max(0, fuelStartingPercent - percentUsed) return math.floor(math.min(100, remaining) + 0.5) else - -- If we're resetting or recalculating, don't return a stale value + if not voltageStabilised or (stabilizeNotBefore and os.clock() < stabilizeNotBefore) then print("Voltage not stabilised or pre-stabilisation delay active, returning nil") return nil @@ -276,9 +198,4 @@ local function smartFuelCalc() end end ---- Returns a table containing the `calculate` function for smart fuel calculations. --- @field calculate Function to perform smart fuel calculations. -return { - calculate = smartFuelCalc, - reset = resetVoltageTracking - } \ No newline at end of file +return {calculate = smartFuelCalc, reset = resetVoltageTracking} diff --git a/scripts/dashx/tasks/sensors/lib/smartfuelvoltage.lua b/scripts/dashx/tasks/sensors/lib/smartfuelvoltage.lua index e6e48a6..88f8b49 100644 --- a/scripts/dashx/tasks/sensors/lib/smartfuelvoltage.lua +++ b/scripts/dashx/tasks/sensors/lib/smartfuelvoltage.lua @@ -1,29 +1,18 @@ -local dashx = require("dashx") ---[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note. Some icons have been sourced from https://www.flaticon.com/ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local batteryConfigCache = nil -local lastVoltages = {} -local maxVoltageSamples = 5 -local voltageStableTime = nil -local voltageStabilised = false -local stabilizeNotBefore = nil -local voltageThreshold = 0.15 -local preStabiliseDelay = 1.5 +local dashx = require("dashx") + +local batteryConfigCache = nil +local lastVoltages = {} +local maxVoltageSamples = 5 +local voltageStableTime = nil +local voltageStabilised = false +local stabilizeNotBefore = nil +local voltageThreshold = 0.15 +local preStabiliseDelay = 1.5 local telemetry local currentMode = dashx.flightmode.current or "preflight" @@ -33,14 +22,9 @@ local lastSensorMode local lastFuelPercent = nil local lastFuelTimestamp = nil +local maxFuelDropPerSecond = 1 --- Very stable, slow decline 1–5 --- Moderate responsiveness 6–10 --- Fast decay (minimal clamping) 12+ -local maxFuelDropPerSecond = 1 -- percent per second - --- Very slow rise per second to avoid false positives (fuel technically can only go down ) -local maxFuelRisePerSecond = 0.2 -- maximum rise in percent per second +local maxFuelRisePerSecond = 0.2 local MAX_FALL_PER_SEC = 0.05 local lastFilteredVoltage = nil @@ -55,8 +39,6 @@ local function fallingLimitedFilter(current_v, prev_v, dt) end end --- Discharge curve with 0.01V per cell resolution from 3.00V to 4.20V (121 points) --- This curve uses a sigmoid approximation to mimic real LiPo discharge behavior local dischargeCurveTable = {} for i = 0, 100 do local v = 3.30 + i * 0.01 @@ -64,7 +46,6 @@ for i = 0, 100 do dischargeCurveTable[i + 1] = math.floor(math.min(100, math.max(0, percent)) + 0.5) end - local function resetVoltageTracking() lastVoltages = {} voltageStableTime = nil @@ -84,9 +65,7 @@ end local function getStickLoadFactor() local rx = dashx.session.rx.values if not rx then return 0 end - local sum = 1.0 * math.abs(rx.aileron or 0) - + 1.0 * math.abs(rx.elevator or 0) - + 1.2 * math.abs(rx.collective or 0) + local sum = 1.0 * math.abs(rx.aileron or 0) + 1.0 * math.abs(rx.elevator or 0) + 1.2 * math.abs(rx.collective or 0) return math.min(1.0, sum / 3000) end @@ -94,7 +73,10 @@ local lastRpm = nil local function getRpmDropFactor() local rpm = telemetry and telemetry.getSensor and telemetry.getSensor("rpm") or nil if not rpm or rpm < 100 then return 0 end - if not lastRpm then lastRpm = rpm; return 0 end + if not lastRpm then + lastRpm = rpm; + return 0 + end local drop = (lastRpm - rpm) / lastRpm lastRpm = rpm return math.max(0, drop) @@ -102,13 +84,11 @@ local function getRpmDropFactor() end local function applySagCompensation(voltage) - if dashx.flightmode.current ~= "inflight" then - return voltage -- no sag compensation unless we're flying - end + if dashx.flightmode.current ~= "inflight" then return voltage end local multiplier = dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.sag_multiplier or 0.7 local sagFactor = math.max(getStickLoadFactor(), getRpmDropFactor()) - -- nonlinear curve that *increases* with multiplier: - local compensationScale = multiplier ^ 1.5 -- adjust exponent for sensitivity + + local compensationScale = multiplier ^ 1.5 return voltage + (compensationScale * sagFactor * 0.5) end @@ -119,14 +99,12 @@ local function fuelPercentageCalcByVoltage(voltage, cellCount) local reserve = bc.consumptionWarningPercentage or 30 local usableRange = fullV - minV - local adjustedMinV = minV + (usableRange * (reserve / 100)) * 1.4 -- 1.4 is a factor to adjust the min voltage for better accuracy - + local adjustedMinV = minV + (usableRange * (reserve / 100)) * 1.4 + local voltagePerCell = voltage / cellCount - -- Clamp voltage to adjusted usable range voltagePerCell = math.max(3.30, math.min(fullV, voltagePerCell)) - -- Remap [adjustedMinV, fullV] → [3.00, 4.20] local sigmoidMin, sigmoidMax = 3.30, 4.20 local scaledV = sigmoidMin + (voltagePerCell - adjustedMinV) / (fullV - adjustedMinV) * (sigmoidMax - sigmoidMin) @@ -137,16 +115,13 @@ local function fuelPercentageCalcByVoltage(voltage, cellCount) end local function smartFuelCalc() - if not telemetry then - telemetry = dashx.tasks.telemetry - end + if not telemetry then telemetry = dashx.tasks.telemetry end if not dashx.session.isConnected or not dashx.session.batteryConfig then resetVoltageTracking() return nil end - -- make sure we reset the method if the sensor mode changes if dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.calc_local then if lastSensorMode ~= dashx.session.modelPreferences.battery.calc_local then resetVoltageTracking() @@ -155,14 +130,7 @@ local function smartFuelCalc() end local bc = dashx.session.batteryConfig - local configSig = table.concat({ - bc.batteryCellCount, - bc.batteryCapacity, - bc.consumptionWarningPercentage, - bc.vbatmaxcellvoltage, - bc.vbatmincellvoltage, - bc.vbatfullcellvoltage - }, ":") + local configSig = table.concat({bc.batteryCellCount, bc.batteryCapacity, bc.consumptionWarningPercentage, bc.vbatmaxcellvoltage, bc.vbatmincellvoltage, bc.vbatfullcellvoltage}, ":") if configSig ~= batteryConfigCache then batteryConfigCache = configSig @@ -217,8 +185,7 @@ local function smartFuelCalc() local compensatedVoltage = applySagCompensation(filteredVoltage / bc.batteryCellCount) * bc.batteryCellCount local percent = fuelPercentageCalcByVoltage(compensatedVoltage, bc.batteryCellCount) local now = os.clock() - if (dashx.flightmode.current == "inflight" or dashx.flightmode.current == "postflight") - and lastFuelPercent and lastFuelTimestamp then + if (dashx.flightmode.current == "inflight" or dashx.flightmode.current == "postflight") and lastFuelPercent and lastFuelTimestamp then local dt = now - lastFuelTimestamp local maxDrop = dt * maxFuelDropPerSecond @@ -231,15 +198,10 @@ local function smartFuelCalc() end end - -- always update the last‑seen values so that when you enter flight mode - -- the timer resets correctly - lastFuelPercent = percent + lastFuelPercent = percent lastFuelTimestamp = now return percent end -return { - calculate = smartFuelCalc, - reset = resetVoltageTracking -} \ No newline at end of file +return {calculate = smartFuelCalc, reset = resetVoltageTracking} diff --git a/scripts/dashx/tasks/sensors/sensors.lua b/scripts/dashx/tasks/sensors/sensors.lua index 2d55d13..93c83ac 100644 --- a/scripts/dashx/tasks/sensors/sensors.lua +++ b/scripts/dashx/tasks/sensors/sensors.lua @@ -1,32 +1,17 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- --- local arg = {...} local config = arg[1] local sensors = {} local loadedSensorModule = nil -local delayDuration = 2 -- seconds +local delayDuration = 2 local delayStartTime = nil local delayPending = false @@ -36,20 +21,8 @@ local log = dashx.utils.log local tasks = dashx.tasks local telemetryStartTime = os.clock() -local TELEMETRY_TIMEOUT = 20 -- seconds - ---[[ - loadSensorModule - Loads the appropriate sensor module based on the current protocol and preferences. +local TELEMETRY_TIMEOUT = 20 - This function checks if the dashx tasks are active and if the API version is available. - Depending on the protocol (either "crsf" or "sport") and the user's preferences, it loads the corresponding sensor module. - - For "crsf" protocol, it loads the "elrs" sensor module if internalElrsSensors preference is enabled. - - For "sport" protocol, it loads either the "frsky" or "frsky_legacy" sensor module based on the API version and internalSportSensors preference. - If no matching sensor is found, it clears the loadedSensorModule to save memory. - - Returns: - nil - If the tasks are not active or the API version is not available. -]] local function loadSensorModule() if not tasks.active() then return nil end if not dashx.session.apiVersion then return nil end @@ -57,17 +30,11 @@ local function loadSensorModule() local protocol = dashx.session.telemetryType or "sport" if system:getVersion().simulation == true then - if not loadedSensorModule or loadedSensorModule.name ~= "sim" then - --log("Loading Simulator sensor module","info") - loadedSensorModule = {name = "sim", module = assert(loadfile("tasks/sensors/sim.lua"))(config)} - end + if not loadedSensorModule or loadedSensorModule.name ~= "sim" then loadedSensorModule = {name = "sim", module = assert(loadfile("tasks/sensors/sim.lua"))(config)} end elseif protocol == "sport" then - if not loadedSensorModule or loadedSensorModule.name ~= "frsky" then - --log("Loading FrSky sensor module","info") - loadedSensorModule = {name = "frsky", module = assert(loadfile("tasks/sensors/frsky.lua"))(config)} - end + if not loadedSensorModule or loadedSensorModule.name ~= "frsky" then loadedSensorModule = {name = "frsky", module = assert(loadfile("tasks/sensors/frsky.lua"))(config)} end else - loadedSensorModule = nil -- No matching sensor, clear to save memory + loadedSensorModule = nil end end @@ -76,46 +43,36 @@ function sensors.wakeup() if dashx.session.resetSensors and not delayPending then delayStartTime = os.clock() delayPending = true - dashx.session.resetSensors = false -- Reset immediately - log("Delaying sensor wakeup for " .. delayDuration .. " seconds","info") - return -- Exit early; wait starts now + dashx.session.resetSensors = false + log("Delaying sensor wakeup for " .. delayDuration .. " seconds", "info") + return end if delayPending then if os.clock() - delayStartTime >= delayDuration then - log("Delay complete; resuming sensor wakeup","info") + log("Delay complete; resuming sensor wakeup", "info") delayPending = false else local module = model.getModule(dashx.session.telemetrySensor:module()) if module ~= nil and module.muteSensorLost ~= nil then module:muteSensorLost(5.0) end - return -- Still waiting; do nothing + return end end loadSensorModule() - if loadedSensorModule and loadedSensorModule.module.wakeup then - loadedSensorModule.module.wakeup() - end + if loadedSensorModule and loadedSensorModule.module.wakeup then loadedSensorModule.module.wakeup() end - -- run smart sensors - if smart and smart.wakeup then - if dashx.session.isConnected then - smart.wakeup() - end - - end + if smart and smart.wakeup then if dashx.session.isConnected then smart.wakeup() end end end function sensors.reset() - if loadedSensorModule and loadedSensorModule.module and loadedSensorModule.module.reset then - loadedSensorModule.module.reset() - end + if loadedSensorModule and loadedSensorModule.module and loadedSensorModule.module.reset then loadedSensorModule.module.reset() end smart.reset() - loadedSensorModule = nil -- Clear loaded sensor module + loadedSensorModule = nil end diff --git a/scripts/dashx/tasks/sensors/sim.lua b/scripts/dashx/tasks/sensors/sim.lua index fa77c3c..de42d28 100644 --- a/scripts/dashx/tasks/sensors/sim.lua +++ b/scripts/dashx/tasks/sensors/sim.lua @@ -1,28 +1,14 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +local dashx = require("dashx") -]] -- local arg = {...} local config = arg[1] -local cacheExpireTime = 30 -- Future-proofed, in case cache flush is implemented +local cacheExpireTime = 30 local lastCacheFlushTime = os.clock() local lastWakeupTime = 0 @@ -36,38 +22,42 @@ sim.name = "sim" local sensorList = dashx.tasks.telemetry.simSensors() --- Drop list as a set-like table (only care about keys) local dropList = { - ["0xF104"] = true, ["0x0300"] = true, ["0x0301"] = true, - ["0x0100"] = true, ["0x0110"] = true, ["0x0500"] = true, - ["0x0200"] = true, ["0x0800"] = true, ["0x0850"] = true, - ["0x0830"] = true, ["0x0820"] = true, ["0x0840"] = true, - ["0xF103"] = true, ["0x0A00"] = true, ["0x0210"] = true, - ["0x0B20"] = true, ["0x0730"] = true, ["0xF108"] = true, - ["0x0B60"] = true, ["0x0D50"] = true, ["0x0D10"] = true, - ["0x0D20"] = true, ["0x0D40"] = true, ["0x0D00"] = true, - ["0x0D30"] = true, ["0x0D60"] = true, ["0x0D70"] = true, - ["0x0E60"] = true, ["0x7360"] = true, + ["0xF104"] = true, + ["0x0300"] = true, + ["0x0301"] = true, + ["0x0100"] = true, + ["0x0110"] = true, + ["0x0500"] = true, + ["0x0200"] = true, + ["0x0800"] = true, + ["0x0850"] = true, + ["0x0830"] = true, + ["0x0820"] = true, + ["0x0840"] = true, + ["0xF103"] = true, + ["0x0A00"] = true, + ["0x0210"] = true, + ["0x0B20"] = true, + ["0x0730"] = true, + ["0xF108"] = true, + ["0x0B60"] = true, + ["0x0D50"] = true, + ["0x0D10"] = true, + ["0x0D20"] = true, + ["0x0D40"] = true, + ["0x0D00"] = true, + ["0x0D30"] = true, + ["0x0D60"] = true, + ["0x0D70"] = true, + ["0x0E60"] = true, + ["0x7360"] = true } -local sensors = { - uid = {}, - lastvalue = {} -} +local sensors = {uid = {}, lastvalue = {}} ---[[ - Creates a sensor with the specified parameters and adds it to the sensors table. - - @param uid (number) - Unique identifier for the sensor. - @param name (string) - Name of the sensor. - @param unit (string) - Unit of measurement for the sensor (optional). - @param dec (number) - Number of decimal places for the sensor value (optional). - @param value (number) - Initial value of the sensor (optional). - @param min (number) - Minimum value the sensor can report (optional, default is -1000000000). - @param max (number) - Maximum value the sensor can report (optional, default is 2147483647). -]] local function createSensor(uid, name, unit, dec, value, min, max) - local sensor = model.createSensor({type=SENSOR_TYPE_DIY}) + local sensor = model.createSensor({type = SENSOR_TYPE_DIY}) sensor:name(name) sensor:appId(uid) sensor:module(dashx.session.telemetrySensor:module()) @@ -89,38 +79,11 @@ local function createSensor(uid, name, unit, dec, value, min, max) sensors.uid[uid] = sensor end ---[[ - dropSensor(uid) - - This function drops a telemetry sensor source identified by the given unique identifier (uid). - - Parameters: - uid (number) - The unique identifier of the telemetry sensor to be dropped. - - The function retrieves the telemetry sensor source using the provided uid and, if found, calls the drop method on the source to remove it. -]] local function dropSensor(uid) local src = system.getSource({category = CATEGORY_TELEMETRY_SENSOR, appId = uid}) - if src then - src:drop() - end + if src then src:drop() end end ---[[ - ensureSensorExists(uid, name, unit, dec, value, min, max) - - Ensures that a sensor with the specified UID exists. If the sensor does not exist, it attempts to find an existing sensor with the given UID. - If an existing sensor is found, it is added to the sensors table. If no existing sensor is found, a new sensor is created with the provided parameters. - - Parameters: - uid (string) - Unique identifier for the sensor. - name (string) - Name of the sensor. - unit (string) - Unit of measurement for the sensor. - dec (number) - Decimal precision for the sensor value. - value (number) - Initial value of the sensor. - min (number) - Minimum value for the sensor. - max (number) - Maximum value for the sensor. -]] local function ensureSensorExists(uid, name, unit, dec, value, min, max) if not sensors.uid[uid] then local existingSensor = system.getSource({category = CATEGORY_TELEMETRY_SENSOR, appId = uid}) @@ -133,30 +96,13 @@ local function ensureSensorExists(uid, name, unit, dec, value, min, max) end end ---[[ - Updates the value of a sensor identified by its unique identifier (uid). - - @param uid (string) - The unique identifier of the sensor. - @param value (number | function) - The new value to set for the sensor. - If a function is provided, it will be called to get the value. -]] local function updateSensorValue(uid, value) if sensors.uid[uid] then - if type(value) == "function" then - value = value() - end + if type(value) == "function" then value = value() end sensors.uid[uid]:value(value) end end ---[[ - Function: flushCacheIfNeeded - Description: This function checks if the cache needs to be flushed based on the elapsed time since the last cache flush. - If the elapsed time is greater than or equal to the cache expiration time, it clears the sensor UID and last value caches, - and updates the last cache flush time to the current time. - Parameters: None - Returns: None -]] local function flushCacheIfNeeded() if os.clock() - lastCacheFlushTime >= cacheExpireTime then sensors.uid = {} @@ -165,38 +111,11 @@ local function flushCacheIfNeeded() end end ---[[ - dropAutoDiscoveredSensors - - This function iterates over the `dropList` table and calls the `dropSensor` function - for each unique identifier (uid) found in the `dropList`. It is used to remove or - drop sensors that have been automatically discovered. - - Parameters: - None +local function dropAutoDiscoveredSensors() for uid in pairs(dropList) do dropSensor(uid) end end - Returns: - None -]] -local function dropAutoDiscoveredSensors() - for uid in pairs(dropList) do - dropSensor(uid) - end -end - ---[[ - handleSensors function iterates through a list of sensors and processes each sensor's data. - - For each sensor in the sensorList: - - Extracts the sensor's unique identifier (uid), name, unit, decimal precision (dec), current value, minimum value, and maximum value. - - If the uid, min, max, and value are all present: - - Calls ensureSensorExists to ensure the sensor is registered with the given parameters. - - Calls updateSensorValue to update the sensor's current value. -]] local function handleSensors() for _, v in ipairs(sensorList) do - local uid, name, unit, dec, value, min, max = - v.sensor.uid, v.name, v.sensor.unit, v.sensor.dec, v.sensor.value, v.sensor.min, v.sensor.max + local uid, name, unit, dec, value, min, max = v.sensor.uid, v.name, v.sensor.unit, v.sensor.dec, v.sensor.value, v.sensor.min, v.sensor.max if uid and min and max and value then ensureSensorExists(uid, name, unit, dec, value, min, max) @@ -205,15 +124,6 @@ local function handleSensors() end end ---[[ - The `wakeup` function is responsible for periodically handling sensor updates and cache management. - - It performs the following tasks: - 1. Checks the current time using `os.clock()`. - 2. If the elapsed time since the last wakeup is greater than or equal to `wakeupInterval`, it calls `handleSensors()` to process sensor data and updates `lastWakeupTime`. - 3. If it is the first run or the elapsed time since the last drop is greater than or equal to `wakeupIntervalDrop`, it calls `dropAutoDiscoveredSensors()` to remove automatically discovered sensors, updates `lastWakeupTimeDrop`, and sets `firstRun` to false. - 4. Calls `flushCacheIfNeeded()` to manage the cache if necessary. ---]] local function wakeup() local now = os.clock() @@ -231,7 +141,6 @@ local function wakeup() flushCacheIfNeeded() end --- reset function sim.reset() sensors.uid = {} sensors.lastvalue = {} diff --git a/scripts/dashx/tasks/sensors/smart.lua b/scripts/dashx/tasks/sensors/smart.lua index 9034d41..f1d636a 100644 --- a/scripts/dashx/tasks/sensors/smart.lua +++ b/scripts/dashx/tasks/sensors/smart.lua @@ -1,123 +1,67 @@ -local dashx = require("dashx") --[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - * Copyright (C) dashx Project - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - - * MSP Sensor Table Structure - * - * msp_sensors: A table defining APIs to be polled via MSP and how to map their values to telemetry sensors. - * Each top-level key is the MSP API name (e.g., "DATAFLASH_SUMMARY"). - * Each entry must include polling intervals and a 'fields' table containing telemetry sensor configs. - * - * Structure: - * { - * API_NAME = { - * interval_armed: -- Interval (in seconds) to poll this API when the model is armed (-1 for no polling) - * interval_disarmed: -- Interval (in seconds) when disarmed (-1 for no polling) - * interval_admin: -- Interval (in seconds) when admin module loaded (-1 for no polling) - * - * fields = { - * field_key = { - * sensorname: -- Label shown in radio telemetry menu - * sessionname: -- Optional session variable name to update - * appId: -- Unique sensor ID (must be unique across all sensors) - * unit: -- Telemetry unit (e.g., UNIT_RAW, UNIT_VOLT, etc.) - * minimum: -- Optional minimum value (default: -1e9) - * maximum: -- Optional maximum value (default: 1e9) - * transform: -- Optional value processing function before display - * }, - * ... - * } - * }, - * ... - * } - - * Possible sensor ids we can use are. - * 0x5FE1 - smartfuel - * 0x5FE0 - armed - * 0x5FDF - inflight - * 0x5FDE - smartconsumption - * 0x5FDD - * 0x5FDC - * 0x5FDB - * 0x5FDA - * 0x5FD9 - * 0x5FD8 - * 0x5FD7 - * 0x5FD6 - * 0x5FD5 - * 0x5FD4 - * 0x5FD3 - * 0x5FD2 - * 0x5FD1 - * 0x5FD0 - * 0x5FCF - * 0x5FCE - -]] +local dashx = require("dashx") local smart = {} local smartfuel = assert(loadfile("tasks/sensors/lib/smartfuel.lua"))() local smartfuelvoltage = assert(loadfile("tasks/sensors/lib/smartfuelvoltage.lua"))() --- container vars local log -local tasks +local tasks -local interval = 1 +local interval = 1 local lastWake = os.clock() local firstWakeup = true - local function calculateFuel() - -- work out what type of sensor we are running and use - -- the appropriate calculation method + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.calc_local then - -- if we dont have a consumption.. fallback to voltage - if dashx.session.modelPreferences.battery.calc_local == 1 or not dashx.tasks.telemetry.getSensorSource("consumption") then + + if dashx.session.modelPreferences.battery.calc_local == 1 or not dashx.tasks.telemetry.getSensorSource("consumption") then return smartfuelvoltage.calculate() - else + else return smartfuel.calculate() - end + end else - return smartfuel.calculate() + return smartfuel.calculate() end end local function calculateConsumption() - -- If smartvoltage is enabled, calculate mAh used based on capacity - if dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.calc_local then - if dashx.session.modelPreferences.battery.calc_local == 1 or not dashx.tasks.telemetry.getSensorSource("consumption") then - local capacity = (dashx.session.batteryConfig and dashx.session.batteryConfig.batteryCapacity) or 1000 -- Default to 1000mAh if not set - local smartfuelPct = dashx.tasks.telemetry.getSensor("smartfuel") - local warningPercentage = (dashx.session.batteryConfig and dashx.session.batteryConfig.consumptionWarningPercentage) or 30 - if smartfuelPct then - local usableCapacity = capacity * (1 - warningPercentage / 100) - local usedPercent = 100 - smartfuelPct -- how much has been used - return (usedPercent / 100) * usableCapacity - end - else - -- fallback to FC "consumption" - return dashx.tasks.telemetry.getSensor("consumption") or 0 - end - else - -- No battery prefs — fallback to FC "consumption" - return dashx.tasks.telemetry.getSensor("consumption") or 0 + + if dashx.session.modelPreferences and dashx.session.modelPreferences.battery and dashx.session.modelPreferences.battery.calc_local then + if dashx.session.modelPreferences.battery.calc_local == 1 or not dashx.tasks.telemetry.getSensorSource("consumption") then + local capacity = (dashx.session.batteryConfig and dashx.session.batteryConfig.batteryCapacity) or 1000 + local smartfuelPct = dashx.tasks.telemetry.getSensor("smartfuel") + local warningPercentage = (dashx.session.batteryConfig and dashx.session.batteryConfig.consumptionWarningPercentage) or 30 + if smartfuelPct then + local usableCapacity = capacity * (1 - warningPercentage / 100) + local usedPercent = 100 - smartfuelPct + return (usedPercent / 100) * usableCapacity end -end + else + return dashx.tasks.telemetry.getSensor("consumption") or 0 + end + else + + return dashx.tasks.telemetry.getSensor("consumption") or 0 + end +end local switchCache = {} local smart_sensors = { armed = { name = "Armed", - appId = 0x5FE0, -- Unique sensor ID - unit = UNIT_RAW, -- Telemetry unit + appId = 0x5FE0, + unit = UNIT_RAW, minimum = 0, maximum = 1, value = function() @@ -125,22 +69,20 @@ local smart_sensors = { local settings = dashx.session.modelPreferences.model if settings.armswitch then local category, member, options = settings.armswitch:match("([^:]+):([^:]+):([^:]+)") - - if not switchCache["armed"] then - switchCache["armed"] = system.getSource({category = category, member = member, options = options}) - end + + if not switchCache["armed"] then switchCache["armed"] = system.getSource({category = category, member = member, options = options}) end local state = switchCache["armed"]:state() - return(state and 0 or 1) - end + return (state and 0 or 1) + end end - return false - end, + return false + end }, inflight = { name = "Inflight", - appId = 0x5FDF, -- Unique sensor ID - unit = UNIT_RAW, -- Telemetry unit + appId = 0x5FDF, + unit = UNIT_RAW, minimum = 0, maximum = 1, value = function() @@ -148,34 +90,18 @@ local smart_sensors = { local settings = dashx.session.modelPreferences.model if settings.inflightswitch then local category, member, options = settings.inflightswitch:match("([^:]+):([^:]+):([^:]+)") - - if not switchCache["inflight"] then - switchCache["inflight"] = system.getSource({category = category, member = member, options = options}) - end + + if not switchCache["inflight"] then switchCache["inflight"] = system.getSource({category = category, member = member, options = options}) end local state = switchCache["inflight"]:state() - return(state and 0 or 1) + return (state and 0 or 1) end - end + end return false - end, - }, - smartfuel = { - name = "Smart Fuel", - appId = 0x5FE1, -- Unique sensor ID - unit = UNIT_PERCENT, -- Telemetry unit - minimum = 0, - maximum = 100, - value = calculateFuel, - }, - - smartconsumption = { - name = "Smart Consumption", - appId = 0x5FDE, -- Unique sensor ID - unit = UNIT_MILLIAMPERE_HOUR, -- Telemetry unit - minimum = 0, - maximum = 1000000000, - value = calculateConsumption, - }, + end + }, + smartfuel = {name = "Smart Fuel", appId = 0x5FE1, unit = UNIT_PERCENT, minimum = 0, maximum = 100, value = calculateFuel}, + + smartconsumption = {name = "Smart Consumption", appId = 0x5FDE, unit = UNIT_MILLIAMPERE_HOUR, minimum = 0, maximum = 1000000000, value = calculateConsumption} } smart.sensors = msp_sensors @@ -183,12 +109,12 @@ local sensorCache = {} local function createOrUpdateSensor(appId, fieldMeta, value) if not sensorCache[appId] then - local existingSensor = system.getSource({ category = CATEGORY_TELEMETRY_SENSOR, appId = appId }) + local existingSensor = system.getSource({category = CATEGORY_TELEMETRY_SENSOR, appId = appId}) if existingSensor then sensorCache[appId] = existingSensor else - local sensor = model.createSensor({type=SENSOR_TYPE_DIY}) + local sensor = model.createSensor({type = SENSOR_TYPE_DIY}) sensor:name(fieldMeta.name) sensor:appId(appId) sensor:physId(0) @@ -208,11 +134,10 @@ local function createOrUpdateSensor(appId, fieldMeta, value) if value then sensorCache[appId]:value(value) else - sensorCache[appId]:reset() + sensorCache[appId]:reset() end end - local lastWakeupTime = 0 function smart.wakeup() @@ -222,10 +147,7 @@ function smart.wakeup() firstWakeup = false end - -- rate-limit: bail out until interval has elapsed - if (os.clock() - lastWake) < interval then - return - end + if (os.clock() - lastWake) < interval then return end lastWake = os.clock() for name, meta in pairs(smart_sensors) do @@ -233,8 +155,8 @@ function smart.wakeup() if type(meta.value) == "function" then value = meta.value() else - value = meta.value -- Assume value is already calculated - end + value = meta.value + end createOrUpdateSensor(meta.appId, meta, value) end end diff --git a/scripts/dashx/tasks/simevent/init.lua b/scripts/dashx/tasks/simevent/init.lua index c7a674b..0d46a92 100644 --- a/scripts/dashx/tasks/simevent/init.lua +++ b/scripts/dashx/tasks/simevent/init.lua @@ -1,28 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 1, -- run every second - script = "simevent.lua", -- run this script - linkrequired = false, -- run this script only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = true, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 1, script = "simevent.lua", linkrequired = false, spreadschedule = true, simulatoronly = true} return init diff --git a/scripts/dashx/tasks/simevent/simevent.lua b/scripts/dashx/tasks/simevent/simevent.lua index 70e3052..6ab6109 100644 --- a/scripts/dashx/tasks/simevent/simevent.lua +++ b/scripts/dashx/tasks/simevent/simevent.lua @@ -1,47 +1,40 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") --- simevent.lua + local simevent = {} --- adjust this to point at your sensor-script folder local source = "SCRIPTS:/" .. dashx.config.baseDir .. "/sim/sensors/" --- map each script-name to the handler you want run on *actual* changes -local handlers = { - simevent_telemetry_state = function(value) - -- set your telemetry state based on the returned value - dashx.simevent.telemetry_state = (value == 0) - end, - -- add more sensor handlers here... -} +local handlers = {simevent_telemetry_state = function(value) dashx.simevent.telemetry_state = (value == 0) end} --- keep track of the last result, per sensor local lastValues = {} --- call this from your own loop whenever you want to poll for changes function simevent.wakeup() - if not system.getVersion().simulation then - return - end - - for name, handler in pairs(handlers) do - local path = source .. name .. ".lua" - -- load and compile the file fresh each time - local chunk, loadErr = loadfile(path) - if not chunk then - print(("sim: could not load %s.lua: %s"):format(name, loadErr)) - else - -- execute the chunk and capture its returned value - local ok, result = pcall(chunk) - if not ok then - print(("sim: error running %s.lua: %s"):format(name, result)) - elseif result ~= lastValues[name] then - -- only fire when the returned value actually changes - lastValues[name] = result - handler(result) - end + if not system.getVersion().simulation then return end + + for name, handler in pairs(handlers) do + local path = source .. name .. ".lua" + + local chunk, loadErr = loadfile(path) + if not chunk then + print(("sim: could not load %s.lua: %s"):format(name, loadErr)) + else + + local ok, result = pcall(chunk) + if not ok then + print(("sim: error running %s.lua: %s"):format(name, result)) + elseif result ~= lastValues[name] then + + lastValues[name] = result + handler(result) + end + end end - end end return simevent diff --git a/scripts/dashx/tasks/tasks.lua b/scripts/dashx/tasks/tasks.lua index ec6866d..47b96dc 100644 --- a/scripts/dashx/tasks/tasks.lua +++ b/scripts/dashx/tasks/tasks.lua @@ -1,50 +1,10 @@ -local dashx = require("dashx") --[[ - - * Copyright (C) DASHX Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * - --- Task scheduler timing notes: --- - The scheduler tick runs at 20 Hz (every 50 ms). --- - Spread tasks can run as often as every tick (minimum ~0.05 s). --- - Non-spread tasks are only checked every other tick (minimum ~0.1 s). --- - A small random jitter (~+0.1 s) is applied when tasks are loaded, --- so very short intervals will often be rounded upward. --- --- Useful for periodic operations that require sub-second timing. - --- The tasks flip with running each on a different cycle is important. --- It prevents the system 'bogging down' if many tasks are due at the same time. --- It also helps to spread out CPU load, which is important for accurate CPU timing. - --- An example for a task meta info is as follows - --- local init = { --- interval = 0.25, -- run every 0.25 seconds. Note. Minimum interval is ~0.05s --- script = "sensors.lua", -- run this script --- linkrequired = true, -- run this script only if link is established --- spreadschedule = false, -- run on every loop --- simulatoronly = false, -- run this script in simulation mode --- connected = true, -- run this script only if msp is connected --- } - + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- --- keep these constant / cheap definitions at file scope +local dashx = require("dashx") + local utils = dashx.utils local compiler = loadfile @@ -55,13 +15,11 @@ local taskSchedulerPercentage local schedulerTick local lastSensorName local tasks, tasksList = {}, {} -tasks.heartbeat, tasks.begin, tasks.wasOn = nil, nil, false -- begin nil by default - +tasks.heartbeat, tasks.begin, tasks.wasOn = nil, nil, false local currentSensor, currentModuleId, currentTelemetryType local internalModule, externalModule - tasks._justInitialized = false tasks._initState = "start" tasks._initMetadata = nil @@ -69,7 +27,7 @@ tasks._initKeys = nil tasks._initIndex = 1 local ethosVersionGood -local telemetryCheckScheduler = os.clock -- keep reference, actual timers set later +local telemetryCheckScheduler = os.clock local lastCheckAt local lastTelemetryType @@ -79,14 +37,12 @@ local NAME_CHECK_INTERVAL = 2.0 local usingSimulator = system.getVersion().simulation -local tlm +local tlm --- CPU constants (cheap -> keep) -local CPU_TICK_HZ = 20 -local SCHED_DT = 1 / CPU_TICK_HZ -local OVERDUE_TOL = SCHED_DT * 0.25 +local CPU_TICK_HZ = 20 +local SCHED_DT = 1 / CPU_TICK_HZ +local OVERDUE_TOL = SCHED_DT * 0.25 --- CPU/mem moving-average state (declare only) local last_wakeup_start local CPU_TICK_BUDGET local CPU_ALPHA @@ -97,37 +53,24 @@ local mem_avg_kb local last_mem_t local MEM_PERIOD - --- ========================= --- Profiler config & helpers --- ========================= --- Zero-overhead when disabled (just a few conditionals). -tasks.profile = { - enabled = false, -- master switch - dumpInterval = 5, -- seconds between dumps at end of wakeup() - minDuration = 0, -- only record runs >= this many seconds - include = nil, -- optional set: { taskname = true, ... } - exclude = nil, -- optional set: { taskname = true, ... } - onDump = nil -- optional function(snapshot) -> true to suppress default logging -} +tasks.profile = {enabled = false, dumpInterval = 5, minDuration = 0, include = nil, exclude = nil, onDump = nil} local function profWanted(name) - if not tasks.profile.enabled then return false end - local inc, exc = tasks.profile.include, tasks.profile.exclude - if inc and not inc[name] then return false end - if exc and exc[name] then return false end - return true + if not tasks.profile.enabled then return false end + local inc, exc = tasks.profile.include, tasks.profile.exclude + if inc and not inc[name] then return false end + if exc and exc[name] then return false end + return true end local function profRecord(task, dur) - if dur < (tasks.profile.minDuration or 0) then return end - task.duration = dur - task.totalDuration = (task.totalDuration or 0) + dur - task.runs = (task.runs or 0) + 1 - task.maxDuration = math.max(task.maxDuration or 0, dur) + if dur < (tasks.profile.minDuration or 0) then return end + task.duration = dur + task.totalDuration = (task.totalDuration or 0) + dur + task.runs = (task.runs or 0) + 1 + task.maxDuration = math.max(task.maxDuration or 0, dur) end --- Returns true if the task is active (based on recent run time or triggers) function tasks.isTaskActive(name) for _, t in ipairs(tasksList) do if t.name == name then @@ -146,30 +89,21 @@ end local function taskOffset(name, interval) local hash = 0 - for i = 1, #name do - hash = (hash * 31 + name:byte(i)) % 100000 - end - local base = (hash % (interval * 1000)) / 1000 -- base hash offset + for i = 1, #name do hash = (hash * 31 + name:byte(i)) % 100000 end + local base = (hash % (interval * 1000)) / 1000 local jitter = math.random() * interval return (base + jitter) % interval end --- Print a human-readable schedule of all tasks function tasks.dumpSchedule() - local now = os.clock() - utils.log("====== Task Schedule Dump ======", "info") - for _, t in ipairs(tasksList) do - local next_run = t.last_run + t.interval - local in_secs = next_run - now - utils.log( - string.format( - "%-15s | interval: %4.3fs | last_run: %8.3f | next in: %6.3fs", - t.name, t.interval, t.last_run, in_secs - ), - "info" - ) - end - utils.log("================================", "info") + local now = os.clock() + utils.log("====== Task Schedule Dump ======", "info") + for _, t in ipairs(tasksList) do + local next_run = t.last_run + t.interval + local in_secs = next_run - now + utils.log(string.format("%-15s | interval: %4.3fs | last_run: %8.3f | next in: %6.3fs", t.name, t.interval, t.last_run, in_secs), "info") + end + utils.log("================================", "info") end function tasks.initialize() @@ -194,15 +128,7 @@ function tasks.initialize() elseif func then local tconfig = func() if type(tconfig) == "table" and tconfig.interval and tconfig.script then - taskMetadata[dir] = { - interval = tconfig.interval, - script = tconfig.script, - linkrequired = tconfig.linkrequired or false, - connected = tconfig.connected or false, - simulatoronly = tconfig.simulatoronly or false, - spreadschedule = tconfig.spreadschedule or false, - init = initPath - } + taskMetadata[dir] = {interval = tconfig.interval, script = tconfig.script, linkrequired = tconfig.linkrequired or false, connected = tconfig.connected or false, simulatoronly = tconfig.simulatoronly or false, spreadschedule = tconfig.spreadschedule or false, init = initPath} end end end @@ -232,14 +158,13 @@ function tasks.findTasks() local scriptPath = taskPath .. dir .. "/" .. tconfig.script local fn, loadErr = loadfile(scriptPath) if fn then - tasks[dir] = fn(config) -- assumes global 'config' + tasks[dir] = fn(config) else utils.log("Failed to load task script " .. scriptPath .. ": " .. loadErr, "warn") end - -- add a small drift to de-synchronize fixed intervals local baseInterval = tconfig.interval or 1 - local interval = baseInterval + (math.random() * 0.1) + local interval = baseInterval + (math.random() * 0.1) local offset = taskOffset(dir, interval) local task = { @@ -251,7 +176,7 @@ function tasks.findTasks() spreadschedule = tconfig.spreadschedule or false, simulatoronly = tconfig.simulatoronly or false, last_run = os.clock() - offset, - -- profiling fields + duration = 0, totalDuration = 0, runs = 0, @@ -259,14 +184,7 @@ function tasks.findTasks() } table.insert(tasksList, task) - taskMetadata[dir] = { - interval = task.interval, - script = task.script, - linkrequired = task.linkrequired, - connected = task.connected, - simulatoronly = task.simulatoronly, - spreadschedule = task.spreadschedule - } + taskMetadata[dir] = {interval = task.interval, script = task.script, linkrequired = task.linkrequired, connected = task.connected, simulatoronly = task.simulatoronly, spreadschedule = task.spreadschedule} end end end @@ -288,71 +206,58 @@ local function clearSessionAndQueue() end --- Telemetry check scheduler: function tasks.telemetryCheckScheduler() local now = os.clock() local telemetryState = (tlm and tlm:state()) or false - if system.getVersion().simulation and dashx.simevent.telemetry_state == false then - telemetryState = false - end + if system.getVersion().simulation and dashx.simevent.telemetry_state == false then telemetryState = false end - -- early out if link is down - if not telemetryState then - return clearSessionAndQueue() - end + if not telemetryState then return clearSessionAndQueue() end - -- fast path: if we already have a sensor, don’t rescan every time if currentSensor then - dashx.session.telemetryState = true - dashx.session.telemetrySensor = currentSensor - dashx.session.telemetryModule = currentModuleId - dashx.session.telemetryType = currentTelemetryType + dashx.session.telemetryState = true + dashx.session.telemetrySensor = currentSensor + dashx.session.telemetryModule = currentModuleId + dashx.session.telemetryType = currentTelemetryType - -- catch switching when to fast for telemetry to drop if now - lastNameCheckAt >= NAME_CHECK_INTERVAL then lastNameCheckAt = now if currentSensor:name() ~= lastSensorName then utils.log("Telemetry sensor changed to " .. tostring(currentSensor:name()), "info") lastSensorName = currentSensor:name() - currentSensor = nil -- force re-detect next time + currentSensor = nil end - end + end - return + return end - -- only do heavy calls when we *don’t* already have a sensor if not internalModule or not externalModule then internalModule = model.getModule(0) externalModule = model.getModule(1) end if internalModule and internalModule:enable() then - currentSensor = system.getSource({ appId = 0xF101 }) - currentModuleId = internalModule + currentSensor = system.getSource({appId = 0xF101}) + currentModuleId = internalModule currentTelemetryType = "sport" elseif externalModule and externalModule:enable() then - currentSensor = system.getSource({ crsfId = 0x14, subIdStart = 0, subIdEnd = 1 }) - currentModuleId = externalModule + currentSensor = system.getSource({crsfId = 0x14, subIdStart = 0, subIdEnd = 1}) + currentModuleId = externalModule currentTelemetryType = "crsf" if not currentSensor then - currentSensor = system.getSource({ appId = 0xF101 }) + currentSensor = system.getSource({appId = 0xF101}) currentTelemetryType = "sport" end end + if not currentSensor then return clearSessionAndQueue() end - if not currentSensor then - return clearSessionAndQueue() - end - - dashx.session.telemetryState = true + dashx.session.telemetryState = true dashx.session.telemetrySensor = currentSensor dashx.session.telemetryModule = currentModuleId - dashx.session.telemetryType = currentTelemetryType - + dashx.session.telemetryType = currentTelemetryType if currentTelemetryType ~= lastTelemetryType then dashx.utils.log("Telemetry type changed to " .. tostring(currentTelemetryType), "info") @@ -373,28 +278,20 @@ function tasks.active() return false end --- compute positive seconds overdue (<= 0 means not yet due) -local function overdue_seconds(task, now, grace_s) - return (now - task.last_run) - (task.interval + (grace_s or 0)) -end +local function overdue_seconds(task, now, grace_s) return (now - task.last_run) - (task.interval + (grace_s or 0)) end --- All-second logic + sub-second tolerance; returns (ok_to_run, overdue_seconds) local function canRunTask(task, now) - local hf = task.interval < SCHED_DT -- high-frequency task - local grace = hf and OVERDUE_TOL or (task.interval * 0.25) -- light grace for slow tasks + local hf = task.interval < SCHED_DT + local grace = hf and OVERDUE_TOL or (task.interval * 0.25) - local od = overdue_seconds(task, now, grace) -- >0 means overdue by that many seconds + local od = overdue_seconds(task, now, grace) local priorityTask = task.name == "msp" or task.name == "callback" local linkOK = not task.linkrequired or dashx.session.telemetryState - local connOK = not task.connected or dashx.session.isConnected + local connOK = not task.connected or dashx.session.isConnected - local ok = - linkOK - and connOK - and (priorityTask or od >= 0 or not (dashx.app.triggers and dashx.app.triggers.mspBusy)) - and (not task.simulatoronly or usingSimulator) + local ok = linkOK and connOK and (priorityTask or od >= 0 or not (dashx.app.triggers and dashx.app.triggers.mspBusy)) and (not task.simulatoronly or usingSimulator) return ok, od end @@ -406,9 +303,7 @@ function tasks.wakeup() tasks.profile.enabled = dashx.preferences and dashx.preferences.developer and dashx.preferences.developer.taskprofiler - if ethosVersionGood == nil then - ethosVersionGood = utils.ethosVersionAtLeast() - end + if ethosVersionGood == nil then ethosVersionGood = utils.ethosVersionAtLeast() end if not ethosVersionGood then return end if tasks.begin == true then @@ -436,7 +331,7 @@ function tasks.wakeup() end end local script = "tasks/" .. key .. "/" .. meta.script - local module = assert(loadfile(script))(config) -- assumes global 'config' + local module = assert(loadfile(script))(config) tasks[key] = module if meta.interval >= 0 then @@ -452,7 +347,7 @@ function tasks.wakeup() connected = meta.connected or false, simulatoronly = meta.simulatoronly or false, last_run = os.clock() - offset, - -- profiling fields + duration = 0, totalDuration = 0, runs = 0, @@ -483,9 +378,7 @@ function tasks.wakeup() if okToRun then local elapsed = now - task.last_run if elapsed + OVERDUE_TOL >= task.interval then - if (od or 0) > 0 then - utils.log(string.format("[scheduler] %s overdue by %.3fs", task.name, od), "debug") - end + if (od or 0) > 0 then utils.log(string.format("[scheduler] %s overdue by %.3fs", task.name, od), "debug") end local fn = tasks[task.name].wakeup if fn then if profWanted(task.name) then @@ -522,14 +415,10 @@ function tasks.wakeup() local elapsed = now - task.last_run if elapsed >= 2 * task.interval then table.insert(mustRunTasks, task) - utils.log(string.format("[scheduler] %s hard overdue by %.3fs", - task.name, elapsed - 2*task.interval), "debug") + utils.log(string.format("[scheduler] %s hard overdue by %.3fs", task.name, elapsed - 2 * task.interval), "debug") elseif elapsed + OVERDUE_TOL >= task.interval then table.insert(normalEligibleTasks, task) - if elapsed - task.interval > 0 then - utils.log(string.format("[scheduler] %s overdue by %.3fs", - task.name, elapsed - task.interval), "debug") - end + if elapsed - task.interval > 0 then utils.log(string.format("[scheduler] %s overdue by %.3fs", task.name, elapsed - task.interval), "debug") end end end end @@ -539,11 +428,7 @@ function tasks.wakeup() table.sort(normalEligibleTasks, function(a, b) return a.last_run < b.last_run end) local nonSpreadCount = 0 - for _, task in ipairs(tasksList) do - if not task.spreadschedule then - nonSpreadCount = nonSpreadCount + 1 - end - end + for _, task in ipairs(tasksList) do if not task.spreadschedule then nonSpreadCount = nonSpreadCount + 1 end end tasksPerCycle = math.ceil(nonSpreadCount * taskSchedulerPercentage) @@ -602,7 +487,6 @@ function tasks.wakeup() runSpreadTasks() end - -- Periodic profile dump (only when profiler is on) if tasks.profile.enabled then tasks._lastProfileDump = tasks._lastProfileDump or now local dumpEvery = tasks.profile.dumpInterval or 5 @@ -612,93 +496,73 @@ function tasks.wakeup() end end - -- track average cpu load - -- Accurate CPU utilization: work_time / wall_time_between_wakeups - local t_end = os.clock() - local work_elapsed = t_end - now - - local dt - if last_wakeup_start ~= nil then - dt = now - last_wakeup_start - else - dt = (1 / CPU_TICK_HZ) - end - - -- Guard against pathological tiny dt (e.g., re-entrancy) - if dt < (0.25 * (1 / CPU_TICK_HZ)) then - dt = (1 / CPU_TICK_HZ) - end - - local instant_util = work_elapsed / dt -- 0..∞ - - -- ---- Simulator CPU bias --- - if usingSimulator then - -- Target the radio's baseline utilization in sim. - local SIM_TARGET_UTIL = 0.50 -- e.g. 20% - local SIM_MAX_UTIL = 0.80 -- never report above this via bias - -- Blend toward the target only when we're below it. - if instant_util < SIM_TARGET_UTIL then - -- Amount to blend this tick (EMA-ish). Tune 0.25..0.5 for snappier/slower convergence. - local BLEND = 0.55 - instant_util = math.min( - SIM_MAX_UTIL, - instant_util + (SIM_TARGET_UTIL - instant_util) * BLEND - ) + local t_end = os.clock() + local work_elapsed = t_end - now + + local dt + if last_wakeup_start ~= nil then + dt = now - last_wakeup_start + else + dt = (1 / CPU_TICK_HZ) end - end - -- ---- end simulator bias ---- + if dt < (0.25 * (1 / CPU_TICK_HZ)) then dt = (1 / CPU_TICK_HZ) end - cpu_avg = CPU_ALPHA * instant_util + (1 - CPU_ALPHA) * cpu_avg - dashx.session.cpuload = math.min(100, math.max(0, cpu_avg * 100)) + local instant_util = work_elapsed / dt - last_wakeup_start = now + if usingSimulator then + local SIM_TARGET_UTIL = 0.50 + local SIM_MAX_UTIL = 0.80 - -- track average memory usage - do - local now2 = os.clock() - if (now2 - last_mem_t) >= MEM_PERIOD then - last_mem_t = now2 - - local m = (system.getMemoryUsage and system.getMemoryUsage()) or nil - if m and m.luaRamAvailable then - -- Primary path: radio reports free Lua RAM (bytes) - local free_now_kb = (m.luaRamAvailable or 0) / 1000 - if mem_avg_kb == nil then - mem_avg_kb = free_now_kb - else - mem_avg_kb = MEM_ALPHA * free_now_kb + (1 - MEM_ALPHA) * mem_avg_kb - end - dashx.session.freeram = mem_avg_kb -- KB (ema) - dashx.session.luaUsedKb = collectgarbage and collectgarbage("count") or nil - dashx.session.memSource = "system" - else - -- Fallback: no system metric; still report Lua heap used so UI isn’t “0” - local used_kb = collectgarbage and collectgarbage("count") or nil - dashx.session.luaUsedKb = used_kb - -- keep last known free if we had one; otherwise mark as nil - dashx.session.freeram = mem_avg_kb -- may be nil if never known - dashx.session.memSource = "lua" + if instant_util < SIM_TARGET_UTIL then + + local BLEND = 0.55 + instant_util = math.min(SIM_MAX_UTIL, instant_util + (SIM_TARGET_UTIL - instant_util) * BLEND) end end + + cpu_avg = CPU_ALPHA * instant_util + (1 - CPU_ALPHA) * cpu_avg + dashx.session.cpuload = math.min(100, math.max(0, cpu_avg * 100)) + + last_wakeup_start = now + + do + local now2 = os.clock() + if (now2 - last_mem_t) >= MEM_PERIOD then + last_mem_t = now2 + + local m = (system.getMemoryUsage and system.getMemoryUsage()) or nil + if m and m.luaRamAvailable then + + local free_now_kb = (m.luaRamAvailable or 0) / 1000 + if mem_avg_kb == nil then + mem_avg_kb = free_now_kb + else + mem_avg_kb = MEM_ALPHA * free_now_kb + (1 - MEM_ALPHA) * mem_avg_kb + end + dashx.session.freeram = mem_avg_kb + dashx.session.luaUsedKb = collectgarbage and collectgarbage("count") or nil + dashx.session.memSource = "system" + else + + local used_kb = collectgarbage and collectgarbage("count") or nil + dashx.session.luaUsedKb = used_kb + + dashx.session.freeram = mem_avg_kb + dashx.session.memSource = "lua" + end + end end end function tasks.reset() - --utils.log("Reset all tasks", "info") - for _, task in ipairs(tasksList) do - if tasks[task.name].reset then - tasks[task.name].reset() - end - end - dashx.utils.session() + + for _, task in ipairs(tasksList) do if tasks[task.name].reset then tasks[task.name].reset() end end + dashx.utils.session() end --- ========================= --- Profiling utilities --- ========================= function tasks.dumpProfile(opts) if not tasks.profile.enabled then return end local sortKey = (opts and opts.sort) or "avg" @@ -706,35 +570,15 @@ function tasks.dumpProfile(opts) for _, t in ipairs(tasksList) do local runs = t.runs or 0 local avg = runs > 0 and ((t.totalDuration or 0) / runs) or 0 - snapshot[#snapshot+1] = { - name = t.name, - last = t.duration or 0, - max = t.maxDuration or 0, - total= t.totalDuration or 0, - runs = runs, - avg = avg, - interval = t.interval or 0 - } + snapshot[#snapshot + 1] = {name = t.name, last = t.duration or 0, max = t.maxDuration or 0, total = t.totalDuration or 0, runs = runs, avg = avg, interval = t.interval or 0} end - local order = { - avg = function(a,b) return a.avg > b.avg end, - last = function(a,b) return a.last > b.last end, - max = function(a,b) return a.max > b.max end, - total = function(a,b) return a.total > b.total end, - runs = function(a,b) return a.runs > b.runs end - } + local order = {avg = function(a, b) return a.avg > b.avg end, last = function(a, b) return a.last > b.last end, max = function(a, b) return a.max > b.max end, total = function(a, b) return a.total > b.total end, runs = function(a, b) return a.runs > b.runs end} table.sort(snapshot, order[sortKey] or order.avg) - -- Allow consumer to intercept (e.g., UI sink) and suppress logs. if tasks.profile.onDump and tasks.profile.onDump(snapshot) then return end utils.log("====== Task Profile ======", "info") - for _, p in ipairs(snapshot) do - utils.log(string.format( - "%-15s | avg:%8.5fs | last:%8.5fs | max:%8.5fs | total:%8.3fs | runs:%6d | int:%4.3fs", - p.name, p.avg, p.last, p.max, p.total, p.runs, p.interval - ), "info") - end + for _, p in ipairs(snapshot) do utils.log(string.format("%-15s | avg:%8.5fs | last:%8.5fs | max:%8.5fs | total:%8.3fs | runs:%6d | int:%4.3fs", p.name, p.avg, p.last, p.max, p.total, p.runs, p.interval), "info") end utils.log("================================", "info") end @@ -748,73 +592,52 @@ function tasks.resetProfile() utils.log("[profile] Cleared profiling stats", "info") end -function tasks.event(widget, category, value, x, y) - print("Event:", widget, category, value, x, y) -end - +function tasks.event(widget, category, value, x, y) print("Event:", widget, category, value, x, y) end function tasks.init() - -- initialize all mutable runtime state here (no heavy work yet) - currentTelemetrySensor = nil - tasksPerCycle = 1 - taskSchedulerPercentage = 0.5 - schedulerTick = 0 - - ethosVersionGood = nil - lastSensorName = nil - lastCheckAt = nil - - -- profiler / CPU / mem tracking baselines - CPU_TICK_BUDGET = 1 / CPU_TICK_HZ - CPU_ALPHA = 0.2 - cpu_avg = 0 - last_wakeup_start = nil - - MEM_ALPHA = 0.2 - mem_avg_kb = nil - last_mem_t = 0 - MEM_PERIOD = 2.0 - - -- reset public flags - tasks.heartbeat = nil - tasks.wasOn = false - tasks._justInitialized = false - - -- fresh task container(s) - tasksList = {} - tasks._initState = "start" - tasks._initMetadata = nil - tasks._initKeys = nil - tasks._initIndex = 1 - - -- mark that we should run the discovery/bootstrap on first wakeup tick - tasks.begin = true - - -- Init telemetry sensor - tlm = system.getSource({ category = CATEGORY_SYSTEM_EVENT, member = TELEMETRY_ACTIVE }) -end + currentTelemetrySensor = nil + tasksPerCycle = 1 + taskSchedulerPercentage = 0.5 + schedulerTick = 0 + ethosVersionGood = nil + lastSensorName = nil + lastCheckAt = nil ---- Sets the telemetry type changed state for all tasks in the `tasksList`. --- Iterates through each task in `tasksList` and calls its `setTelemetryTypeChanged` method if it exists. --- After updating all tasks, invokes the `dashx.utils.session()` function. -function tasks.setTelemetryTypeChanged() - for _, task in ipairs(tasksList) do - if tasks[task.name].setTelemetryTypeChanged then - --dashx.utils.log("Notifying task [" .. task.name .. "] of telemetry type change", "info") - tasks[task.name].setTelemetryTypeChanged() - end - end - dashx.utils.session() -end + CPU_TICK_BUDGET = 1 / CPU_TICK_HZ + CPU_ALPHA = 0.2 + cpu_avg = 0 + last_wakeup_start = nil + + MEM_ALPHA = 0.2 + mem_avg_kb = nil + last_mem_t = 0 + MEM_PERIOD = 2.0 + + tasks.heartbeat = nil + tasks.wasOn = false + tasks._justInitialized = false + + tasksList = {} + tasks._initState = "start" + tasks._initMetadata = nil + tasks._initKeys = nil + tasks._initIndex = 1 + + tasks.begin = true + + tlm = system.getSource({category = CATEGORY_SYSTEM_EVENT, member = TELEMETRY_ACTIVE}) -function tasks.read() - -- print("onRead:") end -function tasks.write() - -- print("onWrite:") +function tasks.setTelemetryTypeChanged() + for _, task in ipairs(tasksList) do if tasks[task.name].setTelemetryTypeChanged then tasks[task.name].setTelemetryTypeChanged() end end + dashx.utils.session() end +function tasks.read() end + +function tasks.write() end + return tasks diff --git a/scripts/dashx/tasks/telemetry/init.lua b/scripts/dashx/tasks/telemetry/init.lua index 7e4cb99..db12470 100644 --- a/scripts/dashx/tasks/telemetry/init.lua +++ b/scripts/dashx/tasks/telemetry/init.lua @@ -1,27 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.5, -- run every 0.5 seconds - script = "telemetry.lua", -- run this script - linkrequired = false, -- run only if link is established - spreadschedule = true, -- run on every loop - simulatoronly = false, -- run in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.5, script = "telemetry.lua", linkrequired = false, spreadschedule = true, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/telemetry/telemetry.lua b/scripts/dashx/tasks/telemetry/telemetry.lua index 6ab9c9c..13809a5 100644 --- a/scripts/dashx/tasks/telemetry/telemetry.lua +++ b/scripts/dashx/tasks/telemetry/telemetry.lua @@ -1,161 +1,67 @@ -local dashx = require("dashx") ---[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- + +local dashx = require("dashx") + local arg = {...} local config = arg[1] local telemetry = {} local protocol, telemetrySOURCE, crsfSOURCE --- sensor cache: weak values so GC can drop cold sources -local sensors = setmetatable({}, { __mode = "v" }) +local sensors = setmetatable({}, {__mode = "v"}) --- debug counters local cache_hits, cache_misses = 0, 0 --- LRU for hot sources -local HOT_SIZE = 40 +local HOT_SIZE = 40 local hot_list, hot_index = {}, {} local function mark_hot(key) - local idx = hot_index[key] - if idx then - table.remove(hot_list, idx) - elseif #hot_list >= HOT_SIZE then - local old = table.remove(hot_list, 1) - hot_index[old] = nil - -- evict the old sensor so cache size ≤ HOT_SIZE - sensors[old] = nil - end - table.insert(hot_list, key) - hot_index[key] = #hot_list + local idx = hot_index[key] + if idx then + table.remove(hot_list, idx) + elseif #hot_list >= HOT_SIZE then + local old = table.remove(hot_list, 1) + hot_index[old] = nil + + sensors[old] = nil + end + table.insert(hot_list, key) + hot_index[key] = #hot_list end function telemetry._debugStats() - local hot_count = #hot_list - return { - hits = cache_hits, - misses = cache_misses, - hot_size = hot_count, - hot_list = hot_list, - } + local hot_count = #hot_list + return {hits = cache_hits, misses = cache_misses, hot_size = hot_count, hot_list = hot_list} end --- Rate‐limiting for wakeup() local sensorRateLimit = os.clock() -local ONCHANGE_RATE = 0.5 -- 1 second between onchange scans +local ONCHANGE_RATE = 0.5 --- Store the last validated sensors and timestamp local lastValidationResult = nil -local lastValidationTime = 0 -local VALIDATION_RATE_LIMIT = 2 -- seconds +local lastValidationTime = 0 +local VALIDATION_RATE_LIMIT = 2 -local lastCacheFlushTime = 0 -local CACHE_FLUSH_INTERVAL = 5 -- seconds +local lastCacheFlushTime = 0 +local CACHE_FLUSH_INTERVAL = 5 local telemetryState = false --- Store last seen values for each sensor (by key) local lastSensorValues = {} - telemetry.sensorStats = {} --- For “reduced table” of onchange‐capable sensors: local filteredOnchangeSensors = nil -local onchangeInitialized = false - --- Predefined sensor mappings ---[[ -sensorTable: A table containing various telemetry sensor configurations for different protocols (sport, crsf, crsf). - -Each sensor configuration includes: -- name: The name of the sensor. -- mandatory: A boolean indicating if the sensor is mandatory. -- sport: A table of sensor configurations for the sport protocol. -- crsf: A table of sensor configurations for the crsf protocol. -- crsf: A table of sensor configurations for the crsf protocol. -- stats: A function to determine if min/max tracking should be active. -- localizations: A function to transform the sensor value. - -Sensors included: -- RSSI Sensors (rssi) -- Arm Flags (armflags) -- Arm Disabled (arm_disabled) -- Voltage Sensors (voltage) -- RPM Sensors (rpm) -- Current Sensors (current) -- Temperature Sensors (temp_esc, temp_mcu) -- Fuel and Capacity Sensors (fuel, capacity) -- Flight Mode Sensors (governor) -- Adjustment Sensors (adj_f, adj_v) -- PID and Rate Profiles (pid_profile, rate_profile) -- Throttle Sensors (throttle_percent) - -Check this url for some useful ID numbers when associating these sensors to the correct telemetry sensors "set telemetry_sensors" -https://github.com/rotorflight/rotorflight-firmware/blob/c7cad2c86fd833fe4bce76728f4914602614058d/src/main/telemetry/sensors.h#L34C15-L34C24 -]]-- +local onchangeInitialized = false local sensorTable = { + rssi = {name = "@i18n(telemetry.sensors.rssi)@", mandatory = true, stats = true, switch_alerts = true, unit = UNIT_PERCENT, unit_string = "%", sensors = {sim = {{appId = 0xF010, subId = 0}}, sport = {{appId = 0xF010, subId = 0}}, crsf = {"Rx Quality"}}}, - -- RSSI Sensors - -- RSSI Sensors - rssi = { - name = "@i18n(telemetry.sensors.rssi)@", - mandatory = true, - stats = true, - switch_alerts = true, - unit = UNIT_PERCENT, - unit_string = "%", - sensors = { - sim = { - { appId = 0xF010, subId = 0 }, - }, - sport = { - { appId = 0xF010, subId = 0 }, - }, - crsf = { "Rx Quality" }, - }, - }, - - -- RSSI Sensors - link = { - name = "@i18n(telemetry.sensors.link)@", - mandatory = true, - stats = true, - switch_alerts = false, - unit = UNIT_DB, - unit_string = "dB", - sensors = { - sim = { - { appId = 0xF101, subId = 0 }, - }, - sport = { - { appId = 0xF101, subId = 0 }, - }, - crsf = { "Rx RSSI1" }, - }, - }, + link = {name = "@i18n(telemetry.sensors.link)@", mandatory = true, stats = true, switch_alerts = false, unit = UNIT_DB, unit_string = "dB", sensors = {sim = {{appId = 0xF101, subId = 0}}, sport = {{appId = 0xF101, subId = 0}}, crsf = {"Rx RSSI1"}}}, - - -- Voltage Sensors voltage = { name = "@i18n(telemetry.sensors.voltage)@", mandatory = true, @@ -165,22 +71,12 @@ local sensorTable = { unit = UNIT_VOLT, unit_string = "V", sensors = { - sim = { - { uid = 0x5002, unit = UNIT_VOLT, dec = 2, - value = function() return dashx.utils.simSensors('voltage') end, - min = 0, max = 3000 }, - }, - sport = { - { appId = 0x0B50, subId = 0 }, - { appId = 0x0210, subId = 0 }, - { appId = 0xF103, subId = 0 }, - { appId = 0xF103, subId = 1 }, - }, - crsf = { "Rx Batt" }, - }, + sim = {{uid = 0x5002, unit = UNIT_VOLT, dec = 2, value = function() return dashx.utils.simSensors('voltage') end, min = 0, max = 3000}}, + sport = {{appId = 0x0B50, subId = 0}, {appId = 0x0210, subId = 0}, {appId = 0xF103, subId = 0}, {appId = 0xF103, subId = 1}}, + crsf = {"Rx Batt"} + } }, - -- RPM Sensors rpm = { name = "@i18n(telemetry.sensors.headspeed)@", mandatory = true, @@ -189,17 +85,7 @@ local sensorTable = { switch_alerts = true, unit = UNIT_RPM, unit_string = "rpm", - sensors = { - sim = { - { uid = 0x5003, unit = UNIT_RPM, dec = nil, - value = function() return dashx.utils.simSensors('rpm') end, - min = 0, max = 4000 }, - }, - sport = { - { appId = 0x0B60, subId = 0 }, - { appId = 0x0500, subId = 0 }, - }, - }, + sensors = {sim = {{uid = 0x5003, unit = UNIT_RPM, dec = nil, value = function() return dashx.utils.simSensors('rpm') end, min = 0, max = 4000}}, sport = {{appId = 0x0B60, subId = 0}, {appId = 0x0500, subId = 0}}} }, fuel = { @@ -210,18 +96,9 @@ local sensorTable = { switch_alerts = true, unit = UNIT_PERCENT, unit_string = "%", - sensors = { - sim = { - { uid = 0x5007, unit = UNIT_PERCENT, dec = 0, - value = function() return dashx.utils.simSensors('fuel') end, - min = 0, max = 100 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0600 }, }, - crsf = { "Rx Batt%" }, - }, + sensors = {sim = {{uid = 0x5007, unit = UNIT_PERCENT, dec = 0, value = function() return dashx.utils.simSensors('fuel') end, min = 0, max = 100}}, sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0600}}, crsf = {"Rx Batt%"}} }, - -- Fuel and Capacity Sensors smartfuel = { name = "@i18n(telemetry.sensors.smartfuel)@", mandatory = false, @@ -230,17 +107,7 @@ local sensorTable = { switch_alerts = true, unit = UNIT_PERCENT, unit_string = "%", - sensors = { - sim = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1 }, - }, - crsf = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1 }, - }, - }, + sensors = {sim = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1}}, sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1}}, crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FE1}}} }, smartconsumption = { @@ -248,18 +115,11 @@ local sensorTable = { mandatory = false, stats = true, switch_alerts = true, - unit = UNIT_MILLIAMPERE_HOUR, unit_string = "mAh", - sensors = { sim = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE }, - }, - sport= { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE }, - }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE }, }, - }, + unit = UNIT_MILLIAMPERE_HOUR, + unit_string = "mAh", + sensors = {sim = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE}}, sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE}}, crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5FDE}}} }, - -- Current Sensors current = { name = "@i18n(telemetry.sensors.current)@", mandatory = false, @@ -268,21 +128,9 @@ local sensorTable = { switch_alerts = true, unit = UNIT_AMPERE, unit_string = "A", - sensors = { - sim = { - { uid = 0x5004, unit = UNIT_AMPERE, dec = 0, - value = function() return dashx.utils.simSensors('current') end, - min = 0, max = 300 }, - }, - sport = { - { appId = 0x0B50, subId = 1 }, - { appId = 0x0200, subId = 0 }, - }, - crsf = { "Rx Current" }, - }, + sensors = {sim = {{uid = 0x5004, unit = UNIT_AMPERE, dec = 0, value = function() return dashx.utils.simSensors('current') end, min = 0, max = 300}}, sport = {{appId = 0x0B50, subId = 1}, {appId = 0x0200, subId = 0}}, crsf = {"Rx Current"}} }, - -- ESC Temperature Sensors temp_esc = { name = "@i18n(telemetry.sensors.esc_temp)@", mandatory = false, @@ -290,32 +138,18 @@ local sensorTable = { set_telemetry_sensors = 23, switch_alerts = true, unit = UNIT_DEGREE, - sensors = { - sim = { - { uid = 0x5005, unit = UNIT_DEGREE, dec = 0, - value = function() return dashx.utils.simSensors('temp_esc') end, - min = 0, max = 100 }, - }, - sport = { - { appId = 0x0B70, subId = 0 }, - }, - }, + sensors = {sim = {{uid = 0x5005, unit = UNIT_DEGREE, dec = 0, value = function() return dashx.utils.simSensors('temp_esc') end, min = 0, max = 100}}, sport = {{appId = 0x0B70, subId = 0}}}, localizations = function(value) local major = UNIT_DEGREE if value == nil then return nil, major, nil end - -- Shortcut to the user’s temperature‐unit preference (may be nil) local prefs = dashx.preferences.localizations local isFahrenheit = prefs and prefs.temperature_unit == 1 - if isFahrenheit then - -- Convert from Celsius to Fahrenheit - return value * 1.8 + 32, major, "°F" - end + if isFahrenheit then return value * 1.8 + 32, major, "°F" end - -- Default: return Celsius return value, major, "°C" - end, + end }, altitude = { @@ -325,18 +159,10 @@ local sensorTable = { switch_alerts = true, unit = UNIT_METER, sensors = { - sim = { - { uid = 0x5016, unit = UNIT_METER, dec = 0, - value = function() return dashx.utils.simSensors('altitude') end, - min = 0, max = 50000 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0820 } , - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0100 } - - }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x10B2 }, }, - crsfLegacy = { nil }, + sim = {{uid = 0x5016, unit = UNIT_METER, dec = 0, value = function() return dashx.utils.simSensors('altitude') end, min = 0, max = 50000}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0820}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0100}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x10B2}}, + crsfLegacy = {nil} }, localizations = function(value) local major = UNIT_METER @@ -345,8 +171,8 @@ local sensorTable = { local isFeet = prefs and prefs.altitude_unit == 1 if isFeet then return value * 3.28084, major, "ft" end return value, major, "m" - end, - }, + end + }, consumption = { name = "@i18n(telemetry.sensors.consumption)@", @@ -356,21 +182,9 @@ local sensorTable = { switch_alerts = true, unit = UNIT_MILLIAMPERE_HOUR, unit_string = "mAh", - sensors = { - sim = { - { uid = 0x5008, unit = UNIT_MILLIAMPERE_HOUR, dec = 0, - value = function() return dashx.utils.simSensors('consumption') end, - min = 0, max = 5000 }, - }, - sport = { - { appId = 0x0B60, subId = 1 }, - { appId = 0x0B30, subId = 0 }, - }, - crsf = { "Rx Cons" }, - }, + sensors = {sim = {{uid = 0x5008, unit = UNIT_MILLIAMPERE_HOUR, dec = 0, value = function() return dashx.utils.simSensors('consumption') end, min = 0, max = 5000}}, sport = {{appId = 0x0B60, subId = 1}, {appId = 0x0B30, subId = 0}}, crsf = {"Rx Cons"}} }, - -- Arrmed Sensors armed = { name = "@i18n(telemetry.sensors.arming_flags)@", mandatory = false, @@ -379,20 +193,9 @@ local sensorTable = { switch_alerts = false, unit = UNIT_RAW, unit_string = nil, - sensors = { - sim = { - { appId = 0x5FE0, subId = 0 }, - }, - sport = { - { appId = 0x5FE0, subId = 0 }, - }, - crsf = { - { appId = 0x5FE0, subId = 0 }, - }, - }, + sensors = {sim = {{appId = 0x5FE0, subId = 0}}, sport = {{appId = 0x5FE0, subId = 0}}, crsf = {{appId = 0x5FE0, subId = 0}}} }, - -- Idleup Sensors inflight = { name = "@i18n(telemetry.sensors.inflight)@", mandatory = false, @@ -401,33 +204,19 @@ local sensorTable = { switch_alerts = false, unit = UNIT_RAW, unit_string = nil, - sensors = { - sim = { - { appId = 0x5FDF, subId = 0 }, - }, - sport = { - { appId = 0x5FDF, subId = 0 }, - }, - crsf = { - { appId = 0x5FDF, subId = 0 }, - }, - }, - }, + sensors = {sim = {{appId = 0x5FDF, subId = 0}}, sport = {{appId = 0x5FDF, subId = 0}}, crsf = {{appId = 0x5FDF, subId = 0}}} + }, - accx = { + accx = { name = "@i18n(sensors.accx)@", mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5019, unit = UNIT_G, dec = 3, - value = function() return dashx.utils.simSensors('accx') end, - min = -4000, max = 4000 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0700 }, }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1111 }, }, - crsfLegacy = { nil }, - }, + sim = {{uid = 0x5019, unit = UNIT_G, dec = 3, value = function() return dashx.utils.simSensors('accx') end, min = -4000, max = 4000}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0700}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1111}}, + crsfLegacy = {nil} + } }, bec_voltage = { @@ -439,21 +228,11 @@ local sensorTable = { unit = UNIT_VOLT, unit_string = "V", sensors = { - sim = { - { uid = 0x5017, unit = UNIT_VOLT, dec = 2, - value = function() return dashx.utils.simSensors('bec_voltage') end, - min = 0, max = 3000 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0901 }, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0219 }, - }, - crsf = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1081 }, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1049 }, - }, - crsfLegacy = { nil }, - }, + sim = {{uid = 0x5017, unit = UNIT_VOLT, dec = 2, value = function() return dashx.utils.simSensors('bec_voltage') end, min = 0, max = 3000}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0901}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0219}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1081}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1049}}, + crsfLegacy = {nil} + } }, accy = { @@ -461,15 +240,11 @@ local sensorTable = { mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5020, unit = UNIT_G, dec = 3, - value = function() return dashx.utils.simSensors('accy') end, -- fixed typo - min = -4000, max = 4000 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0710 }, }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1112 }, }, - crsfLegacy = { nil }, - }, + sim = {{uid = 0x5020, unit = UNIT_G, dec = 3, value = function() return dashx.utils.simSensors('accy') end, min = -4000, max = 4000}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0710}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1112}}, + crsfLegacy = {nil} + } }, cell_count = { @@ -477,50 +252,30 @@ local sensorTable = { mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5018, unit = nil, dec = 0, - value = function() return dashx.utils.simSensors('cell_count') end, - min = 0, max = 50 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5260 }, }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1020 }, }, - crsfLegacy = { nil }, - }, + sim = {{uid = 0x5018, unit = nil, dec = 0, value = function() return dashx.utils.simSensors('cell_count') end, min = 0, max = 50}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5260}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1020}}, + crsfLegacy = {nil} + } }, - accz = { name = "@i18n(sensors.accz)@", mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5021, unit = UNIT_G, dec = 3, - value = function() return dashx.utils.simSensors('accz') end, - min = -4000, max = 4000 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0720 }, }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1113 }, }, - crsfLegacy = { nil }, - }, + sim = {{uid = 0x5021, unit = UNIT_G, dec = 3, value = function() return dashx.utils.simSensors('accz') end, min = -4000, max = 4000}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0720}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1113}}, + crsfLegacy = {nil} + } }, attyaw = { name = "@i18n(sensors.attyaw)@", mandatory = false, stats = false, - sensors = { - sim = { - { uid = 0x5022, unit = UNIT_DEGREE, dec = 1, - value = function() return dashx.utils.simSensors('attyaw') end, - min = -1800, max = 3600 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5210 }, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0830 }, - }, - crsf = { "Yaw" }, - }, + sensors = {sim = {{uid = 0x5022, unit = UNIT_DEGREE, dec = 1, value = function() return dashx.utils.simSensors('attyaw') end, min = -1800, max = 3600}}, sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x5210}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0830}}, crsf = {"Yaw"}} }, attroll = { @@ -528,17 +283,10 @@ local sensorTable = { mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5023, unit = UNIT_DEGREE, dec = 1, - value = function() return dashx.utils.simSensors('attroll') end, - min = -1800, max = 3600 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730 , subId = 0}, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0440 , subId = 0}, - }, - crsf = { "Roll" }, - }, + sim = {{uid = 0x5023, unit = UNIT_DEGREE, dec = 1, value = function() return dashx.utils.simSensors('attroll') end, min = -1800, max = 3600}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730, subId = 0}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0440, subId = 0}}, + crsf = {"Roll"} + } }, attpitch = { @@ -546,148 +294,75 @@ local sensorTable = { mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5024, unit = UNIT_DEGREE, dec = 1, - value = function() return dashx.utils.simSensors('attpitch') end, - min = -1800, max = 3600 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730, subId = 1 }, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0430, subId = 0 }, - }, - crsf = { "Pitch" }, - }, - }, + sim = {{uid = 0x5024, unit = UNIT_DEGREE, dec = 1, value = function() return dashx.utils.simSensors('attpitch') end, min = -1800, max = 3600}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730, subId = 1}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0430, subId = 0}}, + crsf = {"Pitch"} + } + }, flightmode = { name = "@i18n(sensors.flightmode)@", mandatory = false, stats = false, - sensors = { - sim = { - { uid = 0x5024, unit = UNIT_DEGREE, dec = 1, - value = function() return dashx.utils.simSensors('flightmode') end, - min = -1800, max = 3600 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730, subId = 1 }, }, - crsf = { "Flight mode" }, - }, - }, + sensors = {sim = {{uid = 0x5024, unit = UNIT_DEGREE, dec = 1, value = function() return dashx.utils.simSensors('flightmode') end, min = -1800, max = 3600}}, sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0730, subId = 1}}, crsf = {"Flight mode"}} + }, groundspeed = { name = "@i18n(sensors.groundspeed)@", mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5025, unit = UNIT_KNOT, dec = 1, - value = function() return dashx.utils.simSensors('groundspeed') end, - min = -1800, max = 3600 }, - }, - sport = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0830, subId = 0 }, }, - crsf = { { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1128 }, }, - crsfLegacy = { nil }, - }, - }, + sim = {{uid = 0x5025, unit = UNIT_KNOT, dec = 1, value = function() return dashx.utils.simSensors('groundspeed') end, min = -1800, max = 3600}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0830, subId = 0}}, + crsf = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x1128}}, + crsfLegacy = {nil} + } + }, gps_sats = { name = "@i18n(sensors.gps_sats)@", mandatory = false, stats = false, sensors = { - sim = { - { uid = 0x5026, unit = UNIT_KNOT, dec = 0, - value = function() return dashx.utils.simSensors('gps_sats') end, - min = -1800, max = 3600 }, - }, - sport = { - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0480, subId = 0 }, - { category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0410, subId = 0 }, - }, - crsfLegacy = { "GPS Sats" }, - }, - }, - + sim = {{uid = 0x5026, unit = UNIT_KNOT, dec = 0, value = function() return dashx.utils.simSensors('gps_sats') end, min = -1800, max = 3600}}, + sport = {{category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0480, subId = 0}, {category = CATEGORY_TELEMETRY_SENSOR, appId = 0x0410, subId = 0}}, + crsfLegacy = {"GPS Sats"} + } + } - } ---[[ - Retrieves the current sensor protocol. - @return protocol - The protocol used by the sensor. -]] -function telemetry.getSensorProtocol() - return protocol -end +function telemetry.getSensorProtocol() return protocol end ---[[ - Function: telemetry.listSensors - Description: Generates a list of sensors from the sensorTable. - Returns: A table containing sensor details (key, name, and mandatory status). -]] function telemetry.listSensors() local sensorList = {} - for key, sensor in pairs(sensorTable) do - table.insert(sensorList, { - key = key, - name = sensor.name, - mandatory = sensor.mandatory, - set_telemetry_sensors = sensor.set_telemetry_sensors - }) - end + for key, sensor in pairs(sensorTable) do table.insert(sensorList, {key = key, name = sensor.name, mandatory = sensor.mandatory, set_telemetry_sensors = sensor.set_telemetry_sensors}) end return sensorList end ---[[ - Function: telemetry.listSensorAudioUnits - Returns a mapping of sensorKey → unit type, if defined. -]] function telemetry.listSensorAudioUnits() local sensorMap = {} - for key, sensor in pairs(sensorTable) do - if sensor.unit then - sensorMap[key] = sensor.unit - end - end + for key, sensor in pairs(sensorTable) do if sensor.unit then sensorMap[key] = sensor.unit end end return sensorMap end ---[[ - Function: telemetry.listSwitchSensors - Returns a list of sensors flagged for switch alerts. -]] function telemetry.listSwitchSensors() local sensorList = {} - for key, sensor in pairs(sensorTable) do - if sensor.switch_alerts then - table.insert(sensorList, { - key = key, - name = sensor.name, - mandatory = sensor.mandatory, - set_telemetry_sensors = sensor.set_telemetry_sensors - }) - end - end + for key, sensor in pairs(sensorTable) do if sensor.switch_alerts then table.insert(sensorList, {key = key, name = sensor.name, mandatory = sensor.mandatory, set_telemetry_sensors = sensor.set_telemetry_sensors}) end end return sensorList end ---[[ - Helper: Get the raw Source object for a given sensorKey, caching as we go. -]] function telemetry.getSensorSource(name) if not sensorTable[name] then return nil end - -- Return cached if available, bump it as hot: if sensors[name] then - cache_hits = cache_hits + 1 -- debug: we hit the cache :contentReference[oaicite:0]{index=0} + cache_hits = cache_hits + 1 mark_hot(name) return sensors[name] end local function checkCondition(sensorEntry) - if not (dashx.session and dashx.session.apiVersion) then - return true - end + if not (dashx.session and dashx.session.apiVersion) then return true end local roundedApiVersion = dashx.utils.round(dashx.session.apiVersion, 2) if sensorEntry.mspgt then return roundedApiVersion >= dashx.utils.round(sensorEntry.mspgt, 2) @@ -696,59 +371,59 @@ function telemetry.getSensorSource(name) end return true end - + if system.getVersion().simulation == true then protocol = "sport" for _, sensor in ipairs(sensorTable[name].sensors.sim or {}) do - -- handle sensors in regular formt + if sensor.uid then if sensor and type(sensor) == "table" then - local sensorQ = { appId = sensor.uid, category = CATEGORY_TELEMETRY_SENSOR } + local sensorQ = {appId = sensor.uid, category = CATEGORY_TELEMETRY_SENSOR} local source = system.getSource(sensorQ) if source then - cache_misses = cache_misses + 1 -- debug: loaded from system.getSource :contentReference[oaicite:1]{index=1} + cache_misses = cache_misses + 1 sensors[name] = source mark_hot(name) return source end end else - -- handle smart sensors / regular lookups + if checkCondition(sensor) and type(sensor) == "table" then sensor.mspgt = nil sensor.msplt = nil local source = system.getSource(sensor) if source then - cache_misses = cache_misses + 1 -- debug: loaded from system.getSource :contentReference[oaicite:1]{index=1} + cache_misses = cache_misses + 1 sensors[name] = source mark_hot(name) return source end - end - end + end + end end elseif dashx.session.telemetryType == "crsf" then - protocol = "crsf" - for _, sensor in ipairs(sensorTable[name].sensors.crsf or {}) do - local source = system.getSource(sensor) - if source then - cache_misses = cache_misses + 1 - sensors[name] = source - mark_hot(name) - return source - end + protocol = "crsf" + for _, sensor in ipairs(sensorTable[name].sensors.crsf or {}) do + local source = system.getSource(sensor) + if source then + cache_misses = cache_misses + 1 + sensors[name] = source + mark_hot(name) + return source end + end elseif dashx.session.telemetryType == "sport" then - protocol = "sport" - for _, sensor in ipairs(sensorTable[name].sensors.sport or {}) do - local source = system.getSource(sensor) - if source then - cache_misses = cache_misses + 1 -- debug: loaded from system.getSource :contentReference[oaicite:1]{index=1} - sensors[name] = source - mark_hot(name) - return source - end + protocol = "sport" + for _, sensor in ipairs(sensorTable[name].sensors.sport or {}) do + local source = system.getSource(sensor) + if source then + cache_misses = cache_misses + 1 + sensors[name] = source + mark_hot(name) + return source end + end else protocol = "unknown" end @@ -756,19 +431,6 @@ function telemetry.getSensorSource(name) return nil end ---- Retrieves the value of a telemetry sensor by its key. --- This function now supports both physical sensors (linked to telemetry sources) --- and virtual/computed sensors (which define a `.source` function in sensorTable). --- --- 1. If the sensorTable entry includes a `source` function (virtual/computed sensor), --- this function is called and its `.value()` result is returned. --- 2. Otherwise, attempts to resolve the sensor as a physical/real telemetry source. --- If found, returns its value; otherwise, returns nil. --- 3. If a `localizations` function is defined for the sensor, it is applied to --- transform the raw value and resolve units as needed. --- --- @param sensorKey The key identifying the telemetry sensor. --- @return The sensor value (possibly transformed), primary unit (major), and secondary unit (minor) if available. function telemetry.getSensor(sensorKey) local entry = sensorTable[sensorKey] @@ -777,57 +439,32 @@ function telemetry.getSensor(sensorKey) if src and type(src.value) == "function" then local value, major, minor = src.value() major = major or entry.unit - -- Optionally apply localization, if needed: - if entry.localizations and type(entry.localizations) == "function" then - value, major, minor = entry.localizations(value) - end + + if entry.localizations and type(entry.localizations) == "function" then value, major, minor = entry.localizations(value) end return value, major, minor end end - -- Physical/real telemetry source local source = telemetry.getSensorSource(sensorKey) - if not source then - return nil - end + if not source then return nil end - -- get initial defaults local value = source:value() local major = entry and entry.unit or nil local minor = nil - -- if the sensor has a transform function, apply it to the value: - if entry and entry.localizations and type(entry.localizations) == "function" then - value, major, minor = entry.localizations(value) - end + if entry and entry.localizations and type(entry.localizations) == "function" then value, major, minor = entry.localizations(value) end return value, major, minor end ---[[ - Function: telemetry.validateSensors - Purpose: Validates the sensors and returns a list of either valid or invalid sensors based on the input parameter. - Parameters: - returnValid (boolean) - If true, the function returns only valid sensors. If false, it returns only invalid sensors. - Returns: - table - A list of sensors with their keys and names. The list contains either valid or invalid sensors based on the returnValid parameter. - Notes: - - The function uses a rate limit to avoid frequent validations. - - If telemetry is not active, it returns all sensors. - - The function considers the mandatory flag for invalid sensors. -]] function telemetry.validateSensors(returnValid) local now = os.clock() - if (now - lastValidationTime) < VALIDATION_RATE_LIMIT then - return lastValidationResult - end + if (now - lastValidationTime) < VALIDATION_RATE_LIMIT then return lastValidationResult end lastValidationTime = now if not dashx.session.telemetryState then local allSensors = {} - for key, sensor in pairs(sensorTable) do - table.insert(allSensors, { key = key, name = sensor.name }) - end + for key, sensor in pairs(sensorTable) do table.insert(allSensors, {key = key, name = sensor.name}) end lastValidationResult = allSensors return allSensors end @@ -837,13 +474,9 @@ function telemetry.validateSensors(returnValid) local sensorSource = telemetry.getSensorSource(key) local isValid = (sensorSource ~= nil and sensorSource:state() ~= false) if returnValid then - if isValid then - table.insert(resultSensors, { key = key, name = sensor.name }) - end + if isValid then table.insert(resultSensors, {key = key, name = sensor.name}) end else - if not isValid and sensor.mandatory ~= false then - table.insert(resultSensors, { key = key, name = sensor.name }) - end + if not isValid and sensor.mandatory ~= false then table.insert(resultSensors, {key = key, name = sensor.name}) end end end @@ -851,87 +484,51 @@ function telemetry.validateSensors(returnValid) return resultSensors end ---[[ - Function: telemetry.simSensors - Description: Simulates sensors by iterating over a sensor table and returning a list of valid sensors. - Parameters: - returnValid (boolean) - A flag indicating whether to return valid sensors. - Returns: - result (table) - A table containing the names and first sport sensors of valid sensors. - - This function is used to build a list of sensors that are available in 'simulation mode' -]] function telemetry.simSensors(returnValid) local result = {} for key, sensor in pairs(sensorTable) do local name = sensor.name local firstSportSensor = sensor.sensors.sim and sensor.sensors.sim[1] - if firstSportSensor then - table.insert(result, { name = name, sensor = firstSportSensor }) - end + if firstSportSensor then table.insert(result, {name = name, sensor = firstSportSensor}) end end return result end ---[[ - Function: telemetry.active - Description: Checks if telemetry is active. Returns true if the system is in simulation mode, otherwise returns the state of telemetry. - Returns: - - boolean: true if in simulation mode or telemetry is active, false otherwise. -]] -function telemetry.active() - return dashx.session.telemetryState or false -end +function telemetry.active() return dashx.session.telemetryState or false end ---- Clears all cached sources and state. function telemetry.reset() telemetrySOURCE, crsfSOURCE, protocol = nil, nil, nil sensors = {} hot_list, hot_index = {}, {} - --telemetry.sensorStats = {} -- we defer this to onconnect - -- Also reset onchange tracking so we rebuild next time: + filteredOnchangeSensors = nil lastSensorValues = {} onchangeInitialized = false end ---[[ - Primary wakeup() loop: - - Prioritize MSP traffic - - Rate-limit onchange scanning (once per second) - - Periodic cache flush every 5s - - Reset telemetry if needed -]] function telemetry.wakeup() local now = os.clock() - -- Rate‐limited “onchange” scanning (every ONCHANGE_RATE seconds) if (now - sensorRateLimit) >= ONCHANGE_RATE then sensorRateLimit = now - -- Build reduced table of onchange‐capable sensors exactly once: if not filteredOnchangeSensors then filteredOnchangeSensors = {} - for sensorKey, sensorDef in pairs(sensorTable) do - if type(sensorDef.onchange) == "function" then - filteredOnchangeSensors[sensorKey] = sensorDef - end - end - -- Mark that we just built the reduced table; skip invoking onchange this pass + for sensorKey, sensorDef in pairs(sensorTable) do if type(sensorDef.onchange) == "function" then filteredOnchangeSensors[sensorKey] = sensorDef end end + onchangeInitialized = true end - -- If we just built the table on this pass, skip detection; next time, run normally if onchangeInitialized then onchangeInitialized = false else - -- Now iterate only over filteredOnchangeSensors + for sensorKey, sensorDef in pairs(filteredOnchangeSensors) do local source = telemetry.getSensorSource(sensorKey) if source and source:state() then local val = source:value() if lastSensorValues[sensorKey] ~= val then - -- Invoke onchange with the new value + sensorDef.onchange(val) lastSensorValues[sensorKey] = val end @@ -940,19 +537,11 @@ function telemetry.wakeup() end end - - -- Reset if telemetry is inactive or telemetry type changed - if not dashx.session.telemetryState or dashx.session.telemetryTypeChanged then - telemetry.reset() - end + if not dashx.session.telemetryState or dashx.session.telemetryTypeChanged then telemetry.reset() end end --- retrieve min/max values for a sensor -function telemetry.getSensorStats(sensorKey) - return telemetry.sensorStats[sensorKey] or { min = nil, max = nil } -end +function telemetry.getSensorStats(sensorKey) return telemetry.sensorStats[sensorKey] or {min = nil, max = nil} end --- allow sensor table to be accessed externally telemetry.sensorTable = sensorTable -return telemetry \ No newline at end of file +return telemetry diff --git a/scripts/dashx/tasks/timer/init.lua b/scripts/dashx/tasks/timer/init.lua index 37eca54..e72a285 100644 --- a/scripts/dashx/tasks/timer/init.lua +++ b/scripts/dashx/tasks/timer/init.lua @@ -1,27 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - interval = 0.025, -- run every 0.025 seconds - script = "timer.lua", -- run this script - linkrequired = true, -- run this script only if link is established - spreadschedule = false, -- run on every loop - simulatoronly = false, -- run this script in simulation mode -} + +local dashx = require("dashx") + +local init = {interval = 0.025, script = "timer.lua", linkrequired = true, spreadschedule = false, simulatoronly = false} return init diff --git a/scripts/dashx/tasks/timer/timer.lua b/scripts/dashx/tasks/timer/timer.lua index af386a4..26cc713 100644 --- a/scripts/dashx/tasks/timer/timer.lua +++ b/scripts/dashx/tasks/timer/timer.lua @@ -1,31 +1,16 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- - -local arg = { ... } + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") + +local arg = {...} local config = arg[1] local timer = {} local lastFlightMode = nil ---- Resets the flight timer session. --- Logs the reset action, clears the last flight mode, and initializes a new timer session. --- Sets the base lifetime from model preferences, resets session and lifetime counters, --- and marks the flight as not counted. function timer.reset() dashx.utils.log("Resetting flight timers", "info") lastFlightMode = nil @@ -34,27 +19,19 @@ function timer.reset() dashx.session.timer = timerSession dashx.session.flightCounted = false - timerSession.baseLifetime = tonumber( - dashx.ini.getvalue(dashx.session.modelPreferences, "general", "totalflighttime") - ) or 0 + timerSession.baseLifetime = tonumber(dashx.ini.getvalue(dashx.session.modelPreferences, "general", "totalflighttime")) or 0 timerSession.session = 0 timerSession.lifetime = timerSession.baseLifetime end ---- Saves the current flight timer values to the model preferences INI file. --- This function retrieves the model preferences and preferences file from the session. --- If the preferences file is not set, it logs a message and returns. --- Otherwise, it updates the "totalflighttime" and "lastflighttime" values in the "general" section --- of the preferences, then saves the updated preferences back to the INI file. --- Logs actions for debugging and information purposes. function timer.save() local prefs = dashx.session.modelPreferences local prefsFile = dashx.session.modelPreferencesFile if not prefsFile then dashx.utils.log("No model preferences file set, cannot save flight timers", "info") - return + return end dashx.utils.log("Saving flight timers to INI: " .. prefsFile, "info") @@ -63,15 +40,9 @@ function timer.save() dashx.ini.setvalue(prefs, "general", "totalflighttime", dashx.session.timer.baseLifetime or 0) dashx.ini.setvalue(prefs, "general", "lastflighttime", dashx.session.timer.session or 0) dashx.ini.save_ini_file(prefsFile, prefs) - end + end end ---- Finalizes the current flight segment by updating session and lifetime timers. --- Calculates the duration of the current segment, updates the session and lifetime --- timers accordingly, and saves the updated timer state. --- @param now number The current time (in seconds or milliseconds, depending on context). --- @usage --- finalizeFlightSegment(os.clock()) local function finalizeFlightSegment(now) local timerSession = dashx.session.timer local prefs = dashx.session.modelPreferences @@ -80,11 +51,7 @@ local function finalizeFlightSegment(now) timerSession.session = (timerSession.session or 0) + segment timerSession.start = nil - if timerSession.baseLifetime == nil then - timerSession.baseLifetime = tonumber( - dashx.ini.getvalue(prefs, "general", "totalflighttime") - ) or 0 - end + if timerSession.baseLifetime == nil then timerSession.baseLifetime = tonumber(dashx.ini.getvalue(prefs, "general", "totalflighttime")) or 0 end timerSession.baseLifetime = timerSession.baseLifetime + segment timerSession.lifetime = timerSession.baseLifetime @@ -92,27 +59,6 @@ local function finalizeFlightSegment(now) timer.save() end ---- Handles timer updates based on the current flight mode. --- --- This function should be called periodically to update the timer session state. --- It manages the start time, live session duration, and lifetime of the timer, --- and updates persistent model preferences such as total flight time and flight count. --- --- Behavior: --- - In "inflight" mode: --- - Initializes the timer start time if not already set. --- - Updates the live session time and total lifetime. --- - Persists the total flight time to model preferences. --- - Increments and saves the flight count after 25 seconds of flight if not already counted. --- - In other modes: --- - Resets the live session time to the last session value. --- - In "postflight" mode: --- - Finalizes the flight segment if a flight was started. --- --- Dependencies: --- - Relies on `dashx.session` for session state. --- - Uses `dashx.ini` for reading and writing model preferences. --- - Calls `finalizeFlightSegment(now)` when appropriate. function timer.wakeup() local now = os.time() local timerSession = dashx.session.timer @@ -122,9 +68,7 @@ function timer.wakeup() lastFlightMode = flightMode if flightMode == "inflight" then - if not timerSession.start then - timerSession.start = now - end + if not timerSession.start then timerSession.start = now end local currentSegment = now - timerSession.start timerSession.live = (timerSession.session or 0) + currentSegment @@ -132,9 +76,7 @@ function timer.wakeup() local computedLifetime = (timerSession.baseLifetime or 0) + currentSegment timerSession.lifetime = computedLifetime - if prefs then - dashx.ini.setvalue(prefs, "general", "totalflighttime", computedLifetime) - end + if prefs then dashx.ini.setvalue(prefs, "general", "totalflighttime", computedLifetime) end if timerSession.live >= 25 and not dashx.session.flightCounted then dashx.session.flightCounted = true @@ -150,9 +92,7 @@ function timer.wakeup() timerSession.live = timerSession.session or 0 end - if flightMode == "postflight" and timerSession.start then - finalizeFlightSegment(now) - end + if flightMode == "postflight" and timerSession.start then finalizeFlightSegment(now) end end return timer diff --git a/scripts/dashx/widgets/dashboard/dashboard.lua b/scripts/dashx/widgets/dashboard/dashboard.lua index 776e1a7..5f9ef9b 100644 --- a/scripts/dashx/widgets/dashboard/dashboard.lua +++ b/scripts/dashx/widgets/dashboard/dashboard.lua @@ -1,25 +1,12 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- --- Dashboard module table -local dashboard = {} -- main namespace for all dashboard functionality +local dashx = require("dashx") + +local dashboard = {} --- cache some functions and variables for performance local compile = loadfile local baseDir = dashx.config.baseDir @@ -30,28 +17,10 @@ local tasks = dashx.tasks local objectProfiler = false local mod +local supportedResolutions = {{784, 294}, {784, 316}, {800, 458}, {800, 480}, {472, 191}, {472, 210}, {480, 301}, {480, 320}, {630, 236}, {630, 258}, {640, 338}, {640, 360}} --- Supported resolutions -local supportedResolutions = { - { 784, 294 }, -- X20, X20RS etc - { 784, 316 }, -- X20, X20RS etc (no title) - { 800, 458 }, -- X20, X20RS etc (full screen) - { 800, 480 }, -- X20, X20RS etc (full screen / no title) - { 472, 191 }, -- TWXLITE, X18, X18S - { 472, 210 }, -- TWXLITE, X18, X18S (no title) - { 480, 301 }, -- TWXLITE, X18, X18S (full screen) - { 480, 320 }, -- TWXLITE, X18, X18S (full screen / no title) - { 630, 236 }, -- X14 - { 630, 258 }, -- X14 (no title) - { 640, 338 }, -- X14 (full screen) - { 640, 360 }, -- X14 (full screen / no title) -} - - --- Track the previous flight mode so we can detect changes on wakeup local lastFlightMode = nil --- Capture the script start time for uptime or performance measurements local initTime = os.clock() local lastWakeup = os.clock() @@ -59,29 +28,21 @@ local lastWakeup = os.clock() local isSliding = false local isSlidingStart = 0 --- Default theme to fall back on if user or system theme fails to load dashboard.DEFAULT_THEME = "system/default" --- Base paths for loading themes: --- themesBasePath: where system themes are stored --- themesUserPath: where user-defined themes are stored (preferences) local themesBasePath = "SCRIPTS:/" .. baseDir .. "/widgets/dashboard/themes/" local themesUserPath = "SCRIPTS:/" .. preferences .. "/dashboard/" --- Cache for loaded state modules (preflight, inflight, postflight) local loadedStateModules = {} --- Counter used by wakeup to cycle through tasks local wakeupScheduler = 0 --- Check model path aligns local lastModelPath = model.path() local lastModelPathCheckAt = 0 local PATH_CHECK_INTERVAL = 1.0 --- Spread scheduling of object wakeups to avoid doing them all at once: -local objectWakeupIndex = 1 -- current object index for wakeup -local objectWakeupsPerCycle = nil -- number of objects to wake per cycle (calculated later) +local objectWakeupIndex = 1 +local objectWakeupsPerCycle = nil local objectsThreadedWakeupCount = 0 local lastLoadedBoxCount = 0 local lastBoxRectsCount = 0 @@ -89,89 +50,62 @@ local lastLoadedBoxCount = 0 local lastBoxRectsCount = 0 local lastLoadedBoxSig = nil - --- Some placeholders used by dashboard loader local moduleState --- Track background loading of remaining flight mode modules local statePreloadQueue = {"inflight", "postflight"} local statePreloadIndex = 1 -local unsupportedResolution = false -- flag to track unsupported resolutions +local unsupportedResolution = false --- Track last known telemetry values for targeted invalidation dashboard._objectDirty = {} --- precompute indices of boxes whose object has its own `scheduler` field, --- so we can wake them every cycle without scanning all `boxRects`. local scheduledBoxIndices = {} --- Flag to perform initialization logic only once on first wakeup local firstWakeup = true local firstWakeupCustomTheme = true --- Layout state for boxes (UI elements): -dashboard.boxRects = {} -- will hold {x, y, w, h, box} for each box -dashboard.selectedBoxIndex = 1 -- tracks which box is currently selected (for input) +dashboard.boxRects = {} +dashboard.selectedBoxIndex = 1 --- Track whether a fallback theme was used, and when, per state: -dashboard.themeFallbackUsed = { preflight = false, inflight = false, postflight = false } -dashboard.themeFallbackTime = { preflight = 0, inflight = 0, postflight = 0 } +dashboard.themeFallbackUsed = {preflight = false, inflight = false, postflight = false} +dashboard.themeFallbackTime = {preflight = 0, inflight = 0, postflight = 0} --- Current flightmode driving which state module to use (preflight/inflight/postflight) dashboard.flightmode = dashx.flightmode.current or "preflight" --- Path to the current widget/theme in use (set during theme loading) dashboard.currentWidgetPath = nil --- Any overlay message to display on screen (e.g., error or status) dashboard.overlayMessage = nil --- Loaded dashboard objects organized by their "type" field dashboard.objectsByType = {} --- * CONFIGURABLE SIZES * --- Fraction of min(width, height) to use for the spinner/overlay radius. --- Increase to ~0.36 for a 20% larger spinner (0.3 * 1.2) -dashboard.loaderScale = 0.38 -dashboard.overlayScale = 0.38 +dashboard.loaderScale = 0.38 +dashboard.overlayScale = 0.38 --- dark mode state local darkModeState = lcd.darkMode() --- initialize cache once dashboard._moduleCache = dashboard._moduleCache or {} --- how many paint‐cycles to keep showing the spinner dashboard._hg_cycles_required = 2 dashboard._hg_cycles = 0 --- how long the loader must stay visible (in seconds) dashboard._loader_min_duration = 1.5 dashboard._loader_start_time = nil --- ===== Repaint governor ==== ------ -dashboard._minPaintInterval = 0.025 -- 25ms ≈ 40 FPS; tune between 0.033–0.1 +dashboard._minPaintInterval = 0.025 dashboard._lastInvalidateTime = 0 -dashboard._pendingInvalidates = {} -- queued rects to invalidate +dashboard._pendingInvalidates = {} local function _queueInvalidateRect(x, y, w, h) - local r = { x = x, y = y, w = w, h = h } - dashboard._pendingInvalidates[#dashboard._pendingInvalidates+1] = r + local r = {x = x, y = y, w = w, h = h} + dashboard._pendingInvalidates[#dashboard._pendingInvalidates + 1] = r end --- Very cheap “union or fallback” coalescing: local function _flushInvalidatesRespectingBudget() local now = os.clock() - if (now - dashboard._lastInvalidateTime) < dashboard._minPaintInterval then - return false -- skip this cycle; keep queue - end + if (now - dashboard._lastInvalidateTime) < dashboard._minPaintInterval then return false end - if #dashboard._pendingInvalidates == 0 then - return false - end + if #dashboard._pendingInvalidates == 0 then return false end - -- if many rects, full invalidate is cheaper if #dashboard._pendingInvalidates > 6 then lcd.invalidate() dashboard._pendingInvalidates = {} @@ -179,7 +113,6 @@ local function _flushInvalidatesRespectingBudget() return true end - -- simple union local x1, y1, x2, y2 = 1e9, 1e9, -1e9, -1e9 for _, r in ipairs(dashboard._pendingInvalidates) do if r.x < x1 then x1 = r.x end @@ -193,14 +126,7 @@ local function _flushInvalidatesRespectingBudget() return true end --- === Simple per-object *instance* profiler ================================ -dashboard.prof = dashboard.prof or { - enabled = true, -- master toggle - reportEvery = 2.0, -- seconds - lastReport = 0, - perId = {}, -- [id] = { type=..., paint=sec, wakeup=sec, pc=cnt, wc=cnt } - firstInventoryDone = false, -- to print the object list once -} +dashboard.prof = dashboard.prof or {enabled = true, reportEvery = 2.0, lastReport = 0, perId = {}, firstInventoryDone = false} local function _profStart() if not (dashboard.prof and dashboard.prof.enabled) then return 0 end @@ -212,7 +138,7 @@ local function _profStop(kind, id, typ, t0) local dt = os.clock() - t0 local rec = dashboard.prof.perId[id] if not rec then - rec = { type = typ, paint = 0, wakeup = 0, pc = 0, wc = 0 } + rec = {type = typ, paint = 0, wakeup = 0, pc = 0, wc = 0} dashboard.prof.perId[id] = rec end if kind == "paint" then @@ -226,7 +152,7 @@ end local function _profIdFromRect(rect) local b = rect.box - -- Include header flag + exact geometry so same type in different slots are distinct + local H = rect.isHeader and "H" or "B" return string.format("%s@%s:%d,%d,%dx%d", b.type or "?", H, rect.x, rect.y, rect.w, rect.h) end @@ -235,43 +161,39 @@ local function _profReportIfDue() local P = dashboard.prof if not (P and P.enabled) then return end local now = os.clock() - if P.lastReport == 0 then P.lastReport = now return end + if P.lastReport == 0 then + P.lastReport = now + return + end if (now - P.lastReport) < (P.reportEvery or 2.0) then return end - -- Build a sorted list by total time (paint+wakeup) this interval local rows, perTypeAgg = {}, {} for id, v in pairs(P.perId) do local tot = (v.paint + v.wakeup) - rows[#rows+1] = { id=id, type=v.type, paint=v.paint, wake=v.wakeup, pc=v.pc, wc=v.wc, tot=tot } + rows[#rows + 1] = {id = id, type = v.type, paint = v.paint, wake = v.wakeup, pc = v.pc, wc = v.wc, tot = tot} local T = v.type or "?" - local agg = perTypeAgg[T] or { paint=0, wake=0, pc=0, wc=0, tot=0 } - agg.paint, agg.wake, agg.pc, agg.wc, agg.tot = - agg.paint + v.paint, agg.wake + v.wakeup, agg.pc + v.pc, agg.wc + v.wc, agg.tot + tot + local agg = perTypeAgg[T] or {paint = 0, wake = 0, pc = 0, wc = 0, tot = 0} + agg.paint, agg.wake, agg.pc, agg.wc, agg.tot = agg.paint + v.paint, agg.wake + v.wakeup, agg.pc + v.pc, agg.wc + v.wc, agg.tot + tot perTypeAgg[T] = agg end - table.sort(rows, function(a,b) return a.tot > b.tot end) + table.sort(rows, function(a, b) return a.tot > b.tot end) log("--------------- OBJECT PROFILER (per instance) ---------------", "info") for _, r in ipairs(rows) do - local pms, wms = r.paint*1000, r.wake*1000 - local ap = r.pc>0 and (pms/r.pc) or 0 - local aw = r.wc>0 and (wms/r.wc) or 0 - log(string.format("[prof] %-40s | paint:%7.3fms (%4d, avg %6.3f) | wakeup:%7.3fms (%4d, avg %6.3f)", - r.id, pms, r.pc, ap, wms, r.wc, aw), "info") - -- reset this instance for next interval - local rec = P.perId[r.id]; rec.paint, rec.wakeup, rec.pc, rec.wc = 0,0,0,0 + local pms, wms = r.paint * 1000, r.wake * 1000 + local ap = r.pc > 0 and (pms / r.pc) or 0 + local aw = r.wc > 0 and (wms / r.wc) or 0 + log(string.format("[prof] %-40s | paint:%7.3fms (%4d, avg %6.3f) | wakeup:%7.3fms (%4d, avg %6.3f)", r.id, pms, r.pc, ap, wms, r.wc, aw), "info") + + local rec = P.perId[r.id]; + rec.paint, rec.wakeup, rec.pc, rec.wc = 0, 0, 0, 0 end log("-------------------- per-type summary ------------------------", "info") - for T, a in pairs(perTypeAgg) do - log(string.format("[sum ] %-18s | paint:%7.3fms | wakeup:%7.3fms | total:%7.3fms", - T, a.paint*1000, a.wake*1000, a.tot*1000), "info") - end + for T, a in pairs(perTypeAgg) do log(string.format("[sum ] %-18s | paint:%7.3fms | wakeup:%7.3fms | total:%7.3fms", T, a.paint * 1000, a.wake * 1000, a.tot * 1000), "info") end log("--------------------------------------------------------------", "info") P.lastReport = now end --- ======================================================================== - function dashboard.loader(x, y, w, h) dashboard.loaders.staticLoader(dashboard, x, y, w, h) @@ -282,34 +204,27 @@ end local function forceInvalidateAllObjects() for _, rect in ipairs(dashboard.boxRects) do local obj = dashboard.objectsByType[rect.box.type] - if obj and obj.dirty and obj.dirty(rect.box) then - _queueInvalidateRect(rect.x, rect.y, rect.w, rect.h) - end + if obj and obj.dirty and obj.dirty(rect.box) then _queueInvalidateRect(rect.x, rect.y, rect.w, rect.h) end end _flushInvalidatesRespectingBudget() end -function dashboard.overlaymessage(x, y, w, h, txt) - dashboard.loaders.staticOverlayMessage(dashboard, x, y, w, h, txt) -end +function dashboard.overlaymessage(x, y, w, h, txt) dashboard.loaders.staticOverlayMessage(dashboard, x, y, w, h, txt) end ---- Calculates the scheduler percentage based on the number of objects. --- This function determines what fraction of objects should be processed per cycle, --- depending on the total count. Fewer objects result in a higher percentage processed --- per cycle, while more objects reduce the percentage to avoid overloading. --- @param count number The total number of objects to schedule. --- @return number The percentage (as a decimal) of objects to process per cycle. local function computeObjectSchedulerPercentage(count) - if count <= 10 then return 0.8 -- fewer objects → more per cycle - elseif count <= 15 then return 0.7 - elseif count <= 25 then return 0.6 - elseif count <= 40 then return 0.5 - else return 0.4 end -- many objects → fewer per cycle + if count <= 10 then + return 0.8 + elseif count <= 15 then + return 0.7 + elseif count <= 25 then + return 0.6 + elseif count <= 40 then + return 0.5 + else + return 0.4 + end end ---- Loads a single dashboard object type (box) if not already loaded. --- Used during wakeup preload to load one box at a time. --- @param box A box configuration table with a `type` field. function dashboard.loadObjectType(box) local typ = box and box.type if not typ then return end @@ -319,97 +234,62 @@ function dashboard.loadObjectType(box) local bdir = baseDir or "default" local objPath = "SCRIPTS:/" .. bdir .. "/widgets/dashboard/objects/" .. typ .. ".lua" - local ok, obj = pcall(function() - return assert(compile(objPath))() - end) + local ok, obj = pcall(function() return assert(compile(objPath))() end) if ok and type(obj) == "table" then dashboard._moduleCache[typ] = obj else log("Failed to load object: " .. tostring(typ), "info") - print("Error detail: "..tostring(obj)) + print("Error detail: " .. tostring(obj)) dashboard._moduleCache[typ] = false end end - if dashboard._moduleCache[typ] then - dashboard.objectsByType[typ] = dashboard._moduleCache[typ] - end + if dashboard._moduleCache[typ] then dashboard.objectsByType[typ] = dashboard._moduleCache[typ] end end ---- Loads and caches dashboard object modules based on the provided box configurations. --- Iterates through each box config, loading the corresponding object Lua file only once per type. --- Loaded objects are stored in `dashboard.objectsByType` for later use. --- Logs a message if an object fails to load. --- @param boxConfigs Table of box configuration tables, each containing a `type` field. function dashboard.loadAllObjects(boxConfigs) - dashboard.objectsByType = {} -- clear old cache of active objects - - + dashboard.objectsByType = {} for _, box in ipairs(boxConfigs or {}) do local typ = box.type if typ then - -- only load from disk the first time we see this type + if not dashboard._moduleCache[typ] then local bdir = baseDir or "default" local objPath = "SCRIPTS:/" .. bdir .. "/widgets/dashboard/objects/" .. typ .. ".lua" - - local ok, obj = pcall(function() - return assert(compile(objPath))() - end) + local ok, obj = pcall(function() return assert(compile(objPath))() end) if ok and type(obj) == "table" then dashboard._moduleCache[typ] = obj else log("Failed to load object: " .. tostring(typ), "info") - print("Error detail: "..tostring(obj)) - -- ensure we don’t retry a broken type endlessly + print("Error detail: " .. tostring(obj)) + dashboard._moduleCache[typ] = false end end - -- if we have a valid cached module, assign it - if dashboard._moduleCache[typ] then - dashboard.objectsByType[typ] = dashboard._moduleCache[typ] - end + if dashboard._moduleCache[typ] then dashboard.objectsByType[typ] = dashboard._moduleCache[typ] end end end end ---- Returns a table of indices for boxes in `dashboard.boxRects` that have an `onpress` handler. --- @return table Indices of boxes with an `onpress` function. local function getOnpressBoxIndices() local indices = {} - for i, rect in ipairs(dashboard.boxRects) do - if rect.box.onpress then - indices[#indices + 1] = i - end - end + for i, rect in ipairs(dashboard.boxRects) do if rect.box.onpress then indices[#indices + 1] = i end end return indices end ---- Computes and returns an overlay message for the dashboard widget based on the current system state. --- The message indicates issues such as theme load errors, incompatible Ethos version, inactive background tasks, --- disabled RF modules, missing sensors, or invalid telemetry sensors. Returns `nil` if no issues are detected. --- @return string|nil Overlay message if an issue is detected, otherwise `nil`. function dashboard.computeOverlayMessage() local state = dashboard.flightmode or "preflight" local telemetry = tasks.telemetry - local pad = " " -- for RF version banner + local pad = " " - -- 1) Theme load error (recent only) - if dashboard.themeFallbackUsed and dashboard.themeFallbackUsed[state] and - (os.clock() - (dashboard.themeFallbackTime and dashboard.themeFallbackTime[state] or 0)) < 10 then - return "@i18n(widgets.dashboard.theme_load_error)@" - end + if dashboard.themeFallbackUsed and dashboard.themeFallbackUsed[state] and (os.clock() - (dashboard.themeFallbackTime and dashboard.themeFallbackTime[state] or 0)) < 10 then return "@i18n(widgets.dashboard.theme_load_error)@" end + + if not tasks.active() then return "@i18n(widgets.dashboard.check_bg_task)@" end - -- 2) Background task - if not tasks.active() then - return "@i18n(widgets.dashboard.check_bg_task)@" - end - - -- 3) As soon as we know RF version, show it with precedence if dashx.session.apiVersion and dashx.session.rfVersion and not dashx.session.isConnectedLow and state ~= "postflight" then if system.getVersion().simulation == true then return pad .. "SIM " .. dashx.session.apiVersion .. pad @@ -418,25 +298,12 @@ function dashboard.computeOverlayMessage() end end - -- 4) LAST: generic waiting message (don’t let it mask actionable errors) - if not dashx.session.isConnectedHigh and state ~= "postflight" then - return "@i18n(widgets.dashboard.waiting_for_connection)@" - end + if not dashx.session.isConnectedHigh and state ~= "postflight" then return "@i18n(widgets.dashboard.waiting_for_connection)@" end return nil end - ---- Calculates the width and height of a box based on its properties. --- Supports percentage-based, fixed, and grid-span sizing. --- @param box Table containing box properties (w_pct, h_pct, w, h, colspan, rowspan) --- @param boxWidth Default width for a single box/grid cell --- @param boxHeight Default height for a single box/grid cell --- @param PADDING Padding between boxes/cells --- @param WIDGET_W Total widget width (for percentage calculations) --- @param WIDGET_H Total widget height (for percentage calculations) --- @return w, h Calculated width and height of the box -local function getBoxSize(box,boxWidth, boxHeight, PADDING, WIDGET_W, WIDGET_H) +local function getBoxSize(box, boxWidth, boxHeight, PADDING, WIDGET_W, WIDGET_H) if box.w_pct and box.h_pct then local wp = box.w_pct local hp = box.h_pct @@ -456,25 +323,14 @@ local function getBoxSize(box,boxWidth, boxHeight, PADDING, WIDGET_W, WIDGET_H) end end ---- Calculates the (x, y) position for a UI box based on its configuration. --- Priority for position: percentage (x_pct/y_pct) > absolute (x/y) > grid (col/row). --- @param box Table containing box position properties. --- @param w Optional width override for the box. --- @param h Optional height override for the box. --- @param boxWidth Default width of the box. --- @param boxHeight Default height of the box. --- @param PADDING Padding between boxes. --- @param WIDGET_W Total widget width. --- @param WIDGET_H Total widget height. --- @return x, y Calculated top-left position of the box. local function getBoxPosition(box, w, h, boxWidth, boxHeight, PADDING, WIDGET_W, WIDGET_H) - -- Priority: x_pct/y_pct > x/y > col/row + if box.x_pct and box.y_pct then local xp = box.x_pct local yp = box.y_pct if xp > 1 then xp = xp / 100 end if yp > 1 then yp = yp / 100 end - -- x/y is top-left corner; w/h is known + local x = math.floor(xp * (WIDGET_W - (w or boxWidth))) local y = math.floor(yp * (WIDGET_H - (h or boxHeight))) return x, y @@ -494,23 +350,20 @@ local function getBoxPosition(box, w, h, boxWidth, boxHeight, PADDING, WIDGET_W, end function dashboard.renderLayout(widget, config) - local utils = dashboard.utils + local utils = dashboard.utils local telemetry = tasks.telemetry - -- create once dashboard.boxRects = dashboard.boxRects or {} scheduledBoxIndices = scheduledBoxIndices or {} - dashboard._objectDirty = dashboard._objectDirty or {} + dashboard._objectDirty = dashboard._objectDirty or {} local function resolve(val, ...) return type(val) == "function" and val(...) or val end - -- Load layout and box definitions - local layout = resolve(config.layout) or {} + local layout = resolve(config.layout) or {} local headerLayout = resolve(config.header_layout) or {} - local boxes = resolve(config.boxes or layout.boxes or {}) + local boxes = resolve(config.boxes or layout.boxes or {}) local headerBoxes = resolve(config.header_boxes or {}) - -- Reload widgets if layout changed if (#boxes + #headerBoxes) ~= lastLoadedBoxCount then local allBoxes = {} for _, b in ipairs(boxes) do table.insert(allBoxes, b) end @@ -519,84 +372,67 @@ function dashboard.renderLayout(widget, config) lastLoadedBoxCount = #boxes + #headerBoxes end - -- Build a stable signature of the box "types" present (order-insensitive) local function makeBoxesSig(bx, hbx) local t = {} - for _, b in ipairs(bx or {}) do t[#t+1] = tostring(b.type or "") end - for _, b in ipairs(hbx or {}) do t[#t+1] = tostring(b.type or "") end + for _, b in ipairs(bx or {}) do t[#t + 1] = tostring(b.type or "") end + for _, b in ipairs(hbx or {}) do t[#t + 1] = tostring(b.type or "") end table.sort(t) return table.concat(t, "|") end local thisSig = makeBoxesSig(boxes, headerBoxes) - -- Reload widgets if layout changed (count OR types) if ((#boxes + #headerBoxes) ~= lastLoadedBoxCount) or (thisSig ~= lastLoadedBoxSig) then local allBoxes = {} - for _, b in ipairs(boxes) do allBoxes[#allBoxes+1] = b end - for _, b in ipairs(headerBoxes) do allBoxes[#allBoxes+1] = b end + for _, b in ipairs(boxes) do allBoxes[#allBoxes + 1] = b end + for _, b in ipairs(headerBoxes) do allBoxes[#allBoxes + 1] = b end dashboard.loadAllObjects(allBoxes) lastLoadedBoxCount = #boxes + #headerBoxes - lastLoadedBoxSig = thisSig -- remember the types we loaded + lastLoadedBoxSig = thisSig end + for k in pairs(dashboard._objectDirty) do dashboard._objectDirty[k] = nil end - for k in pairs(dashboard._objectDirty) do dashboard._objectDirty[k] = nil end - - -- Grid and screen setup local W_raw, H_raw = lcd.getWindowSize() local isFullScreen = utils.isFullScreen(W_raw, H_raw) - local cols = layout.cols or 1 - local rows = layout.rows or 1 - local pad = layout.padding or 0 + local cols = layout.cols or 1 + local rows = layout.rows or 1 + local pad = layout.padding or 0 - local function adjustDimension(dim, cells, padCount) - return dim - ((dim - padCount*pad) % cells) - end + local function adjustDimension(dim, cells, padCount) return dim - ((dim - padCount * pad) % cells) end - -- Adjust height for header if specified - if isFullScreen and headerLayout and headerLayout.height and type(headerLayout.height) == "number" then - H_raw = H_raw - headerLayout.height - end + if isFullScreen and headerLayout and headerLayout.height and type(headerLayout.height) == "number" then H_raw = H_raw - headerLayout.height end local W = adjustDimension(W_raw, cols, cols - 1) - local H = adjustDimension(H_raw, rows, rows + 1) -- +1 for vertical pad + local H = adjustDimension(H_raw, rows, rows + 1) local xOffset = math.floor((W_raw - W) / 2) local contentW = W - ((cols - 1) * pad) local contentH = H - ((rows + 1) * pad) - local boxW = contentW / cols - local boxH = contentH / rows + local boxW = contentW / cols + local boxH = contentH / rows - ---------------------------------------------------------------- - -- PHASE 1: Build Box Rects and Collect Scheduled Indices - ---------------------------------------------------------------- utils.setBackgroundColourBasedOnTheme() - for i=#dashboard.boxRects,1,-1 do dashboard.boxRects[i] = nil end - for i=#scheduledBoxIndices,1,-1 do scheduledBoxIndices[i] = nil end + for i = #dashboard.boxRects, 1, -1 do dashboard.boxRects[i] = nil end + for i = #scheduledBoxIndices, 1, -1 do scheduledBoxIndices[i] = nil end for _, box in ipairs(boxes) do local w, h = getBoxSize(box, boxW, boxH, pad, W, H) box.xOffset = xOffset local x, y = getBoxPosition(box, w, h, boxW, boxH, pad, W, H) - if isFullScreen and headerLayout and headerLayout.height and type(headerLayout.height) == "number" then - y = y + headerLayout.height -- Adjust y position for header - end + if isFullScreen and headerLayout and headerLayout.height and type(headerLayout.height) == "number" then y = y + headerLayout.height end - local rect = { x = x, y = y, w = w, h = h, box = box, isHeader = false } + local rect = {x = x, y = y, w = w, h = h, box = box, isHeader = false} table.insert(dashboard.boxRects, rect) local rectIndex = #dashboard.boxRects dashboard._objectDirty[rectIndex] = nil local obj = dashboard.objectsByType[box.type] - if obj and obj.scheduler and obj.wakeup then - table.insert(scheduledBoxIndices, rectIndex) - end + if obj and obj.scheduler and obj.wakeup then table.insert(scheduledBoxIndices, rectIndex) end end - -- now do the same for headerBoxes so they get scheduled and invalidated just like normal boxes if isFullScreen then local headerGeoms = {} local rightmost_idx, rightmost_x = 1, 0 @@ -610,29 +446,23 @@ function dashboard.renderLayout(widget, config) end end - -- Now insert header rects, stretching the rightmost box for idx, geom in ipairs(headerGeoms) do local w = geom.w - if idx == rightmost_idx then - w = W_raw - geom.x - end + if idx == rightmost_idx then w = W_raw - geom.x end - local rect = { x = geom.x, y = geom.y, w = w, h = geom.h, box = geom.box, isHeader = true } + local rect = {x = geom.x, y = geom.y, w = w, h = geom.h, box = geom.box, isHeader = true} table.insert(dashboard.boxRects, rect) local idx_rect = #dashboard.boxRects dashboard._objectDirty[idx_rect] = nil local obj = dashboard.objectsByType[geom.box.type] - if obj and obj.scheduler and obj.wakeup then - table.insert(scheduledBoxIndices, idx_rect) - end + if obj and obj.scheduler and obj.wakeup then table.insert(scheduledBoxIndices, idx_rect) end end end - -- Scheduler setup if not objectWakeupsPerCycle or #dashboard.boxRects ~= lastBoxRectsCount then local count = #dashboard.boxRects - local percentage = 1.0 --dashboard._spreadRatioOverride or computeObjectSchedulerPercentage(count) + local percentage = 1.0 if objectsThreadedWakeupCount < 1 then percentage = 1.0 @@ -640,15 +470,11 @@ function dashboard.renderLayout(widget, config) end objectWakeupsPerCycle = math.max(1, math.ceil(count * percentage)) - lastBoxRectsCount = count + lastBoxRectsCount = count - log("Object scheduler set to " .. objectWakeupsPerCycle .. - " out of " .. count .. " boxes", "info") + log("Object scheduler set to " .. objectWakeupsPerCycle .. " out of " .. count .. " boxes", "info") end - ---------------------------------------------------------------- - -- PHASE 2: Spinner Until First Wakeup Pass Completes - ---------------------------------------------------------------- dashboard._loader_start_time = dashboard._loader_start_time or os.clock() local loaderElapsed = os.clock() - dashboard._loader_start_time if objectsThreadedWakeupCount < 1 or loaderElapsed < dashboard._loader_min_duration then @@ -659,10 +485,7 @@ function dashboard.renderLayout(widget, config) return end - ---------------------------------------------------------------- - -- PHASE 3: Paint Actual Widgets - ---------------------------------------------------------------- - local selColor = layout.selectcolor or utils.resolveColor("yellow") or lcd.RGB(255,255,0) + local selColor = layout.selectcolor or utils.resolveColor("yellow") or lcd.RGB(255, 255, 0) local selBorder = layout.selectborder or 2 for i, rect in ipairs(dashboard.boxRects) do @@ -676,7 +499,7 @@ function dashboard.renderLayout(widget, config) obj.paint(rect.x, rect.y, rect.w, rect.h, box) _profStop("paint", id, box.type, t0) else - obj.paint(rect.x, rect.y, rect.w, rect.h, box) + obj.paint(rect.x, rect.y, rect.w, rect.h, box) end end @@ -687,33 +510,25 @@ function dashboard.renderLayout(widget, config) end end - - - ------------------------------------------------------------------------ - -- PHASE 4: Draw Header - if applicable - ------------------------------------------------------------------------ if isFullScreen and config.header_layout and #headerBoxes > 0 then local header = config.header_layout local h_cols = header.cols or 1 local h_rows = header.rows or 1 - local h_pad = header.padding or 0 + local h_pad = header.padding or 0 local headerW = W_raw local headerH = header.height or 0 - local function adjustHeaderDimension(dim, cells, padCount) - return dim - ((dim - padCount * h_pad) % cells) - end + local function adjustHeaderDimension(dim, cells, padCount) return dim - ((dim - padCount * h_pad) % cells) end local adjustedW = adjustHeaderDimension(headerW, h_cols, h_cols - 1) local adjustedH = adjustHeaderDimension(headerH, h_rows, h_rows - 1) local contentW = adjustedW - ((h_cols - 1) * h_pad) local contentH = adjustedH - ((h_rows - 1) * h_pad) - local h_boxW = contentW / h_cols - local h_boxH = contentH / h_rows + local h_boxW = contentW / h_cols + local h_boxH = contentH / h_rows - -- Build box geoms and find rightmost local rightmost_idx, rightmost_x = 1, 0 local headerGeoms = {} for idx, box in ipairs(headerBoxes) do @@ -726,28 +541,23 @@ function dashboard.renderLayout(widget, config) end end - -- Paint all boxes, stretch rightmost to edge for idx, geom in ipairs(headerGeoms) do local w = geom.w - if idx == rightmost_idx then - w = W_raw - geom.x - end + if idx == rightmost_idx then w = W_raw - geom.x end local obj = dashboard.objectsByType[geom.box.type] if obj and obj.paint then if objectProfiler then - local fakeRect = { x = geom.x, y = geom.y, w = w, h = geom.h, box = geom.box, isHeader = true } + local fakeRect = {x = geom.x, y = geom.y, w = w, h = geom.h, box = geom.box, isHeader = true} local id = _profIdFromRect(fakeRect) local t0 = _profStart() obj.paint(geom.x, geom.y, w, geom.h, geom.box) - _profStop("paint", id, geom.box.type, t0) -- <-- was box.type before + _profStop("paint", id, geom.box.type, t0) else obj.paint(geom.x, geom.y, w, geom.h, geom.box) end end end - - -- Optional: Draw header grid if header_layout.showgrid is set if isFullScreen and headerLayout and headerLayout.showgrid then lcd.color(headerLayout.showgrid) lcd.pen(1) @@ -765,24 +575,19 @@ function dashboard.renderLayout(widget, config) lcd.pen(SOLID) end - end - - -- Draw optional grid overlay if layout.showgrid or dashx.preferences.developer.overlaygrid then lcd.color(layout.showgrid) lcd.pen(1) local headerOffset = (isFullScreen and headerLayout and headerLayout.height) or 0 - -- Vertical lines for i = 1, cols - 1 do local x = math.floor(i * (boxW + pad)) + xOffset - math.floor(pad / 2) lcd.drawLine(x, headerOffset, x, H_raw + headerOffset) end - -- Horizontal lines for i = 1, rows - 1 do local y = math.floor(i * (boxH + pad)) + pad + headerOffset lcd.drawLine(0, y, W_raw, y) @@ -791,78 +596,47 @@ function dashboard.renderLayout(widget, config) lcd.pen(SOLID) end - -- Optional: Overlay cpu/ram stats if layout.showstats is set if layout.showstats or dashx.preferences.developer.overlaystats then local headerOffset = (isFullScreen and headerLayout and headerLayout.height) or 0 local cpuUsage = (dashx.performance and dashx.performance.cpuload) or 0 - local loopMs = (dashx.performance and dashx.performance.loop_ms) or 0 - local budgetMs = (dashx.performance and dashx.performance.budget_ms) or 50 -- 20 Hz -> 50ms - local tickMs = (dashx.performance and dashx.performance.tick_ms) -- optional, if you published it + local loopMs = (dashx.performance and dashx.performance.loop_ms) or 0 + local budgetMs = (dashx.performance and dashx.performance.budget_ms) or 50 + local tickMs = (dashx.performance and dashx.performance.tick_ms) local headroomPct = math.max(0, 100 - (cpuUsage or 0)) - local ramFreeKB = (dashx.performance and dashx.performance.luaRamKB) or 0 - local ramUsedGC_KB = (dashx.performance and dashx.performance.usedram) or 0 - local sysRamFreeKB = (dashx.performance and dashx.performance.ramKB) or 0 - local bitmapRamFreeKB = (dashx.performance and dashx.performance.luaBitmapsRamKB) or 0 - local mainStackKB = (dashx.performance and dashx.performance.mainStackKB) or 0 + local ramFreeKB = (dashx.performance and dashx.performance.luaRamKB) or 0 + local ramUsedGC_KB = (dashx.performance and dashx.performance.usedram) or 0 + local sysRamFreeKB = (dashx.performance and dashx.performance.ramKB) or 0 + local bitmapRamFreeKB = (dashx.performance and dashx.performance.luaBitmapsRamKB) or 0 + local mainStackKB = (dashx.performance and dashx.performance.mainStackKB) or 0 - -- fonts lcd.font(FONT_S) local _, lineH = lcd.getTextSize("A") - local cfg = { - padX = 8, padY = 6, - colGap = 10, rowGap = 2, - labelW = 170, valueW = 120, unitW = 30, - sectionGap = 8, -- extra gap before each section - decimalsMS = 1, decimalsKB = 1, - boxX = 4, boxY = 4 + headerOffset, - bg = {0,0,0,0.9}, fg = {255,255,255}, - border = true, - showActualPeriod = true, -- set false if you didn’t publish tick_ms - } - + local cfg = {padX = 8, padY = 6, colGap = 10, rowGap = 2, labelW = 170, valueW = 120, unitW = 30, sectionGap = 8, decimalsMS = 1, decimalsKB = 1, boxX = 4, boxY = 4 + headerOffset, bg = {0, 0, 0, 0.9}, fg = {255, 255, 255}, border = true, showActualPeriod = true} local function fmtPct(n) return dashx.utils.round(n or 0, 0) end - local function fmtMS(n) return string.format("%."..cfg.decimalsMS.."f", n or 0) end - local function fmtKB(n) return string.format("%."..cfg.decimalsKB.."f", n or 0) end - - -- Build rows for each section: {label, value, unit} - local schedRows = { - { "LOAD", fmtPct(cpuUsage), "%" }, - { "LOAD (100ms window)", fmtPct(dashx.performance.cpuload_window100 or 0), "%" }, - { "HEADROOM", fmtPct(headroomPct), "%" }, - { "LOOP / BUDGET", fmtMS(loopMs).." / "..fmtMS(budgetMs), "ms" }, - } - if cfg.showActualPeriod and tickMs then - table.insert(schedRows, { "ACTUAL PERIOD", fmtMS(tickMs), "ms" }) - end + local function fmtMS(n) return string.format("%." .. cfg.decimalsMS .. "f", n or 0) end + local function fmtKB(n) return string.format("%." .. cfg.decimalsKB .. "f", n or 0) end + + local schedRows = {{"LOAD", fmtPct(cpuUsage), "%"}, {"LOAD (100ms window)", fmtPct(dashx.performance.cpuload_window100 or 0), "%"}, {"HEADROOM", fmtPct(headroomPct), "%"}, {"LOOP / BUDGET", fmtMS(loopMs) .. " / " .. fmtMS(budgetMs), "ms"}} + if cfg.showActualPeriod and tickMs then table.insert(schedRows, {"ACTUAL PERIOD", fmtMS(tickMs), "ms"}) end - local memRows = { - { "LUA RAM FREE", fmtKB(ramFreeKB), "KB" }, - { "LUA RAM USED (GC)", fmtKB(ramUsedGC_KB), "KB" }, - { "SYSTEM RAM FREE", fmtKB(sysRamFreeKB), "KB" }, - { "LUA BITMAP RAM", fmtKB(bitmapRamFreeKB), "KB" }, - } + local memRows = {{"LUA RAM FREE", fmtKB(ramFreeKB), "KB"}, {"LUA RAM USED (GC)", fmtKB(ramUsedGC_KB), "KB"}, {"SYSTEM RAM FREE", fmtKB(sysRamFreeKB), "KB"}, {"LUA BITMAP RAM", fmtKB(bitmapRamFreeKB), "KB"}} + + local boxW = cfg.padX * 2 + cfg.labelW + cfg.colGap + cfg.valueW + cfg.colGap + cfg.unitW - -- Measure widths/heights - local boxW = cfg.padX*2 + cfg.labelW + cfg.colGap + cfg.valueW + cfg.colGap + cfg.unitW - -- header rows add an extra line height local sectionHeaderH = lineH local totalRows = #schedRows + #memRows - local boxH = cfg.padY*2 - + sectionHeaderH + (#schedRows * (lineH + cfg.rowGap)) + cfg.sectionGap - + sectionHeaderH + (#memRows * (lineH + cfg.rowGap)) + local boxH = cfg.padY * 2 + sectionHeaderH + (#schedRows * (lineH + cfg.rowGap)) + cfg.sectionGap + sectionHeaderH + (#memRows * (lineH + cfg.rowGap)) - -- Center the box local screenW, screenH = lcd.getWindowSize() local boxX = math.floor((screenW - boxW) / 2) local boxY = math.floor((screenH - boxH) / 2) local minY = 4 + headerOffset if boxY < minY then boxY = minY end - -- Background + border lcd.color(lcd.RGB(cfg.bg[1], cfg.bg[2], cfg.bg[3], cfg.bg[4])) lcd.drawFilledRectangle(boxX, boxY, boxW, boxH) if cfg.border then @@ -872,33 +646,29 @@ function dashboard.renderLayout(widget, config) lcd.pen(0) end - -- Column anchors local labelX = boxX + cfg.padX local valueX = labelX + cfg.labelW + cfg.colGap - local unitX = valueX + cfg.valueW + cfg.colGap + local unitX = valueX + cfg.valueW + cfg.colGap local y = boxY + cfg.padY - -- Draw a section (title + rows) local function drawSection(title, rows) - -- title + lcd.color(lcd.RGB(cfg.fg[1], cfg.fg[2], cfg.fg[3])) lcd.font(FONT_S_BOLD) lcd.drawText(labelX, y, title) lcd.font(FONT_S) y = y + sectionHeaderH + cfg.rowGap - -- rows for i = 1, #rows do local label, value, unit = rows[i][1], rows[i][2], rows[i][3] lcd.drawText(labelX, y, label) - -- right align value within valueW + local tw = lcd.getTextSize(tostring(value)) lcd.drawText(valueX + cfg.valueW - tw, y, tostring(value)) lcd.drawText(unitX, y, tostring(unit)) y = y + lineH + cfg.rowGap end - -- gap before next section y = y + cfg.sectionGap end @@ -906,12 +676,7 @@ function dashboard.renderLayout(widget, config) drawSection("MEMORY", memRows) end - - - -- Handle overlay messages - if dashboard.overlayMessage then - dashboard._hg_cycles = dashboard._hg_cycles_required - end + if dashboard.overlayMessage then dashboard._hg_cycles = dashboard._hg_cycles_required end if dashboard._hg_cycles > 0 then local loaderY = (isFullScreen and headerLayout.height) or 0 dashboard.overlaymessage(0, loaderY, W, H - loaderY, dashboard.overlayMessage) @@ -924,8 +689,6 @@ function dashboard.renderLayout(widget, config) dashboard._forceFullRepaint = true end - --- Utility to resolve a theme for a given flight mode local function getThemeForState(state) local prefs = dashx.session.modelPreferences and dashx.session.modelPreferences.dashboard local fallback = dashx.preferences.dashboard @@ -933,103 +696,66 @@ local function getThemeForState(state) return (val and val ~= "nil" and val) or fallback["theme_" .. state] or dashboard.DEFAULT_THEME end - ---- Loads a state-specific script for the dashboard widget, handling theme selection and fallbacks. --- --- This function attempts to load the `init.lua` file from the specified `theme_folder` to determine --- the script to use for the given `state`. If loading fails at any point, it falls back to the default theme. --- It also updates fallback tracking and the current widget path. --- --- @param theme_folder (string) The theme folder in the format "source/folder" (e.g., "user/mytheme"). --- @param state (string) The dashboard state for which to load the script (e.g., "main", "settings"). --- @return (function|table|nil) Returns the loaded script chunk or module table, or nil if loading fails. --- --- Side effects: --- - Updates `dashboard.themeFallbackUsed[state]` and `dashboard.themeFallbackTime[state]` on fallback. --- - Sets `dashboard.currentWidgetPath` to the active theme path. --- --- Logging: --- - Logs info and error messages if loading or execution fails. local function load_state_script(theme_folder, state, isFallback) isFallback = isFallback or false local src, folder = theme_folder:match("([^/]+)/(.+)") local base = (src == "user") and themesUserPath or themesBasePath - -- if parsing failed, try default if not src or not folder then - if not isFallback then - return load_state_script(dashboard.DEFAULT_THEME, state, true) - end - -- default is broken too + if not isFallback then return load_state_script(dashboard.DEFAULT_THEME, state, true) end + dashboard.themeFallbackUsed[state] = true dashboard.themeFallbackTime[state] = os.clock() return nil end - -- helper to set the current widget path - local function setPath() dashboard.currentWidgetPath = src.."/"..folder end + local function setPath() dashboard.currentWidgetPath = src .. "/" .. folder end - -- 1) load init.lua - local initPath = base..folder.."/init.lua" + local initPath = base .. folder .. "/init.lua" local initChunk, initErr = compile(initPath) if not initChunk then - if not isFallback then - return load_state_script(dashboard.DEFAULT_THEME, state, true) - end + if not isFallback then return load_state_script(dashboard.DEFAULT_THEME, state, true) end dashboard.themeFallbackUsed[state] = true dashboard.themeFallbackTime[state] = os.clock() return nil end - -- run init.lua local ok, initTable = pcall(initChunk) if not ok or type(initTable) ~= "table" then - print("Error running init.lua for theme="..tostring(theme_folder)..", state="..tostring(state)) - print("Error detail: "..tostring(initTable)) - if not isFallback then - return load_state_script(dashboard.DEFAULT_THEME, state, true) - end + print("Error running init.lua for theme=" .. tostring(theme_folder) .. ", state=" .. tostring(state)) + print("Error detail: " .. tostring(initTable)) + if not isFallback then return load_state_script(dashboard.DEFAULT_THEME, state, true) end dashboard.themeFallbackUsed[state] = true dashboard.themeFallbackTime[state] = os.clock() return nil end - -- decide which state file to load - local scriptName = (type(initTable[state])=="string" and initTable[state]~="") - and initTable[state] - or (state..".lua") - local scriptPath = base..folder.."/"..scriptName + local scriptName = (type(initTable[state]) == "string" and initTable[state] ~= "") and initTable[state] or (state .. ".lua") + local scriptPath = base .. folder .. "/" .. scriptName - -- 2) load the actual state script (or fallback to default) local chunk, chunkErr = compile(scriptPath) if not chunk then - if not isFallback then - return load_state_script(dashboard.DEFAULT_THEME, state, true) - end - -- even default missing? give up - log("dashboard: Could not load "..scriptName.." for "..folder.." or default: "..tostring(chunkErr), "info") + if not isFallback then return load_state_script(dashboard.DEFAULT_THEME, state, true) end + + log("dashboard: Could not load " .. scriptName .. " for " .. folder .. " or default: " .. tostring(chunkErr), "info") dashboard.themeFallbackUsed[state] = true dashboard.themeFallbackTime[state] = os.clock() return nil end - -- at this point, we successfully have a chunk; mark no fallback dashboard.themeFallbackUsed[state] = (isFallback == true) dashboard.themeFallbackTime[state] = isFallback and os.clock() or 0 setPath() - -- if standalone, return the chunk itself; otherwise run it and return module if initTable.standalone then return chunk else local ok2, module = pcall(chunk) if not ok2 then - print("Error running init.lua for theme="..tostring(theme_folder)..", state="..tostring(state)) - print("Error detail: "..tostring(module)) - if not isFallback then - return load_state_script(dashboard.DEFAULT_THEME, state, true) - end + print("Error running init.lua for theme=" .. tostring(theme_folder) .. ", state=" .. tostring(state)) + print("Error detail: " .. tostring(module)) + if not isFallback then return load_state_script(dashboard.DEFAULT_THEME, state, true) end dashboard.themeFallbackUsed[state] = true dashboard.themeFallbackTime[state] = os.clock() return nil @@ -1038,7 +764,6 @@ local function load_state_script(theme_folder, state, isFallback) end end --- Utility to get the correct theme for a given state local function getThemeForState(state) local modelPrefs = dashx.session.modelPreferences and dashx.session.modelPreferences.dashboard local userPrefs = dashx.preferences.dashboard @@ -1050,7 +775,6 @@ local function getThemeForState(state) return val or userPrefs["theme_" .. state] or dashboard.DEFAULT_THEME end --- Reload just the active state script local function reload_state_only(state) dashboard.utils.resetImageCache() loadedStateModules[state] = load_state_script(getThemeForState(state), state) @@ -1061,13 +785,10 @@ local function reload_state_only(state) objectWakeupsPerCycle = nil lastLoadedBoxSig = nil dashboard.boxRects = {} - if dashboard.boxRects then - for k in pairs(dashboard.boxRects) do dashboard.boxRects[k] = nil end - end + if dashboard.boxRects then for k in pairs(dashboard.boxRects) do dashboard.boxRects[k] = nil end end lcd.invalidate() end - function dashboard.reload_active_theme_only(force) dashboard.utils.resetImageCache() @@ -1080,21 +801,19 @@ function dashboard.reload_active_theme_only(force) else log("Skipped reloading active theme: already loaded", "info") end - + firstWakeup = true - lcd.invalidate() -- Triggers paint, which shows the loader - - -- Reset scheduler & layout state so the hourglass & wakeup cycle restart cleanly - wakeupScheduler = 0 - dashboard.boxRects = {} - objectsThreadedWakeupCount = 0 - objectWakeupIndex = 1 - lastLoadedBoxCount = 0 - lastBoxRectsCount = 0 - objectWakeupsPerCycle = nil + lcd.invalidate() + + wakeupScheduler = 0 + dashboard.boxRects = {} + objectsThreadedWakeupCount = 0 + objectWakeupIndex = 1 + lastLoadedBoxCount = 0 + lastBoxRectsCount = 0 + objectWakeupsPerCycle = nil lastLoadedBoxSig = nil - -- Force spinner draw lcd.invalidate() end @@ -1109,10 +828,7 @@ function dashboard.applySchedulerSettings() dashboard._useSpreadScheduling = (initTable.spread_scheduling ~= false) dashboard._useSpreadSchedulingPaint = (initTable.spread_scheduling_paint ~= false) - -- NEW: optionally override spread ratio - dashboard._spreadRatioOverride = (type(initTable.spread_ratio) == "number" and initTable.spread_ratio > 0 and initTable.spread_ratio <= 1) - and initTable.spread_ratio - or nil + dashboard._spreadRatioOverride = (type(initTable.spread_ratio) == "number" and initTable.spread_ratio > 0 and initTable.spread_ratio <= 1) and initTable.spread_ratio or nil else dashboard._useSpreadScheduling = true dashboard._useSpreadSchedulingPaint = true @@ -1128,77 +844,50 @@ end function dashboard.reload_themes(force) - -- Clear cached subtype renderers (e.g. time/flight/telemetry modules) dashboard.renders = {} - -- Step 1: Load just the active theme and reset core state dashboard.reload_active_theme_only(force) - -- Step 2: Reset state preload index (wake-up loop will handle it) - statePreloadIndex = 1 -- start loading immediately + statePreloadIndex = 1 - -- Step 3: Apply scheduler settings dashboard.applySchedulerSettings() - -- Step 4: Load object types for active module only local boxes = {} if mod and mod.boxes then local rawBoxes = type(mod.boxes) == "function" and mod.boxes() or mod.boxes - for _, box in ipairs(rawBoxes or {}) do - table.insert(boxes, box) - end + for _, box in ipairs(rawBoxes or {}) do table.insert(boxes, box) end end dashboard.loadAllObjects(boxes) - - -- Force full redraw from top firstWakeup = true dashboard._loader_start_time = nil dashboard._hg_cycles = dashboard._hg_cycles_required - -- Reset rendering state explicitly dashboard._forceFullRepaint = true - if dashboard.boxRects then - for k in pairs(dashboard.boxRects) do dashboard.boxRects[k] = nil end - end + if dashboard.boxRects then for k in pairs(dashboard.boxRects) do dashboard.boxRects[k] = nil end end lastBoxRectsCount = 0 lastLoadedBoxCount = 0 objectWakeupIndex = 1 objectWakeupsPerCycle = nil objectsThreadedWakeupCount = 0 - -- force module.layout to be rendered local mod = loadedStateModules[dashboard.flightmode or "preflight"] if type(mod) == "table" and mod.layout and mod.boxes then log("Manually triggering renderLayout after theme reload", "info") dashboard.renderLayout(nil, mod) end - end - - ---- Calls a state-specific function for the dashboard widget, handling fallbacks and errors. --- @param funcName string: The name of the function to call (e.g., "paint"). --- @param widget table: The widget instance to pass to the state function. --- @param paintFallback boolean: If true, displays an error if the function is not implemented for the current state. --- @return any: The result of the called state function, the module (for "paint" layout), or nil if not applicable. local function callStateFunc(funcName, widget, paintFallback) local state = dashboard.flightmode or "preflight" local module = loadedStateModules[state] - if not tasks.active() then - return nil - end + if not tasks.active() then return nil end - if type(module) == "table" and module.layout and funcName == "paint" then - return module -- Let `dashboard.paint()` handle rendering - end + if type(module) == "table" and module.layout and funcName == "paint" then return module end - if module and type(module[funcName]) == "function" then - return module[funcName](widget) - end + if module and type(module[funcName]) == "function" then return module[funcName](widget) end if paintFallback then local msg = "dashboard: " .. funcName .. " not implemented for " .. state .. "." @@ -1206,54 +895,35 @@ local function callStateFunc(funcName, widget, paintFallback) end end ---- Creates a dashboard widget by invoking the "create" state function. --- @param widget The widget instance to be created. --- @return The result of the "create" state function for the given widget. function dashboard.create() - -- 1) one-time (per Lua VM) helper modules; don’t recompile per instance - if not dashboard.utils then - dashboard.utils = assert(compile("SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/lib/utils.lua"))() - end - if not dashboard.loaders then - dashboard.loaders = assert(compile("SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/lib/loaders.lua"))() - end - -- 2) ensure user theme dir exists + if not dashboard.utils then dashboard.utils = assert(compile("SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/lib/utils.lua"))() end + if not dashboard.loaders then dashboard.loaders = assert(compile("SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/lib/loaders.lua"))() end + os.mkdir("SCRIPTS:/" .. dashx.config.preferences .. "/dashboard/") - -- 3) reset per-instance runtime flags/state dashboard._pendingInvalidates = {} dashboard._lastInvalidateTime = 0 - dashboard._hg_cycles = 0 - dashboard.overlayMessage = nil - - -- per-instance scheduling - firstWakeup = true - firstWakeupCustomTheme = true - wakeupScheduler = 0 - objectWakeupIndex = 1 + dashboard._hg_cycles = 0 + dashboard.overlayMessage = nil + + firstWakeup = true + firstWakeupCustomTheme = true + wakeupScheduler = 0 + objectWakeupIndex = 1 objectsThreadedWakeupCount = 0 - objectWakeupsPerCycle = nil - scheduledBoxIndices = {} - dashboard.boxRects = {} + objectWakeupsPerCycle = nil + scheduledBoxIndices = {} + dashboard.boxRects = {} dashboard.selectedBoxIndex = nil - -- kick a first frame if you want the hourglass immediately lcd.invalidate() - -- return widget instance table (Ethos will pass it back to wakeup/paint/…) - return { value = 0 } + return {value = 0} end ---- Paints the dashboard widget based on the current flight mode state. --- Determines the current state and retrieves the corresponding module from `loadedStateModules`. --- If the module is valid and contains `layout` and `boxes`, it renders the layout and calls the module's custom paint function if available. --- Otherwise, it falls back to calling a generic state paint function. --- @param widget The widget object to be painted. function dashboard.paint(widget) - -- we expect the isCompiledCheck to be replaced at build time with "true" - -- if this has not happened; abort as they clearly have a non-release build local isCompiledCheck = "@i18n(iscompiledcheck)@" if isCompiledCheck ~= "true" then dashboard.utils.screenError("i18n not compiled - download a release version", true, 0.6) @@ -1261,27 +931,24 @@ function dashboard.paint(widget) end if unsupportedResolution then - -- If the resolution is unsupported, show an error message and return + local W, H = lcd.getWindowSize() - if H < (system.getVersion().lcdHeight/5) or W < (system.getVersion().lcdWidth/10) then - dashboard.utils.screenError("@i18n(widgets.dashboard.unsupported_resolution)@", true, 0.4) + if H < (system.getVersion().lcdHeight / 5) or W < (system.getVersion().lcdWidth / 10) then + dashboard.utils.screenError("@i18n(widgets.dashboard.unsupported_resolution)@", true, 0.4) else - dashboard.overlaymessage(0, 0, W, H , "@i18n(widgets.dashboard.unsupported_resolution)@") - end + dashboard.overlaymessage(0, 0, W, H, "@i18n(widgets.dashboard.unsupported_resolution)@") + end return end - - -- on the *first* paint, immediately draw the spinner and bail out if firstWakeup then local W, H = lcd.getWindowSize() local loaderY = (isFullScreen and headerLayout.height) or 0 dashboard.loader(0, loaderY, W, H - loaderY) - lcd.invalidate() -- Ensures repaint while theme loads + lcd.invalidate() return end - -- we must reset if model changes if os.clock() - lastModelPathCheckAt >= PATH_CHECK_INTERVAL then local newModelPath = model.path() if newModelPath ~= lastModelPath then @@ -1291,74 +958,32 @@ function dashboard.paint(widget) local W, H = lcd.getWindowSize() local loaderY = (isFullScreen and headerLayout.height) or 0 dashboard.loader(0, loaderY, W, H - loaderY) - lcd.invalidate() -- Ensures repaint while theme loads + lcd.invalidate() return end - end + end local state = dashboard.flightmode or "preflight" local module = loadedStateModules[state] if type(module) == "table" and module.layout and module.boxes then dashboard.renderLayout(widget, module) - if type(module.paint) == "function" then - module.paint(widget, module.layout, module.boxes) - end + if type(module.paint) == "function" then module.paint(widget, module.layout, module.boxes) end else callStateFunc("paint", widget) end - if objectProfiler then - _profReportIfDue() - end + if objectProfiler then _profReportIfDue() end end ---- Configures the given dashboard widget by invoking the "configure" state function. --- If the state function does not return a value, the original widget is returned. --- @param widget table: The widget instance to configure. --- @return table: The configured widget, or the original widget if no configuration was applied. -function dashboard.configure(widget) - return callStateFunc("configure", widget) or widget -end +function dashboard.configure(widget) return callStateFunc("configure", widget) or widget end ---- Reads data from the given dashboard widget by invoking the appropriate state function. --- @param widget The widget instance to read data from. --- @return The result of the state function call for reading the widget. -function dashboard.read(widget) - return callStateFunc("read", widget) -end +function dashboard.read(widget) return callStateFunc("read", widget) end ---- Writes data to the specified widget by invoking the appropriate state function. --- @param widget The widget object to write data to. --- @return The result of the state function call for writing. -function dashboard.write(widget) - return callStateFunc("write", widget) -end +function dashboard.write(widget) return callStateFunc("write", widget) end ---- Builds the dashboard widget by invoking the appropriate state function. --- @param widget The widget instance to be built. --- @return The result of the state function call for building the widget. -function dashboard.build(widget) - return callStateFunc("build", widget) -end +function dashboard.build(widget) return callStateFunc("build", widget) end ---- Handles events for the dashboard widget, including key presses, rotary encoder, and touch events. --- --- @param widget The widget instance receiving the event. --- @param category The event category (e.g., EVT_KEY for key events, 1 for touch). --- @param value The event value (e.g., key code, touch code). --- @param x (optional) The x-coordinate for touch events. --- @param y (optional) The y-coordinate for touch events. --- --- Handles the following: --- - State transitions between "preflight" and "postflight" modes. --- - Navigation between selectable boxes using rotary encoder or keys. --- - Selection and activation of boxes via key or touch events. --- - Delegates event handling to the current state module if available. --- - Clears selection on EXIT key. --- - Ensures focus and valid indices before processing events. --- --- @return true if the event was handled, otherwise delegates to the state module or returns nil. function dashboard.event(widget, category, value, x, y) local state = dashboard.flightmode or "preflight" @@ -1369,10 +994,9 @@ function dashboard.event(widget, category, value, x, y) dashboard.resetFlightModeAsk() end - -- Touch and hold - if category == 1 and value == TOUCH_MOVE then - isSliding = true - isSlidingStart = os.clock() + if category == 1 and value == TOUCH_MOVE then + isSliding = true + isSlidingStart = os.clock() end if category == EVT_KEY and lcd.hasFocus() then @@ -1383,25 +1007,31 @@ function dashboard.event(widget, category, value, x, y) local current = dashboard.selectedBoxIndex or 1 local pos = 1 for i, idx in ipairs(indices) do - if idx == current then pos = i break end + if idx == current then + pos = i + break + end end - if value == 4099 then -- rotary left + if value == 4099 then pos = pos - 1 if pos < 1 then pos = count end dashboard.selectedBoxIndex = indices[pos] lcd.invalidate(widget) return true - elseif value == 4100 then -- rotary right + elseif value == 4100 then pos = pos + 1 if pos > count then pos = 1 end dashboard.selectedBoxIndex = indices[pos] lcd.invalidate(widget) - return true + return true elseif value == 33 and category == EVT_KEY then local inIndices = false for i = 1, #indices do - if indices[i] == dashboard.selectedBoxIndex then inIndices = true break end + if indices[i] == dashboard.selectedBoxIndex then + inIndices = true + break + end end if not inIndices then dashboard.selectedBoxIndex = indices[1] @@ -1418,13 +1048,13 @@ function dashboard.event(widget, category, value, x, y) end end end - if value == 35 and dashboard.selectedBoxIndex then -- EXIT key + if value == 35 and dashboard.selectedBoxIndex then dashboard.selectedBoxIndex = nil lcd.invalidate(widget) return true end - if category == 1 and value == 16641 and lcd.hasFocus() then -- touch + if category == 1 and value == 16641 and lcd.hasFocus() then if x and y then for i, rect in ipairs(dashboard.boxRects) do if x >= rect.x and x < rect.x + rect.w and y >= rect.y and y < rect.y + rect.h then @@ -1440,41 +1070,28 @@ function dashboard.event(widget, category, value, x, y) end end - if type(module) == "table" and type(module.event) == "function" then - return module.event(widget, category, value, x, y) - end + if type(module) == "table" and type(module.event) == "function" then return module.event(widget, category, value, x, y) end end ---- Handles the periodic wakeup logic for the dashboard widget. --- --- This function is called regularly by the Ethos system to update the dashboard's state. --- It manages theme reloading on first wakeup, interval-based updates depending on widget visibility, --- flight mode changes, overlay message updates, state-specific wakeup logic, and per-object wakeups. --- It also handles focus removal from selected boxes when the widget loses focus. --- --- @param widget The widget instance to update. function dashboard.wakeup(widget) - -- Check if MSP is allow msp to be prioritized if dashx.session and dashx.session.mspBusy and not (dashx.session and dashx.session.isConnected) then return end - -- Quick exit if not visible or running admin app local now = os.clock() local visible = lcd.isVisible() - local admin = dashx.app and dashx.app.guiIsRunning + local admin = dashx.app and dashx.app.guiIsRunning - -- Throttle CPU usage based on connection and visibility if admin or not visible then - -- not visible or in admin - return + + return elseif isSliding then - -- check if sliding timeout expired + if (now - isSlidingStart) > 1 then isSliding = false else return - end + end end objectProfiler = dashx.preferences and dashx.preferences.developer and dashx.preferences.developer.logobjprof @@ -1482,21 +1099,19 @@ function dashboard.wakeup(widget) local telemetry = tasks.telemetry local W, H = lcd.getWindowSize() - -- cache last window size + support result to avoid rework every wakeup - dashboard._lastWH = dashboard._lastWH or { w = nil, h = nil, supported = nil } + dashboard._lastWH = dashboard._lastWH or {w = nil, h = nil, supported = nil} if W ~= dashboard._lastWH.w or H ~= dashboard._lastWH.h then local supported = dashboard.utils.supportedResolution(W, H, supportedResolutions) if supported ~= dashboard._lastWH.supported then unsupportedResolution = not supported dashboard._lastWH.supported = supported - -- Only invalidate on a state flip (supported <-> unsupported) + lcd.invalidate(widget) end dashboard._lastWH.w, dashboard._lastWH.h = W, H end - -- Early out if currently unsupported (no need to keep invalidating) if unsupportedResolution then return end if lcd.darkMode() ~= darkModeState then @@ -1522,25 +1137,19 @@ function dashboard.wakeup(widget) local mod = loadedStateModules[state] if mod and mod.boxes then local boxes = type(mod.boxes) == "function" and mod.boxes() or mod.boxes - for _, box in ipairs(boxes or {}) do - dashboard.loadObjectType(box) - end + for _, box in ipairs(boxes or {}) do dashboard.loadObjectType(box) end end end statePreloadIndex = statePreloadIndex + 1 end - if firstWakeupCustomTheme and - dashx.session.mcu_id and - dashx.session.modelPreferences and - dashx.session.modelPreferences.dashboard then + if firstWakeupCustomTheme and dashx.session.mcu_id and dashx.session.modelPreferences and dashx.session.modelPreferences.dashboard then local modelPrefs = dashx.session.modelPreferences.dashboard local currentPrefs = dashx.preferences.dashboard - if (modelPrefs.theme_preflight and modelPrefs.theme_preflight ~= "nil" and modelPrefs.theme_preflight ~= currentPrefs.theme_preflight) or - (modelPrefs.theme_inflight and modelPrefs.theme_inflight ~= "nil" and modelPrefs.theme_inflight ~= currentPrefs.theme_inflight) or - (modelPrefs.theme_postflight and modelPrefs.theme_postflight ~= "nil" and modelPrefs.theme_postflight ~= currentPrefs.theme_postflight) then + if (modelPrefs.theme_preflight and modelPrefs.theme_preflight ~= "nil" and modelPrefs.theme_preflight ~= currentPrefs.theme_preflight) or (modelPrefs.theme_inflight and modelPrefs.theme_inflight ~= "nil" and modelPrefs.theme_inflight ~= currentPrefs.theme_inflight) or + (modelPrefs.theme_postflight and modelPrefs.theme_postflight ~= "nil" and modelPrefs.theme_postflight ~= currentPrefs.theme_postflight) then dashboard.reload_themes() firstWakeupCustomTheme = false end @@ -1551,9 +1160,7 @@ function dashboard.wakeup(widget) dashboard.flightmode = currentFlightMode reload_state_only(currentFlightMode) lastFlightMode = currentFlightMode - if dashboard._useSpreadSchedulingPaint then - lcd.invalidate(widget) - end + if dashboard._useSpreadSchedulingPaint then lcd.invalidate(widget) end end local newMessage = dashboard.computeOverlayMessage() @@ -1573,7 +1180,7 @@ function dashboard.wakeup(widget) end if #dashboard.boxRects > 0 then - -- Always wake explicitly scheduled objects + for _, idx in ipairs(scheduledBoxIndices) do local rect = dashboard.boxRects[idx] local obj = dashboard.objectsByType[rect.box.type] @@ -1593,41 +1200,27 @@ function dashboard.wakeup(widget) local dirtyRects = {} if dashboard._useSpreadScheduling == false then - -- Wake up all boxes regardless of scheduler flag + for i, rect in ipairs(dashboard.boxRects) do local obj = dashboard.objectsByType[rect.box.type] - if obj and obj.wakeup and not obj.scheduler then - obj.wakeup(rect.box) - end + if obj and obj.wakeup and not obj.scheduler then obj.wakeup(rect.box) end if not needsFullInvalidate then local dirtyFn = obj and obj.dirty - if dirtyFn and dirtyFn(rect.box) then - table.insert(dirtyRects, { - x = rect.x - 1, y = rect.y - 1, - w = rect.w + 2, h = rect.h + 2 - }) - end + if dirtyFn and dirtyFn(rect.box) then table.insert(dirtyRects, {x = rect.x - 1, y = rect.y - 1, w = rect.w + 2, h = rect.h + 2}) end end end else - -- Spread mode: stagger wakeups + for i = 1, objectWakeupsPerCycle do local idx = objectWakeupIndex local rect = dashboard.boxRects[idx] if rect then local obj = dashboard.objectsByType[rect.box.type] - if obj and obj.wakeup and not obj.scheduler then - obj.wakeup(rect.box) - end + if obj and obj.wakeup and not obj.scheduler then obj.wakeup(rect.box) end if not needsFullInvalidate then local dirtyFn = obj and obj.dirty - if dirtyFn and dirtyFn(rect.box) then - table.insert(dirtyRects, { - x = rect.x - 1, y = rect.y - 1, - w = rect.w + 2, h = rect.h + 2 - }) - end + if dirtyFn and dirtyFn(rect.box) then table.insert(dirtyRects, {x = rect.x - 1, y = rect.y - 1, w = rect.w + 2, h = rect.h + 2}) end end end objectWakeupIndex = (#dashboard.boxRects > 0) and ((objectWakeupIndex % #dashboard.boxRects) + 1) or 1 @@ -1637,51 +1230,30 @@ function dashboard.wakeup(widget) objectsThreadedWakeupCount = objectsThreadedWakeupCount + 1 - -- Force repaint if dashboard._useSpreadSchedulingPaint then if needsFullInvalidate then - -- queue a full repaint + _queueInvalidateRect(0, 0, W, H) - dashboard._forceFullRepaint = false -- reset once consumed + dashboard._forceFullRepaint = false else - for _, r in ipairs(dirtyRects) do - _queueInvalidateRect(r.x, r.y, r.w, r.h) - end + for _, r in ipairs(dirtyRects) do _queueInvalidateRect(r.x, r.y, r.w, r.h) end end else _queueInvalidateRect(0, 0, W, H) end - -- try to flush; if budget says "too soon", it’ll wait until a later wakeup _flushInvalidatesRespectingBudget() end - if not lcd.hasFocus(widget) and dashboard.selectedBoxIndex ~= nil then log("Removing focus from box " .. tostring(dashboard.selectedBoxIndex), "info") dashboard.selectedBoxIndex = nil - if dashboard._useSpreadSchedulingPaint then - lcd.invalidate(widget) - end + if dashboard._useSpreadSchedulingPaint then lcd.invalidate(widget) end end - if not dashboard._useSpreadSchedulingPaint then - lcd.invalidate() - end + if not dashboard._useSpreadSchedulingPaint then lcd.invalidate() end end - ---- Lists available dashboard themes by scanning system and user theme directories. --- --- This function searches for theme folders in predefined base paths, loads their `init.lua` files, --- and collects theme metadata if the theme is valid and permitted by developer settings. --- --- @return themes (table) A list of theme tables, each containing: --- - name (string): The display name of the theme. --- - configure (function|nil): Optional configuration function for the theme. --- - folder (string): The folder name where the theme is located. --- - idx (number): The index of the theme in the list. --- - source (string): The source type ("system" or "user"). function dashboard.listThemes() local themes = {} local num = 0 @@ -1690,7 +1262,7 @@ function dashboard.listThemes() local folders = system.listFiles(basePath) if not folders then return end for _, folder in ipairs(folders) do - if folder ~= ".." and folder ~= "." and not folder:match("%.%a+$") then + if folder ~= ".." and folder ~= "." and not folder:match("%.%a+$") then local themeDir = basePath .. folder .. "/" local initPath = themeDir .. "init.lua" if utils.dir_exists(basePath, folder) then @@ -1700,16 +1272,10 @@ function dashboard.listThemes() if ok and initTable and type(initTable.name) == "string" then if not initTable.developer or dashx.preferences.developer.devtools == true then num = num + 1 - themes[num] = { - name = initTable.name, - configure = initTable.configure, - folder = folder, - idx = num, - source = sourceType, - } + themes[num] = {name = initTable.name, configure = initTable.configure, folder = folder, idx = num, source = sourceType} end else - print("Error detail: "..tostring(initTable)) + print("Error detail: " .. tostring(initTable)) end end end @@ -1719,18 +1285,11 @@ function dashboard.listThemes() scanThemes(themesBasePath, "system") local basePath = "SCRIPTS:/" .. preferences .. "/" - if utils.dir_exists(basePath, 'dashboard') then - scanThemes(themesUserPath, "user") - end + if utils.dir_exists(basePath, 'dashboard') then scanThemes(themesUserPath, "user") end return themes end ---- Retrieves a preference value for the dashboard widget. --- Depending on whether the GUI is running, this function fetches the preference value --- from either the current widget path or the dashboard editing theme. --- @param key string: The preference key to retrieve. --- @return any|nil: The value associated with the given key, or nil if not found or prerequisites are missing. function dashboard.getPreference(key) if not dashx.session.modelPreferences or not dashboard.currentWidgetPath then return nil end @@ -1741,16 +1300,8 @@ function dashboard.getPreference(key) end end ---- Saves a user preference for the dashboard widget. --- Depending on whether the GUI is running, the preference is saved either for the current widget path --- or for the dashboard editing theme. --- @param key string: The preference key to save. --- @param value any: The value to associate with the key. --- @return boolean: True if the preference was saved successfully, false otherwise. function dashboard.savePreference(key, value) - if not dashx.session.modelPreferences or not dashx.session.modelPreferencesFile or not dashboard.currentWidgetPath then - return false - end + if not dashx.session.modelPreferences or not dashx.session.modelPreferencesFile or not dashboard.currentWidgetPath then return false end if not dashx.app.guiIsRunning then dashx.ini.setvalue(dashx.session.modelPreferences, dashboard.currentWidgetPath, key, value) return dashx.ini.save_ini_file(dashx.session.modelPreferencesFile, dashx.session.modelPreferences) @@ -1760,50 +1311,29 @@ function dashboard.savePreference(key, value) end end --- Ask user for confirmation before erasing dataflash function dashboard.resetFlightModeAsk() - local buttons = {{ - label = "@i18n(app.btn_ok)@", - action = function() - tasks.events.flightmode.reset() - lcd.invalidate() - return true - end - }, { - label = "@i18n(app.btn_cancel)@", - action = function() - return true - end - }} - - form.openDialog({ - width = nil, - title = "@i18n(widgets.dashboard.reset_flight_ask_title)@", - message = "@i18n(widgets.dashboard.reset_flight_ask_text)@", - buttons = buttons, - wakeup = function() - end, - paint = function() - end, - options = TEXT_LEFT - }) - -end - -function dashboard.menu(widget) - return { - {"@i18n(widgets.dashboard.reset_flight)@", dashboard.resetFlightModeAsk}, + local buttons = { + { + label = "@i18n(app.btn_ok)@", + action = function() + tasks.events.flightmode.reset() + lcd.invalidate() + return true + end + }, {label = "@i18n(app.btn_cancel)@", action = function() return true end} } + + form.openDialog({width = nil, title = "@i18n(widgets.dashboard.reset_flight_ask_title)@", message = "@i18n(widgets.dashboard.reset_flight_ask_text)@", buttons = buttons, wakeup = function() end, paint = function() end, options = TEXT_LEFT}) + end --- table to stall object cache +function dashboard.menu(widget) return {{"@i18n(widgets.dashboard.reset_flight)@", dashboard.resetFlightModeAsk}} end + dashboard.renders = dashboard.renders or {} --- disabled use of title dashboard.title = false --- expose widget moving dashboard.isSliding = function() return isSliding end return dashboard diff --git a/scripts/dashx/widgets/dashboard/init.lua b/scripts/dashx/widgets/dashboard/init.lua index 2a496a6..ac7ee06 100644 --- a/scripts/dashx/widgets/dashboard/init.lua +++ b/scripts/dashx/widgets/dashboard/init.lua @@ -1,27 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- -local init = { - script = "dashboard.lua", -- run this script - varname = "dashboard", -- variable name used in the suite. (if nil, we use the script var with .lua removed) - name = "DashX", -- name of the widget - key = "dshxdsh" -- key id used for widget -} + +local dashx = require("dashx") + +local init = {script = "dashboard.lua", varname = "dashboard", name = "DashX", key = "dshxdsh"} return init diff --git a/scripts/dashx/widgets/dashboard/lib/loaders.lua b/scripts/dashx/widgets/dashboard/lib/loaders.lua index 5a05715..1169763 100644 --- a/scripts/dashx/widgets/dashboard/lib/loaders.lua +++ b/scripts/dashx/widgets/dashboard/lib/loaders.lua @@ -1,187 +1,179 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") --- loaders.lua + local loaders = {} --- Helper to draw logo image centered local function drawLogoImage(cx, cy, w, h) - local imageName = "SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/gfx/logo.png" - local bmp = dashx.utils.loadImage(imageName) - if bmp then - local imgSize = math.min(w, h) * 0.5 - lcd.drawBitmap(cx - imgSize / 2, cy - imgSize / 2, bmp, imgSize, imgSize) - end + local imageName = "SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/gfx/logo.png" + local bmp = dashx.utils.loadImage(imageName) + if bmp then + local imgSize = math.min(w, h) * 0.5 + lcd.drawBitmap(cx - imgSize / 2, cy - imgSize / 2, bmp, imgSize, imgSize) + end end --- Helper to wrap and truncate message text local function getWrappedTextLines(message, fonts, maxWidth, maxHeight) - local lines = {} - local chosenFont = fonts[1] - local _, lineH = lcd.getTextSize("Ay") - - for i = #fonts, 1, -1 do - lcd.font(fonts[i]) - local tw, th = lcd.getTextSize(message) - if tw <= maxWidth and th <= maxHeight then - chosenFont = fonts[i] - _, lineH = lcd.getTextSize("Ay") - break + local lines = {} + local chosenFont = fonts[1] + local _, lineH = lcd.getTextSize("Ay") + + for i = #fonts, 1, -1 do + lcd.font(fonts[i]) + local tw, th = lcd.getTextSize(message) + if tw <= maxWidth and th <= maxHeight then + chosenFont = fonts[i] + _, lineH = lcd.getTextSize("Ay") + break + end end - end - - local function wrap(str) - local words = {} - for w in str:gmatch("%S+") do table.insert(words, w) end - local current = words[1] or "" - for i = 2, #words do - local test = current .. " " .. words[i] - if lcd.getTextSize(test) <= maxWidth then - current = test - else + + local function wrap(str) + local words = {} + for w in str:gmatch("%S+") do table.insert(words, w) end + local current = words[1] or "" + for i = 2, #words do + local test = current .. " " .. words[i] + if lcd.getTextSize(test) <= maxWidth then + current = test + else + table.insert(lines, current) + current = words[i] + end + end table.insert(lines, current) - current = words[i] - end end - table.insert(lines, current) - end - - wrap(message) - local maxLines = math.floor(maxHeight / lineH) - if #lines > maxLines then - lines = { table.unpack(lines, 1, maxLines) } - local last = lines[#lines] - while lcd.getTextSize(last .. "…") > maxWidth and #last > 1 do - last = last:sub(1, -2) + + wrap(message) + local maxLines = math.floor(maxHeight / lineH) + if #lines > maxLines then + lines = {table.unpack(lines, 1, maxLines)} + local last = lines[#lines] + while lcd.getTextSize(last .. "…") > maxWidth and #last > 1 do last = last:sub(1, -2) end + lines[#lines] = last .. "…" end - lines[#lines] = last .. "…" - end - return lines, chosenFont, lineH + return lines, chosenFont, lineH end --- Shared overlay rendering local function drawOverlayBackground(cx, cy, innerR, bg) - lcd.color(bg) - if lcd.drawFilledCircle then - lcd.drawFilledCircle(cx, cy, innerR) - else - lcd.drawFilledRectangle(cx - innerR, cy - innerR, innerR * 2, innerR * 2) - end + lcd.color(bg) + if lcd.drawFilledCircle then + lcd.drawFilledCircle(cx, cy, innerR) + else + lcd.drawFilledRectangle(cx - innerR, cy - innerR, innerR * 2, innerR * 2) + end end local function renderOverlayText(dashboard, cx, cy, innerR, fg) - local message = dashboard._overlay_text or "@i18n(widgets.dashboard.loading)@" - local fonts = dashboard.utils.getFontListsForResolution().value_default - local lines, chosenFont, lineH = getWrappedTextLines(message, fonts, innerR * 2 * 0.9, innerR * 2 * 0.8) - - lcd.color(fg) - lcd.font(chosenFont) - local totalH = #lines * lineH - for i, line in ipairs(lines) do - local tw = lcd.getTextSize(line) - lcd.drawText(cx - tw / 2, cy - totalH / 2 + (i - 1) * lineH, line) - end + local message = dashboard._overlay_text or "@i18n(widgets.dashboard.loading)@" + local fonts = dashboard.utils.getFontListsForResolution().value_default + local lines, chosenFont, lineH = getWrappedTextLines(message, fonts, innerR * 2 * 0.9, innerR * 2 * 0.8) + + lcd.color(fg) + lcd.font(chosenFont) + local totalH = #lines * lineH + for i, line in ipairs(lines) do + local tw = lcd.getTextSize(line) + lcd.drawText(cx - tw / 2, cy - totalH / 2 + (i - 1) * lineH, line) + end end - --- Static loader (no pulse) function loaders.staticLoader(dashboard, x, y, w, h) - local cx, cy = x + w / 2, y + h / 2 - local radius = math.min(w, h) * (dashboard.loaderScale or 0.3) - local thickness = math.max(6, radius * 0.15) - - local r, g, b = lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0 - lcd.color(lcd.RGB(r, g, b, 1.0)) -- Solid color with full opacity - - if lcd.drawFilledCircle then - lcd.drawFilledCircle(cx, cy, radius) - lcd.color(lcd.darkMode() and lcd.RGB(0, 0, 0, 1.0) or lcd.RGB(0, 0, 0, 1.0)) - lcd.drawFilledCircle(cx, cy, radius - thickness) - end - - drawLogoImage(cx, cy, w, h) - --- Animated dots below the logo - dashboard._dots_index = dashboard._dots_index or 1 - dashboard._dots_time = dashboard._dots_time or os.clock() - if os.clock() - dashboard._dots_time > 0.5 then - dashboard._dots_time = os.clock() - dashboard._dots_index = (dashboard._dots_index % 3) + 1 - end - - local dotRadius = 4 - local spacing = 3 * dotRadius - local startX = cx - spacing - local yPos = cy + (radius - thickness / 2) / 2 -- Midway between center and outer edge - - for i = 1, 3 do - if i == dashboard._dots_index then - lcd.color(lcd.darkMode() and lcd.RGB(255,255,255) or lcd.RGB(0,0,0)) - else - lcd.color(lcd.darkMode() and lcd.RGB(80,80,80) or lcd.RGB(180,180,180)) + local cx, cy = x + w / 2, y + h / 2 + local radius = math.min(w, h) * (dashboard.loaderScale or 0.3) + local thickness = math.max(6, radius * 0.15) + + local r, g, b = lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0 + lcd.color(lcd.RGB(r, g, b, 1.0)) + + if lcd.drawFilledCircle then + lcd.drawFilledCircle(cx, cy, radius) + lcd.color(lcd.darkMode() and lcd.RGB(0, 0, 0, 1.0) or lcd.RGB(0, 0, 0, 1.0)) + lcd.drawFilledCircle(cx, cy, radius - thickness) + end + + drawLogoImage(cx, cy, w, h) + + dashboard._dots_index = dashboard._dots_index or 1 + dashboard._dots_time = dashboard._dots_time or os.clock() + if os.clock() - dashboard._dots_time > 0.5 then + dashboard._dots_time = os.clock() + dashboard._dots_index = (dashboard._dots_index % 3) + 1 + end + + local dotRadius = 4 + local spacing = 3 * dotRadius + local startX = cx - spacing + local yPos = cy + (radius - thickness / 2) / 2 + + for i = 1, 3 do + if i == dashboard._dots_index then + lcd.color(lcd.darkMode() and lcd.RGB(255, 255, 255) or lcd.RGB(0, 0, 0)) + else + lcd.color(lcd.darkMode() and lcd.RGB(80, 80, 80) or lcd.RGB(180, 180, 180)) + end + lcd.drawFilledCircle(startX + (i - 1) * spacing, yPos, dotRadius) end - lcd.drawFilledCircle(startX + (i - 1) * spacing, yPos, dotRadius) - end end --- Static overlay message (no pulse animation) function loaders.staticOverlayMessage(dashboard, x, y, w, h, txt) - dashboard._overlay_cycles_required = dashboard._overlay_cycles_required or math.ceil(5 / (dashboard.paint_interval or 0.5)) - dashboard._overlay_cycles = dashboard._overlay_cycles or 0 - - if txt and txt ~= "" then - dashboard._overlay_text = txt - dashboard._overlay_cycles = dashboard._overlay_cycles_required - end - - if dashboard._overlay_cycles <= 0 then return end - dashboard._overlay_cycles = dashboard._overlay_cycles - 1 - - local fg = lcd.darkMode() and lcd.RGB(255,255,255) or lcd.RGB(255,255,255) - local bg = lcd.darkMode() and lcd.RGB(0,0,0,1.0) or lcd.RGB(255,255,255,1.0) - - local cx, cy = x + w / 2, y + h / 2 - local radius = math.min(w, h) * (dashboard.overlayScale or 0.35) - local thickness = math.max(6, radius * 0.15) - local innerR = radius - (thickness / 2) - 1 - - -- draw solid background circle - drawOverlayBackground(cx, cy, innerR, bg) - - -- outer circle with full opacity - local r, g, b = lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0 - lcd.color(lcd.RGB(r, g, b, 1.0)) - if lcd.drawFilledCircle then - lcd.drawFilledCircle(cx, cy, radius) - lcd.color(lcd.darkMode() and lcd.RGB(0,0,0,1.0) or lcd.RGB(0,0,0,1.0)) - lcd.drawFilledCircle(cx, cy, radius - thickness) - end - --- Animated dots below the logo - dashboard._dots_index = dashboard._dots_index or 1 - dashboard._dots_time = dashboard._dots_time or os.clock() - if os.clock() - dashboard._dots_time > 0.5 then - dashboard._dots_time = os.clock() - dashboard._dots_index = (dashboard._dots_index % 3) + 1 - end - - local dotRadius = 4 - local spacing = 3 * dotRadius - local startX = cx - spacing - local yPos = cy + (radius - thickness / 2) / 2 -- Midway between center and outer edge - - for i = 1, 3 do - if i == dashboard._dots_index then - lcd.color(lcd.darkMode() and lcd.RGB(255,255,255) or lcd.RGB(0,0,0)) - else - lcd.color(lcd.darkMode() and lcd.RGB(80,80,80) or lcd.RGB(180,180,180)) + dashboard._overlay_cycles_required = dashboard._overlay_cycles_required or math.ceil(5 / (dashboard.paint_interval or 0.5)) + dashboard._overlay_cycles = dashboard._overlay_cycles or 0 + + if txt and txt ~= "" then + dashboard._overlay_text = txt + dashboard._overlay_cycles = dashboard._overlay_cycles_required end - lcd.drawFilledCircle(startX + (i - 1) * spacing, yPos, dotRadius) - end - renderOverlayText(dashboard, cx, cy, innerR, fg) -end + if dashboard._overlay_cycles <= 0 then return end + dashboard._overlay_cycles = dashboard._overlay_cycles - 1 + + local fg = lcd.darkMode() and lcd.RGB(255, 255, 255) or lcd.RGB(255, 255, 255) + local bg = lcd.darkMode() and lcd.RGB(0, 0, 0, 1.0) or lcd.RGB(255, 255, 255, 1.0) + + local cx, cy = x + w / 2, y + h / 2 + local radius = math.min(w, h) * (dashboard.overlayScale or 0.35) + local thickness = math.max(6, radius * 0.15) + local innerR = radius - (thickness / 2) - 1 + + drawOverlayBackground(cx, cy, innerR, bg) + local r, g, b = lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0, lcd.darkMode() and 255 or 0 + lcd.color(lcd.RGB(r, g, b, 1.0)) + if lcd.drawFilledCircle then + lcd.drawFilledCircle(cx, cy, radius) + lcd.color(lcd.darkMode() and lcd.RGB(0, 0, 0, 1.0) or lcd.RGB(0, 0, 0, 1.0)) + lcd.drawFilledCircle(cx, cy, radius - thickness) + end + + dashboard._dots_index = dashboard._dots_index or 1 + dashboard._dots_time = dashboard._dots_time or os.clock() + if os.clock() - dashboard._dots_time > 0.5 then + dashboard._dots_time = os.clock() + dashboard._dots_index = (dashboard._dots_index % 3) + 1 + end + + local dotRadius = 4 + local spacing = 3 * dotRadius + local startX = cx - spacing + local yPos = cy + (radius - thickness / 2) / 2 + + for i = 1, 3 do + if i == dashboard._dots_index then + lcd.color(lcd.darkMode() and lcd.RGB(255, 255, 255) or lcd.RGB(0, 0, 0)) + else + lcd.color(lcd.darkMode() and lcd.RGB(80, 80, 80) or lcd.RGB(180, 180, 180)) + end + lcd.drawFilledCircle(startX + (i - 1) * spacing, yPos, dotRadius) + end + + renderOverlayText(dashboard, cx, cy, innerR, fg) +end return loaders diff --git a/scripts/dashx/widgets/dashboard/lib/utils.lua b/scripts/dashx/widgets/dashboard/lib/utils.lua index e800a28..e45233b 100644 --- a/scripts/dashx/widgets/dashboard/lib/utils.lua +++ b/scripts/dashx/widgets/dashboard/lib/utils.lua @@ -1,33 +1,16 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- - + +local dashx = require("dashx") + local utils = {} local imageCache = {} -local fontCache +local fontCache -function utils.standardHeaderLayout(headeropts) - return { - height = headeropts.height, - cols = 7, - rows = 1 - } -end +function utils.standardHeaderLayout(headeropts) return {height = headeropts.height, cols = 7, rows = 1} end function utils.standardHeaderBoxes(i18n, colorMode, headeropts, txbatt_type) local txbatt_min, txbatt_max = utils.getTxBatteryVoltageRange() @@ -44,49 +27,48 @@ function utils.standardHeaderBoxes(i18n, colorMode, headeropts, txbatt_type) end return { - -- Craftname - { - col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", - font = headeropts.font, valuealign = "left", valuepaddingleft = 5, - bgcolor = colorMode.tbbgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.cntextcolor - }, - -- RF Logo - { - col = 3, row = 1, colspan = 3, type = "image", subtype = "image", + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.tbbgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.cntextcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.tbbgcolor}, txBox, { + col = 7, + row = 1, + type = "gauge", + subtype = "step", + source = "rssi", + font = "FONT_XS", + stepgap = 2, + stepcount = 5, + decimals = 0, + valuealign = "left", + barpaddingleft = headeropts.barpaddingleft, + barpaddingright = headeropts.barpaddingright, + barpaddingbottom = headeropts.barpaddingbottom, + barpaddingtop = headeropts.barpaddingtop, + valuepaddingleft = headeropts.valuepaddingleft, + valuepaddingbottom = headeropts.valuepaddingbottom, bgcolor = colorMode.tbbgcolor, - }, - - -- TX Battery (DYNAMIC) - txBox, - - -- RSSI - { - col = 7, row = 1, - type = "gauge", subtype = "step", source = "rssi", - font = "FONT_XS", stepgap = 2, stepcount = 5, decimals = 0, valuealign = "left", - barpaddingleft = headeropts.barpaddingleft, barpaddingright = headeropts.barpaddingright, - barpaddingbottom = headeropts.barpaddingbottom, barpaddingtop = headeropts.barpaddingtop, - valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.tbbgcolor, textcolor = colorMode.rssitextcolor, - fillcolor = colorMode.rssifillcolor, fillbgcolor = colorMode.rssifillbgcolor, + textcolor = colorMode.rssitextcolor, + fillcolor = colorMode.rssifillcolor, + fillbgcolor = colorMode.rssifillbgcolor } } end function utils.getTxBox(colorMode, headeropts, txbatt_min, txbatt_max, txbatt_warn) return { - col = 6, row = 1, - type = "gauge", - subtype = "bar", + col = 6, + row = 1, + type = "gauge", + subtype = "bar", source = "txbatt", - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -96,21 +78,19 @@ function utils.getTxBox(colorMode, headeropts, txbatt_min, txbatt_max, txbatt_wa gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, cappaddingright = headeropts.cappaddingright, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.tbbgcolor, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, min = txbatt_min, max = txbatt_max, - thresholds = { - { value = txbatt_warn, fillcolor = colorMode.fillwarncolor }, - { value = txbatt_max, fillcolor = colorMode.txfillcolor } - } + thresholds = {{value = txbatt_warn, fillcolor = colorMode.fillwarncolor}, {value = txbatt_max, fillcolor = colorMode.txfillcolor}} } end local function txTextBox(colorMode, headeropts) return { - col = 6, row = 1, + col = 6, + row = 1, type = "text", subtype = "telemetry", source = "txbatt", @@ -130,41 +110,36 @@ end local function txDigitalBox(colorMode, headeropts, txbatt_min, txbatt_max, txbatt_warn) return { - col = 6, + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.txdbattfont, - battery = false, + battery = false, roundradius = headeropts.roundradius, - decimals = 1, - unit = "v", + decimals = 1, + unit = "v", gaugepaddingright = headeropts.txdgaugepaddingright, gaugepaddingleft = headeropts.txdgaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, valuepaddingleft = headeropts.txdvaluepaddingleft, valuepaddingtop = headeropts.txdvaluepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.tbbgcolor, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.tbtextcolor, min = txbatt_min, max = txbatt_max, - thresholds = { - { value = txbatt_warn, fillcolor = colorMode.fillwarncolor }, - { value = txbatt_max, fillcolor = colorMode.txfillcolor } - } + thresholds = {{value = txbatt_warn, fillcolor = colorMode.fillwarncolor}, {value = txbatt_max, fillcolor = colorMode.txfillcolor}} } end function utils.getTxBatteryVoltageRange() if system and system.voltageRange then local ok, vmin, vmax = pcall(system.voltageRange) - if ok and vmin and vmax and vmin < vmax then - return vmin, vmax - end + if ok and vmin and vmax and vmin < vmax then return vmin, vmax end end return 7.2, 8.4 @@ -173,113 +148,69 @@ end function utils.themeColors() local colorMode = { dark = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillwarncolor = "orange", - fillcritcolor = "red", - fillbgcolor = "grey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillwarncolor = "orange", + fillcritcolor = "red", + fillbgcolor = "grey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - tbbgcolor = "headergrey", - cntextcolor = "white", - tbtextcolor = "white" + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + tbbgcolor = "headergrey", + cntextcolor = "white", + tbtextcolor = "white" }, light = { - textcolor = "lmgrey", - titlecolor = "lmgrey", - bgcolor = "white", - fillcolor = "lightgreen", - fillwarncolor = "lightorange", - fillcritcolor = "lightred", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "lightgreen", + textcolor = "lmgrey", + titlecolor = "lmgrey", + bgcolor = "white", + fillcolor = "lightgreen", + fillwarncolor = "lightorange", + fillcritcolor = "lightred", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "lightgreen", rssifillbgcolor = "grey", - txaccentcolor = "white", - txfillcolor = "lightgreen", - txbgfillcolor = "grey", - tbbgcolor = "darkgrey", - cntextcolor = "white", - tbtextcolor = "white" + txaccentcolor = "white", + txfillcolor = "lightgreen", + txbgfillcolor = "grey", + tbbgcolor = "darkgrey", + cntextcolor = "white", + tbtextcolor = "white" } } return lcd.darkMode() and colorMode.dark or colorMode.light end --- Determine layout and screensize in use function utils.isFullScreen(w, h) - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard if (w == 800 and (h == 458 or h == 480)) then return true end if (w == 784 and (h == 294 or h == 316)) then return false end - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard if (w == 480 and (h == 301 or h == 320)) then return true end if (w == 472 and (h == 191 or h == 210)) then return false end - -- Small screens - (X14 / X14S) Full/Standard if (w == 640 and (h == 338 or h == 360)) then return true end if (w == 630 and (h == 236 or h == 258)) then return false end - return nil -- Unknown resolution, assume not fullscreen + return nil end ---- Checks if the model preferences are ready. --- This function returns true if the `dashx` table, its `session` field, --- and the `modelPreferences` field within `session` are all non-nil. --- @return boolean True if model preferences are ready, false otherwise. -function utils.isModelPrefsReady() - return dashx and dashx.session and dashx.session.modelPreferences -end +function utils.isModelPrefsReady() return dashx and dashx.session and dashx.session.modelPreferences end ---- Resets the cache of a given box object by clearing all entries in its `_cache` table. --- If the box has a `_cache` table, all its keys are set to nil, effectively emptying the cache. --- @param box table The box object whose cache should be reset. -function utils.resetBoxCache(box) - if box._cache then - for k in pairs(box._cache) do - box._cache[k] = nil - end - end -end +function utils.resetBoxCache(box) if box._cache then for k in pairs(box._cache) do box._cache[k] = nil end end end --- Returns true if (W, H) exactly matches one of the entries in supportedResolutions. --- W, H: current window width and height (numbers) --- supportedResolutions: an array of {width, height} pairs, e.g. --- { --- { 784, 294 }, --- { 784, 316 }, --- { 472, 191 }, --- { 472, 210 }, --- { 630, 236 }, --- { 630, 258 }, --- } -function utils.supportedResolution(W,H, supportedResolutions) - - for _, res in ipairs(supportedResolutions) do - if W == res[1] and H == res[2] then - return true - end - end +function utils.supportedResolution(W, H, supportedResolutions) + + for _, res in ipairs(supportedResolutions) do if W == res[1] and H == res[2] then return true end end return false end ---- Draws a bar-style needle (such as for a gauge or meter) at a specified position, angle, and size. --- The needle is rendered as a thick, filled bar with a specified thickness and length, centered at (cx, cy), --- and rotated by angleDeg degrees. The needle is drawn with a slight overlap at both ends for visual effect. --- --- @param cx number: X-coordinate of the needle's base (center point). --- @param cy number: Y-coordinate of the needle's base (center point). --- @param length number: Length of the needle from base to tip. --- @param thickness number: Thickness of the needle bar. --- @param angleDeg number: Angle of the needle in degrees (0 is to the right, increases counterclockwise). --- @param color number: Color value to use for drawing the needle. function utils.drawBarNeedle(cx, cy, length, thickness, angleDeg, color) local angleRad = math.rad(angleDeg) local step = 1 @@ -292,11 +223,6 @@ function utils.drawBarNeedle(cx, cy, length, thickness, angleDeg, color) end end ---- Returns a table of font lists appropriate for the current radio's screen resolution. --- The function detects the radio's LCD width and height, then selects a set of font sizes --- for default, reduced, and title text usage based on known device resolutions. --- If the resolution is not recognized, it logs a warning and falls back to the default (800x480) font set. --- @return table A table containing font lists for 'value_default', 'value_reduced', and 'value_title' keys. function utils.getFontListsForResolution() local version = system.getVersion() local LCD_W = version.lcdWidth @@ -304,51 +230,26 @@ function utils.getFontListsForResolution() local resolution = LCD_W .. "x" .. LCD_H local radios = { - -- TANDEM X20, TANDEM XE (800x480) - ["800x480"] = { - value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL, FONT_XXL, FONT_XXXXL}, - value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, - value_title = {FONT_XXS, FONT_XS, FONT_S, FONT_M} - }, - -- TANDEM X18, TWIN X Lite (480x320) - ["480x320"] = { - value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL}, - value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, - value_title = {FONT_XXS, FONT_XS, FONT_S} - }, - -- Horus X10, Horus X12 (480x272) - ["480x272"] = { - value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M}, - value_reduced = {FONT_XXS, FONT_XS, FONT_S}, - value_title = {FONT_XXS, FONT_XS, FONT_S} - }, - -- Twin X14 (632x314) - ["640x360"] = { - value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL}, - value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, - value_title = {FONT_XXS, FONT_XS, FONT_S} - } + + ["800x480"] = {value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL, FONT_XXL, FONT_XXXXL}, value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, value_title = {FONT_XXS, FONT_XS, FONT_S, FONT_M}}, + + ["480x320"] = {value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL}, value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, value_title = {FONT_XXS, FONT_XS, FONT_S}}, + + ["480x272"] = {value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M}, value_reduced = {FONT_XXS, FONT_XS, FONT_S}, value_title = {FONT_XXS, FONT_XS, FONT_S}}, + + ["640x360"] = {value_default = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL}, value_reduced = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L}, value_title = {FONT_XXS, FONT_XS, FONT_S}} } if not radios[resolution] then - dashx.utils.log("Unsupported resolution: " .. resolution .. ". Using default fonts.","info") + dashx.utils.log("Unsupported resolution: " .. resolution .. ". Using default fonts.", "info") return radios["800x480"] end - return radios[resolution] + return radios[resolution] end ---- Returns a table of recommended header layout options for the current radio’s screen resolution. --- This function detects the radio’s LCD width, then returns a set of standard header options --- (such as height and padding values) appropriate for known device resolutions. --- These values ensure consistent header spacing, sizing, and alignment for the three supported full-screen --- layouts (X20, X18, X14). --- --- The returned table can be used directly as a `header_layout` for dashboard themes, or individual fields - function utils.getHeaderOptions() local W, H = lcd.getWindowSize() - -- X20/X20RS: 800x480 or 784x294 if W == 800 or W == 784 then return { height = 36, @@ -366,10 +267,9 @@ function utils.getHeaderOptions() barpaddingbottom = 2, barpaddingtop = 4, valuepaddingleft = 20, - valuepaddingbottom = 20, + valuepaddingbottom = 20 } - -- X18/TWXLITE: 480x320 or 472x191 elseif W == 480 or W == 472 then return { height = 30, @@ -386,9 +286,9 @@ function utils.getHeaderOptions() barpaddingright = 18, barpaddingbottom = 2, barpaddingtop = 2, - valuepaddingbottom = 20, + valuepaddingbottom = 20 } - -- X14/X14S: 640x360 or 630x236 + elseif W == 640 or W == 630 then return { height = 30, @@ -405,47 +305,28 @@ function utils.getHeaderOptions() barpaddingright = 21, barpaddingbottom = 2, barpaddingtop = 2, - valuepaddingbottom = 20, + valuepaddingbottom = 20 } end end ---- Resets the image cache by removing all entries from the `imageCache` table. --- This function iterates over all keys in the `imageCache` table and sets their values to nil, --- effectively clearing the cache and freeing up memory used by cached images. -function utils.resetImageCache() - for k in pairs(imageCache) do - imageCache[k] = nil - end -end +function utils.resetImageCache() for k in pairs(imageCache) do imageCache[k] = nil end end ---- Displays an error message centered on the screen, automatically selecting the largest font size ---- that fits within 90% of the screen's width and height. The text color adapts to the current ---- dark mode setting. --- @param msg string: The error message to display. --- @param bool : The draw border around the text (default true). --- @param pct number: The percentage of the screen size to use for text fitting (default 0.5). --- @param padX number: Horizontal padding around the text (default 8). --- @param padY number: Vertical padding around the text (default 4). function utils.screenError(msg, border, pct, padX, padY) - -- Default values + if not pct then pct = 0.5 end if border == nil then border = true end - if not padX then padX = 8 end -- default horizontal padding - if not padY then padY = 4 end -- default vertical padding + if not padX then padX = 8 end + if not padY then padY = 4 end - -- Get display size and mode local w, h = lcd.getWindowSize() local isDarkMode = lcd.darkMode() - -- Available fonts to try local fonts = {FONT_XXS, FONT_XS, FONT_S, FONT_M, FONT_L, FONT_XL, FONT_XXL, FONT_XXXXL} - - -- Compute maximum allowed dimensions for text + local maxW, maxH = w * pct, h * pct local bestFont, bestW, bestH = FONT_XXS, 0, 0 - -- Choose the largest font that fits within maxW x maxH for _, font in ipairs(fonts) do lcd.font(font) local tsizeW, tsizeH = lcd.getTextSize(msg) @@ -457,119 +338,83 @@ function utils.screenError(msg, border, pct, padX, padY) end end - -- Set chosen font lcd.font(bestFont) - -- Determine text and border color local textColor = isDarkMode and lcd.RGB(255, 255, 255, 1) or lcd.RGB(90, 90, 90) lcd.color(textColor) - -- Calculate centered text position local x = (w - bestW) / 2 local y = (h - bestH) / 2 - -- Draw border rectangle if requested - if border then - lcd.drawRectangle(x - padX, y - padY, bestW + padX * 2, bestH + padY * 2) - end + if border then lcd.drawRectangle(x - padX, y - padY, bestW + padX * 2, bestH + padY * 2) end - -- Draw the text centered lcd.drawText(x, y, msg) end ---- Resolve a named, bright/light, dark, or raw-RGB color. --- @param value string (e.g. "red", "brightBlue", "darkGreen") or {r,g,b,...} --- @param variantFactor number? how strongly to lighten/darken (0–1). Defaults to 0.3. --- @return lcd.RGB(...) or nil function utils.resolveColor(value, variantFactor) - -- base named colors + local namedColors = { - red = {255, 0, 0}, - green = {0, 188, 4}, - blue = {0, 122, 255}, - white = {255, 255, 255}, - black = {0, 0, 0}, - gray = {185, 185, 185}, - grey = {185, 185, 185}, - orange = {255, 165, 0}, - yellow = {255, 255, 0}, - cyan = {0, 255, 255}, - magenta = {255, 0, 255}, - pink = {255, 105, 180}, - purple = {128, 0, 128}, - violet = {143, 0, 255}, - brown = {139, 69, 19}, - lime = {0, 255, 0}, - olive = {128, 128, 0}, - gold = {255, 215, 0}, - silver = {192, 192, 192}, - teal = {0, 128, 128}, - navy = {0, 0, 128}, - maroon = {128, 0, 0}, - beige = {245, 245, 220}, - turquoise = {64, 224, 208}, - indigo = {75, 0, 130}, - coral = {255, 127, 80}, - salmon = {250, 128, 114}, - mint = {62, 180, 137}, - lightgreen = {144, 238, 144}, - darkgreen = {0, 100, 0}, - lightred = {255, 102, 102}, - darkred = {139, 0, 0}, - lightorange = {255, 200, 100}, - lightblue = {173, 216, 230}, - darkblue = {0, 0, 139}, - lightpurple = {216, 191, 216}, - darkpurple = {48, 25, 52}, - lightyellow = {255, 255, 224}, - darkyellow = {204, 204, 0}, - lightgrey = {211, 211, 211}, - lightgray = {211, 211, 211}, - darkgrey = {90, 90, 90}, - darkgray = {90, 90, 90}, - lmgrey = {80, 80, 80}, - darkwhite = {245, 245, 245}, + red = {255, 0, 0}, + green = {0, 188, 4}, + blue = {0, 122, 255}, + white = {255, 255, 255}, + black = {0, 0, 0}, + gray = {185, 185, 185}, + grey = {185, 185, 185}, + orange = {255, 165, 0}, + yellow = {255, 255, 0}, + cyan = {0, 255, 255}, + magenta = {255, 0, 255}, + pink = {255, 105, 180}, + purple = {128, 0, 128}, + violet = {143, 0, 255}, + brown = {139, 69, 19}, + lime = {0, 255, 0}, + olive = {128, 128, 0}, + gold = {255, 215, 0}, + silver = {192, 192, 192}, + teal = {0, 128, 128}, + navy = {0, 0, 128}, + maroon = {128, 0, 0}, + beige = {245, 245, 220}, + turquoise = {64, 224, 208}, + indigo = {75, 0, 130}, + coral = {255, 127, 80}, + salmon = {250, 128, 114}, + mint = {62, 180, 137}, + lightgreen = {144, 238, 144}, + darkgreen = {0, 100, 0}, + lightred = {255, 102, 102}, + darkred = {139, 0, 0}, + lightorange = {255, 200, 100}, + lightblue = {173, 216, 230}, + darkblue = {0, 0, 139}, + lightpurple = {216, 191, 216}, + darkpurple = {48, 25, 52}, + lightyellow = {255, 255, 224}, + darkyellow = {204, 204, 0}, + lightgrey = {211, 211, 211}, + lightgray = {211, 211, 211}, + darkgrey = {90, 90, 90}, + darkgray = {90, 90, 90}, + lmgrey = {80, 80, 80}, + darkwhite = {245, 245, 245} } - -- fallback to default 30% if not provided or out of range - local VARIANT_FACTOR = type(variantFactor) == "number" - and math.max(0, math.min(1, variantFactor)) - or 0.3 + local VARIANT_FACTOR = type(variantFactor) == "number" and math.max(0, math.min(1, variantFactor)) or 0.3 - local function clamp(v) - return math.max(0, math.min(255, math.floor(v + 0.5))) - end + local function clamp(v) return math.max(0, math.min(255, math.floor(v + 0.5))) end - local function lighten(rgb) - return { - clamp(rgb[1] + (255 - rgb[1]) * VARIANT_FACTOR), - clamp(rgb[2] + (255 - rgb[2]) * VARIANT_FACTOR), - clamp(rgb[3] + (255 - rgb[3]) * VARIANT_FACTOR), - } - end + local function lighten(rgb) return {clamp(rgb[1] + (255 - rgb[1]) * VARIANT_FACTOR), clamp(rgb[2] + (255 - rgb[2]) * VARIANT_FACTOR), clamp(rgb[3] + (255 - rgb[3]) * VARIANT_FACTOR)} end - local function darken(rgb) - return { - clamp(rgb[1] * (1 - VARIANT_FACTOR)), - clamp(rgb[2] * (1 - VARIANT_FACTOR)), - clamp(rgb[3] * (1 - VARIANT_FACTOR)), - } - end + local function darken(rgb) return {clamp(rgb[1] * (1 - VARIANT_FACTOR)), clamp(rgb[2] * (1 - VARIANT_FACTOR)), clamp(rgb[3] * (1 - VARIANT_FACTOR))} end if type(value) == "string" then local lower = value:lower() - -- detect prefix and strip - local prefix, baseName = lower:match("^(bright)(.+)"), - lower:match("^bright(.+)") - if not prefix then - prefix, baseName = lower:match("^(light)(.+)"), - lower:match("^light(.+)") - end - if not prefix then - prefix, baseName = lower:match("^(dark)(.+)"), - lower:match("^dark(.+)") - end + local prefix, baseName = lower:match("^(bright)(.+)"), lower:match("^bright(.+)") + if not prefix then prefix, baseName = lower:match("^(light)(.+)"), lower:match("^light(.+)") end + if not prefix then prefix, baseName = lower:match("^(dark)(.+)"), lower:match("^dark(.+)") end if prefix and baseName then local baseColor = namedColors[baseName] @@ -579,34 +424,30 @@ function utils.resolveColor(value, variantFactor) end elseif namedColors[lower] then - -- exact named color + local c = namedColors[lower] return lcd.RGB(c[1], c[2], c[3], 1) end elseif type(value) == "table" and #value >= 3 then - -- raw RGB table + return lcd.RGB(value[1], value[2], value[3], 1) end - -- unrecognized return nil end --- Single color resolve by context key (returns RGB number) function utils.resolveThemeColor(colorkey, value) - -- If already a number (e.g. lcd.RGB), just return + if type(value) == "number" then return value end - -- an oddbal of a string "transparent" should return nil - if type(value) == "string" and value == "transparent" then - return nil - end - -- If string (like "red"), use resolveColor + + if type(value) == "string" and value == "transparent" then return nil end + if type(value) == "string" then local resolved = utils.resolveColor(value) if resolved then return resolved end end - -- Provide context defaults + if colorkey == "fillcolor" then return lcd.darkMode() and lcd.RGB(40, 40, 40) or lcd.RGB(240, 240, 240) elseif colorkey == "fillbgcolor" then @@ -614,99 +455,48 @@ function utils.resolveThemeColor(colorkey, value) elseif colorkey == "framecolor" then return lcd.darkMode() and lcd.RGB(40, 40, 40) or lcd.RGB(240, 240, 240) elseif colorkey == "textcolor" then - return lcd.RGB(255,255,255) + return lcd.RGB(255, 255, 255) elseif colorkey == "titlecolor" then - return lcd.RGB(255,255,255) + return lcd.RGB(255, 255, 255) elseif colorkey == "accentcolor" then return lcd.RGB(255, 255, 255) end - -- fallback + return lcd.darkMode() and lcd.RGB(40, 40, 40) or lcd.RGB(240, 240, 240) end --- For arrays like bandColors (returns a resolved RGB array) function utils.resolveThemeColorArray(colorkey, arr) local resolved = {} - if type(arr) == "table" then - for i = 1, #arr do - resolved[i] = utils.resolveThemeColor(colorkey, arr[i]) - end - end + if type(arr) == "table" then for i = 1, #arr do resolved[i] = utils.resolveThemeColor(colorkey, arr[i]) end end return resolved end --- Draws a telemetry value box with colored background, value, title, unit, and flexible padding/alignment. --- --- All color arguments (bgcolor, textcolor, titlecolor) must be resolved numbers (not strings). --- Text sizing can be static (via 'font'/'titlefont') or dynamic if omitted. --- --- @param x number X-coordinate of the box. --- @param y number Y-coordinate of the box. --- @param w number Width of the box. --- @param h number Height of the box. --- @param title string (Optional) Title string (shown above or below the value). --- @param titlepos string (Optional) Title position: "top" or "bottom". Defaults to "top". --- @param titlealign string (Optional) Title alignment: "center", "left", or "right". --- @param titlefont string|number (Optional) Font to use for title (e.g., "FONT_XL"). If nil, uses dynamic sizing. --- @param titlespacing number (Optional) Controls the vertical gap between title and value text. --- @param titlecolor number (Optional) Title text color (resolved LCD color number). --- @param titlepadding number (Optional) Padding for all sides of the title (overridden by the next four if set). --- @param titlepaddingleft number (Optional) Left padding for the title. --- @param titlepaddingright number (Optional) Right padding for the title. --- @param titlepaddingtop number (Optional) Top padding for the title. --- @param titlepaddingbottom number (Optional) Bottom padding for the title. --- @param displayValue string|number Main value to display (pre-formatted for display). --- @param unit string (Optional) Unit string appended to value, if provided. --- @param font string|number (Optional) Font to use for the value (e.g., "FONT_XL"). If nil, uses dynamic sizing. --- @param valuealign string (Optional) Value alignment: "center", "left", or "right". --- @param textcolor number (Optional) Value/main label text color (resolved LCD color number). --- @param valuepadding number (Optional) Padding for all sides of the value (overridden by the next four if set). --- @param valuepaddingleft number (Optional) Left padding for the value. --- @param valuepaddingright number (Optional) Right padding for the value. --- @param valuepaddingtop number (Optional) Top padding for the value. --- @param valuepaddingbottom number (Optional) Bottom padding for the value. --- @param bgcolor number (Optional) Box background color (must be a resolved LCD color number). - -function utils.box( - x, y, w, h, - title, titlepos, titlealign, titlefont, titlespacing, - titlecolor, titlepadding, titlepaddingleft, titlepaddingright, - titlepaddingtop, titlepaddingbottom, - displayValue, unit, font, valuealign, textcolor, - valuepadding, valuepaddingleft, valuepaddingright, - valuepaddingtop, valuepaddingbottom, - bgcolor, - image, imagewidth, imageheight, imagealign -) - -- Padding defaults +function utils.box(x, y, w, h, title, titlepos, titlealign, titlefont, titlespacing, titlecolor, titlepadding, titlepaddingleft, titlepaddingright, titlepaddingtop, titlepaddingbottom, displayValue, unit, font, valuealign, textcolor, valuepadding, valuepaddingleft, valuepaddingright, + valuepaddingtop, valuepaddingbottom, bgcolor, image, imagewidth, imageheight, imagealign) + local DEFAULT_TITLE_PADDING = 0 local DEFAULT_VALUE_PADDING = 6 local DEFAULT_TITLE_SPACING = 6 - titlepaddingleft = titlepaddingleft or titlepadding or DEFAULT_TITLE_PADDING - titlepaddingright = titlepaddingright or titlepadding or DEFAULT_TITLE_PADDING - titlepaddingtop = titlepaddingtop or titlepadding or DEFAULT_TITLE_PADDING - titlepaddingbottom = titlepaddingbottom or titlepadding or DEFAULT_TITLE_PADDING + titlepaddingleft = titlepaddingleft or titlepadding or DEFAULT_TITLE_PADDING + titlepaddingright = titlepaddingright or titlepadding or DEFAULT_TITLE_PADDING + titlepaddingtop = titlepaddingtop or titlepadding or DEFAULT_TITLE_PADDING + titlepaddingbottom = titlepaddingbottom or titlepadding or DEFAULT_TITLE_PADDING - valuepaddingleft = valuepaddingleft or valuepadding or DEFAULT_VALUE_PADDING - valuepaddingright = valuepaddingright or valuepadding or DEFAULT_VALUE_PADDING - valuepaddingtop = valuepaddingtop or valuepadding or DEFAULT_VALUE_PADDING - valuepaddingbottom = valuepaddingbottom or valuepadding or DEFAULT_VALUE_PADDING + valuepaddingleft = valuepaddingleft or valuepadding or DEFAULT_VALUE_PADDING + valuepaddingright = valuepaddingright or valuepadding or DEFAULT_VALUE_PADDING + valuepaddingtop = valuepaddingtop or valuepadding or DEFAULT_VALUE_PADDING + valuepaddingbottom = valuepaddingbottom or valuepadding or DEFAULT_VALUE_PADDING titlespacing = titlespacing or DEFAULT_TITLE_SPACING - -- Draw background if bgcolor then lcd.color(bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Cache fonts if not already - if not fontCache then - fontCache = utils.getFontListsForResolution() - end + if not fontCache then fontCache = utils.getFontListsForResolution() end - -- Title font selection, auto-fit logic local actualTitleFont, tsizeW, tsizeH = nil, 0, 0 if title then local minValueFontH = 9999 @@ -737,7 +527,6 @@ function utils.box( end end - -- Calculate region for value/image local region_vx, region_vy, region_vw, region_vh if title and (titlepos or "top") == "top" then region_vy = y + titlepaddingtop + tsizeH + titlepaddingbottom + titlespacing + valuepaddingtop @@ -752,10 +541,9 @@ function utils.box( region_vx = x + valuepaddingleft region_vw = w - valuepaddingleft - valuepaddingright - -- Draw image if specified (fallback to displayValue) if image then local bitmapPtr = nil - -- If image is a string (path), load it and cache it + if type(image) == "string" and dashx and dashx.utils and dashx.utils.loadImage then imageCache = imageCache or {} local cacheKey = image or "default_image" @@ -765,7 +553,7 @@ function utils.box( imageCache[cacheKey] = bitmapPtr end elseif type(image) == "userdata" then - -- Already a Bitmap object + bitmapPtr = image end @@ -797,11 +585,8 @@ function utils.box( local value_str = tostring(displayValue) .. (unit or "") - -- replace . and % symbols with 'W' for width calculation - -- note. gsub %% is escaping the % symbol as % is the lua pattern escape character - -- multi subs are used because different characters need different replacements local value_str_calc = string.gsub(value_str, "[%%]", "W") - value_str_calc = string.gsub(value_str, "[°]", ".") + value_str_calc = string.gsub(value_str, "[°]", ".") local valueFont, bestW, bestH = FONT_XXS, 0, 0 if font and _G[font] then @@ -813,19 +598,12 @@ function utils.box( for _, tryFont in ipairs(fontCache.value_default) do lcd.font(tryFont) local tW, tH = lcd.getTextSize(value_str_calc) - if tW <= region_vw and tH <= region_vh then - valueFont, bestW, bestH = tryFont, tW, tH - end + if tW <= region_vw and tH <= region_vh then valueFont, bestW, bestH = tryFont, tW, tH end end lcd.font(valueFont) end - -- Optional: vertical fudge for title placement - local fudgeTitle = (title and (titlepos or "top") == "top") - and -math.floor(bestH * 0.15 + 0.5) - or (title and titlepos == "bottom") - and math.floor(bestH * 0.15 + 0.5) - or 0 + local fudgeTitle = (title and (titlepos or "top") == "top") and -math.floor(bestH * 0.15 + 0.5) or (title and titlepos == "bottom") and math.floor(bestH * 0.15 + 0.5) or 0 local sy = region_vy + ((region_vh - bestH) / 2) + fudgeTitle local align = (valuealign or "center"):lower() @@ -835,19 +613,16 @@ function utils.box( elseif align == "right" then sx = region_vx + region_vw - bestW else - sx = region_vx + (region_vw - bestW) / 2 + sx = region_vx + (region_vw - bestW) / 2 end lcd.color(textcolor) lcd.drawText(sx, sy, value_str) end - -- Draw title text (centered, at top or bottom) if title then lcd.font(actualTitleFont) local region_tw = w - titlepaddingleft - titlepaddingright - local sy = (titlepos or "top") == "bottom" - and (y + h - titlepaddingbottom - tsizeH) - or (y + titlepaddingtop) + local sy = (titlepos or "top") == "bottom" and (y + h - titlepaddingbottom - tsizeH) or (y + titlepaddingtop) local align = (titlealign or "center"):lower() local sx if align == "left" then @@ -862,27 +637,13 @@ function utils.box( end end ---- Resolves a color (typically textcolor or fillcolor) for a value using flexible threshold logic. --- If the box table includes a 'thresholds' array: - -- For string values, returns the colorKey (e.g. textcolor/fillcolor) for the threshold whose value exactly matches. - -- For numeric values, returns the colorKey for the first threshold whose value is greater than or equal to the given value (less-than-or-equal logic). - -- For function-valued thresholds, calls the function with (box, value) to resolve the thresholdValue. - -- Falls back to the colorKey in box, or a theme default, if no threshold matches. --- @param value number|string -- The value to evaluate against thresholds. --- @param box table -- The widget's config table (may contain thresholds, textcolor, fillcolor, etc.) --- @param colorKey string -- The key to look up in thresholds and box (e.g. "textcolor" or "fillcolor"). --- @param fallbackThemeKey string -- The theme key to use if no color is found (e.g. "textcolor", "fillcolor"). --- @return number -- The LCD color to use for rendering. - function utils.resolveThresholdColor(value, box, colorKey, fallbackThemeKey, thresholdsOverride) local color = utils.resolveThemeColor(fallbackThemeKey, utils.getParam(box, colorKey)) local thresholds = thresholdsOverride or utils.getParam(box, "thresholds") if thresholds and value ~= nil then for _, t in ipairs(thresholds) do local thresholdValue = t.value - if type(thresholdValue) == "function" then - thresholdValue = thresholdValue(box, value) - end + if type(thresholdValue) == "function" then thresholdValue = thresholdValue(box, value) end if type(value) == "string" and thresholdValue == value and t[colorKey] then color = utils.resolveThemeColor(colorKey, t[colorKey]) @@ -896,26 +657,10 @@ function utils.resolveThresholdColor(value, box, colorKey, fallbackThemeKey, thr return color end ---- Transforms and formats a numeric value for display, applying any configured transform and decimals. --- --- This function checks the given `box` for a `transform` property (either a string or a function). --- If provided, it applies the transformation to the input value: --- - "floor": Rounds down to nearest integer. --- - "ceil": Rounds up to nearest integer. --- - "round": Rounds to nearest integer. --- - function: Calls the function with the value and uses the result. --- Next, if a `decimals` property is present in the box, it formats the value to the specified number --- of decimal places as a string. If neither is set, the raw value is returned as a string. --- --- @param value number The raw numeric value to be transformed and formatted. --- @param box table The box configuration table, containing optional `transform` and `decimals`. --- @return string The transformed and formatted value, ready for display. - function utils.transformValue(value, box) local transform = utils.getParam(box, "transform") - -- Apply transformation if configured if transform then if type(transform) == "function" then value = transform(value) @@ -928,7 +673,7 @@ function utils.transformValue(value, box) end end local decimals = utils.getParam(box, "decimals") - -- Apply decimal formatting if configured + if decimals ~= nil and value ~= nil then value = string.format("%." .. decimals .. "f", value) elseif value ~= nil then @@ -937,9 +682,6 @@ function utils.transformValue(value, box) return value end ---- Sets the background color of the LCD based on the current theme (dark or light mode). --- Determines the window size, selects an appropriate background color depending on whether --- dark mode is enabled, and fills the entire window with the selected color. function utils.setBackgroundColourBasedOnTheme() local w, h = lcd.getWindowSize() if lcd.darkMode() then @@ -950,20 +692,8 @@ function utils.setBackgroundColourBasedOnTheme() lcd.drawFilledRectangle(0, 0, w, h) end ---- Retrieves a parameter from the given `box` table by `key`. --- If the value associated with `key` is a function, it calls the function with `box`, `key`, and any additional arguments, and returns the result. --- Otherwise, it returns the value directly. --- @param box table: The table from which to retrieve the parameter. --- @param key any: The key to look up in the table. --- @param ... any: Additional arguments to pass if the value is a function. --- @return any: The value associated with `key`, or the result of calling the function if the value is a function. function utils.getParam(box, key, ...) - local SKIP_CALL_KEYS = { - transform = true, - thresholds = true, - value = true, - -- add more keys here if needed - } + local SKIP_CALL_KEYS = {transform = true, thresholds = true, value = true} local v = box[key] if type(v) == "function" and not SKIP_CALL_KEYS[key] then @@ -973,20 +703,10 @@ function utils.getParam(box, key, ...) end end ---- Applies offset values from a given box table to the provided x and y coordinates. --- The function retrieves "offsetx" and "offsety" parameters from the box using utils.getParam. --- If the parameters are not present, it defaults to 0. --- @param x number: The original x coordinate. --- @param y number: The original y coordinate. --- @param box table: A table potentially containing "offsetx" and "offsety" values. --- @return number, number: The x and y coordinates after applying the offsets. function utils.applyOffset(x, y, box) local ox = utils.getParam(box, "offsetx") or 0 local oy = utils.getParam(box, "offsety") or 0 return x + ox, y + oy end - - - return utils diff --git a/scripts/dashx/widgets/dashboard/objects/dial.lua b/scripts/dashx/widgets/dashboard/objects/dial.lua index e33883d..bb3e444 100644 --- a/scripts/dashx/widgets/dashboard/objects/dial.lua +++ b/scripts/dashx/widgets/dashboard/objects/dial.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -14,25 +19,16 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now end @@ -44,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/dial/image.lua b/scripts/dashx/widgets/dashboard/objects/dial/image.lua index 0bf5a69..c0b5029 100644 --- a/scripts/dashx/widgets/dashboard/objects/dial/image.lua +++ b/scripts/dashx/widgets/dashboard/objects/dial/image.lua @@ -1,50 +1,9 @@ -local dashx = require("dashx") --[[ - Dial Image Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - - -- title parameters - title : string -- (Optional) Title text - titlealign : string -- (Optional) "center", "left", "right" - titlefont : font -- (Optional) Title font (e.g., font_l, font_xl) - titlespacing : number -- (Optional) Gap below title - titlecolor : color -- (Optional) Title text color - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) - titlepaddingright : number -- (Optional) - titlepaddingtop : number -- (Optional) - titlepaddingbottom : number -- (Optional) - - -- value / source parameters - value : any -- (Optional) Static value to display if telemetry is not present - source : string -- Telemetry sensor name - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", etc.) - decimals : number -- (Optional) Decimal precision - novalue : string -- (Optional) Text if telemetry is missing (default: "-") - unit : string -- (Optional) Unit label ("" hides unit) - font : font -- (Optional) Value font (e.g. font_l) - valuealign : string -- (Optional) "center", "left", "right" - textcolor : color -- (Optional) Text color - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) - valuepaddingright : number -- (Optional) - valuepaddingtop : number -- (Optional) - valuepaddingbottom : number -- (Optional) - - -- dial image & needle styling - dial : string|number|function -- Dial image selector (used for asset path) - scalefactor : number -- (Optional) Image scale multiplier (default: 0.4) - needlecolor : color -- (Optional) Needle color (default: theme) - needlehubcolor : color -- (Optional) Hub color (default: theme) - needlethickness : number -- (Optional) Needle width in pixels (default: 3) - needlehubsize : number -- (Optional) Hub circle radius in pixels (default: needle thickness + 2) - needlestartangle : number -- (Optional) Needle starting angle in degrees (default: 135) - needlesweepangle : number -- (Optional) Needle sweep angle in degrees (default: 270) - - bgcolor : color -- Widget background color (default: theme fallback) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -53,7 +12,7 @@ local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -91,13 +50,11 @@ local function loadDialPanelCached(dialId) return dashx.session.dialImageCache[key] end -local function calDialAngle(percent, startAngle, sweepAngle) - return (startAngle or 315) + (sweepAngle or 270) * (percent or 0) / 100 -end +local function calDialAngle(percent, startAngle, sweepAngle) return (startAngle or 315) + (sweepAngle or 270) * (percent or 0) / 100 end local function computeDrawArea(img, x, y, w, h, scalefactor) if not img then return x, y, w, h end - + local iw, ih = img:width(), img:height() local scale = math.max(w / iw, h / ih) * (scalefactor or 1.0) local drawW = iw * scale @@ -111,19 +68,15 @@ function render.wakeup(box) local telemetry = dashx.tasks.telemetry - -- Value extraction local source = getParam(box, "source") local value, _, dynamicUnit - if telemetry and source then - value, _, dynamicUnit = telemetry.getSensor(source) - end + if telemetry and source then value, _, dynamicUnit = telemetry.getSensor(source) end - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -132,89 +85,71 @@ function render.wakeup(box) unit = "" end - -- Min/Max boundaries local min = getParam(box, "min") or 0 local max = getParam(box, "max") or 100 - -- Percent calculation local percent = 0 - if value and max ~= min then - percent = math.max(0, math.min(1, (value - min) / (max - min))) - end + if value and max ~= min then percent = math.max(0, math.min(1, (value - min) / (max - min))) end - -- Transform and decimals (if required) local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- ... style loading indicator if value == nil then local maxDots = 3 - if box._dotCount == nil then - box._dotCount = 0 - end + if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) - if displayValue == "" then - displayValue = "." - end + if displayValue == "" then displayValue = "." end unit = nil end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end - - -- Set box.value so dashboard/dirty can track change for redraws + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end + box._currentDisplayValue = value box._cache = { - value = value, - displayvalue = displayValue, - percent = percent * 100, - unit = unit, - min = min, - max = max, - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos"), - titlefont = getParam(box, "titlefont"), - titlealign = getParam(box, "titlealign"), - titlespacing = getParam(box, "titlespacing") or 0, - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), - titlepaddingbottom = getParam(box, "titlepaddingbottom"), - font = getParam(box, "font") or "FONT_M", - textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), - valuepaddingbottom = getParam(box, "valuepaddingbottom"), - dialid = getParam(box, "dial"), - panelimg = loadDialPanelCached(getParam(box, "dial")), - scalefactor = tonumber(getParam(box, "scalefactor")) or 0.4, - needlecolor = resolveThemeColor("needlecolor", getParam(box, "needlecolor")), - hubcolor = resolveThemeColor("needlehubcolor", getParam(box, "needlehubcolor")), - needlethickness = getParam(box, "needlethickness") or 3, - hubradius = getParam(box, "needlehubsize") or 5, - needlestartangle = getParam(box, "needlestartangle") or 135, - sweep = getParam(box, "needlesweepangle") or 270, - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + value = value, + displayvalue = displayValue, + percent = percent * 100, + unit = unit, + min = min, + max = max, + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos"), + titlefont = getParam(box, "titlefont"), + titlealign = getParam(box, "titlealign"), + titlespacing = getParam(box, "titlespacing") or 0, + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), + titlepaddingbottom = getParam(box, "titlepaddingbottom"), + font = getParam(box, "font") or "FONT_M", + textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), + valuepaddingbottom = getParam(box, "valuepaddingbottom"), + dialid = getParam(box, "dial"), + panelimg = loadDialPanelCached(getParam(box, "dial")), + scalefactor = tonumber(getParam(box, "scalefactor")) or 0.4, + needlecolor = resolveThemeColor("needlecolor", getParam(box, "needlecolor")), + hubcolor = resolveThemeColor("needlehubcolor", getParam(box, "needlehubcolor")), + needlethickness = getParam(box, "needlethickness") or 3, + hubradius = getParam(box, "needlehubsize") or 5, + needlestartangle = getParam(box, "needlestartangle") or 135, + sweep = getParam(box, "needlesweepangle") or 270, + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) } end - function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Title layout height calculation local titleHeight = 0 if c.title then lcd.font(_G[c.titlefont] or FONT_XS) @@ -222,7 +157,6 @@ function render.paint(x, y, w, h, box) titleHeight = (th or 0) + (c.titlespacing or 0) + (c.titlepaddingtop or 0) + (c.titlepaddingbottom or 0) end - -- Dial image region: based on title position local imgRegionY, imgRegionH if c.titlepos == "top" then imgRegionY = y + titleHeight @@ -235,45 +169,30 @@ function render.paint(x, y, w, h, box) imgRegionH = h end - -- Widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Draw panel image (dial) local drawX, drawY, drawW, drawH = x, y, w, h if c.panelimg then drawX, drawY, drawW, drawH = computeDrawArea(c.panelimg, x, imgRegionY, w, imgRegionH, c.scalefactor) lcd.drawBitmap(drawX, drawY, c.panelimg, drawW, drawH) end - -- Draw needle and hub if value exists if c.value ~= nil then local angle = calDialAngle(c.percent, c.needlestartangle, c.sweep) local cx = drawX + drawW / 2 local cy = drawY + drawH / 2 local radius = math.min(drawW, drawH) * 0.40 local needleLength = radius - 6 - if c.percent and type(c.percent) == "number" and not (c.percent ~= c.percent) then - utils.drawBarNeedle(cx, cy, needleLength, c.needlethickness, angle, c.needlecolor) - end + if c.percent and type(c.percent) == "number" and not (c.percent ~= c.percent) then utils.drawBarNeedle(cx, cy, needleLength, c.needlethickness, angle, c.needlecolor) end lcd.color(c.hubcolor) lcd.drawFilledCircle(cx, cy, c.hubradius) end - -- Draw title and value text - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayvalue, c.unit, - c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayvalue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/dial/rainbow.lua b/scripts/dashx/widgets/dashboard/objects/dial/rainbow.lua index f05515e..fbe2f66 100644 --- a/scripts/dashx/widgets/dashboard/objects/dial/rainbow.lua +++ b/scripts/dashx/widgets/dashboard/objects/dial/rainbow.lua @@ -1,59 +1,9 @@ -local dashx = require("dashx") --[[ - Rainbow Gauge Widget - - Configurable Parameters (box table fields): - ------------------------------------------- - - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - - -- title parameters - title : string -- (Optional) Title text - titlealign : string -- (Optional) "center", "left", "right" - titlefont : font -- (Optional) Title font (e.g., font_l, font_xl) - titlespacing : number -- (Optional) Gap below title - titlecolor : color -- (Optional) Title text color - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) - titlepaddingright : number -- (Optional) - titlepaddingtop : number -- (Optional) - titlepaddingbottom : number -- (Optional) - - -- value / source parameters - value : any -- (Optional) Static value to display if telemetry is not present - showvalue : bool -- (Optional) If false, hides the main value text (default true) - source : string -- Telemetry sensor name - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", etc.) - decimals : number -- (Optional) Decimal precision - novalue : string -- (Optional) Text if telemetry is missing (default: "-") - unit : string -- (Optional) Unit label ("" hides unit) - font : font -- (Optional) Value font (e.g., font_l) - valuealign : string -- (Optional) "center", "left", "right" - textcolor : color -- (Optional) Text color - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) - valuepaddingright : number -- (Optional) - valuepaddingtop : number -- (Optional) - valuepaddingbottom : number -- (Optional) - - -- arc band parameters - bandlabels : table -- List of labels for each band (e.g. {"Low", "Med", "High"}) - bandcolors : table -- List of band colors (e.g. {lcd.RGB(180,50,50), lcd.RGB(...)}) - bandlabeloffset : number -- (Optional) Outward for left/right labels (default 18) - bandlabeloffsettop : number -- (Optional) Down from the arc edge for the top label (default 8) - bandlabelfont : font -- (Optional) Font for band labels (e.g. FONT_XS, FONT_S). Defaults to FONT_XS - - -- appearance / theming - bgcolor : color -- (Optional) Widget background color - fillbgcolor : color -- (Optional) Arc background color (optional) - titlecolor : color -- (Optional) Title text color fallback - - -- needle styling - accentcolor : color -- (Optional) Needle and hub color - needlethickness : number -- (Optional) Needle width (default: 5) - needlehubsize : number -- (Optional) Needle hub circle radius (default: 7) - -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -65,7 +15,7 @@ local resolveThemeColorArray = utils.resolveThemeColorArray local lastDisplayValue = nil function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -79,53 +29,43 @@ function render.dirty(box) return false end --- Draw a rainbow arc using drawAnnulusSector for each colored band local function drawRainbowArc(cx, cy, radius, thickness, startAngle, endAngle, colors) local inner = math.max(1, radius - thickness) local outer = radius local segmentCount = #colors if segmentCount == 0 then return end - -- Normalize and unwrap angles startAngle = startAngle % 360 endAngle = endAngle % 360 - if endAngle <= startAngle then - endAngle = endAngle + 360 - end + if endAngle <= startAngle then endAngle = endAngle + 360 end local angleSweep = endAngle - startAngle local anglePerSegment = angleSweep / segmentCount for i, color in ipairs(colors) do local segStart = startAngle + (i - 1) * anglePerSegment - local segEnd = startAngle + i * anglePerSegment + local segEnd = startAngle + i * anglePerSegment lcd.color(color) lcd.drawAnnulusSector(cx, cy, inner, outer, segStart, segEnd) end end -local function calDialAngle(percent, startAngle, sweepAngle) - return (startAngle or 135) + (sweepAngle or 270) * (percent or 0) -end +local function calDialAngle(percent, startAngle, sweepAngle) return (startAngle or 135) + (sweepAngle or 270) * (percent or 0) end function render.wakeup(box) local telemetry = dashx.tasks.telemetry - - -- Value extraction + local source = getParam(box, "source") local value, _, dynamicUnit - if telemetry and source then - value, _, dynamicUnit = telemetry.getSensor(source) - end + if telemetry and source then value, _, dynamicUnit = telemetry.getSensor(source) end - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -134,7 +74,6 @@ function render.wakeup(box) unit = "" end - -- Calculate percent fill for the gauge (clamped 0-1) local min = getParam(box, "min") or 0 local max = getParam(box, "max") or 100 local percent = 0 @@ -143,77 +82,64 @@ function render.wakeup(box) percent = math.max(0, math.min(1, percent)) end - -- Transform and decimals (if required) local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- ... style loading indicator if value == nil then local maxDots = 3 - if box._dotCount == nil then - box._dotCount = 0 - end + if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) - if displayValue == "" then - displayValue = "." - end + if displayValue == "" then displayValue = "." end unit = nil end - -- Optional: local showvalue local showvalue = getParam(box, "showvalue") if showvalue == nil then showvalue = true end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end - - -- Caching values + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end + box._currentDisplayValue = value box._cache = { - value = value, - displayValue = displayValue, - percent = percent, - unit = unit, - min = min, - max = max, - showvalue = showvalue, - titlepos = "bottom", - font = getParam(box, "font") or "FONT_M", - textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), - fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), - title = getParam(box, "title"), - titlefont = getParam(box, "titlefont"), - titlealign = getParam(box, "titlealign"), - titlespacing = getParam(box, "titlespacing") or 0, - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), - titlepaddingbottom = getParam(box, "titlepaddingbottom"), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), - valuepaddingbottom = getParam(box, "valuepaddingbottom"), - bandlabeloffset = getParam(box, "bandlabeloffset") or 14, - bandlabeloffsettop = getParam(box, "bandlabeloffsettop") or 8, - bandlabelfont = getParam(box, "bandlabelfont") or "FONT_XS", - bandlabels = getParam(box, "bandlabels") or { "Low", "Med", "High" }, - bandcolors = resolveThemeColorArray("fillcolor", getParam(box, "bandcolors") or {"red", "orange", "green"}), - needlethickness = getParam(box, "needlethickness") or 5, - needlehubsize = getParam(box, "needlehubsize") or 7, - needlestartangle = getParam(box, "needlestartangle") or 150, - needlesweepangle = getParam(box, "needlesweepangle") or 240, - accentcolor = resolveThemeColor("accentcolor", getParam(box, "accentcolor")), + value = value, + displayValue = displayValue, + percent = percent, + unit = unit, + min = min, + max = max, + showvalue = showvalue, + titlepos = "bottom", + font = getParam(box, "font") or "FONT_M", + textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), + fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + title = getParam(box, "title"), + titlefont = getParam(box, "titlefont"), + titlealign = getParam(box, "titlealign"), + titlespacing = getParam(box, "titlespacing") or 0, + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), + titlepaddingbottom = getParam(box, "titlepaddingbottom"), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), + valuepaddingbottom = getParam(box, "valuepaddingbottom"), + bandlabeloffset = getParam(box, "bandlabeloffset") or 14, + bandlabeloffsettop = getParam(box, "bandlabeloffsettop") or 8, + bandlabelfont = getParam(box, "bandlabelfont") or "FONT_XS", + bandlabels = getParam(box, "bandlabels") or {"Low", "Med", "High"}, + bandcolors = resolveThemeColorArray("fillcolor", getParam(box, "bandcolors") or {"red", "orange", "green"}), + needlethickness = getParam(box, "needlethickness") or 5, + needlehubsize = getParam(box, "needlehubsize") or 7, + needlestartangle = getParam(box, "needlestartangle") or 150, + needlesweepangle = getParam(box, "needlesweepangle") or 240, + accentcolor = resolveThemeColor("accentcolor", getParam(box, "accentcolor")) } end @@ -221,11 +147,9 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Calculate space above for band label subtext (single line, e.g. "Low/Med/High") lcd.font(_G[c.bandlabelfont] or FONT_XS) local subtextHeight = select(2, lcd.getTextSize("Med")) + 2 - -- Calculate title height and allocate vertical regions for title, subtext, and arc local titleHeight = 0 if c.title then lcd.font(_G[c.titlefont] or FONT_XS) @@ -233,7 +157,6 @@ function render.paint(x, y, w, h, box) titleHeight = (th or 0) + (c.titlespacing or 0) + (c.titlepaddingtop or 0) + (c.titlepaddingbottom or 0) end - -- Calculate available arc geometry to maximize arc size within widget region local arcRegionY = y + subtextHeight local arcRegionH = h - subtextHeight - titleHeight local arcMargin = 2 @@ -245,24 +168,18 @@ function render.paint(x, y, w, h, box) local cx = x + w / 2 local cy = arcRegionY + arcRegionH / 2 + 15 - -- Draw widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Draw colored arc bands local bandCount = #c.bandlabels local startAngle = 240 - local endAngle = 120 - if bandCount > 0 and c.bandcolors then - drawRainbowArc(cx, cy, radius, thickness, startAngle, endAngle, c.bandcolors) - end + local endAngle = 120 + if bandCount > 0 and c.bandcolors then drawRainbowArc(cx, cy, radius, thickness, startAngle, endAngle, c.bandcolors) end - -- Needle hub vertical offset local needleHubYOffset = 6 - -- Draw needle with its own sweep if c.percent then local angleDeg = calDialAngle(c.percent, c.needlestartangle or 150, c.needlesweepangle or 240) local needleLen = radius @@ -272,11 +189,9 @@ function render.paint(x, y, w, h, box) lcd.drawFilledCircle(cx, cy_needle, c.needlehubsize) end - -- Draw band labels at the top of the arc local sweep = (endAngle - startAngle + 360) % 360 lcd.font(_G[c.bandlabelfont] or FONT_XS) - -- Clockwise label shift in degrees local angleOffset = -30 for i = 1, bandCount do @@ -302,19 +217,8 @@ function render.paint(x, y, w, h, box) end end - -- Draw value and title using standard layout helper - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.showvalue ~= false and c.displayValue or nil, - c.showvalue ~= false and c.unit or nil, - c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.showvalue ~= false and c.displayValue or nil, c.showvalue ~= false and c.unit or nil, c.font, + c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/func.lua b/scripts/dashx/widgets/dashboard/objects/func.lua index 2800cc3..6bd7f07 100644 --- a/scripts/dashx/widgets/dashboard/objects/func.lua +++ b/scripts/dashx/widgets/dashboard/objects/func.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -14,25 +19,16 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now end @@ -44,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/func/func.lua b/scripts/dashx/widgets/dashboard/objects/func/func.lua index c8821c7..3433f3d 100644 --- a/scripts/dashx/widgets/dashboard/objects/func/func.lua +++ b/scripts/dashx/widgets/dashboard/objects/func/func.lua @@ -1,23 +1,15 @@ -local dashx = require("dashx") --[[ - Custom Function Widget - Configurable Arguments (box table keys): - ---------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - wakeup : function -- Custom wakeup function, called with (box, telemetry), should return a table to cache - paint : function -- Custom paint function, called with (x, y, w, h, box, cache, telemetry) + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - -- Note: This widget does not process colors, layout, or padding. All rendering and caching logic must be handled in the user's custom functions. -]] +local dashx = require("dashx") local render = {} local utils = dashx.widgets.dashboard.utils -function render.dirty(box) - return true -- Always dirty, since the user-defined functions can change at any time -end - +function render.dirty(box) return true end function render.wakeup(box) @@ -33,9 +25,7 @@ end function render.paint(x, y, w, h, box, telemetry) x, y = utils.applyOffset(x, y, box) local v = box.paint - if type(v) == "function" then - v(x, y, w, h, box, box._cache, telemetry) - end + if type(v) == "function" then v(x, y, w, h, box, box._cache, telemetry) end end return render diff --git a/scripts/dashx/widgets/dashboard/objects/gauge.lua b/scripts/dashx/widgets/dashboard/objects/gauge.lua index 1d31508..7b41ec5 100644 --- a/scripts/dashx/widgets/dashboard/objects/gauge.lua +++ b/scripts/dashx/widgets/dashboard/objects/gauge.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -14,25 +19,16 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now end @@ -44,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/gauge/arc.lua b/scripts/dashx/widgets/dashboard/objects/gauge/arc.lua index 452df2c..df9e7ca 100644 --- a/scripts/dashx/widgets/dashboard/objects/gauge/arc.lua +++ b/scripts/dashx/widgets/dashboard/objects/gauge/arc.lua @@ -1,60 +1,9 @@ -local dashx = require("dashx") --[[ - Arc Gauge Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - -- Title parameters - title : string -- (Optional) Title text - titlepos : string -- (Optional) If `title` is present but `titlepos` is not set, title is placed at the top by default. - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) - titlespacing : number -- (Optional) Vertical gap between title and value - titlecolor : color -- (Optional) Title text color (theme/text fallback) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - - -- Value/Source parameters - value : any -- (Optional) Static value to display if telemetry is not present - source : string -- Telemetry sensor source name (e.g., "voltage", "current") - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) - decimals : number -- (Optional) Number of decimal places for numeric display - thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - - -- Maxval parameters - arcmax : bool -- (Optional) Draw arcmac gauge within the outer arc (false by default) - maxfont : font -- (Optional) Font for max value label (e.g., FONT_XS, FONT_S, FONT_M, default: FONT_S) - maxtextcolor : color -- (Optional) Max text color (theme/text fallback) - maxpadding : number -- (Optional) Padding (Y-offset) below arc center for max value label (default: 0) - maxpaddingleft : number -- (Optional) Additional X-offset for max label (default: 0) - maxpaddingtop : number -- (Optional) Additional Y-offset for max label (default: 0) - - -- Appearance/Theming - bgcolor : color -- (Optional) Widget background color (theme fallback) - fillbgcolor : color -- (Optional) Arc background color (theme fallback) - fillcolor : color -- (Optional) Arc foreground color (theme fallback) - maxprefix : string -- (Optional) Prefix for max value label (default: "+") - - -- Arc Geometry/Advanced - min : number -- (Optional) Minimum value of the arc (default: 0) - max : number -- (Optional) Maximum value of the arc (default: 100) - thickness : number -- (Optional) Arc thickness in pixels - gaugepadding : number -- (Optional) Horizontal-only padding applied to arc radius (shrinks arc from left/right only) - gaugepaddingbottom : number -- (Optional) Extra space added below arc region, pushing arc upward (vertical only) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -65,7 +14,7 @@ local resolveThresholdColor = utils.resolveThresholdColor local lastDisplayValue = nil function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -79,18 +28,14 @@ function render.dirty(box) return false end --- Arc drawing function local function drawArc(cx, cy, radius, thickness, startAngle, endAngle, color) lcd.color(color) local outer = radius local inner = math.max(1, radius - (thickness or 6)) - -- Normalize angles startAngle = startAngle % 360 endAngle = endAngle % 360 - if endAngle <= startAngle then - endAngle = endAngle + 360 - end + if endAngle <= startAngle then endAngle = endAngle + 360 end local sweep = endAngle - startAngle if sweep <= 180 then @@ -106,14 +51,10 @@ function render.wakeup(box) local telemetry = dashx.tasks.telemetry - -- Value extraction local source = getParam(box, "source") local value, _, dynamicUnit - if telemetry and source then - value, _, dynamicUnit = telemetry.getSensor(source) - end + if telemetry and source then value, _, dynamicUnit = telemetry.getSensor(source) end - -- Optionally cache and calculate max value for max arc local arcmax = getParam(box, "arcmax") == true local maxval = nil if arcmax and source then @@ -123,12 +64,11 @@ function render.wakeup(box) maxval = currentMax or prevMax end - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -137,29 +77,22 @@ function render.wakeup(box) unit = "" end - -- Resolve arc min/max local min = getParam(box, "min") or 0 local max = getParam(box, "max") or 100 - -- Only convert to Fahrenheit or Ft if localization is changed local isFahrenheit = unit and unit:match("F$") ~= nil local isFeet = unit and unit:lower():match("ft$") ~= nil if isFahrenheit then min = min * 9 / 5 + 32 max = max * 9 / 5 + 32 - if arcmax and maxval then - maxval = maxval * 9 / 5 + 32 - end + if arcmax and maxval then maxval = maxval * 9 / 5 + 32 end elseif isFeet then min = min * 3.28084 max = max * 3.28084 - if arcmax and maxval then - maxval = maxval * 3.28084 - end + if arcmax and maxval then maxval = maxval * 3.28084 end end - -- Clone and convert threshold values to match display units if using Fahrenheit or feet local thresholds = getParam(box, "thresholds") local adjustedThresholds = thresholds @@ -179,7 +112,6 @@ function render.wakeup(box) end end - -- Calculate percent fill for the gauge (clamped 0-1) local percent = 0 if value and max ~= min then percent = (value - min) / (max - min) @@ -191,84 +123,69 @@ function render.wakeup(box) maxPercent = math.max(0, math.min(1, maxPercent)) end - -- Transform and decimals (if required) local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- Transform and decimals (if required - for arcmax) local displayMaxValue = nil - if arcmax and maxval ~= nil then - displayMaxValue = utils.transformValue(maxval, box) - end + if arcmax and maxval ~= nil then displayMaxValue = utils.transformValue(maxval, box) end - -- ... style loading indicator if value == nil then local maxDots = 3 - if box._dotCount == nil then - box._dotCount = 0 - end + if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) - if displayValue == "" then - displayValue = "." - end + if displayValue == "" then displayValue = "." end unit = nil end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end - -- Set box.value so dashboard/dirty can track change for redraws box._currentDisplayValue = value box._cache = { - value = value, - maxval = maxval, - displayValue = displayValue, - displayMaxValue = displayMaxValue, - arcmax = arcmax, - min = min, - max = max, - percent = percent, - maxPercent = maxPercent, - unit = unit, - textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor", adjustedThresholds), - maxtextcolor = resolveThresholdColor(maxval, box, "maxtextcolor","textcolor", adjustedThresholds), - fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor", adjustedThresholds), - maxfillcolor = resolveThresholdColor(maxval, box, "fillcolor", "fillcolor", adjustedThresholds), - fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos") or (getParam(box, "title") and "top"), - titlealign = getParam(box, "titlealign"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing") or 0, - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), + value = value, + maxval = maxval, + displayValue = displayValue, + displayMaxValue = displayMaxValue, + arcmax = arcmax, + min = min, + max = max, + percent = percent, + maxPercent = maxPercent, + unit = unit, + textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor", adjustedThresholds), + maxtextcolor = resolveThresholdColor(maxval, box, "maxtextcolor", "textcolor", adjustedThresholds), + fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor", adjustedThresholds), + maxfillcolor = resolveThresholdColor(maxval, box, "fillcolor", "fillcolor", adjustedThresholds), + fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos") or (getParam(box, "title") and "top"), + titlealign = getParam(box, "titlealign"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing") or 0, + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), titlepaddingbottom = getParam(box, "titlepaddingbottom"), - font = getParam(box, "font") or "FONT_M", - maxfont = getParam(box, "maxfont") or "FONT_S", - decimals = getParam(box, "decimals"), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop") or 18, + font = getParam(box, "font") or "FONT_M", + maxfont = getParam(box, "maxfont") or "FONT_S", + decimals = getParam(box, "decimals"), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop") or 18, valuepaddingbottom = getParam(box, "valuepaddingbottom"), - thickness = getParam(box, "thickness"), - maxprefix = getParam(box, "maxprefix") or "+", - maxpadding = getParam(box, "maxpadding") or 0, - maxpaddingleft = getParam(box, "maxpaddingleft") or 0, - maxpaddingtop = getParam(box, "maxpaddingtop") or 0, - gaugepadding = getParam(box, "gaugepadding") or 0, - gaugepaddingbottom = getParam(box, "gaugepaddingbottom") or 0, + thickness = getParam(box, "thickness"), + maxprefix = getParam(box, "maxprefix") or "+", + maxpadding = getParam(box, "maxpadding") or 0, + maxpaddingleft = getParam(box, "maxpaddingleft") or 0, + maxpaddingtop = getParam(box, "maxpaddingtop") or 0, + gaugepadding = getParam(box, "gaugepadding") or 0, + gaugepaddingbottom = getParam(box, "gaugepaddingbottom") or 0 } end @@ -276,7 +193,6 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Title/Arc layout calculation local titleHeight = 0 if c.title then lcd.font(_G[c.titlefont] or FONT_XS) @@ -284,7 +200,6 @@ function render.paint(x, y, w, h, box) titleHeight = (th or 0) + (c.titlespacing or 0) + (c.titlepaddingtop or 0) + (c.titlepaddingbottom or 0) end - -- Arc region: based on title position local arcRegionY, arcRegionH, cy, radius local thickness, maxRadius @@ -307,27 +222,22 @@ function render.paint(x, y, w, h, box) maxRadius = (arcRegionH / 2) - (thickness / 2) radius = math.min((w / 2) - gaugepadding, maxRadius + 8) - -- Widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Arc layout local cx = x + w / 2 local startAngle = 225 local endAngle = (startAngle + 270) % 360 - -- Draw background arc (full 270° from 225 to 135) drawArc(cx, cy, radius, thickness, startAngle, endAngle, c.fillbgcolor) - -- Foreground arc based on percent fill if c.percent and c.percent > 0 then local valueEndAngle = (startAngle + 270 * c.percent) % 360 drawArc(cx, cy, radius, thickness, startAngle, valueEndAngle, c.fillcolor) end - -- Max value arc if enabled if c.arcmax and c.maxval and c.max ~= c.min and c.maxPercent > 0 then local innerRadius = radius * 0.74 local innerThickness = thickness * 0.8 @@ -335,30 +245,16 @@ function render.paint(x, y, w, h, box) drawArc(cx, cy, innerRadius, innerThickness, startAngle, maxEndAngle, c.maxfillcolor) end - -- Draw title and value - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) - - -- Draw max value label if enabled + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) + if c.arcmax and c.maxval then local maxStr = tostring(c.maxprefix or "") .. (c.displayMaxValue or c.maxval) .. (c.unit or "") local maxTextColor = c.maxtextcolor or c.textcolor lcd.color(maxTextColor) lcd.font(_G[c.maxfont] or FONT_S) local tw2, th2 = lcd.getTextSize(maxStr) - lcd.drawText( - cx - tw2 / 2 + (c.maxpaddingleft or 0), - cy + radius * 0.25 + (c.maxpadding or 0) + (c.maxpaddingtop or 0), - maxStr - ) + lcd.drawText(cx - tw2 / 2 + (c.maxpaddingleft or 0), cy + radius * 0.25 + (c.maxpadding or 0) + (c.maxpaddingtop or 0), maxStr) end end diff --git a/scripts/dashx/widgets/dashboard/objects/gauge/bar.lua b/scripts/dashx/widgets/dashboard/objects/gauge/bar.lua index 3bb2d4c..6d0bd8e 100644 --- a/scripts/dashx/widgets/dashboard/objects/gauge/bar.lua +++ b/scripts/dashx/widgets/dashboard/objects/gauge/bar.lua @@ -1,86 +1,9 @@ -local dashx = require("dashx") --[[ - Bar Gauge Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - -- Title/label - title : string -- (Optional) Title text - titlepos : string -- (Optional) "top" or "bottom" - titlealign : string -- (Optional) "center", "left", "right" - titlefont : font -- (Optional) Title font (e.g., FONT_L) - titlespacing : number -- (Optional) Vertical gap below title - titlecolor : color -- (Optional) Title text color (theme/text fallback) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) - titlepaddingright : number -- (Optional) - titlepaddingtop : number -- (Optional) - titlepaddingbottom : number -- (Optional) - - -- Value/source - value : any -- (Optional) Static value to display if no telemetry - hidevalue : bool -- (Optional) If true, do not display the value text (default: false; value is shown) - source : string -- (Optional) Telemetry sensor source name - transform : string|function|number -- (Optional) Value transformation - decimals : number -- (Optional) Number of decimal places for display - thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} - novalue : string -- (Optional) Text shown if value missing (default: "-") - unit : string -- (Optional) Unit label, "" to hide, or nil to auto-resolve - font : font -- (Optional) Value font (e.g., FONT_L) - valuealign : string -- (Optional) "center", "left", "right" - textcolor : color -- (Optional) Value text color (theme/text fallback) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) - valuepaddingright : number -- (Optional) - valuepaddingtop : number -- (Optional) - valuepaddingbottom : number -- (Optional) - - -- Bar geometry/appearance - min : number -- (Optional) Min value (alias for gaugemin) - max : number -- (Optional) Max value (alias for gaugemax) - gaugeorientation : string -- (Optional) "vertical" or "horizontal" - gaugepaddingleft : number -- (Optional) - gaugepaddingright : number -- (Optional) - gaugepaddingtop : number -- (Optional) - gaugepaddingbottom : number -- (Optional) - roundradius : number -- (Optional) Corner radius to apply rounding on edges of the bar - - -- Appearance/Theming - bgcolor : color -- (Optional) Widget background color (theme fallback) - fillbgcolor : color -- (Optional) Bar background color (theme fallback) - fillcolor : color -- (Optional) Bar fill color (theme fallback) - - -- Battery-style bar options - batteryframe : bool -- (Optional) Draw battery frame & cap around the bar (applies to both standard and segmented bars) - battery : bool -- (Optional) If true, draw a segmented battery bar instead of a standard fill bar - batteryframethickness : number -- (Optional) Battery frame outline thickness (default: 2) - batterysegments : number -- (Optional) Number of segments for segmented battery bar (default: 6) - batteryspacing : number -- (Optional) Spacing (pixels) between battery segments (default: 2) - batterysegmentpaddingtop : number -- (Optional) Padding (pixels) from the top of each horizontal segment (default: 0) - batterysegmentpaddingbottom : number -- (Optional) Padding (pixels) from the bottom of each horizontal segment (default: 0) - accentcolor : color -- (Optional) Color for the battery frame and cap (theme fallback) - - -- Battery Advanced Info (Optional overlay for battery/fuel bar) - battadv : bool -- (Optional) If true, shows advanced battery/fuel telemetry info lines (voltage, per-cell voltage, consumption, cell count) - battadvfont : font -- Font for advanced info lines (e.g., "FONT_XS", "FONT_M"). Defaults to FONT_XS if unset - battadvblockalign : string -- Horizontal alignment of the entire info block: "left", "center", or "right" (default: "right") - battadvvaluealign : string -- Text alignment within each info line: "left", "center", or "right" (default: "left") - battadvpadding : number -- Padding (pixels) applied to all sides unless overridden by individual paddings (default: 4) - battadvpaddingleft : number -- Padding (pixels) on the left side of the info block (overrides battadvpadding) - battadvpaddingright : number -- Padding (pixels) on the right side of the info block (overrides battadvpadding) - battadvpaddingtop : number -- Padding (pixels) above the first info line (overrides battadvpadding) - battadvpaddingbottom : number -- Padding (pixels) below the last info line (overrides battadvpadding) - battadvgap : number -- Vertical gap (pixels) between info lines (default: 5) - - -- Subtext - subtext : string -- (Optional) A line of subtext to draw inside the bar (usually below value) - subtextfont : font -- (Optional) Font for subtext (default: FONT_XS) - subtextalign : string -- (Optional) "center", "left", or "right" (default: "left") - subtextpaddingleft : number -- (Optional) Padding from left edge of bar (default: 0) - subtextpaddingright : number -- (Optional) Padding from right edge of bar (default: 0) - subtextpaddingtop : number -- (Optional) Extra offset from top of bar (default: 0) - subtextpaddingbottom : number -- (Optional) Padding above bottom of bar (default: 0) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -91,7 +14,7 @@ local resolveThresholdColor = utils.resolveThresholdColor local lastDisplayValue = nil function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -105,7 +28,6 @@ function render.dirty(box) return false end - local function drawFilledRoundedRectangle(x, y, w, h, r) x = math.floor(x + 0.5) y = math.floor(y + 0.5) @@ -113,9 +35,9 @@ local function drawFilledRoundedRectangle(x, y, w, h, r) h = math.floor(h + 0.5) r = r or 0 if r > 0 then - lcd.drawFilledRectangle(x + r, y, w - 2*r, h) - lcd.drawFilledRectangle(x, y + r, r, h - 2*r) - lcd.drawFilledRectangle(x + w - r, y + r, r, h - 2*r) + lcd.drawFilledRectangle(x + r, y, w - 2 * r, h) + lcd.drawFilledRectangle(x, y + r, r, h - 2 * r) + lcd.drawFilledRectangle(x + w - r, y + r, r, h - 2 * r) lcd.drawFilledCircle(x + r, y + r, r) lcd.drawFilledCircle(x + w - r - 1, y + r, r) lcd.drawFilledCircle(x + r, y + h - r - 1, r) @@ -125,16 +47,7 @@ local function drawFilledRoundedRectangle(x, y, w, h, r) end end -local function drawBatteryBox( - x, y, w, h, - percent, - gaugeorientation, - batterysegments, batteryspacing, - fillbgcolor, fillcolor, - batteryframe, batteryframethickness, accentcolor, battery, - batterysegmentpaddingtop, batterysegmentpaddingbottom, - batterysegmentpaddingleft, batterysegmentpaddingright -) +local function drawBatteryBox(x, y, w, h, percent, gaugeorientation, batterysegments, batteryspacing, fillbgcolor, fillcolor, batteryframe, batteryframethickness, accentcolor, battery, batterysegmentpaddingtop, batterysegmentpaddingbottom, batterysegmentpaddingleft, batterysegmentpaddingright) local frameThickness = batteryframethickness or 4 local segments = batterysegments or 5 @@ -145,21 +58,17 @@ local function drawBatteryBox( if batteryframe then local maxCapH = math.floor(h * 0.5) capH = math.min(math.max(8, math.floor(h * 0.10)), maxCapH) - -- draw cap + end local bodyY = y + capH local bodyH = h - capH - -- Draw cap at top inside widget if batteryframe then lcd.color(accentcolor) local capW = math.min(math.max(4, math.floor(w * 0.40)), w) - for i = 0, frameThickness - 1 do - lcd.drawFilledRectangle(x + (w - capW) / 2 - i, y + i, capW + 2 * i, capH - i) - end + for i = 0, frameThickness - 1 do lcd.drawFilledRectangle(x + (w - capW) / 2 - i, y + i, capW + 2 * i, capH - i) end end - -- Draw body/frame if battery then local segCount = math.max(1, segments) local fillSegs = math.floor(segCount * percent + 0.5) @@ -181,19 +90,17 @@ local function drawBatteryBox( end end - -- Draw frame around body if batteryframe then lcd.color(accentcolor) lcd.drawRectangle(x, bodyY, w, bodyH, frameThickness) end else - -- --- Horizontal battery --- + local maxCapW = math.floor(w * 0.5) local capOffset = math.min(math.max(8, math.floor(w * 0.03)), maxCapW) local bodyW = w - capOffset - -- Draw fill or segments inside battery body if battery then local segCount = math.max(1, segments) local fillSegs = math.floor(segCount * percent + 0.5) @@ -222,15 +129,12 @@ local function drawBatteryBox( end end - -- Frame & cap if batteryframe then lcd.color(accentcolor) lcd.drawRectangle(x, y, bodyW, h, frameThickness) local capW = capOffset local capH = math.min(math.max(4, math.floor(h * 0.33)), h) - for i = 0, frameThickness - 1 do - lcd.drawFilledRectangle(x + bodyW + i, y + (h - capH) / 2 + i, capW, capH - 2 * i) - end + for i = 0, frameThickness - 1 do lcd.drawFilledRectangle(x + bodyW + i, y + (h - capH) / 2 + i, capW, capH - 2 * i) end end end end @@ -238,13 +142,12 @@ end function render.wakeup(box) local telemetry = dashx.tasks.telemetry - - -- Value extraction + local source = getParam(box, "source") local value, _, dynamicUnit if source == "txbatt" then - local src = system.getSource({ category = CATEGORY_SYSTEM, member = MAIN_VOLTAGE }) + local src = system.getSource({category = CATEGORY_SYSTEM, member = MAIN_VOLTAGE}) value = src and src.value and src:value() or nil dynamicUnit = "V" elseif telemetry and source then @@ -253,19 +156,17 @@ function render.wakeup(box) value = getParam(box, "value") end - -- Battery Advanced value extraction local getSensor = telemetry and telemetry.getSensor - local voltage = getSensor and getSensor("voltage") or 0 + local voltage = getSensor and getSensor("voltage") or 0 local cellCount = getSensor and getSensor("cell_count") or 0 - local consumed = getSensor and getSensor("consumption") or 0 + local consumed = getSensor and getSensor("consumption") or 0 local perCellVoltage = (cellCount > 0) and (voltage / cellCount) or 0 - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -274,18 +175,11 @@ function render.wakeup(box) unit = "" end - -- Transform and decimals (if required) local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- Force suppress value if hidevalue is true - if getParam(box, "hidevalue") == true then - displayValue = nil - end + if getParam(box, "hidevalue") == true then displayValue = nil end - -- Resolve bar min/max local min, max if source == "txbatt" then min = getParam(box, "min") or 7.2 @@ -295,28 +189,21 @@ function render.wakeup(box) max = getParam(box, "max") or 100 end - -- Calculate percent fill for the gauge (clamped 0-1) local percent = 0 if value and max ~= min then percent = (value - min) / (max - min) percent = math.max(0, math.min(1, percent)) end - -- ... style loading indicator if value == nil then local maxDots = 3 - if box._dotCount == nil then - box._dotCount = 0 - end + if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) - if displayValue == "" then - displayValue = "." - end + if displayValue == "" then displayValue = "." end unit = nil end - -- Calculate title area height(s) for layout local title = getParam(box, "title") local titlefont = getParam(box, "titlefont") local titlespacing = getParam(box, "titlespacing") or 0 @@ -328,107 +215,97 @@ function render.wakeup(box) lcd.font(_G[titlefont] or FONT_XS) local _, tsizeH = lcd.getTextSize(title) if titlepos == "bottom" then - title_area_bottom = (tsizeH or 0) + (getParam(box, "titlepaddingtop") or 0) - + (getParam(box, "titlepaddingbottom") or 0) + titlespacing + title_area_bottom = (tsizeH or 0) + (getParam(box, "titlepaddingtop") or 0) + (getParam(box, "titlepaddingbottom") or 0) + titlespacing else - title_area_top = (tsizeH or 0) + (getParam(box, "titlepaddingtop") or 0) - + (getParam(box, "titlepaddingbottom") or 0) + titlespacing + title_area_top = (tsizeH or 0) + (getParam(box, "titlepaddingtop") or 0) + (getParam(box, "titlepaddingbottom") or 0) + titlespacing end else title_area_top = 0 title_area_bottom = 0 end - -- If battadv is enabled, cache extra telemetry info for detailed battery display local battadv = getParam(box, "battadv") if battadv then - box._batteryLines = { - line1 = string.format("%.1fv / %.2fv (%dS)", voltage, perCellVoltage, cellCount), - line2 = string.format("%d mah", consumed) - } + box._batteryLines = {line1 = string.format("%.1fv / %.2fv (%dS)", voltage, perCellVoltage, cellCount), line2 = string.format("%d mah", consumed)} else box._batteryLines = nil end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end - - -- Set box.value so dashboard/dirty can track change for redraws + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end + box._currentDisplayValue = value box._cache = { - value = value, - displayValue = displayValue, - unit = unit, - min = min, - max = max, - percent = percent, - title = title, - titlepos = titlepos, - titlefont = titlefont, - titlespacing = titlespacing, - title_area_top = title_area_top, - title_area_bottom = title_area_bottom, - voltage = voltage, - cellCount = cellCount, - consumed = consumed, - perCellVoltage = perCellVoltage, - battadv = battadv, - hidevalue = getParam(box, "hidevalue"), - textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor"), - fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor"), - fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - accentcolor = resolveThemeColor("accentcolor", getParam(box, "accentcolor")), - font = getParam(box, "font") or "FONT_XL", - titlealign = getParam(box, "titlealign"), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), - titlepaddingbottom = getParam(box, "titlepaddingbottom"), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), - valuepaddingbottom = getParam(box, "valuepaddingbottom"), - gaugeorientation = getParam(box, "gaugeorientation") or "horizontal", - gpad_left = getParam(box, "gaugepaddingleft"), - gpad_right = getParam(box, "gaugepaddingright"), - gpad_top = getParam(box, "gaugepaddingtop"), - gpad_bottom = getParam(box, "gaugepaddingbottom"), - roundradius = getParam(box, "roundradius"), - battery = getParam(box, "battery"), - batteryframe = getParam(box, "batteryframe"), - batteryframethickness = getParam(box, "batteryframethickness"), - batterysegments = getParam(box, "batterysegments"), - batteryspacing = getParam(box, "batteryspacing"), - batterysegmentpaddingleft = getParam(box, "batterysegmentpaddingleft") or 0, - batterysegmentpaddingright = getParam(box, "batterysegmentpaddingright") or 0, - batterysegmentpaddingtop = getParam(box, "batterysegmentpaddingtop") or 0, + value = value, + displayValue = displayValue, + unit = unit, + min = min, + max = max, + percent = percent, + title = title, + titlepos = titlepos, + titlefont = titlefont, + titlespacing = titlespacing, + title_area_top = title_area_top, + title_area_bottom = title_area_bottom, + voltage = voltage, + cellCount = cellCount, + consumed = consumed, + perCellVoltage = perCellVoltage, + battadv = battadv, + hidevalue = getParam(box, "hidevalue"), + textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor"), + fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor"), + fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + accentcolor = resolveThemeColor("accentcolor", getParam(box, "accentcolor")), + font = getParam(box, "font") or "FONT_XL", + titlealign = getParam(box, "titlealign"), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), + titlepaddingbottom = getParam(box, "titlepaddingbottom"), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), + valuepaddingbottom = getParam(box, "valuepaddingbottom"), + gaugeorientation = getParam(box, "gaugeorientation") or "horizontal", + gpad_left = getParam(box, "gaugepaddingleft"), + gpad_right = getParam(box, "gaugepaddingright"), + gpad_top = getParam(box, "gaugepaddingtop"), + gpad_bottom = getParam(box, "gaugepaddingbottom"), + roundradius = getParam(box, "roundradius"), + battery = getParam(box, "battery"), + batteryframe = getParam(box, "batteryframe"), + batteryframethickness = getParam(box, "batteryframethickness"), + batterysegments = getParam(box, "batterysegments"), + batteryspacing = getParam(box, "batteryspacing"), + batterysegmentpaddingleft = getParam(box, "batterysegmentpaddingleft") or 0, + batterysegmentpaddingright = getParam(box, "batterysegmentpaddingright") or 0, + batterysegmentpaddingtop = getParam(box, "batterysegmentpaddingtop") or 0, batterysegmentpaddingbottom = getParam(box, "batterysegmentpaddingbottom") or 0, - battadvfont = getParam(box, "battadvfont") or "FONT_S", - battadvblockalign = getParam(box, "battadvblockalign") or "right", - battadvvaluealign = getParam(box, "battadvvaluealign") or "left", - battadvpadding = getParam(box, "battadvpadding") or 4, - battadvpaddingleft = getParam(box, "battadvpaddingleft") or 0, - battadvpaddingright = getParam(box, "battadvpaddingright") or 0, - battadvpaddingtop = getParam(box, "battadvpaddingtop") or 0, - battadvpaddingbottom = getParam(box, "battadvpaddingbottom") or 0, - battadvgap = getParam(box, "battadvgap") or 5, - battstats = getParam(box, "battstats") or false, - subtext = getParam(box, "subtext"), - subtextfont = getParam(box, "subtextfont") or "FONT_XS", - subtextalign = getParam(box, "subtextalign") or "left", - subtextpaddingleft = getParam(box, "subtextpaddingleft") or 0, - subtextpaddingright = getParam(box, "subtextpaddingright") or 0, - subtextpaddingtop = getParam(box, "subtextpaddingtop") or 0, - subtextpaddingbottom = getParam(box, "subtextpaddingbottom") or 0, + battadvfont = getParam(box, "battadvfont") or "FONT_S", + battadvblockalign = getParam(box, "battadvblockalign") or "right", + battadvvaluealign = getParam(box, "battadvvaluealign") or "left", + battadvpadding = getParam(box, "battadvpadding") or 4, + battadvpaddingleft = getParam(box, "battadvpaddingleft") or 0, + battadvpaddingright = getParam(box, "battadvpaddingright") or 0, + battadvpaddingtop = getParam(box, "battadvpaddingtop") or 0, + battadvpaddingbottom = getParam(box, "battadvpaddingbottom") or 0, + battadvgap = getParam(box, "battadvgap") or 5, + battstats = getParam(box, "battstats") or false, + subtext = getParam(box, "subtext"), + subtextfont = getParam(box, "subtextfont") or "FONT_XS", + subtextalign = getParam(box, "subtextalign") or "left", + subtextpaddingleft = getParam(box, "subtextpaddingleft") or 0, + subtextpaddingright = getParam(box, "subtextpaddingright") or 0, + subtextpaddingtop = getParam(box, "subtextpaddingtop") or 0, + subtextpaddingbottom = getParam(box, "subtextpaddingbottom") or 0 } end @@ -436,35 +313,24 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Gauge rectangle (with padding and title space) local gauge_x = x + (c.gpad_left or 0) local gauge_y = y + (c.gpad_top or 0) + (c.title_area_top or 0) local gauge_w = w - (c.gpad_left or 0) - (c.gpad_right or 0) local gauge_h = h - (c.gpad_top or 0) - (c.gpad_bottom or 0) - (c.title_area_top or 0) - (c.title_area_bottom or 0) if c.batteryframe or c.battery then - drawBatteryBox( - gauge_x, gauge_y, gauge_w, gauge_h, - c.percent, - c.gaugeorientation, - c.batterysegments, c.batteryspacing, - c.fillbgcolor, c.fillcolor, - c.batteryframe, c.batteryframethickness, c.accentcolor, c.battery, - c.batterysegmentpaddingtop, c.batterysegmentpaddingbottom, - c.batterysegmentpaddingleft, c.batterysegmentpaddingright - ) + drawBatteryBox(gauge_x, gauge_y, gauge_w, gauge_h, c.percent, c.gaugeorientation, c.batterysegments, c.batteryspacing, c.fillbgcolor, c.fillcolor, c.batteryframe, c.batteryframethickness, c.accentcolor, c.battery, c.batterysegmentpaddingtop, c.batterysegmentpaddingbottom, + c.batterysegmentpaddingleft, c.batterysegmentpaddingright) else - -- Standard bar background + lcd.color(c.fillbgcolor) drawFilledRoundedRectangle(gauge_x, gauge_y, gauge_w, gauge_h, c.roundradius) - -- Bar fill if not c.battstats and (tonumber(c.percent) or 0) > 0 then lcd.color(c.fillcolor) if c.gaugeorientation == "vertical" then @@ -501,33 +367,21 @@ function render.paint(x, y, w, h, box) lcd.drawText(sx, sy, c.subtext) end - - -- Draw title and value local boxValue = c.displayValue local boxUnit = c.unit if c.hidevalue then boxValue = nil boxUnit = nil end - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - boxValue, boxUnit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) - - -- Ensure paddings are numeric and defaulted - c.battadvpaddingleft = tonumber(c.battadvpaddingleft) or 0 - c.battadvpaddingright = tonumber(c.battadvpaddingright) or 0 - c.battadvpaddingtop = tonumber(c.battadvpaddingtop) or 0 - c.battadvpaddingbottom = tonumber(c.battadvpaddingbottom) or 0 - c.battadvgap = tonumber(c.battadvgap) or 5 - - -- battadv info lines + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, boxValue, boxUnit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) + + c.battadvpaddingleft = tonumber(c.battadvpaddingleft) or 0 + c.battadvpaddingright = tonumber(c.battadvpaddingright) or 0 + c.battadvpaddingtop = tonumber(c.battadvpaddingtop) or 0 + c.battadvpaddingbottom = tonumber(c.battadvpaddingbottom) or 0 + c.battadvgap = tonumber(c.battadvgap) or 5 + if c.battadv and box._batteryLines then local textColor = c.textcolor local line1 = box._batteryLines.line1 or "" @@ -539,7 +393,6 @@ function render.paint(x, y, w, h, box) local blockW = math.max(w1, w2) + c.battadvpaddingleft + c.battadvpaddingright local blockH = h1 + h2 + c.battadvpaddingtop + c.battadvpaddingbottom + c.battadvgap - -- Block alignment local startY = y + math.max(0, math.floor((h - blockH) / 2 + 0.5)) local startX if c.battadvblockalign == "left" then @@ -550,28 +403,10 @@ function render.paint(x, y, w, h, box) startX = x + w - blockW end - -- Draw line 1 - utils.box( - startX + c.battadvpaddingleft, startY + c.battadvpaddingtop, - blockW - c.battadvpaddingleft - c.battadvpaddingright, h1, - nil, nil, c.battadvvaluealign, c.battadvfont, 0, - textColor, - 0, 0, 0, 0, 0, - line1, nil, c.battadvfont, c.battadvvaluealign, textColor, - 0, 0, 0, 0, 0, - nil - ) - -- Draw line 2 - utils.box( - startX + c.battadvpaddingleft, startY + c.battadvpaddingtop + h1 + c.battadvgap, - blockW - c.battadvpaddingleft - c.battadvpaddingright, h2, - nil, nil, c.battadvvaluealign, c.battadvfont, 0, - textColor, - 0, 0, 0, 0, 0, - line2, nil, c.battadvfont, c.battadvvaluealign, textColor, - 0, 0, 0, 0, 0, - nil - ) + utils.box(startX + c.battadvpaddingleft, startY + c.battadvpaddingtop, blockW - c.battadvpaddingleft - c.battadvpaddingright, h1, nil, nil, c.battadvvaluealign, c.battadvfont, 0, textColor, 0, 0, 0, 0, 0, line1, nil, c.battadvfont, c.battadvvaluealign, textColor, 0, 0, 0, 0, 0, nil) + + utils.box(startX + c.battadvpaddingleft, startY + c.battadvpaddingtop + h1 + c.battadvgap, blockW - c.battadvpaddingleft - c.battadvpaddingright, h2, nil, nil, c.battadvvaluealign, c.battadvfont, 0, textColor, 0, 0, 0, 0, 0, line2, nil, c.battadvfont, c.battadvvaluealign, textColor, 0, 0, 0, + 0, 0, nil) end end diff --git a/scripts/dashx/widgets/dashboard/objects/gauge/ring.lua b/scripts/dashx/widgets/dashboard/objects/gauge/ring.lua index df0d798..47f449e 100644 --- a/scripts/dashx/widgets/dashboard/objects/gauge/ring.lua +++ b/scripts/dashx/widgets/dashboard/objects/gauge/ring.lua @@ -1,64 +1,9 @@ -local dashx = require("dashx") --[[ - Rainbow Gauge Widget - Configurable Parameters (box table fields): - ------------------------------------------- - - -- Timing - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - - -- Title parameters - title : string -- (Optional) Title text - titlepos : string -- (Optional) If `title` is present but `titlepos` is not set, title is placed at the top by default - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) - titlespacing : number -- (Optional) Vertical gap between title and value - titlecolor : color -- (Optional) Title text color (theme/text fallback) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - - -- Value/Source parameters - value : any -- (Optional) Static value to display if telemetry is not present - source : string -- Telemetry sensor source name (e.g., "temp_esc") - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) - decimals : number -- (Optional) Number of decimal places for numeric display - thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - - -- Appearance/Theming - bgcolor : color -- (Optional) Widget background color (theme fallback) - fillbgcolor : color -- (Optional) Ring background color (theme fallback) - fillcolor : color -- (Optional) Ring foreground color (theme fallback) - - -- Geometry - thickness : number -- (Optional) Ring thickness in pixels (default is proportional to radius) - - -- Battery Ring Mode (Optional fuel-based battery style) - ringbatt : bool -- If true, draws 360° fill ring based on fuel (%) and shows mAh consumption - ringbattsubfont : font -- (Optional) Font for subtext in ringbatt mode (e.g., FONT_XS, FONT_S, FONT_M; default: FONT_XS) - innerringcolor : color -- Color of the inner decorative ring in ringbatt mode (default: white) - ringbattsubtext : string|bool -- (Optional) Overrides subtext below value in ringbatt mode (set "" or false to hide) - innerringthickness : number -- (Optional) Thickness of inner decorative ring in ringbatt mode (default: 8) - ringbattsubalign : string -- (Optional) "left", "center", or "right" alignment of subtext (default: center under value) - ringbattsubpadding : number -- (Optional) General padding (px) for subtext (applies if per-side not set) - ringbattsubpaddingleft : number -- (Optional) Left padding override for subtext - ringbattsubpaddingright : number -- (Optional) Right padding override for subtext - ringbattsubpaddingtop : number -- (Optional) Top padding override for subtext - ringbattsubpaddingbottom : number -- (Optional) Bottom padding override for subtext -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local render = {} @@ -69,7 +14,7 @@ local resolveThresholdColor = utils.resolveThresholdColor local lastDisplayValue = nil function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -88,12 +33,9 @@ local function drawArc(cx, cy, radius, thickness, startAngle, endAngle, color) local outer = radius local inner = math.max(1, radius - (thickness or 6)) - -- Normalize angles startAngle = startAngle % 360 endAngle = endAngle % 360 - if endAngle <= startAngle then - endAngle = endAngle + 360 - end + if endAngle <= startAngle then endAngle = endAngle + 360 end local sweep = endAngle - startAngle if sweep <= 180 then @@ -108,15 +50,11 @@ end function render.wakeup(box) local telemetry = dashx.tasks.telemetry - - -- Value extraction + local source = getParam(box, "source") local value, _, dynamicUnit - if telemetry and source then - value, _, dynamicUnit = telemetry.getSensor(source) - end + if telemetry and source then value, _, dynamicUnit = telemetry.getSensor(source) end - -- Ringbatt value extraction local ringbatt = getParam(box, "ringbatt") local percent = 0 local mahUnit = "" @@ -129,7 +67,6 @@ function render.wakeup(box) percent = math.max(0, math.min(1, fuel / 100)) mahUnit = string.format("%dmah", math.floor(consumption + 0.5)) - -- Apply optional override or suppression of subtext local override = getParam(box, "ringbattsubtext") if override == "" or override == false then mahUnit = nil @@ -137,13 +74,12 @@ function render.wakeup(box) mahUnit = override end end - - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) + local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -152,76 +88,64 @@ function render.wakeup(box) unit = "" end - -- Transform and decimals (if required) local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- ... style loading indicator if value == nil then local maxDots = 3 - if box._dotCount == nil then - box._dotCount = 0 - end + if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) - if displayValue == "" then - displayValue = "." - end + if displayValue == "" then displayValue = "." end unit = nil end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end - - -- Set box.value so dashboard/dirty can track change for redraws + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end + box._currentDisplayValue = value box._cache = { - value = value, - displayValue = displayValue, - unit = unit, - ringbatt = ringbatt, - percent = percent, - mahUnit = mahUnit, - novalue = getParam(box, "novalue") or "-", - fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor", getParam(box, "thresholds")), - textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor", thresholds), - fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - thresholds = getParam(box, "thresholds"), - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos") or (getParam(box, "title") and "top"), - titlealign = getParam(box, "titlealign"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing"), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), - titlepaddingbottom = getParam(box, "titlepaddingbottom"), - font = getParam(box, "font") or "FONT_M", - decimals = getParam(box, "decimals"), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), - valuepaddingbottom = getParam(box, "valuepaddingbottom"), - thickness = getParam(box, "thickness"), - innerringcolor = resolveThemeColor("innerringcolor", getParam(box, "innerringcolor") or "white"), - innerringthickness = getParam(box, "innerringthickness") or 8, - ringbattsubalign = getParam(box, "ringbattsubalign"), - ringbattsubpadding = getParam(box, "ringbattsubpadding") or 2, - ringbattsubpaddingleft = getParam(box, "ringbattsubpaddingleft"), - ringbattsubpaddingright = getParam(box, "ringbattsubpaddingright"), - ringbattsubpaddingtop = getParam(box, "ringbattsubpaddingtop"), + value = value, + displayValue = displayValue, + unit = unit, + ringbatt = ringbatt, + percent = percent, + mahUnit = mahUnit, + novalue = getParam(box, "novalue") or "-", + fillcolor = resolveThresholdColor(value, box, "fillcolor", "fillcolor", getParam(box, "thresholds")), + textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor", thresholds), + fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + thresholds = getParam(box, "thresholds"), + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos") or (getParam(box, "title") and "top"), + titlealign = getParam(box, "titlealign"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing"), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), + titlepaddingbottom = getParam(box, "titlepaddingbottom"), + font = getParam(box, "font") or "FONT_M", + decimals = getParam(box, "decimals"), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), + valuepaddingbottom = getParam(box, "valuepaddingbottom"), + thickness = getParam(box, "thickness"), + innerringcolor = resolveThemeColor("innerringcolor", getParam(box, "innerringcolor") or "white"), + innerringthickness = getParam(box, "innerringthickness") or 8, + ringbattsubalign = getParam(box, "ringbattsubalign"), + ringbattsubpadding = getParam(box, "ringbattsubpadding") or 2, + ringbattsubpaddingleft = getParam(box, "ringbattsubpaddingleft"), + ringbattsubpaddingright = getParam(box, "ringbattsubpaddingright"), + ringbattsubpaddingtop = getParam(box, "ringbattsubpaddingtop"), ringbattsubpaddingbottom = getParam(box, "ringbattsubpaddingbottom"), - ringbattsubfont = getParam(box, "ringbattsubfont") or "FONT_XS", + ringbattsubfont = getParam(box, "ringbattsubfont") or "FONT_XS" } end @@ -230,16 +154,13 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Arc layout local cx = x + w / 2 - -- Calculate total height used by the title (if present) local titleHeight = 0 if c.title then lcd.font(_G[c.titlefont] or FONT_XS) @@ -247,7 +168,6 @@ function render.paint(x, y, w, h, box) titleHeight = (th or 0) + (c.titlespacing or 0) + (c.titlepaddingtop or 0) + (c.titlepaddingbottom or 0) end - -- Compute vertical center of the arc based on title position local cy if c.titlepos == "top" then cy = y + titleHeight + (h - titleHeight) * 0.45 @@ -257,43 +177,36 @@ function render.paint(x, y, w, h, box) cy = y + h * 0.5 end - -- Compute radius and thickness, slightly enlarging the ring when no title is present local ringPadding = 2 local baseSize = math.min(w, h - (c.title and ringPadding * 2 or 0)) local ringSize = math.min(0.88 * (c.title and 1 or 1.05), 1.0) - local radius = baseSize * 0.5 * ringSize + local radius = baseSize * 0.5 * ringSize local thickness = c.thickness or math.max(8, radius * 0.18) - -- Ring draw if c.ringbatt then - -- Outer ring fill background + drawArc(cx, cy, radius, thickness, 0, 360, c.fillbgcolor) - -- Fill ring (based on fuel %) local startAngle = 360 - (c.percent * 360) drawArc(cx, cy, radius, thickness, startAngle, 360, c.fillcolor) - -- Inner decorative ring (configurable thickness, flush against inner edge) drawArc(cx, cy, radius - thickness, c.innerringthickness, 0, 360, c.innerringcolor) else - -- Default full ring behavior + drawArc(cx, cy, radius, thickness, 0, 360, c.fillbgcolor) drawArc(cx, cy, radius, thickness, 0, 360, c.fillcolor) end - -- Draw subtext (mah or override) below main value if c.ringbatt and c.mahUnit then - -- Resolve subtext font and size + lcd.font(_G[c.ringbattsubfont] or FONT_XS) local tw, th = lcd.getTextSize(c.mahUnit) - -- Padding resolution (global fallback or per-side) - local padL = c.ringbattsubpaddingleft or c.ringbattsubpadding or 0 - local padR = c.ringbattsubpaddingright or c.ringbattsubpadding or 0 - local padT = c.ringbattsubpaddingtop or c.ringbattsubpadding or 0 + local padL = c.ringbattsubpaddingleft or c.ringbattsubpadding or 0 + local padR = c.ringbattsubpaddingright or c.ringbattsubpadding or 0 + local padT = c.ringbattsubpaddingtop or c.ringbattsubpadding or 0 local padB = c.ringbattsubpaddingbottom or c.ringbattsubpadding or 0 - -- Horizontal alignment (default = center) local textX if c.ringbattsubalign == "left" then textX = x + padL @@ -303,29 +216,18 @@ function render.paint(x, y, w, h, box) textX = x + (w - tw) / 2 + (padL - padR) end - -- Vertical alignment (default = below value center) lcd.font(_G[c.font] or FONT_M) local _, mainH = lcd.getTextSize("0") local centerY = y + h / 2 local textY = centerY + mainH / 2 + padT - padB - -- Final render lcd.font(_G[c.ringbattsubfont] or FONT_XS) lcd.color(c.textcolor) lcd.drawText(textX, textY, c.mahUnit) end - -- Draw title and value - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/gauge/step.lua b/scripts/dashx/widgets/dashboard/objects/gauge/step.lua index 5d83bef..fd33b48 100644 --- a/scripts/dashx/widgets/dashboard/objects/gauge/step.lua +++ b/scripts/dashx/widgets/dashboard/objects/gauge/step.lua @@ -1,55 +1,9 @@ -local dashx = require("dashx") --[[ - Step Bar Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- (Optional) Wakeup interval in seconds for the widget (set in wrapper) - - -- Title parameters - title : string -- (Optional) Title text (e.g., "2.4G", "Lora") - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Vertical gap between title and bar/value - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Title padding (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - - -- Value/telemetry parameters - value : number -- (Optional) Static value to display if no telemetry - hidevalue : bool -- (Optional) If true, value/unit will NOT be displayed (default: false) - source : string -- (Optional) Telemetry sensor source name (e.g., "rssi", "voltage", "current") - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) - decimals : number -- (Optional) Number of decimal places for numeric display - thresholds : table -- (Optional) List of threshold tables: {value=..., fillcolor=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value ("" hides, default resolves dynamically) - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Value padding (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- - -- Step bar parameters - stepcount : number -- (Optional) Number of steps/bars to draw (default: 4) - stepgap : number -- (Optional) Pixel gap between each step/bar (default: 1) - fillcolor : color -- (Optional) Color for active steps (theme fallback, or resolved by thresholds) - fillbgcolor : color -- (Optional) Color for inactive steps (theme fallback) - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - - -- Bar padding parameters - barpadding : number -- (Optional) Bar padding (all sides unless overridden) - barpaddingleft : number -- (Optional) Left padding for bar - barpaddingright : number -- (Optional) Right padding for bar - barpaddingtop : number -- (Optional) Top padding for bar - barpaddingbottom : number -- (Optional) Bottom padding for bar -]] +local dashx = require("dashx") local render = {} @@ -59,7 +13,7 @@ local resolveThemeColor = utils.resolveThemeColor local resolveThresholdColor = utils.resolveThresholdColor function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -76,18 +30,14 @@ end function render.wakeup(box) local telemetry = dashx.tasks.telemetry - -- Value extraction local source = getParam(box, "source") local value, _, dynamicUnit - if telemetry and source then - value, _, dynamicUnit = telemetry.getSensor(source) - end + if telemetry and source then value, _, dynamicUnit = telemetry.getSensor(source) end - -- Dynamic unit logic local manualUnit = getParam(box, "unit") local unit if manualUnit ~= nil then - unit = manualUnit -- use user value, even if "" + unit = manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -96,30 +46,23 @@ function render.wakeup(box) unit = "" end - -- Value transformation & decimals local displayValue - if value ~= nil then - displayValue = utils.transformValue(value, box) - end + if value ~= nil then displayValue = utils.transformValue(value, box) end - -- Hide value logic if getParam(box, "hidevalue") == true then displayValue = nil unit = nil end - -- Bar min/max local min = getParam(box, "min") or 0 local max = getParam(box, "max") or 100 - -- Calculate percent fill (clamp 0-1) local percent = 0 if value ~= nil and max ~= min then percent = (value - min) / (max - min) percent = math.max(0, math.min(1, percent)) end - -- Loading dots if no value if value == nil then local maxDots = 3 if box._dotCount == nil then box._dotCount = 0 end @@ -129,7 +72,6 @@ function render.wakeup(box) unit = nil end - -- Threshold color logic local thresholds = getParam(box, "thresholds") local fillcolor = resolveThemeColor("fillcolor", getParam(box, "fillcolor")) or lcd.WHITE local textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) or lcd.WHITE @@ -138,62 +80,57 @@ function render.wakeup(box) textcolor = resolveThresholdColor(value, box, "textcolor", "textcolor", thresholds) end - -- Save for dirty check box._currentDisplayValue = percent - -- Cache everything for paint box._cache = { - value = value, - displayValue = displayValue, - unit = unit, - min = min, - max = max, - percent = percent, - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing"), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), - titlepaddingbottom = getParam(box, "titlepaddingbottom"), - stepcount = getParam(box, "stepcount") or 4, - fillcolor = fillcolor, - fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), - font = getParam(box, "font"), - valuealign = getParam(box, "valuealign"), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), - valuepaddingbottom = getParam(box, "valuepaddingbottom"), - barpadding = getParam(box, "barpadding"), - barpaddingleft = getParam(box, "barpaddingleft"), - barpaddingright = getParam(box, "barpaddingright"), - barpaddingtop = getParam(box, "barpaddingtop"), - barpaddingbottom = getParam(box, "barpaddingbottom"), - textcolor = textcolor, - hidevalue = getParam(box, "hidevalue"), - thresholds = thresholds, - stepgap = getParam(box, "stepgap") or 1, + value = value, + displayValue = displayValue, + unit = unit, + min = min, + max = max, + percent = percent, + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing"), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), + titlepaddingbottom = getParam(box, "titlepaddingbottom"), + stepcount = getParam(box, "stepcount") or 4, + fillcolor = fillcolor, + fillbgcolor = resolveThemeColor("fillbgcolor", getParam(box, "fillbgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + font = getParam(box, "font"), + valuealign = getParam(box, "valuealign"), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), + valuepaddingbottom = getParam(box, "valuepaddingbottom"), + barpadding = getParam(box, "barpadding"), + barpaddingleft = getParam(box, "barpaddingleft"), + barpaddingright = getParam(box, "barpaddingright"), + barpaddingtop = getParam(box, "barpaddingtop"), + barpaddingbottom = getParam(box, "barpaddingbottom"), + textcolor = textcolor, + hidevalue = getParam(box, "hidevalue"), + thresholds = thresholds, + stepgap = getParam(box, "stepgap") or 1 } end - function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - -- Widget background if c.bgcolor then lcd.color(c.bgcolor) lcd.drawFilledRectangle(x, y, w, h) end - -- Calculate title area height(s) for layout local title = c.title local titlefont = c.titlefont local titlespacing = c.titlespacing or 0 @@ -211,17 +148,15 @@ function render.paint(x, y, w, h, box) end end - -- Step Bar Geometry, title-aware, with bar padding local stepGap = c.stepgap or 1 local minStepW = 4 local minStepH = 6 - -- Resolve bar padding - local barpadding = c.barpadding or 0 - local barpaddingleft = c.barpaddingleft or barpadding + local barpadding = c.barpadding or 0 + local barpaddingleft = c.barpaddingleft or barpadding local barpaddingright = c.barpaddingright or barpadding - local barpaddingtop = c.barpaddingtop or barpadding - local barpaddingbottom= c.barpaddingbottom or barpadding + local barpaddingtop = c.barpaddingtop or barpadding + local barpaddingbottom = c.barpaddingbottom or barpadding local barX = x + barpaddingleft local barY = y + title_area_top + barpaddingtop @@ -231,29 +166,20 @@ function render.paint(x, y, w, h, box) local reqSteps = c.stepcount or 4 local maxFitSteps = math.max(2, math.floor((barW + stepGap) / (minStepW + stepGap))) local steps = math.min(reqSteps, maxFitSteps) - local stepW = (barW - (steps-1)*stepGap) / steps + local stepW = (barW - (steps - 1) * stepGap) / steps local maxStepH = math.max(minStepH, barH) local activeSteps = math.floor((c.percent or 0) * steps + 0.5) for i = 1, steps do local stepH = math.floor((maxStepH / steps) * i) local stepY = barY + maxStepH - stepH - local stepX = barX + (i-1) * (stepW + stepGap) + local stepX = barX + (i - 1) * (stepW + stepGap) lcd.color(i <= activeSteps and c.fillcolor or c.fillbgcolor) lcd.drawFilledRectangle(stepX, stepY, stepW, stepH) end - -- Draw title and value - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - nil - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, nil) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/image.lua b/scripts/dashx/widgets/dashboard/objects/image.lua index 1654f35..43c91f0 100644 --- a/scripts/dashx/widgets/dashboard/objects/image.lua +++ b/scripts/dashx/widgets/dashboard/objects/image.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -14,27 +19,18 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now - end + end local subtype = box.subtype or "model" @@ -44,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/image/image.lua b/scripts/dashx/widgets/dashboard/objects/image/image.lua index 3aa693b..2144b2c 100644 --- a/scripts/dashx/widgets/dashboard/objects/image/image.lua +++ b/scripts/dashx/widgets/dashboard/objects/image/image.lua @@ -1,31 +1,9 @@ -local dashx = require("dashx") --[[ - Image Box Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - image : string -- (Optional) Path to image file (no extension needed; .png is tried first, then .bmp) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Gap between title and image - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - valuepadding : number -- (Optional) Padding for image (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for image - valuepaddingright : number -- (Optional) Right padding for image - valuepaddingtop : number -- (Optional) Top padding for image - valuepaddingbottom : number -- (Optional) Bottom padding for image - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - imagewidth : number -- (Optional) Image width (px) - imageheight : number -- (Optional) Image height (px) - imagealign : string -- (Optional) Image alignment ("center", "left", "right", "top", "bottom") -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -34,10 +12,8 @@ local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor local loadImage = dashx.utils.loadImage --- External invalidation when runtime params/theme change function render.invalidate(box) box._cfg = nil end --- Only repaint when displayed image path changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -50,10 +26,9 @@ function render.dirty(box) return false end --- Resolve image path once, trying .png then .bmp; return fallback if none local function resolveImagePath(imageParam) if imageParam and imageParam ~= "" then - local baseNoExt = imageParam:gsub("%.png$",""):gsub("%.bmp$","") + local baseNoExt = imageParam:gsub("%.png$", ""):gsub("%.bmp$", "") local pngPath = baseNoExt .. ".png" local bmpPath = baseNoExt .. ".bmp" if loadImage and loadImage(pngPath) then @@ -65,42 +40,40 @@ local function resolveImagePath(imageParam) return "widgets/dashboard/gfx/logo.png" end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.imagewidth = getParam(box, "imagewidth") - cfg.imageheight = getParam(box, "imageheight") - cfg.imagealign = getParam(box, "imagealign") + cfg.imagewidth = getParam(box, "imagewidth") + cfg.imageheight = getParam(box, "imageheight") + cfg.imagealign = getParam(box, "imagealign") - -- Resolve image path once per param/theme change - cfg.image = resolveImagePath(getParam(box, "image")) + cfg.image = resolveImagePath(getParam(box, "image")) box._cfg = cfg end @@ -109,7 +82,7 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- Dynamic part is just the path; keep it here for consistency + box._currentDisplayValue = cfg.image end @@ -117,20 +90,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - nil, nil, nil, nil, nil, -- value text not used in image widget - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor, - c.image, c.imagewidth, c.imageheight, c.imagealign - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, nil, nil, nil, nil, nil, c.valuepadding, c.valuepaddingleft, c.valuepaddingright, + c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor, c.image, c.imagewidth, c.imageheight, c.imagealign) end --- No need for frequent wakeups; only changes when params/theme change render.scheduler = 2.0 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/image/model.lua b/scripts/dashx/widgets/dashboard/objects/image/model.lua index e224ae6..6156a72 100644 --- a/scripts/dashx/widgets/dashboard/objects/image/model.lua +++ b/scripts/dashx/widgets/dashboard/objects/image/model.lua @@ -1,34 +1,9 @@ -local dashx = require("dashx") --[[ - Model Image Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Gap between title and image - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - font : font -- (Unused, for consistency) - valuealign : string -- (Unused, for consistency) - textcolor : color -- (Unused, for consistency) - valuepadding : number -- (Optional) Padding for image (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for image - valuepaddingright : number -- (Optional) Right padding for image - valuepaddingtop : number -- (Optional) Top padding for image - valuepaddingbottom : number -- (Optional) Bottom padding for image - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - image : string -- (Auto) Image path, auto-resolved from model name or ID - imagewidth : number -- (Optional) Image width (px) - imageheight : number -- (Optional) Image height (px) - imagealign : string -- (Optional) Image alignment ("center", "left", "right", "top", "bottom") -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -37,10 +12,8 @@ local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor local loadImage = dashx.utils.loadImage --- External invalidation when runtime params/theme change function render.invalidate(box) box._cfg = nil end --- Only repaint when displayed image path changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -53,11 +26,10 @@ function render.dirty(box) return false end --- Simple in-memory image-path cache keyed by craft name local _imgCache = {} local function resolveModelImage(cfg) - -- 1) Craft name specific bitmap in /bitmaps/models/.(png|bmp) + local craftName = dashx and dashx.session and dashx.session.craftName if craftName and craftName ~= "" then local cached = _imgCache[craftName] @@ -66,20 +38,16 @@ local function resolveModelImage(cfg) local pngPath = base .. ".png" local bmpPath = base .. ".bmp" cached = loadImage and (loadImage(pngPath) or loadImage(bmpPath)) - _imgCache[craftName] = cached or false -- remember miss too + _imgCache[craftName] = cached or false end if cached then return cached end end - -- 2) Radio model bitmap if present and non-default if model and model.bitmap then local bm = model.bitmap() - if bm and type(bm) == "string" and not string.find(bm, "default_") then - return bm - end + if bm and type(bm) == "string" and not string.find(bm, "default_") then return bm end end - -- 3) Explicit param override (optional) local paramImage = getParam(cfg.box, "image") if paramImage and paramImage ~= "" then local base = paramImage:gsub("%.png$", ""):gsub("%.bmp$", "") @@ -88,47 +56,44 @@ local function resolveModelImage(cfg) return (loadImage and (loadImage(pngPath) or loadImage(bmpPath))) or paramImage end - -- 4) Fallback return "widgets/dashboard/gfx/logo.png" end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.box = box -- for resolveModelImage param lookups - - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.box = box + + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.imagewidth = getParam(box, "imagewidth") - cfg.imageheight = getParam(box, "imageheight") - cfg.imagealign = getParam(box, "imagealign") + cfg.imagewidth = getParam(box, "imagewidth") + cfg.imageheight = getParam(box, "imageheight") + cfg.imagealign = getParam(box, "imagealign") - -- Resolve once now - cfg.image = resolveModelImage(cfg) + cfg.image = resolveModelImage(cfg) box._cfg = cfg end @@ -138,14 +103,12 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- If craftName changed since last tick, refresh the image path local craftName = dashx and dashx.session and dashx.session.craftName if cfg._lastCraftName ~= craftName then cfg.image = resolveModelImage(cfg) cfg._lastCraftName = craftName end - -- Dynamic part is just the path box._currentDisplayValue = cfg.image end @@ -153,20 +116,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - nil, nil, nil, nil, nil, -- no value text - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor, - c.image, c.imagewidth, c.imageheight, c.imagealign - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, nil, nil, nil, nil, nil, c.valuepadding, c.valuepaddingleft, c.valuepaddingright, + c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor, c.image, c.imagewidth, c.imageheight, c.imagealign) end --- Image rarely changes; relaxed scheduler render.scheduler = 2.0 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/navigation.lua b/scripts/dashx/widgets/dashboard/objects/navigation.lua index 768168f..9a39b4a 100644 --- a/scripts/dashx/widgets/dashboard/objects/navigation.lua +++ b/scripts/dashx/widgets/dashboard/objects/navigation.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -12,29 +17,18 @@ function wrapper.paint(x, y, w, h, box) render.paint(x, y, w, h, box) end - function wrapper.wakeup(box) + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end - - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now end @@ -46,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/navigation/ah.lua b/scripts/dashx/widgets/dashboard/objects/navigation/ah.lua index acafee0..4c687d1 100644 --- a/scripts/dashx/widgets/dashboard/objects/navigation/ah.lua +++ b/scripts/dashx/widgets/dashboard/objects/navigation/ah.lua @@ -1,38 +1,17 @@ -local dashx = require("dashx") --[[ - Attitude Horizon Widget (AH) - Configurable Parameters (box table fields): - ------------------------------------------------ - wakeupinterval : number -- Optional wakeup interval in seconds (default: 0.2) - pixelsperdeg : number -- Pixels per degree for pitch & compass (default: 2.0) - dynamicscalemin : number -- Minimum scale factor (default: 1.05) - dynamicscalemax : number -- Maximum scale factor (default: 1.95) - showarc : bool -- Show arc markers (default: true) - showladder : bool -- Show pitch ladder (default: true) - showcompass : bool -- Show compass ribbon (default: true) - showaltitude : bool -- Show altitude bar on right (default: false) - showgroundspeed : bool -- Show groundspeed bar on left (default: false) - arccolor : color -- Color for arc markings (default: white) - laddercolor : color -- Color for pitch ladder (default: white) - compasscolor : color -- Color for compass (default: white) - crosshaircolor : color -- Color for central cross marker (default: white) - altitudecolor : color -- Color for altitude bar (default: white) - groundspeedcolor : color -- Color for groundspeed bar (default: white) - altitudemin : number -- Minimum displayed altitude (default: 0) - altitudemax : number -- Maximum displayed altitude (default: 200) - groundspeedmin : number -- Minimum displayed groundspeed (default: 0) - groundspeedmax : number -- Maximum displayed groundspeed (default: 100) -]] - -local render = {} -local utils = dashx.widgets.dashboard.utils -local getParam = utils.getParam + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") + +local render = {} +local utils = dashx.widgets.dashboard.utils +local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- external invalidation hook function render.invalidate(box) box._cfg = nil end --- rotation helper (unchanged) local function rotate(px, py, cx, cy, angle) local s = math.sin(angle) local c = math.cos(angle) @@ -42,47 +21,43 @@ local function rotate(px, py, cx, cy, angle) return xnew + cx, ynew + cy end --- Build/refresh static config once local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - - cfg.ppd = getParam(box, "pixelsperdeg") or 2.0 - cfg.dMin = getParam(box, "dynamicscalemin") or 1.05 - cfg.dMax = getParam(box, "dynamicscalemax") or ((getParam(box, "dynamicscalemin") or 1.05) + 0.9) - - cfg.showarc = getParam(box, "showarc") ~= false - cfg.showladder = getParam(box, "showladder") ~= false - cfg.showcompass = getParam(box, "showcompass") ~= false - cfg.showaltitude = getParam(box, "showaltitude") ~= false - cfg.showgroundspeed = getParam(box, "showgroundspeed") ~= false - - cfg.arccolor = resolveThemeColor("arccolor", getParam(box, "arccolor") or lcd.RGB(255,255,255)) - cfg.laddercolor = resolveThemeColor("laddercolor", getParam(box, "laddercolor") or lcd.RGB(255,255,255)) - cfg.compasscolor = resolveThemeColor("compasscolor", getParam(box, "compasscolor") or lcd.RGB(255,255,255)) - cfg.crosshaircolor = resolveThemeColor("crosshaircolor", getParam(box, "crosshaircolor") or lcd.RGB(255,255,255)) - cfg.altitudecolor = resolveThemeColor("altitudecolor", getParam(box, "altitudecolor") or lcd.RGB(255,255,255)) - cfg.groundspeedcolor = resolveThemeColor("groundspeedcolor", getParam(box, "groundspeedcolor") or lcd.RGB(255,255,255)) - - cfg.altitudemin = getParam(box, "altitudemin") or 0 - cfg.altitudemax = getParam(box, "altitudemax") or 200 - cfg.groundspeedmin = getParam(box, "groundspeedmin") or 0 - cfg.groundspeedmax = getParam(box, "groundspeedmax") or 100 + cfg._theme_version = theme_version + cfg._param_version = param_version + + cfg.ppd = getParam(box, "pixelsperdeg") or 2.0 + cfg.dMin = getParam(box, "dynamicscalemin") or 1.05 + cfg.dMax = getParam(box, "dynamicscalemax") or ((getParam(box, "dynamicscalemin") or 1.05) + 0.9) + + cfg.showarc = getParam(box, "showarc") ~= false + cfg.showladder = getParam(box, "showladder") ~= false + cfg.showcompass = getParam(box, "showcompass") ~= false + cfg.showaltitude = getParam(box, "showaltitude") ~= false + cfg.showgroundspeed = getParam(box, "showgroundspeed") ~= false + + cfg.arccolor = resolveThemeColor("arccolor", getParam(box, "arccolor") or lcd.RGB(255, 255, 255)) + cfg.laddercolor = resolveThemeColor("laddercolor", getParam(box, "laddercolor") or lcd.RGB(255, 255, 255)) + cfg.compasscolor = resolveThemeColor("compasscolor", getParam(box, "compasscolor") or lcd.RGB(255, 255, 255)) + cfg.crosshaircolor = resolveThemeColor("crosshaircolor", getParam(box, "crosshaircolor") or lcd.RGB(255, 255, 255)) + cfg.altitudecolor = resolveThemeColor("altitudecolor", getParam(box, "altitudecolor") or lcd.RGB(255, 255, 255)) + cfg.groundspeedcolor = resolveThemeColor("groundspeedcolor", getParam(box, "groundspeedcolor") or lcd.RGB(255, 255, 255)) + + cfg.altitudemin = getParam(box, "altitudemin") or 0 + cfg.altitudemax = getParam(box, "altitudemax") or 200 + cfg.groundspeedmin = getParam(box, "groundspeedmin") or 0 + cfg.groundspeedmax = getParam(box, "groundspeedmax") or 100 box._cfg = cfg end return box._cfg end --- Only repaint when telemetry-driven display actually changed -function render.dirty(box) - return true -end +function render.dirty(box) return true end function render.wakeup(box) ensureCfg(box) @@ -91,115 +66,100 @@ function render.wakeup(box) if not telemetry then return end local getSensor = telemetry.getSensor - -- Dynamic values only local pitch = getSensor("attpitch") or 0 - local roll = getSensor("attroll") or 0 - local yaw = getSensor("attyaw") or 0 - local altitude = getSensor("altitude") or 0 + local roll = getSensor("attroll") or 0 + local yaw = getSensor("attyaw") or 0 + local altitude = getSensor("altitude") or 0 local groundspeed = getSensor("groundspeed") or 0 - - local alpha = 0.75 -- 0 < alpha <= 1 (lower = smoother, slower) + + local alpha = 0.75 local function smooth(prev, new) if prev == nil then return new end return prev + alpha * (new - prev) end - box._dyn = { - pitch = smooth((box._dyn and box._dyn.pitch), pitch), - roll = smooth((box._dyn and box._dyn.roll), roll), - yaw = smooth((box._dyn and box._dyn.yaw), yaw), - altitude = altitude, - groundspeed = groundspeed, - } + box._dyn = {pitch = smooth((box._dyn and box._dyn.pitch), pitch), roll = smooth((box._dyn and box._dyn.roll), roll), yaw = smooth((box._dyn and box._dyn.yaw), yaw), altitude = altitude, groundspeed = groundspeed} end function render.paint(x, y, w, h, box) - local c = box._cfg; if not c then return end - local d = box._dyn; if not d then return end + local c = box._cfg; + if not c then return end + local d = box._dyn; + if not d then return end local pitch, roll, yaw = d.pitch, d.roll, d.yaw local ppd = c.ppd local cx, cy = x + w / 2, y + h / 2 - -- Define sky and ground colors - local skyColor = lcd.RGB(70, 130, 180) -- Steel blue - local groundColor = lcd.RGB(160, 82, 45) -- Saddle brown + local skyColor = lcd.RGB(70, 130, 180) + local groundColor = lcd.RGB(160, 82, 45) lcd.setClipping(x, y, w, h) - -- 1. Fill background with dominant color lcd.color(pitch >= 0 and skyColor or groundColor) lcd.drawFilledRectangle(x, y, w, h) - -- 3. Overlay the opposite half-plane using a normal to the rotated horizon local horizonY = cy + pitch * ppd - local rollRad = math.rad(roll) + local rollRad = math.rad(roll) - -- Two far apart points on the horizon line (rotate a long segment) - local xL, yL = rotate(cx - 3*w, horizonY, cx, horizonY, rollRad) - local xR, yR = rotate(cx + 3*w, horizonY, cx, horizonY, rollRad) + local xL, yL = rotate(cx - 3 * w, horizonY, cx, horizonY, rollRad) + local xR, yR = rotate(cx + 3 * w, horizonY, cx, horizonY, rollRad) - -- Normal pointing "down" from the horizon line (screen coords: y+ is down) local nx, ny = -math.sin(rollRad), math.cos(rollRad) - -- Pick which side to paint as the overlay (opposite of the base color) - local overlayColor = (pitch >= 0) and groundColor or skyColor + local overlayColor = (pitch >= 0) and groundColor or skyColor lcd.color(overlayColor) - -- Big extension to cover the whole box even at steep rolls local BIG = 4 * math.max(w, h) local sx, sy if pitch >= 0 then - sx, sy = nx*BIG, ny*BIG -- ground side + sx, sy = nx * BIG, ny * BIG else - sx, sy = -nx*BIG, -ny*BIG -- sky side + sx, sy = -nx * BIG, -ny * BIG end - -- Build a quad for the chosen half-plane and draw as two triangles + local p1x, p1y = xL + sx, yL + sy local p2x, p2y = xR + sx, yR + sy - local p3x, p3y = xR, yR - local p4x, p4y = xL, yL + local p3x, p3y = xR, yR + local p4x, p4y = xL, yL lcd.drawFilledTriangle(p1x, p1y, p2x, p2y, p3x, p3y) lcd.drawFilledTriangle(p1x, p1y, p3x, p3y, p4x, p4y) - -- 4. Crosshair lcd.color(c.crosshaircolor) lcd.drawLine(cx - 5, cy, cx + 5, cy) lcd.drawLine(cx, cy - 5, cx, cy + 5) lcd.drawCircle(cx, cy, 3) - -- 5. Arc markers if c.showarc then lcd.color(c.arccolor) local arcR = w * 0.4 - for _, ang in ipairs({-60,-45,-30,-20,-10,0,10,20,30,45,60}) do + for _, ang in ipairs({-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60}) do local rad = math.rad(ang) - local x1 = cx + arcR * math.sin(rad) - local y1 = y + 10 + arcR * (1 - math.cos(rad)) - local x2 = cx + (arcR - 6) * math.sin(rad) - local y2 = y + 10 + (arcR - 6) * (1 - math.cos(rad)) + local x1 = cx + arcR * math.sin(rad) + local y1 = y + 10 + arcR * (1 - math.cos(rad)) + local x2 = cx + (arcR - 6) * math.sin(rad) + local y2 = y + 10 + (arcR - 6) * (1 - math.cos(rad)) lcd.drawLine(x1, y1, x2, y2) end - lcd.drawFilledTriangle(cx, y+5, cx-6, y+15, cx+6, y+15) + lcd.drawFilledTriangle(cx, y + 5, cx - 6, y + 15, cx + 6, y + 15) end - -- 6. Pitch ladder if c.showladder then lcd.color(c.laddercolor) for ang = -90, 90, 10 do local off = (pitch - ang) * ppd - local py = cy + off - if py > y-40 and py < y+h+40 then + local py = cy + off + if py > y - 40 and py < y + h + 40 then local major = (ang % 20 == 0) - local len = major and 25 or 15 - local x1,y1 = rotate(cx-len, py, cx, cy, math.rad(roll)) - local x2,y2 = rotate(cx+len, py, cx, cy, math.rad(roll)) + local len = major and 25 or 15 + local x1, y1 = rotate(cx - len, py, cx, cy, math.rad(roll)) + local x2, y2 = rotate(cx + len, py, cx, cy, math.rad(roll)) lcd.drawLine(x1, y1, x2, y2) if major then local lbl = tostring(ang) - local lx,ly = rotate(cx-len-10, py-4, cx, cy, math.rad(roll)) - local rx,ry = rotate(cx+len+2, py-4, cx, cy, math.rad(roll)) + local lx, ly = rotate(cx - len - 10, py - 4, cx, cy, math.rad(roll)) + local rx, ry = rotate(cx + len + 2, py - 4, cx, cy, math.rad(roll)) lcd.drawText(lx, ly, lbl, RIGHT) lcd.drawText(rx, ry, lbl, LEFT) end @@ -207,35 +167,33 @@ function render.paint(x, y, w, h, box) end end - -- 7. Compass ribbon if c.showcompass then lcd.color(c.compasscolor) - local heading = math.floor((yaw + 360) % 360) + local heading = math.floor((yaw + 360) % 360) local compassY = y + h - 24 - local labels = {[0]="N",[45]="NE",[90]="E",[135]="SE",[180]="S",[225]="SW",[270]="W",[315]="NW"} + local labels = {[0] = "N", [45] = "NE", [90] = "E", [135] = "SE", [180] = "S", [225] = "SW", [270] = "W", [315] = "NW"} for ang = -90, 90, 10 do local hdg = (heading + ang + 360) % 360 - local px = cx + ang * ppd - if px > x and px < x+w then + local px = cx + ang * ppd + if px > x and px < x + w then local th = (hdg % 30 == 0) and 8 or 4 - lcd.drawLine(px, compassY, px, compassY-th) - if hdg % 30 == 0 then - lcd.drawText(px, compassY-th-8, labels[hdg] or tostring(hdg), CENTERED+FONT_XS) - end + lcd.drawLine(px, compassY, px, compassY - th) + if hdg % 30 == 0 then lcd.drawText(px, compassY - th - 8, labels[hdg] or tostring(hdg), CENTERED + FONT_XS) end end end - lcd.drawFilledTriangle(cx, compassY+1, cx-5, compassY-7, cx+5, compassY-7) + lcd.drawFilledTriangle(cx, compassY + 1, cx - 5, compassY - 7, cx + 5, compassY - 7) local bw, bh = 60, 14 - local bx, by = cx - bw/2, compassY + 6 + local bx, by = cx - bw / 2, compassY + 6 if by + bh < y + h then - lcd.color(lcd.RGB(0,0,0)); lcd.drawFilledRectangle(bx, by, bw, bh) - lcd.color(c.compasscolor); lcd.drawRectangle(bx, by, bw, bh) - lcd.drawText(cx, by+1, string.format("%03d° %s", heading, labels[heading - (heading % 45)] or (heading.."°")), CENTERED+FONT_XS) + lcd.color(lcd.RGB(0, 0, 0)); + lcd.drawFilledRectangle(bx, by, bw, bh) + lcd.color(c.compasscolor); + lcd.drawRectangle(bx, by, bw, bh) + lcd.drawText(cx, by + 1, string.format("%03d° %s", heading, labels[heading - (heading % 45)] or (heading .. "°")), CENTERED + FONT_XS) end end - -- 8. Altitude bar if c.showaltitude then lcd.color(c.altitudecolor) local barX = x + w - 10 @@ -250,7 +208,6 @@ function render.paint(x, y, w, h, box) lcd.drawText(barX - 4, barY + barH - fillH - 6, label, RIGHT) end - -- 9. Groundspeed bar if c.showgroundspeed then lcd.color(c.groundspeedcolor) local barX = x + 4 @@ -268,7 +225,6 @@ function render.paint(x, y, w, h, box) lcd.setClipping(0, 0, lcd.getWindowSize()) end --- Update rate similar to original (fluid horizon) render.scheduler = 0.00025 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/text.lua b/scripts/dashx/widgets/dashboard/objects/text.lua index a64574d..3135194 100644 --- a/scripts/dashx/widgets/dashboard/objects/text.lua +++ b/scripts/dashx/widgets/dashboard/objects/text.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -14,25 +19,16 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now end @@ -44,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/text/apiversion.lua b/scripts/dashx/widgets/dashboard/objects/text/apiversion.lua index eb36744..4c824d1 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/apiversion.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/apiversion.lua @@ -1,33 +1,9 @@ -local dashx = require("dashx") --[[ - API Version Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if not present - novalue : string -- (Optional) Text shown if telemetry value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -35,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -51,36 +25,35 @@ function render.dirty(box) return false end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.unit = nil -- explicit: API version has no unit + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.unit = nil box._cfg = cfg end return box._cfg @@ -89,10 +62,8 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- Value extraction local value = dashx.session.apiVersion - -- Dots loading indicator if value is nil local displayValue if value == nil then local maxDots = 3 @@ -103,7 +74,6 @@ function render.wakeup(box) displayValue = tostring(value) end - -- Set for dirty() + paint() box._currentDisplayValue = displayValue end @@ -111,19 +81,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Reasonable default refresh render.scheduler = 0.5 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/armflags.lua b/scripts/dashx/widgets/dashboard/objects/text/armflags.lua index 1184d1d..260a5d5 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/armflags.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/armflags.lua @@ -1,42 +1,9 @@ -local dashx = require("dashx") --[[ - Arm Flags Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if not present - thresholds : table -- (Optional) List of thresholds: {value=..., textcolor=...} for coloring ARMED/DISARMED states. - novalue : string -- (Optional) Text shown if telemetry value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value (not used here) - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - - -- Example thresholds: - -- thresholds = { - -- { value = "ARMED", textcolor = "green" }, - -- { value = "DISARMED", textcolor = "red" }, - -- { value = "Throttle high", textcolor = "orange" }, - -- { value = "Failsafe", textcolor = "orange" }, - -- } -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -45,10 +12,8 @@ local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor local armingDisableFlagsToString = dashx.utils.armingDisableFlagsToString --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -61,37 +26,36 @@ function render.dirty(box) return false end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.novalue = getParam(box, "novalue") or "-" - cfg.unit = nil -- explicit: no unit for flags widget - cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.novalue = getParam(box, "novalue") or "-" + cfg.unit = nil + cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) box._cfg = cfg end return box._cfg @@ -104,11 +68,9 @@ function render.wakeup(box) local value = telemetry and telemetry.getSensor("armflags") local disableflags = telemetry and telemetry.getSensor("armdisableflags") - -- displayValue: reason or ARMED/DISARMED local displayValue local showReason = false - -- Prefer disable reason when present and not "OK" if disableflags ~= nil and armingDisableFlagsToString then disableflags = math.floor(disableflags) local reason = armingDisableFlagsToString(disableflags) @@ -118,7 +80,6 @@ function render.wakeup(box) end end - -- Fall back to ARMED/DISARMED string translated if not showReason then if value ~= nil then if value == 1 or value == 3 then @@ -129,7 +90,6 @@ function render.wakeup(box) end end - -- Loading dots only when *no* data at all yet if displayValue == nil and value == nil and disableflags == nil then local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) @@ -139,7 +99,6 @@ function render.wakeup(box) displayValue = cfg.novalue end - -- Dynamic color from thresholds based on the display string box._dynamicTextColor = utils.resolveThresholdColor(displayValue, box, "textcolor", "textcolor") or cfg.defaultTextColor box._currentDisplayValue = displayValue @@ -150,19 +109,10 @@ function render.paint(x, y, w, h, box) local c = box._cfg or {} local textColor = box._dynamicTextColor or c.defaultTextColor - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, c.unit, c.font, c.valuealign, textColor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, c.unit, c.font, c.valuealign, textColor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Reasonable default refresh render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/clock.lua b/scripts/dashx/widgets/dashboard/objects/text/clock.lua index 7b6ccae..ddf2af4 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/clock.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/clock.lua @@ -1,31 +1,9 @@ -local dashx = require("dashx") --[[ - Clock Widget - Configurable Parameters (box table fields): - ------------------------------------------- - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value or configure as "" to omit the unit from being displayed. - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -33,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -49,46 +25,44 @@ function render.dirty(box) return false end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.unit = getParam(box, "unit") -- allow "" to hide + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.unit = getParam(box, "unit") box._cfg = cfg end return box._cfg end function render.wakeup(box) - -- Ensure static cfg present + local cfg = ensureCfg(box) - -- Always use system time local now = os.time() local t = os.date("*t", now) local displayValue = string.format("%02d:%02d:%02d", t.hour, t.min, t.sec) @@ -100,19 +74,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Update once per second is enough for a clock render.scheduler = 1.0 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/craftname.lua b/scripts/dashx/widgets/dashboard/objects/text/craftname.lua index 092401e..33d494f 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/craftname.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/craftname.lua @@ -1,33 +1,9 @@ -local dashx = require("dashx") --[[ - Craft Name Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if not present - novalue : string -- (Optional) Text shown if craft name is missing (default: "-") - unit : string -- (Optional) Unit label to append to value - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -35,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if not dashx.session.telemetryState then return false end if box._lastDisplayValue == nil then @@ -52,37 +26,36 @@ function render.dirty(box) return false end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.novalue = getParam(box, "novalue") or "Craftname not set" - cfg.unit = nil -- explicit: no unit for craft name + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.novalue = getParam(box, "novalue") or "Craftname not set" + cfg.unit = nil box._cfg = cfg end return box._cfg @@ -94,10 +67,7 @@ function render.wakeup(box) local value = dashx.session.craftName local telemetryActive = dashx.session and dashx.session.isConnected - -- Cache last valid value when telemetry is active and string is non-blank - if value and type(value) == "string" and value:match("^%s*$") == nil and telemetryActive then - box._lastValidCraftName = value - end + if value and type(value) == "string" and value:match("^%s*$") == nil and telemetryActive then box._lastValidCraftName = value end local displayValue if value and type(value) == "string" and value:match("^%s*$") == nil then @@ -105,7 +75,7 @@ function render.wakeup(box) elseif box._lastValidCraftName then displayValue = box._lastValidCraftName else - -- Loading dots animation if value has never been seen + local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) @@ -119,19 +89,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Reasonable default refresh (for loading dots) render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/governor.lua b/scripts/dashx/widgets/dashboard/objects/text/governor.lua index 0e9fe85..004f92e 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/governor.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/governor.lua @@ -1,41 +1,9 @@ -local dashx = require("dashx") --[[ - Governor State Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Vertical gap between title and value text - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - displayValue : any -- (Optional) Value to display (processed governor state) - unit : string -- (Not used) - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - - -- Example thresholds: - -- thresholds = { - -- { value = "DISARMED", textcolor = "red" }, - -- { value = "ACTIVE", textcolor = "green" }, - -- ... - -- } -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -43,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- Allow external invalidation when runtime params change function render.invalidate(box) box._cfg = nil end --- Dirty check: only repaint when the displayed value actually changed function render.dirty(box) if not dashx.session.telemetryState then return false end if box._lastDisplayValue == nil then @@ -60,36 +26,35 @@ function render.dirty(box) return false end --- Build/refresh static config if needed (theme & params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- you can bump this externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.unit = nil - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.unit = nil + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) box._cfg = cfg end return box._cfg @@ -98,12 +63,10 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- Pull governor value from telemetry and translate local telemetry = dashx.tasks.telemetry local raw = telemetry and telemetry.getSensor("governor") local displayValue = dashx.utils.getGovernorState(raw) - -- Loading dots when no telemetry yet if raw == nil then local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) @@ -111,11 +74,8 @@ function render.wakeup(box) if displayValue == "" then displayValue = "." end end - if displayValue == nil or displayValue == "" then - displayValue = getParam(box, "novalue") or "-" - end + if displayValue == nil or displayValue == "" then displayValue = getParam(box, "novalue") or "-" end - -- Dynamic color from thresholds (uses string state) box._dynamicTextColor = utils.resolveThresholdColor(displayValue, box, "textcolor", "textcolor") or cfg.defaultTextColor box._isLoadingDots = (raw == nil) @@ -128,21 +88,12 @@ function render.paint(x, y, w, h, box) local c = box._cfg or {} local unitForPaint = box._isLoadingDots and nil or c.unit - local textColor = box._dynamicTextColor or c.defaultTextColor - - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, unitForPaint, c.font, c.valuealign, textColor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + local textColor = box._dynamicTextColor or c.defaultTextColor + + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, unitForPaint, c.font, c.valuealign, textColor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Governor state doesn’t need ultra-high refresh; keep consistent with other widgets render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/pidrates.lua b/scripts/dashx/widgets/dashboard/objects/text/pidrates.lua index a125d8a..4c7f744 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/pidrates.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/pidrates.lua @@ -1,60 +1,9 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") ---[[ - PID/Rates Profile Display Object - - Configurable Parameters (box table fields): - ------------------------------------------- - - -- Profile Source Selection - object : string -- Required: must be "pid" or "rates"; maps to telemetry source "pid_profile" or "rate_profile" - profilecount : number -- (Optional) How many profile numbers to draw (1 to 6, default 6) - - -- Telemetry and Value Handling - value : number -- (Optional) Static fallback value if telemetry is unavailable - transform : string|function|number -- (Optional) Value transform logic (e.g., "floor", multiplier, or custom function) - decimals : number -- (Optional) Decimal precision for transformed value - thresholds : table -- (Optional) Value threshold list: { value=..., textcolor=... } - novalue : string -- (Optional) Fallback text if no telemetry or static value is available - unit : string -- (Optional) Placeholder only; not used in this object - - -- Value Styling and Alignment - font : font -- (Optional) Font for profile number text - textcolor : color -- (Optional) Text color for inactive profile / rates - fillcolor : color -- (Optional) Text color for active profile / rates - valuealign : string -- (Optional) Ignored; profile numbers are always centered - valuepadding : number -- (Optional) General padding around value area (overridden by sides) - valuepaddingleft : number - valuepaddingright : number - valuepaddingtop : number - valuepaddingbottom : number - - -- Title Styling - title : string -- (Optional) Title label (e.g., "Active Profile") - titlepos : string -- (Optional) "top" or "bottom" - titlealign : string -- (Optional) Title alignment: "center", "left", or "right" - titlefont : font -- (Optional) Title font (e.g., FONT_L) - titlespacing : number -- (Optional) Gap between title and profile number row - titlecolor : color -- (Optional) Title text color - titlepadding : number -- (Optional) General padding around title (overridden by sides) - titlepaddingleft : number - titlepaddingright : number - titlepaddingtop : number - titlepaddingbottom : number - - -- Row Layout and Font Options - rowalign : string -- (Optional) Alignment for number row: "left", "center", or "right" - rowspacing : number -- (Optional) Spacing between profile numbers (default: width / profilecount) - rowfont : font -- (Optional) Font for profile numbers (fallbacks to `font`) - rowpadding : number -- (Optional) General padding for number row (overridden by sides) - rowpaddingleft : number - rowpaddingright : number - rowpaddingtop : number - rowpaddingbottom : number - highlightlarger : boolean -- (Optional) If true, enlarges the active index using the next font in the list - - -- Background - bgcolor : color -- (Optional) Widget background color -]] local render = {} @@ -62,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation: call when runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -78,18 +25,16 @@ function render.dirty(box) return false end --- Build/refresh static config (theme & params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version + cfg._theme_version = theme_version + cfg._param_version = param_version - -- Source/object selection (static) - cfg.object = getParam(box, "object") + cfg.object = getParam(box, "object") if cfg.object == "pid" then cfg.source = "pid_profile" elseif cfg.object == "rates" then @@ -98,46 +43,42 @@ local function ensureCfg(box) cfg.source = nil end - -- Title styling - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - -- Value styling/static params - cfg.font = getParam(box, "font") or FONT_L - cfg.valuealign = getParam(box, "valuealign") - cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.fillcolor = utils.resolveThemeColor("fillcolor", getParam(box, "fillcolor")) - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") or FONT_L + cfg.valuealign = getParam(box, "valuealign") + cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.fillcolor = utils.resolveThemeColor("fillcolor", getParam(box, "fillcolor")) + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - -- Row layout/static - cfg.rowalign = getParam(box, "rowalign") - cfg.rowpadding = getParam(box, "rowpadding") - cfg.rowpaddingleft = getParam(box, "rowpaddingleft") - cfg.rowpaddingright = getParam(box, "rowpaddingright") - cfg.rowpaddingtop = getParam(box, "rowpaddingtop") - cfg.rowpaddingbottom = getParam(box, "rowpaddingbottom") - cfg.rowspacing = getParam(box, "rowspacing") - cfg.rowfont = getParam(box, "rowfont") - cfg.highlightlarger = getParam(box, "highlightlarger") - cfg.profilecount = math.max(1, math.min(6, tonumber(getParam(box, "profilecount")) or 6)) - - -- Misc - cfg.novalue = getParam(box, "novalue") or "-" - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.fontList = (utils.getFontListsForResolution().value_default) or {} + cfg.rowalign = getParam(box, "rowalign") + cfg.rowpadding = getParam(box, "rowpadding") + cfg.rowpaddingleft = getParam(box, "rowpaddingleft") + cfg.rowpaddingright = getParam(box, "rowpaddingright") + cfg.rowpaddingtop = getParam(box, "rowpaddingtop") + cfg.rowpaddingbottom = getParam(box, "rowpaddingbottom") + cfg.rowspacing = getParam(box, "rowspacing") + cfg.rowfont = getParam(box, "rowfont") + cfg.highlightlarger = getParam(box, "highlightlarger") + cfg.profilecount = math.max(1, math.min(6, tonumber(getParam(box, "profilecount")) or 6)) + + cfg.novalue = getParam(box, "novalue") or "-" + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.fontList = (utils.getFontListsForResolution().value_default) or {} box._cfg = cfg end @@ -149,16 +90,12 @@ function render.wakeup(box) local telemetry = dashx.tasks.telemetry local value - if telemetry and cfg.source then - value = select(1, telemetry.getSensor(cfg.source)) - end - if value == nil then - value = getParam(box, "value") - end + if telemetry and cfg.source then value = select(1, telemetry.getSensor(cfg.source)) end + if value == nil then value = getParam(box, "value") end local displayValue if value == nil then - -- loading dots + local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) @@ -168,16 +105,10 @@ function render.wakeup(box) end local index = tonumber(displayValue) - if index == nil or index < 1 or index > 6 then - if value ~= nil then - displayValue = cfg.novalue - end - end + if index == nil or index < 1 or index > 6 then if value ~= nil then displayValue = cfg.novalue end end - -- Dynamic text color based on thresholds and *numeric* value when present local dynColor = utils.resolveThresholdColor(value, box, "textcolor", "textcolor") or cfg.defaultTextColor - -- Set for dirty()/paint() box._currentDisplayValue = displayValue box._dynamicTextColor = dynColor box._isLoadingDots = (value == nil) @@ -187,41 +118,33 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - nil, nil, c.font, c.valuealign, box._dynamicTextColor or c.defaultTextColor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) - - -- Draw row of numbers 1..profilecount, highlighting the active index + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, nil, nil, c.font, c.valuealign, box._dynamicTextColor or c.defaultTextColor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) + local fontList = c.fontList or {} local baseFont = _G[c.rowfont] or _G[c.font] or FONT_L local baseIndex - for i, f in ipairs(fontList) do if f == baseFont then baseIndex = i; break end end - local largerFont = baseFont - if c.highlightlarger and baseIndex and baseIndex < #fontList then - largerFont = fontList[baseIndex + 1] + for i, f in ipairs(fontList) do + if f == baseFont then + baseIndex = i; + break + end end + local largerFont = baseFont + if c.highlightlarger and baseIndex and baseIndex < #fontList then largerFont = fontList[baseIndex + 1] end lcd.font(baseFont) local _, baseHeight = lcd.getTextSize("8") local rowpadding = c.rowpadding or 0 - local padLeft = c.rowpaddingleft or rowpadding - local padRight = c.rowpaddingright or rowpadding - local padTop = c.rowpaddingtop or rowpadding - local padBottom = c.rowpaddingbottom or rowpadding + local padLeft = c.rowpaddingleft or rowpadding + local padRight = c.rowpaddingright or rowpadding + local padTop = c.rowpaddingtop or rowpadding + local padBottom = c.rowpaddingbottom or rowpadding local rowY = y + padTop - if c.title then - rowY = y + h - baseHeight - padBottom - end + if c.title then rowY = y + h - baseHeight - padBottom end local totalWidth = w - padLeft - padRight local count = c.profilecount or 6 @@ -238,7 +161,6 @@ function render.paint(x, y, w, h, box) startX = x + padLeft + (totalWidth - totalContentWidth) / 2 end - -- Active index from displayValue local activeIndex = tonumber(box._currentDisplayValue) for i = 1, count do diff --git a/scripts/dashx/widgets/dashboard/objects/text/session.lua b/scripts/dashx/widgets/dashboard/objects/text/session.lua index 36d8dae..ae03a74 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/session.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/session.lua @@ -1,32 +1,9 @@ -local dashx = require("dashx") --[[ - Session Value Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - source : string -- Session variable to display - unit : string -- (Optional) Unit label to append to value - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -34,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -50,40 +25,39 @@ function render.dirty(box) return true end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.defaultTextColor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.source = getParam(box, "source") - cfg.novalue = getParam(box, "novalue") or "-" - cfg.manualUnit = getParam(box, "unit") -- "" allowed to hide + cfg.source = getParam(box, "source") + cfg.novalue = getParam(box, "novalue") or "-" + cfg.manualUnit = getParam(box, "unit") box._cfg = cfg end @@ -93,18 +67,14 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- Value extraction from session table local value = cfg.source and dashx.session[cfg.source] - if type(value) == "boolean" then - value = value and "TRUE" or "FALSE" - end + if type(value) == "boolean" then value = value and "TRUE" or "FALSE" end - -- Decide display value and unit local displayValue local unit = cfg.manualUnit if value == nil then - -- Animated loading dots if not yet available + local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) @@ -120,34 +90,21 @@ function render.wakeup(box) end end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end - -- Expose dynamic-only fields for paint box._currentDisplayValue = displayValue box._dyn_unit = unit - box._dyn_textcolor = nil -- no thresholds here, use default + box._dyn_textcolor = nil end function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, c.defaultTextColor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, c.defaultTextColor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Reasonable refresh for loading dots render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/stats.lua b/scripts/dashx/widgets/dashboard/objects/text/stats.lua index f71b198..ae8d7f0 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/stats.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/stats.lua @@ -1,51 +1,9 @@ -local dashx = require("dashx") --[[ - Stats Display Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - - -- Title & Layout - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL) - titlespacing : number -- (Optional) Vertical gap between title and value - titlecolor : color -- (Optional) Title text color (theme fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - - -- Stat Source & Value - source : string -- (Required for stat mode) Telemetry sensor name used to fetch stats (e.g., "rpm", "current") - stattype : string -- (Optional) Which stat to show ("max", "min", "avg", etc; default: "max") - value : any -- (Optional, advanced) Static value. If omitted, widget shows the selected stat for 'source' - - -- Value Display - unit : string -- (Optional) Dynamic localized unit displayed by default, you can use override this or "" to hide unit - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL) - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - - -- General - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) - decimals : number -- (Optional) Number of decimal places for numeric display - thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - - Notes: - - The widget only displays stat values (not live telemetry). "source" and "stattype" select which telemetry stat to show. - - "unit" always overrides; if not set, unit is resolved from telemetry.sensorTable[source] if available. - - To display min stats, set stattype = "min"; for max, omit or set stattype = "max". ---]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -55,7 +13,7 @@ local resolveThemeColor = utils.resolveThemeColor local lastDisplayValue = nil function render.dirty(box) - -- Always dirty on first run + if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue return true @@ -69,12 +27,9 @@ function render.dirty(box) return false end --- Precompile the value transform local function compileTransform(t, decimals) local pow = decimals and (10 ^ decimals) or nil - local function round(v) - return pow and (math.floor(v * pow + 0.5) / pow) or v - end + local function round(v) return pow and (math.floor(v * pow + 0.5) / pow) or v end if type(t) == "number" then local mul = t @@ -96,84 +51,70 @@ function render.wakeup(box) local telemetry = dashx.tasks.telemetry - -- Reuse cache table local c = box._cache or {} box._cache = c - -- Build static config once local cfg = box._cfg if not cfg then cfg = {} - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.source = getParam(box, "source") - cfg.stattype = getParam(box, "stattype") or "max" - cfg.manualUnit = getParam(box, "unit") - cfg.decimals = getParam(box, "decimals") - cfg.transform = getParam(box, "transform") - cfg.transformFn = compileTransform(cfg.transform, cfg.decimals) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.source = getParam(box, "source") + cfg.stattype = getParam(box, "stattype") or "max" + cfg.manualUnit = getParam(box, "unit") + cfg.decimals = getParam(box, "decimals") + cfg.transform = getParam(box, "transform") + cfg.transformFn = compileTransform(cfg.transform, cfg.decimals) box._cfg = cfg end - -- Value extraction local source = cfg.source local statType = cfg.stattype local value, unit - -- Determine if telemetry is active local telemetryActive = dashx.session and dashx.session.isConnected if source and telemetry and telemetry.getSensorStats then local stats = telemetry.getSensorStats(source) - if stats and stats[statType] then - value = stats[statType] - end + if stats and stats[statType] then value = stats[statType] end - -- Check localization local sensorDef = telemetry.sensorTable and telemetry.sensorTable[source] local localize = sensorDef and sensorDef.localizations - if sensorDef and sensorDef.unit_string then - unit = sensorDef.unit_string - end + if sensorDef and sensorDef.unit_string then unit = sensorDef.unit_string end - -- Only localize the unit string for display, never the value itself if localize and type(localize) == "function" and value ~= nil then local _, _, localizedUnit = localize(value) if localizedUnit ~= nil then unit = localizedUnit end end end - -- User-specified unit *always* overrides local overrideUnit = cfg.manualUnit - if overrideUnit ~= nil then - unit = overrideUnit - end + if overrideUnit ~= nil then unit = overrideUnit end - -- Cache the last valid value/unit if telemetry is active and value is present if value ~= nil and telemetryActive then box._lastValidValue = value box._lastValidUnit = unit elseif box._lastValidValue ~= nil then - -- Use cached value/unit if telemetry is lost + value = box._lastValidValue unit = box._lastValidUnit end @@ -182,7 +123,7 @@ function render.wakeup(box) local displayValue if value == nil then - -- Show animated dots if stat value is not available yet + local maxDots = 3 if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) @@ -192,56 +133,42 @@ function render.wakeup(box) displayValue = cfg.transformFn(value) end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end - -- Set box.value so dashboard/dirty can track change for redraws box._currentDisplayValue = displayValue - -- Resolve colors local textcolor = utils.resolveThresholdColor(value, box, "textcolor", "textcolor") - -- Mutate cache - c.displayValue = displayValue - c.unit = unit - c.textcolor = textcolor - c.title = cfg.title - c.titlepos = cfg.titlepos - c.titlealign = cfg.titlealign - c.titlefont = cfg.titlefont - c.titlespacing = cfg.titlespacing - c.titlepadding = cfg.titlepadding - c.titlepaddingleft = cfg.titlepaddingleft - c.titlepaddingright = cfg.titlepaddingright - c.titlepaddingtop = cfg.titlepaddingtop + c.displayValue = displayValue + c.unit = unit + c.textcolor = textcolor + c.title = cfg.title + c.titlepos = cfg.titlepos + c.titlealign = cfg.titlealign + c.titlefont = cfg.titlefont + c.titlespacing = cfg.titlespacing + c.titlepadding = cfg.titlepadding + c.titlepaddingleft = cfg.titlepaddingleft + c.titlepaddingright = cfg.titlepaddingright + c.titlepaddingtop = cfg.titlepaddingtop c.titlepaddingbottom = cfg.titlepaddingbottom - c.titlecolor = cfg.titlecolor - c.font = cfg.font - c.valuealign = cfg.valuealign - c.valuepadding = cfg.valuepadding - c.valuepaddingleft = cfg.valuepaddingleft - c.valuepaddingright = cfg.valuepaddingright - c.valuepaddingtop = cfg.valuepaddingtop + c.titlecolor = cfg.titlecolor + c.font = cfg.font + c.valuealign = cfg.valuealign + c.valuepadding = cfg.valuepadding + c.valuepaddingleft = cfg.valuepaddingleft + c.valuepaddingright = cfg.valuepaddingright + c.valuepaddingtop = cfg.valuepaddingtop c.valuepaddingbottom = cfg.valuepaddingbottom - c.bgcolor = cfg.bgcolor + c.bgcolor = cfg.bgcolor end function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/telemetry.lua b/scripts/dashx/widgets/dashboard/objects/text/telemetry.lua index 20ebcab..ab0b87e 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/telemetry.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/telemetry.lua @@ -1,37 +1,9 @@ -local dashx = require("dashx") --[[ - Telemetry Value Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if telemetry is not present - source : string -- Telemetry sensor source name (e.g., "voltage", "current") - transform : string|function|number -- (Optional) Value transformation ("floor", "ceil", "round", multiplier, or custom function) - decimals : number -- (Optional) Number of decimal places for numeric display - thresholds : table -- (Optional) List of threshold tables: {value=..., textcolor=...} - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value or configure as "" to omit the unit from being displayed. If not specified, the widget attempts to resolve a dynamic unit - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -39,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if not dashx.session.telemetryState then return false end if box._lastDisplayValue == nil then @@ -56,12 +26,9 @@ function render.dirty(box) return false end --- Precompile the value transform local function compileTransform(t, decimals) local pow = decimals and (10 ^ decimals) or nil - local function round(v) - return pow and (math.floor(v * pow + 0.5) / pow) or v - end + local function round(v) return pow and (math.floor(v * pow + 0.5) / pow) or v end if type(t) == "string" then return t @@ -81,41 +48,40 @@ local function compileTransform(t, decimals) end end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.source = getParam(box, "source") - cfg.manualUnit = getParam(box, "unit") -- "" allowed to hide - cfg.decimals = getParam(box, "decimals") - cfg.transform = getParam(box, "transform") - cfg.transformFn = compileTransform(cfg.transform, cfg.decimals) - cfg.novalue = getParam(box, "novalue") or "-" + cfg.source = getParam(box, "source") + cfg.manualUnit = getParam(box, "unit") + cfg.decimals = getParam(box, "decimals") + cfg.transform = getParam(box, "transform") + cfg.transformFn = compileTransform(cfg.transform, cfg.decimals) + cfg.novalue = getParam(box, "novalue") or "-" box._cfg = cfg end @@ -127,13 +93,12 @@ function render.wakeup(box) local telemetry = dashx.tasks.telemetry - -- Value extraction local source = cfg.source local thresholdsCfg = getParam(box, "thresholds") local value, _, dynamicUnit, _, _, localizedThresholds if source == "txbatt" then - local src = system.getSource({ category = CATEGORY_SYSTEM, member = MAIN_VOLTAGE }) + local src = system.getSource({category = CATEGORY_SYSTEM, member = MAIN_VOLTAGE}) value = src and src.value and src:value() or nil dynamicUnit = "V" localizedThresholds = thresholdsCfg @@ -141,29 +106,26 @@ function render.wakeup(box) value, _, dynamicUnit, _, _, localizedThresholds = telemetry.getSensor(source, nil, nil, thresholdsCfg) end - -- Transform and decimals local displayValue if value ~= nil then if type(cfg.transformFn) == "string" then displayValue = value - else + else displayValue = cfg.transformFn(value) end else - -- Animated loading dots if no telemetry value + local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) displayValue = string.rep(".", box._dotCount) if displayValue == "" then displayValue = "." end end - -- Threshold logic (use localized thresholds) local textcolor = utils.resolveThresholdColor(value, box, "textcolor", "textcolor", localizedThresholds) - -- Dynamic unit logic (User can force a unit or omit unit using "" to hide) local unit if cfg.manualUnit ~= nil then - unit = cfg.manualUnit -- use user value, even if "" + unit = cfg.manualUnit elseif dynamicUnit ~= nil then unit = dynamicUnit elseif source and telemetry and telemetry.sensorTable[source] then @@ -172,15 +134,10 @@ function render.wakeup(box) unit = "" end - -- Suppress unit if we're displaying loading dots - if type(displayValue) == "string" and displayValue:match("^%.+$") then - unit = nil - end + if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil end - -- Set current value for dirty() + paint() box._currentDisplayValue = displayValue - -- Store dynamic-only fields for paint box._dyn_textcolor = textcolor box._dyn_unit = unit end @@ -189,19 +146,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, box._dyn_textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, box._dyn_textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Reasonable default refresh render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/text.lua b/scripts/dashx/widgets/dashboard/objects/text/text.lua index 04af7bd..3a2a364 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/text.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/text.lua @@ -1,37 +1,9 @@ -local dashx = require("dashx") --[[ - Text Display Widget (Static/Label) - - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text displayed above or below the value - titlepos : string -- (Optional) Title position: "top" or "bottom" - titlealign : string -- (Optional) Title alignment: "center", "left", or "right" - titlefont : font -- (Optional) Font for title (e.g., FONT_L, FONT_XL). Uses theme or default if unset. - titlespacing : number -- (Optional) Vertical gap between title and value (pixels) - titlecolor : color -- (Optional) Title text color (theme fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : string|number -- (Optional) **Static** value to display (required for this widget) - font : font -- (Optional) Font for value (e.g., FONT_L, FONT_XL). Uses theme or default if unset. - valuealign : string -- (Optional) Value alignment: "center", "left", or "right" - textcolor : color -- (Optional) Value text color (theme fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) - novalue : string -- (Optional) Text to show if value is nil (default: "-") - - -- Note: - -- This widget is for **static or label text only**. It does not support live telemetry or stats. - -- If you need dynamic stats or telemetry (min/max/live), use `stats.lua` or other appropriate widgets. -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -39,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- External invalidation if runtime params change at runtime function render.invalidate(box) box._cfg = nil end --- Only repaint when the displayed value changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -55,45 +25,40 @@ function render.dirty(box) return false end --- Build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - - -- title + layout - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - -- value style - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - -- colours - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - -- static value + fallbacks - cfg.novalue = getParam(box, "novalue") or "-" - cfg.unit = nil -- explicit: no unit for plain text widget + cfg.novalue = getParam(box, "novalue") or "-" + cfg.unit = nil box._cfg = cfg end @@ -103,7 +68,6 @@ end function render.wakeup(box) local cfg = ensureCfg(box) - -- Compute display value from params; this is static unless params change local value = getParam(box, "value") local displayValue = (value ~= nil) and tostring(value) or cfg.novalue @@ -114,19 +78,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- Static label: no need for frequent wakeups; keep it slow render.scheduler = 2.0 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/text/watts.lua b/scripts/dashx/widgets/dashboard/objects/text/watts.lua index 2bd36c0..b01f51f 100644 --- a/scripts/dashx/widgets/dashboard/objects/text/watts.lua +++ b/scripts/dashx/widgets/dashboard/objects/text/watts.lua @@ -1,43 +1,9 @@ -local dashx = require("dashx") --[[ - Dynamic Power (Watts) Display Widget - - Computes and displays instantaneous, min, max, or average power by reading voltage and current sensors. - - Configurable Parameters (box table fields): - ------------------------------------------- - title : string -- (Optional) Title text displayed above or below the value - titlepos : string -- "top" or "bottom" (default) - titlealign : string -- "center", "left", or "right" - titlefont : font -- Font for title (e.g., FONT_L) - titlespacing : number -- Vertical gap between title and value (pixels) - titlecolor : color -- Title text color - titlepadding : number -- Padding for title (all sides) - font : font -- Font for value (e.g., FONT_XL) - valuealign : string -- "center", "left", or "right" - textcolor : color -- Value text color - valuepadding : number -- Padding for value (all sides) - bgcolor : color -- Widget background color - novalue : string -- Text to show if sensors unavailable (default: "-") - source : string -- "current", "min", "max", or "avg" (default: "current") -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- ---[[ - Dynamic Power (Watts) Display Widget — cached version - - Computes and displays instantaneous, min, max, or average power by reading - voltage and current sensors. Caches all static params once into box._cfg, - only recomputes the dynamic value/unit per tick. - - Params (box fields): - title, titlepos, titlealign, titlefont, titlespacing, - titlepadding, titlepaddingleft, titlepaddingright, titlepaddingtop, titlepaddingbottom, - font, valuealign, valuepadding, valuepaddingleft, valuepaddingright, valuepaddingtop, valuepaddingbottom, - textcolor, titlecolor, bgcolor, - novalue (string, default "-"), - source ("current" | "min" | "max" | "avg", default "current"), - unit (optional manual override; "" to hide) -]] +local dashx = require("dashx") local render = {} @@ -45,10 +11,8 @@ local utils = dashx.widgets.dashboard.utils local getParam = utils.getParam local resolveThemeColor = utils.resolveThemeColor --- external invalidation hook function render.invalidate(box) box._cfg = nil end --- repaint only when value actually changes function render.dirty(box) if box._lastDisplayValue == nil then box._lastDisplayValue = box._currentDisplayValue @@ -61,49 +25,47 @@ function render.dirty(box) return false end --- build/refresh static config (theme/params aware) local function ensureCfg(box) local theme_version = (dashx and dashx.theme and dashx.theme.version) or 0 - local param_version = box._param_version or 0 -- bump externally when params change + local param_version = box._param_version or 0 local cfg = box._cfg if (not cfg) or (cfg._theme_version ~= theme_version) or (cfg._param_version ~= param_version) then cfg = {} - cfg._theme_version = theme_version - cfg._param_version = param_version - - cfg.title = getParam(box, "title") - cfg.titlepos = getParam(box, "titlepos") - cfg.titlealign = getParam(box, "titlealign") - cfg.titlefont = getParam(box, "titlefont") - cfg.titlespacing = getParam(box, "titlespacing") - cfg.titlepadding = getParam(box, "titlepadding") - cfg.titlepaddingleft = getParam(box, "titlepaddingleft") - cfg.titlepaddingright = getParam(box, "titlepaddingright") - cfg.titlepaddingtop = getParam(box, "titlepaddingtop") + cfg._theme_version = theme_version + cfg._param_version = param_version + + cfg.title = getParam(box, "title") + cfg.titlepos = getParam(box, "titlepos") + cfg.titlealign = getParam(box, "titlealign") + cfg.titlefont = getParam(box, "titlefont") + cfg.titlespacing = getParam(box, "titlespacing") + cfg.titlepadding = getParam(box, "titlepadding") + cfg.titlepaddingleft = getParam(box, "titlepaddingleft") + cfg.titlepaddingright = getParam(box, "titlepaddingright") + cfg.titlepaddingtop = getParam(box, "titlepaddingtop") cfg.titlepaddingbottom = getParam(box, "titlepaddingbottom") - cfg.font = getParam(box, "font") - cfg.valuealign = getParam(box, "valuealign") - cfg.valuepadding = getParam(box, "valuepadding") - cfg.valuepaddingleft = getParam(box, "valuepaddingleft") - cfg.valuepaddingright = getParam(box, "valuepaddingright") - cfg.valuepaddingtop = getParam(box, "valuepaddingtop") + cfg.font = getParam(box, "font") + cfg.valuealign = getParam(box, "valuealign") + cfg.valuepadding = getParam(box, "valuepadding") + cfg.valuepaddingleft = getParam(box, "valuepaddingleft") + cfg.valuepaddingright = getParam(box, "valuepaddingright") + cfg.valuepaddingtop = getParam(box, "valuepaddingtop") cfg.valuepaddingbottom = getParam(box, "valuepaddingbottom") - cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) - cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) - cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) + cfg.titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")) + cfg.textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")) + cfg.bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) - cfg.novalue = getParam(box, "novalue") or "-" - cfg.source = (getParam(box, "source") or "current"):lower() - cfg.manualUnit = getParam(box, "unit") -- "" allowed to hide + cfg.novalue = getParam(box, "novalue") or "-" + cfg.source = (getParam(box, "source") or "current"):lower() + cfg.manualUnit = getParam(box, "unit") box._cfg = cfg end return box._cfg end --- helpers for loading dots local function nextDots(box) local maxDots = 3 box._dotCount = ((box._dotCount or 0) + 1) % (maxDots + 1) @@ -130,9 +92,9 @@ function render.wakeup(box) local function statsWatts(kind) if not (vStats and iStats) then return nil end - if kind == "min" and vStats.min and iStats.min then return vStats.min * iStats.min end - if kind == "max" and vStats.max and iStats.max then return vStats.max * iStats.max end - if kind == "avg" and vStats.avg and iStats.avg then return vStats.avg * iStats.avg end + if kind == "min" and vStats.min and iStats.min then return vStats.min * iStats.min end + if kind == "max" and vStats.max and iStats.max then return vStats.max * iStats.max end + if kind == "avg" and vStats.avg and iStats.avg then return vStats.avg * iStats.avg end return nil end @@ -145,13 +107,11 @@ function render.wakeup(box) value = nil end - -- cache last valid number if type(value) == "number" and telemetryActive then box._lastValidValue = value box._lastValidUnit = "W" end - -- use last valid if unavailable; else show dots until first value local displayValue local unit = cfg.manualUnit @@ -165,7 +125,6 @@ function render.wakeup(box) unit = nil end - -- manual unit override (including "" to hide) always wins unless dots if type(displayValue) == "string" and displayValue:match("^%.+$") then unit = nil elseif cfg.manualUnit ~= nil then @@ -182,19 +141,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cfg or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, box._currentDisplayValue, box._dyn_unit, c.font, c.valuealign, c.textcolor, c.valuepadding, + c.valuepaddingleft, c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- frequent enough for live power render.scheduler = 0.5 -return render \ No newline at end of file +return render diff --git a/scripts/dashx/widgets/dashboard/objects/time.lua b/scripts/dashx/widgets/dashboard/objects/time.lua index 98471cd..1c2ddc7 100644 --- a/scripts/dashx/widgets/dashboard/objects/time.lua +++ b/scripts/dashx/widgets/dashboard/objects/time.lua @@ -1,3 +1,8 @@ +--[[ + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + local dashx = require("dashx") local wrapper = {} @@ -5,7 +10,6 @@ local renders = dashx.widgets.dashboard.renders local folder = "SCRIPTS:/" .. dashx.config.baseDir .. "/widgets/dashboard/objects/time/" local utils = dashx.widgets.dashboard.utils - function wrapper.paint(x, y, w, h, box) local subtype = box.subtype or "flight" local render = renders[subtype] @@ -15,27 +19,18 @@ end function wrapper.wakeup(box) - -- Ensure model preferences and telemetry are available - if not utils.isModelPrefsReady() then - utils.resetBoxCache(box) - end + if not utils.isModelPrefsReady() then utils.resetBoxCache(box) end - -- Wakeup interval control using optional parameter (wakeupinterval) if box.wakeupinterval ~= nil then - local now = os.clock() + local now = os.clock() - -- initialize on first use box._wakeupInterval = box._wakeupInterval or interval - box._lastWakeup = box._lastWakeup or 0 + box._lastWakeup = box._lastWakeup or 0 - -- if not enough time has passed, bail out - if now - box._lastWakeup < box._wakeupInterval then - return - end + if now - box._lastWakeup < box._wakeupInterval then return end - -- record this wakeup box._lastWakeup = now - end + end local subtype = box.subtype or "flight" @@ -45,7 +40,7 @@ function wrapper.wakeup(box) if loader then renders[subtype] = loader() else - return -- silently fail or log error + return end end diff --git a/scripts/dashx/widgets/dashboard/objects/time/count.lua b/scripts/dashx/widgets/dashboard/objects/time/count.lua index fb6a44d..3e4234e 100644 --- a/scripts/dashx/widgets/dashboard/objects/time/count.lua +++ b/scripts/dashx/widgets/dashboard/objects/time/count.lua @@ -1,33 +1,9 @@ -local dashx = require("dashx") --[[ - Flight Count Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if not present - novalue : string -- (Optional) Text shown if value is missing (default: "-") - unit : string -- (Optional) Unit label to append to value - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -57,20 +33,16 @@ function render.wakeup(box) local unit = getParam(box, "unit") local displayValue - -- Detect telemetry state (true if session exists and is connected) local telemetryActive = dashx.session and dashx.session.isConnected - -- Only cache when we receive a valid number *and* telemetry is active - if type(value) == "number" and telemetryActive then - box._lastValidFlightCount = value - end + if type(value) == "number" and telemetryActive then box._lastValidFlightCount = value end if type(value) == "number" then displayValue = tostring(value) elseif box._lastValidFlightCount ~= nil then displayValue = tostring(box._lastValidFlightCount) else - -- Animated "..." indicator when no flight count is available ever + local maxDots = 3 if box._dotCount == nil then box._dotCount = 0 end box._dotCount = (box._dotCount + 1) % (maxDots + 1) @@ -79,32 +51,31 @@ function render.wakeup(box) unit = nil end - -- Set box.value so dashboard/dirty can track change for redraws box._currentDisplayValue = displayValue box._cache = { - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos"), - titlealign = getParam(box, "titlealign"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing"), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos"), + titlealign = getParam(box, "titlealign"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing"), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), titlepaddingbottom = getParam(box, "titlepaddingbottom"), - displayValue = displayValue, - unit = unit, - font = getParam(box, "font"), - valuealign = getParam(box, "valuealign"), - textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), + displayValue = displayValue, + unit = unit, + font = getParam(box, "font"), + valuealign = getParam(box, "valuealign"), + textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), valuepaddingbottom = getParam(box, "valuepaddingbottom"), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) } end @@ -112,16 +83,8 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end return render diff --git a/scripts/dashx/widgets/dashboard/objects/time/flight.lua b/scripts/dashx/widgets/dashboard/objects/time/flight.lua index 2d60c24..657e85f 100644 --- a/scripts/dashx/widgets/dashboard/objects/time/flight.lua +++ b/scripts/dashx/widgets/dashboard/objects/time/flight.lua @@ -1,32 +1,9 @@ -local dashx = require("dashx") --[[ - Flight Time Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - unit : string -- (Optional) Unit label to append to value - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local render = {} @@ -52,12 +29,11 @@ function render.dirty(box) end function render.wakeup(box) - -- Always show the session time (accumulated time since last disconnect) + local value = dashx.session.timer and dashx.session.timer.live local unit = getParam(box, "unit") local displayValue - -- Format to MM:SS if type(value) == "number" and value > 0 then local minutes = math.floor(value / 60) local seconds = math.floor(value % 60) @@ -68,39 +44,33 @@ function render.wakeup(box) unit = nil end - -- use the last display value if the current one is nil - if displayValue == "00:00" and box._lastDisplayValue ~= nil then - displayValue = box._lastDisplayValue - end + if displayValue == "00:00" and box._lastDisplayValue ~= nil then displayValue = box._lastDisplayValue end - -- Set box.value so dashboard/dirty can track change for redraws box._currentDisplayValue = displayValue - - box._cache = { - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos"), - titlealign = getParam(box, "titlealign"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing"), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos"), + titlealign = getParam(box, "titlealign"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing"), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), titlepaddingbottom = getParam(box, "titlepaddingbottom"), - displayValue = displayValue, - unit = unit, - font = getParam(box, "font"), - valuealign = getParam(box, "valuealign"), - textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), + displayValue = displayValue, + unit = unit, + font = getParam(box, "font"), + valuealign = getParam(box, "valuealign"), + textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), valuepaddingbottom = getParam(box, "valuepaddingbottom"), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) } end @@ -108,24 +78,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end - --- set rate at which objects wakeup must be called --- using this value will short circut the spread scheduling in --- dashboard.lua to ensure object gets a heartbeat when required. --- its mostly only used for objects that need to be updated like the --- flight time objects render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/objects/time/total.lua b/scripts/dashx/widgets/dashboard/objects/time/total.lua index d6ac5dd..74032f3 100644 --- a/scripts/dashx/widgets/dashboard/objects/time/total.lua +++ b/scripts/dashx/widgets/dashboard/objects/time/total.lua @@ -1,32 +1,9 @@ -local dashx = require("dashx") --[[ - Total Flight Time Widget - Configurable Parameters (box table fields): - ------------------------------------------- - wakeupinterval : number -- Optional wakeup interval in seconds (set in wrapper) - title : string -- (Optional) Title text - titlepos : string -- (Optional) Title position ("top" or "bottom") - titlealign : string -- (Optional) Title alignment ("center", "left", "right") - titlefont : font -- (Optional) Title font (e.g., FONT_L, FONT_XL), dynamic by default - titlespacing : number -- (Optional) Controls the vertical gap between title text and the value text, regardless of their paddings. - titlecolor : color -- (Optional) Title text color (theme/text fallback if nil) - titlepadding : number -- (Optional) Padding for title (all sides unless overridden) - titlepaddingleft : number -- (Optional) Left padding for title - titlepaddingright : number -- (Optional) Right padding for title - titlepaddingtop : number -- (Optional) Top padding for title - titlepaddingbottom : number -- (Optional) Bottom padding for title - value : any -- (Optional) Static value to display if telemetry is not present - unit : string -- (Optional) Unit label to append to value or configure as "" to omit the unit from being displayed. If not specified, the widget attempts to resolve a dynamic unit - font : font -- (Optional) Value font (e.g., FONT_L, FONT_XL), dynamic by default - valuealign : string -- (Optional) Value alignment ("center", "left", "right") - textcolor : color -- (Optional) Value text color (theme/text fallback if nil) - valuepadding : number -- (Optional) Padding for value (all sides unless overridden) - valuepaddingleft : number -- (Optional) Left padding for value - valuepaddingright : number -- (Optional) Right padding for value - valuepaddingtop : number -- (Optional) Top padding for value - valuepaddingbottom : number -- (Optional) Bottom padding for value - bgcolor : color -- (Optional) Widget background color (theme fallback if nil) -]] + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- + +local dashx = require("dashx") local render = {} @@ -56,7 +33,6 @@ function render.wakeup(box) local unit = getParam(box, "unit") local displayValue - -- Format to HH:MM:SS if type(value) == "number" and value > 0 then local hours = math.floor(value / 3600) local minutes = math.floor((value % 3600) / 60) @@ -70,40 +46,33 @@ function render.wakeup(box) unit = nil end - -- use the last display value if the current one is nil - if displayValue == "00:00:00" and box._lastDisplayValue ~= nil then - displayValue = box._lastDisplayValue - end - + if displayValue == "00:00:00" and box._lastDisplayValue ~= nil then displayValue = box._lastDisplayValue end - -- Set box.value so dashboard/dirty can track change for redraws box._currentDisplayValue = displayValue - - box._cache = { - title = getParam(box, "title"), - titlepos = getParam(box, "titlepos"), - titlealign = getParam(box, "titlealign"), - titlefont = getParam(box, "titlefont"), - titlespacing = getParam(box, "titlespacing"), - titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), - titlepadding = getParam(box, "titlepadding"), - titlepaddingleft = getParam(box, "titlepaddingleft"), - titlepaddingright = getParam(box, "titlepaddingright"), - titlepaddingtop = getParam(box, "titlepaddingtop"), + title = getParam(box, "title"), + titlepos = getParam(box, "titlepos"), + titlealign = getParam(box, "titlealign"), + titlefont = getParam(box, "titlefont"), + titlespacing = getParam(box, "titlespacing"), + titlecolor = resolveThemeColor("titlecolor", getParam(box, "titlecolor")), + titlepadding = getParam(box, "titlepadding"), + titlepaddingleft = getParam(box, "titlepaddingleft"), + titlepaddingright = getParam(box, "titlepaddingright"), + titlepaddingtop = getParam(box, "titlepaddingtop"), titlepaddingbottom = getParam(box, "titlepaddingbottom"), - displayValue = displayValue, - unit = unit, - font = getParam(box, "font"), - valuealign = getParam(box, "valuealign"), - textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), - valuepadding = getParam(box, "valuepadding"), - valuepaddingleft = getParam(box, "valuepaddingleft"), - valuepaddingright = getParam(box, "valuepaddingright"), - valuepaddingtop = getParam(box, "valuepaddingtop"), + displayValue = displayValue, + unit = unit, + font = getParam(box, "font"), + valuealign = getParam(box, "valuealign"), + textcolor = resolveThemeColor("textcolor", getParam(box, "textcolor")), + valuepadding = getParam(box, "valuepadding"), + valuepaddingleft = getParam(box, "valuepaddingleft"), + valuepaddingright = getParam(box, "valuepaddingright"), + valuepaddingtop = getParam(box, "valuepaddingtop"), valuepaddingbottom = getParam(box, "valuepaddingbottom"), - bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")), + bgcolor = resolveThemeColor("bgcolor", getParam(box, "bgcolor")) } end @@ -111,23 +80,10 @@ function render.paint(x, y, w, h, box) x, y = utils.applyOffset(x, y, box) local c = box._cache or {} - utils.box( - x, y, w, h, - c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, - c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, - c.titlepaddingtop, c.titlepaddingbottom, - c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, - c.valuepadding, c.valuepaddingleft, c.valuepaddingright, - c.valuepaddingtop, c.valuepaddingbottom, - c.bgcolor - ) + utils.box(x, y, w, h, c.title, c.titlepos, c.titlealign, c.titlefont, c.titlespacing, c.titlecolor, c.titlepadding, c.titlepaddingleft, c.titlepaddingright, c.titlepaddingtop, c.titlepaddingbottom, c.displayValue, c.unit, c.font, c.valuealign, c.textcolor, c.valuepadding, c.valuepaddingleft, + c.valuepaddingright, c.valuepaddingtop, c.valuepaddingbottom, c.bgcolor) end --- set rate at which objects wakeup must be called --- using this value will short circut the spread scheduling in --- dashboard.lua to ensure object gets a heartbeat when required. --- its mostly only used for objects that need to be updated like the --- flight time objects render.scheduler = 0.5 return render diff --git a/scripts/dashx/widgets/dashboard/themes/@rt-rc/inflight.lua b/scripts/dashx/widgets/dashboard/themes/@rt-rc/inflight.lua index e7d40ee..1812994 100644 --- a/scripts/dashx/widgets/dashboard/themes/@rt-rc/inflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/@rt-rc/inflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 20, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 20, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,242 +97,136 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 4, - rows = 14, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 4, rows = 14, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize - local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -{ - type = "gauge", - subtype = "arc", - col = 1, row = 1, - rowspan = 12, - colspan = 2, - source = "voltage", - thickness = opts.thickness, - font = opts.font, - arcbgcolor = colorMode.arcbgcolor, - title = "VOLTAGE", - titlepos = "bottom", - bgcolor = colorMode.bgcolor, - gaugepadding = opts.gaugepadding, - valuepaddingtop = opts.valuepaddingtop, - min = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 - return math.max(0, cells * minV) - end, - - max = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 - return math.max(0, cells * maxV) - end, - - -- (b) The “dynamic” thresholds (using functions that no longer reference box._cache) - thresholds = { - { - value = function(box) - -- Fetch the raw gaugemin parameter (could itself be a function) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - -- Fetch the raw gaugemax parameter (could itself be a function) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 30% above gaugemin - return raw_gm + 0.30 * (raw_gM - raw_gm) - end, - fillcolor = "red", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 50% above gaugemin - return raw_gm + 0.50 * (raw_gM - raw_gm) - end, - fillcolor = "orange", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Top‐end threshold = gaugemax - return raw_gM - end, - fillcolor = colorMode.fillcolor, - textcolor = colorMode.textcolor + return { + + { + type = "gauge", + subtype = "arc", + col = 1, + row = 1, + rowspan = 12, + colspan = 2, + source = "voltage", + thickness = opts.thickness, + font = opts.font, + arcbgcolor = colorMode.arcbgcolor, + title = "VOLTAGE", + titlepos = "bottom", + bgcolor = colorMode.bgcolor, + gaugepadding = opts.gaugepadding, + valuepaddingtop = opts.valuepaddingtop, + min = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 + return math.max(0, cells * minV) + end, + + max = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 + return math.max(0, cells * maxV) + end, + + thresholds = { + { + value = function(box) + + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.30 * (raw_gM - raw_gm) + end, + fillcolor = "red", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.50 * (raw_gM - raw_gm) + end, + fillcolor = "orange", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gM + end, + fillcolor = colorMode.fillcolor, + textcolor = colorMode.textcolor + } } - } - }, - { - type = "gauge", - subtype = "arc", - col = 3, row = 1, - rowspan = 12, - thickness = opts.thickness, - colspan = 2, - source = "smartfuel", - transform = "floor", - min = 0, - max = 140, - font = opts.font, - arcbgcolor = colorMode.arcbgcolor, - title = "FUEL", - titlepos = "bottom", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - gaugepadding = opts.gaugepadding, - valuepaddingtop = opts.valuepaddingtop, - thresholds = { - { value = 30, fillcolor = "red", textcolor = colorMode.textcolor }, - { value = 50, fillcolor = "orange", textcolor = colorMode.textcolor }, - { value = 140, fillcolor = colorMode.fillcolor, textcolor = colorMode.textcolor } - }, - }, - { - col = 1, - row = 13, - rowspan = 2, - type = "text", - subtype = "telemetry", - nosource = "-", - source = "temp_esc", - transform = "floor", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - }, - { - col = 4, - row = 13, - rowspan = 2, - type = "time", - subtype = "flight", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - }, - { - col = 3, - row = 13, - rowspan = 2, - type = "text", - subtype = "telemetry", - source = "rpm", - nosource = "-", - unit = "rpm", - transform = "floor", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - }, - { - col = 2, - row = 13, - rowspan = 2, - type = "text", - subtype = "telemetry", - source = "rssi", - nosource = "-", - unit = "dB", - transform = "floor", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - } + }, { + type = "gauge", + subtype = "arc", + col = 3, + row = 1, + rowspan = 12, + thickness = opts.thickness, + colspan = 2, + source = "smartfuel", + transform = "floor", + min = 0, + max = 140, + font = opts.font, + arcbgcolor = colorMode.arcbgcolor, + title = "FUEL", + titlepos = "bottom", + bgcolor = colorMode.bgcolor, + titlecolor = colorMode.titlecolor, + textcolor = colorMode.titlecolor, + gaugepadding = opts.gaugepadding, + valuepaddingtop = opts.valuepaddingtop, + thresholds = {{value = 30, fillcolor = "red", textcolor = colorMode.textcolor}, {value = 50, fillcolor = "orange", textcolor = colorMode.textcolor}, {value = 140, fillcolor = colorMode.fillcolor, textcolor = colorMode.textcolor}} + }, {col = 1, row = 13, rowspan = 2, type = "text", subtype = "telemetry", nosource = "-", source = "temp_esc", transform = "floor", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor}, + {col = 4, row = 13, rowspan = 2, type = "time", subtype = "flight", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor}, + {col = 3, row = 13, rowspan = 2, type = "text", subtype = "telemetry", source = "rpm", nosource = "-", unit = "rpm", transform = "floor", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor}, + {col = 2, row = 13, rowspan = 2, type = "text", subtype = "telemetry", source = "rssi", nosource = "-", unit = "dB", transform = "floor", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor} -} + } end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -432,28 +235,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -462,11 +259,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -480,14 +277,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}} diff --git a/scripts/dashx/widgets/dashboard/themes/@rt-rc/init.lua b/scripts/dashx/widgets/dashboard/themes/@rt-rc/init.lua index c88df5b..6d64d2d 100644 --- a/scripts/dashx/widgets/dashboard/themes/@rt-rc/init.lua +++ b/scripts/dashx/widgets/dashboard/themes/@rt-rc/init.lua @@ -1,29 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- --- Theme initialization table -local init = { - name = "@RT-RC", -- Theme name - preflight = "preflight.lua", -- Script to run before takeoff - inflight = "inflight.lua", -- Script to run during flight - postflight = "postflight.lua", -- Script to run after landing - standalone = false, -- If true, theme handles all rendering itself -} -return init \ No newline at end of file +local dashx = require("dashx") + +local init = {name = "@RT-RC", preflight = "preflight.lua", inflight = "inflight.lua", postflight = "postflight.lua", standalone = false} + +return init diff --git a/scripts/dashx/widgets/dashboard/themes/@rt-rc/postflight.lua b/scripts/dashx/widgets/dashboard/themes/@rt-rc/postflight.lua index cab11f6..13169ac 100644 --- a/scripts/dashx/widgets/dashboard/themes/@rt-rc/postflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/@rt-rc/postflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 20, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 20, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,94 +97,54 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 3, - rows = 3, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 3, rows = 3, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { - -- Flight info and RPM info - {col = 1, row = 1, type = "time", subtype = "flight", title = "Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 1, row = 2, type = "time", subtype = "total", title = "Total Model Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 1, row = 3, type = "text", subtype = "stats", source = "rpm", title = "RPM Max", unit = " rpm", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + return { - -- Flight max/min stats 1 - {col = 2, row = 1, type = "text", subtype = "stats", source = "current", title = "Current Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 2, row = 2, type = "text", subtype = "stats", source = "temp_esc", title = "ESC Temp Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 2, row = 3, type = "text", subtype = "watts", source = "max", title = "Max Watts", unit = "W", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 1, row = 1, type = "time", subtype = "flight", title = "Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 1, row = 2, type = "time", subtype = "total", title = "Total Model Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 1, row = 3, type = "text", subtype = "stats", source = "rpm", title = "RPM Max", unit = " rpm", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - -- Flight max/min stats 2 - {col = 3, row = 1, type = "text", subtype = "stats", stattype = "max", source = "smartconsumption", title = "Consumed mAh", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 3, row = 2, type = "text", subtype = "telemetry", source = "smartfuel", title = "Fuel Remaining", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 3, row = 3, type = "text", subtype = "stats", stattype = "min", source = "rssi", title = "Link Min", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor} + {col = 2, row = 1, type = "text", subtype = "stats", source = "current", title = "Current Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 2, row = 2, type = "text", subtype = "stats", source = "temp_esc", title = "ESC Temp Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 2, row = 3, type = "text", subtype = "watts", source = "max", title = "Max Watts", unit = "W", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, -} + {col = 3, row = 1, type = "text", subtype = "stats", stattype = "max", source = "smartconsumption", title = "Consumed mAh", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 3, row = 2, type = "text", subtype = "telemetry", source = "smartfuel", title = "Fuel Remaining", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 3, row = 3, type = "text", subtype = "stats", stattype = "min", source = "rssi", title = "Link Min", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor} + + } end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -284,28 +153,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -314,11 +177,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -332,14 +195,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}} diff --git a/scripts/dashx/widgets/dashboard/themes/@rt-rc/preflight.lua b/scripts/dashx/widgets/dashboard/themes/@rt-rc/preflight.lua index 1e0264b..83e4421 100644 --- a/scripts/dashx/widgets/dashboard/themes/@rt-rc/preflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/@rt-rc/preflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 30, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 40, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 30, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 30, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 40, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 30, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,286 +97,144 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 20, - rows = 8, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 20, rows = 8, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { - - { - col = 1, - row = 1, - colspan = 8, - rowspan = 3, - type = "image", - subtype = "model", - bgcolor = colorMode.bgcolor, - }, - { - col = 1, - row = 4, - colspan = 8, - rowspan = 3, - type = "text", - subtype = "telemetry", - source = "rpm", - nosource= "-", - unit = "", - transform = "floor", - title = "RPM", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 1, - row = 7, - colspan = 4, - rowspan = 2, - type = "text", - subtype = "telemetry", - source = "temp_esc", - title = "TEMP", - titlepos= "bottom", - transform = "floor", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 5, - row = 7, - colspan = 4, - rowspan = 2, - type = "time", - subtype = "count", - title = "FLIGHTS", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 9, - row = 7, - colspan = 6, - rowspan = 2, - type = "time", - subtype = "flight", - title = "TIME", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 15, - row = 7, - colspan = 6, - rowspan = 2, - type = "text", - subtype = "telemetry", - source = "rssi", - nosource= "-", - unit = "dB", - title = "LQ", - titlepos= "bottom", - transform = "floor", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - type = "gauge", - subtype = "arc", - col = 9, - row = 1, - colspan = 6, - rowspan = 6, - thickness= opts.thickness, - source = "smartfuel", - unit = "%", - transform = "floor", - min = 0, - max = 100, - font = opts.font, - arcbgcolor = colorMode.arcbgcolor, - title = "FUEL", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - gaugepadding = opts.gaugepadding, - valuepaddingtop = opts.valuepaddingtop, - --valuepaddingbottom = opts.valuepaddingbottom, - --gaugepaddingtop = opts.gaugepaddingtop, - thresholds = { - { value = 30, fillcolor = "red", textcolor = colorMode.textcolor }, - { value = 50, fillcolor = "orange", textcolor = colorMode.textcolor }, - { value = 140, fillcolor = colorMode.fillcolor, textcolor = colorMode.textcolor } - }, - }, - { - col = 15, - row = 1, - colspan = 6, - rowspan = 6, - type = "gauge", - subtype = "arc", - source = "voltage", - fillbgcolor = colorMode.fillbgcolor, - title = "VOLTAGE", - font = opts.font, - thickness= opts.thickness, - gaugepadding = opts.gaugepadding, - titlepos = "bottom", - fillcolor= colorMode.fillcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - valuepaddingtop = opts.valuepaddingtop, - --valuepaddingbottom = opts.valuepaddingbottom, - --gaugepaddingtop = opts.gaugepaddingtop, - min = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 - return math.max(0, cells * minV) - end, - - max = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 - return math.max(0, cells * maxV) - end, - - -- (b) The “dynamic” thresholds (using functions that no longer reference box._cache) - thresholds = { - { - value = function(box) - -- Fetch the raw gaugemin parameter (could itself be a function) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - -- Fetch the raw gaugemax parameter (could itself be a function) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 30% above gaugemin - return raw_gm + 0.30 * (raw_gM - raw_gm) - end, - fillcolor = "red", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 50% above gaugemin - return raw_gm + 0.50 * (raw_gM - raw_gm) + return { + + {col = 1, row = 1, colspan = 8, rowspan = 3, type = "image", subtype = "model", bgcolor = colorMode.bgcolor}, + {col = 1, row = 4, colspan = 8, rowspan = 3, type = "text", subtype = "telemetry", source = "rpm", nosource = "-", unit = "", transform = "floor", title = "RPM", titlepos = "bottom", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 1, row = 7, colspan = 4, rowspan = 2, type = "text", subtype = "telemetry", source = "temp_esc", title = "TEMP", titlepos = "bottom", transform = "floor", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 5, row = 7, colspan = 4, rowspan = 2, type = "time", subtype = "count", title = "FLIGHTS", titlepos = "bottom", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 9, row = 7, colspan = 6, rowspan = 2, type = "time", subtype = "flight", title = "TIME", titlepos = "bottom", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 15, row = 7, colspan = 6, rowspan = 2, type = "text", subtype = "telemetry", source = "rssi", nosource = "-", unit = "dB", title = "LQ", titlepos = "bottom", transform = "floor", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, { + type = "gauge", + subtype = "arc", + col = 9, + row = 1, + colspan = 6, + rowspan = 6, + thickness = opts.thickness, + source = "smartfuel", + unit = "%", + transform = "floor", + min = 0, + max = 100, + font = opts.font, + arcbgcolor = colorMode.arcbgcolor, + title = "FUEL", + titlepos = "bottom", + titlecolor = colorMode.titlecolor, + textcolor = colorMode.titlecolor, + bgcolor = colorMode.bgcolor, + gaugepadding = opts.gaugepadding, + valuepaddingtop = opts.valuepaddingtop, + + thresholds = {{value = 30, fillcolor = "red", textcolor = colorMode.textcolor}, {value = 50, fillcolor = "orange", textcolor = colorMode.textcolor}, {value = 140, fillcolor = colorMode.fillcolor, textcolor = colorMode.textcolor}} + }, { + col = 15, + row = 1, + colspan = 6, + rowspan = 6, + type = "gauge", + subtype = "arc", + source = "voltage", + fillbgcolor = colorMode.fillbgcolor, + title = "VOLTAGE", + font = opts.font, + thickness = opts.thickness, + gaugepadding = opts.gaugepadding, + titlepos = "bottom", + fillcolor = colorMode.fillcolor, + titlecolor = colorMode.titlecolor, + textcolor = colorMode.titlecolor, + bgcolor = colorMode.bgcolor, + valuepaddingtop = opts.valuepaddingtop, + + min = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 + return math.max(0, cells * minV) end, - fillcolor = "orange", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Top‐end threshold = gaugemax - return raw_gM + + max = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 + return math.max(0, cells * maxV) end, - fillcolor = colorMode.fillcolor, - textcolor = colorMode.textcolor - } - } + thresholds = { + { + value = function(box) + + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.30 * (raw_gM - raw_gm) + end, + fillcolor = "red", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.50 * (raw_gM - raw_gm) + end, + fillcolor = "orange", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gM + end, + fillcolor = colorMode.fillcolor, + textcolor = colorMode.textcolor + } + } + } } -} end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -476,28 +243,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -506,11 +267,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -524,14 +285,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}} diff --git a/scripts/dashx/widgets/dashboard/themes/default/inflight.lua b/scripts/dashx/widgets/dashboard/themes/default/inflight.lua index 7ccaed2..0e77dae 100644 --- a/scripts/dashx/widgets/dashboard/themes/default/inflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/default/inflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 20, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 20, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,196 +97,112 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 4, - rows = 14, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 4, rows = 14, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize - local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown - { - col = 1, - row = 1, - rowspan = 7, - colspan = 2, - type = "time", - subtype = "flight", - title = "FLIGHT TIME", - titlepos = "bottom", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - }, - { - col = 1, - row = 8, - rowspan = 7, - colspan = 2, - type = "text", - title = "LQ", - subtype = "telemetry", - titlepos = "bottom", - source = "rssi", - nosource = "-", - unit = "dB", - transform = "floor", - bgcolor = colorMode.bgcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - }, - - { - type = "gauge", - subtype = "arc", - col = 3, row = 1, - rowspan = 14, - colspan = 2, - source = "voltage", - thickness = opts.thickness, - font = opts.font, - arcbgcolor = colorMode.arcbgcolor, - title = "VOLTAGE", - titlepos = "bottom", - bgcolor = colorMode.bgcolor, - gaugepadding = opts.gaugepadding, - valuepaddingtop = opts.valuepaddingtop, - min = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 - return math.max(0, cells * minV) - end, - - max = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 - return math.max(0, cells * maxV) - end, - - -- (b) The “dynamic” thresholds (using functions that no longer reference box._cache) - thresholds = { - { - value = function(box) - -- Fetch the raw gaugemin parameter (could itself be a function) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - -- Fetch the raw gaugemax parameter (could itself be a function) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 30% above gaugemin - return raw_gm + 0.30 * (raw_gM - raw_gm) - end, - fillcolor = "red", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 50% above gaugemin - return raw_gm + 0.50 * (raw_gM - raw_gm) - end, - fillcolor = "orange", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Top‐end threshold = gaugemax - return raw_gM - end, - fillcolor = colorMode.fillcolor, - textcolor = colorMode.textcolor + return { + + {col = 1, row = 1, rowspan = 7, colspan = 2, type = "time", subtype = "flight", title = "FLIGHT TIME", titlepos = "bottom", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor}, + {col = 1, row = 8, rowspan = 7, colspan = 2, type = "text", title = "LQ", subtype = "telemetry", titlepos = "bottom", source = "rssi", nosource = "-", unit = "dB", transform = "floor", bgcolor = colorMode.bgcolor, titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor}, { + type = "gauge", + subtype = "arc", + col = 3, + row = 1, + rowspan = 14, + colspan = 2, + source = "voltage", + thickness = opts.thickness, + font = opts.font, + arcbgcolor = colorMode.arcbgcolor, + title = "VOLTAGE", + titlepos = "bottom", + bgcolor = colorMode.bgcolor, + gaugepadding = opts.gaugepadding, + valuepaddingtop = opts.valuepaddingtop, + min = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 + return math.max(0, cells * minV) + end, + + max = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 + return math.max(0, cells * maxV) + end, + + thresholds = { + { + value = function(box) + + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.30 * (raw_gM - raw_gm) + end, + fillcolor = "red", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.50 * (raw_gM - raw_gm) + end, + fillcolor = "orange", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gM + end, + fillcolor = colorMode.fillcolor, + textcolor = colorMode.textcolor + } } } - } -} + } end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -386,28 +211,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -416,11 +235,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -434,14 +253,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}} diff --git a/scripts/dashx/widgets/dashboard/themes/default/init.lua b/scripts/dashx/widgets/dashboard/themes/default/init.lua index 09b1a01..3462b49 100644 --- a/scripts/dashx/widgets/dashboard/themes/default/init.lua +++ b/scripts/dashx/widgets/dashboard/themes/default/init.lua @@ -1,29 +1,10 @@ -local dashx = require("dashx") --[[ - * Copyright (C) dashx Project - * - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - - * Note. Some icons have been sourced from https://www.flaticon.com/ - * + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html ]] -- --- Theme initialization table -local init = { - name = "@Default", -- Theme name - preflight = "preflight.lua", -- Script to run before takeoff - inflight = "inflight.lua", -- Script to run during flight - postflight = "postflight.lua", -- Script to run after landing - standalone = false, -- If true, theme handles all rendering itself -} -return init \ No newline at end of file +local dashx = require("dashx") + +local init = {name = "@Default", preflight = "preflight.lua", inflight = "inflight.lua", postflight = "postflight.lua", standalone = false} + +return init diff --git a/scripts/dashx/widgets/dashboard/themes/default/postflight.lua b/scripts/dashx/widgets/dashboard/themes/default/postflight.lua index ac8af2c..0e92974 100644 --- a/scripts/dashx/widgets/dashboard/themes/default/postflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/default/postflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 20, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 35, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 20, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 35, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,87 +97,48 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 2, - rows = 2, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 2, rows = 2, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { - -- Flight info and RPM info - {col = 1, row = 1, type = "time", subtype = "flight", title = "Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 1, row = 2, type = "time", subtype = "total", title = "Total Model Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + return { - -- Flight max/min stats 1 - {col = 2, row = 1, type = "text", subtype = "stats", source = "voltage", title = "Voltage Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, - {col = 2, row = 2, type = "text", subtype = "stats", stattype = "min", source = "voltage", title = "Voltage Min", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 1, row = 1, type = "time", subtype = "flight", title = "Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 1, row = 2, type = "time", subtype = "total", title = "Total Model Flight Duration", titlepos = "bottom", bgcolor = colorMode.bgcolor, textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, -} + {col = 2, row = 1, type = "text", subtype = "stats", source = "voltage", title = "Voltage Max", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor}, + {col = 2, row = 2, type = "text", subtype = "stats", stattype = "min", source = "voltage", title = "Voltage Min", titlepos = "bottom", bgcolor = colorMode.bgcolor, transform = "floor", textcolor = colorMode.textcolor, titlecolor = colorMode.titlecolor} + + } end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -277,28 +147,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -307,11 +171,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -325,14 +189,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}} diff --git a/scripts/dashx/widgets/dashboard/themes/default/preflight.lua b/scripts/dashx/widgets/dashboard/themes/default/preflight.lua index 5e2d50f..10c0617 100644 --- a/scripts/dashx/widgets/dashboard/themes/default/preflight.lua +++ b/scripts/dashx/widgets/dashboard/themes/default/preflight.lua @@ -1,21 +1,9 @@ -local dashx = require("dashx") --[[ - * Copyright (C) Rotorflight Project - * - * License GPLv3: https://www.gnu.org/licenses/gpl-3.0.en.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 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. - * - * Note: Some icons have been sourced from https://www.flaticon.com/ -]]-- + Copyright (C) 2025 Rob Thomson + GPLv3 — https://www.gnu.org/licenses/gpl-3.0.en.html +]] -- +local dashx = require("dashx") local utils = dashx.widgets.dashboard.utils local boxes_cache = nil @@ -23,160 +11,81 @@ local themeconfig = nil local lastScreenW = nil local darkMode = { - textcolor = "white", - titlecolor = "white", - bgcolor = "black", - fillcolor = "green", - fillbgcolor = "darkgrey", - accentcolor = "white", - rssifillcolor = "green", + textcolor = "white", + titlecolor = "white", + bgcolor = "black", + fillcolor = "green", + fillbgcolor = "darkgrey", + accentcolor = "white", + rssifillcolor = "green", rssifillbgcolor = "darkgrey", - txaccentcolor = "grey", - txfillcolor = "green", - txbgfillcolor = "darkgrey", - bgcolortop = lcd.RGB(10, 10, 10), + txaccentcolor = "grey", + txfillcolor = "green", + txbgfillcolor = "darkgrey", + bgcolortop = lcd.RGB(10, 10, 10) } local lightMode = { - textcolor = "black", - titlecolor = "black", - bgcolor = "white", - fillcolor = "green", - fillbgcolor = "lightgrey", - accentcolor = "darkgrey", - rssifillcolor = "green", + textcolor = "black", + titlecolor = "black", + bgcolor = "white", + fillcolor = "green", + fillbgcolor = "lightgrey", + accentcolor = "darkgrey", + rssifillcolor = "green", rssifillbgcolor = "grey", - txaccentcolor = "darkgrey", - txfillcolor = "green", - txbgfillcolor = "grey", - bgcolortop = "grey" + txaccentcolor = "darkgrey", + txfillcolor = "green", + txbgfillcolor = "grey", + bgcolortop = "grey" } --- User voltage min/max override support local function getUserVoltageOverride(which) - local prefs = dashx.session and dashx.session.modelPreferences - if prefs and prefs["system/@default"] then - local v = tonumber(prefs["system/@default"][which]) - -- Only use override if it is present and different from the default 6S values - -- (Defaults: min=18.0, max=25.2) - if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end - if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end - end - return nil + local prefs = dashx.session and dashx.session.modelPreferences + if prefs and prefs["system/@default"] then + local v = tonumber(prefs["system/@default"][which]) + + if which == "v_min" and v and math.abs(v - 18.0) > 0.05 then return v end + if which == "v_max" and v and math.abs(v - 25.2) > 0.05 then return v end + end + return nil end --- alias current mode local colorMode = lcd.darkMode() and darkMode or lightMode --- Theme based configuration settings local theme_section = "system/@default" -local THEME_DEFAULTS = { - rpm_min = 0, - rpm_max = 3000, - bec_min = 3.0, - bec_max = 13.0, - esctemp_warn = 90, - esctemp_max = 140, - tx_min = 7.2, - tx_warn = 7.4, - tx_max = 8.4 -} +local THEME_DEFAULTS = {rpm_min = 0, rpm_max = 3000, bec_min = 3.0, bec_max = 13.0, esctemp_warn = 90, esctemp_max = 140, tx_min = 7.2, tx_warn = 7.4, tx_max = 8.4} --- Theme Options based on screen width local function getThemeOptionKey(W) - if W == 800 then return "ls_full" - elseif W == 784 then return "ls_std" - elseif W == 640 then return "ss_full" - elseif W == 630 then return "ss_std" - elseif W == 480 then return "ms_full" - elseif W == 472 then return "ms_std" + if W == 800 then + return "ls_full" + elseif W == 784 then + return "ls_std" + elseif W == 640 then + return "ss_full" + elseif W == 630 then + return "ss_std" + elseif W == 480 then + return "ms_full" + elseif W == 472 then + return "ms_std" end end --- Theme Options based on screen width local themeOptions = { - -- Large screens - (X20 / X20RS / X18RS etc) Full/Standard - ls_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 30, - batteryframethickness = 4, - titlepaddingbottom = 15, - valuepaddingleft = 25, - valuepaddingtop = 40, - valuepaddingbottom = 25, - gaugepaddingtop = 20, - gaugepadding = 20 - }, - - ls_std = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 30, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 75, - valuepaddingtop = 5, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - - -- Medium screens (X18 / X18S / TWXLITE) - Full/Standard - ms_full = { - font = "FONT_XXL", - advfont = "FONT_M", - thickness = 27, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ms_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 20, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 5, - }, - - -- Small screens - (X14 / X14S) Full/Standard - ss_full = { - font = "FONT_XL", - advfont = "FONT_M", - thickness = 25, - batteryframethickness = 4, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 5, - valuepaddingbottom = 15, - gaugepaddingtop = 5, - gaugepadding = 10, - }, - - ss_std = { - font = "FONT_XL", - advfont = "FONT_S", - thickness = 22, - batteryframethickness = 2, - titlepaddingbottom = 0, - valuepaddingleft = 20, - valuepaddingtop = 10, - valuepaddingbottom = 25, - gaugepaddingtop = 5, - gaugepadding = 10, - }, + + ls_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 30, batteryframethickness = 4, titlepaddingbottom = 15, valuepaddingleft = 25, valuepaddingtop = 40, valuepaddingbottom = 25, gaugepaddingtop = 20, gaugepadding = 20}, + + ls_std = {font = "FONT_XL", advfont = "FONT_M", thickness = 30, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 75, valuepaddingtop = 5, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_full = {font = "FONT_XXL", advfont = "FONT_M", thickness = 27, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ms_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 20, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 5}, + + ss_full = {font = "FONT_XL", advfont = "FONT_M", thickness = 25, batteryframethickness = 4, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 5, valuepaddingbottom = 15, gaugepaddingtop = 5, gaugepadding = 10}, + + ss_std = {font = "FONT_XL", advfont = "FONT_S", thickness = 22, batteryframethickness = 2, titlepaddingbottom = 0, valuepaddingleft = 20, valuepaddingtop = 10, valuepaddingbottom = 25, gaugepaddingtop = 5, gaugepadding = 10} } local function getThemeValue(key) @@ -188,224 +97,118 @@ local function getThemeValue(key) return THEME_DEFAULTS[key] end --- Caching for boxes local lastScreenW = nil local boxes_cache = nil local themeconfig = nil local headeropts = utils.getHeaderOptions() --- Theme Layout -local layout = { - cols = 20, - rows = 8, - padding = 1, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local layout = {cols = 20, rows = 8, padding = 1} -local header_layout = { - height = headeropts.height, - cols = 7, - rows = 1, - padding = 0, - --showgrid = lcd.RGB(100, 100, 100) -- or any color you prefer -} +local header_layout = {height = headeropts.height, cols = 7, rows = 1, padding = 0} --- Boxes local function buildBoxes(W) - - -- Object based options determined by screensize + local opts = themeOptions[getThemeOptionKey(W)] or themeOptions.unknown -return { - - { - col = 1, - row = 1, - colspan = 8, - rowspan = 3, - type = "image", - subtype = "model", - bgcolor = colorMode.bgcolor, - }, - { - col = 1, - row = 4, - colspan = 8, - rowspan = 3, - type = "time", - subtype = "flight", - title = "TIME", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 1, - row = 7, - colspan = 4, - rowspan = 2, - type = "text", - subtype = "telemetry", - source = "rssi", - nosource= "-", - unit = "dB", - title = "LQ", - titlepos= "bottom", - transform = "floor", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 5, - row = 7, - colspan = 4, - rowspan = 2, - type = "time", - subtype = "count", - title = "FLIGHTS", - titlepos= "bottom", - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - }, - { - col = 9, - row = 1, - colspan = 12, - rowspan = 8, - type = "gauge", - subtype = "arc", - source = "voltage", - fillbgcolor = colorMode.fillbgcolor, - title = "VOLTAGE", - font = opts.font, - thickness= opts.thickness, - gaugepadding = opts.gaugepadding, - titlepos = "bottom", - fillcolor= colorMode.fillcolor, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.titlecolor, - bgcolor = colorMode.bgcolor, - valuepaddingtop = opts.valuepaddingtop, - --valuepaddingbottom = opts.valuepaddingbottom, - --gaugepaddingtop = opts.gaugepaddingtop, - min = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 - return math.max(0, cells * minV) - end, - - max = function() - local cfg = dashx.session.batteryConfig - local cells = (cfg and cfg.batteryCellCount) or 3 - local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 - return math.max(0, cells * maxV) - end, - - -- (b) The “dynamic” thresholds (using functions that no longer reference box._cache) - thresholds = { - { - value = function(box) - -- Fetch the raw gaugemin parameter (could itself be a function) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - -- Fetch the raw gaugemax parameter (could itself be a function) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 30% above gaugemin - return raw_gm + 0.30 * (raw_gM - raw_gm) - end, - fillcolor = "red", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gm = utils.getParam(box, "min") - if type(raw_gm) == "function" then - raw_gm = raw_gm(box) - end - - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Return 50% above gaugemin - return raw_gm + 0.50 * (raw_gM - raw_gm) + return { + + {col = 1, row = 1, colspan = 8, rowspan = 3, type = "image", subtype = "model", bgcolor = colorMode.bgcolor}, + {col = 1, row = 4, colspan = 8, rowspan = 3, type = "time", subtype = "flight", title = "TIME", titlepos = "bottom", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 1, row = 7, colspan = 4, rowspan = 2, type = "text", subtype = "telemetry", source = "rssi", nosource = "-", unit = "dB", title = "LQ", titlepos = "bottom", transform = "floor", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, + {col = 5, row = 7, colspan = 4, rowspan = 2, type = "time", subtype = "count", title = "FLIGHTS", titlepos = "bottom", titlecolor = colorMode.titlecolor, textcolor = colorMode.titlecolor, bgcolor = colorMode.bgcolor}, { + col = 9, + row = 1, + colspan = 12, + rowspan = 8, + type = "gauge", + subtype = "arc", + source = "voltage", + fillbgcolor = colorMode.fillbgcolor, + title = "VOLTAGE", + font = opts.font, + thickness = opts.thickness, + gaugepadding = opts.gaugepadding, + titlepos = "bottom", + fillcolor = colorMode.fillcolor, + titlecolor = colorMode.titlecolor, + textcolor = colorMode.titlecolor, + bgcolor = colorMode.bgcolor, + valuepaddingtop = opts.valuepaddingtop, + + min = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local minV = (cfg and cfg.vbatmincellvoltage) or 3.0 + return math.max(0, cells * minV) end, - fillcolor = "orange", - textcolor = colorMode.textcolor - }, - { - value = function(box) - local raw_gM = utils.getParam(box, "max") - if type(raw_gM) == "function" then - raw_gM = raw_gM(box) - end - - -- Top‐end threshold = gaugemax - return raw_gM + + max = function() + local cfg = dashx.session.batteryConfig + local cells = (cfg and cfg.batteryCellCount) or 3 + local maxV = (cfg and cfg.vbatfullcellvoltage) or 4.2 + return math.max(0, cells * maxV) end, - fillcolor = colorMode.fillcolor, - textcolor = colorMode.textcolor - } - } + thresholds = { + { + value = function(box) + + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.30 * (raw_gM - raw_gm) + end, + fillcolor = "red", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gm = utils.getParam(box, "min") + if type(raw_gm) == "function" then raw_gm = raw_gm(box) end + + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gm + 0.50 * (raw_gM - raw_gm) + end, + fillcolor = "orange", + textcolor = colorMode.textcolor + }, { + value = function(box) + local raw_gM = utils.getParam(box, "max") + if type(raw_gM) == "function" then raw_gM = raw_gM(box) end + + return raw_gM + end, + fillcolor = colorMode.fillcolor, + textcolor = colorMode.textcolor + } + } + } } -} end local header_boxes = { --- Craftname - { - col = 1, - row = 1, - colspan = 2, - type = "text", - subtype = "craftname", - font = headeropts.font, - valuealign = "left", - valuepaddingleft = 5, - bgcolor = colorMode.bgcolortop, - titlecolor = colorMode.titlecolor, - textcolor = colorMode.textcolor - }, - - -- RF Logo - { - col = 3, - row = 1, - colspan = 3, - type = "image", - subtype = "image", - bgcolor = colorMode.bgcolortop - }, - - -- TX Battery - { - col = 6, + + {col = 1, row = 1, colspan = 2, type = "text", subtype = "craftname", font = headeropts.font, valuealign = "left", valuepaddingleft = 5, bgcolor = colorMode.bgcolortop, titlecolor = colorMode.titlecolor, textcolor = colorMode.textcolor}, + + {col = 3, row = 1, colspan = 3, type = "image", subtype = "image", bgcolor = colorMode.bgcolortop}, { + col = 6, row = 1, - type = "gauge", - subtype = "bar", + type = "gauge", + subtype = "bar", source = "txbatt", font = headeropts.font, - battery = true, - batteryframe = true, + battery = true, + batteryframe = true, hidevalue = true, - valuealign = "left", - batterysegments = 4, - batteryspacing = 1, - batteryframethickness = 2, + valuealign = "left", + batterysegments = 4, + batteryspacing = 1, + batteryframethickness = 2, batterysegmentpaddingtop = headeropts.batterysegmentpaddingtop, batterysegmentpaddingbottom = headeropts.batterysegmentpaddingbottom, batterysegmentpaddingleft = headeropts.batterysegmentpaddingleft, @@ -414,28 +217,22 @@ local header_boxes = { gaugepaddingleft = headeropts.gaugepaddingleft, gaugepaddingbottom = headeropts.gaugepaddingbottom, gaugepaddingtop = headeropts.gaugepaddingtop, - fillbgcolor = colorMode.txbgfillcolor, + fillbgcolor = colorMode.txbgfillcolor, bgcolor = colorMode.bgcolortop, - accentcolor = colorMode.txaccentcolor, + accentcolor = colorMode.txaccentcolor, textcolor = colorMode.textcolor, - min = getThemeValue("tx_min"), - max = getThemeValue("tx_max"), - thresholds = { - { value = getThemeValue("tx_warn"), fillcolor = "orange" }, - { value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor } - } - }, - - -- RSSI - { - col = 7, + min = getThemeValue("tx_min"), + max = getThemeValue("tx_max"), + thresholds = {{value = getThemeValue("tx_warn"), fillcolor = "orange"}, {value = getThemeValue("tx_max"), fillcolor = colorMode.txfillcolor}} + }, { + col = 7, row = 1, - type = "gauge", - subtype = "step", + type = "gauge", + subtype = "step", source = "rssi", - font = "FONT_XS", - stepgap = 2, - stepcount = 5, + font = "FONT_XS", + stepgap = 2, + stepcount = 5, decimals = 0, valuealign = "left", barpaddingleft = headeropts.barpaddingleft, @@ -444,11 +241,11 @@ local header_boxes = { barpaddingtop = headeropts.barpaddingtop, valuepaddingleft = headeropts.valuepaddingleft, valuepaddingbottom = headeropts.valuepaddingbottom, - bgcolor = colorMode.bgcolortop, - textcolor = colorMode.textcolor, + bgcolor = colorMode.bgcolortop, + textcolor = colorMode.textcolor, fillcolor = colorMode.rssifillcolor, - fillbgcolor = colorMode.rssifillbgcolor, - }, + fillbgcolor = colorMode.rssifillbgcolor + } } local function boxes() @@ -462,14 +259,4 @@ local function boxes() return boxes_cache end -return { - layout = layout, - boxes = boxes, - header_boxes = header_boxes, - header_layout = header_layout, - scheduler = { - spread_scheduling = true, -- (optional: spread scheduling over the interval to avoid spikes in CPU usage) - spread_scheduling_paint = false, -- optional: spread scheduling for paint (if true, paint will be spread over the interval) - spread_ratio = 0.5 -- optional: manually override default ratio logic (applies if spread_scheduling is true) - } -} +return {layout = layout, boxes = boxes, header_boxes = header_boxes, header_layout = header_layout, scheduler = {spread_scheduling = true, spread_scheduling_paint = false, spread_ratio = 0.5}}