[education/kstars] /: Add a script to generate the indidrivers.xml

Jasem Mutlaq <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 90c0662b2afdec432b33c76672bd539510ed82ab by Jasem Mutlaq.
Committed on 01/08/2026 at 07:42.
Pushed by mutlaqja into branch 'master'.

Add a script to generate the indidrivers.xml

M  +130  -106  kstars/data/indidrivers.xml
A  +174  -0    tools/generate_indidrivers_xml.py

https://invent.kde.org/education/kstars/-/commit/90c0662b2afdec432b33c76672bd539510ed82ab

diff --git a/kstars/data/indidrivers.xml b/kstars/data/indidrivers.xml
index aaa3e033c2..9c27b15f05 100644
--- a/kstars/data/indidrivers.xml
+++ b/kstars/data/indidrivers.xml
@@ -107,21 +107,33 @@
             <driver name="Pegasus NYX-101">indi_lx200_pegasus_nyx101</driver>
             <version>2.2</version>
         </device>
+        <device label="ZWO AM7 WiFi" manufacturer="ZWO">
+            <driver name="ZWO AM5">indi_lx200am5</driver>
+            <version>1.5</version>
+        </device>
+        <device label="ZWO AM7 USB" manufacturer="ZWO">
+            <driver name="ZWO AM5">indi_lx200am5</driver>
+            <version>1.5</version>
+        </device>
         <device label="ZWO AM5 WiFi" manufacturer="ZWO">
             <driver name="ZWO AM5">indi_lx200am5</driver>
-            <version>1.4</version>
+            <version>1.5</version>
         </device>
         <device label="ZWO AM5 USB" manufacturer="ZWO">
             <driver name="ZWO AM5">indi_lx200am5</driver>
-            <version>1.4</version>
+            <version>1.5</version>
         </device>
         <device label="ZWO AM3 WiFi" manufacturer="ZWO">
             <driver name="ZWO AM5">indi_lx200am5</driver>
-            <version>1.4</version>
+            <version>1.5</version>
         </device>
         <device label="ZWO AM3 USB" manufacturer="ZWO">
             <driver name="ZWO AM5">indi_lx200am5</driver>
-            <version>1.4</version>
+            <version>1.5</version>
+        </device>
+        <device label="TOUPTEK SH20" manufacturer="Touptek">
+            <driver name="TOUPTEK SH20">indi_touptek_sh20</driver>
+            <version>1.0</version>
         </device>
         <device label="Celestron GPS" manufacturer="Celestron">
             <driver name="Celestron GPS">indi_celestron_gps</driver>
@@ -381,12 +393,16 @@
         </device>
         <device label="Planewave" manufacturer="Planewave Instruments">
             <driver name="Planewave">indi_planewave_telescope</driver>
-            <version>0.1</version>
+            <version>0.2</version>
         </device>
         <device label="ESP32go" manufacturer="DIY">
             <driver name="LX200 ESP32go">indi_lx200_esp32go</driver>
             <version>1.0</version>
         </device>
+        <device label="MX-HD Telescope" manufacturer="GOTO INC">
+            <driver name="MX-HD Telescope">indi_mxhd_telescope</driver>
+            <version>1.0</version>
+        </device>
     </devGroup>
     <devGroup group="Rotators">
         <device label="Rigelsys nFrame" manufacturer="Rigel Systems">
@@ -437,12 +453,20 @@
             <driver name="Camelot Rotator">indi_camelot_rotator</driver>
             <version>1.0</version>
         </device>
+        <device label="PlaneWave Rotator" manufacturer="PlaneWave Instruments">
+            <driver name="PlaneWave Rotator">indi_planewave_rotator</driver>
+            <version>1.0</version>
+        </device>
     </devGroup>
     <devGroup group="Focusers">
         <device label="Alpaca Focuser" manufacturer="Alpaca">
             <driver name="Alpaca Focuser">indi_alpaca_focuser</driver>
             <version>1.0</version>
         </device>
+        <device label="Askar WAF" manufacturer="Askar">
+            <driver name="Askar WAF">indi_askar_waf_focus</driver>
+            <version>1.0</version>
+        </device>
         <device label="Focuser Simulator" manufacturer="Simulator">
             <driver name="Focuser Simulator">indi_simulator_focus</driver>
             <version>1.0</version>
@@ -469,7 +493,7 @@
         </device>
         <device label="PlaneWave EFA" manufacturer="Planewave Instruments">
             <driver name="PlaneWave EFA">indi_efa_focus</driver>
-            <version>1.1</version>
+            <version>1.4</version>
         </device>
         <device label="Rainbow Astro RSF" manufacturer="Rainbow Astro">
             <driver name="Rainbow Astro RSF">indi_rainbowrsf_focus</driver>
@@ -735,7 +759,7 @@
     <devGroup group="Filter Wheels">
         <device label="Alpaca FilterWheel" manufacturer="Alpaca">
             <driver name="Alpaca FilterWheel">indi_alpaca_filterwheel</driver>
-            <version>1.0</version>
+            <version>1.1</version>
         </device>
         <device label="XAGYL Wheel" manufacturer="XAGYL">
             <driver name="XAGYL Wheel">indi_xagyl_wheel</driver>
@@ -785,6 +809,10 @@
             <driver name="Wanderer Snowflake Filter Wheel">indi_wanderer_snowflake_wheel</driver>
             <version>1.0</version>
         </device>
+        <device label="Wanderer FilterCube" manufacturer="Wanderer Astro">
+            <driver name="RASA FilterCube Filter Wheel">indi_rasa_filtercube_wheel</driver>
+            <version>1.0</version>
+        </device>
     </devGroup>
     <devGroup group="Auxiliary">
         <device label="Astrometry" manufacturer="Others">
@@ -811,6 +839,14 @@
             <driver name="Pegasus FlatMaster">indi_pegasus_flatmaster</driver>
             <version>1.2</version>
         </device>
+        <device label="Pegasus FlatMaster Neo" manufacturer="Pegasus Astro">
+            <driver name="Pegasus FlatMaster Neo">indi_pegasus_flatmaster_neo</driver>
+            <version>1.0</version>
+        </device>
+        <device label="Pegasus FlatMaster WiFi" manufacturer="Pegasus Astro">
+            <driver name="Pegasus FlatMaster WiFi">indi_pegasus_flatmaster_wifi</driver>
+            <version>1.0</version>
+        </device>
         <device label="PlaneWave Delta-T" manufacturer="Planewave Instruments">
             <driver name="PlaneWave Delta-T">indi_planewave_deltat</driver>
             <version>1.1</version>
@@ -1177,15 +1213,7 @@
     <devGroup group="Focusers">
         <device label="Altair AAF" mdpd="true" manufacturer="Altair">
             <driver name="Altair">indi_altaircam_focuser</driver>
-            <version>2.4</version>
-        </device>
-    </devGroup>
-</driversList>
-<driversList>
-    <devGroup group="Spectrographs">
-        <device label="XC Correlator" manufacturer="AHP">
-            <driver name="AHP XC Correlator">indi_ahp_xc</driver>
-            <version>1.0</version>
+            <version>2.6</version>
         </device>
     </devGroup>
 </driversList>
@@ -1265,22 +1293,6 @@
         </device>
     </devGroup>
 </driversList>
-<driversList>
-    <devGroup group="Auxiliary">
-        <device label="AstroLink 4">
-            <driver name="AstroLink 4">indi_astrolink4</driver>
-            <version>0.7</version>
-        </device>
-    </devGroup>
-</driversList>
-<driversList>
-    <devGroup group="Focusers">
-        <device label="Astromechanics FOC" manufacturer="Astromechanics">
-            <driver name="Astromechanics FOC">indi_astromechfoc</driver>
-            <version>0.2</version>
-        </device>
-    </devGroup>
-</driversList>
 <driversList>
     <devGroup group="CCDs">
         <device label="Atik CCD" mdpd="true" manufacturer="Atik">
@@ -1295,6 +1307,22 @@
         </device>
     </devGroup>
 </driversList>
+<driversList>
+    <devGroup group="Filter Wheels">
+        <device label="Atik EFW" mdpd="true" manufacturer="Atik">
+            <driver name="Atik EFW">indi_atik_efw</driver>
+            <version>1.2</version>
+        </device>
+    </devGroup>
+</driversList>
+<driversList>
+    <devGroup group="Telescopes">
+        <device label="Avalon StarGo" manufacturer="Avalon-Instruments">
+            <driver name="LX200 StarGo">indi_lx200stargo</driver>
+            <version>1.14</version>
+        </device>
+    </devGroup>
+</driversList>
 <driversList>
     <devGroup group="Telescopes">
         <device label="AvalonUD Telescope" manufacturer="Avalon-Instruments">
@@ -1315,14 +1343,6 @@
         </device>
     </devGroup>
 </driversList>
-<driversList>
-    <devGroup group="Telescopes">
-        <device label="Avalon StarGo" manufacturer="Avalon-Instruments">
-            <driver name="LX200 StarGo">indi_lx200stargo</driver>
-            <version>1.14</version>
-        </device>
-    </devGroup>
-</driversList>
 <driversList>
     <devGroup group="Focusers">
         <device label="Bee Focuser" manufacturer="DIY">
@@ -1371,63 +1391,59 @@
     <devGroup group="Telescopes">
         <device label="Celestron AUX" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
+        </device>
+        <device label="Celestron Wedge" manufacturer="Celestron">
+            <driver name="Celestron AUX">indi_celestron_aux</driver>
+            <version>1.7</version>
         </device>
         <device label="Celestron WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Evolution WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Evolution WiFi Wedge" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron CPC Wired" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron CGEM II Wired" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron CGEM II WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron CGX Wired" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron CGX WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Advanced VX Wired" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Advanced VX WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Advanced GT Wired" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-            <version>1.6</version>
+            <version>1.7</version>
         </device>
         <device label="Celestron Advanced GT WiFi" manufacturer="Celestron">
             <driver name="Celestron AUX">indi_celestron_aux</driver>
-      <version>1.5</version>
-        </device>
-    </devGroup>
-</driversList>
-<driversList>
-    <devGroup group="Focusers">
-        <device label="DreamFocuser" manufacturer="Astromaniak">
-            <driver name="DreamFocuser">indi_dreamfocuser_focus</driver>
-            <version>2.1</version>
+            <version>1.7</version>
         </device>
     </devGroup>
 </driversList>
@@ -1549,14 +1565,6 @@
         </device>
     </devGroup>
 </driversList>
-<driversList>
-    <devGroup group="CCDs">
-        <device label="FLI Kepler" manufacturer="Finger Lakes Instruments">
-            <driver name="FLI Kepler">indi_kepler_ccd</driver>
-            <version>2.0</version>
-        </device>
-    </devGroup>
-</driversList>
 <driversList>
     <devGroup group="CCDs">
         <device label="FLI CCD" manufacturer="Finger Lakes Instruments">
@@ -1579,9 +1587,9 @@
 </driversList>
 <driversList>
     <devGroup group="CCDs">
-        <device label="Generic CCD" mdpd="true">
-            <driver name="Generic CCD">indi_generic_ccd</driver>
-            <version>1.0</version>
+        <device label="FLI Kepler" manufacturer="Finger Lakes Instruments">
+            <driver name="FLI Kepler">indi_kepler_ccd</driver>
+            <version>2.0</version>
         </device>
     </devGroup>
 </driversList>
@@ -1589,27 +1597,27 @@
     <devGroup group="CCDs">
         <device label="Canon DSLR" mdpd="true" manufacturer="DSLRs">
             <driver name="Canon DSLR">indi_canon_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
         <device label="Nikon DSLR" mdpd="true" manufacturer="DSLRs">
             <driver name="Nikon DSLR">indi_nikon_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
         <device label="Pentax DSLR Legacy" mdpd="true" manufacturer="DSLRs">
             <driver name="Pentax DSLR">indi_pentax_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
         <device label="Sony DSLR" mdpd="true" manufacturer="DSLRs">
             <driver name="Sony DSLR">indi_sony_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
         <device label="Fuji DSLR" mdpd="true" manufacturer="DSLRs">
             <driver name="Fuji DSLR">indi_fuji_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
         <device label="GPhoto CCD" manufacturer="DSLRs">
             <driver name="GPhoto CCD">indi_gphoto_ccd</driver>
-            <version>3.4</version>
+            <version>3.5</version>
         </device>
     </devGroup>
 </driversList>
@@ -1809,15 +1817,7 @@
     <devGroup group="Focusers">
         <device label="Nncam AAF" mdpd="true" manufacturer="Nn">
             <driver name="Nn">indi_nncam_focuser</driver>
-            <version>2.4</version>
-        </device>
-    </devGroup>
-</driversList>
-<driversList>
-    <devGroup group="Weather">
-        <device label="NUTMonitor" manufacturer="Others">
-            <driver name="NUTMonitor">indi_nut</driver>
-            <version>0.1</version>
+            <version>2.6</version>
         </device>
     </devGroup>
 </driversList>
@@ -1961,14 +1961,6 @@
         </device>
     </devGroup>
 </driversList>
-<driversList>
-    <devGroup group="Spectrographs">
-        <device label="RTL-SDR" mdpd="false">
-            <driver name="RTL-SDR">indi_rtlsdr</driver>
-            <version>1.0</version>
-        </device>
-    </devGroup>
-</driversList>
 <driversList>
     <devGroup group="CCDs">
         <device label="SBIG CCD" manufacturer="Diffraction Limited">
@@ -1983,11 +1975,11 @@
 </driversList>
 <driversList>
     <devGroup group="Spectrographs">
-        <device label="Shelyak eShel"  manufacturer="Shelyak">
+        <device label="Shelyak eShel" manufacturer="Shelyak">
             <driver name="Shelyak eShel">indi_shelyakeshel_spectrograph</driver>
             <version>1.0</version>
         </device>
-        <device label="Shelyak SPOX"  manufacturer="Shelyak">
+        <device label="Shelyak SPOX" manufacturer="Shelyak">
             <driver name="Shelyak SPOX">indi_shelyakspox_spectrograph</driver>
             <version>1.0</version>
         </device>
@@ -2029,6 +2021,14 @@
         </device>
     </devGroup>
 </driversList>
+<driversList>
+    <devGroup group="CCDs">
+        <device label="SVBONY CCD" mdpd="true" manufacturer="SVBONY">
+            <driver name="SVBONY CCD">indi_svbony_ccd</driver>
+            <version>1.4</version>
+        </device>
+    </devGroup>
+</driversList>
 <driversList>
     <devGroup group="CCDs">
         <device label="SVbonycam" mdpd="true" manufacturer="SVBONY2">
@@ -2049,14 +2049,6 @@
         </device>
     </devGroup>
 </driversList>
-<driversList>
-    <devGroup group="CCDs">
-        <device label="SVBONY CCD" mdpd="true" manufacturer="SVBONY">
-            <driver name="SVBONY CCD">indi_svbony_ccd</driver>
-            <version>1.4</version>
-        </device>
-    </devGroup>
-</driversList>
 <driversList>
     <devGroup group="CCDs">
         <device label="SX CCD" mdpd="true" manufacturer="Starlight XPress">
@@ -2158,10 +2150,42 @@
     </devGroup>
 </driversList>
 <driversList>
+    <devGroup group="Auxiliary">
+        <device label="AstroLink 4">
+            <driver name="AstroLink 4">indi_astrolink4</driver>
+            <version>0.7</version>
+        </device>
+    </devGroup>
     <devGroup group="CCDs">
+        <device label="Generic CCD" mdpd="true">
+            <driver name="Generic CCD">indi_generic_ccd</driver>
+            <version>1.0</version>
+        </device>
         <device label="Cam90 CCD" mdpd="true">
             <driver name="Cam90 CCD">indi_cam90_ccd</driver>
             <version>1.0</version>
         </device>
     </devGroup>
-</driversList>
\ No newline at end of file
+    <devGroup group="Focusers">
+        <device label="DreamFocuser" manufacturer="Astromaniak">
+            <driver name="DreamFocuser">indi_dreamfocuser_focus</driver>
+            <version>2.1</version>
+        </device>
+    </devGroup>
+    <devGroup group="Spectrographs">
+        <device label="XC Correlator" manufacturer="AHP">
+            <driver name="AHP XC Correlator">indi_ahp_xc</driver>
+            <version>1.0</version>
+        </device>
+        <device label="RTL-SDR" mdpd="false">
+            <driver name="RTL-SDR">indi_rtlsdr</driver>
+            <version>1.0</version>
+        </device>
+    </devGroup>
+    <devGroup group="Weather">
+        <device label="NUTMonitor" manufacturer="Others">
+            <driver name="NUTMonitor">indi_nut</driver>
+            <version>0.1</version>
+        </device>
+    </devGroup>
+</driversList>
diff --git a/tools/generate_indidrivers_xml.py b/tools/generate_indidrivers_xml.py
new file mode 100755
index 0000000000..e9959d595e
--- /dev/null
+++ b/tools/generate_indidrivers_xml.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+"""
+Regenerate kstars/data/indidrivers.xml from the locally installed INDI driver
+XML files (mirrors the file selection logic in DriverManager::readXMLDrivers()).
+
+Unlike a plain `cat` of every driver XML file, this script:
+
+  * Pretty-prints every block with a consistent 4-space indent, regardless of
+    how the source file was formatted.
+  * Never silently drops a driver. Entries that already exist in the current
+    indidrivers.xml but are not found on the local machine (e.g. arm64-only
+    drivers such as libcamera, or externally maintained drivers such as
+    Cam90 that aren't installed here) are kept as-is, appended at the end.
+    Pass --no-preserve-missing to disable this and do a hard refresh instead.
+
+Usage:
+    tools/generate_indidrivers_xml.py [--indi-dir DIR] [--output FILE]
+                                       [--no-preserve-missing] [--dry-run]
+"""
+import argparse
+import re
+import sys
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>\n'
+INDENT = "    "
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+DEFAULT_OUTPUT = REPO_ROOT / "kstars" / "data" / "indidrivers.xml"
+DEFAULT_INDI_DIRS = ["/usr/share/indi", "/usr/local/share/indi"]
+
+
+def find_indi_dir(explicit):
+    if explicit:
+        p = Path(explicit)
+        if not (p / "drivers.xml").is_file():
+            sys.exit(f"error: {p} does not contain a drivers.xml")
+        return p
+    for candidate in DEFAULT_INDI_DIRS:
+        p = Path(candidate)
+        if (p / "drivers.xml").is_file():
+            return p
+    sys.exit("error: could not find an INDI drivers directory; pass --indi-dir")
+
+
+def device_key(group, device_el):
+    driver_el = device_el.find("driver")
+    driver_name = driver_el.text.strip() if driver_el is not None and driver_el.text else ""
+    return (group, device_el.get("label", ""), driver_name)
+
+
+def load_devgroups(xml_path):
+    """Parse a <driversList> XML file and return its <devGroup> children."""
+    root = ET.parse(xml_path).getroot()
+    if root.tag != "driversList":
+        raise ValueError(f"unexpected root <{root.tag}>, expected <driversList>")
+    return list(root.findall("devGroup"))
+
+
+def collect_local_driver_files(indi_dir):
+    """Same file selection as DriverManager::readXMLDrivers(): drivers.xml plus
+    every indi_*.xml file, skipping the *_sk.xml skeleton files."""
+    primary = indi_dir / "drivers.xml"
+    others = sorted(
+        p for p in indi_dir.glob("indi_*.xml") if not p.name.endswith("_sk.xml")
+    )
+    return [primary] + others
+
+
+def load_existing_devices(existing_path):
+    """Return {key: (group, device_element)} for every device currently in
+    indidrivers.xml, in encounter order. The file is a concatenation of
+    multiple <driversList> roots (that's the format DriverManager streams
+    in), so it isn't well-formed standalone XML and needs wrapping first."""
+    if not existing_path.is_file():
+        return {}
+    raw = existing_path.read_text(encoding="utf-8")
+    raw = re.sub(r"<\?xml[^>]*\?>", "", raw)
+    wrapped = ET.fromstring(f"<root>{raw}</root>")
+    devices = {}
+    for drivers_list in wrapped.findall("driversList"):
+        for devgroup in drivers_list.findall("devGroup"):
+            group = devgroup.get("group")
+            for device in devgroup.findall("device"):
+                devices.setdefault(device_key(group, device), (group, device))
+    return devices
+
+
+def build_blocks(local_files, existing_devices, preserve_missing):
+    """Returns (blocks, num_local_devices, num_preserved) where blocks is a
+    list of lists of <devGroup> elements, one list per output <driversList>."""
+    blocks = []
+    found = set()
+    for path in local_files:
+        if not path.is_file():
+            continue
+        try:
+            devgroups = load_devgroups(path)
+        except (ET.ParseError, ValueError) as exc:
+            print(f"warning: skipping {path}: {exc}", file=sys.stderr)
+            continue
+        for dg in devgroups:
+            group = dg.get("group")
+            for device in dg.findall("device"):
+                found.add(device_key(group, device))
+        blocks.append(devgroups)
+
+    preserved = 0
+    if preserve_missing:
+        leftover_by_group = {}
+        for key, (group, device) in existing_devices.items():
+            if key in found:
+                continue
+            leftover_by_group.setdefault(group, []).append(device)
+            preserved += 1
+        if leftover_by_group:
+            leftover_devgroups = []
+            for group in sorted(leftover_by_group):
+                dg = ET.Element("devGroup", {"group": group})
+                for device in leftover_by_group[group]:
+                    dg.append(device)
+                leftover_devgroups.append(dg)
+            blocks.append(leftover_devgroups)
+
+    return blocks, len(found), preserved
+
+
+def serialize(blocks):
+    parts = []
+    for devgroups in blocks:
+        root = ET.Element("driversList")
+        root.extend(devgroups)
+        ET.indent(root, space=INDENT)
+        parts.append(ET.tostring(root, encoding="unicode"))
+    return XML_DECLARATION + "\n".join(parts) + "\n"
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument("--indi-dir", help="Directory containing the local INDI driver XML files "
+                                            f"(default: first of {DEFAULT_INDI_DIRS} that exists)")
+    parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="Output path (default: %(default)s)")
+    parser.add_argument("--no-preserve-missing", action="store_true",
+                         help="Do not keep drivers that are in the current output file but not "
+                              "found locally (hard refresh instead of a safe merge)")
+    parser.add_argument("--dry-run", action="store_true", help="Print a summary without writing the file")
+    args = parser.parse_args()
+
+    indi_dir = find_indi_dir(args.indi_dir)
+    output_path = Path(args.output)
+
+    local_files = collect_local_driver_files(indi_dir)
+    existing_devices = load_existing_devices(output_path)
+    blocks, num_local, num_preserved = build_blocks(
+        local_files, existing_devices, preserve_missing=not args.no_preserve_missing
+    )
+
+    print(f"INDI driver dir : {indi_dir}")
+    print(f"Source files    : {len(local_files)}")
+    print(f"Local devices   : {num_local}")
+    print(f"Preserved       : {num_preserved} (in current file, not found locally)")
+    print(f"Total devices   : {num_local + num_preserved}")
+
+    if args.dry_run:
+        print("(dry run, nothing written)")
+        return
+
+    output_path.write_text(serialize(blocks), encoding="utf-8")
+    print(f"Wrote {output_path}")
+
+
+if __name__ == "__main__":
+    main()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.