elevator01El programa elevator01 és un controlador d'ascensor de 3 plantes escrit en Python. Controla un motor, portes, botons de planta i cabina, un encoder de posició i un sistema de seguretat. Funciona tant en el simulador web com connectat a maquinari real via Raspberry Pi.
Si alguna vegada has vist o dissenyat el control d'un ascensor amb PLC, reconeixeràs immediatament el que fa aquest codi. La diferència és que aquí, en lloc de blocs Ladder o un GRAFCET, el programa s'escriu en Python.
El controlador no és un programa que "fa les coses en ordre". És un programa que sap en quin estat es troba i decideix què fer en funció d'aquest estat i de les condicions actuals.
L'ascensor sempre és en exactament un d'aquests sis estats:
| Estat | Nom | Què significa |
|---|---|---|
ST_WAIT |
Espera | L'ascensor és quiet. Espera que algú premi un botó. |
ST_MOVE |
Moviment | El motor està en marxa. S'acosta a la planta destí. |
ST_OPEN |
Obrint | El motor de portes actua per obrir-les. |
ST_OPENED |
Portes obertes | Les portes estan obertes. Espera el passatger. |
ST_CLOSE |
Tancant | El motor de portes actua per tancar-les. |
ST_EMERG |
Emergència | S'ha detectat una alarma o sobrecàrrega. Motor aturat. |
El programa guarda l'estat actual en una variable: state = ST_WAIT.
Diagrama d'estats complet de l'ascensor. Cada caixa és un estat; cada fletxa, una transició amb la condició que la desencadena.
Abans de llegir el codi, cal saber a quin actuador o sensor correspon cada bit. El programa ho explica als comentaris de la capçalera:
Sortides digitals (el programa controla):
Vèrtebra "0x0", costat A:
bit 0 → Motor amunt (contactor de pujada)
bit 1 → Motor avall (contactor de baixada)
bit 2 → Fre (fre electromagnètic)
bit 3 → Obrir portes (motor de portes, sentit obertura)
bit 4 → Tancar portes (motor de portes, sentit tancament)
bit 5 → LED planta 0
bit 6 → LED planta 1
bit 7 → LED planta 2
Vèrtebra "0x1", costat A:
bit 0 → Llum de portes obertes
bit 1 → Llum d'alarma
Entrades digitals (el programa llegeix):
Vèrtebra "0x0", costat B:
bit 0 → Botó cabina planta 0
bit 1 → Botó cabina planta 1
bit 2 → Botó cabina planta 2
bit 3 → Alarma tèrmica (sobreescalfament del motor)
bit 4 → Sobrecàrrega
bit 5 → Botó de planta 0 (puja)
bit 6 → Botó de planta 1 (puja)
bit 7 → Botó de planta 1 (baixa)
Vèrtebra "0x1", costat B:
bit 0 → Botó de planta 2 (baixa)
bit 1 → Botó d'obrir portes (manual)
bit 2 → Botó de tancar portes (manual)
bit 3 → Sensor de seguretat de portes (fotocel·le)
Entrada analògica (el programa llegeix):
Vèrtebra "0x0", costat B, canal 1 → Encoder de posició
−10 V = planta 0 (0 metres)
+10 V = planta 2 (6 metres)
Al principi del codi hi ha les constants que defineixen la geometria de l'ascensor:
FLOOR_POS_0 = 0.0 # planta 0 = 0 metres
FLOOR_POS_1 = 3.0 # planta 1 = 3 metres
FLOOR_POS_2 = 6.0 # planta 2 = 6 metres
MARGIN = 0.2 # tolerància de posicionament: ±20 cm
SPEED_MAX = 4095 # velocitat màxima (valor DAC)
SPEED_MED = 2048 # velocitat mitja
SPEED_LOW = 800 # velocitat baixa (aproximació)
DOOR_TIME = 3.0 # temps mínim de portes obertes (s)
WAIT_TIME = 5.0 # temps addicional d'espera (s)
Les funcions auxiliars simplifiquen el codi principal:
read_encoder() — Llegeix el canal analògic i el converteix a metres:
def read_encoder():
raw = iotv.ain("0x0", "b", 1) # valor cru del ADC
voltage = iotv.ain2v(raw) # converteix a volts (−10 a +10)
pos = (voltage + 10.0) / 20.0 * 6.0 # remapeja a metres (0 a 6)
...
return pos
El slider del simulador que va de −10 V a +10 V representa la posició de la cabina de 0 a 6 metres.
stop_motor() — Atura el motor i activa el fre:
def stop_motor():
iotv.doutbit("0x0", "a", bMotorUp, 0) # desactiva pujada
iotv.doutbit("0x0", "a", bMotorDown, 0) # desactiva baixada
iotv.doutbit("0x0", "a", bBrake, 1) # activa fre
iotv.aout("0x0", "a", 1, 0) # consigna de velocitat = 0
show_floor(floor) — Actualitza els LEDs indicadors de planta:
def show_floor(floor):
iotv.doutbit("0x0", "a", bDispP0, 0) # apaga tots
iotv.doutbit("0x0", "a", bDispP1, 0)
iotv.doutbit("0x0", "a", bDispP2, 0)
if floor == 0:
iotv.doutbit("0x0", "a", bDispP0, 1) # encén el de la planta actual
...
El cor del programa és un bucle while True que es repeteix cada 100 ms. En cada iteració fa tres coses:
1. Llegeix tots els botons i actualitza les "plantes pendents":
if iotv.dinbit("0x0", "b", bCabCall0) == 1:
pending_0 = 1
if iotv.dinbit("0x0", "b", bP0Up) == 1:
pending_0 = 1
...
pending_0 = 1 vol dir "hi ha algú esperant a la planta 0 o volent anar-hi". Quan l'ascensor arriba, es posa a 0.
2. Executa la lògica de l'estat actual.
3. Espera 100 ms i torna a començar.
El programa comprova si hi ha alguna planta pendent. Si n'hi ha, escull la més propera en la direcció actual i transita a ST_MOVE. Si no n'hi ha, no fa res.
if state == ST_WAIT:
if is_emergency() == 1:
state = ST_EMERG
else:
# escull la pròxima planta destí...
if target_floor >= 0:
if target_floor == current_floor:
state = ST_OPEN # ja hi som, obrim portes
else:
# engega el motor en la direcció correcta
state = ST_MOVE
El programa llegeix l'encoder contínuament i calcula la distància a la planta destí. Ajusta la velocitat del motor segons la distància:
if state == ST_MOVE:
pos = read_encoder()
dist = abs(target_pos - pos)
if dist > 1.5:
iotv.aout("0x0", "a", 1, SPEED_MAX) # velocitat màxima (lluny)
elif dist > 0.5:
iotv.aout("0x0", "a", 1, SPEED_MED) # velocitat mitja (s'apropa)
else:
iotv.aout("0x0", "a", 1, SPEED_LOW) # velocitat baixa (aterrant)
if dist < MARGIN: # ha arribat!
stop_motor()
state = ST_OPEN
Això és un perfil de velocitat trapezial: va ràpid, desaccelera a temps i s'atura amb precisió. En un ascensor real hi hauria una rampa de jerk (derivada de l'acceleració) per donar comoditat als passatgers, però el principi és el mateix.
El programa comprova el sensor de seguretat. Si no hi ha obstacle, activa el motor de portes en sentit obertura i passa a ST_OPENED.
if state == ST_OPEN:
safety = iotv.dinbit("0x1", "b", bDoorSafety)
if safety == 1:
state = ST_WAIT # sensor actiu, no obrim
else:
iotv.doutbit("0x0", "a", bDoorOpen, 1)
door_timer = time.time() # marca l'hora d'obertura
state = ST_OPENED
El programa espera que el passatger entri. Les portes es tanquen si:
DOOR_TIME + WAIT_TIME = 8 segons sense que ningú faci resif state == ST_OPENED:
elapsed = time.time() - door_timer
close_btn = iotv.dinbit("0x1", "b", bDoorCloseBtn)
if close_btn == 1:
if elapsed > 1.0: # marge mínim d'1 s per evitar tancaments accidentals
state = ST_CLOSE
if elapsed > DOOR_TIME + WAIT_TIME:
state = ST_CLOSE
El programa comprova el sensor de seguretat de portes. Si detecta un obstacle mentre es tanquen, reobre immediatament. Si no, tanca i torna a espera.
if state == ST_CLOSE:
safety = iotv.dinbit("0x1", "b", bDoorSafety)
if safety == 1:
print("Obstacle! Reopening")
state = ST_OPEN # reobre sense discussió
else:
iotv.doutbit("0x0", "a", bDoorClose, 1)
time.sleep(1.5)
iotv.doutbit("0x0", "a", bDoorClose, 0)
state = ST_WAIT
El programa atura el motor, activa la llum d'alarma i obre les portes. Llavors entra en un bucle intern esperant que l'alarma o la sobrecàrrega es resolgui. Quan es resol, recupera la posició i torna a ST_WAIT.
if state == ST_EMERG:
stop_motor()
iotv.doutbit("0x1", "a", bAlarmLight, 1)
iotv.doutbit("0x0", "a", bDoorOpen, 1)
while True: # espera activa fins que s'esborra l'alarma
a = iotv.dinbit("0x0", "b", bCabAlarm)
o = iotv.dinbit("0x0", "b", bCabOverload)
if a == 0:
if o == 0:
break
time.sleep(0.2)
...
state = ST_WAIT
Un cop carregat i executat el codi:
El canvi més senzill és afegir un comptador de viatges. Cerca al codi el bloc # Arrived? dins de ST_MOVE i afegeix-hi:
# 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)
Un altre canvi fàcil: modificar DOOR_TIME i WAIT_TIME per ajustar quan es tanquen les portes.
Aquest controlador és equivalent a una SFC (Sequential Function Chart) de PLC IEC 61131-3, que és el que s'usa en ascensors industrials reals. Cada estat de la variable state correspon a un pas de la SFC. Les transicions (les condicions que canvien l'estat) corresponen als arcs de la SFC.
La diferència principal entre aquest codi Python i un PLC de producció és la certificació de seguretat (SIL2 o SIL3 per a ascensors). El codi és conceptualment idèntic — el que diferencia el PLC és que el hardware és determinista i ha superat auditories de fiabilitat. Per a prototipatge, formació i sistemes no crítics, IoT-Vertebrae ofereix la mateixa lògica a una fracció del cost.