Saltearse al contenido

Unidad 3

En esta unidad vas a seguir explorando la integración entre micro:bit y p5.js,
al tiempo que profundizarás en la técnica de programación con máquinas de estado. Volverás a practicar dicha técnica con el micro:bit, pero también en p5.js.

Te preguntarás ¿Por qué tanta insistencia del profe con esta técnica? Y la razón es que ella te permitirá lograr en tus aplicaciones escalabilidad, en términos de conconcurrencia, es decir, permitirá que una aplicación tenga múltiples TAREAS haciendo al mismo tiempo varias cosas.

Set: ¿Qué aprenderás en esta unidad? 💡

Sección titulada «Set: ¿Qué aprenderás en esta unidad? 💡»

Vas a seguir practicando la técnica de programación con máquinas de estado y a transferirla a p5.js.

En la unidad anterior construiste un semáforo con el micro:bit (cierto? pero si no o tu implementación no estuvo bien, entonces tienes una nueva oportunidad). Ahora te pediré que hagas una modificación. Esta vez construirás tres semáforos concurrente en el micro:bit.

Cada uno de los semáforos tendrá unos tiempos en rojo, amarillo y verde diferentes. Recuerda que el micro:bit tiene un solo display de 5x5 leds. Además, todos los leds son de color rojo. Así que tendrás que ser creativo para representar los colores amarillo y verde.

Los tiempos para los semáforos serán los siguientes:

  • Semáforo 1: 5 segundos en rojo, 2 segundos en amarillo y 3 segundos en verde.
  • Semáforo 2: 3 segundos en rojo, 1 segundo en amarillo y 2 segundos en verde.
  • Semáforo 3: 4 segundos en rojo, 3 segundos en amarillo y 2 segundos en verde.

La estructura de tu programa será similar a la siguiente:

class Semaforo:
.
.
.
semaforo1 = Semaforo(...)
semaforo2 = Semaforo(...)
semaforo3 = Semaforo(...)
while True:
semaforo1.update()
semaforo2.update()
semaforo3.update()
Código
# Imports go at the top
from microbit import *
import utime
display.clear()
class Semaforo:
def __init__(self,x,y,tr,ty,tg):
self.x = x
self.y = y
self.tr = tr
self.ty =ty
self.tg = tg
self.startTime = utime.ticks_ms()
self.state = 'WaitInRed'
display.set_pixel(self.x,self.y,9)
def update(self):
if self.state == 'WaitInRed':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) >= self.tr:
display.set_pixel(self.x,self.y,0)
display.set_pixel(self.x,self.y+1,9)
self.startTime = utime.ticks_ms()
self.state = 'WaitInYellow'
elif self.state == 'WaitInYellow':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) >= self.ty:
display.set_pixel(self.x,self.y+1,0)
display.set_pixel(self.x,self.y+2,9)
self.startTime = utime.ticks_ms()
self.state = 'WaitInGreen'
elif self.state == 'WaitInGreen':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) >= self.tg:
display.set_pixel(self.x,self.y+2,0)
display.set_pixel(self.x,self.y,9)
self.startTime = utime.ticks_ms()
self.state = 'WaitInRed'
semaforo1 = Semaforo(0,0,5000,2000,3000)
semaforo2 = Semaforo(1,0,3000,1000,2000)
semaforo3 = Semaforo(2,0,4000,3000,2000)
while True:
semaforo1.update()
semaforo2.update()
semaforo3.update()
  1. Qué ventajas tiene usar una clase (class) en este caso para representar un semáforo?

  2. Puedes ver cómo la técnica de programación con máquinas de estado y el uso de funciona no bloqueantes te permite que varios semáforos funcionen al mismo tiempo?

La técnica de máquina de estados no solo permite la concurrencia sino también la escalabilidad en término de nuevas funcionalidades.

Vas a retomar la bomba de la unidad anterior y le adicionarás algunas funcionalidades:

  • Una vez la bomba esté armada es posible desactivarla con la secuencia botón A, botón B, botón A.
  • Si la secuencia se ingresa correctamente la bomba pasará de nuevo al modo de configuración de lo contrario continuará la fatal cuenta regresiva.
Código
# Imports go at the top
from microbit import *
import utime
display.clear()
class BombTask:
def __init__(self):
self.PASSWORD = ['A','B','A']
self.key = ['']*len(self.PASSWORD)
self.keyindex = 0
self.count = 20
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
display.clear()
display.show(self.count,wait=False)
def update(self):
if self.state == 'CONFIG':
if button_a.was_pressed():
self.count = min(self.count+1,60)
display.show(self.count,wait=False)
if button_b.was_pressed():
self.count = max(10,self.count-1)
display.show(self.count, wait=False)
if accelerometer.was_gesture('shake'):
self.startTime = utime.ticks_ms()
self.state = 'ARMED'
elif self.state == 'ARMED':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) > 1000:
self.startTime = utime.ticks_ms()
self.count = self.count - 1
display.show(self.count,wait=False)
if self.count == 0:
display.show(Image.SKULL)
self.state = 'EXPLODED'
if button_a.was_pressed():
self.key[self.keyindex] = 'A'
self.keyindex = self.keyindex + 1
if button_b.was_pressed():
self.key[self.keyindex] = 'B'
self.keyindex = self.keyindex + 1
if self.keyindex == len(self.key):
passIsOK = True
for i in range(len(self.key)):
if self.key[i] != self.PASSWORD[i]:
passIsOK = False
break;
if passIsOK == True:
self.count = 20
display.show(self.count,wait=False)
self.keyindex = 0
self.state = 'CONFIG'
else:
self.keyindex = 0
elif self.state == 'EXPLODED':
if pin_logo.is_touched():
self.count = 20
display.show(self.count,wait=False)
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
bombTask = BombTask()
while True:
bombTask.update()

Seguiremos adicionando funcionalidades a la bomba.

  • Además de controlar la bomba con los sensores del micro:bit, también podrás controlarla desde el puerto serial, es decir, se controla desde dos fuentes diferentes.
  • Debes hacer un refactoring a tu código. La idea es que ahora la máquina de estados de la bomba no lea directamente los sensores del micro:bit, sino que reciba EVENTOS GENÉRICOS. De esta manera, la máquina de estados de la bomba no sabrá de dónde vienen los eventos, si del micro:bit o del puerto serial.

¿Cómo puedes hacer esto?

Puedes crear una variable que represente el evento que ocurrió. Cuando el evento ocurre escribes la variable con un valor representativo del evento. Además, esa variable debe tener un valor que indique que no ha ocurrido un evento.

Es importante que consumas el evento, es decir, una vez la máquina de estados de la bomba reciba el evento, la variable que representa el evento debe tomar el valor que inidique que no hay evento.

Por ejemplo, la variable event tomará el valor 0 si no hay evento y ‘A’ si se presionó el botón o se recibió la orden por el puerto serial.

En resumen: cada que ocurra un evento, debes almacenar el evento en la variable que lo represeta y cuando lo consumas debes borrarlo.

Estructura de tu programa así:

class Event:
.
.
.
class BombTask:
.
.
.
class SerialTask:
.
.
.
class ButtonTask:
.
.
.
serial = SerialTask()
buttons = ButtonTask()
bomb = BombTask()
while True:
serial.update()
buttons.update()
bomb.update()

¿Cómo enviarás los eventos del serial?

Puedes usar esta aplicación que te permitirá enviar datos por el puerto serial.

Código
# Imports go at the top
from microbit import *
import utime
display.clear()
class Event:
def __init__(self):
self.value = 0
def set(self,_val):
self.value = _val
def clear(self):
self.value = 0
def read(self):
return self.value
class SerialTask:
def __init__(self):
uart.init(baudrate=115200)
def update(self):
if uart.any():
data = uart.read(1)
if data:
if data[0] == ord('A'):
event.set('A')
elif data[0] == ord('B'):
event.set('B')
elif data[0] == ord('S'):
event.set('S')
elif data[0] == ord('T'):
event.set('T')
class ButtonTask:
def __init__(self):
pass
def update(self):
if button_a.was_pressed():
event.set('A')
elif button_b.was_pressed():
event.set('B')
elif accelerometer.was_gesture('shake'):
event.set('S')
elif pin_logo.is_touched():
event.set('T')
class BombTask:
def __init__(self):
self.PASSWORD = ['A','B','A']
self.key = ['']*len(self.PASSWORD)
self.keyindex = 0
self.count = 20
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
display.clear()
display.show(self.count,wait=False)
def update(self):
if self.state == 'CONFIG':
if event.read() == 'A':
event.clear()
self.count = min(self.count+1,60)
display.show(self.count,wait=False)
if event.read() == 'B':
event.clear()
self.count = max(10,self.count-1)
display.show(self.count, wait=False)
if event.read() == 'S':
event.clear()
self.startTime = utime.ticks_ms()
self.state = 'ARMED'
elif self.state == 'ARMED':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) > 1000:
self.startTime = utime.ticks_ms()
self.count = self.count - 1
display.show(self.count,wait=False)
if self.count == 0:
display.show(Image.SKULL)
self.state = 'EXPLODED'
if event.read() == 'A':
event.clear()
self.key[self.keyindex] = 'A'
self.keyindex = self.keyindex + 1
if event.read() == 'B':
event.clear()
self.key[self.keyindex] = 'B'
self.keyindex = self.keyindex + 1
if self.keyindex == len(self.key):
passIsOK = True
for i in range(len(self.key)):
if self.key[i] != self.PASSWORD[i]:
passIsOK = False
break;
if passIsOK == True:
self.count = 20
display.show(self.count,wait=False)
self.keyindex = 0
self.state = 'CONFIG'
else:
self.keyindex = 0
elif self.state == 'EXPLODED':
if event.read() == 'T':
event.clear()
self.count = 20
display.show(self.count,wait=False)
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
bombTask = BombTask()
serialTask = SerialTask()
buttonTask = ButtonTask()
event = Event()
while True:
serialTask.update()
buttonTask.update()
bombTask.update()

En la actividad anterior controlaste la bomba desde una terminal serial. Ahora vas a controlar la bomba desde tu sketch en p5.js.

Código
let port;
let connectBtn;
let connectionInitialized = false;
let validChars = "ABST";
function setup() {
createCanvas(400, 400);
background(220);
port = createSerial();
connectBtn = createButton("Connect to micro:bit");
connectBtn.position(80, 300);
connectBtn.mousePressed(connectBtnClick);
}
function draw() {
background(220);
if (port.opened() && !connectionInitialized) {
port.clear();
connectionInitialized = true;
}
textAlign(CENTER);
text("Press A,B,S,T to simulate micro:bit keys", width / 2, height / 2);
if (!port.opened()) {
connectBtn.html("Connect to micro:bit");
} else {
connectBtn.html("Disconnect");
}
}
function keyPressed() {
keyValue = key.toUpperCase();
if(validChars.includes(keyValue)){
console.log(keyValue);
port.write(keyValue);
}
}
function connectBtnClick() {
if (!port.opened()) {
port.open("MicroPython", 115200);
connectionInitialized = false;
} else {
port.close();
}
}

Como bonus a las actividades anteriores. ¿Qué debo hacer si quiero controlar la bomba desde el micro:bit, p5.js y desde un segundo micro:bit de manera inalámbrica, todo al mismo tiempo?

Respuesta Este sería el código para el control remoto:

from microbit import *
import radio
display.show(Image.HAPPY)
class RadioRemote:
def __init__(self):
radio.config(group=69)
radio.on
def update(self):
if button_a.was_pressed():
radio.send("A")
if button_b.was_pressed():
radio.send("B")
if accelerometer.was_gesture("shake"):
radio.send("S")
if pin_logo.is_touched():
radio.send("T")
radioRemote = RadioRemote()
while True:
radioRemote.update()

Y este sería el código del micro:bit modificado para soportar el control remoto:

from microbit import *
import utime
import radio
display.clear()
class Event:
def __init__(self):
self.value = 0
def write(self,value):
self.value = value
def read(self):
return self.value
def clear(self):
self.value = 0
class MicroBitSensors():
def __init__(self):
pass
def update(self):
if button_a.was_pressed():
event.write("A")
if button_b.was_pressed():
event.write("B")
if accelerometer.was_gesture("shake"):
event.write("S")
if pin_logo.is_touched():
event.write("T")
class RemoteTask:
def __init__(self):
uart.init(baudrate=115200)
def update(self):
if uart.any():
data = uart.read(1)
if data:
if data[0] == ord('A'):
event.write("A")
if data[0] == ord('B'):
event.write("B")
if data[0]== ord('S'):
event.write("S")
if data[0] == ord('T'):
event.write("T")
class RadioRemote:
def __init__(self):
radio.config(group=69)
radio.on
def update(self):
message = radio.receive()
if message:
if message == "A":
event.write("A")
elif message == "B":
event.write("B")
elif message == "S":
event.write("S")
elif message == "T":
event.write("T")
class BombTask:
def __init__(self):
self.PASSWORD = ['A','B','A']
self.key = ['']*len(self.PASSWORD)
self.keyindex = 0
self.count = 20
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
display.clear()
display.show(self.count,wait=False)
def update(self):
if self.state == 'CONFIG':
if event.read()== "A":
event.clear()
self.count = min(self.count+1,60)
display.show(self.count,wait=False)
if event.read()== "B":
event.clear()
self.count = max(10,self.count-1)
display.show(self.count, wait=False)
if event.read()== "S":
event.clear()
self.startTime = utime.ticks_ms()
self.state = 'ARMED'
elif self.state == 'ARMED':
if utime.ticks_diff(utime.ticks_ms(),self.startTime) > 1000:
self.startTime = utime.ticks_ms()
self.count = self.count - 1
display.show(self.count,wait=False)
if self.count == 0:
display.show(Image.SKULL)
self.state = 'EXPLODED'
if event.read()== "A":
event.clear()
self.key[self.keyindex] = 'A'
self.keyindex = self.keyindex + 1
if event.read()== "B":
event.clear()
self.key[self.keyindex] = 'B'
self.keyindex = self.keyindex + 1
if self.keyindex == len(self.key):
passIsOK = True
for i in range(len(self.key)):
if self.key[i] != self.PASSWORD[i]:
passIsOK = False
break;
if passIsOK == True:
self.count = 20
display.show(self.count,wait=False)
self.keyindex = 0
self.state = 'CONFIG'
else:
self.keyindex = 0
elif self.state == 'EXPLODED':
if event.read()== "T":
event.clear()
self.count = 20
display.show(self.count,wait=False)
self.startTime = utime.ticks_ms()
self.state = 'CONFIG'
bombTask = BombTask()
event = Event()
sensors = MicroBitSensors()
remoteTask = RemoteTask()
radioRemote = RadioRemote()
while True:
radioRemote.update()
remoteTask.update()
sensors.update()
bombTask.update()

Es momento de modelar la bomba y definir vectores de prueba

Sección titulada «Es momento de modelar la bomba y definir vectores de prueba»

Ahora es momento de modelar la bomba con una máquina de estados y definir una tabla con vectores de prueba.

En esta actividad vas a transferir la técnica de programación con máquinas de estado a p5.js.

Crea la bomba versión 2.0 en p5.js. No olvides que al aplicar la técnica de máquinas de estado en micro:bit se evitaba colocar acciones por fuera de los eventos. En el caso de p5.js será necesario que tengas acciones por fuera de eventos porque es necesario dibujar el canvas en cada frame.

Vas a practicar de nuevo la técnica de máquina de estados y eventos genéricos, pero esta vez vas a controlar la bomba desde el micro:bit y desde p5.js. TEN PRESENTE que la bomba estará corriendo en p5.js, pero deberás controlarla también desde los botones del micro:bit mediante el puerto serial.

Reflect: Consolidación y metacognición 🤔

Sección titulada «Reflect: Consolidación y metacognición 🤔»

El objetivo aquí es doble. Primero, que recuperes de tu memoria los conceptos de diseño y programación con máquinas de estados sin ayuda externa. Este esfuerzo por recordar (práctica de recuperación) es clave para un aprendizaje duradero. Segundo, que reflexiones sobre tu proceso de diseño y depuración, una habilidad esencial para cualquier ingeniero.

Mi objetivo es crear la mejor experiencia de aprendizaje posible, y tu perspectiva es esencial para lograrlo. Este es tu espacio para darme feedback honesto y directo sobre esta unidad, lo que me ayudará a refinarla para futuros estudiantes.