defmodule DaProductApp.TerminalManagement.AppPackageService do @moduledoc """ Service module for handling app package deployment to devices via MQTT. Implements package deployment, status tracking, and rollback functionality. """ require Logger alias DaProductApp.TerminalManagement alias DaProductApp.TerminalManagement.{AppPackage, AppUpgradeConfig, AppUpgradeDeviceStatus} @doc """ Deploys an app package to a specific device. """ def deploy_package_to_device(device_serial, package_id, config_id) do with {:ok, package} <- get_package(package_id), {:ok, config} <- get_config(config_id), {:ok, topic} <- build_deployment_topic(device_serial, package), {:ok, payload} <- build_deployment_payload(package, config) do Logger.info("Sending app package deployment to device #{device_serial} on topic: #{topic}") Logger.info("Deployment payload: #{payload}") case DaProductApp.MQTT.publish(topic, payload, qos: 1) do :ok -> # Update device status to "pushed" update_device_deployment_status(device_serial, config_id, "pushed", "Package deployment sent") {:ok, "App package deployment sent successfully"} {:error, reason} -> Logger.error("Failed to send app package deployment to #{device_serial}: #{inspect(reason)}") update_device_deployment_status(device_serial, config_id, "failed", "MQTT publish failed: #{inspect(reason)}") {:error, "Failed to send app package deployment"} end else {:error, reason} -> Logger.error("App package deployment validation failed: #{reason}") update_device_deployment_status(device_serial, config_id, "failed", reason) {:error, reason} end end @doc """ Handles device response to app package deployment. """ def handle_deployment_response(device_serial, request_id, response) do Logger.info("Processing app package deployment response for device #{device_serial}, request_id: #{request_id}") status = case response do %{"status" => "success", "result" => result} -> update_device_deployment_status(device_serial, request_id, "success", "Deployment completed: #{result}") "success" %{"status" => "failed", "error" => error} -> update_device_deployment_status(device_serial, request_id, "failed", "Deployment failed: #{error}") "failed" %{"status" => "in_progress", "progress" => progress} -> update_device_deployment_status(device_serial, request_id, "in_progress", "Deployment progress: #{progress}%") "in_progress" _ -> update_device_deployment_status(device_serial, request_id, "unknown", "Unknown response format") "unknown" end # Broadcast status update for real-time UI updates Phoenix.PubSub.broadcast( DaProductApp.PubSub, "app_deployment:#{device_serial}", {:deployment_status_updated, device_serial, request_id, status, response} ) :ok end @doc """ Rolls back an app package to a previous version. """ def rollback_package(device_serial, target_version) do with {:ok, topic} <- build_rollback_topic(device_serial), {:ok, payload} <- build_rollback_payload(target_version) do Logger.info("Sending app package rollback to device #{device_serial}") case DaProductApp.MQTT.publish(topic, payload, qos: 1) do :ok -> {:ok, "App package rollback sent successfully"} {:error, reason} -> Logger.error("Failed to send app package rollback to #{device_serial}: #{inspect(reason)}") {:error, "Failed to send app package rollback"} end else {:error, reason} -> Logger.error("App package rollback validation failed: #{reason}") {:error, reason} end end @doc """ Creates a comprehensive deployment command with all package information. """ def create_comprehensive_deployment(device_serial, package_id, deployment_options \\ %{}) do with {:ok, package} <- get_package(package_id) do # Create a temporary config with deployment options config_attrs = %{ package_id: package_id, force_upgrade: deployment_options["force_upgrade"] || false, is_iterate: deployment_options["is_iterate"] || false, status: "pending" } case TerminalManagement.create_app_upgrade_config(config_attrs) do {:ok, config} -> deploy_package_to_device(device_serial, package_id, config.id) {:error, changeset} -> {:error, "Failed to create deployment config: #{inspect(changeset.errors)}"} end else {:error, reason} -> {:error, reason} end end # Private functions defp get_package(package_id) do case TerminalManagement.get_app_package!(package_id) do nil -> {:error, "Package not found"} package -> {:ok, package} end rescue Ecto.NoResultsError -> {:error, "Package not found"} end defp get_config(config_id) when is_nil(config_id), do: {:ok, nil} defp get_config(config_id) do case TerminalManagement.get_app_upgrade_config!(config_id) do nil -> {:error, "Config not found"} config -> {:ok, config} end rescue Ecto.NoResultsError -> {:error, "Config not found"} end defp build_deployment_topic(device_serial, package) do # Use consistent topic format similar to OTA updates topic = "/ota/pFppbioOCKlo5c8E/#{device_serial}/app_deploy" {:ok, topic} end defp build_rollback_topic(device_serial) do topic = "/ota/pFppbioOCKlo5c8E/#{device_serial}/app_rollback" {:ok, topic} end defp build_deployment_payload(package, config) do payload = %{ type: "app_deployment", request_id: System.unique_integer([:positive]), package_info: %{ id: package.id, version_name: package.version_name, model: package.model, vendor: package.vendor, app_version: package.app_version, data_version: package.data_version, system_version: package.system_version }, download_info: %{ url: build_download_url(package.file_path), file_size: get_file_size(package.file_path), checksum: calculate_checksum(package.file_path) }, deployment_options: build_deployment_options(config), timestamp: System.system_time(:second) } {:ok, Jason.encode!(payload)} end defp build_rollback_payload(target_version) do payload = %{ type: "app_rollback", request_id: System.unique_integer([:positive]), target_version: target_version, timestamp: System.system_time(:second) } {:ok, Jason.encode!(payload)} end defp build_deployment_options(nil) do %{ force_upgrade: false, backup_current: true, restart_after_install: true } end defp build_deployment_options(config) do %{ force_upgrade: config.force_upgrade || false, backup_current: true, restart_after_install: true, is_iterate: config.is_iterate || false } end defp build_download_url(file_path) do # Build full download URL for the package file "https://demo.ctrmv.com#{file_path}" end defp get_file_size(file_path) do # Get file size - implement based on your file storage case File.stat("priv/static#{file_path}") do {:ok, %{size: size}} -> size _ -> 0 end end defp calculate_checksum(file_path) do # Calculate file checksum for integrity verification case File.read("priv/static#{file_path}") do {:ok, content} -> :crypto.hash(:sha256, content) |> Base.encode16(case: :lower) _ -> "" end end defp update_device_deployment_status(device_serial, config_id, status, message) do # Find or create device status record case find_or_create_device_status(device_serial, config_id) do {:ok, device_status} -> TerminalManagement.update_app_upgrade_device_status(device_status, %{ status: status, remark: message, finish_time: if(status in ["success", "failed"], do: DateTime.utc_now(), else: nil), pushed_time: if(status == "pushed", do: DateTime.utc_now(), else: device_status.pushed_time) }) {:error, reason} -> Logger.error("Failed to update device deployment status: #{reason}") end end defp find_or_create_device_status(device_serial, config_id) when is_nil(config_id) do # Handle case where no specific config_id is provided {:ok, nil} end defp find_or_create_device_status(device_serial, config_id) do case TerminalManagement.get_device_status_by_serial_and_config(device_serial, config_id) do nil -> # Create new device status if it doesn't exist case TerminalManagement.get_terminal_by_serial(device_serial) do nil -> {:error, "Terminal not found"} terminal -> TerminalManagement.create_app_upgrade_device_status(%{ config_id: config_id, device_id: terminal.id, device_sn: device_serial, vendor: terminal.vendor || "Unknown", model: terminal.model || "Unknown", status: "pending" }) end device_status -> {:ok, device_status} end end end