1113  Sigfox Hands-On Lab

1113.1 Introduction

⏱️ ~25 min | ⭐⭐ Intermediate | 📋 P09.C11.U04

This hands-on chapter provides practical experience with Sigfox concepts through an interactive ESP32 simulation and Python implementations. You’ll learn how Sigfox handles message constraints, implements low-power operation, and manages daily message budgets.

NoteLearning Objectives

By the end of this lab, you will be able to:

  • Implement Sigfox’s 12-byte payload encoding strategies
  • Simulate daily message budget constraints (140 messages/day)
  • Measure and optimize power consumption patterns
  • Calculate battery life for different messaging scenarios
  • Build payload encoders/decoders in Python

1113.2 Prerequisites

Before this chapter, ensure you’ve completed:


1113.3 Hands-On Lab: Sigfox Communication Simulator

This interactive lab simulates Sigfox ultra-narrowband communication using an ESP32. You’ll learn how Sigfox handles severe message constraints, implements low-power operation, and manages daily message budgets.

NoteLab Objectives
  • Understand Sigfox’s 12-byte uplink payload limitation
  • Implement efficient payload encoding strategies
  • Simulate daily message budget constraints (140 messages/day)
  • Measure and optimize power consumption patterns
  • Visualize ultra-narrowband communication characteristics
  • Handle message transmission failures and retries

1113.3.1 Wokwi Simulation

TipSimulation Features

Hardware Components: - ESP32 microcontroller (simulating Sigfox module) - DHT22 temperature/humidity sensor - OLED display (128x64) for status information - Push button for manual message transmission - LED indicators for TX/RX/Error status

Sigfox Characteristics Simulated: - 12-byte maximum payload size - 140 messages per day limit - Ultra-narrowband modulation (100 bps) - Low power sleep modes - Message transmission timing - Downlink message windows (4 messages/day) - Coverage simulation with RSSI indication

Interactive Controls: - Button: Send immediate message (counts toward daily limit) - Serial Monitor: View detailed transmission logs - OLED: Real-time status and statistics

Learning Outcomes: - Optimize sensor data to fit 12-byte payload - Manage daily message budget strategically - Implement power-efficient transmission scheduling - Handle transmission failures gracefully - Understand ultra-narrowband trade-offs

ImportantLab Instructions
  1. Click “Start Simulation” in the Wokwi window above
  2. Observe the OLED display showing:
    • Current sensor readings (temperature, humidity, battery)
    • Messages sent today / daily limit (140)
    • Next scheduled transmission time
    • TX power state and RSSI signal strength
  3. Press the button to send an immediate message (watch payload encoding)
  4. Monitor Serial output for detailed logs:
    • Payload byte breakdown
    • Message transmission timing
    • Power consumption estimates
    • Success/failure indicators
  5. Watch LED indicators:
    • Blue LED: Message transmission in progress
    • Green LED: Successful transmission
    • Red LED: Transmission failure or daily limit exceeded
  6. Experiment with scenarios:
    • Observe how the 12-byte payload is efficiently packed
    • Watch the daily message counter increment
    • See what happens when approaching the 140 message limit
    • Notice the ultra-low power sleep periods between transmissions

1113.3.2 Wokwi Code

Copy and paste this code into the Wokwi editor:

diagram.json (Hardware Configuration):

{
  "version": 1,
  "author": "IoT Class - Sigfox Lab",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-esp32-devkit-v1", "id": "esp32", "top": 0, "left": 0, "attrs": {} },
    { "type": "wokwi-dht22", "id": "dht1", "top": -30, "left": 120, "attrs": {} },
    { "type": "wokwi-ssd1306", "id": "oled1", "top": -160, "left": -120, "attrs": { "i2cAddress": "0x3c" } },
    { "type": "wokwi-pushbutton", "id": "btn1", "top": 100, "left": -150, "attrs": { "color": "green" } },
    { "type": "wokwi-led", "id": "led_tx", "top": 180, "left": -140, "attrs": { "color": "blue" } },
    { "type": "wokwi-led", "id": "led_rx", "top": 180, "left": -100, "attrs": { "color": "green" } },
    { "type": "wokwi-led", "id": "led_err", "top": 180, "left": -60, "attrs": { "color": "red" } },
    { "type": "wokwi-resistor", "id": "r1", "top": 200, "left": -140, "attrs": { "value": "220" } },
    { "type": "wokwi-resistor", "id": "r2", "top": 200, "left": -100, "attrs": { "value": "220" } },
    { "type": "wokwi-resistor", "id": "r3", "top": 200, "left": -60, "attrs": { "value": "220" } }
  ],
  "connections": [
    [ "esp32:TX", "$serialMonitor:RX", "", [] ],
    [ "esp32:RX", "$serialMonitor:TX", "", [] ],
    [ "dht1:VCC", "esp32:3V3", "red", [ "v0" ] ],
    [ "dht1:GND", "esp32:GND.1", "black", [ "v0" ] ],
    [ "dht1:SDA", "esp32:D15", "green", [ "v0" ] ],
    [ "oled1:VCC", "esp32:3V3", "red", [ "v0" ] ],
    [ "oled1:GND", "esp32:GND.2", "black", [ "v0" ] ],
    [ "oled1:SCL", "esp32:D22", "yellow", [ "v0" ] ],
    [ "oled1:SDA", "esp32:D21", "green", [ "v0" ] ],
    [ "btn1:1.l", "esp32:D4", "blue", [ "v0" ] ],
    [ "btn1:2.l", "esp32:GND.1", "black", [ "v0" ] ],
    [ "led_tx:A", "esp32:D25", "blue", [ "v0" ] ],
    [ "led_tx:C", "r1:1", "blue", [ "v0" ] ],
    [ "r1:2", "esp32:GND.1", "black", [ "v0" ] ],
    [ "led_rx:A", "esp32:D26", "green", [ "v0" ] ],
    [ "led_rx:C", "r2:1", "green", [ "v0" ] ],
    [ "r2:2", "esp32:GND.1", "black", [ "v0" ] ],
    [ "led_err:A", "esp32:D27", "red", [ "v0" ] ],
    [ "led_err:C", "r3:1", "red", [ "v0" ] ],
    [ "r3:2", "esp32:GND.1", "black", [ "v0" ] ]
  ]
}

sketch.ino (Main Code):

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

// Hardware Pin Definitions
#define DHT_PIN 15
#define BUTTON_PIN 4
#define LED_TX_PIN 25
#define LED_RX_PIN 26
#define LED_ERR_PIN 27
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_RESET -1

// Sigfox Constants
#define SIGFOX_MAX_PAYLOAD 12        // 12 bytes maximum
#define SIGFOX_DAILY_LIMIT 140       // 140 messages per day uplink
#define SIGFOX_DOWNLINK_LIMIT 4      // 4 messages per day downlink
#define SIGFOX_TX_TIME_MS 2000       // ~2 seconds transmission time (100 bps)
#define SIGFOX_TX_CURRENT_MA 40      // 40mA during transmission
#define SIGFOX_SLEEP_CURRENT_UA 1    // 1uA in deep sleep
#define SIGFOX_FREQ_EU 868           // MHz (Europe)
#define SIGFOX_BANDWIDTH 100         // Hz ultra-narrowband
#define SIGFOX_DATARATE 100          // bps

// Simulation Parameters
#define MESSAGE_INTERVAL_DEMO_MS 15000  // 15 seconds for simulation
#define BATTERY_CAPACITY_MAH 2000    // 2000mAh battery
#define MIN_RSSI -142                // Sigfox receiver sensitivity
#define MAX_RSSI -110                // Strong signal

// Global Objects
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
DHT dht(DHT_PIN, DHT22);

// Sigfox State
struct SigfoxState {
  uint16_t messagesSentToday;
  uint16_t messagesFailedToday;
  uint16_t downlinksReceivedToday;
  uint32_t nextTransmissionMs;
  float batteryVoltage;
  float totalEnergyUsed_mAh;
  int8_t lastRSSI;
  bool coverageAvailable;
  uint32_t messageCounter;
} sigfoxState;

// Sensor Data Structure (packed into 12 bytes)
struct __attribute__((packed)) SigfoxPayload {
  int16_t temperature;     // 2 bytes: temp * 100
  uint16_t humidity;       // 2 bytes: humidity * 100
  uint16_t battery;        // 2 bytes: voltage * 1000
  uint8_t messageCount;    // 1 byte: daily message counter
  uint8_t flags;           // 1 byte: status flags
  uint32_t timestamp;      // 4 bytes: seconds since boot
  // Total: 12 bytes exactly
};

// Status Flags
enum StatusFlags {
  FLAG_LOW_BATTERY = 0x01,
  FLAG_COVERAGE_OK = 0x02,
  FLAG_TEMP_ALERT = 0x04,
  FLAG_HUMIDITY_ALERT = 0x08,
  FLAG_BUTTON_PRESSED = 0x10
};

// Current Sensor Values
float currentTemp = 0.0;
float currentHumidity = 0.0;
unsigned long lastButtonPress = 0;
const unsigned long debounceDelay = 300;

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("\n\n======================================");
  Serial.println("   SIGFOX COMMUNICATION SIMULATOR");
  Serial.println("======================================\n");

  initHardware();

  // Initialize Sigfox state
  sigfoxState.messagesSentToday = 0;
  sigfoxState.messagesFailedToday = 0;
  sigfoxState.downlinksReceivedToday = 0;
  sigfoxState.batteryVoltage = 3.7;
  sigfoxState.totalEnergyUsed_mAh = 0.0;
  sigfoxState.coverageAvailable = true;
  sigfoxState.nextTransmissionMs = millis() + MESSAGE_INTERVAL_DEMO_MS;
  sigfoxState.messageCounter = 0;
  sigfoxState.lastRSSI = -120;

  Serial.println("Sigfox module initialized");
  Serial.printf("Region: Europe (%d MHz)\n", SIGFOX_FREQ_EU);
  Serial.printf("Bandwidth: %d Hz (ultra-narrowband)\n", SIGFOX_BANDWIDTH);
  Serial.printf("Daily limit: %d uplink / %d downlink\n",
                SIGFOX_DAILY_LIMIT, SIGFOX_DOWNLINK_LIMIT);
  Serial.printf("Max payload: %d bytes\n\n", SIGFOX_MAX_PAYLOAD);

  updateDisplay();
}

void loop() {
  unsigned long currentMs = millis();

  checkButton();
  simulateCoverage();
  updateSensors();

  if (currentMs >= sigfoxState.nextTransmissionMs) {
    transmitSigfoxMessage(false);
    sigfoxState.nextTransmissionMs = currentMs + MESSAGE_INTERVAL_DEMO_MS;
  }

  updateDisplay();

  // Reset daily counters (simulated: every 30 messages)
  if (sigfoxState.messagesSentToday >= 30) {
    resetDailyCounters();
  }

  delay(100);
}

void initHardware() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_TX_PIN, OUTPUT);
  pinMode(LED_RX_PIN, OUTPUT);
  pinMode(LED_ERR_PIN, OUTPUT);

  Wire.begin(21, 22);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("ERROR: OLED initialization failed!");
    while (1) delay(100);
  }

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Sigfox Module");
  display.println("Initializing...");
  display.display();

  dht.begin();
  delay(2000);
}

void updateSensors() {
  currentTemp = dht.readTemperature();
  currentHumidity = dht.readHumidity();

  if (isnan(currentTemp) || isnan(currentHumidity)) {
    currentTemp = 22.5 + (random(-50, 50) / 10.0);
    currentHumidity = 65.0 + (random(-100, 100) / 10.0);
  }

  sigfoxState.batteryVoltage -= 0.0001;
  if (sigfoxState.batteryVoltage < 3.0) {
    sigfoxState.batteryVoltage = 3.7;
  }
}

void transmitSigfoxMessage(bool manualTrigger) {
  if (sigfoxState.messagesSentToday >= SIGFOX_DAILY_LIMIT) {
    Serial.println("\nDAILY MESSAGE LIMIT REACHED!");
    Serial.printf("Already sent %d/%d messages today\n",
                  sigfoxState.messagesSentToday, SIGFOX_DAILY_LIMIT);
    blinkLED(LED_ERR_PIN, 1000);
    return;
  }

  if (!sigfoxState.coverageAvailable) {
    Serial.println("\nNO SIGFOX COVERAGE DETECTED");
    sigfoxState.messagesFailedToday++;
    blinkLED(LED_ERR_PIN, 500);
    return;
  }

  Serial.println("\n=== SIGFOX TRANSMISSION ===");

  sigfoxState.messageCounter++;

  // Prepare 12-byte payload
  SigfoxPayload payload;
  payload.temperature = (int16_t)(currentTemp * 100);
  payload.humidity = (uint16_t)(currentHumidity * 100);
  payload.battery = (uint16_t)(sigfoxState.batteryVoltage * 1000);
  payload.messageCount = sigfoxState.messagesSentToday + 1;
  payload.timestamp = millis() / 1000;

  payload.flags = 0;
  if (sigfoxState.batteryVoltage < 3.3) payload.flags |= FLAG_LOW_BATTERY;
  if (sigfoxState.coverageAvailable) payload.flags |= FLAG_COVERAGE_OK;
  if (currentTemp > 30.0 || currentTemp < 10.0) payload.flags |= FLAG_TEMP_ALERT;
  if (currentHumidity > 80.0 || currentHumidity < 30.0) payload.flags |= FLAG_HUMIDITY_ALERT;
  if (manualTrigger) payload.flags |= FLAG_BUTTON_PRESSED;

  printPayloadDetails(payload);

  Serial.printf("\nTransmitting via Sigfox...\n");
  Serial.printf("Frequency: %d MHz | Bandwidth: %d Hz | Data rate: %d bps\n",
                SIGFOX_FREQ_EU, SIGFOX_BANDWIDTH, SIGFOX_DATARATE);

  blinkLED(LED_TX_PIN, SIGFOX_TX_TIME_MS);

  float txEnergy_mAh = (SIGFOX_TX_CURRENT_MA * SIGFOX_TX_TIME_MS) / 3600000.0;
  sigfoxState.totalEnergyUsed_mAh += txEnergy_mAh;

  Serial.printf("\nEnergy: %.4f mAh (Total: %.2f mAh)\n",
                txEnergy_mAh, sigfoxState.totalEnergyUsed_mAh);

  bool txSuccess = (random(100) < 95);

  if (txSuccess) {
    Serial.println("Transmission successful!");
    sigfoxState.messagesSentToday++;
    blinkLED(LED_RX_PIN, 500);
  } else {
    Serial.println("Transmission failed!");
    sigfoxState.messagesFailedToday++;
    blinkLED(LED_ERR_PIN, 500);
  }

  float estimatedLifeDays = calculateBatteryLife();
  Serial.printf("Battery life estimate: %.1f years\n", estimatedLifeDays / 365.0);
  Serial.printf("Messages today: %d/%d (%.1f%%)\n",
                sigfoxState.messagesSentToday, SIGFOX_DAILY_LIMIT,
                (sigfoxState.messagesSentToday * 100.0) / SIGFOX_DAILY_LIMIT);
}

void printPayloadDetails(SigfoxPayload &payload) {
  Serial.println("\nPayload (12 bytes):");
  Serial.printf("  Temperature: %.2fC (encoded: %d)\n", currentTemp, payload.temperature);
  Serial.printf("  Humidity:    %.2f%% (encoded: %d)\n", currentHumidity, payload.humidity);
  Serial.printf("  Battery:     %.2fV (encoded: %d)\n", sigfoxState.batteryVoltage, payload.battery);
  Serial.printf("  Msg Count:   %d\n", payload.messageCount);
  Serial.printf("  Flags:       0x%02X\n", payload.flags);
  Serial.printf("  Timestamp:   %lu sec\n", payload.timestamp);

  Serial.printf("  Raw hex: ");
  uint8_t* bytes = (uint8_t*)&payload;
  for (int i = 0; i < sizeof(SigfoxPayload); i++) {
    Serial.printf("%02X ", bytes[i]);
  }
  Serial.printf("\n  Size: %d bytes (%.1f%% utilization)\n",
                sizeof(SigfoxPayload),
                (sizeof(SigfoxPayload) * 100.0) / SIGFOX_MAX_PAYLOAD);
}

void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);

  display.println("SIGFOX MODULE");
  display.drawLine(0, 10, OLED_WIDTH, 10, SSD1306_WHITE);

  display.setCursor(0, 14);
  display.printf("Temp: %.1fC", currentTemp);
  display.setCursor(0, 24);
  display.printf("Hum:  %.1f%%", currentHumidity);
  display.setCursor(0, 34);
  display.printf("Batt: %.2fV", sigfoxState.batteryVoltage);

  display.setCursor(0, 44);
  display.printf("Msgs: %d/%d", sigfoxState.messagesSentToday, SIGFOX_DAILY_LIMIT);

  display.setCursor(0, 54);
  if (sigfoxState.coverageAvailable) {
    display.printf("RSSI:%ddBm OK", sigfoxState.lastRSSI);
  } else {
    display.print("NO COVERAGE!");
  }

  display.display();
}

void simulateCoverage() {
  sigfoxState.lastRSSI = random(MIN_RSSI - 10, MAX_RSSI);
  sigfoxState.coverageAvailable = (sigfoxState.lastRSSI >= MIN_RSSI);

  if (random(1000) < 10) {
    sigfoxState.coverageAvailable = false;
    sigfoxState.lastRSSI = MIN_RSSI - 15;
  }
}

void checkButton() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    if (millis() - lastButtonPress > debounceDelay) {
      lastButtonPress = millis();
      Serial.println("\n>>> Manual transmission triggered <<<");
      transmitSigfoxMessage(true);
    }
  }
}

void blinkLED(int pin, int duration) {
  digitalWrite(pin, HIGH);
  delay(duration);
  digitalWrite(pin, LOW);
}

void resetDailyCounters() {
  Serial.println("\n=== DAILY COUNTER RESET ===");
  Serial.printf("Yesterday: %d sent, %d failed\n",
                sigfoxState.messagesSentToday,
                sigfoxState.messagesFailedToday);
  sigfoxState.messagesSentToday = 0;
  sigfoxState.messagesFailedToday = 0;
  sigfoxState.downlinksReceivedToday = 0;
}

float calculateBatteryLife() {
  float messagesPerDay = 144;
  if (messagesPerDay > SIGFOX_DAILY_LIMIT) {
    messagesPerDay = SIGFOX_DAILY_LIMIT;
  }

  float txEnergy_mAh = (SIGFOX_TX_CURRENT_MA * SIGFOX_TX_TIME_MS) / 3600000.0;
  float dailyTxEnergy_mAh = txEnergy_mAh * messagesPerDay;
  float sleepHoursPerDay = 24.0 - (messagesPerDay * SIGFOX_TX_TIME_MS / 3600000.0);
  float dailySleepEnergy_mAh = (SIGFOX_SLEEP_CURRENT_UA / 1000.0) * sleepHoursPerDay;
  float dailyTotalEnergy_mAh = dailyTxEnergy_mAh + dailySleepEnergy_mAh;

  return BATTERY_CAPACITY_MAH / dailyTotalEnergy_mAh;
}

Key Sigfox Characteristics Implemented:

  1. 12-Byte Payload Constraint:
    • SigfoxPayload struct is exactly 12 bytes
    • Uses efficient encoding (int16, uint16) instead of float
    • Bit flags pack multiple boolean values into single byte
    • Temperature scaled by 100 (22.5C = 2250)
  2. Daily Message Limit (140):
    • Counter tracks messages sent today
    • Blocks transmission when limit reached
    • Resets at “midnight” (simulated every 30 messages)
  3. Ultra-Narrowband Characteristics:
    • 100 Hz bandwidth (vs. 125 kHz for LoRa)
    • 100 bps data rate (extremely slow)
    • 2-second transmission time simulated
    • High receiver sensitivity (-142 dBm)
  4. Low Power Operation:
    • 40mA during 2-second transmission
    • 1uA sleep current between transmissions
    • Energy tracking shows mAh consumption
    • Battery life calculation (10+ years possible)
  5. Coverage Simulation:
    • RSSI varies between -142 dBm (minimum) and -110 dBm (strong)
    • Coverage loss simulated randomly
    • Transmission blocked when RSSI below sensitivity

Compare with LoRaWAN: - Sigfox: 12 bytes, 140 msgs/day, operator network, ultra-simple - LoRaWAN: 51-242 bytes, unlimited msgs, private network, more complex

  1. How much of the daily message budget is used by sending data every 10 minutes?
    • Every 10 minutes = 6 messages/hour = 144 messages/day
    • This exceeds the 140 message limit!
    • Solution: Send every 10.3 minutes = 140 messages/day exactly
  2. Why is Sigfox transmission so slow (2 seconds for 12 bytes)?
    • Ultra-narrowband: 100 Hz bandwidth = 100 bps data rate
    • 12 bytes = 96 bits
    • At 100 bps: 96 bits / 100 bps = 0.96 seconds (plus overhead)
    • Trade-off: Slow speed enables extreme range and penetration
  3. What’s the theoretical battery life with 144 messages/day?
    • TX energy: 0.022 mAh per message
    • Daily TX energy: 0.022 x 140 = 3.08 mAh
    • Sleep energy: 0.024 mAh/day (1uA x 24h)
    • Total: 3.1 mAh/day
    • With 2000 mAh battery: 645 days = 1.8 years
    • With fewer messages (24/day): 11+ years possible
  4. When would you choose Sigfox over LoRaWAN?
    • Use Sigfox when:
      • Simplest possible device design required
      • No infrastructure investment available
      • < 140 messages/day is sufficient
      • Small payloads (< 12 bytes)
      • Sigfox coverage exists in deployment area
    • Use LoRaWAN when:
      • Need larger payloads
      • Need more frequent messages
      • Want control over network infrastructure
      • Need lower latency
      • Data privacy is critical

1113.4 Python Implementations

1113.4.1 Sigfox Message Capacity and Battery Life Calculator

This Python implementation calculates whether your application fits Sigfox constraints and estimates battery life:

#!/usr/bin/env python3
"""
Sigfox Deployment Analysis Tool
Calculates message constraints and battery life
"""

# Sigfox Constants
SIGFOX_MAX_PAYLOAD_BYTES = 12
SIGFOX_DAILY_UPLINK_LIMIT = 140
SIGFOX_DAILY_DOWNLINK_LIMIT = 4
SIGFOX_TX_CURRENT_MA = 40
SIGFOX_TX_DURATION_SEC = 6  # Includes triple transmission
SIGFOX_RX_CURRENT_MA = 50
SIGFOX_RX_DURATION_SEC = 25
SIGFOX_SLEEP_CURRENT_UA = 1

def analyze_sigfox_deployment(
    payload_bytes: int,
    messages_per_day: int,
    downlinks_per_day: int = 0,
    battery_mah: int = 2000
) -> dict:
    """
    Analyze Sigfox deployment feasibility and battery life.

    Args:
        payload_bytes: Size of each message payload
        messages_per_day: Number of uplink messages per day
        downlinks_per_day: Number of downlink messages per day
        battery_mah: Battery capacity in mAh

    Returns:
        Dictionary with analysis results
    """
    results = {
        'payload_valid': payload_bytes <= SIGFOX_MAX_PAYLOAD_BYTES,
        'uplink_valid': messages_per_day <= SIGFOX_DAILY_UPLINK_LIMIT,
        'downlink_valid': downlinks_per_day <= SIGFOX_DAILY_DOWNLINK_LIMIT,
        'overall_valid': True,
        'errors': []
    }

    # Validate constraints
    if not results['payload_valid']:
        results['errors'].append(
            f"Payload {payload_bytes} bytes exceeds {SIGFOX_MAX_PAYLOAD_BYTES} byte limit"
        )
        results['overall_valid'] = False

    if not results['uplink_valid']:
        results['errors'].append(
            f"Messages {messages_per_day}/day exceeds {SIGFOX_DAILY_UPLINK_LIMIT}/day limit"
        )
        results['overall_valid'] = False

    if not results['downlink_valid']:
        results['errors'].append(
            f"Downlinks {downlinks_per_day}/day exceeds {SIGFOX_DAILY_DOWNLINK_LIMIT}/day limit"
        )
        results['overall_valid'] = False

    # Calculate energy consumption
    tx_energy_per_msg = (SIGFOX_TX_CURRENT_MA * SIGFOX_TX_DURATION_SEC) / 3600  # mAh
    rx_energy_per_msg = (SIGFOX_RX_CURRENT_MA * SIGFOX_RX_DURATION_SEC) / 3600  # mAh

    daily_tx_energy = tx_energy_per_msg * messages_per_day
    daily_rx_energy = rx_energy_per_msg * downlinks_per_day

    # Sleep energy (24h minus active time)
    active_hours = (messages_per_day * SIGFOX_TX_DURATION_SEC +
                   downlinks_per_day * SIGFOX_RX_DURATION_SEC) / 3600
    sleep_hours = 24 - active_hours
    daily_sleep_energy = (SIGFOX_SLEEP_CURRENT_UA / 1000) * sleep_hours

    daily_total_energy = daily_tx_energy + daily_rx_energy + daily_sleep_energy

    # Battery life calculation
    battery_life_days = battery_mah / daily_total_energy if daily_total_energy > 0 else float('inf')
    battery_life_years = battery_life_days / 365

    results['energy'] = {
        'tx_per_message_mah': round(tx_energy_per_msg, 4),
        'rx_per_downlink_mah': round(rx_energy_per_msg, 4),
        'daily_tx_mah': round(daily_tx_energy, 4),
        'daily_rx_mah': round(daily_rx_energy, 4),
        'daily_sleep_mah': round(daily_sleep_energy, 4),
        'daily_total_mah': round(daily_total_energy, 4),
        'battery_life_days': round(battery_life_days, 1),
        'battery_life_years': round(battery_life_years, 1)
    }

    # Usage statistics
    results['usage'] = {
        'uplink_utilization': round(messages_per_day / SIGFOX_DAILY_UPLINK_LIMIT * 100, 1),
        'downlink_utilization': round(downlinks_per_day / SIGFOX_DAILY_DOWNLINK_LIMIT * 100, 1),
        'payload_utilization': round(payload_bytes / SIGFOX_MAX_PAYLOAD_BYTES * 100, 1),
        'messages_remaining': SIGFOX_DAILY_UPLINK_LIMIT - messages_per_day,
        'downlinks_remaining': SIGFOX_DAILY_DOWNLINK_LIMIT - downlinks_per_day,
        'bytes_remaining': SIGFOX_MAX_PAYLOAD_BYTES - payload_bytes
    }

    return results


def print_analysis(results: dict, scenario_name: str = ""):
    """Pretty print analysis results."""
    print("=" * 60)
    if scenario_name:
        print(f"Scenario: {scenario_name}")
        print("-" * 60)

    # Validation
    print("\nConstraint Validation:")
    status = "VALID" if results['overall_valid'] else "INVALID"
    print(f"  Overall: {status}")

    if results['errors']:
        for error in results['errors']:
            print(f"  ERROR: {error}")

    # Usage
    print("\nUsage Statistics:")
    print(f"  Uplink utilization:   {results['usage']['uplink_utilization']}%")
    print(f"  Downlink utilization: {results['usage']['downlink_utilization']}%")
    print(f"  Payload utilization:  {results['usage']['payload_utilization']}%")
    print(f"  Messages remaining:   {results['usage']['messages_remaining']}/day")
    print(f"  Bytes remaining:      {results['usage']['bytes_remaining']} bytes")

    # Energy
    print("\nEnergy Analysis:")
    print(f"  Daily TX energy:    {results['energy']['daily_tx_mah']} mAh")
    print(f"  Daily RX energy:    {results['energy']['daily_rx_mah']} mAh")
    print(f"  Daily sleep energy: {results['energy']['daily_sleep_mah']} mAh")
    print(f"  Daily total:        {results['energy']['daily_total_mah']} mAh")
    print(f"\n  Battery Life: {results['energy']['battery_life_years']} years ({results['energy']['battery_life_days']} days)")

    print("=" * 60)


# Example usage
if __name__ == "__main__":
    # Scenario 1: Environmental sensor (hourly readings)
    results1 = analyze_sigfox_deployment(
        payload_bytes=10,
        messages_per_day=24,
        downlinks_per_day=0,
        battery_mah=2000
    )
    print_analysis(results1, "Environmental Sensor (Hourly)")

    # Scenario 2: Asset tracker (every 15 minutes)
    results2 = analyze_sigfox_deployment(
        payload_bytes=12,
        messages_per_day=96,
        downlinks_per_day=1,
        battery_mah=3000
    )
    print_analysis(results2, "Asset Tracker (15-min intervals)")

    # Scenario 3: High-frequency monitoring (exceeds limit)
    results3 = analyze_sigfox_deployment(
        payload_bytes=8,
        messages_per_day=200,  # Exceeds 140 limit!
        downlinks_per_day=0,
        battery_mah=2000
    )
    print_analysis(results3, "High-Frequency (WILL FAIL)")

Example Output:

============================================================
Scenario: Environmental Sensor (Hourly)
------------------------------------------------------------

Constraint Validation:
  Overall: VALID

Usage Statistics:
  Uplink utilization:   17.1%
  Downlink utilization: 0.0%
  Payload utilization:  83.3%
  Messages remaining:   116/day
  Bytes remaining:      2 bytes

Energy Analysis:
  Daily TX energy:    1.6 mAh
  Daily RX energy:    0.0 mAh
  Daily sleep energy: 0.024 mAh
  Daily total:        1.624 mAh

  Battery Life: 3.4 years (1231 days)
============================================================

1113.4.2 Sigfox Payload Encoder/Decoder

#!/usr/bin/env python3
"""
Sigfox Payload Encoder/Decoder
Demonstrates efficient encoding for 12-byte payloads
"""
import struct
from dataclasses import dataclass
from typing import Tuple

@dataclass
class EnvironmentalPayload:
    """Environmental sensor payload (12 bytes)"""
    temperature: float     # -327.67 to +327.67 C (0.01 resolution)
    humidity: float        # 0-100% (0.01 resolution)
    pressure: float        # 500-1155 hPa (0.01 resolution)
    battery: float         # 0-5.1V (0.02 resolution)
    flags: int             # 8 boolean flags packed into 1 byte

    @classmethod
    def encode(cls, temp: float, humidity: float, pressure: float,
               battery: float, flags: int) -> bytes:
        """Encode sensor data to 12-byte payload."""
        # Scale values to integers
        temp_int = int(temp * 100)          # 2 bytes, signed
        humidity_int = int(humidity * 100)  # 2 bytes, unsigned
        pressure_int = int((pressure - 500) * 100)  # 2 bytes, offset encoding
        battery_int = int(battery * 50)     # 1 byte (0-255 = 0-5.1V)

        # Pack into bytes (little-endian for consistency)
        payload = struct.pack('<hhHHBBxx',  # 12 bytes total
                              temp_int,
                              humidity_int,
                              pressure_int,
                              0,  # Reserved
                              battery_int,
                              flags)

        return payload[:12]  # Ensure exactly 12 bytes

    @classmethod
    def decode(cls, payload: bytes) -> 'EnvironmentalPayload':
        """Decode 12-byte payload to sensor data."""
        if len(payload) != 12:
            raise ValueError(f"Payload must be 12 bytes, got {len(payload)}")

        temp_int, humidity_int, pressure_int, _, battery_int, flags = \
            struct.unpack('<hhHHBBxx', payload)

        return cls(
            temperature=temp_int / 100,
            humidity=humidity_int / 100,
            pressure=(pressure_int / 100) + 500,
            battery=battery_int / 50,
            flags=flags
        )

    def to_hex(self) -> str:
        """Convert to hexadecimal string representation."""
        payload = self.encode(
            self.temperature, self.humidity,
            self.pressure, self.battery, self.flags
        )
        return payload.hex().upper()


@dataclass
class GPSPayload:
    """GPS tracker payload (12 bytes)"""
    latitude: float    # -90 to +90 degrees
    longitude: float   # -180 to +180 degrees
    speed: int         # 0-255 km/h
    heading: int       # 0-359 degrees (mapped to 0-255)
    battery: int       # 0-100%
    flags: int         # 8 boolean flags

    @classmethod
    def encode(cls, lat: float, lon: float, speed: int,
               heading: int, battery: int, flags: int) -> bytes:
        """Encode GPS data to 12-byte payload."""
        # Latitude: 3 bytes, ~1m resolution
        lat_int = int((lat + 90) * 93206)  # 0-180 deg -> 0-16777215 (24 bits)

        # Longitude: 3 bytes, ~1m resolution
        lon_int = int((lon + 180) * 46603)  # 0-360 deg -> 0-16777215 (24 bits)

        # Heading: map 0-360 to 0-255
        heading_byte = int(heading * 255 / 360)

        # Pack: 3 + 3 + 1 + 1 + 1 + 1 + 2(reserved) = 12 bytes
        payload = struct.pack('<3s3sBBBBxx',
                              lat_int.to_bytes(3, 'little'),
                              lon_int.to_bytes(3, 'little'),
                              min(speed, 255),
                              heading_byte,
                              min(battery, 100),
                              flags)

        return payload

    @classmethod
    def decode(cls, payload: bytes) -> 'GPSPayload':
        """Decode 12-byte payload to GPS data."""
        if len(payload) != 12:
            raise ValueError(f"Payload must be 12 bytes, got {len(payload)}")

        lat_bytes = payload[0:3]
        lon_bytes = payload[3:6]
        speed = payload[6]
        heading_byte = payload[7]
        battery = payload[8]
        flags = payload[9]

        lat_int = int.from_bytes(lat_bytes, 'little')
        lon_int = int.from_bytes(lon_bytes, 'little')

        return cls(
            latitude=(lat_int / 93206) - 90,
            longitude=(lon_int / 46603) - 180,
            speed=speed,
            heading=int(heading_byte * 360 / 255),
            battery=battery,
            flags=flags
        )


# Example usage
if __name__ == "__main__":
    print("=" * 50)
    print("Sigfox Payload Encoding Examples")
    print("=" * 50)

    # Environmental sensor example
    print("\n1. Environmental Sensor Payload:")
    env_payload = EnvironmentalPayload.encode(
        temp=22.5,
        humidity=65.0,
        pressure=1013.25,
        battery=3.7,
        flags=0b00000011  # Two flags set
    )
    print(f"   Hex: {env_payload.hex().upper()}")
    print(f"   Size: {len(env_payload)} bytes")

    # Decode and verify
    decoded = EnvironmentalPayload.decode(env_payload)
    print(f"   Decoded: T={decoded.temperature}C, H={decoded.humidity}%")

    # GPS tracker example
    print("\n2. GPS Tracker Payload:")
    gps_payload = GPSPayload.encode(
        lat=51.5074,
        lon=-0.1278,
        speed=45,
        heading=90,
        battery=78,
        flags=0b00000001
    )
    print(f"   Hex: {gps_payload.hex().upper()}")
    print(f"   Size: {len(gps_payload)} bytes")

    # Decode and verify
    gps_decoded = GPSPayload.decode(gps_payload)
    print(f"   Decoded: Lat={gps_decoded.latitude:.4f}, Lon={gps_decoded.longitude:.4f}")
    print(f"   Speed={gps_decoded.speed}km/h, Heading={gps_decoded.heading}deg")

1113.5 Knowledge Check


1113.6 Summary

In this hands-on chapter, you learned:

Wokwi Simulation:

  • How to encode sensor data into Sigfox’s 12-byte payload
  • Daily message budget management (140 messages/day)
  • Power consumption tracking and battery life estimation
  • Coverage simulation with RSSI indication

Python Implementations:

  • Sigfox deployment feasibility calculator
  • Energy consumption and battery life estimation
  • Efficient payload encoding/decoding strategies
  • Scaled integer encoding for compact data representation

Key Takeaways:

  • Sigfox’s 12-byte limit requires careful payload design
  • Use scaled integers instead of floats to maximize data density
  • Pack multiple booleans into bit flags
  • Always calculate battery life before deployment
  • Test coverage thoroughly before production rollout

1113.7 What’s Next

Complete your Sigfox learning: