# How to Implement Firmware Updates (FUOTA) over HTTPS

**URL:** <https://community.tago.io/t/how-to-implement-firmware-updates-fuota-over-https/1841>\
**Category:** Tutorials\
**Tags:** tutorials\
**Created:** [January 28, 2026, 1:18pm UTC](https://community.tago.io/t/how-to-implement-firmware-updates-fuota-over-https/1841 "2026-01-28T13:18:05Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![philip.cotrin](https://avatars.discourse-cdn.com/v4/letter/p/b487fb/32.png) [@philip.cotrin](https://community.tago.io/u/philip.cotrin)\
**Post date:** [January 28, 2026, 1:18pm UTC](https://community.tago.io/t/how-to-implement-firmware-updates-fuota-over-https/1841/1 "2026-01-28T13:18:05Z")

</div>

# **FUOTA over HTTPS**

Remote firmware updates are a cornerstone of scalable IoT. This guide shows you how to build a complete Firmware Update Over The Air (FUOTA) flow using **TagoIO Files** , **Analysis** , and a **Dashboard Template**.

**Note:** This tutorial covers the HTTPS method, where the device downloads the file via a direct URL. If your devices require updates over MQTT (chunking), please refer to our **[How to Implement Firmware Updates (FUOTA) over MQTT](https://community.tago.io/t/fuota-over-mqtt/1849)**.

## **The Architecture**

1. **User Action:** An operator uploads a `.bin` file via a pre-built **Blueprint Dashboard**.

2. **Storage:** The dashboard automatically saves the file to TagoIO Files and triggers an Analysis.

3. **Secure Automation:** The Analysis (running on **Deno** ) updates the device’s Configuration Parameters, setting the status to `sent: false` (Pending).

4. **Device Logic:** The device polls for pending parameters, downloads the file, and marks the parameter as `sent: true` (Acknowledged).

* * *

## **Step 1: Create the Analysis Script (Deno)**

We will use **Deno** for this script. We also use the `Resources` class, which allows the script to run using its own specific token rather than your master Account Token.

1. Go to **Analysis** in the sidebar.

2. Create a new analysis.

3. Set the **Runtime** to **Deno**.

4. Paste the code below. You do **not** need to set any Environment Variables.

```typescript
/*
 * Analysis: FUOTA Handler (HTTPS) with Cleanup & Validation
 * Runtime: Deno
 * Triggered by: Action (Variable: firmware_file)
 */
import { Analysis, Resources, Utils } from "npm:@tago-io/sdk";
import { DateTime } from "npm:luxon";

type validation_type = "success" | "danger" | "warning" | string;

interface IValidateOptions {
  show_markdown?: boolean;
  user_id?: string;
  session_id?: string;
}

/**
 * Helper: Sends validation feedback to the Input Form widget.
 */
function initializeValidation(resources: Resources, validationVariable: string, device_id: string, opts?: IValidateOptions) {
  let i = 0;
  return async function _(message: string, type: validation_type = "success") {
    if (!message || !type) throw "Missing message or type";

    i += 1;
    // 1. Clear old validation messages
    await resources.devices
      .deleteDeviceData(device_id, { variables: validationVariable, qty: 999 })
      .catch(console.log);

    // 2. Insert the new validation message
    await resources.devices
      .sendDeviceData(device_id, {
        variable: validationVariable,
        value: message,
        time: DateTime.now().plus({ milliseconds: i * 200 }).toJSDate(), 
        metadata: {
          type: ["success", "danger", "warning"].includes(type) ? type : null,
          color: !["success", "danger", "warning"].includes(type) ? type : undefined,
          show_markdown: !!opts?.show_markdown,
          user_id: opts?.user_id,
          session_id: opts?.session_id,
        },
      })
      .catch(console.error);

    return message;
  };
}

async function startAnalysis(context: any, scope: any) {
  context.log("Starting FUOTA Analysis...");
  const resources = new Resources({ token: context.token });

  // 1. Identify the device
  // Since we use an Action, scope[0] contains the triggering data (firmware_file)
  const deviceId = scope[0]?.device;
  if (!deviceId) {
    return context.log("Error: Analysis triggered without a device context.");
  }

  // 2. Setup Validation
  // If triggered by UI, we try to get the user ID for targeted feedback
  const environment = Utils.envToJson(context.environment);
  const userId = environment._user_id; 
  const validate = initializeValidation(resources, "firmware_validation", deviceId, { user_id: userId });

  try {
    const fileVar = scope.find((x: any) => x.variable === "firmware_file");
    const versionVar = scope.find((x: any) => x.variable === "firmware_version");

    // Check A: Missing Fields
    if (!fileVar || !versionVar) {
      throw await validate("Missing file or version. Please use the Input Form.", "danger");
    }

    // Check B: File Extension
    const filename = String(fileVar.value || "");
    if (!filename.toLowerCase().endsWith(".bin")) {
       throw await validate(`Invalid file: ${filename}. Please upload a .bin file.`, "danger");
    }

    // Check C: File URL
    const fileUrl = fileVar.metadata?.file?.url;
    if (!fileUrl) {
      throw await validate("File URL not found. Check Input Form settings.", "danger");
    }

    const newVersion = versionVar.value;
    context.log(`Deploying Firmware v${newVersion} to device ${deviceId}`);
    await validate(`Initializing update v${newVersion}...`, "warning");

    // =========================================================
    // CLEANUP STEP: Remove old parameters
    // =========================================================
    const currentParams = await resources.devices.paramList(deviceId);
    const keysToRemove = ["firmware_url", "target_version"];
    
    const deletePromises = currentParams
        .filter((p) => keysToRemove.includes(p.key))
        .map((p) => resources.devices.paramRemove(deviceId, p.id));
    
    if (deletePromises.length > 0) {
        await Promise.all(deletePromises);
        context.log(`Cleaned up ${deletePromises.length} old parameters.`);
    }

    // =========================================================
    // UPDATE STEP: Set new parameters
    // =========================================================
    const params = [
      { key: "firmware_url", value: fileUrl, sent: false },
      { key: "target_version", value: String(newVersion), sent: false },
    ];

    await resources.devices.paramSet(deviceId, params);

    // Success Message
    await validate(`Update v${newVersion} ready. Waiting for device...`, "success");
    context.log("Configuration parameters updated.");

  } catch (error) {
    if (typeof error !== "string") {
      context.log(error);
      await validate("Internal Error. Check analysis logs.", "danger");
    }
  }
}

Analysis.use(startAnalysis);

```

* * *

## **Step 2: Configure Access Management (IAM)**

The Analysis script needs permission to read the file URL from the device parameters and send the data chunks back.

1. Go to **Access Management** in the sidebar.

2. Click **Create Policy**.

3. **Permissions:**

4. Click **Create Policy**.

_Note: Without this policy, your script will fail with “Permission Denied” errors when trying to send the chunks._

* * *

## **Step 3: Install the Dashboard Template**

To make setup faster, use our ready-to-use template instead of building widgets manually.

1. **[Click here to install the FUOTA HTTPS Dashboard Template.](https://admin.tago.io/template/69792067a6e2fb001170010c)**

2. Follow the prompts to install the dashboard into your account.

_The template is pre-configured to upload files to the system (“Send to resources”) and pass the correct variables to your script._

* * *

## **Step 4: Configure the Action**

Instead of hiding the logic inside the dashboard widget, we will create an explicit rule.

1. Go to **Actions** in the sidebar.

2. Click **Add Action**.

3. **General:**

4. **Trigger:**

5. Click **Save**.

* * *

## **Step 5: Device Logic (Python Simulation)**

We use a Python script to simulate the client-side logic. This demonstrates the flow you will implement on your hardware (C++, Rust, etc.).

**The Logic:**  
The device polls TagoIO for parameters where `sent` **is** `false`. This means “Pending.” If found, it downloads the file and then updates the parameter to `sent: true` to confirm receipt.

### **API References for Developers**

When implementing this on your actual hardware, refer to these documentation endpoints:

- **Get Configuration Parameters:** [List Config. parameters | TagoIO Docs](https://docs.tago.io/docs/api/get-configuration-parameters "https://docs.tago.io/docs/api/get-configuration-parameters")

- **Mark as Read:** [Mark Config. Parameter as Read | TagoIO Docs](https://docs.tago.io/docs/api/mark-a-configuration-parameter-as-read "https://docs.tago.io/docs/api/mark-a-configuration-parameter-as-read")

**Simulator Code (Python):**

```python
import requests
import time

# --- DEVICE CONFIGURATION ---
# Replace with the Device Token of the device selected in your Blueprint
DEVICE_TOKEN = "YOUR_DEVICE_TOKEN" 
API_URL = "https://api.tago.io/device/params"

headers = {
    "Authorization": DEVICE_TOKEN,
    "Content-Type": "application/json"
}

def check_for_updates():
    print("Checking for firmware updates...")
    
    # 1. Request only parameters that are PENDING (sent=false)
    # Ref: https://docs.tago.io/en/articles/14-device-api#configuration-parameters
    try:
        response = requests.get(f"{API_URL}?sent_status=false", headers=headers)
        if response.status_code != 200:
            print(f"Connection Error: {response.text}")
            return
            
        params = response.json().get("result", [])
    except Exception as e:
        print(f"Network Error: {e}")
        return

    # 2. Parse parameters
    firmware_url = None
    target_version = None
    param_ids = [] 

    for param in params:
        if param['key'] == 'firmware_url':
            firmware_url = param['value']
            param_ids.append(param['id'])
        if param['key'] == 'target_version':
            target_version = param['value']
            param_ids.append(param['id'])

    # 3. Decision Logic
    if firmware_url and target_version:
        print(f"--> Update Found! Target Version: {target_version}")
        perform_update(firmware_url, param_ids)
    else:
        print("--> System Up-to-Date.")

def perform_update(url, param_ids):
    print(f"Downloading firmware from: {url}")
    
    try:
        # 4. Download File
        file_resp = requests.get(url)
        
        # Simulate writing to flash memory
        filename = "update_package.bin"
        with open(filename, "wb") as f:
            f.write(file_resp.content)
            
        print(f"Firmware saved to local storage as '{filename}'")
        print("Verifying integrity... OK")
        print("Installing... OK")
        
        # 5. Acknowledge Update (Mark as Read/Sent)
        # We update 'sent: true' to tell TagoIO we received the data.
        # Ref: https://docs.tago.io/en/articles/14-device-api#edit-parameter
        for pid in param_ids:
             requests.put(f"{API_URL}/{pid}", headers=headers, json={"sent": True})
        
        print("Update confirmed to cloud. Rebooting device...")
        
    except Exception as e:
        print(f"Update Failed: {e}")

# Simulate a device loop
if __name__ == " __main__":
    while True:
        check_for_updates()
        # Sleep for 10 seconds before checking again
        time.sleep(10)

```

## **How to Test**

1. **Run the Simulator:** Start the Python script locally. It will print “System Up-to-Date”.

2. **Use the Dashboard:**

3. **Watch the Result:**

---

<div class="post-metadata">

**Author:** ![carolina.hidalgo](https://yyz2.discourse-cdn.com/flex034/user_avatar/community.tago.io/carolina.hidalgo/32/1602_2.png) [@carolina.hidalgo](https://community.tago.io/u/carolina.hidalgo)\
**Post date:** [January 28, 2026, 1:18pm UTC](https://community.tago.io/t/how-to-implement-firmware-updates-fuota-over-https/1841/2 "2026-01-28T13:18:19Z")

</div>



---

<div class="post-metadata">

**Author:** ![vitorfdl](https://yyz2.discourse-cdn.com/flex034/user_avatar/community.tago.io/vitorfdl/32/1728_2.png) [@vitorfdl](https://community.tago.io/u/vitorfdl)\
**Post date:** [February 27, 2026, 4:45pm UTC](https://community.tago.io/t/how-to-implement-firmware-updates-fuota-over-https/1841/3 "2026-02-27T16:45:13Z")

</div>


