Files
android-fs5/fs5
T
Jane Alesi aaf008c1bf feat(remote-control): add screen/screenshot/tap/swipe/type/key/launch/ocr/sms commands
- lib/input.sh: ADB input automation library (screenshot, tap, swipe, type, key, launch, OCR, SMS/OTP via KDE Connect)
- fs5: 9 new remote control commands for AI agent capabilities
- tests/test_remote_control.bats: 31 tests, all passing
- specs/002-remote-control/spec.md: feature specification

Tools: scrcpy 3.3.4 (GUI mirror), tesseract-ocr (OCR), KDE Connect (SMS/OTP)
Constraints: no root required, Android 9 (SDK 28)

Tests: 64/64 passing (33 device mgmt + 31 remote control)
2026-03-02 16:10:11 +01:00

862 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
# fs5 — Android FS5 device management CLI
# Device: exone GmbH FS5 (Android 9)
# Usage: ./fs5 <command> [options]
set -uo pipefail
# Note: -e intentionally omitted; we handle errors explicitly for correct exit codes
# ── Resolve script directory ──────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Load config from .env (environment variables take precedence) ─────────────
ENV_FILE="${SCRIPT_DIR}/.env"
if [[ -f "${ENV_FILE}" ]]; then
# Load only variables not already set in the environment
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "${key}" =~ ^[[:space:]]*# ]] && continue
[[ -z "${key}" ]] && continue
key="${key// /}"
# Only set if not already exported in environment
if [[ -z "${!key+x}" ]]; then
export "${key}=${value}"
fi
done < "${ENV_FILE}"
fi
# Defaults (fallback if neither env nor .env provided)
DEVICE_SERIAL="${DEVICE_SERIAL:-RD51QE202392}"
LOG_FILE="${LOG_FILE:-${SCRIPT_DIR}/fs5.log}"
ADB_TIMEOUT="${ADB_TIMEOUT:-10}"
# ── Load libraries ────────────────────────────────────────────────────────────
# shellcheck source=lib/output.sh
source "${SCRIPT_DIR}/lib/output.sh"
# shellcheck source=lib/adb.sh
source "${SCRIPT_DIR}/lib/adb.sh"
# shellcheck source=lib/input.sh
source "${SCRIPT_DIR}/lib/input.sh"
# ── Global flags ──────────────────────────────────────────────────────────────
JSON_OUTPUT=false
FORCE=false
# ── Usage ─────────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: fs5 <command> [options]
Device Management:
status Show device health and info
push <src> <dst> Push file/dir to device
pull <src> <dst> Pull file/dir from device
app list List installed apps
app install <apk> Install APK on device
app uninstall <pkg> Uninstall package from device
shell [cmd] Execute shell command on device
logs Stream or capture logcat output
Remote Control (AI capabilities):
screen Launch scrcpy GUI mirror with input
screenshot [path] Capture screen to PNG file
tap <x> <y> Send tap at coordinates
swipe <x1> <y1> <x2> <y2> [ms] Send swipe gesture
type <text> Type text into focused field
key <keycode> Send Android key event
launch <package> Launch app by package name
ocr Screenshot + extract all text via OCR
sms list List recent SMS messages
sms otp Extract latest OTP code from SMS
sms watch Watch for new SMS messages
Global Options:
--json Output as JSON
--force Allow destructive/overwrite operations
--help Show this help
Exit Codes:
0 Success
1 General error
2 Device not found
Device: ${DEVICE_SERIAL}
EOF
}
# ── Parse global flags (modifies globals, returns remaining args via array) ───
# Usage: parse_global_flags REMAINING_ARRAY_NAME "$@"
parse_global_flags() {
local -n _remaining_ref="${1}"
shift
_remaining_ref=()
for arg in "$@"; do
case "${arg}" in
--json) JSON_OUTPUT=true ;;
--force) FORCE=true ;;
--help|-h) usage; exit 0 ;;
*) _remaining_ref+=("${arg}") ;;
esac
done
}
# ── Command: status ───────────────────────────────────────────────────────────
cmd_status() {
local show_help=false
for arg in "$@"; do
case "${arg}" in
--json) JSON_OUTPUT=true ;;
--help) show_help=true ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 status [--json]
Show device health information including model, Android version,
battery level, storage usage, and ADB connection state.
EOF
return 0
fi
adb_check_device
local model android_version battery_level battery_charging
local storage_total storage_used storage_free storage_pct adb_state ts
model=$(adb_get_prop "ro.product.model")
android_version=$(adb_get_prop "ro.build.version.release")
battery_level=$(adb_battery_level)
battery_charging=$(adb_battery_charging)
adb_state=$(adb -s "${DEVICE_SERIAL}" get-state 2>/dev/null || echo "unknown")
ts=$(date -Iseconds)
# Storage: total used free pct
read -r storage_total storage_used storage_free storage_pct \
<<< "$(adb_storage_info)"
if [[ "${JSON_OUTPUT}" == "true" ]]; then
out_json \
"serial=${DEVICE_SERIAL}" \
"model=${model}" \
"manufacturer=exone GmbH" \
"android_version=${android_version}" \
"battery_level=${battery_level}" \
"battery_charging=${battery_charging}" \
"storage_total_kb=${storage_total}" \
"storage_used_kb=${storage_used}" \
"storage_free_kb=${storage_free}" \
"storage_percent_used=${storage_pct}" \
"adb_state=${adb_state}" \
"timestamp=${ts}"
else
echo "Device: exone GmbH ${model}"
echo "Serial: ${DEVICE_SERIAL}"
echo "Android: ${android_version}"
echo "ADB State: ${adb_state}"
echo "Battery: ${battery_level}% (charging: ${battery_charging})"
echo "Storage: ${storage_used}K used / ${storage_total}K total (${storage_pct}%)"
echo "Timestamp: ${ts}"
fi
}
# ── Command: push ─────────────────────────────────────────────────────────────
cmd_push() {
local src="" dst="" show_help=false force_flag=""
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
--force) force_flag="--force" ;;
*)
if [[ -z "${src}" ]]; then src="${arg}"
elif [[ -z "${dst}" ]]; then dst="${arg}"
fi
;;
esac
done
if [[ "${FORCE}" == "true" ]]; then force_flag="--force"; fi
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 push <local-path> <device-path> [--force]
Push a local file or directory to the device.
Use --force to overwrite existing files on the device.
EOF
return 0
fi
if [[ -z "${src}" ]] || [[ -z "${dst}" ]]; then
out_error "Usage: fs5 push <src> <dst>"
return 1
fi
adb_check_device
adb_push "${src}" "${dst}" "${force_flag}"
}
# ── Command: pull ─────────────────────────────────────────────────────────────
cmd_pull() {
local src="" dst="" show_help=false force_flag=""
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
--force) force_flag="--force" ;;
*)
if [[ -z "${src}" ]]; then src="${arg}"
elif [[ -z "${dst}" ]]; then dst="${arg}"
fi
;;
esac
done
if [[ "${FORCE}" == "true" ]]; then force_flag="--force"; fi
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 pull <device-path> <local-path> [--force]
Pull a file or directory from the device to local filesystem.
Use --force to overwrite existing local files.
EOF
return 0
fi
if [[ -z "${src}" ]] || [[ -z "${dst}" ]]; then
out_error "Usage: fs5 pull <src> <dst>"
return 1
fi
adb_check_device
adb_pull "${src}" "${dst}" "${force_flag}"
}
# ── Command: app ──────────────────────────────────────────────────────────────
cmd_app() {
local subcmd="${1:-}"
shift || true
case "${subcmd}" in
list) cmd_app_list "$@" ;;
install) cmd_app_install "$@" ;;
uninstall) cmd_app_uninstall "$@" ;;
--help|-h)
cat <<EOF
Usage: fs5 app <subcommand> [options]
Subcommands:
list [--all] [--json] List installed apps
install <apk> Install APK
uninstall <package> --force Uninstall package
EOF
;;
*)
out_error "Unknown app subcommand: '${subcmd}'. Use: list, install, uninstall"
return 1
;;
esac
}
cmd_app_list() {
local show_all=false show_help=false
for arg in "$@"; do
case "${arg}" in
--all) show_all=true ;;
--help) show_help=true ;;
--json) JSON_OUTPUT=true ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 app list [--all] [--json]"
return 0
fi
adb_check_device
local pm_args="-3"
[[ "${show_all}" == "true" ]] && pm_args=""
local raw_packages
# shellcheck disable=SC2086
raw_packages=$(adb_cmd shell pm list packages ${pm_args} 2>/dev/null | tr -d '\r')
if [[ "${JSON_OUTPUT}" == "true" ]]; then
echo "${raw_packages}" | python3 -c "
import sys, json
packages = []
for line in sys.stdin:
line = line.strip()
if line.startswith('package:'):
packages.append({'package': line[8:]})
print(json.dumps(packages, indent=2))
"
else
echo "${raw_packages}"
fi
}
cmd_app_install() {
local apk="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) [[ -z "${apk}" ]] && apk="${arg}" ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 app install <apk-path>"
return 0
fi
if [[ -z "${apk}" ]]; then
out_error "Usage: fs5 app install <apk-path>"
return 1
fi
if [[ ! -f "${apk}" ]]; then
out_error "APK not found: ${apk}"
return 1
fi
adb_check_device
local result
result=$(adb_cmd install "${apk}" 2>&1)
if echo "${result}" | grep -q "Success"; then
adb_log_op "INSTALL" "${apk}"
out_success "Installed: ${apk}"
else
out_error "Install failed: ${result}"
return 1
fi
}
cmd_app_uninstall() {
local pkg="" show_help=false force_flag=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
--force) force_flag=true ;;
*) [[ -z "${pkg}" ]] && pkg="${arg}" ;;
esac
done
if [[ "${FORCE}" == "true" ]]; then force_flag=true; fi
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 app uninstall <package> --force"
return 0
fi
if [[ -z "${pkg}" ]]; then
out_error "Usage: fs5 app uninstall <package> --force"
return 1
fi
if [[ "${force_flag}" != "true" ]]; then
out_error "Uninstall requires --force flag to prevent accidental removal"
return 1
fi
adb_check_device
local result
result=$(adb_cmd shell pm uninstall "${pkg}" 2>&1 | tr -d '\r')
if [[ "${result}" == "Success" ]]; then
adb_log_op "UNINSTALL" "${pkg}"
out_success "Uninstalled: ${pkg}"
else
out_error "Uninstall failed (package may not be installed): ${pkg}"
return 1
fi
}
# ── Command: shell ────────────────────────────────────────────────────────────
cmd_shell() {
local show_help=false
local -a cmd_args=()
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) cmd_args+=("${arg}") ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 shell [command]
Execute a shell command on the device. If no command is given,
opens an interactive shell session.
EOF
return 0
fi
adb_check_device
if [[ ${#cmd_args[@]} -eq 0 ]]; then
adb_cmd shell
else
adb_cmd shell "${cmd_args[@]}"
fi
}
# ── Command: logs ─────────────────────────────────────────────────────────────
cmd_logs() {
local lines="" filter="" output_file="" show_help=false
local i=0
local -a args=("$@")
while [[ ${i} -lt ${#args[@]} ]]; do
case "${args[${i}]}" in
--help) show_help=true ;;
--lines) i=$(( i + 1 )); lines="${args[${i}]}" ;;
--filter) i=$(( i + 1 )); filter="${args[${i}]}" ;;
--output) i=$(( i + 1 )); output_file="${args[${i}]}" ;;
esac
i=$(( i + 1 ))
done
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 logs [--lines N] [--filter TAG] [--output FILE]
Stream or capture Android logcat output.
Options:
--lines N Print last N lines and exit (non-streaming)
--filter TAG Filter output by log tag
--output FILE Write output to file instead of stdout
EOF
return 0
fi
adb_check_device
local -a logcat_args=()
if [[ -n "${lines}" ]]; then
logcat_args+=("-d" "-t" "${lines}")
fi
if [[ -n "${filter}" ]]; then
logcat_args+=("${filter}:V" "*:S")
fi
if [[ -n "${output_file}" ]]; then
adb_cmd logcat "${logcat_args[@]+"${logcat_args[@]}"}" > "${output_file}"
out_success "Logs written to: ${output_file}"
else
adb_cmd logcat "${logcat_args[@]+"${logcat_args[@]}"}"
fi
}
# ── Command: screen ──────────────────────────────────────────────────────────
cmd_screen() {
local headless=false show_help=false
for arg in "$@"; do
case "${arg}" in
--headless) headless=true ;;
--help) show_help=true ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 screen [--headless]
Launch scrcpy to mirror and control the device screen.
--headless: run without display window (for automation/OCR only).
EOF
return 0
fi
adb_check_device
if [[ "${headless}" == "true" ]]; then
scrcpy -s "${DEVICE_SERIAL}" --no-display &
out_success "scrcpy running headless (PID $!)"
else
scrcpy -s "${DEVICE_SERIAL}"
fi
}
# ── Command: screenshot ───────────────────────────────────────────────────────
cmd_screenshot() {
local output="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) [[ -z "${output}" ]] && output="${arg}" ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 screenshot [output-path.png]"
return 0
fi
adb_check_device
local path
if [[ -n "${output}" ]]; then
path=$(adb_screenshot "${output}")
else
path=$(adb_screenshot)
fi
if [[ "${JSON_OUTPUT}" == "true" ]]; then
out_json "path=${path}" "timestamp=$(date -Iseconds)"
else
out_success "Screenshot saved: ${path}"
echo "${path}"
fi
}
# ── Command: tap ─────────────────────────────────────────────────────────────
cmd_tap() {
local x="" y="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*)
if [[ -z "${x}" ]]; then x="${arg}"
elif [[ -z "${y}" ]]; then y="${arg}"
fi
;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 tap <x> <y>"
return 0
fi
if [[ -z "${x}" ]] || [[ -z "${y}" ]]; then
out_error "Usage: fs5 tap <x> <y>"
return 1
fi
adb_check_device
adb_tap "${x}" "${y}"
out_success "Tapped: (${x}, ${y})"
}
# ── Command: swipe ────────────────────────────────────────────────────────────
cmd_swipe() {
local x1="" y1="" x2="" y2="" duration="300" show_help=false
local -a pos=()
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) pos+=("${arg}") ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 swipe <x1> <y1> <x2> <y2> [duration_ms]"
return 0
fi
x1="${pos[0]:-}" y1="${pos[1]:-}" x2="${pos[2]:-}" y2="${pos[3]:-}"
[[ ${#pos[@]} -ge 5 ]] && duration="${pos[4]}"
if [[ -z "${x1}" ]] || [[ -z "${y1}" ]] || [[ -z "${x2}" ]] || [[ -z "${y2}" ]]; then
out_error "Usage: fs5 swipe <x1> <y1> <x2> <y2> [duration_ms]"
return 1
fi
adb_check_device
adb_swipe "${x1}" "${y1}" "${x2}" "${y2}" "${duration}"
out_success "Swiped: (${x1},${y1}) → (${x2},${y2}) in ${duration}ms"
}
# ── Command: type ─────────────────────────────────────────────────────────────
cmd_type() {
local text="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) text="${text}${text:+ }${arg}" ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 type <text>"
return 0
fi
if [[ -z "${text}" ]]; then
out_error "Usage: fs5 type <text>"
return 1
fi
adb_check_device
adb_type "${text}"
out_success "Typed: ${text}"
}
# ── Command: key ──────────────────────────────────────────────────────────────
cmd_key() {
local keycode="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) [[ -z "${keycode}" ]] && keycode="${arg}" ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
cat <<EOF
Usage: fs5 key <keycode>
Send Android key event. Common keycodes:
KEYCODE_HOME KEYCODE_BACK KEYCODE_POWER
KEYCODE_ENTER KEYCODE_TAB KEYCODE_DEL
3 (HOME) 4 (BACK) 26 (POWER)
EOF
return 0
fi
if [[ -z "${keycode}" ]]; then
out_error "Usage: fs5 key <keycode>"
return 1
fi
adb_check_device
adb_key "${keycode}"
out_success "Key sent: ${keycode}"
}
# ── Command: launch ───────────────────────────────────────────────────────────
cmd_launch() {
local pkg="" show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
*) [[ -z "${pkg}" ]] && pkg="${arg}" ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 launch <package-name>"
return 0
fi
if [[ -z "${pkg}" ]]; then
out_error "Usage: fs5 launch <package-name>"
return 1
fi
adb_check_device
if adb_launch "${pkg}"; then
out_success "Launched: ${pkg}"
else
return 1
fi
}
# ── Command: ocr ──────────────────────────────────────────────────────────────
cmd_ocr() {
local show_help=false
for arg in "$@"; do
[[ "${arg}" == "--help" ]] && show_help=true
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 ocr"
echo "Take a screenshot and extract all visible text via OCR (tesseract)."
return 0
fi
adb_check_device
local text
text=$(adb_ocr)
if [[ "${JSON_OUTPUT}" == "true" ]]; then
python3 -c "import json,sys; print(json.dumps({'text': sys.stdin.read()}))" <<< "${text}"
else
echo "${text}"
fi
}
# ── Command: sms ──────────────────────────────────────────────────────────────
cmd_sms() {
local subcmd="${1:-}"
shift || true
case "${subcmd}" in
list) cmd_sms_list "$@" ;;
otp) cmd_sms_otp "$@" ;;
watch) cmd_sms_watch "$@" ;;
--help|-h)
cat <<EOF
Usage: fs5 sms <subcommand>
Subcommands:
list [--limit N] [--json] List recent SMS messages
otp [--wait] Extract latest OTP code from SMS
watch Watch for new SMS messages (Ctrl+C to stop)
EOF
;;
*)
out_error "Unknown sms subcommand: '${subcmd}'. Use: list, otp, watch"
return 1
;;
esac
}
cmd_sms_list() {
local limit=10 show_help=false
local i=0
local -a args=("$@")
while [[ ${i} -lt ${#args[@]} ]]; do
case "${args[${i}]}" in
--help) show_help=true ;;
--json) JSON_OUTPUT=true ;;
--limit) i=$(( i + 1 )); limit="${args[${i}]}" ;;
esac
i=$(( i + 1 ))
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 sms list [--limit N] [--json]"
return 0
fi
adb_check_device
adb_sms_list "${limit}"
}
cmd_sms_otp() {
local wait_secs=0 show_help=false
for arg in "$@"; do
case "${arg}" in
--help) show_help=true ;;
--wait) wait_secs=60 ;;
esac
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 sms otp [--wait]"
echo "Extract latest OTP/2FA code from SMS inbox."
echo "--wait: poll up to 60 seconds for a new OTP."
return 0
fi
adb_check_device
local otp
otp=$(adb_sms_otp "${wait_secs}")
local exit_code=$?
if [[ ${exit_code} -eq 0 ]]; then
if [[ "${JSON_OUTPUT}" == "true" ]]; then
out_json "otp=${otp}" "timestamp=$(date -Iseconds)"
else
echo "${otp}"
fi
fi
return ${exit_code}
}
cmd_sms_watch() {
local show_help=false
for arg in "$@"; do
[[ "${arg}" == "--help" ]] && show_help=true
done
if [[ "${show_help}" == "true" ]]; then
echo "Usage: fs5 sms watch"
echo "Poll for new SMS messages every 5 seconds. Press Ctrl+C to stop."
return 0
fi
adb_check_device
out_success "Watching for SMS (Ctrl+C to stop)..."
local last_id=""
last_id=$(adb_cmd shell content query \
--uri content://sms/inbox \
--projection "_id" \
--sort "date DESC" \
2>/dev/null | grep "_id=" | head -1 | grep -oE '_id=[0-9]+' | cut -d= -f2 | tr -d '\r')
while true; do
local raw new_id
raw=$(adb_cmd shell content query \
--uri content://sms/inbox \
--projection "_id,address,date,body" \
--sort "date DESC" \
2>/dev/null | tr -d '\r' | head -10)
new_id=$(echo "${raw}" | grep "_id=" | head -1 | grep -oE '_id=[0-9]+' | cut -d= -f2)
if [[ -n "${new_id}" ]] && [[ "${new_id}" != "${last_id}" ]]; then
local ts body address
ts=$(date -Iseconds)
address=$(echo "${raw}" | grep "address=" | head -1 | grep -oE 'address=[^,]+' | cut -d= -f2)
body=$(echo "${raw}" | grep "body=" | head -1 | sed 's/.*body=//')
echo "[${ts}] FROM: ${address}"
echo " ${body}"
echo ""
last_id="${new_id}"
fi
sleep 5
done
}
# ── Main dispatcher ───────────────────────────────────────────────────────────
main() {
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
# Extract command (first arg)
local command="${1}"
shift
# Handle global --help before command dispatch
if [[ "${command}" == "--help" ]] || [[ "${command}" == "-h" ]]; then
usage
exit 0
fi
# Parse remaining args for global flags; remaining_args gets non-flag args
local -a remaining_args=()
parse_global_flags remaining_args "$@"
case "${command}" in
status) cmd_status "${remaining_args[@]+"${remaining_args[@]}"}" ;;
push) cmd_push "${remaining_args[@]+"${remaining_args[@]}"}" ;;
pull) cmd_pull "${remaining_args[@]+"${remaining_args[@]}"}" ;;
app) cmd_app "${remaining_args[@]+"${remaining_args[@]}"}" ;;
shell) cmd_shell "${remaining_args[@]+"${remaining_args[@]}"}" ;;
logs) cmd_logs "${remaining_args[@]+"${remaining_args[@]}"}" ;;
screen) cmd_screen "${remaining_args[@]+"${remaining_args[@]}"}" ;;
screenshot) cmd_screenshot "${remaining_args[@]+"${remaining_args[@]}"}" ;;
tap) cmd_tap "${remaining_args[@]+"${remaining_args[@]}"}" ;;
swipe) cmd_swipe "${remaining_args[@]+"${remaining_args[@]}"}" ;;
type) cmd_type "${remaining_args[@]+"${remaining_args[@]}"}" ;;
key) cmd_key "${remaining_args[@]+"${remaining_args[@]}"}" ;;
launch) cmd_launch "${remaining_args[@]+"${remaining_args[@]}"}" ;;
ocr) cmd_ocr "${remaining_args[@]+"${remaining_args[@]}"}" ;;
sms) cmd_sms "${remaining_args[@]+"${remaining_args[@]}"}" ;;
*)
out_error "Unknown command: ${command}"
usage
exit 1
;;
esac
}
main "$@"