feat: add android-fs5 CLI toolset

- fs5 entrypoint with status, push, pull, app, shell, logs commands
- lib/adb.sh: ADB wrapper functions with idempotency and --force guards
- lib/output.sh: human + JSON output formatting via Python
- 33 bats tests (6 status, 9 transfer, 7 app, 5 shell, 6 logs) — all passing
- Zero shellcheck warnings
- .env-based config (device serial, log path)
- Spec-Driven Development artifacts in specs/001-android-fs5-management/

Device: exone GmbH FS5 (RD51QE202392, Android 9)
This commit is contained in:
Jane Alesi
2026-03-02 15:55:09 +01:00
commit e6e5f34455
15 changed files with 1713 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# lib/adb.sh — ADB wrapper functions for android-fs5
# shellcheck shell=bash
# adb_check_device — validate device is connected; exit 2 if not
adb_check_device() {
local state
state=$(adb -s "${DEVICE_SERIAL}" get-state 2>/dev/null || echo "offline")
if [[ "${state}" != "device" ]]; then
out_error "Device not found: ${DEVICE_SERIAL}"
exit 2
fi
}
# adb_cmd ARGS... — run adb with the configured device serial
adb_cmd() {
adb -s "${DEVICE_SERIAL}" "$@"
}
# adb_get_prop KEY — read a device system property
adb_get_prop() {
local key="${1}"
adb_cmd shell getprop "${key}" 2>/dev/null | tr -d '\r'
}
# adb_shell CMD — execute a shell command on the device
adb_shell() {
adb_cmd shell "$@" 2>&1
}
# adb_file_exists DEVICE_PATH — return 0 if file exists on device, 1 otherwise
adb_file_exists() {
local path="${1}"
local result
result=$(adb_cmd shell "[ -e '${path}' ] && echo yes || echo no" 2>/dev/null | tr -d '\r')
[[ "${result}" == "yes" ]]
}
# adb_push SRC DST [--force] — push local file to device
# Skips if destination exists unless --force is set
adb_push() {
local src="${1}"
local dst="${2}"
local force="${3:-}"
if [[ ! -e "${src}" ]]; then
out_error "Source not found: ${src}"
return 1
fi
if adb_file_exists "${dst}" && [[ "${force}" != "--force" ]]; then
out_warn "File exists on device, use --force to overwrite: ${dst}"
return 0
fi
adb_cmd push "${src}" "${dst}"
local exit_code=$?
if [[ ${exit_code} -eq 0 ]]; then
adb_log_op "PUSH" "${src} -> ${dst}"
out_success "Pushed: ${src}${dst}"
else
out_error "Push failed: ${src}${dst}"
return 1
fi
}
# adb_pull SRC DST [--force] — pull file from device to local
# Skips if local destination exists unless --force is set
adb_pull() {
local src="${1}"
local dst="${2}"
local force="${3:-}"
if ! adb_file_exists "${src}"; then
out_error "Source not found on device: ${src}"
return 1
fi
if [[ -e "${dst}" ]] && [[ "${force}" != "--force" ]]; then
out_warn "Local file exists, use --force to overwrite: ${dst}"
return 0
fi
adb_cmd pull "${src}" "${dst}"
local exit_code=$?
if [[ ${exit_code} -eq 0 ]]; then
adb_log_op "PULL" "${src} -> ${dst}"
out_success "Pulled: ${src}${dst}"
else
out_error "Pull failed: ${src}${dst}"
return 1
fi
}
# adb_log_op OPERATION DETAIL — append timestamped entry to LOG_FILE
adb_log_op() {
local op="${1}"
local detail="${2}"
local ts
ts=$(date -Iseconds)
echo "${ts} ${op} ${detail} [OK]" >> "${LOG_FILE:-./fs5.log}"
}
# adb_battery_level — return battery percentage as integer
adb_battery_level() {
adb_cmd shell dumpsys battery 2>/dev/null \
| grep -E '^\s+level:' \
| awk '{print $2}' \
| tr -d '\r'
}
# adb_battery_charging — return true/false string
adb_battery_charging() {
local status
status=$(adb_cmd shell dumpsys battery 2>/dev/null \
| grep -E '^\s+status:' \
| awk '{print $2}' \
| tr -d '\r')
# status 2 = charging
if [[ "${status}" == "2" ]]; then
echo "true"
else
echo "false"
fi
}
# adb_storage_info — echo space-separated: total_kb used_kb free_kb percent_used
adb_storage_info() {
adb_cmd shell df /sdcard 2>/dev/null \
| tail -1 \
| awk '{print $2, $3, $4, $5}' \
| tr -d '\r%'
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# lib/output.sh — Human and JSON output formatting for android-fs5
# shellcheck shell=bash
# ANSI colors (disabled when not a terminal)
if [[ -t 1 ]]; then
_RED='\033[0;31m'
_GREEN='\033[0;32m'
_YELLOW='\033[1;33m'
_CYAN='\033[0;36m'
_RESET='\033[0m'
else
_RED='' _GREEN='' _YELLOW='' _CYAN='' _RESET=''
fi
# out_human MSG — print informational message to stdout
out_human() {
echo -e "${_CYAN}${1}${_RESET}"
}
# out_success MSG — print success message with checkmark
out_success() {
echo -e "${_GREEN}${1}${_RESET}"
}
# out_warn MSG — print warning to stdout
out_warn() {
echo -e "${_YELLOW}${1}${_RESET}"
}
# out_error MSG — print error message to stderr
out_error() {
echo -e "${_RED}${1}${_RESET}" >&2
}
# out_json KEY=VALUE ... — emit a JSON object from key=value pairs
# Usage: out_json serial="RD51QE202392" model="FS5" battery_level=85
out_json() {
local -a pairs=("$@")
python3 - "${pairs[@]}" <<'PYEOF'
import sys, json
data = {}
for arg in sys.argv[1:]:
if '=' in arg:
key, _, val = arg.partition('=')
# Attempt numeric conversion
try:
if '.' in val:
data[key] = float(val)
else:
data[key] = int(val)
except ValueError:
# Boolean strings
if val.lower() == 'true':
data[key] = True
elif val.lower() == 'false':
data[key] = False
else:
data[key] = val
print(json.dumps(data, indent=2))
PYEOF
}