Solucions — Exercicis IoT-Vertebrae

Simulador Python · jordibinefa.github.io/iotv

Document per al professor. Conté les solucions completes i comentades de tots els exercicis.


Nivell 0 — Primeres passes


0A — El primer LED

iotv.doutbit("0x0", "a", 0, 1)
iotv.doutbit("0x0", "a", 2, 1)
iotv.doutbit("0x0", "a", 4, 1)
time.sleep(5)

Alternativa amb màscara de bits (bits 0, 2, 4 = 0b00010101 = 0x15 = 21):

iotv.dout("0x0", "a", 0b00010101)
time.sleep(5)

Punt clau: dout envia un sol missatge CAN per a totes 8 sortides alhora — més eficient que tres crides a doutbit.


0B — Parpelleig (blink)

while True:
    iotv.doutbit("0x0", "a", 0, 1)
    time.sleep(1)
    iotv.doutbit("0x0", "a", 0, 0)
    time.sleep(1)

Punt clau: while True: és el patró universal de bucle infinit en sistemes encastats. Equivalent a void loop() d'Arduino.


0C — Llegir un botó

Versió bàsica:

while True:
    estat = iotv.dinbit("0x0", "b", 0)
    if estat == 1:
        print("Botó premut")
    else:
        print("Botó no premut")
    time.sleep(0.5)

Versió amb extensió (LED mirall):

while True:
    estat = iotv.dinbit("0x0", "b", 0)
    iotv.doutbit("0x0", "a", 0, estat)   # estat ja és 0 o 1
    if estat == 1:
        print("Botó premut → LED ON")
    else:
        print("Botó no premut → LED OFF")
    time.sleep(0.5)

Punt clau: dinbit retorna exactament 0 o 1, el mateix que accepta doutbit — es pot passar directament sense cap if.


0D — Seqüència temporitzada

print("Iniciant seqüència d'arrencada...")

iotv.doutbit("0x0", "a", 0, 1)
print("DO0 — pilot d'avís")
time.sleep(2)

iotv.doutbit("0x0", "a", 1, 1)
print("DO1 — motor")
time.sleep(3)

iotv.doutbit("0x0", "a", 2, 1)
print("DO2 — ventilador")
time.sleep(2)

iotv.dout("0x0", "a", 0x00)
print("Seqüència completada")

Punt clau: iotv.dout("0x0", "a", 0x00) apaga les 8 sortides en una sola crida. En producció, un bloc try/finally garanteix que s'apagui tot fins i tot si el programa s'interromp.


0E — Semàfor

def apagar_tots():
    iotv.doutbit("0x0", "a", 0, 0)
    iotv.doutbit("0x0", "a", 1, 0)
    iotv.doutbit("0x0", "a", 2, 0)

print("=== Semàfor iniciat ===")

while True:
    apagar_tots()
    iotv.doutbit("0x0", "a", 0, 1)
    print("VERMELL")
    time.sleep(4)

    iotv.doutbit("0x0", "a", 1, 1)
    print("VERMELL + AMBRE")
    time.sleep(1)

    apagar_tots()
    iotv.doutbit("0x0", "a", 2, 1)
    print("VERD")
    time.sleep(3)

    apagar_tots()
    iotv.doutbit("0x0", "a", 1, 1)
    print("AMBRE")
    time.sleep(2)

Punt clau: La funció apagar_tots() evita que quedin LEDs encesos per oblit. En automatització industrial, les columnes lluminoses (Werma, Patlite, Banner) funcionen exactament amb aquest patró — tres sortides digitals temporitzades.


0F — Comptador de polsos

comptador = 0
estat_anterior = 0

while True:
    estat_actual = iotv.dinbit("0x0", "b", 0)

    if estat_actual == 1:
        if estat_anterior == 0:
            comptador = comptador + 1
            print("Total polsos:", comptador)

    estat_anterior = estat_actual
    time.sleep(0.05)

Punt clau: La detecció de flanc de pujada (0→1) és el patró més usat en comptadors industrials. La variable estat_anterior és la memòria del sistema — equivalent al bit d'estat del bloc CTU (Counter Up) en IEC 61131-3.


Nivell 1 — Automatització bàsica


1A — Interruptor i LED amb detecció de canvi

estat_anterior = -1

while True:
    estat = iotv.dinbit("0x0", "b", 0)
    iotv.doutbit("0x0", "a", 0, estat)

    if estat != estat_anterior:
        if estat == 1:
            print("Interruptor ON → LED ON")
        else:
            print("Interruptor OFF → LED OFF")
        estat_anterior = estat

    time.sleep(0.05)

Punt clau: estat_anterior = -1 força que el primer cicle sempre imprimeixi l'estat inicial, sigui quin sigui, sense necessitar un if extra.


1B — Seqüència de LEDs amb velocitat variable

Versió bàsica:

while True:
    for bit in range(8):
        iotv.doutbit("0x0", "a", bit, 1)
        time.sleep(0.2)
        iotv.doutbit("0x0", "a", bit, 0)

Versió amb velocitat variable (ampliació):

while True:
    for bit in range(8):
        v = iotv.ain2v(iotv.ain("0x0", "b", 1))
        retard = (10 - v) / 20 * 0.45 + 0.05

        iotv.doutbit("0x0", "a", bit, 1)
        time.sleep(retard)
        iotv.doutbit("0x0", "a", bit, 0)

Verificació de la fórmula: slider a +10 V → (10−10)/20*0.45+0.05 = 0.05 s; slider a −10 V → (10−(−10))/20*0.45+0.05 = 0.50 s.


1C — Sensor de llum amb histèresi

llum_encesa = False

while True:
    v = iotv.ain2v(iotv.ain("0x0", "b", 1))
    pct = (v + 10) / 20 * 100

    if llum_encesa == False:
        if pct < 30:
            iotv.doutbit("0x0", "a", 0, 1)
            llum_encesa = True
            print("Llum:", int(pct), "% → ENCESA")
    else:
        if pct > 40:
            iotv.doutbit("0x0", "a", 0, 0)
            llum_encesa = False
            print("Llum:", int(pct), "% → APAGADA")

    time.sleep(0.3)

Punt clau: Sense histèresi, la llum parpelleja incontroladament si el sensor s'estabilitza al 31%. La banda morta de 10 punts (30–40%) elimina el chattering. Patró estàndard en termoregulació, climatització i control de pressió.


1D — Control de velocitat per potenciòmetre

while True:
    raw = iotv.ain("0x0", "b", 1)
    v_entrada = iotv.ain2v(raw)

    pct = (v_entrada + 10) / 20 * 100
    v_sortida = pct / 100 * 10

    iotv.aout("0x0", "a", 1, iotv.v2aout(v_sortida))

    print("Consigna:", int(v_entrada * 10) / 10, "V → Velocitat:", int(pct), "%")
    time.sleep(0.2)

Fórmula simplificada equivalent: v_sortida = (v_entrada + 10) / 2

Punt clau: En un variador de freqüència real, 10 V correspon a la velocitat màxima del motor (p.ex. 1500 rpm a 50 Hz). La vèrtebra analògica pot fer exactament de pont entre la consigna digital i el variador.


1E — Mescla de color RGBW

Versió bàsica:

while True:
    r = iotv.dinbit("0x0", "b", 0)
    g = iotv.dinbit("0x0", "b", 1)
    b = iotv.dinbit("0x0", "b", 2)
    w = iotv.dinbit("0x0", "b", 3)

    iotv.doutbit("0x0", "a", 0, r)
    iotv.doutbit("0x0", "a", 1, g)
    iotv.doutbit("0x0", "a", 2, b)
    iotv.doutbit("0x0", "a", 3, w)

    print("R:", r, "G:", g, "B:", b, "W:", w)
    time.sleep(0.2)

Versió amb blanc automàtic (extensió):

while True:
    r = iotv.dinbit("0x0", "b", 0)
    g = iotv.dinbit("0x0", "b", 1)
    b = iotv.dinbit("0x0", "b", 2)
    w = iotv.dinbit("0x0", "b", 3)

    if r == 1:
        if g == 1:
            if b == 1:
                w = 1

    iotv.doutbit("0x0", "a", 0, r)
    iotv.doutbit("0x0", "a", 1, g)
    iotv.doutbit("0x0", "a", 2, b)
    iotv.doutbit("0x0", "a", 3, w)

    print("R:", r, "G:", g, "B:", b, "W:", w)
    time.sleep(0.2)

Punt clau: El simulador no suporta and. Els if niuats són l'equivalent correcte. En LEDs RGBW reals, el canal W dona una blancor més eficient que la mescla R+G+B.


1F — Sensor BME280 ambiental

Versió bàsica:

while True:
    v_t = iotv.ain2v(iotv.ain("0x0", "b", 1))
    v_h = iotv.ain2v(iotv.ain("0x0", "b", 2))
    v_p = iotv.ain2v(iotv.ain("0x0", "b", 3))

    temp = (v_t + 10) / 20 * 125 - 40
    hum  = (v_h + 10) / 20 * 100
    pres = (v_p + 10) / 20 * 800 + 300

    print("T:", int(temp * 10) / 10, "°C | H:", int(hum), "% | P:", int(pres), "hPa")
    time.sleep(0.5)

Versió amb alertes (extensió):

while True:
    v_t = iotv.ain2v(iotv.ain("0x0", "b", 1))
    v_h = iotv.ain2v(iotv.ain("0x0", "b", 2))
    v_p = iotv.ain2v(iotv.ain("0x0", "b", 3))

    temp = (v_t + 10) / 20 * 125 - 40
    hum  = (v_h + 10) / 20 * 100
    pres = (v_p + 10) / 20 * 800 + 300

    print("T:", int(temp * 10) / 10, "°C | H:", int(hum), "% | P:", int(pres), "hPa")

    if temp > 40:
        iotv.doutbit("0x0", "a", 0, 1)
        print("  ⚠ TEMPERATURA ALTA")
    else:
        iotv.doutbit("0x0", "a", 0, 0)

    if hum > 80:
        iotv.doutbit("0x0", "a", 1, 1)
        print("  ⚠ HUMITAT ALTA")
    else:
        iotv.doutbit("0x0", "a", 1, 0)

    if pres < 970:
        iotv.doutbit("0x0", "a", 2, 1)
        print("  ⚠ PRESSIÓ BAIXA")
    else:
        iotv.doutbit("0x0", "a", 2, 0)

    time.sleep(0.5)

Valors de referència útils per provar: slider AI1 a 0 V → 22.5 °C; slider AI3 a +8.8 V → 1013 hPa (pressió normal a Barcelona).


Nivell 2 — Sistemes de control


2A — Cinta transportadora amb parada d'emergència

vertebraA = "0x0"
vertebraD = "0x1"
bMotorA    = 0
bMotorB    = 1
bSolenoide = 2
bSensorE   = 5
bSensorD   = 6
bEmergencia = 7
canalVel   = 1

print("=== Cinta transportadora ===")

iotv.doutbit(vertebraD, "a", bSolenoide, 1)
time.sleep(0.1)
iotv.doutbit(vertebraD, "a", bSolenoide, 0)
print("Caixa dispensada")
time.sleep(1)

iotv.aout(vertebraA, "a", canalVel, iotv.v2aout(5.0))

direction = 1
iotv.doutbit(vertebraD, "a", bMotorA, 1)
iotv.doutbit(vertebraD, "a", bMotorB, 0)
print("→ Motor endavant")

comptador_dreta = 0
comptador_esquerra = 0

while True:
    adc = iotv.ain(vertebraA, "b", canalVel)
    speed_v = iotv.ain2v(adc)
    speed_dac = (speed_v + 10) / 2
    if speed_dac < 0:
        speed_dac = 0
    if speed_dac > 10:
        speed_dac = 10
    iotv.aout(vertebraA, "a", canalVel, iotv.v2aout(speed_dac))

    emergency = iotv.dinbit(vertebraD, "b", bEmergencia)
    if emergency == 1:
        print("⚠ EMERGÈNCIA — Motor aturat")
        iotv.doutbit(vertebraD, "a", bMotorA, 0)
        iotv.doutbit(vertebraD, "a", bMotorB, 0)
        time.sleep(0.5)
        continue

    if direction == 1:
        iotv.doutbit(vertebraD, "a", bMotorA, 1)
        iotv.doutbit(vertebraD, "a", bMotorB, 0)
    if direction != 1:
        iotv.doutbit(vertebraD, "a", bMotorA, 0)
        iotv.doutbit(vertebraD, "a", bMotorB, 1)

    sensorE = iotv.dinbit(vertebraD, "b", bSensorE)
    sensorD = iotv.dinbit(vertebraD, "b", bSensorD)

    detected = 0
    if direction == 1:
        if sensorD == 1:
            detected = 1
    if direction != 1:
        if sensorE == 1:
            detected = 1

    if detected == 1:
        direction = direction * -1
        if direction == 1:
            comptador_esquerra = comptador_esquerra + 1
            print("← Esquerra | E:", comptador_esquerra, "D:", comptador_dreta)
        if direction != 1:
            comptador_dreta = comptador_dreta + 1
            print("→ Dreta    | E:", comptador_esquerra, "D:", comptador_dreta)

    time.sleep(0.01)

2B — Sensor BME280 amb control HVAC

ventilador_on = False
deshumid_on   = False

while True:
    v_t = iotv.ain2v(iotv.ain("0x0", "b", 1))
    v_h = iotv.ain2v(iotv.ain("0x0", "b", 2))

    temp = (v_t + 10) / 20 * 125 - 40
    hum  = (v_h + 10) / 20 * 100

    # Control temperatura amb histèresi (23–26 °C)
    if ventilador_on == False:
        if temp > 26:
            iotv.doutbit("0x0", "a", 0, 1)
            ventilador_on = True
            print("Ventilador ON — T:", int(temp * 10) / 10, "°C")
    else:
        if temp < 23:
            iotv.doutbit("0x0", "a", 0, 0)
            ventilador_on = False
            print("Ventilador OFF — T:", int(temp * 10) / 10, "°C")

    # Control humitat amb histèresi (60–70%)
    if deshumid_on == False:
        if hum > 70:
            iotv.doutbit("0x0", "a", 1, 1)
            deshumid_on = True
            print("Deshumidificador ON — H:", int(hum), "%")
    else:
        if hum < 60:
            iotv.doutbit("0x0", "a", 1, 0)
            deshumid_on = False
            print("Deshumidificador OFF — H:", int(hum), "%")

    time.sleep(0.5)

Punt clau: Dos controls d'histèresi independents al mateix bucle és exactament com funciona un controlador HVAC real: cada paràmetre té la seva banda morta pròpia i el seu actuador independent.


2C — RGBW analògic (dimmer de 4 canals)

Versió bàsica:

while True:
    r = iotv.ain2v(iotv.ain("0x0", "b", 1))
    g = iotv.ain2v(iotv.ain("0x0", "b", 2))
    b = iotv.ain2v(iotv.ain("0x0", "b", 3))
    w = iotv.ain2v(iotv.ain("0x0", "b", 4))

    r_out = (r + 10) / 2
    g_out = (g + 10) / 2
    b_out = (b + 10) / 2
    w_out = (w + 10) / 2

    iotv.aout("0x0", "a", 1, iotv.v2aout(r_out))
    iotv.aout("0x0", "a", 2, iotv.v2aout(g_out))
    iotv.aout("0x0", "a", 3, iotv.v2aout(b_out))
    iotv.aout("0x0", "a", 4, iotv.v2aout(w_out))

    print("R:", int(r_out * 10), "% G:", int(g_out * 10), "% B:", int(b_out * 10), "% W:", int(w_out * 10), "%")
    time.sleep(0.2)

Versió amb limitació de potència (extensió):

while True:
    r = (iotv.ain2v(iotv.ain("0x0", "b", 1)) + 10) / 2
    g = (iotv.ain2v(iotv.ain("0x0", "b", 2)) + 10) / 2
    b = (iotv.ain2v(iotv.ain("0x0", "b", 3)) + 10) / 2
    w = (iotv.ain2v(iotv.ain("0x0", "b", 4)) + 10) / 2

    total = r + g + b + w
    limit = 30.0   # 300% del màxim de 10V = 30V total

    if total > limit:
        factor = limit / total
        r = r * factor
        g = g * factor
        b = b * factor
        w = w * factor
        print("Limitació aplicada — factor:", int(factor * 100), "%")

    iotv.aout("0x0", "a", 1, iotv.v2aout(r))
    iotv.aout("0x0", "a", 2, iotv.v2aout(g))
    iotv.aout("0x0", "a", 3, iotv.v2aout(b))
    iotv.aout("0x0", "a", 4, iotv.v2aout(w))

    time.sleep(0.2)

Nivell 3 — Sistemes industrials complexos


3A — Ascensor: respostes de comprensió

1. Condició de transició ST_MOVE → ST_OPEN:
Quan la distància entre la posició de l'encoder i la posició destí és inferior a MARGIN (0.2 m): if dist < MARGIN: state = ST_OPEN.

2. Per quin motiu no usa time.sleep() llarg al bucle principal:
Perquè necessita reaccionar ràpidament als botons i sensors. Un sleep llarg bloquejarà el sistema fins que acabi, ignorant possibles emergències o pulsacions. El retard de 0.1 s al final és el màxim acceptable.

3. Funció del canal analògic AI1:
Simula l'encoder de posició de la cabina. El valor −10..+10 V es converteix a 0..6 metres (P0=0 m, P1=3 m, P2=6 m). En un ascensor real seria un encoder incremental o absolut.

4. Mecanisme de seguretat de portes:
El sensor DoorSafety (vèrtebra "0x1", costat B, bit 3). Si s'activa durant el tancament (ST_CLOSE), el sistema torna immediatament a ST_OPEN. Simula un fotocel·le o una barra de pressió.

Modificació — comptador de viatges:

Al principi del programa (fora del bucle):

comptador_viatges = 0

Dins del bloc if dist < MARGIN:, just abans de state = ST_OPEN:

comptador_viatges = comptador_viatges + 1
print("Viatge nº", comptador_viatges, ": Planta", current_floor)

3B — Ascensor: respostes sobre la memòria de plantes

1. Quan s'esborra pending_0:
S'esborra (pending_0 = 0) quan l'ascensor arriba a la planta 0 (current_floor == 0 dins de ST_MOVE). Es posa a 1 quan es detecta qualsevol botó de la planta 0 (botó de cabina bCabCall0 o botó de planta bP0Up).

2. Ordre de servei amb P0 i P2 pendents des de P1:
L'ascensor comprova primer les plantes en la direcció actual. Amb direction >= 0 (puja), mira P1, P2 en ordre. Com que ja és a P1, el target serà P2. Quan baixa de P2, la direcció és −1 i cerca P1, P0 — troba P0. L'ordre serà P2 → P0. És l'algoritme SCAN (ascensor de disc dur), que minimitza el recorregut total.

3. Mostrar plantes pendents (modificació):

Al final del bloc if state == ST_WAIT:, just abans del if target_floor >= 0::

print("Pendents: P0=", pending_0, "P1=", pending_1, "P2=", pending_2)

3C — RPi real vs simulador: respostes

1. Com es detecten els canvis sense polling a la RPi:
Amb un callback asíncron. on_din_change(addr, callback) registra una funció que la biblioteca can_iotv_v2_1 crida automàticament en un thread de fons quan la vèrtebra envia un missatge espontani de canvi. El programa no pregunta — escolta.

2. Línies d'inici i aturada:

3. can_on() i can_off():
Inicialitzen i alliberen el driver del bus CAN físic (TWAI a l'ESP32 o socket CAN a Linux). Al simulador no cal perquè la connexió al bus virtual via MQTT es gestiona automàticament en obrir la pàgina.

4. Per quin motiu no es pot cridar time.sleep() dins del callback:
El callback s'executa en el thread del dispatcher (core 0), mentre el bucle principal corre al core 1. Una funció bloquejant com sleep paralitzaria el dispatcher i podria perdre missatges CAN. El patró correcte és que el callback es limiti a actualitzar una variable volatile, i el bucle principal gestioni l'acció.

Taula comparativa completa:

Característica Simulador (polling) RPi real (callback)
Latència de resposta depèn del time.sleep() pràcticament immediata
Consum de CPU alt (consulta contínua) baix (activa només en canvi)
Consum de bus CAN pot saturar-lo no genera tràfic addicional
Complexitat del codi baixa, seqüencial mitjana, cal entendre threads
Ús recomanat prototips, simulador producció, sistemes en temps real
Senyals de seguretat ⚠ latència variable ✅ resposta garantida