Skip to content

๐Ÿ›  Building

This project uses a Taskfile.yml for common development tasks. After installing Task, you can run the following commands.

Build the project:

task build
pio run

Upload the firmware:

task upload
pio run --target upload

Rebooting the Device

After uploading the firmware, you may need to unplug the dongle and plug it back in to reboot it and apply the changes.

Monitor the serial output:

task monitor
pio device monitor

Clean build files:

task clean
pio run --target clean

List all available tasks:

task -l

๐Ÿงช Testing the API

The test-api.sh script automates testing the device's web API functionality by performing various requests and verifying the responses.

๐Ÿ“ฆ Dependencies

You must have the following dependencies installed on your system:

  • curl: For interacting with web services.
  • jq: A lightweight and flexible command-line JSON processor.
sudo apt install curl jq
brew install curl jq

โš™ Configuration

The script requires an .env file with the device's IP address.

  1. Copy the template .env File:

    cp scripts/.env.tmpl scripts/.env
    
  2. Edit scripts/.env: Update the FTP_HOST variable with your device's IP address.

scripts/.env

FTP_HOST="192.168.2.169"
FTP_USER="user"
FTP_PASSWORD="password"
WEB_SERVER_USER=""
WEB_SERVER_PASSWORD=""
LOCAL_DIR="data"
REMOTE_DIR="/"

๐Ÿ“ Script Usage

  1. Ensure the device is online and accessible at the configured IP address.
  2. Run the script from the scripts directory:
task test-api
cd scripts
./test-api.sh
test-api.sh
#!/usr/bin/env bash
################################################################################
#
# test-api.sh
# ----------------
# Tests the FrameFi device API by performing a GET request and verifying the JSON response.
#
# @author Nicholas Wilde, 0xb299a622
# @date 16 Aug 2025
# @version 0.1.0
#
################################################################################

# Options
set -e
set -o pipefail

# These are constants
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)
readonly RED GREEN YELLOW BLUE RESET

DELAY_SECONDS=1
readonly DELAY_SECONDS

# Log function for standardized output
function log() {
  local TYPE="$1"
  local MESSAGE="$2"
  local COLOR=""
  local EMOJI=""

  case "$TYPE" in
    "INFO") COLOR="${BLUE}"; EMOJI="";;
    "WARN") COLOR="${YELLOW}"; EMOJI="โš ๏ธ ";;
    "ERRO") COLOR="${RED}"; EMOJI="โŒ ";;
    "SUCCESS") COLOR="${BLUE}"; EMOJI="โœ… "; TYPE="INFO";;
    *) COLOR="${RESET}";;
  esac

  echo "${COLOR}${TYPE}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${EMOJI}${MESSAGE}"
}

# Check for dependencies
function check_dependencies() {
  log "INFO" "Checking dependencies..."
  if ! command -v curl &> /dev/null; then
    log "ERRO" "curl could not be found. Please install it."
    exit 1
  fi
  if ! command -v jq &> /dev/null; then
    log "ERRO" "jq could not be found. Please install it."
    exit 1
  fi
  log "SUCCESS" "Dependencies checked."
}

function load_vars() {
  local ENV_FILE="$(dirname "$0")/.env"

  if [ ! -f "${ENV_FILE}" ]; then
    log "ERRO" "Environment file not found: ${ENV_FILE}"
    log "ERRO" "Please create it from .env.tmpl and ensure FTP_HOST is set."
    exit 1
  fi

  source "${ENV_FILE}"

  if [ -z "${FTP_HOST}" ]; then
    log "ERRO" "FTP_HOST not set in ${ENV_FILE}"
    exit 1
  fi
}

# Check if the device is online
function check_device_status() {
  log "INFO" "Checking if device at ${FTP_HOST} is online..."
  if ! curl -s -o /dev/null --fail --connect-timeout 5 "http://${FTP_HOST}/"; then
    log "ERRO" "Device at ${FTP_HOST} is not responding."
    log "ERRO" "Please ensure the device is connected to the network and the IP address is correct."
    exit 1
  fi
  log "SUCCESS" "Device is online."
}

# Check initial device mode
function check_initial_mode() {
  log "INFO" "Verifying initial device mode is USB MSC..."
  local RESPONSE
  RESPONSE=$(curl -s "http://${FTP_HOST}/")
  local CURRENT_MODE
  CURRENT_MODE=$(echo "${RESPONSE}" | jq -r '.mode')

  if [ "${CURRENT_MODE}" != "USB MSC" ]; then
    log "ERRO" "Device is not in USB MSC mode. Current mode: ${CURRENT_MODE}"
    log "ERRO" "Please set the device to USB MSC mode before running tests."
    exit 1
  fi
  log "SUCCESS" "Device is in USB MSC mode."
}

# Function to perform an API request and validate the JSON response
function request_and_verify() {
  local METHOD="$1"
  local ENDPOINT="$2"
  local PAYLOAD="$3"
  local EXPECTED_FIELDS="$4"
  local API_URL="http://${FTP_HOST}${ENDPOINT}"

  log "INFO" "Testing ${METHOD} ${API_URL}"

  local RESPONSE
  if [ "${METHOD}" == "POST" ]; then
    if [ -n "${PAYLOAD}" ]; then
      RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" -d "${PAYLOAD}" "${API_URL}")
    else
      RESPONSE=$(curl -s -X POST "${API_URL}")
    fi
  else # GET
    RESPONSE=$(curl -s "${API_URL}")
  fi

  if ! echo "${RESPONSE}" | jq -e . > /dev/null; then
    log "ERRO" "Response from ${ENDPOINT} is not valid JSON."
    log "ERRO" "Response: ${RESPONSE}"
    exit 1
  fi

  if [ "${METHOD}" == "POST" ]; then
    local STATUS
    STATUS=$(echo "${RESPONSE}" | jq -r '.status')
    if [ "${STATUS}" != "success" ]; then
      log "ERRO" "${METHOD} ${ENDPOINT} failed. Status: ${STATUS}, Message: $(echo "${RESPONSE}" | jq -r '.message')"
      exit 1
    fi
    log "INFO" "${METHOD} ${ENDPOINT} successful. Message: $(echo "${RESPONSE}" | jq -r '.message')"
  elif [ "${METHOD}" == "GET" ]; then
    for field in ${EXPECTED_FIELDS}; do
      local VALUE
      VALUE=$(echo "${RESPONSE}" | jq -r "${field}")
      if [ "${VALUE}" == "null" ]; then
        log "ERRO" "JSON response from ${ENDPOINT} missing '${field}' field or it's null."
        exit 1
      fi
      log "INFO" "${ENDPOINT} - ${field}: ${VALUE}"
    done
    log "SUCCESS" "${ENDPOINT} test completed successfully."
  fi
}

function verify_display_toggle() {
  log "INFO" "Verifying display APIs..."

  # Test display off
  log "INFO" "Testing display off..."
  request_and_verify "POST" "/display/off" "" ""
  sleep ${DELAY_SECONDS}
  local STATUS
  STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "OFF" ]; then
    log "ERRO" "Display failed to turn off. Current status: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "Display is OFF."

  # Test display off again (already off)
  log "INFO" "Testing display off again (already off)..."
  local RESPONSE
  RESPONSE=$(curl -s -X POST "http://${FTP_HOST}/display/off")
  local STATUS=$(echo "${RESPONSE}" | jq -r '.status')
  local MESSAGE=$(echo "${RESPONSE}" | jq -r '.message')
  if [ "${STATUS}" != "success" ] || [ "${MESSAGE}" != "Display turned off." ]; then
    log "ERRO" "Display off again failed. Status: ${STATUS}, Message: ${MESSAGE}"
    exit 1
  fi
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "OFF" ]; then
    log "ERRO" "Display state changed unexpectedly after turning off again. Current status: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "Display is still OFF and message is correct."

  # Test display on
  log "INFO" "Testing display on..."
  request_and_verify "POST" "/display/on" "" ""
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "ON" ]; then
    log "ERRO" "Display failed to turn on. Current status: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "Display is ON."

  # Test display on again (already on)
  log "INFO" "Testing display on again (already on)..."
  local RESPONSE
  RESPONSE=$(curl -s -X POST "http://${FTP_HOST}/display/on")
  local STATUS=$(echo "${RESPONSE}" | jq -r '.status')
  local MESSAGE=$(echo "${RESPONSE}" | jq -r '.message')
  if [ "${STATUS}" != "success" ] || [ "${MESSAGE}" != "Display turned on." ]; then
    log "ERRO" "Display on again failed. Status: ${STATUS}, Message: ${MESSAGE}"
    exit 1
  fi
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "ON" ]; then
    log "ERRO" "Display state changed unexpectedly after turning on again. Current status: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "Display is still ON and message is correct."

  # Test display toggle
  log "INFO" "Verifying display toggle..."
  local INITIAL_STATUS
  INITIAL_STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status')
  log "INFO" "Initial display status: ${INITIAL_STATUS}"

  request_and_verify "POST" "/display/toggle" "" ""
  sleep ${DELAY_SECONDS}

  local NEW_STATUS
  NEW_STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status')
  log "INFO" "New display status: ${NEW_STATUS}"

  if [ "${INITIAL_STATUS}" == "${NEW_STATUS}" ]; then
    log "ERRO" "Display status did not change after toggle."
    exit 1
  fi
  log "SUCCESS" "Display toggle verified successfully."

  # Toggle display back on
  log "INFO" "Toggling display back on..."
  request_and_verify "POST" "/display/toggle" "" ""
  sleep ${DELAY_SECONDS}

  NEW_STATUS=$(curl -s "http://${FTP_HOST}/display/status" | jq -r '.display_status' | tr '[:lower:]' '[:upper:]')
  if [ "${NEW_STATUS}" != "ON" ]; then
    log "ERRO" "Display failed to turn back on. Current status: ${NEW_STATUS}"
    exit 1
  fi
  log "SUCCESS" "Display is ON again."
}

function verify_led_toggle() {
  log "INFO" "Verifying LED APIs..."

  # Test LED off
  log "INFO" "Testing LED off..."
  request_and_verify "POST" "/led/off" "" ""
  sleep ${DELAY_SECONDS}
  local STATUS
  STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "OFF" ]; then
    log "ERRO" "LED failed to turn off. Current state: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "LED is OFF."

  # Test LED off again (already off)
  log "INFO" "Testing LED off again (already off)..."
  local RESPONSE
  RESPONSE=$(curl -s -X POST "http://${FTP_HOST}/led/off")
  local STATUS=$(echo "${RESPONSE}" | jq -r '.status')
  local MESSAGE=$(echo "${RESPONSE}" | jq -r '.message')
  if [ "${STATUS}" != "success" ] || [ "${MESSAGE}" != "LED turned off." ]; then
    log "ERRO" "LED off again failed. Status: ${STATUS}, Message: ${MESSAGE}"
    exit 1
  fi
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "OFF" ]; then
    log "ERRO" "LED state changed unexpectedly after turning off again. Current state: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "LED is still OFF and message is correct."

  # Test LED on
  log "INFO" "Testing LED on..."
  request_and_verify "POST" "/led/on" "" ""
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "ON" ]; then
    log "ERRO" "LED failed to turn on. Current state: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "LED is ON."

  # Test LED on again (already on)
  log "INFO" "Testing LED on again (already on)..."
  local RESPONSE
  RESPONSE=$(curl -s -X POST "http://${FTP_HOST}/led/on")
  local STATUS=$(echo "${RESPONSE}" | jq -r '.status')
  local MESSAGE=$(echo "${RESPONSE}" | jq -r '.message')
  if [ "${STATUS}" != "success" ] || [ "${MESSAGE}" != "LED turned on." ]; then
    log "ERRO" "LED on again failed. Status: ${STATUS}, Message: ${MESSAGE}"
    exit 1
  fi
  sleep ${DELAY_SECONDS}
  STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state' | tr '[:lower:]' '[:upper:]')
  if [ "${STATUS}" != "ON" ]; then
    log "ERRO" "LED state changed unexpectedly after turning on again. Current state: ${STATUS}"
    exit 1
  fi
  log "SUCCESS" "LED is still ON and message is correct."

  # Test LED toggle
  log "INFO" "Verifying LED toggle..."
  local INITIAL_STATUS
  INITIAL_STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state')
  log "INFO" "Initial LED state: ${INITIAL_STATUS}"

  request_and_verify "POST" "/led/toggle" "" ""
  sleep ${DELAY_SECONDS}

  local NEW_STATUS
  NEW_STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state')
  log "INFO" "New LED state: ${NEW_STATUS}"

  if [ "${INITIAL_STATUS}" == "${NEW_STATUS}" ]; then
    log "ERRO" "LED state did not change after toggle."
    exit 1
  fi
  log "SUCCESS" "LED toggle verified successfully."

  # Toggle LED back on
  log "INFO" "Toggling LED back on..."
  request_and_verify "POST" "/led/toggle" "" ""
  sleep ${DELAY_SECONDS}

  NEW_STATUS=$(curl -s "http://${FTP_HOST}/led/status" | jq -r '.state' | tr '[:lower:]' '[:upper:]')
  if [ "${NEW_STATUS}" != "ON" ]; then
    log "ERRO" "LED failed to turn back on. Current state: ${NEW_STATUS}"
    exit 1
  fi
  log "SUCCESS" "LED is ON again."
}

function verify_led_brightness() {
  log "INFO" "Verifying LED brightness..."
  local INITIAL_BRIGHTNESS
  INITIAL_BRIGHTNESS=$(curl -s "http://${FTP_HOST}/led/brightness" | jq -r '.brightness')
  log "INFO" "Initial LED brightness: ${INITIAL_BRIGHTNESS}"

  local NEW_BRIGHTNESS_VAL=128
  if [ "${INITIAL_BRIGHTNESS}" == "128" ]; then
    NEW_BRIGHTNESS_VAL=255
  fi

  request_and_verify "POST" "/led/brightness" "${NEW_BRIGHTNESS_VAL}" ""

  local NEW_BRIGHTNESS
  NEW_BRIGHTNESS=$(curl -s "http://${FTP_HOST}/led/brightness" | jq -r '.brightness')
  log "INFO" "New LED brightness: ${NEW_BRIGHTNESS}"

  if [ "${NEW_BRIGHTNESS}" != "${NEW_BRIGHTNESS_VAL}" ]; then
    log "ERRO" "LED brightness did not change as expected."
    exit 1
  fi
  log "SUCCESS" "LED brightness verified successfully."
  request_and_verify "POST" "/led/brightness" "${INITIAL_BRIGHTNESS}" ""
}

function verify_mqtt_actions() {
  log "INFO" "Verifying MQTT APIs..."

  # Test MQTT disable
  log "INFO" "Testing MQTT disable..."
  request_and_verify "POST" "/mqtt/disable" "" ""
  sleep ${DELAY_SECONDS}
  local ENABLED_STATUS
  ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  if [ "${ENABLED_STATUS}" != "false" ]; then
    log "ERRO" "MQTT disable failed. Enabled status: ${ENABLED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "MQTT is DISABLED."

  # Test MQTT disable again (already disabled)
  log "INFO" "Testing MQTT disable again (already disabled)..."
  request_and_verify "POST" "/mqtt/disable" "" ""
  sleep ${DELAY_SECONDS}
  ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  if [ "${ENABLED_STATUS}" != "false" ]; then
    log "ERRO" "MQTT enabled status changed unexpectedly after disabling again. Current status: ${ENABLED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "MQTT is still DISABLED."

  # Test MQTT enable
  log "INFO" "Testing MQTT enable..."
  request_and_verify "POST" "/mqtt/enable" "" ""
  sleep ${DELAY_SECONDS}
  ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  if [ "${ENABLED_STATUS}" != "true" ]; then
    log "ERRO" "MQTT enable failed. Enabled status: ${ENABLED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "MQTT is ENABLED."

  # Test MQTT enable again (already enabled)
  log "INFO" "Testing MQTT enable again (already enabled)..."
  request_and_verify "POST" "/mqtt/enable" "" ""
  sleep ${DELAY_SECONDS}
  ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  if [ "${ENABLED_STATUS}" != "true" ]; then
    log "ERRO" "MQTT enabled status changed unexpectedly after enabling again. Current status: ${ENABLED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "MQTT is still ENABLED."

  # Test MQTT toggle
  log "INFO" "Verifying MQTT toggle..."
  local INITIAL_ENABLED_STATUS
  INITIAL_ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  log "INFO" "Initial MQTT enabled status: ${INITIAL_ENABLED_STATUS}"

  request_and_verify "POST" "/mqtt/toggle" "" ""
  sleep ${DELAY_SECONDS}

  local NEW_ENABLED_STATUS
  NEW_ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  log "INFO" "New MQTT enabled status: ${NEW_ENABLED_STATUS}"

  if [ "${INITIAL_ENABLED_STATUS}" == "${NEW_ENABLED_STATUS}" ]; then
    log "ERRO" "MQTT enabled status did not change after toggle."
    exit 1
  fi
  log "SUCCESS" "MQTT toggle verified successfully."

  # Toggle MQTT back to initial state (ON)
  log "INFO" "Toggling MQTT back to initial state..."
  request_and_verify "POST" "/mqtt/toggle" "" ""
  sleep ${DELAY_SECONDS}

  NEW_ENABLED_STATUS=$(curl -s "http://${FTP_HOST}/mqtt/status" | jq -r '.mqtt_enabled')
  if [ "${NEW_ENABLED_STATUS}" != "true" ]; then
    log "ERRO" "MQTT failed to turn back on. Current status: ${NEW_ENABLED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "MQTT is ON again."
}

function verify_gets(){
  log "INFO" "Starting API tests for device at ${FTP_HOST}"

  request_and_verify "GET" "/" "" ".mode .display.status .display.orientation .sd_card.total_size .sd_card.used_size .sd_card.free_size .sd_card.file_count .mqtt.enabled .mqtt.state .mqtt.connected .led.color .led.state .led.brightness"
  # request_and_verify "GET" "/mode/msc" "" ".status .message"
  # request_and_verify "GET" "/mode/ftp" "" ".status .message"
  request_and_verify "GET" "/display/status" "" ".status .display_status"
  request_and_verify "GET" "/mqtt/status" "" ".status .mqtt_enabled .mqtt_state .mqtt_connected"
  request_and_verify "GET" "/led/status" "" ".status .color .state .brightness"
  request_and_verify "GET" "/led/brightness" "" ".status .brightness"
}

# Check initial display and LED status
function check_initial_display_and_led_status() {
  log "INFO" "Verifying initial display and LED status..."
  local RESPONSE
  RESPONSE=$(curl -s "http://${FTP_HOST}/")

  local DISPLAY_STATUS
  DISPLAY_STATUS=$(echo "${RESPONSE}" | jq -r '.display.status' | tr '[:lower:]' '[:upper:]')
  if [ "${DISPLAY_STATUS}" != "ON" ]; then
    log "ERRO" "Initial display status is not ON. Current status: ${DISPLAY_STATUS}"
    exit 1
  fi
  log "SUCCESS" "Initial display status is ON."

  local LED_STATUS
  LED_STATUS=$(echo "${RESPONSE}" | jq -r '.led.state' | tr '[:lower:]' '[:upper:]')
  if [ "${LED_STATUS}" != "ON" ]; then
    log "ERRO" "Initial LED status is not ON. Current status: ${LED_STATUS}"
    exit 1
  fi
  log "SUCCESS" "Initial LED status is ON."
}

function verify_posts() {
  log "INFO" "Starting POST API tests..."

  log "INFO" "--- Display API Tests ---"
  verify_display_toggle

  log "INFO" "--- LED API Tests ---"
  verify_led_toggle
  verify_led_brightness

  log "INFO" "--- MQTT API Tests ---"
  verify_mqtt_actions
}

function verify_file_upload() {
  local EXPECTED_STATUS="$1" # "success" or "error"
  log "INFO" "Verifying file upload via web server (expecting ${EXPECTED_STATUS})..."

  # Create a dummy file to upload
  local DUMMY_FILE="test-upload.txt"
  local DUMMY_FILENAME="test-on-device.txt"
  local DUMMY_CONTENT="This is a test file for web upload."
  echo "${DUMMY_CONTENT}" > "${DUMMY_FILE}"

  log "INFO" "Uploading '${DUMMY_FILE}' to the device as '${DUMMY_FILENAME}'..."

  # Upload the file using curl with POST and multipart/form-data
  local RESPONSE
  RESPONSE=$(curl -s -X POST -F "file=@${DUMMY_FILE};filename=${DUMMY_FILENAME}" "http://${FTP_HOST}/upload")

  # Clean up the dummy file
  rm "${DUMMY_FILE}"

  log "INFO" "Upload response: ${RESPONSE}"

  # Verify the response
  local STATUS
  STATUS=$(echo "${RESPONSE}" | jq -r '.status')
  if [ "${STATUS}" != "${EXPECTED_STATUS}" ]; then
    log "ERRO" "File upload returned unexpected status. Got: '${STATUS}', Expected: '${EXPECTED_STATUS}'. Message: $(echo "${RESPONSE}" | jq -r '.message')"
    exit 1
  fi

  log "SUCCESS" "File upload test completed with expected status '${EXPECTED_STATUS}'."
}

function run_usb_msc_tests(){
  log "INFO" "=== USB MSC MODE Tests ==="
  verify_gets
  verify_posts
  verify_file_upload "error"
}

function run_ftp_tests() {
  log "INFO" "=== FTP MODE Tests ==="
  log "INFO" "Switching to FTP mode"
  request_and_verify "POST" "/mode/ftp" "" ""
  sleep 10
  verify_gets
  verify_posts
  verify_file_upload "success"
  log "INFO" "Switching back to USB MSC mode"
  request_and_verify "POST" "/mode/msc" "" ""
  sleep 10
}

# Main function to orchestrate the script execution
function main() {
  local START_TIME
  START_TIME=$(date +%s)
  log "INFO" "=== Starting "$0" ==="
  check_dependencies
  load_vars
  check_device_status
  check_initial_mode
  check_initial_display_and_led_status
  run_usb_msc_tests
  run_ftp_tests

  local END_TIME
  END_TIME=$(date +%s)
  local DURATION=$((END_TIME - START_TIME))
  log "INFO" "Script finished in $(($DURATION / 60)) minutes and $(($DURATION % 60)) seconds."
  log "SUCCESS" "=== All API tests completed successfully. ==="
}

# Call main to start the script
main "$@"

๐Ÿงช Testing the FTP Server

The test-ftp.sh script automates testing the FTP server functionality by uploading, downloading, and verifying a test file.

๐Ÿ“ฆ Dependencies

You must have the following dependencies installed on your system:

  • curl: For interacting with web services (used to check device status).
  • jq: A lightweight and flexible command-line JSON processor.
  • lftp: A sophisticated file transfer program.
sudo apt install curl jq lftp
brew install curl jq lftp

โš™ Configuration

The script requires an .env file with the device's FTP host, username, and password.

  1. Copy the template .env File:

    cp scripts/.env.tmpl scripts/.env
    
  2. Edit scripts/.env: Update the FTP_HOST, FTP_USER, and FTP_PASSWORD variables with your device's credentials.

scripts/.env

FTP_HOST="192.168.2.169"
FTP_USER="user"
FTP_PASSWORD="password"
WEB_SERVER_USER=""
WEB_SERVER_PASSWORD=""
LOCAL_DIR="data"
REMOTE_DIR="/"

๐Ÿ“ Script Usage

  1. Ensure the device is in FTP Server Mode.
  2. Run the script from the scripts directory:
task test-ftp
cd scripts
./test-ftp.sh
test-ftp.sh
#!/usr/bin/env bash
################################################################################
#
# test-ftp.sh
# ----------------
# Tests the FTP functionality of the FrameFi device by uploading, downloading,
# and verifying a test file.
#
# @author Nicholas Wilde, 0xb299a622
# @date 17 Aug 2025
# @version 0.1.0
#
################################################################################

# Options
set -e
set -o pipefail

# These are constants
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)
readonly RED GREEN YELLOW BLUE RESET

DELAY_SECONDS=1
readonly DELAY_SECONDS

TEST_FILE_NAME="test_ftp_file.txt"
readonly TEST_FILE_NAME

TEST_FILE_CONTENT="This is a test file for FTP functionality."
readonly TEST_FILE_CONTENT

# Log function for standardized output
function log() {
  local TYPE="$1"
  local MESSAGE="$2"
  local COLOR=""
  local EMOJI=""

  case "$TYPE" in
    "INFO") COLOR="${BLUE}"; EMOJI="";;
    "WARN") COLOR="${YELLOW}"; EMOJI="โš ๏ธ ";;
    "ERRO") COLOR="${RED}"; EMOJI="โŒ ";;
    "SUCCESS") COLOR="${BLUE}"; EMOJI="โœ… "; TYPE="INFO";;
    *) COLOR="${RESET}";;
  esac

  echo "${COLOR}${TYPE}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${EMOJI}${MESSAGE}"
}

# Check for dependencies
function check_dependencies() {
  log "INFO" "Checking dependencies..."
  if ! command -v curl &> /dev/null; then
    log "ERRO" "curl could not be found. Please install it."
    exit 1
  fi
  if ! command -v jq &> /dev/null; then
    log "ERRO" "jq could not be found. Please install it."
    exit 1
  fi
  if ! command -v lftp &> /dev/null; then
    log "ERRO" "lftp could not be found. Please install it."
    exit 1
  fi
  log "SUCCESS" "Dependencies checked."
}

function load_vars() {
  local ENV_FILE="$(dirname "$0")/.env"

  if [ ! -f "${ENV_FILE}" ]; then
    log "ERRO" "Environment file not found: ${ENV_FILE}"
    log "ERRO" "Please create it from .env.tmpl and ensure FTP_HOST, FTP_USER, FTP_PASSWORD are set."
    exit 1
  fi

  source "${ENV_FILE}"

  if [ -z "${FTP_HOST}" ] || [ -z "${FTP_USER}" ] || [ -z "${FTP_PASSWORD}" ]; then
    log "ERRO" "FTP_HOST, FTP_USER, or FTP_PASSWORD not set in ${ENV_FILE}"
    exit 1
  fi
}

# Check if the device is online
function check_device_status() {
  log "INFO" "Checking if device at ${FTP_HOST} is online..."
  if ! curl -s -o /dev/null --fail --connect-timeout 5 "http://${FTP_HOST}/"; then
    log "ERRO" "Device at ${FTP_HOST} is not responding."
    log "ERRO" "Please ensure the device is connected to the network and the IP address is correct."
    exit 1
  fi
  log "SUCCESS" "Device is online."
}

# Check device mode
function check_device_mode() {
  log "INFO" "Verifying device mode is FTP..."
  local RESPONSE
  RESPONSE=$(curl -s "http://${FTP_HOST}/")
  local CURRENT_MODE
  CURRENT_MODE=$(echo "${RESPONSE}" | jq -r '.mode')

  if [ "${CURRENT_MODE}" != "Application (FTP Server)" ]; then
    log "ERRO" "Device is not in FTP mode. Current mode: ${CURRENT_MODE}"
    log "ERRO" "Please set the device to FTP mode before running tests (e.g., curl -X POST http://${FTP_HOST}/mode/ftp)."
    exit 1
  fi
  log "SUCCESS" "Device is in FTP mode."
}

# Function to upload a test file
function upload_test_file() {
  log "INFO" "Creating local test file: ${TEST_FILE_NAME}"
  echo "${TEST_FILE_CONTENT}" > "${TEST_FILE_NAME}"

  log "INFO" "Uploading ${TEST_FILE_NAME} to FTP server at ${FTP_HOST}..."
  lftp -c "
  set ftp:ssl-allow no;
  open -u "${FTP_USER}","${FTP_PASSWORD}" "${FTP_HOST}";
  put -O / "${TEST_FILE_NAME}";
  "
  if [ $? -ne 0 ]; then
    log "ERRO" "Failed to upload ${TEST_FILE_NAME}."
    exit 1
  fi
  log "SUCCESS" "Successfully uploaded ${TEST_FILE_NAME}."
}

# Function to download a test file
function download_test_file() {
  log "INFO" "Downloading ${TEST_FILE_NAME} from FTP server at ${FTP_HOST}..."
  lftp -c "
  set ftp:ssl-allow no;
  set xfer:clobber true;
  open -u "${FTP_USER}","${FTP_PASSWORD}" "${FTP_HOST}";
  get -O . "${TEST_FILE_NAME}";
  "
  if [ $? -ne 0 ]; then
    log "ERRO" "Failed to download ${TEST_FILE_NAME}."
    exit 1
  fi
  log "SUCCESS" "Successfully downloaded ${TEST_FILE_NAME}."
}

# Function to verify the content of the downloaded file
function verify_downloaded_file() {
  log "INFO" "Verifying content of downloaded file: ${TEST_FILE_NAME}"
  local DOWNLOADED_CONTENT
  DOWNLOADED_CONTENT=$(cat "${TEST_FILE_NAME}")

  if [ "${DOWNLOADED_CONTENT}" == "${TEST_FILE_CONTENT}" ]; then
    log "SUCCESS" "Content of ${TEST_FILE_NAME} matches expected content."
  else
    log "ERRO" "Content mismatch for ${TEST_FILE_NAME}."
    log "ERRO" "Expected: '${TEST_FILE_CONTENT}'"
    log "ERRO" "Got:      '${DOWNLOADED_CONTENT}'"
    exit 1
  fi
}

# Function to delete the test file from the FTP server
# Function to delete the test file from the FTP server
function delete_test_file_remote() {
  log "INFO" "Attempting to delete ${TEST_FILE_NAME} from FTP server at ${FTP_HOST} for cleanup..."
  local LFTP_OUTPUT
  local LFTP_EXIT_CODE

  # Execute lftp command, capture stderr and stdout
  LFTP_OUTPUT=$(lftp -c "set ftp:ssl-allow no; open -u \"${FTP_USER}\",\"${FTP_PASSWORD}\" \"${FTP_HOST}\"; rm \"${TEST_FILE_NAME}\";")
  LFTP_EXIT_CODE=$?

  if [ ${LFTP_EXIT_CODE} -ne 0 ]; then
    if echo "${LFTP_OUTPUT}" | grep -q "550 ${TEST_FILE_NAME}: No such file or directory"; then
      log "WARN" "File ${TEST_FILE_NAME} not found on remote during cleanup. This is expected if it didn't exist."
    else
      log "ERRO" "Failed to delete ${TEST_FILE_NAME} from remote. Error: ${LFTP_OUTPUT}"
      exit 1
    fi
  else
    log "SUCCESS" "Successfully deleted ${TEST_FILE_NAME} from remote."
  fi
}

# Function to clean up local test file
function cleanup_local_file() {
  if [ -f "${TEST_FILE_NAME}" ]; then
    log "INFO" "Cleaning up local test file: ${TEST_FILE_NAME}"
    rm "${TEST_FILE_NAME}"
    log "SUCCESS" "Successfully removed local test file."
  fi
}

# Main function to orchestrate the script execution
function main() {
  local START_TIME
  START_TIME=$(date +%s)
  log "INFO" "=== Starting "$0" ==="

  check_dependencies
  load_vars
  check_device_status
  check_device_mode

  # Ensure clean state before starting tests
  delete_test_file_remote
  cleanup_local_file

  upload_test_file
  download_test_file
  verify_downloaded_file
  delete_test_file_remote
  cleanup_local_file

  local END_TIME
  END_TIME=$(date +%s)
  local DURATION=$((END_TIME - START_TIME))
  log "INFO" "Script finished in $(($DURATION / 60)) minutes and $(($DURATION % 60)) seconds."
  log "SUCCESS" "=== All FTP tests completed successfully. ==="
}

# Call main to start the script
main "$@"

๐Ÿ”— References