#!/bin/bash # liberate.sh — AdaCam Liberation Script v0.5 # # Usage: ssh root@192.168.0.10 'bash -s' < liberate.sh # # v0.5 changes from v0.4: # - NO AP/SSID/IP changes (keep factory 192.168.0.10) # - NO firewall rules # - NO reverse tunnel masking # - Adds: SSH key deployment (safe path, no sshd_config changes) # - Adds: adacam services install (capture, forwarder, wigle) embedded inline # # Philosophy: get working first, harden later. # # CRITICAL: Only /data is reliably writable. /etc may be writable. Assume /usr, /var read-only. set -euo pipefail ADACAM_DATA="/data/adacam" log() { echo "[liberate] $1"; } ok() { echo "[liberate] ✓ $1"; } warn() { echo "[liberate] ⚠ $1"; } die() { echo "[liberate] ✗ FATAL: $1"; exit 1; } log "" log "╔══════════════════════════════════════════╗" log "║ AdaCam Liberation Script v0.5 ║" log "║ Liberating bees from the hive ║" log "╚══════════════════════════════════════════╝" log "" [ "$(id -u)" = "0" ] || die "must run as root" if [ -f "$ADACAM_DATA/liberated" ]; then die "device already liberated (marker exists at $ADACAM_DATA/liberated)" fi # ── WRITABILITY CHECK ───────────────────────────────────────────────────────── log "=== Checking filesystem writability ===" DATA_WRITABLE=false; ETC_WRITABLE=false; OPT_WRITABLE=false for d in /data /etc /opt; do if touch "${d}/.adacam_test" 2>/dev/null; then rm -f "${d}/.adacam_test" ok "Writable: $d" case "$d" in /data) DATA_WRITABLE=true ;; /etc) ETC_WRITABLE=true ;; /opt) OPT_WRITABLE=true ;; esac else warn "Read-only or missing: $d" fi done [ "$DATA_WRITABLE" = "true" ] || die "/data is not writable — cannot proceed" [ -f /opt/odc-api/odc-api-bee.js ] || warn "odc-api not found — may not be a factory device" ok "running on: $(uname -n)" # ── DETECT SERIAL ───────────────────────────────────────────────────────────── log "" log "=== Detecting device serial ===" SERIAL="" SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0') || true [ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 2>/dev/null | grep link/ether | awk '{print $2}' | tr -d ':') || true [ -z "$SERIAL" ] && SERIAL=$(cat /sys/class/net/wlan0/address 2>/dev/null | tr -d ':') || true [ -z "$SERIAL" ] && die "Cannot determine device serial" ok "device serial: $SERIAL" API_TOKEN=$(echo -n "adacam-api-${SERIAL}-token" | sha256sum | cut -c1-32) ok "API token: $API_TOKEN" # ── SETUP /data/adacam ──────────────────────────────────────────────────────── log "" log "=== Setting up /data/adacam ===" mkdir -p "$ADACAM_DATA/logs" "$ADACAM_DATA/cache" echo "$SERIAL" > "$ADACAM_DATA/device_serial" echo "$API_TOKEN" > "$ADACAM_DATA/api_token" chmod 600 "$ADACAM_DATA/device_serial" "$ADACAM_DATA/api_token" cat > "$ADACAM_DATA/config.json" << CONFIG { "device_id": "$SERIAL", "ap_ip": "192.168.0.10", "adamaps_key": "adamaps-ingest-2026", "adamaps_api": "https://api.adamaps.org", "liberated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "liberate_version": "0.5" } CONFIG ok "config written" # ── PHASE 1: KILL HIVEMAPPER SERVICES ───────────────────────────────────────── log "" log "=== Phase 1: Stopping Hivemapper services ===" # NOTE: usb-updater is NOT touched — recovery path KILL_SERVICES="odc-api mitmproxy mender-client here-plugin model-zoo collectd hivemapper-data-logger hivemapper-folder-purger beekeeper-plugin video-processor cpu-mem-logger vnstat vnstatd rm_vpu_daemon dnf-automatic dnf-automatic-download dnf-automatic-install dnf-makecache" for svc in $KILL_SERVICES; do if systemctl list-unit-files "${svc}.service" 2>/dev/null | grep -q "$svc"; then systemctl stop "${svc}.service" 2>/dev/null || true systemctl disable "${svc}.service" 2>/dev/null || true systemctl mask "${svc}.service" 2>/dev/null || true ok "killed: $svc" fi done # ── PHASE 2: BLOCK HIVEMAPPER DOMAINS ───────────────────────────────────────── log "" log "=== Phase 2: Blocking Hivemapper endpoints ===" if [ "$ETC_WRITABLE" = "true" ]; then BLOCK_HOSTS="hivemapper.com api.hivemapper.com beemaps.com api.trybeekeeper.ai docker.mender.io s3.mender.io direct.data.api.platform.here.com api-lookup.data.api.platform.here.com account.api.here.com edge.hereapi.com olp.here.com dashcam-firmware.s3.us-west-2.amazonaws.com cfapi.cloudflare.com" for host in $BLOCK_HOSTS; do if ! grep -q "$host" /etc/hosts 2>/dev/null; then echo "0.0.0.0 $host" >> /etc/hosts && ok "blocked: $host" || warn "failed: $host" fi done else warn "/etc not writable — skipping host blocking" fi # ── PHASE 3: SSH KEYS ───────────────────────────────────────────────────────── log "" log "=== Phase 3: Deploying SSH keys ===" # Safe path — no sshd_config changes, no overlay touches # Writing to /home/root/.ssh/ which persists (bind-mounted from /data/home at runtime) SSH_DIR="/home/root/.ssh" AUTH_KEYS="$SSH_DIR/authorized_keys" mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" # Write keys — these are the canonical AdaCam authorized keys cat > "$AUTH_KEYS" << 'KEYS' ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw KEYS chmod 600 "$AUTH_KEYS" ok "SSH keys written to $AUTH_KEYS" ok "Password auth preserved (no sshd_config changes)" # ── PHASE 4: INSTALL ADACAM SERVICES ────────────────────────────────────────── log "" log "=== Phase 4: Installing adacam services ===" # Determine install dir — prefer /opt/adacam, fall back to /data/adacam/bin if [ "$OPT_WRITABLE" = "true" ]; then INSTALL_DIR="/opt/adacam" ok "Installing to /opt/adacam" else INSTALL_DIR="/data/adacam/bin" warn "/opt not writable — installing to $INSTALL_DIR" fi mkdir -p "$INSTALL_DIR/capture" mkdir -p "$INSTALL_DIR/wigle" mkdir -p /tmp/adacam/pics mkdir -p /tmp/recording/pics # ── adacam-capture.py ───────────────────────────────────────────────────────── cat > "$INSTALL_DIR/capture/adacam-capture.py" << 'PYEOF' #!/usr/bin/env python3 """adacam-capture: GStreamer camera capture for AdaCam/Keem Bay.""" import json, logging, os, signal, subprocess, sys, time from pathlib import Path logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', stream=sys.stdout) log = logging.getLogger('adacam-capture') CONFIG_PATH = Path('/data/adacam/config.json') FRAME_DIR = Path('/tmp/adacam/pics') PIPE_FRAME = Path('/tmp/recording/pics/cam0pipe.jpg') VPU_FWNAME = '/sys/devices/platform/soc/soc:vpusmm/fwname' MAX_FILES, MAX_BYTES = 1300, 500 * 1024 * 1024 MODULES = ['kmb_flash', 'kmb_lens', 'kmb_cam', 'pwm_keembay', 'hantro'] GST_PIPELINE = '''kmbcamsrc num-frames=-1 name=kmbsrc transform-hub=full awb-mode=daylight stride-align=64 scanline-align=64 ! capsfilter caps="video/x-raw(memory:DMABuf),format=NV12,width=2028,height=1024,framerate=30/1" ! vaapijpegenc ! multifilesink location=/tmp/recording/pics/cam0pipe.jpg''' running, redis_client = True, None def load_config(): if CONFIG_PATH.exists(): with open(CONFIG_PATH) as f: return json.load(f) return {'device_id': 'unknown', 'adamaps_key': '', 'adamaps_api': ''} def get_redis(): global redis_client if redis_client: try: redis_client.ping(); return redis_client except: redis_client = None try: import redis redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True) redis_client.ping(); log.info('Connected to Redis'); return redis_client except Exception as e: log.warning(f'Redis unavailable: {e}'); return None def load_modules(): try: with open('/proc/modules') as f: loaded = {l.split()[0] for l in f} except: return for mod in MODULES: if mod not in loaded: try: subprocess.run(['modprobe', mod], check=True, capture_output=True) except: log.warning(f'Failed to load {mod}') def load_vpu_firmware(): if os.path.exists(VPU_FWNAME): try: with open(VPU_FWNAME, 'w') as f: f.write('luxonis_vpu.bin') log.info('VPU firmware loaded') except Exception as e: log.warning(f'VPU firmware error: {e}') else: try: subprocess.run(['StartVpu', 'luxonis_vpu.bin'], check=True, capture_output=True); log.info('VPU loaded via StartVpu') except: log.warning('StartVpu not available') def start_gstreamer(): PIPE_FRAME.parent.mkdir(parents=True, exist_ok=True) FRAME_DIR.mkdir(parents=True, exist_ok=True) log.info('Starting GStreamer pipeline') return subprocess.Popen(['gst-launch-1.0', '-e'] + GST_PIPELINE.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) def cleanup_frames(): try: frames = sorted(FRAME_DIR.glob('*.jpg'), key=lambda p: p.stat().st_mtime) except: return while len(frames) > MAX_FILES: try: frames[0].unlink(); frames.pop(0) except: break total = sum(f.stat().st_size for f in frames if f.exists()) while total > MAX_BYTES and frames: try: sz = frames[0].stat().st_size; frames[0].unlink(); frames.pop(0); total -= sz except: break def process_frame(): if not PIPE_FRAME.exists(): return False try: ts = int(time.time() * 1000) dest = FRAME_DIR / f'{ts}.jpg' os.rename(PIPE_FRAME, dest) r = get_redis() if r: try: r.publish('adacam:frames', str(dest)) except Exception as e: log.warning(f'Redis publish failed: {e}') return True except FileNotFoundError: return False except Exception as e: log.error(f'Frame error: {e}'); return False def handle_signal(signum, frame): global running running = False def main(): global running signal.signal(signal.SIGTERM, handle_signal); signal.signal(signal.SIGINT, handle_signal) config = load_config() log.info(f"AdaCam Capture starting (device: {config.get('device_id', 'unknown')})") load_modules(); load_vpu_firmware() gst_proc, last_cleanup = None, 0 while running: if gst_proc is None or gst_proc.poll() is not None: if gst_proc: time.sleep(2) gst_proc = start_gstreamer() frame_found = process_frame() now = time.time() if now - last_cleanup > 60: cleanup_frames(); last_cleanup = now time.sleep(0.01 if frame_found else 0.05) if gst_proc and gst_proc.poll() is None: gst_proc.terminate() try: gst_proc.wait(timeout=5) except: gst_proc.kill() if __name__ == '__main__': main() PYEOF chmod +x "$INSTALL_DIR/capture/adacam-capture.py" ok "adacam-capture.py installed" # ── adacam-forwarder.py ─────────────────────────────────────────────────────── cat > "$INSTALL_DIR/capture/adacam-forwarder.py" << 'PYEOF' #!/usr/bin/env python3 """adacam-forwarder: GPS-tagged frame forwarder for AdaMaps.""" import json, logging, signal, sqlite3, sys, threading, time from pathlib import Path import requests logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', stream=sys.stdout) log = logging.getLogger('adacam-forwarder') CONFIG_PATH = Path('/data/adacam/config.json') QUEUE_DB = Path('/data/adacam/forward_queue.db') ODC_DB = Path('/data/recording/odc-api.db') GNSS_KEYS = ['GNSSFusion30Hz', 'GnssData', 'GnssPvt', 'GnssPosition'] config, redis_client = {}, None running = True def _handle_signal(signum, frame): global running; running = False signal.signal(signal.SIGTERM, _handle_signal); signal.signal(signal.SIGINT, _handle_signal) def load_config(): if CONFIG_PATH.exists(): with open(CONFIG_PATH) as f: return json.load(f) return {'device_id': 'unknown', 'adamaps_key': '', 'adamaps_api': 'https://api.adamaps.org'} def init_queue_db(): QUEUE_DB.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(QUEUE_DB)) conn.execute('CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, payload TEXT, created_at REAL, attempts INTEGER DEFAULT 0)') conn.commit(); return conn def get_redis(): global redis_client if redis_client: try: redis_client.ping(); return redis_client except: redis_client = None try: import redis redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True) redis_client.ping(); return redis_client except Exception as e: log.warning(f'Redis unavailable: {e}'); return None def get_latest_gps(r=None): if ODC_DB.exists(): try: conn = sqlite3.connect(str(ODC_DB)) row = conn.execute('SELECT latitude, longitude, altitude, hdop, satellites_used, time FROM framekms WHERE latitude IS NOT NULL AND latitude != 0 ORDER BY time DESC LIMIT 1').fetchone() conn.close() if row: return {'lat': row[0], 'lon': row[1], 'alt': row[2], 'hdop': row[3], 'satellites': row[4], 'ts': row[5]} except Exception as e: log.debug(f'odc-api.db GPS read failed: {e}') if r: for key in GNSS_KEYS: try: for getter in [lambda k: r.zrevrange(k, 0, 0), lambda k: [r.get(k)]]: items = getter(key) if items and items[0]: data = json.loads(items[0]) lat = data.get('latitude') or data.get('lat_deg') lon = data.get('longitude') or data.get('lon_deg') if lat and lon: return {'lat': lat, 'lon': lon, 'alt': data.get('altitude') or data.get('alt_m', 0), 'hdop': data.get('hdop', 99), 'satellites': data.get('satellites_used') or data.get('num_satellites', 0), 'ts': int(data.get('unix_milliseconds', time.time() * 1000))} except: continue return None def queue_payload(db, payload): try: db.execute('INSERT INTO queue (payload, created_at) VALUES (?, ?)', (json.dumps(payload), time.time())); db.commit() except Exception as e: log.error(f'Queue error: {e}') def post_to_adamaps(payload): if not config.get('adamaps_key'): return False url = f"{config.get('adamaps_api', 'https://api.adamaps.org')}/api/ingest" try: resp = requests.post(url, json=payload, headers={'X-AdaMaps-Key': config['adamaps_key']}, timeout=10) return resp.status_code == 200 except: return False def process_queue(db): try: for row_id, payload_json, attempts in db.execute('SELECT id, payload, attempts FROM queue ORDER BY created_at LIMIT 50').fetchall(): if attempts > 10: db.execute('DELETE FROM queue WHERE id = ?', (row_id,)); continue if post_to_adamaps(json.loads(payload_json)): db.execute('DELETE FROM queue WHERE id = ?', (row_id,)) else: db.execute('UPDATE queue SET attempts = ? WHERE id = ?', (attempts + 1, row_id)) time.sleep(0.1) db.commit() except Exception as e: log.error(f'Queue process error: {e}') def queue_worker(db): while running: time.sleep(60); running and process_queue(db) def handle_frame(frame_path, db): path = Path(frame_path) if not path.exists(): return r = get_redis(); gps = get_latest_gps(r) try: frame_size = path.stat().st_size except: frame_size = 0 try: ts = int(path.stem) except: ts = int(time.time() * 1000) payload = {'device_id': config.get('device_id', 'unknown'), 'ts': ts, 'lat': gps['lat'] if gps else None, 'lon': gps['lon'] if gps else None, 'alt': gps['alt'] if gps else None, 'frame_path': str(path), 'frame_size': frame_size} if not post_to_adamaps(payload): queue_payload(db, payload) def main(): global config, running config = load_config() log.info(f"AdaCam Forwarder starting (device: {config.get('device_id', 'unknown')})") db = init_queue_db() threading.Thread(target=queue_worker, args=(db,), daemon=True).start() backoff = 1 while running: r = get_redis() if not r: time.sleep(backoff); backoff = min(backoff * 2, 30); continue backoff = 1 try: import redis as redis_module pubsub = r.pubsub(); pubsub.subscribe('adacam:frames') for msg in pubsub.listen(): if not running: break if msg['type'] == 'message': handle_frame(msg['data'], db) except: time.sleep(1) if __name__ == '__main__': try: main() except KeyboardInterrupt: running = False PYEOF chmod +x "$INSTALL_DIR/capture/adacam-forwarder.py" ok "adacam-forwarder.py installed" # ── adacam-wigle.py ─────────────────────────────────────────────────────────── cat > "$INSTALL_DIR/wigle/adacam-wigle.py" << 'PYEOF' #!/usr/bin/env python3 """AdaCam WiGLE wardriving service — WiFi scanning + WiGLE upload.""" import base64, json, logging, os, signal, sqlite3, subprocess, time from datetime import datetime import redis, requests logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') log = logging.getLogger('adacam-wigle') DB_PATH = '/data/adacam/wigle.db' ODC_DB = '/data/recording/odc-api.db' INTERFACE = 'wlp1s0f1' WIGLE_UPLOAD_URL = 'https://api.wigle.net/api/v2/file/upload' WIGLE_CSV_HEADER = 'WigleWifi-1.4,appRelease=adacam-1.0,model=HivemapperBee,release=1.0,device=AdaCam,display=AdaCam,board=KeemBay,brand=AdaMaps\nMAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type' GNSS_KEYS = ['GNSSFusion30Hz', 'GnssData', 'GnssPvt', 'GnssPosition'] running = True def _handle_signal(signum, frame): global running; running = False signal.signal(signal.SIGTERM, _handle_signal); signal.signal(signal.SIGINT, _handle_signal) def init_db(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.execute('''CREATE TABLE IF NOT EXISTS wigle_records (id INTEGER PRIMARY KEY AUTOINCREMENT, bssid TEXT NOT NULL, ssid TEXT, auth_mode TEXT, first_seen TEXT, channel INTEGER, rssi INTEGER, lat REAL, lon REAL, alt REAL, accuracy REAL, uploaded INTEGER DEFAULT 0, created_at INTEGER)''') conn.execute('''CREATE TABLE IF NOT EXISTS wigle_config (key TEXT PRIMARY KEY, value TEXT)''') conn.commit(); return conn def get_config(conn): cfg = dict(conn.execute('SELECT key, value FROM wigle_config').fetchall()) return {'enabled': cfg.get('enabled', '0') == '1', 'api_name': cfg.get('api_name', ''), 'api_token': cfg.get('api_token', ''), 'scan_interval_seconds': int(cfg.get('scan_interval_seconds', '30')), 'upload_interval_seconds': int(cfg.get('upload_interval_seconds', '300'))} def get_latest_gps(r=None): if os.path.exists(ODC_DB): try: conn = sqlite3.connect(ODC_DB) row = conn.execute('SELECT latitude, longitude, altitude, hdop, satellites_used FROM framekms WHERE latitude IS NOT NULL AND latitude != 0 ORDER BY time DESC LIMIT 1').fetchone() conn.close() if row: return {'lat': row[0], 'lon': row[1], 'alt': row[2], 'hdop': row[3], 'satellites_used': row[4]} except: pass if r: for key in GNSS_KEYS: try: for val in [r.zrange(key, -1, -1), [r.get(key)]]: item = val[0] if val else None if item: d = json.loads(item) lat = d.get('latitude') or d.get('lat_deg') lon = d.get('longitude') or d.get('lon_deg') if lat and lon: return {'lat': lat, 'lon': lon, 'alt': d.get('altitude') or d.get('alt_m', 0), 'hdop': d.get('hdop', 99), 'satellites_used': d.get('satellites_used') or d.get('num_satellites', 0)} except: continue return None def freq_to_channel(freq): if 2412 <= freq <= 2484: return (freq - 2407) // 5 if 5170 <= freq <= 5825: return (freq - 5000) // 5 return 0 def parse_auth_mode(flags): modes = [] if 'WPA3' in flags: modes.append('WPA3-PSK-SAE') elif 'WPA2' in flags: modes.append('WPA2-PSK-CCMP') elif 'WPA' in flags: modes.append('WPA-PSK-TKIP') if 'WPS' in flags: modes.append('WPS') if not modes: modes.append('ESS') return '[' + ']['.join(modes) + ']' def get_scan_results(): try: subprocess.run(['wpa_cli', '-i', INTERFACE, 'scan'], capture_output=True, timeout=5) time.sleep(4) result = subprocess.run(['wpa_cli', '-i', INTERFACE, 'scan_results'], capture_output=True, text=True, timeout=5) networks = [] for line in result.stdout.strip().split('\n')[1:]: parts = line.split('\t') if len(parts) >= 5: networks.append({'bssid': parts[0], 'frequency': int(parts[1]) if parts[1].isdigit() else 0, 'signal_dbm': int(parts[2]) if parts[2].lstrip('-').isdigit() else -100, 'flags': parts[3], 'ssid': parts[4]}) return networks except Exception as e: log.warning(f'Scan failed: {e}'); return [] def store_records(conn, networks, gps): now, first_seen = int(time.time()), datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') accuracy = gps.get('hdop', 5) * 5 for n in networks: conn.execute('INSERT INTO wigle_records (bssid, ssid, auth_mode, first_seen, channel, rssi, lat, lon, alt, accuracy, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)', (n['bssid'], n['ssid'], parse_auth_mode(n['flags']), first_seen, freq_to_channel(n['frequency']), n['signal_dbm'], gps['lat'], gps['lon'], gps.get('alt', 0), accuracy, now)) conn.commit(); log.info(f'Stored {len(networks)} networks') def upload_to_wigle(csv_content, api_name, api_token): creds = base64.b64encode(f'{api_name}:{api_token}'.encode()).decode() try: resp = requests.post(WIGLE_UPLOAD_URL, headers={'Authorization': f'Basic {creds}'}, files={'file': ('adacam-scan.csv', csv_content.encode(), 'text/csv')}, data={'donate': 'false'}, timeout=30) return resp.ok, resp.json() if resp.ok else resp.text except Exception as e: return False, str(e) def upload_pending(conn, cfg): if not cfg['api_name'] or not cfg['api_token']: return rows = conn.execute('SELECT id, bssid, ssid, auth_mode, first_seen, channel, rssi, lat, lon, alt, accuracy FROM wigle_records WHERE uploaded=0 LIMIT 10000').fetchall() if not rows: return ids = [r[0] for r in rows] csv_lines = [WIGLE_CSV_HEADER] + [f'{r[1]},{r[2]},{r[3]},{r[4]},{r[5]},{r[6]},{r[7]},{r[8]},{r[9]},{r[10]},WIFI' for r in rows] ok_flag, result = upload_to_wigle('\n'.join(csv_lines), cfg['api_name'], cfg['api_token']) if ok_flag: conn.execute(f'UPDATE wigle_records SET uploaded=1 WHERE id IN ({",".join("?" * len(ids))})', ids) conn.commit(); log.info(f'Uploaded {len(ids)} records to WiGLE') else: log.warning(f'WiGLE upload failed: {result}') def main(): global running log.info('AdaCam WiGLE service starting') conn, r, last_upload = init_db(), None, 0 while running: try: if r is None: r = redis.Redis(host='localhost', port=6379, decode_responses=True) cfg = get_config(conn) if not cfg['enabled']: time.sleep(10); continue gps = get_latest_gps(r) if gps and gps.get('hdop', 99) < 5 and gps.get('satellites_used', 0) >= 4: networks = get_scan_results() if networks: store_records(conn, networks, gps) now = time.time() if now - last_upload >= cfg['upload_interval_seconds']: upload_pending(conn, cfg); last_upload = now time.sleep(cfg['scan_interval_seconds']) except redis.RedisError: r = None; time.sleep(5) except Exception as e: log.error(f'Error: {e}'); time.sleep(10) if __name__ == '__main__': main() PYEOF chmod +x "$INSTALL_DIR/wigle/adacam-wigle.py" ok "adacam-wigle.py installed" # ── INSTALL PYTHON DEPS ─────────────────────────────────────────────────────── log "" log "=== Installing Python dependencies ===" if command -v pip3 &>/dev/null; then pip3 install redis requests --quiet && ok "Python deps installed" || warn "pip3 install failed — may need manual install" else warn "pip3 not found — skipping dep install" fi # ── INSTALL SYSTEMD UNITS ───────────────────────────────────────────────────── log "" log "=== Installing systemd units ===" if [ "$ETC_WRITABLE" = "true" ] || [ -d /etc/systemd/system ]; then cat > /etc/systemd/system/adacam-capture.service << SVCEOF [Unit] Description=AdaCam Camera Capture Service After=network.target redis.service Wants=redis.service [Service] Type=simple ExecStart=${INSTALL_DIR}/capture/adacam-capture.py Restart=always RestartSec=5 MemoryMax=256M CPUQuota=80% [Install] WantedBy=multi-user.target SVCEOF cat > /etc/systemd/system/adacam-forwarder.service << SVCEOF [Unit] Description=AdaCam Frame Forwarder Service After=network.target redis.service adacam-capture.service Wants=redis.service [Service] Type=simple ExecStart=${INSTALL_DIR}/capture/adacam-forwarder.py Restart=always RestartSec=5 MemoryMax=128M CPUQuota=20% [Install] WantedBy=multi-user.target SVCEOF cat > /etc/systemd/system/adacam-wigle.service << SVCEOF [Unit] Description=AdaCam WiGLE Wardriving Service After=network.target redis.service Wants=redis.service [Service] Type=simple ExecStart=/usr/bin/python3 ${INSTALL_DIR}/wigle/adacam-wigle.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target SVCEOF systemctl daemon-reload systemctl enable adacam-capture adacam-forwarder adacam-wigle ok "systemd units installed and enabled" else warn "/etc/systemd/system not writable — units not installed" fi # ── MARK LIBERATED ──────────────────────────────────────────────────────────── log "" log "=== Marking device as liberated ===" cat > "$ADACAM_DATA/liberated" << MARKER liberated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) serial=$SERIAL version=0.5 MARKER ok "liberation marker written" # ── SUMMARY ─────────────────────────────────────────────────────────────────── log "" echo "════════════════════════════════════════════════════════" echo " AdaCam Liberation Complete (v0.5)" echo "════════════════════════════════════════════════════════" echo "" echo " Device Serial: $SERIAL" echo " API Token: $API_TOKEN" echo " AP IP: 192.168.0.10 (UNCHANGED — factory)" echo " SSH: root@192.168.0.10 (password + key auth)" echo "" echo " Services installed:" echo " adacam-capture → camera → /tmp/adacam/pics/" echo " adacam-forwarder → frames + GPS → ADAMaps queue" echo " adacam-wigle → WiFi scan → WiGLE (disabled by default)" echo "" echo " Config: /data/adacam/config.json" echo " Data: /data/adacam/" echo "" echo " REBOOT to start services." echo " Services autostart on next boot." echo "" echo " NO firewall changes. NO AP changes. NO IP changes." echo "════════════════════════════════════════════════════════" echo "" log "Run: reboot"