defmodule DaProductApp.TerminalManagement.FileDownloadService do @moduledoc """ Service module for handling file download commands to devices via MQTT. Implements file download functionality including logos, firmware, applications, etc. """ require Logger alias DaProductApp.TerminalManagement @doc """ Sends a file download command to a device via MQTT. Topic format: /ota/{product_key}/{device_sn}/update """ def send_file_download_command(device_serial, download_params) do #check if command available in download_prams for "command" => "file_download" if not add in download_params download_params = Map.put_new(download_params, "command", "file_download") #check if request_id available or else generate a unique one download_params = Map.put_new(download_params, "request_id", "download_#{System.unique_integer([:positive])}") with {:ok, validated_params} <- validate_download_params(download_params), {:ok, topic} <- build_download_topic(device_serial, validated_params), {:ok, payload} <- build_download_payload(validated_params) do Logger.info("Sending file download command to device #{device_serial} on topic: #{topic}") Logger.info("Download payload: #{payload}") case DaProductApp.MQTT.publish(topic, payload, qos: 1) do :ok -> # Log the successful send log_download_command(device_serial, validated_params, "sent") {:ok, "File download command sent successfully"} {:ok, _reference} -> # Handle QoS 1 success with message reference log_download_command(device_serial, validated_params, "sent") {:ok, "File download command sent successfully"} {:error, reason} -> Logger.error("Failed to send file download command to #{device_serial}: #{inspect(reason)}") log_download_command(device_serial, validated_params, "failed", "MQTT publish failed: #{inspect(reason)}") {:error, "Failed to send file download command"} end else {:error, reason} -> Logger.error("File download command validation failed: #{reason}") {:error, reason} end end @doc """ Handles device response to file download command. """ def handle_download_response(device_serial, request_id, response) do Logger.info("Processing file download response for device #{device_serial}, request_id: #{request_id}") status = case response do %{"status" => "success"} -> "completed" %{"status" => "failed"} -> "failed" _ -> "unknown" end error_message = case response do %{"error_code" => error_code} -> "Error code: #{error_code}" %{"error_message" => message} -> message _ -> nil end # Update job status via broadcast for real-time UI updates Phoenix.PubSub.broadcast( DaProductApp.PubSub, "file_download:#{device_serial}", {:download_response, %{ request_id: request_id, status: status, response: response, timestamp: DateTime.utc_now() }} ) # Log the response log_download_response(device_serial, request_id, response, status, error_message) :ok end defp validate_download_params(params) do required_fields = ["command", "local_path_to_save", "url_path_from_download", "file_name", "request_id"] case Enum.find(required_fields, fn field -> is_nil(params[field]) or params[field] == "" end) do nil -> validated_params = params |> Map.put_new("retry_count", "3") |> Map.put_new("merchant_config", "true") |> Map.put_new("file_category", "application") {:ok, validated_params} missing_field -> {:error, "Missing required field: #{missing_field}"} end end defp build_download_topic(device_serial, params) do # Use default product key if not provided product_key = params["product_key"] || "pFppbioOCKlo5c8E" topic = "/ota/#{product_key}/#{device_serial}/update" {:ok, topic} end defp build_download_payload(params) do payload = %{ "command" => "file_download", "local_path_to_save" => params["local_path_to_save"], "url_path_from_download" => params["url_path_from_download"], "file_size" => params["file_size"], "file_category" => params["file_category"], "file_name" => params["file_name"], "merchant_config" => params["merchant_config"], "retry_count" => params["retry_count"], "request_id" => params["request_id"] } |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new() case Jason.encode(payload) do {:ok, json} -> {:ok, json} {:error, reason} -> {:error, "Failed to encode payload: #{inspect(reason)}"} end end defp log_download_command(device_serial, params, status, error_message \\ nil) do log_entry = %{ device_serial: device_serial, request_id: params["request_id"], command: "file_download", file_name: params["file_name"], url: params["url_path_from_download"], status: status, error_message: error_message, timestamp: DateTime.utc_now(), params: params } # For now, just log to console. In production, this could be stored in database Logger.info("File download command log: #{inspect(log_entry)}") end defp log_download_response(device_serial, request_id, response, status, error_message) do log_entry = %{ device_serial: device_serial, request_id: request_id, command: "file_download_response", status: status, error_message: error_message, response: response, timestamp: DateTime.utc_now() } # For now, just log to console. In production, this could be stored in database Logger.info("File download response log: #{inspect(log_entry)}") end @doc """ Creates a standard file download command for common use cases. """ def create_logo_download_command(device_serial, logo_url, file_name \\ "merchant_logo.png") do params = %{ "command" => "file_download", "local_path_to_save" => "exdata", "url_path_from_download" => logo_url, "file_name" => file_name, "file_category" => "logo_image", "merchant_config" => "true", "retry_count" => "3", "request_id" => "logo_#{System.unique_integer([:positive])}" } send_file_download_command(device_serial, params) end def create_firmware_download_command(device_serial, firmware_url, file_name) do params = %{ "command" => "file_download", "local_path_to_save" => "firmware", "url_path_from_download" => firmware_url, "file_name" => file_name, "file_category" => "firmware", "merchant_config" => "false", "retry_count" => "5", "request_id" => "firmware_#{System.unique_integer([:positive])}" } send_file_download_command(device_serial, params) end def create_application_download_command(device_serial, app_url, file_name) do params = %{ "command" => "file_download", "local_path_to_save" => "apps", "url_path_from_download" => app_url, "file_name" => file_name, "file_category" => "application", "merchant_config" => "false", "retry_count" => "3", "request_id" => "app_#{System.unique_integer([:positive])}" } send_file_download_command(device_serial, params) end end