Add liberate v0.5 — no AP/firewall changes, factory IP preserved, services + keys

This commit is contained in:
Kayos 2026-03-22 00:41:18 -07:00
parent ed7ae5ba57
commit 2455241345

672
liberate-v0.5.sh Normal file
View file

@ -0,0 +1,672 @@
#!/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"