Iot

Escopo: transversal, com padrões de domínio IoT em exemplos MicroPython 1.28.

Sistemas IoT (Internet of Things, Internet das Coisas) têm restrições que não existem em servidores: memória em kilobytes, CPU (Central Processing Unit, unidade central de processamento) em megahertz, sem sistema operacional completo, alimentação por bateria e conectividade instável. As boas práticas derivam dessas restrições.

Conceitos fundamentais

ConceitoO que é
debounce (filtragem de ruído)Ignorar leituras redundantes de sensor durante uma janela de tempo após a primeira leitura
FSM (Finite State Machine, Máquina de Estados Finitos)Modelo de controle onde o sistema está sempre em um estado definido; transições são explícitas
watchdog (cão de guarda)Timer de hardware que reinicia o dispositivo se o firmware travar
idempotency (idempotência)Enviar o mesmo alerta múltiplas vezes não causa efeito duplicado no servidor
polling (varredura)Leitura periódica de um sensor; alternativa a interrupções quando GPIO não suporta IRQ
IRQ (Interrupt Request, Requisição de Interrupção)Sinal de hardware que interrompe o loop principal para tratar um evento
machineMódulo MicroPython de acesso ao hardware: GPIO, I2C, SPI, UART, Timer, WDT

Naming de sensores e atuadores

Nomes refletem o papel do sensor no domínio, não o tipo de hardware.

❌ Ruim: nome técnico sem contexto de domínio
pin0 = machine.Pin(0, machine.Pin.IN)
adc1 = machine.ADC(1)
pwm_out = machine.PWM(machine.Pin(2))
✅ Bom: nome de domínio revela intenção
door_sensor = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)
temperature_adc = machine.ADC(1)
fan_pwm = machine.PWM(machine.Pin(2), freq=1000)

Debounce: filtragem de ruído de botão e sensor

Sensores físicos produzem leituras ruidosas na transição. Ignore leituras dentro da janela de tempo após a primeira detecção.

❌ Ruim: sem debounce, evento disparado múltiplas vezes
import machine

button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)

def on_press(pin):
    print("button pressed")  # dispara 5–50 vezes por toque físico

button.irq(trigger=machine.Pin.IRQ_FALLING, handler=on_press)
✅ Bom: debounce por timestamp
import machine
import utime

DEBOUNCE_MS = 200

button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
last_press_ms = 0

def on_button_press(pin):
    global last_press_ms

    now = utime.ticks_ms()
    elapsed = utime.ticks_diff(now, last_press_ms)

    if elapsed < DEBOUNCE_MS:
        return

    last_press_ms = now
    handle_door_open()

button.irq(trigger=machine.Pin.IRQ_FALLING, handler=on_button_press)

def handle_door_open():
    print("door opened")

Máquina de estados: FSM

Modele o comportamento do dispositivo como estados explícitos. Evite flags booleanos soltos.

❌ Ruim: flags soltos sem estado explícito
is_door_open = False
is_alarm_active = False
is_waiting = False

# lógica espalhada em if-else sem estrutura
if is_door_open and not is_alarm_active:
    is_alarm_active = True
    is_waiting = False
✅ Bom: FSM com estados nomeados e transições explícitas
import utime

IDLE = "idle"
DOOR_OPEN = "door_open"
ALARM_ACTIVE = "alarm_active"
ALARM_SILENCED = "alarm_silenced"

ALARM_DELAY_MS = 30_000

state = IDLE
door_open_at = 0

def transition_to(new_state):
    global state
    print(f"state: {state} -> {new_state}")
    state = new_state

def on_door_opened():
    global door_open_at
    if state == IDLE:
        door_open_at = utime.ticks_ms()
        transition_to(DOOR_OPEN)

def on_door_closed():
    if state in (DOOR_OPEN, ALARM_SILENCED):
        transition_to(IDLE)

def on_silence_button():
    if state == ALARM_ACTIVE:
        transition_to(ALARM_SILENCED)

def tick():
    if state == DOOR_OPEN:
        elapsed = utime.ticks_diff(utime.ticks_ms(), door_open_at)
        if elapsed >= ALARM_DELAY_MS:
            transition_to(ALARM_ACTIVE)
            trigger_alarm()

Alertas idempotentes

Envie alertas com identificador único. O servidor ignora duplicatas. Evite re-enviar o mesmo alerta enquanto a condição não mudar.

❌ Ruim: alerta reenviado a cada tick enquanto condição persiste
import urequests

def check_temperature(temp_celsius):
    if temp_celsius > 80:
        urequests.post("http://server/alert", json={"type": "overheating", "value": temp_celsius})
        # reenviado a cada segundo enquanto temperatura permanecer alta
✅ Bom: alerta enviado uma vez por evento, com ID único
import urequests
import ubinascii
import os

alert_sent = False

def check_temperature(temp_celsius):
    global alert_sent

    if temp_celsius > 80 and not alert_sent:
        alert_id = ubinascii.hexlify(os.urandom(8)).decode()

        payload = {
            "alert_id": alert_id,
            "type": "overheating",
            "value": temp_celsius,
        }

        try:
            urequests.post("http://server/alert", json=payload).close()
            alert_sent = True
        except Exception as error:
            print(f"alert failed: {error}")

    elif temp_celsius <= 80 and alert_sent:
        alert_sent = False  # reseta quando condição normaliza

Watchdog: recuperação de travamento

O Watchdog reinicia o dispositivo se o loop principal parar de alimentá-lo. Útil para recuperação automática de travamentos ou deadlocks de rede.

❌ Ruim: watchdog nunca alimentado, ou ausente
import machine

# sem watchdog: travamento de rede deixa o dispositivo parado para sempre

def main_loop():
    while True:
        data = read_sensor()
        send_data(data)  # pode travar indefinidamente em timeout de rede
✅ Bom: watchdog alimentado a cada iteração do loop
import machine
import utime

WATCHDOG_TIMEOUT_MS = 8000

wdt = machine.WDT(timeout=WATCHDOG_TIMEOUT_MS)

def main_loop():
    while True:
        wdt.feed()  # reseta o timer do watchdog

        try:
            data = read_sensor()
            send_data(data)
        except Exception as error:
            print(f"loop error: {error}")
            # watchdog reinicia o device se o erro persistir por 8s

        utime.sleep_ms(1000)

Polling vs IRQ

Use IRQ para eventos rápidos (botão, borda de sinal). Use polling para leituras periódicas de sensores analógicos ou quando o hardware não suporta interrupção.

✅ Bom: polling periódico com sleep para sensores analógicos
import machine
import utime

temperature_adc = machine.ADC(26)  # GPIO26 no Pico: ADC0
READ_INTERVAL_MS = 5_000

def read_temperature_celsius() -> float:
    raw = temperature_adc.read_u16()
    voltage = raw * 3.3 / 65535
    temperature = 27 - (voltage - 0.706) / 0.001721  # fórmula do sensor interno RP2040

    return temperature

def main_loop():
    while True:
        temperature = read_temperature_celsius()

        print(f"temperature: {temperature:.1f}°C")

        utime.sleep_ms(READ_INTERVAL_MS)

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.