Transport¶
Introducció¶
Cada cop és més habitual trobar a les parades dels autobusos, una pantalla informativa. La pantalla té com objectiu indicar els temps que un autobús li falta per arribar a la parada. En el cas que les previsions fallin per problemes de circulació, s’indiquen les noves previsions.
Es tracta de dissenyar una aplicació que simuli una xarxa de pantalles informatives. Per això simularem la circulació d’un conjunt d’autobusos repartits per un conjunt de línies de transport i visualitzi la informació pertinent a la pantalla de cada parada d’autobús.
Les parades i els autobusos estaran connectats en una xarxa de comunicació mitjançant el protocol MQTT.
L’esquema de l’aplicació es mostra en el següent diagrama:
L’aplicació constarà dels següents processos:
Un programa central que llegirà la planificació de la jornada de transports escrita en un fitxer que conté la descripció de les línies, parades, i autobusos previstos. El programa crearà un procés per cada parada prevista (procés parada), i un procés per cada línia de trajectes previstos (procés gestorLínia). Finalment, coordinarà l’acabament de tot el sistema.
El procés parada (un per cada parada de les línies), processa tots els missatges rebuts pels autobusos de les línies a la que pertany la parada, i notifica en la seva pantalla, els temps d’arribada previstos.
El procés gestorLínia, responsable de gestionar la flota d’autobusos de la línia seguint un encàrrec. Cada cert interval de temps, el procés posarà en marxa el servei d’un autobús de la línia, creant i engegant un procés bus.
El procés bus executa el servei d’un autobús consistent en recórrer les estacions de la línia un nombre de cops (nombre de torns) previst. Un cop fet tots els torns, el seu servei s’acaba. Entre torns a fer hi haurà un període de descans previst. En arribar a una parada, l’autobús envia missatges a la parada on està, i a les parades de la línia que encara queden per recórrer. La parada on està l’autobús rebrà un temps d’espera de 0 minuts.
Comunicació entre processos¶
La comunicació entre processos es farà en el protocol MQTT sobre TCP/IP. La biblioteca de python paho-mqtt
(paho-mqtt) facilita la programació del protocol seguint el model de publicar-subscriure. Usarem el servidor mosquitto [1] com a intermediari del model.
Mòduls¶
La següent figura mostra els mòduls de l’aplicació amb la relació d’usos:
Per facilitar el temps de desenvolupament, els quadre en negre es donaran ja fets, i només caldrà fer els mòduls en color cian. Els fitxers corresponents a cada mòdul:
parada.py
bus.py
gestorLinia.py
central.py
L’especificació de la resta de mòduls, ho trobareu els següents enllaços:
Fitxer de dades de simulació¶
L’aplicació podrà córrer individualment tres programes: bus, parada, i central. Els tres ho fan llegint un fitxer on hi ha les dades de la simulació. El nom del fitxer és el primer argument de la comanda que executa el programa.
Un exemple de fitxer és:
# Línia 1 llista (nom parada, temps que cal per arribar a la següent parada).
L1
A,3
B,2
C,3
D,2
E,3
L2
F,3
G,2
C,3
D,2
H,3
I,3
L3
J,3
K,4
L,2
A,3
B,2
C,2
# parades (nom parada, x, y )
P
A,300, 0
B,300,100
C,300,200
D,300,300
E,300,400
F, 0,200
G,100,200
H,400,300
I,500,300
J, 0, 0
K,100, 0
L,200, 0
C
transport
publica
localhost
1883
# autobusos (identificador autobus,
# Línia assignada,
# nombre torns,
# temps descans)
A
a01, L1, 2, 5
a02, L2, 2, 5
a03, L3, 3, 5
a04, L1, 2, 5
La sintaxi és relativament senzilla: Si el primer caràcter d’una línia és:
“L” es refereix a una línia de trajecte d’autobús. Tot els noms de les línies començaran per “L”. Per tant, la línia que el primer caràcter és una “L” conté el nom de la línia. Li seguiran un conjunt de línies. Cadascuna d’elles portarà el nom d’una parada i el temps previst per anar a la següent parada. Ambdós separats per una coma. Hi haurà un sagnat abans del nom. L’ordre en que apareixen els noms és la seqüència de parades del trajecte.
“P” es refereix a la llista de parades de totes les línies. Li seguiran un conjunt de línies. Cada línia desprès d’un sagnat contindrà el nom o identificador de la parada, i dos enters relatius a les coordenades de la seva posició en pantalla. Es pot posar suposant un espai de coordenades. El programa central farà un encaix de l’espai suposat per que càpiga en l’escriptori (en coordenades píxel).
“A” es refereix a la llista d’autobusos de totes les línies. Li seguiran un conjunt de línies. Cada línia desprès d’un sagnat conté el nom o identificador de l’autobús, nom de la línia on circularà, nombre enter de vegades (torns) que farà el trajecte de la línia, temps en minuts de descans a fer entre cada torn.
“C” es refereix a les dades de la connexió amb el servidor del model publica-subscriu. Li seguiran 4 línies. La primera línia desprès d’un sagnat, conté el nom d’usuari, la segona, la contrasenya, la tercera, l’adreça d’internet, i la darrera el nombre de port.
S’ignoraran les línies que no comencen per cap dels quatre caràcters esmentats.
El fitxer es llegeix cridant la funció varies.llegirDades()
. Per exemple,
>>> from varies import llegirDades
>>> import tempfile
>>> f, nomf = tempfile.mkstemp(text=True)
>>> with open(nomf, 'w') as g:
... g.write("""
... A
... A01,L1,1,5
... A02,L1,1,5
... L1
... A,3
... B,2
... C,3
... D,2
... E,3
... P
... A,0,100
... B,100,100
... C,200,100
... D,300,100
... E,400,100
... C
... transport
... publica
... localhost
... 1883
... """)
195
>>> g.close()
>>> línies, parades, busos, dConnexió = llegirDades(nomf)
>>> línies
{'L1': [('A', 3), ('B', 2), ('C', 3), ('D', 2), ('E', 3)]}
>>> parades
[('A', 0, 100), ('B', 100, 100), ('C', 200, 100), ('D', 300, 100), ('E', 400, 100)]
>>> busos
[('A01', 'L1', 1, 5), ('A02', 'L1', 1, 5)]
>>> dConnexió
['transport', 'publica', 'localhost', 1883]
Parada¶
El procés parada serà el programa que simularà la pantalla on es donen els temps d’espera dels autobusos per arribar. Usarà la pantalla d’un xterm o un terminal on s’executi. Per exemple si la parada s’anomena “La_Parada”, i circulen dos autobusos de la línia “L1” que trigaran respectivament 3 minuts i 10 minuts, i un autobús de la línia “L2” que trigarà 7 minuts, en la pantalla de la parada es mostrarà:
La_Parada
L1: 3m 10m
L2: 7m
Seguint l’exemple, per poder actualitzar la pantalla, el procés està subscrit al servidor intermediari al tema “La_Parada”. Els autobusos circulants, cada cop que passen per una parada anterior a la parada en qüestió, publiquen en el tema de cada parada per fer, les previsions d’arribada a la parada. El missatge inclou identificador de bus, identificador de línia, i temps previst d’arribada. Amb aquestes dades actualitzarà la pantalla. En l’exemple de pantalla anterior, li haurà arribat en el seu moment, els missatges «a01, L1, 3», «a11, L1, 10», «a03, L2, 3» que es mostraran com a la pantalla de l’exemple si tots els missatges han arribat en menys d’un minut.
El procés parada tindrà en compte els minuts que passen, i cada cop que passa un minut, es descomptarà un minut al temps previst. El descompte es fa des del moment en que s’ha iniciat el comptador de minuts d’un autobús determinat. És a dir, que cada comptador de minuts té un cronòmetre independent.
Per implementar-ho usarem un objecte de la classe crono.CronometresMostra
que mitjançant un fil de control de la classe crono.CronoMostra
(classe derivada de Threadings.Thread
) visualitzarà per pantalla tots els temps d’arribada previstos i notificats, actualitzant i mostrant de nou els temps cada minut que passi per cada comptador. El mètode crono.CronometresMostra.fixa()
posa la informació notificada en l’objecte per ser mostrada cronològicament.
Com que en cada línia de trajecte pot circular més d’un autobús, hem d’usar un cronòmetre independent per cada autobús. Cada cronòmetre s’identificarà pel nom de l’autobús i la línia per la que circula. Aixi doncs, l’identificador consistirà d’un tuple que té el nom de l’autobús i el nom de la línia. Per exemple, pel missatge «L1, a01, 3» faríem crono.CronometresMostra.fixa((“L1”, “a01”), 3). Important l’ordre en el tuple: línia, identificador d’autobús. En la visualització final, la identificació de l’autobús no apareix.
El procés parada es guardarà en el fitxer parada.py
. L’especificació del mòdul parada
és:
- class parada.Parada(nomParada, connexió)¶
Bases:
object
Simula una parada d’autobús. La parada té un nom, i cal tenir les dades de connexió al servidor intermediari per accedir les comunicacions dels autobusos que venen a visitar-la.
Exemple:
>>> p = Parada('Diagonal', ('transports', 'publica', 'localhost', 1883))
p està preparat per actuar com a client de l’intermediari (broker) situat en el mateix ordinador on s’executa. S’ha configurat una funció de callback que cridarà el mètode
p.actualitza()
per processar els missatges. El processament i recepció de missatges MQTT del tema subscrit “Diagonal” es farà efectiu cridantp.activa()
.- __init__(nomParada, connexió)¶
- Parameters:
- Returns:
Una instància de la classe
parada.Parada
Iniciador de la classe
parada.Parada
. Obra la connexió amb el servidor intermediari. Programa un callback per la recepció de missatges. El callback apunta a una funció que descodifica el missatge i crida al mètodeParada.actualitza()
.Crea els atributs:
nom: on guarda el paràmetre nomParada
tauler: objecte de tipus
tauler.TaulerBus
crono: objecte
CronometresMostra
client: objecte per gestionar la comunicació amb el servidor MQTT.
- activa()¶
El mètode posa en marxa la parada, subcribint-se al tema que porta com a nom el nom o identificador de la parada. Entra llavors en iteració permanent de processament de missatges. La funció de callback prevista cridarà el mètode
parada.actualitza()
un cop s’ha rebut i descodificat un missatge.
- actualitza(missatge)¶
- Parameters:
missatge (str) –
str
que conté separat per comes, identificador de línia, identificador d’autobús, i temps d’espera; ostr
contenint «Final».
Donat un missatge, si es tracta del
str
«Final», l’escriu a pantalla, desconnectant-se del servidor intermediari i acabant el servei de la pantalla de la parada.Si es tracta d’un
str
amb dades noves de circulació actualitza el tauler i el presenta a pantalla mitjançant l’ús del mètodeCronoMostra.fixa()
- parada.parada(idParada, connexió)¶
Crea un objecte de la classe Parada amb els:
- Parameters:
i activa el processament de missatges de l’objecte.
Execució de la parada¶
Per executar el programa parada farem un fitxer sense extensió en el nom, parada
en el següent contingut:
#!/usr/bin/env python3
from parada import parada
from varies import llegirDades
import sys
línies, parades, busos, connexió = llegirDades(sys.argv[1])
idParada = sys.argv[2]
parada(idParada, connexió)
Cal que el fitxer parada tingui l’indicador d’executable (comanda chmod).
Se us dona fet el mòdul varies amb la varies.llegirDades()
que llegeix les dades relatives a les línies, autobusos, parades, i dades de connexió guardats en un fitxer que es proporciona a l’executar parada (argument 1 de la comanda). Si el fitxer on hi han les dades és dades.txt
, i es vol fer funcionar la parada “Pl. Universitat”, escriurem en el terminal (el directori de treball serà on hi ha l’aplicació i les dades):
parada dades.txt "Pl. Universitat"
i el terminal es convertirà en la pantalla de la parada.
Si no hi han autobusos circulant, en la pantalla només sortirà el nom de la parada. Des d’un altre terminal, es pot engegar el programa bus, si ja el teniu fet, i comprovar que a la parada li arriben els missatges de l’autobús. Més detalls en l’apartat Execució del programa bus.
Observeu que cada parada ha de tenir el seu terminal com a pantalla de notificació.
També podeu fer:
python3 parada.py dades.txt "Pl. Universitat"
En aquest cas, cal posar al final del mòdul, el següent text:
if __name__ == "__main__":
from varies import llegirDades
import sys
línies, parades, busos, connexió = llegirDades(sys.argv[1])
idParada = sys.argv[2]
parada(idParada, connexió)
Recordeu que el condicional protegeix de que el mòdul es posi a executar si fem per exemple un import de parada.py
en una sessió de python:
>>> import parada
>>> línies, parades, busos, connexió = llegirDades('dades.txt')
>>> parada('C', connexió)
Bus¶
La idea del procés bus és simular el trajecte d’un bus passant per totes les parades de la línia en que circula. Quan està entre parades, el procés dorm el temps previst per passar d’una parada a l’altre. Un cop està en la parada, envia a la parada i a les següents del trajecte, la seva nova planificació temporal d’arribades.
L’especificació del mòdul bus
(a guardar en el fitxer bus.py
):
- class bus.Bus(idBus, línia, nTorns, descans, connexió)¶
Bases:
object
Simula un autobús d’una línia línia determinada. Ha de fer el trajecte marcat per les parades de la línia nTorns vegades. Entre torns fa un descans minuts de descans. Quan fa el recorregut, a cada parada on arriba envia missatges a la parada que passa i a les que encara ha de passar indicant el temps previst d’arribada. Un temps previst de 0 minuts vol dir que ha arribat a la parada que ho rep.
- __init__(idBus, línia, nTorns, descans, connexió)¶
Inicialitzador de la classe
bus.Bus
.- Parameters:
idBus (str) – nom de l’autobús
nTorns (int) – nombre de torns a fer
línia (linia.Línia) – llista de parades de la línia a la que pertany. Cada element de la llista inclou un tuple amb el nom de la parada, i el temps previst per arribar a la següent parada.
descans (int) – temps en minuts de descans entre torns
connexió (tuple) – Dades de la connexió al servidor intermediari per implementar el model publicar-subscriure. Consisteix d’un tuple amb (adreça, nom usuari, contrasenya, port)
Guarda els paràmetres idBus, línia, nTorns, i descans. Crea l’atribut client on guarda l’objecte de connexió amb el servidor intermediari MQTT.
- activa()¶
Fa el recorregut de la línia, enviant missatges a cada parada pendent d’arribar amb els temps previst per arribar-hi:
Cada parada de la línia és un tema de subscripció del servidor intermediari.
Cada cop que s’arriba a una parada, s’envien missatges a cada parada que falta recórrer indicant el temps previst específic per arribar a cada parada.
Cada missatge té informació de línia, nom de l’autobús, i temps previst d’arribada. El temps previst per cada parada, es calcula mitjançant el mètode
Línia.tempsEntreEstacions()
Un cop publicats els missatges en el servidor, el procés dorm un temps que ve donat pel temps previst per arribar a la següent parada (guardat en línia) més una quantitat aleatòria (
random.randint()
) entre 0 i un 50% del temps previst que simula possibles problemes de circulació.
- bus.bus(nomBus, línia, trajecte, nTorns, espera, dConnexió)¶
Activa el servei planificat l’autobús nomBus de la línia línia
- Parameters:
nomBus (str) – nom de l’autobús.
línia (str) – nom de la línia de l’autobús.
trajecte (list) – llista de parades de la línia a la que pertany el bus. Cada element de la llista inclou un tuple amb el nom de la parada, i el temps previst per arribar a la següent parada.
nTorns (int) – nombre de torns a fer.
espera (int) – temps en minuts pel descans entre torns.
connexió (tuple) – Dades de la connexió al servidor intermediari per implementar el model publicar-subscriure. Consisteix d’un tuple amb (adreça, nom usuari, contrasenya, port)
Execució del programa bus¶
Per executar farem:
bus dades.txt "a01"
On dades.txt
és el fitxer on tenim la descripció de línies, parades, autobusos, i les dades de connexió, i «a01» és el nom de l’autobús descrit en el fitxer dades.txt
.
Per provar els programes bus i parada, per comprovar que es veurà a la parada C quan circula l’autobús a01, tots descrits en el fitxer dades.txt
. Si la descripció la parada “C” forma part de la mateixa línia per la que passa l’autobús, escrivim:
parada dades.txt C & bus dades.txt a01 > logBus &
per comprovar el seu funcionament. També podem engegar els programes en terminals diferents. El canal estàndard de sortida del programa bus el desviem cap al fitxer logBus
per a que no interfereixi la pantalla (terminal) de la parada C, ja que ambdós programes comparteixen el mateix terminal.
Per parar la parada, cal fer comandes de fg i prémer <ctrl><c>, o usar la comanda ps i kill des d’un altre terminal. Des del terminal que té el programa parada, al fer esborrats de pantalla, pot no donar temps de veure el que s’escriu pel terminal.
GestorLínia¶
L’objectiu d’aquest procés és posar en marxa a intervals regulars de temps, cadascun dels autobusos programats per la línia de trajecte encomanada.
L’especificació del mòdul gestorLinia
(a guardar en el fitxer gestorLinia.py
):
- class gestorLinia.GestorLínia(lstBusos, tentre)¶
Bases:
Process
Gestiona una línia d’autobusos.
Classe derivada de
multiprocessing.Process
que executa el mètodeGestorLínia.gestionaServeis()
- __init__(lstBusos, tentre)¶
Iniciador de la classe, guarda els següents paràmetres com a tributs per ser usat en el mètode
GestorLínia.gestionaServeis()
- gestionaServeis()¶
Crea i activa un procés per cada autobús de lstBusos cada tentre minuts. Després espera que cada procés acabi.
- run()¶
Cos del procés. Crida
GestorLínia.gestionaServeis()
- gestorLinia.repartirBusosPerLínia(busos, línies, connexió)¶
Crea un diccionari com s’especifica en el retorn.
- Parameters:
busos (list) – llista d”autobús, on cada autobús és un tuple (nomAutobus, línia a la que pertany, nombre torns previstos, temps descans entre torns)
línies (list) – llista de línia, on cada línia és una llista de tuples (nom parada, temps previst per següent parada)
connexió (tuple) – Dades de la connexió al servidor intermediari per implementar el model publicar-subscriure. Consisteix d’un tuple amb (adreça, nom usuari, contrasenya, port)
- Returns:
diccionari on les claus son els noms de les línies de línies, i els valors són una llista de busos (tipus
bus.Bus
) pertanyents a la línia que indica la clau.
La funció gestorLinia.repartirBusosPerLínia()
es dona feta en el fitxer esborrany.py
. Només cal que l’afegiu al fitxer gestorLinia.py
Central¶
L’objectiu del programa central és el de fer funcionar totes les parades i autobusos que estan descrits en el fitxer de configuració de l’aplicació. A partir de les biblioteca de python subprocess
i multiprocessing
.
Cada procés de parada ha de tenir un terminal associat per poder mostrar les notificacions de la seva pantalla. Una manera de fer-ho és utilitzant el programa xterm que emula un terminal i ve normalment en les distribucions de Linux. Des de Debian, si no està instal·lat, feu:
apt install xterm
El programa té força opcions que podeu veure fent:
man xterm
entre elles, la d’executar un programa un cop s’ha obert el xterm.
La comanda:
xterm -geometry 32x4+50+75 -T prova -fs 10 -bg orange -fg white -e "./parada dades.txt laParada
obra un xterm d’amplada 32 caràcters, d’alçada 4 línies de text, a la posició del píxel de pantalla (50, 75). Tindrà com a títol «prova», la mida de la lletra serà de 10 punts, el color de fons serà taronja, i el color de primer pla serà blanc. Executarà el programa parada amb els arguments donats.
Per executar una comanda com un procés disposem de la funció subprocess.Popen()
de la biblioteca subprocess
.
A partir de les dades aportades pel fitxer de configuració de l’aplicació, el programa central crearà un procés per cada parada existent.
Tot seguit es distribuiran els autobusos segons línies, i crearà un procés per cada línia que gestiona la seva flota d’autobusos.
Quan la circulació d’autobusos ha acabat, el programa emetrà un missatge “Final” a totes les estacions i esperarà que acabin tots els processos.
El programa es guardarà en el fitxer central.py
.
Descàrrega de fitxers¶
Baixeu-vos:
doctests de mòduls¶
Instal·lació de programari¶
Si ho feu des de casa, cal instal·lar els següents paquets (Els podeu posar en una sola comanda):
apt install xterm
apt install mosquitto
apt install python3-paho-mqtt
Notes