Isolate multi-VM setup with deterministic device targeting

This commit is contained in:
Lakr
2026-03-06 12:46:12 +08:00
parent 5c2bce03dd
commit b50b630d19
11 changed files with 252 additions and 59 deletions

View File

@@ -8,6 +8,10 @@ CPU ?= 8
MEMORY ?= 8192
DISK_SIZE ?= 64
CFW_INPUT ?= cfw_input
RESTORE_UDID ?=
RESTORE_ECID ?=
IRECOVERY_ECID ?=
SSH_PORT ?= 2222
# ─── Build info ──────────────────────────────────────────────────
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
@@ -77,7 +81,7 @@ help:
@echo " make cfw_install_dev Install CFW mods via SSH (dev mode)"
@echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)"
@echo ""
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)"
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE) SSH_PORT=$(SSH_PORT)"
# ═══════════════════════════════════════════════════════════════════
# Setup
@@ -208,10 +212,16 @@ fw_patch_jb:
.PHONY: restore_get_shsh restore
restore_get_shsh:
cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" -e -y ./iPhone*_Restore -t
cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" \
$(if $(RESTORE_UDID),-u $(RESTORE_UDID),) \
$(if $(RESTORE_ECID),-i $(RESTORE_ECID),) \
-e -y ./iPhone*_Restore -t
restore:
cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" -e -y ./iPhone*_Restore
cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" \
$(if $(RESTORE_UDID),-u $(RESTORE_UDID),) \
$(if $(RESTORE_ECID),-i $(RESTORE_ECID),) \
-e -y ./iPhone*_Restore
# ═══════════════════════════════════════════════════════════════════
# Ramdisk
@@ -223,7 +233,8 @@ ramdisk_build:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/ramdisk_build.py" .
ramdisk_send:
cd $(VM_DIR) && IRECOVERY="$(CURDIR)/$(IRECOVERY)" zsh "$(CURDIR)/$(SCRIPTS)/ramdisk_send.sh"
cd $(VM_DIR) && IRECOVERY="$(CURDIR)/$(IRECOVERY)" IRECOVERY_ECID="$(IRECOVERY_ECID)" \
zsh "$(CURDIR)/$(SCRIPTS)/ramdisk_send.sh"
# ═══════════════════════════════════════════════════════════════════
# CFW
@@ -232,10 +243,10 @@ ramdisk_send:
.PHONY: cfw_install cfw_install_dev cfw_install_jb
cfw_install:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
cfw_install_dev:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" .
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" .
cfw_install_jb:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .

View File

@@ -29,7 +29,7 @@ CFW_INPUT="cfw_input"
CFW_ARCHIVE="cfw_input.tar.zst"
TEMP_DIR="$VM_DIR/.cfw_temp"
SSH_PORT=2222
SSH_PORT="${SSH_PORT:-2222}"
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"

View File

@@ -29,7 +29,7 @@ CFW_INPUT="cfw_input"
CFW_ARCHIVE="cfw_input.tar.zst"
TEMP_DIR="$VM_DIR/.cfw_temp"
SSH_PORT=2222
SSH_PORT="${SSH_PORT:-2222}"
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"

View File

@@ -35,7 +35,7 @@ CFW_JB_INPUT="cfw_jb_input"
CFW_JB_ARCHIVE="cfw_jb_input.tar.zst"
TEMP_DIR="$VM_DIR/.cfw_temp"
SSH_PORT=2222
SSH_PORT="${SSH_PORT:-2222}"
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"

View File

@@ -8,8 +8,26 @@
set -euo pipefail
IRECOVERY="${IRECOVERY:-irecovery}"
IRECOVERY_ECID="${IRECOVERY_ECID:-}"
RAMDISK_DIR="${1:-Ramdisk}"
IRECOVERY_ARGS=()
if [[ -n "$IRECOVERY_ECID" ]]; then
IRECOVERY_ECID="${IRECOVERY_ECID#0x}"
IRECOVERY_ECID="${IRECOVERY_ECID#0X}"
[[ "$IRECOVERY_ECID" =~ ^[0-9A-Fa-f]{1,16}$ ]] || {
echo "[-] Invalid IRECOVERY_ECID: ${IRECOVERY_ECID}"
exit 1
}
IRECOVERY_ECID="0x${IRECOVERY_ECID:u}"
IRECOVERY_ARGS=(-i "$IRECOVERY_ECID")
echo "[*] Using ECID selector for irecovery: ${IRECOVERY_ECID}"
fi
irecovery_cmd() {
"$IRECOVERY" "${IRECOVERY_ARGS[@]}" "$@"
}
if [[ ! -d "$RAMDISK_DIR" ]]; then
echo "[-] Ramdisk directory not found: $RAMDISK_DIR"
echo " Run 'make ramdisk_build' first."
@@ -30,48 +48,48 @@ fi
# 1. Load iBSS + iBEC (DFU → recovery)
echo " [1/8] Loading iBSS..."
"$IRECOVERY" -f "$RAMDISK_DIR/iBSS.vresearch101.RELEASE.img4"
irecovery_cmd -f "$RAMDISK_DIR/iBSS.vresearch101.RELEASE.img4"
echo " [2/8] Loading iBEC..."
"$IRECOVERY" -f "$RAMDISK_DIR/iBEC.vresearch101.RELEASE.img4"
"$IRECOVERY" -c go
irecovery_cmd -f "$RAMDISK_DIR/iBEC.vresearch101.RELEASE.img4"
irecovery_cmd -c go
sleep 1
# 2. Load SPTM
echo " [3/8] Loading SPTM..."
"$IRECOVERY" -f "$RAMDISK_DIR/sptm.vresearch1.release.img4"
"$IRECOVERY" -c firmware
irecovery_cmd -f "$RAMDISK_DIR/sptm.vresearch1.release.img4"
irecovery_cmd -c firmware
# 3. Load TXM
echo " [4/8] Loading TXM..."
"$IRECOVERY" -f "$RAMDISK_DIR/txm.img4"
"$IRECOVERY" -c firmware
irecovery_cmd -f "$RAMDISK_DIR/txm.img4"
irecovery_cmd -c firmware
# 4. Load trustcache
echo " [5/8] Loading trustcache..."
"$IRECOVERY" -f "$RAMDISK_DIR/trustcache.img4"
"$IRECOVERY" -c firmware
irecovery_cmd -f "$RAMDISK_DIR/trustcache.img4"
irecovery_cmd -c firmware
# 5. Load ramdisk
echo " [6/8] Loading ramdisk..."
"$IRECOVERY" -f "$RAMDISK_DIR/ramdisk.img4"
irecovery_cmd -f "$RAMDISK_DIR/ramdisk.img4"
sleep 2
"$IRECOVERY" -c ramdisk
irecovery_cmd -c ramdisk
# 6. Load device tree
echo " [7/8] Loading device tree..."
"$IRECOVERY" -f "$RAMDISK_DIR/DeviceTree.vphone600ap.img4"
"$IRECOVERY" -c devicetree
irecovery_cmd -f "$RAMDISK_DIR/DeviceTree.vphone600ap.img4"
irecovery_cmd -c devicetree
# 7. Load SEP
echo " [8/8] Loading SEP..."
"$IRECOVERY" -f "$RAMDISK_DIR/sep-firmware.vresearch101.RELEASE.img4"
"$IRECOVERY" -c firmware
irecovery_cmd -f "$RAMDISK_DIR/sep-firmware.vresearch101.RELEASE.img4"
irecovery_cmd -c firmware
# 8. Load kernel and boot
echo " [*] Booting kernel..."
"$IRECOVERY" -f "$KERNEL_IMG"
"$IRECOVERY" -c bootx
irecovery_cmd -f "$KERNEL_IMG"
irecovery_cmd -c bootx
echo "[+] Boot sequence complete. Device should be booting into ramdisk."

View File

@@ -17,7 +17,7 @@ cd "$PROJECT_ROOT"
LOG_DIR="${PROJECT_ROOT}/setup_logs"
DFU_LOG="${LOG_DIR}/boot_dfu.log"
IPROXY_LOG="${LOG_DIR}/iproxy_2222.log"
IPROXY_LOG=""
DFU_PID=""
IPROXY_PID=""
@@ -32,9 +32,16 @@ POST_RESTORE_KILL_DELAY="${POST_RESTORE_KILL_DELAY:-30}"
POST_KILL_SETTLE_DELAY="${POST_KILL_SETTLE_DELAY:-5}"
RAMDISK_SSH_TIMEOUT="${RAMDISK_SSH_TIMEOUT:-60}"
RAMDISK_SSH_INTERVAL="${RAMDISK_SSH_INTERVAL:-2}"
RAMDISK_SSH_PORT="${RAMDISK_SSH_PORT:-2222}"
RAMDISK_SSH_PORT="${RAMDISK_SSH_PORT:-}"
RAMDISK_SSH_USER="${RAMDISK_SSH_USER:-root}"
RAMDISK_SSH_PASS="${RAMDISK_SSH_PASS:-alpine}"
RAMDISK_SSH_PORT_EXPLICIT=0
if [[ -n "$RAMDISK_SSH_PORT" ]]; then
RAMDISK_SSH_PORT_EXPLICIT=1
fi
DEVICE_UDID=""
DEVICE_ECID=""
JB_MODE=0
DEV_MODE=0
SKIP_PROJECT_SETUP=0
@@ -49,6 +56,92 @@ require_cmd() {
command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd"
}
normalize_ecid() {
local ecid="$1"
ecid="${ecid#0x}"
ecid="${ecid#0X}"
[[ "$ecid" =~ ^[0-9A-Fa-f]{1,16}$ ]] || return 1
printf "%016s" "${ecid:u}" | tr ' ' '0'
}
load_device_identity() {
local prediction_file="${VM_DIR_ABS}/udid-prediction.txt"
local timeout=30
local waited=0
local key value
local udid_ecid
while [[ ! -f "$prediction_file" && "$waited" -lt "$timeout" ]]; do
if [[ -n "$DFU_PID" ]] && ! kill -0 "$DFU_PID" 2>/dev/null; then
break
fi
sleep 1
(( waited++ ))
done
[[ -f "$prediction_file" ]] || die "Missing ${prediction_file}. Rebuild and run make boot_dfu to generate it."
DEVICE_UDID=""
DEVICE_ECID=""
while IFS='=' read -r key value; do
case "$key" in
UDID)
DEVICE_UDID="${value:u}"
;;
ECID)
DEVICE_ECID="$(normalize_ecid "$value" || true)"
;;
esac
done < "$prediction_file"
[[ "$DEVICE_UDID" =~ ^[0-9A-F]{8}-[0-9A-F]{16}$ ]] \
|| die "Invalid UDID in ${prediction_file}: ${DEVICE_UDID}"
if [[ -z "$DEVICE_ECID" ]]; then
DEVICE_ECID="${DEVICE_UDID#*-}"
fi
[[ "$DEVICE_ECID" =~ ^[0-9A-F]{16}$ ]] \
|| die "Invalid ECID in ${prediction_file}: ${DEVICE_ECID}"
udid_ecid="${DEVICE_UDID#*-}"
[[ "$udid_ecid" == "$DEVICE_ECID" ]] \
|| die "UDID/ECID mismatch in ${prediction_file}: ${DEVICE_UDID} vs 0x${DEVICE_ECID}"
echo "[+] Device identity loaded: UDID=${DEVICE_UDID} ECID=0x${DEVICE_ECID}"
}
port_is_listening() {
local port="$1"
lsof -n -t -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
}
pick_random_ssh_port() {
local attempt port
for attempt in {1..200}; do
port=$((20000 + (RANDOM % 40000)))
if ! port_is_listening "$port"; then
echo "$port"
return 0
fi
done
return 1
}
choose_ramdisk_ssh_port() {
if [[ -n "$RAMDISK_SSH_PORT" ]]; then
[[ "$RAMDISK_SSH_PORT" == <-> ]] || die "RAMDISK_SSH_PORT must be an integer"
(( RAMDISK_SSH_PORT >= 1 && RAMDISK_SSH_PORT <= 65535 )) \
|| die "RAMDISK_SSH_PORT out of range: ${RAMDISK_SSH_PORT}"
if port_is_listening "$RAMDISK_SSH_PORT"; then
die "RAMDISK_SSH_PORT ${RAMDISK_SSH_PORT} is already in use"
fi
return
fi
RAMDISK_SSH_PORT="$(pick_random_ssh_port)" \
|| die "Failed to allocate a random local SSH forward port"
}
collect_vm_lock_pids() {
local -a paths pids
local path pid
@@ -329,12 +422,19 @@ wait_for_post_restore_reboot() {
wait_for_recovery() {
local irecovery="${PROJECT_ROOT}/.limd/bin/irecovery"
local -a query_args
[[ -x "$irecovery" ]] || die "irecovery not found at $irecovery"
if [[ -n "$DEVICE_ECID" ]]; then
query_args=(-i "0x${DEVICE_ECID}")
else
query_args=()
fi
echo "[*] Waiting for recovery/DFU endpoint..."
local i
for i in {1..90}; do
if "$irecovery" -q >/dev/null 2>&1; then
if "$irecovery" "${query_args[@]}" -q >/dev/null 2>&1; then
echo "[+] Device endpoint is reachable"
return
fi
@@ -346,29 +446,28 @@ wait_for_recovery() {
exit 1
}
start_iproxy_2222() {
start_iproxy() {
local iproxy_bin
local -a stale_pids
local pid
iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy"
[[ -x "$iproxy_bin" ]] || die "iproxy not found at $iproxy_bin (run: make setup_libimobiledevice)"
[[ -n "$DEVICE_UDID" ]] || die "Device UDID is empty; cannot start isolated iproxy"
stale_pids=(${(@f)$(lsof -n -t -iTCP:2222 -sTCP:LISTEN 2>/dev/null || true)})
if (( ${#stale_pids[@]} > 0 )); then
echo "[*] Found stale listener(s) on tcp/2222, terminating..."
for pid in "${stale_pids[@]}"; do
[[ -z "$pid" || "$pid" == "$$" ]] && continue
kill_descendants "$pid"
kill -9 "$pid" >/dev/null 2>&1 || true
done
sleep 1
choose_ramdisk_ssh_port
if port_is_listening "$RAMDISK_SSH_PORT"; then
if [[ "$RAMDISK_SSH_PORT_EXPLICIT" == "1" ]]; then
die "RAMDISK_SSH_PORT ${RAMDISK_SSH_PORT} is already in use"
fi
RAMDISK_SSH_PORT="$(pick_random_ssh_port)" \
|| die "Failed to allocate a free random local SSH forward port"
fi
IPROXY_LOG="${LOG_DIR}/iproxy_${RAMDISK_SSH_PORT}.log"
mkdir -p "$LOG_DIR"
: > "$IPROXY_LOG"
echo "[*] Starting iproxy 2222 -> 22..."
("$iproxy_bin" 2222 22 >"$IPROXY_LOG" 2>&1) &
echo "[*] Starting iproxy ${RAMDISK_SSH_PORT} -> 22 (UDID=${DEVICE_UDID})..."
("$iproxy_bin" -u "$DEVICE_UDID" "$RAMDISK_SSH_PORT" 22 >"$IPROXY_LOG" 2>&1) &
IPROXY_PID=$!
sleep 1
@@ -431,14 +530,16 @@ wait_for_ramdisk_ssh() {
done
echo "[-] Timed out waiting for ramdisk SSH readiness."
echo "[-] iproxy log tail:"
tail -n 40 "$IPROXY_LOG" 2>/dev/null || true
if [[ -n "$IPROXY_LOG" ]]; then
echo "[-] iproxy log tail:"
tail -n 40 "$IPROXY_LOG" 2>/dev/null || true
fi
echo "[-] boot_dfu log tail:"
tail -n 60 "$DFU_LOG" 2>/dev/null || true
die "Ramdisk SSH did not become ready in ${RAMDISK_SSH_TIMEOUT}s."
}
stop_iproxy_2222() {
stop_iproxy() {
if [[ -n "$IPROXY_PID" ]] && kill -0 "$IPROXY_PID" 2>/dev/null; then
echo "[*] Stopping iproxy (pid=$IPROXY_PID)..."
kill_descendants "$IPROXY_PID"
@@ -522,9 +623,10 @@ main() {
echo ""
echo "=== Restore phase ==="
start_boot_dfu
load_device_identity
wait_for_recovery
run_make "Restore" restore_get_shsh
run_make "Restore" restore
run_make "Restore" restore_get_shsh RESTORE_UDID="$DEVICE_UDID" RESTORE_ECID="0x$DEVICE_ECID"
run_make "Restore" restore RESTORE_UDID="$DEVICE_UDID" RESTORE_ECID="0x$DEVICE_ECID"
wait_for_post_restore_reboot
stop_boot_dfu
echo "[*] Waiting ${POST_KILL_SETTLE_DELAY}s for cleanup before ramdisk stage..."
@@ -533,16 +635,17 @@ main() {
echo ""
echo "=== Ramdisk + CFW phase ==="
start_boot_dfu
load_device_identity
wait_for_recovery
run_make "Ramdisk" ramdisk_build
run_make "Ramdisk" ramdisk_send
start_iproxy_2222
run_make "Ramdisk" ramdisk_send IRECOVERY_ECID="0x$DEVICE_ECID"
start_iproxy
wait_for_ramdisk_ssh
run_make "CFW install" "$cfw_install_target"
run_make "CFW install" "$cfw_install_target" SSH_PORT="$RAMDISK_SSH_PORT"
stop_boot_dfu
stop_iproxy_2222
stop_iproxy
echo ""
echo "=== First boot ==="

View File

@@ -544,7 +544,7 @@ class VPhoneControl {
let timeoutSeconds = max(Int(timeout.rounded()), 1)
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + timeout) { [weak self] in
guard let self else { return }
guard let pending = self.removePending(id: id) else { return }
guard let pending = removePending(id: id) else { return }
pending.handler(.failure(ControlError.requestTimedOut(type: type, seconds: timeoutSeconds)))
}
}

View File

@@ -195,7 +195,7 @@ class VPhoneFileBrowserModel {
transferName = nil
await refresh()
// Set error after refresh so refresh() doesn't clear it before the alert fires.
if let e = uploadError { self.error = e }
if let e = uploadError { error = e }
}
func createNewFolder(name: String) async {

View File

@@ -13,6 +13,9 @@ import Virtualization
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
///
enum VPhoneHardware {
/// Fixed CPID for the current vphone hardware descriptor.
static let udidChipID: UInt32 = 0xFE01
static func createModel() throws -> VZMacHardwareModel {
// platformVersion=3, boardID=0x90, ISA=2 matches vresearch101
let desc = Dynamic._VZMacHardwareModelDescriptor()

View File

@@ -46,7 +46,9 @@ class VPhoneLocationProvider: NSObject {
private var replayTask: Task<Void, Never>?
private var replayName: String?
var isReplaying: Bool { replayTask != nil }
var isReplaying: Bool {
replayTask != nil
}
init(control: VPhoneControl) {
self.control = control
@@ -135,7 +137,7 @@ class VPhoneLocationProvider: NSObject {
var index = 0
while !Task.isCancelled {
let point = points[index]
self.sendSimulatedLocation(
sendSimulatedLocation(
latitude: point.latitude,
longitude: point.longitude,
altitude: point.altitude,

View File

@@ -27,6 +27,12 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
var kernelDebugPort: Int = 5909
}
private struct DeviceIdentity {
let cpidHex: String
let ecidHex: String
let udid: String
}
init(options: Options) throws {
// --- Hardware model (PV=3) ---
let hwModel = try VPhoneHardware.createModel()
@@ -36,17 +42,34 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
let platform = VZMacPlatformConfiguration()
// Persist machineIdentifier for stable ECID
let machineIdentifier: VZMacMachineIdentifier
if let savedData = try? Data(contentsOf: options.machineIDURL),
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
{
platform.machineIdentifier = savedID
machineIdentifier = savedID
print("[vphone] Loaded machineIdentifier (ECID stable)")
} else {
let newID = VZMacMachineIdentifier()
platform.machineIdentifier = newID
machineIdentifier = newID
try newID.dataRepresentation.write(to: options.machineIDURL)
print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)")
}
platform.machineIdentifier = machineIdentifier
if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) {
print("[vphone] ECID: 0x\(identity.ecidHex)")
print("[vphone] Predicted UDID: \(identity.udid)")
do {
let outputURL = try Self.writeUDIDPrediction(
identity: identity, machineIDURL: options.machineIDURL
)
print("[vphone] Wrote UDID prediction: \(outputURL.path)")
} catch {
print("[vphone] Warning: failed to write udid-prediction.txt: \(error)")
}
} else {
print("[vphone] Warning: failed to resolve ECID from machineIdentifier")
}
let auxStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: options.nvramURL,
@@ -204,6 +227,39 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
}
}
private static func resolveDeviceIdentity(machineIdentifier: VZMacMachineIdentifier)
-> DeviceIdentity?
{
let ecidValue: UInt64? = if let ecid = Dynamic(machineIdentifier)._ECID.asUInt64 {
ecid
} else if let ecidNumber = Dynamic(machineIdentifier)._ECID.asObject as? NSNumber {
ecidNumber.uint64Value
} else {
nil
}
guard let ecidValue else { return nil }
let cpidHex = String(format: "%08X", VPhoneHardware.udidChipID)
let ecidHex = String(format: "%016llX", ecidValue)
let udid = "\(cpidHex)-\(ecidHex)"
return DeviceIdentity(cpidHex: cpidHex, ecidHex: ecidHex, udid: udid)
}
private static func writeUDIDPrediction(identity: DeviceIdentity, machineIDURL: URL) throws -> URL {
let outputURL = machineIDURL.deletingLastPathComponent().appendingPathComponent(
"udid-prediction.txt"
)
let content = """
UDID=\(identity.udid)
CPID=0x\(identity.cpidHex)
ECID=0x\(identity.ecidHex)
MACHINE_IDENTIFIER=\(machineIDURL.lastPathComponent)
"""
try content.write(to: outputURL, atomically: true, encoding: .utf8)
return outputURL
}
// MARK: - Battery
/// Update the synthetic battery charge and connectivity at runtime.