#!/usr/bin/env bash # setup.sh - One-time environment bootstrap for the GPS processing pipeline. # # Installs (all local to this directory, no sudo required): # venv/ Python env: pyubx2, pygnssutils, pyserial, numpy, pandas, pyproj # tools/bin/ RTKLIB demo5 v2.5.1 (convbin, rnx2rtkp, pos2kml) [stock apt fallback] # tools/antex/ igs20.atx + ngs20.atx antenna models; tools/antenna.json (rover antenna) # tools/bin/CRX2RNX Hatanaka RINEX decompressor (fallback for .d CORS files) # tools/cache/ NGS CORS station catalog # # Idempotent: re-running skips anything already installed. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" TOOLS="$SCRIPT_DIR/tools" VENV="$SCRIPT_DIR/venv" DEMO5_TAG="v2.5.1" DEMO5_REPO="https://github.com/rtklibexplorer/RTKLIB" PASS=() WARN=() say() { printf '\n=== %s ===\n' "$*"; } ok() { printf ' [ok] %s\n' "$*"; PASS+=("$*"); } warn() { printf ' [WARN] %s\n' "$*"; WARN+=("$*"); } skip() { printf ' [skip] %s (already present)\n' "$*"; } fetch() { # fetch curl -fsSL --max-time 300 -o "$2" "$1" } # ---------------------------------------------------------------- 1. Python venv say "1/6 Python virtual environment" if [ -x "$VENV/bin/python" ]; then skip "venv" else python3 -m venv "$VENV" || { echo "FATAL: venv creation failed"; exit 1; } fi if "$VENV/bin/python" -c "import pyubx2, pygnssutils, serial, numpy, pandas, pyproj, matplotlib" 2>/dev/null; then skip "python packages" else "$VENV/bin/pip" install --quiet --upgrade pip "$VENV/bin/pip" install --quiet pyubx2 pygnssutils pyserial numpy pandas pyproj matplotlib \ || { echo "FATAL: pip install failed"; exit 1; } fi "$VENV/bin/python" -c "import pyubx2, pygnssutils, serial, numpy, pandas, pyproj, matplotlib" \ && ok "venv with pyubx2/pygnssutils/pyserial/numpy/pandas/pyproj/matplotlib" \ || { echo "FATAL: package import check failed"; exit 1; } mkdir -p "$TOOLS/bin" "$TOOLS/antex" "$TOOLS/cache" "$TOOLS/src" # ---------------------------------------------------------------- 2. RTKLIB demo5 say "2/6 RTKLIB demo5 $DEMO5_TAG (convbin, rnx2rtkp, pos2kml)" build_demo5_app() { # build_demo5_app local app="$1" local gccdir="$TOOLS/src/RTKLIB/app/consapp/$app/gcc" [ -d "$gccdir" ] || return 1 # -lgfortran in some makefiles is vestigial; try without it first. if ! make -C "$gccdir" -j"$(nproc)" LDLIBS="-lm" >"$TOOLS/src/build_$app.log" 2>&1; then make -C "$gccdir" clean >/dev/null 2>&1 make -C "$gccdir" -j"$(nproc)" >>"$TOOLS/src/build_$app.log" 2>&1 || return 1 fi cp "$gccdir/$app" "$TOOLS/bin/$app" } if [ -x "$TOOLS/bin/convbin" ] && [ -x "$TOOLS/bin/rnx2rtkp" ] && [ -x "$TOOLS/bin/pos2kml" ]; then skip "RTKLIB binaries" [ -f "$TOOLS/STOCK_RTKLIB" ] && warn "using STOCK RTKLIB (BDS/L5 signals will be missing from RINEX)" else if [ ! -d "$TOOLS/src/RTKLIB" ]; then git clone --depth 1 --branch "$DEMO5_TAG" "$DEMO5_REPO" "$TOOLS/src/RTKLIB" \ || warn "git clone of demo5 RTKLIB failed" fi built=true for app in convbin rnx2rtkp pos2kml; do if build_demo5_app "$app"; then printf ' built %s\n' "$app" else warn "demo5 build failed for $app (see tools/src/build_$app.log)" built=false fi done if $built; then rm -f "$TOOLS/STOCK_RTKLIB" ok "RTKLIB demo5 $DEMO5_TAG built from source" else # Fallback: stock RTKLIB 2.4.3 from the Ubuntu archive (works, but silently # drops BeiDou + L5/E5 observations from this receiver's raw data). echo " falling back to stock RTKLIB 2.4.3 from apt archive..." tmpd="$(mktemp -d)" if (cd "$tmpd" && apt-get download rtklib >/dev/null 2>&1 && dpkg -x rtklib_*.deb x); then cp "$tmpd"/x/usr/bin/{convbin,rnx2rtkp,pos2kml} "$TOOLS/bin/" touch "$TOOLS/STOCK_RTKLIB" warn "STOCK RTKLIB installed - BDS/L5 will be MISSING from RINEX output" else warn "stock RTKLIB fallback also failed - pipeline cannot run" fi rm -rf "$tmpd" fi fi for app in convbin rnx2rtkp pos2kml; do [ -x "$TOOLS/bin/$app" ] || warn "$app missing from tools/bin" done # ---------------------------------------------------------------- 3. Antenna models say "3/6 Antenna calibration models (ANTEX)" if [ -s "$TOOLS/antex/igs20.atx" ]; then skip "igs20.atx"; else fetch "https://files.igs.org/pub/station/general/igs20.atx" "$TOOLS/antex/igs20.atx" \ && ok "igs20.atx (IGS antenna models incl. CORS base antennas)" \ || warn "igs20.atx download failed" fi if [ -s "$TOOLS/antex/ngs20.atx" ]; then skip "ngs20.atx"; else fetch "https://geodesy.noaa.gov/ANTCAL/LoadFile?file=ngs20.atx" "$TOOLS/antex/ngs20.atx" \ && ok "ngs20.atx (NGS absolute calibrations incl. ArduSimple)" \ || warn "ngs20.atx download failed" fi # Resolve the rover antenna (NGS calibration AS-ANT3BCAL01, ArduSimple) to its # exact 20-char ANTEX type string, consumed by process_gps.py as ant1-anttype. "$VENV/bin/python" - "$TOOLS" <<'EOF' import json, re, sys, pathlib tools = pathlib.Path(sys.argv[1]) result = {"antex_name": None, "source": None, "candidates": []} for atx in ("ngs20.atx", "igs20.atx"): p = tools / "antex" / atx if not p.is_file(): continue for line in p.open(encoding="utf-8", errors="replace"): if "TYPE / SERIAL" in line and "ANT3B" in line[:20].upper(): name = line[:20].rstrip() result["candidates"].append({"name": name, "file": atx}) if result["antex_name"] is None: result["antex_name"], result["source"] = name, atx (tools / "antenna.json").write_text(json.dumps(result, indent=2)) if result["antex_name"]: print(f' [ok] rover antenna resolved: "{result["antex_name"]}" ({result["source"]})') else: print(" [WARN] no ANT3B antenna found in ANTEX files - PPK will run without rover PCO/PCV") EOF # ---------------------------------------------------------------- 4. CRX2RNX say "4/6 CRX2RNX (Hatanaka RINEX decompressor, fallback for .d CORS files)" if [ -x "$TOOLS/bin/CRX2RNX" ]; then skip "CRX2RNX" else tmpd="$(mktemp -d)" if fetch "https://terras.gsi.go.jp/ja/crx2rnx/RNXCMP_4.2.0_Linux_gcc_64bit.tar.gz" "$tmpd/rnxcmp.tar.gz" \ && tar -xzf "$tmpd/rnxcmp.tar.gz" -C "$tmpd" \ && cp "$tmpd"/RNXCMP_*/bin/CRX2RNX "$TOOLS/bin/CRX2RNX" 2>/dev/null \ || cp "$tmpd"/RNXCMP_*/CRX2RNX "$TOOLS/bin/CRX2RNX" 2>/dev/null; then chmod +x "$TOOLS/bin/CRX2RNX" ok "CRX2RNX 4.2.0" else warn "CRX2RNX install failed (only needed for CORS stations without plain .o files)" fi rm -rf "$tmpd" fi # ---------------------------------------------------------------- 5. CORS catalog say "5/6 NGS CORS station catalog" if [ -s "$TOOLS/cache/itrf2020_geo.comp.txt" ]; then skip "station catalog" else fetch "https://geodesy.noaa.gov/corsdata/coord/coord_20/itrf2020_geo.comp.txt" \ "$TOOLS/cache/itrf2020_geo.comp.txt" \ && ok "CORS station catalog (ITRF2020 coordinates)" \ || warn "CORS catalog download failed (process_gps.py will retry at run time)" fi # ---------------------------------------------------------------- 6. Self-check say "6/6 Self-check summary" for app in convbin rnx2rtkp pos2kml; do if [ -x "$TOOLS/bin/$app" ]; then printf ' %-10s %s\n' "$app" "$("$TOOLS/bin/$app" -h 2>&1 | grep -m1 -iE 'synops|usage|version' || echo present)" fi done [ -x "$TOOLS/bin/CRX2RNX" ] && printf ' %-10s %s\n' "CRX2RNX" "present" [ -s "$TOOLS/cache/itrf2020_geo.comp.txt" ] && printf ' %-10s %d stations\n' "catalog" "$(wc -l < "$TOOLS/cache/itrf2020_geo.comp.txt")" [ -s "$TOOLS/antenna.json" ] && printf ' %-10s %s\n' "antenna" "$(cat "$TOOLS/antenna.json" | "$VENV/bin/python" -c 'import json,sys; print(json.load(sys.stdin)["antex_name"])')" echo if [ ${#WARN[@]} -gt 0 ]; then echo "Completed with ${#WARN[@]} warning(s):" printf ' - %s\n' "${WARN[@]}" else echo "Setup complete - no warnings." fi echo echo "Mode 1 (real-time RTK) additionally requires free registration at" echo " http://rtn.usm.edu/RegisterAccount.aspx (Mississippi GCGC RTN)" echo "then: export GCGC_USER= GCGC_PASS="