Baja tensión. Nada de 230 V. GPIO a 3,3 V: meter 5 V en un pin lo rompe.
Proyectos Raspberry Pi
Cien montajes para abrir con doble clic, sin red. Veinte por nivel: Básico, Birdie, Falcon, Eagle y Fénix. El temario va en este mismo archivo.
Orientativo 2024–2026, no es una tienda. La Pi no entra en la cuenta. irrisorio <1 € · poco ≈1–3 € · medio ≈3–6 € · bastante ≈6–9 € · consultar >10 €
Ningún proyecto coincide con esa búsqueda.
Básico
Birdie
Falcon
Eagle
Fénix
01 LED que parpadea
Básico
Qué hace
El LED del GPIO17 se enciende un segundo y se apaga otro, sin que pulses nada. Sirve para comprobar la Pi, el cable y la resistencia antes de cualquier otro montaje. No hay sensor: solo un temporizador y esa salida.
Materiales
Raspberry Pi con header de 40 pines
Protoboard
1 LED de 5 mm
1 resistencia de 330 Ω
2 cables dupont
Cableado
Ánodo del LED (pata larga) a la resistencia de 330 Ω y de ahí al GPIO17, físico 11. Cátodo a GND, físico 6.
Nada va a 5 V (físicos 2 y 4) ni hace falta el 3,3 V (físico 1): el GPIO ya entrega 3,3 V a través de la resistencia. Cada GPIO es de 3,3 V. Meter 5 V en el físico 11 lo rompe.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Código con gpiozero. El parpadeo lo hace el bucle, no una librería de temporizadores externa.
from gpiozero import LED
from time import sleep
led = LED(17)
try:
while True:
led.on()
sleep(1)
led.off()
sleep(1)
finally:
led.close()
Tres LED hacen el ciclo de un semáforo de coches: verde, ámbar y rojo, con tiempos distintos. No hay pulsador ni peatón; eso es la ficha 17. Aquí solo compruebas tres salidas y una secuencia.
Materiales
Raspberry Pi
Protoboard
3 LED (rojo, ámbar y verde)
3 resistencias de 330 Ω
4 cables dupont
Cableado
Rojo: GPIO17, físico 11, con 330 Ω. Ámbar: GPIO27, físico 13, con 330 Ω. Verde: GPIO22, físico 15, con 330 Ω. Los tres cátodos a GND, físico 6.
Nada a 5 V. El 3,3 V del físico 1 no alimenta estos LED: cada uno sale de su GPIO. Meter 5 V en cualquiera de esos pines lo rompe.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero enciende un LED y apaga los otros. Los segundos imitan un ciclo corto de maqueta, no un cruce real.
from gpiozero import LED
from time import sleep
rojo = LED(17)
ambar = LED(27)
verde = LED(22)
def solo(luz, segundos):
rojo.off(); ambar.off(); verde.off()
luz.on()
sleep(segundos)
try:
while True:
solo(verde, 4)
solo(ambar, 1)
solo(rojo, 4)
finally:
rojo.close(); ambar.close(); verde.close()
Mientras mantienes el pulsador, el LED luce. Al soltarlo, se apaga. El botón va a GND y la Pi pone la pull-up interna: no hace falta resistencia externa de pull-up.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
1 pulsador
3 cables dupont
Cableado
LED: GPIO17, físico 11, serie 330 Ω, cátodo a GND físico 6.
Pulsador entre GPIO23, físico 16, y GND físico 9. La otra pareja de patas del pulsador no se usa. Nada a 5 V ni al 3,3 V: la pull-up es interna.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero.Button con pull_up=True lee nivel bajo cuando pulsas. El LED sigue ese nivel.
from gpiozero import LED, Button
from signal import pause
led = LED(17)
boton = Button(23, pull_up=True)
boton.when_pressed = led.on
boton.when_released = led.off
pause()
Cada pulsación cuenta como una, aunque el contacto rebote unos milisegundos. El LED cambia de estado al pulsar y un número en la terminal sube de uno en uno. Sin antirrebote, una sola pulsación puede sumar tres o cuatro.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
1 pulsador
3 cables dupont
Cableado
Igual que la ficha 03: LED en GPIO17 (físico 11) con 330 Ω a GND físico 6. Pulsador en GPIO23 (físico 16) a GND físico 9.
Sigue sin haber 5 V en el circuito. No pongas un condensador todavía: el antirrebote de esta ficha es el de gpiozero, bounce_time=0,05 s.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Button filtra rebotes durante 50 ms. when_pressed solo dispara el flanco útil. El enlace de software es el del rebote, no un retardo inventado en el bucle.
from gpiozero import LED, Button
led = LED(17)
boton = Button(23, pull_up=True, bounce_time=0.05)
cuenta = 0
def al_pulsar():
global cuenta
cuenta += 1
led.toggle()
print("pulsaciones validas:", cuenta)
boton.when_pressed = al_pulsar
from signal import pause
pause()
Un LED RGB de cátodo común muestra rojo, luego verde, luego azul, nunca mezclados. Cada canal lleva su resistencia. Si más adelante usas el relé de GPIO12, este RGB puede convivir en pines, pero no enciendas a la vez un montaje que robe estos GPIO.
Materiales
Raspberry Pi
Protoboard
1 LED RGB de cátodo común
3 resistencias de 330 Ω
4 cables dupont
Cableado
Rojo del RGB a GPIO16, físico 36, con 330 Ω. Verde a GPIO20, físico 38, con 330 Ω. Azul a GPIO21, físico 40, con 330 Ω. Cátodo común a GND, físico 39.
Nada a 5 V. Un ánodo común no vale para este código: gpiozero aquí pone el GPIO a nivel alto para encender. Meter 5 V en el físico 36, 38 o 40 lo rompe.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
RGBLED(16, 20, 21) con color (1,0,0), (0,1,0) y (0,0,1). Un 1 es encendido pleno, no un ciclo PWM mezclado.
from gpiozero import RGBLED
from time import sleep
rgb = RGBLED(16, 20, 21)
try:
while True:
rgb.color = (1, 0, 0)
sleep(1)
rgb.color = (0, 1, 0)
sleep(1)
rgb.color = (0, 0, 1)
sleep(1)
finally:
rgb.close()
Un zumbador activo suena mientras el GPIO25 está alto y calla cuando está bajo. No genera notas: el tono lo trae el propio componente. Tres pitidos cortos y un silencio, como un aviso de que el programa sigue vivo.
Materiales
Raspberry Pi
Protoboard
1 zumbador activo de módulo, apto para 3,3 V en la patilla de señal
3 cables dupont
Cableado
Señal del módulo (I/O o S) al GPIO25, físico 22. GND del módulo al físico 6.
VCC del módulo al 3,3 V, físico 1, si el módulo lo admite. Si el texto del módulo exige 5 V, VCC al físico 2 y la señal sigue en el GPIO25: no pongas el positivo de un zumbador de 5 V directo al GPIO. Cada GPIO es de 3,3 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero.Buzzer enciende y apaga. No uses TonalBuzzer aquí: eso es el zumbador pasivo de la ficha 07.
from gpiozero import Buzzer
from time import sleep
z = Buzzer(25)
try:
while True:
for _ in range(3):
z.on()
sleep(0.15)
z.off()
sleep(0.15)
sleep(1)
finally:
z.off()
z.close()
Lee la serigrafía: activo, no pasivo. El activo suele llevar una pegatina + o un oscilador en la placa.
Señal al físico 22, masa al 6, VCC según el párrafo de cableado.
Ejecuta. Deben oírse tres pitidos y una pausa.
Si no suena y el módulo es de 5 V con VCC en 3,3 V, cámbialo al físico 2 sin mover la señal.
No dejes el zumbador en alto permanente: molesta y calienta el módulo.
Ctrl+C y comprueba el silencio.
Circuito
Variantes
En la Pi 5 no uses RPi.GPIO. gpiozero sí, con el backend lgpio.
Un solo pitido largo de 1 s al arrancar y luego silencio, para usarlo como «listo».
Sácalo a GPIO18, físico 12, si más adelante el 25 lo necesita el RST de un RFID: dilo en tu copia, esta ficha usa el 25.
Fallos típicos
Zumbador pasivo en este código: oirás clics, no un tono, porque no hay PWM de nota.
VCC y señal intercambiados.
Módulo alimentado a 5 V con la señal mal entendida como alimentación y el GPIO unido al positivo.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 25
Línea buena: 25: op dh pn | hi // GPIO25 = output mientras pita
Línea mala: 25: ip pd | lo // GPIO25 = input
07 Zumbador pasivo, una nota
Básico
Qué hace
Un piezo pasivo no suena solo: hay que mandarle una frecuencia. Esta ficha toca un la4 (440 Hz) durante un segundo y calla otro. Es PWM de sonido en el GPIO18, no el zumbador activo del físico 22.
Materiales
Raspberry Pi
Protoboard
1 zumbador pasivo o piezo sin oscilador
1 resistencia de 330 Ω en serie
2 cables dupont
Cableado
GPIO18, físico 12, resistencia de 330 Ω, pata del piezo, la otra pata a GND físico 6.
Nada a 5 V ni a 3,3 V. El piezo lo mueve el PWM del GPIO. No lo conectes al físico 22: ese es el GPIO25, reservado al zumbador activo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
TonalBuzzer y Tone de gpiozero. La nota sale del PWM; no hay fichero de audio.
from gpiozero import TonalBuzzer
from gpiozero.tones import Tone
from time import sleep
z = TonalBuzzer(18)
try:
while True:
z.play(Tone("A4"))
sleep(1)
z.stop()
sleep(1)
finally:
z.stop()
z.close()
Confirma que el componente no pone «active». Si es módulo activo, vuelve a la ficha 06.
Serie 330 Ω entre el físico 12 y el piezo.
Ejecuta y escucha un tono estable, no un chasquido.
Cambia la cadena a C5 en una copia si quieres oír que la nota sí depende del código.
Vuelve a A4 para dejar la ficha como está.
Ctrl+C. Si el piezo queda zumbando, no llamaste a stop().
Circuito
Variantes
En la Pi 5 no uses RPi.GPIO. gpiozero sí, con el backend lgpio.
Tres notas seguidas, C4 E4 G4, un segundo cada una, y silencio.
Bajar el volumen no es bajar la tensión a 5 V: acorta el tiempo de play o acepta que este piezo es binario en la práctica.
Fallos típicos
Piezo activo con TonalBuzzer: el tono interno pelea con el PWM y suena sucio.
Cable en el físico 11 por costumbre del LED.
Quitar la resistencia «porque es un altavoz»: el GPIO no alimenta un altavoz de bobina.
Franja de precio
Franja de precio: irrisorio (<1 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 18
Línea buena: 18: a0 pn | hi // GPIO18 en función PWM o op alternando
Línea mala: 18: ip pd | lo // GPIO18 = input, el piezo no recibe señal
08 Dos botones y un LED (AND y OR)
Básico
Qué hace
Dos pulsadores y un LED muestran en la misma protoboard una puerta AND y una OR, elegidas al arrancar. En AND el LED solo luce si estás pulsando los dos. En OR basta con uno. Los dos van a GND con pull-up interna.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 pulsadores
5 cables dupont
Cableado
LED en GPIO17, físico 11, 330 Ω, cátodo a GND físico 6.
Primer pulsador: GPIO23, físico 16, a GND físico 9. Segundo: GPIO24, físico 18, a GND físico 14. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Se leen is_pressed, que en gpiozero es True cuando el contacto cierra a GND. La lógica es Python, no un chip 74HC.
from gpiozero import LED, Button
from time import sleep
led = LED(17)
a = Button(23, pull_up=True)
b = Button(24, pull_up=True)
modo = input("Escribe AND o OR: ").strip().upper()
if modo not in ("AND", "OR"):
raise SystemExit("Solo AND o OR")
try:
while True:
if modo == "AND":
led.value = a.is_pressed and b.is_pressed
else:
led.value = a.is_pressed or b.is_pressed
sleep(0.05)
finally:
led.close()
Una palanca se queda donde la dejas, no como un pulsador. El LED copia esa posición: palanca cerrada a GND, LED encendido. El programa no hace memoria; la memoria la tiene el interruptor.
Materiales
Raspberry Pi
Protoboard
1 interruptor de palanca
1 LED
1 resistencia de 330 Ω
3 cables dupont
Cableado
Un polo de la palanca al GPIO23, físico 16. El otro polo a GND, físico 9. LED en GPIO17, físico 11, con 330 Ω, cátodo a GND físico 6.
Nada a 5 V ni al 3,3 V. Pull-up interna. Si tu palanca es de tres patas, usa común y un extremo, no los dos extremos.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DigitalInputDevice con pull_up sigue el contacto sin tratarlo como un clic. El LED refleja pin.value == 0, porque cerrado a GND es nivel bajo.
from gpiozero import LED, DigitalInputDevice
from time import sleep
led = LED(17)
palanca = DigitalInputDevice(23, pull_up=True)
try:
while True:
cerrado_a_gnd = not palanca.value
led.value = cerrado_a_gnd
print("palanca cerrada" if cerrado_a_gnd else "palanca abierta")
sleep(0.2)
finally:
led.close(); palanca.close()
Cuatro LED se encienden en orden, como una carrera de un extremo a otro de la protoboard, y vuelven. El cuarto usa el GPIO10 porque esta ficha no toca el SPI. Si más adelante montas un RFID, ese pin deja de estar libre.
Materiales
Raspberry Pi
Protoboard
4 LED
4 resistencias de 330 Ω
5 cables dupont
Cableado
GPIO17 físico 11, GPIO27 físico 13, GPIO22 físico 15 y GPIO10 físico 19. Cada uno con su 330 Ω y el cátodo al GND físico 6.
Nada a 5 V. No pongas el cuarto LED en el físico 21: ese es MISO, GPIO9, y no es el LED4 de este mapa.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Un LEDBoard enciende un índice y apaga el resto. El sentido se invierte al llegar al final.
from gpiozero import LEDBoard
from time import sleep
luces = LEDBoard(17, 27, 22, 10)
indice = 0
paso = 1
try:
while True:
luces.off()
luces[indice].on()
sleep(0.15)
indice += paso
if indice in (0, 3):
paso = -paso
indice += paso
finally:
luces.close()
Siete LED en forma de dado muestran una cara al azar cuando pulsas el botón. No es un dado de pantalla: cada punto es un LED con su resistencia, y el GPIO10 entra porque aquí no hay SPI.
Pulsador del GPIO23, físico 16, a GND físico 9. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero elige una cara y enciende solo los LED de esa cara. El azar es el módulo random de la biblioteca estándar.
from gpiozero import LED, Button
from random import randint
from signal import pause
# orden: AI AD CI C CD BI BD
pines = (17, 27, 22, 10, 16, 20, 21)
leds = [LED(p) for p in pines]
caras = {
1: (3,),
2: (0, 6),
3: (0, 3, 6),
4: (0, 1, 5, 6),
5: (0, 1, 3, 5, 6),
6: (0, 1, 2, 4, 5, 6),
}
def tirar():
for led in leds:
led.off()
for i in caras[randint(1, 6)]:
leds[i].on()
Button(23, pull_up=True, bounce_time=0.05).when_pressed = tirar
tirar()
pause()
Un display de un dígito, cátodo común, cuenta del 0 al 9 y vuelve a empezar. Cada segmento es un LED y lleva su resistencia. No hay circuito integrado de driver: los siete GPIO los pone gpiozero.
Materiales
Raspberry Pi
Protoboard
1 display de 7 segmentos, cátodo común
7 resistencias de 330 Ω
8 cables dupont
Cableado
Segmento a GPIO17 físico 11, b GPIO27 físico 13, c GPIO22 físico 15, d GPIO10 físico 19, e GPIO16 físico 36, f GPIO20 físico 38, g GPIO21 físico 40. Cada uno con 330 Ω.
Cátodo común a GND físico 6. El punto decimal no se usa. Nada a 5 V. Este mapa usa el GPIO10: no lo montes junto a un dispositivo SPI.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Una tabla dice qué segmentos se encienden para cada cifra. gpiozero solo obedece esa tabla.
from gpiozero import LED
from time import sleep
seg = [LED(p) for p in (17, 27, 22, 10, 16, 20, 21)]
# a b c d e f g
cifras = {
0: "1111110", 1: "0110000", 2: "1101101", 3: "1111001",
4: "0110011", 5: "1011011", 6: "1011111", 7: "1110000",
8: "1111111", 9: "1111011",
}
def mostrar(n):
for led, bit in zip(seg, cifras[n]):
led.value = bit == "1"
try:
while True:
for n in range(10):
mostrar(n)
sleep(0.7)
finally:
for led in seg:
led.close()
Cuando tapas la fotorresistencia, se enciende el LED. No hay conversor analógico: gpiozero mide cuánto tarda un condensador en cargarse a través de la LDR. Oscuridad, resistencia alta, carga lenta, valor bajo, LED encendido.
Materiales
Raspberry Pi
Protoboard
1 LDR
1 condensador de 1 µF
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
LED en GPIO17, físico 11, con 330 Ω, cátodo a GND físico 6.
LDR entre el 3,3 V (físico 1) y el GPIO27 (físico 13). Condensador de 1 µF entre ese mismo GPIO27 y GND físico 9. No lleves la LDR a un pin de 5 V ni a un GPIO creyendo que es un ADC.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
LightSensor no es un voltaje. when_dark y when_light usan el tiempo de carga. El umbral por defecto es 0,1.
from gpiozero import LED, LightSensor
from signal import pause
led = LED(17)
ldr = LightSensor(27)
ldr.when_dark = led.on
ldr.when_light = led.off
pause()
El cursor de un potenciómetro no toca ningún GPIO. Entra en el canal AIN0 de un ADS1115, y el número en bruto manda el brillo PWM del LED. No calculamos voltios: el ciclo de trabajo es ese entero partido del fondo de escala.
Materiales
Raspberry Pi
Protoboard
1 ADS1115
1 potenciómetro de 10 kΩ
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
SDA del ADS al GPIO2, físico 3. SCL al GPIO3, físico 5. VDD del ADS al 3,3 V, físico 1. GND al físico 6. ADDR a GND para la dirección 0x48.
Extremos del potenciómetro: uno al 3,3 V y otro a GND. Cursor solo a AIN0. LED en GPIO17, físico 11, con 330 Ω, cátodo a GND. Nada del cursor va a un GPIO. I2C se activa con dtparam=i2c_arm=on en config.txt.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El ADS se lee por /dev/i2c-1. La palabra 0xC383 es un disparo único de AIN0 con PGA ±4,096 V, sacada del datasheet, no una calibración. El entero se lleva a gpiozero.PWMLED. Los voltios, si los quieres, están en Software, no aquí.
import fcntl, os, time
from gpiozero import PWMLED
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
led = PWMLED(17)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
valor = (hi << 8) | lo
if valor & 0x8000:
valor -= 0x10000
return valor
try:
while True:
n = max(0, min(32767, crudo()))
led.value = n / 32767
print("entero", n)
time.sleep(0.1)
finally:
led.close()
os.close(fd)
Si pulsas el botón, el zumbador activo pita y el LED se queda encendido hasta que vuelves a pulsar. Es un aviso que tú confirmas, no una sirena que se calla sola al soltar.
Materiales
Raspberry Pi
Protoboard
1 pulsador
1 zumbador activo de 3,3 V en la señal
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
Pulsador GPIO23 físico 16 a GND físico 9. LED GPIO17 físico 11 con 330 Ω a GND físico 6.
Zumbador activo: señal al GPIO25 físico 22, GND al físico 14, VCC al 3,3 V físico 1 si el módulo es de 3,3 V. Si exige 5 V, VCC al físico 2 y la señal sigue en el GPIO, nunca el positivo del zumbador de 5 V directo al pin.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Un booleano alterna la alarma en cada pulsación filtrada. Buzzer y LED van juntos.
from gpiozero import LED, Button, Buzzer
from signal import pause
led = LED(17)
z = Buzzer(25)
armada = False
def cambiar():
global armada
armada = not armada
if armada:
led.on()
z.beep(on_time=0.2, off_time=0.2)
else:
led.off()
z.off()
Button(23, pull_up=True, bounce_time=0.05).when_pressed = cambiar
pause()
Los coches siguen en verde hasta que pides paso. Entonces pasan a ámbar, a rojo, y el peatón tiene verde unos segundos. Al acabar, el peatón vuelve a rojo y los coches a verde. El botón solo pide; no mantiene el verde peatonal mientras lo pulses.
Materiales
Raspberry Pi
Protoboard
5 LED
5 resistencias de 330 Ω
1 pulsador
cables dupont
Cableado
Coches: rojo GPIO17 físico 11, ámbar GPIO27 físico 13, verde GPIO22 físico 15. Peatón: rojo GPIO16 físico 36, verde GPIO20 físico 38. Todos con 330 Ω y cátodo a GND físico 6.
Pulsador GPIO23 físico 16 a GND físico 9. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Un hilo de gpiozero no hace falta: el botón marca un aviso y el bucle principal hace la secuencia cuando le toca.
from gpiozero import LED, Button
from time import sleep
c_rojo, c_ambar, c_verde = LED(17), LED(27), LED(22)
p_rojo, p_verde = LED(16), LED(20)
pedido = False
def pedir():
global pedido
pedido = True
Button(23, pull_up=True, bounce_time=0.05).when_pressed = pedir
def coches_verde():
c_rojo.off(); c_ambar.off(); c_verde.on()
p_rojo.on(); p_verde.off()
try:
coches_verde()
while True:
sleep(0.1)
if not pedido:
continue
pedido = False
c_verde.off(); c_ambar.on(); sleep(1.2)
c_ambar.off(); c_rojo.on()
p_rojo.off(); p_verde.on(); sleep(4)
p_verde.off(); p_rojo.on(); sleep(1)
coches_verde()
finally:
for luz in (c_rojo, c_ambar, c_verde, p_rojo, p_verde):
luz.close()
Un LED late despacio mientras este programa está vivo. No mira la red ni el disco: solo dice que el script sigue en marcha. Si matas el proceso, el latido para y el LED queda apagado.
Escribes una frase corta y el LED la deletrea en morse. El punto dura 0,2 s, la raya tres puntos, y la pausa entre letras es de tres puntos. No hay sonido: solo luz.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
GPIO17 físico 11, 330 Ω, LED, GND físico 6. Nada a 5 V.
No hace falta botón: el texto entra por el teclado de la terminal.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
La tabla de morse está en el propio script. gpiozero solo enciende y apaga el LED el tiempo que toca.
from gpiozero import LED
from time import sleep
led = LED(17)
PUNTO = 0.2
MORSE = {
"A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".",
"F": "..-.", "G": "--.", "H": "....", "I": "..", "J": ".---",
"K": "-.-", "L": ".-..", "M": "--", "N": "-.", "O": "---",
"P": ".--.", "Q": "--.-", "R": ".-.", "S": "...", "T": "-",
"U": "..-", "V": "...-", "W": ".--", "X": "-..-", "Y": "-.--",
"Z": "--..", "0": "-----", "1": ".----", "2": "..---", "3": "...--",
"4": "....-", "5": ".....", "6": "-....", "7": "--...", "8": "---..",
"9": "----.",
}
def elemento(sim):
led.on()
sleep(PUNTO if sim == "." else PUNTO * 3)
led.off()
sleep(PUNTO)
texto = input("Texto: ").upper()
try:
for letra in texto:
if letra == " ":
sleep(PUNTO * 4)
continue
codigo = MORSE.get(letra)
if not codigo:
continue
for sim in codigo:
elemento(sim)
sleep(PUNTO * 2)
finally:
led.close()
Cuatro pasos se repiten. En cada paso luce un LED y, si ese paso está marcado, el zumbador pasivo da un clic tonal corto. El patrón inicial es bombo, silencio, caja, silencio. El GPIO10 es el cuarto LED porque no hay SPI, y el sonido va al GPIO18.
Materiales
Raspberry Pi
Protoboard
4 LED
4 resistencias de 330 Ω
1 zumbador pasivo
1 resistencia de 330 Ω para el piezo
cables dupont
Cableado
LED en GPIO17 físico 11, GPIO27 físico 13, GPIO22 físico 15 y GPIO10 físico 19, cada uno con 330 Ω, cátodos a GND físico 6.
Piezo pasivo: GPIO18 físico 12, serie 330 Ω, otra pata a GND físico 14. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
TonalBuzzer toca un golpe breve, no una melodía. Los LED marcan el paso aunque el paso esté en silencio.
from gpiozero import LED, TonalBuzzer
from gpiozero.tones import Tone
from time import sleep
leds = [LED(p) for p in (17, 27, 22, 10)]
z = TonalBuzzer(18)
patron = [True, False, True, False]
try:
while True:
for i, suena in enumerate(patron):
for j, led in enumerate(leds):
led.value = j == i
if suena:
z.play(Tone(220))
sleep(0.08)
z.stop()
sleep(0.22)
finally:
z.stop()
for led in leds:
led.close()
z.close()
Lees temperatura y humedad de un DHT11 por el driver del kernel, no por una fórmula escrita aquí. Si el fichero sysfs no aparece, el script se para y lo dice. No usa el GPIO4: el dato va al GPIO26.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
cables dupont
Cableado
VCC del DHT11 al 3,3 V, físico 1. GND al físico 6. Datos al GPIO26, físico 37. Resistencia de 10 kΩ entre datos y 3,3 V.
No alimentes el DHT11 a 5 V: la línea de datos subiría de 3,3 V. No uses el GPIO4, que queda para el DS18B20.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
En /boot/firmware/config.txt añade dtoverlay=dht11,gpiopin=26 y reinicia. El kernel escribe in_temp_input en miligrados e in_humidityrelative_input en milésimas. Dividir entre 1000 cambia de unidad del driver, no calibra el sensor. gpiozero no trae clase DHT11.
import glob, os, time
def leer():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
h = os.path.join(base, "in_humidityrelative_input")
if os.path.exists(t) and os.path.exists(h):
temp = int(open(t).read().strip()) / 1000.0
hum = int(open(h).read().strip()) / 1000.0
return temp, hum
raise SystemExit("No veo el DHT en sysfs. Revisa dtoverlay=dht11,gpiopin=26. No invento la cifra.")
while True:
print(leer())
time.sleep(2)
El script encuentra el BMP280 en el bus I2C, lee el identificador del registro 0xD0 y, si cuadra, enseña los enteros en bruto de presión y temperatura. Esos enteros no son hectopascales ni grados. La compensación está en Software, no en una fórmula inventada aquí.
Materiales
Raspberry Pi
Protoboard
1 módulo BMP280 a 3,3 V
cables dupont
Cableado
VCC al 3,3 V físico 1. GND al físico 9. SDA al GPIO2 físico 3. SCL al GPIO3 físico 5.
SDO o CSB según el módulo: déjalos como indique la placa para la dirección 0x76. No alimentes el módulo a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Hace falta dtparam=i2c_arm=on. i2cdetect -y 1 debe mostrar 0x76 o 0x77. El 0x58 en el registro 0xD0 es el BMP280; 0x60 sería un BME280, y tampoco convertimos su humedad aquí.
import fcntl, os, time
I2C_SLAVE = 0x0703
def abrir(addr):
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, addr)
return fd
def id_chip(fd):
os.write(fd, bytes([0xD0]))
return os.read(fd, 1)[0]
def crudo(fd):
os.write(fd, bytes([0xF4, 0x25]))
time.sleep(0.02)
os.write(fd, bytes([0xF7]))
d = os.read(fd, 6)
pres = (d[0] << 12) | (d[1] << 4) | (d[2] >> 4)
temp = (d[3] << 12) | (d[4] << 4) | (d[5] >> 4)
return pres, temp
fd = None
for addr in (0x76, 0x77):
try:
candidato = abrir(addr)
except OSError:
continue
ident = id_chip(candidato)
print("direccion", hex(addr), "id", hex(ident))
if ident in (0x58, 0x60):
fd = candidato
break
os.close(candidato)
if fd is None:
raise SystemExit("No hay BMP280/BME280. No invento presión.")
print("id 0x60 es BME280: la humedad no se calcula en esta ficha.")
while True:
pres, temp = crudo(fd)
print("crudo presion", pres, "crudo temperatura", temp)
time.sleep(1)
Sopla hacia el sensor: el entero de presión puede moverse. Sigue siendo crudo.
No multipliques por un factor sacado de un foro para «ver hPa».
Si el id es otro, no es este chip.
Circuito
Variantes
Fija la dirección si tu módulo no responde en las dos.
Lee solo una vez y sal, para pegar el crudo en un cuaderno.
Fallos típicos
Módulo a 5 V: el bus I2C de la Pi es de 3,3 V.
Creer que el crudo ya son °C. No lo son.
CSB en modo SPI y el chip no aparece en i2cdetect.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: en la cuadrícula aparece 76 (BMP280, o 77 si SDO está alto), no dos guiones en esa celda.
Línea mala: Could not open file /dev/i2c-1, o toda la tabla en guiones y el chip no sale.
23 DS18B20 a prueba de agua
Birdie
Qué hace
Una sonda estanca DS18B20, con tres hilos, publica su temperatura por el bus 1-Wire del kernel. El número t= del fichero w1_slave lo escribe el driver. Esta ficha no inventa la conversión: pasa de miligrados a grados porque así está la unidad del kernel.
Materiales
Raspberry Pi
Protoboard
1 DS18B20 impermeable de tres hilos
1 resistencia de 4,7 kΩ
cables dupont
Cableado
Rojo de la sonda al 3,3 V físico 1. Negro a GND físico 9. Amarillo (datos) al GPIO4, físico 7. Resistencia de 4,7 kΩ entre datos y 3,3 V.
No uses modo parásito en esta ficha: los tres hilos van conectados. El dato no va al GPIO26; ese es el DHT11.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
En config.txt: dtoverlay=w1-gpio y reinicia. El directorio 28-… aparece bajo /sys/bus/w1/devices. Si pone NO en vez de YES, la lectura se descarta.
import glob, os, time
def leer():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
raise SystemExit("No hay DS18B20. Revisa dtoverlay=w1-gpio. No invento grados.")
texto = open(rutas[0]).read()
if "YES" not in texto:
raise SystemExit("CRC no válida. No uso esa línea.")
mili = int(texto.strip().split("t=")[-1])
return os.path.basename(os.path.dirname(rutas[0])), mili / 1000.0
while True:
print(leer())
time.sleep(1)
Cablea rojo, negro y amarillo. La pull-up es de 4,7 kΩ, no de 10 kΩ.
Añade dtoverlay=w1-gpio y reinicia.
Comprueba que existe una carpeta 28- bajo /sys/bus/w1/devices.
Ejecuta. El nombre de la carpeta es el identificador de esa sonda.
Sujeta la cápsula con los dedos: el número debe subir despacio.
No la metas aún en agua hasta que el sellado de la tuya esté claro. El cable sí puede mojarse; la unión de los hilos, no, si no está aislada.
Circuito
Variantes
Imprime solo si el cambio es mayor de 0,2 grados.
Deja el identificador escrito para no mezclarla cuando añadas otra en la ficha 72.
Fallos típicos
Pull-up ausente: a veces lees y a veces no.
Datos en el GPIO26.
Tomar una línea con CRC malo y convertirla igual.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: una carpeta 28- y doce hexadecimales, además de w1_bus_master1.
Línea mala: solo w1_bus_master1, o No such file or directory en /sys/bus/w1.
24 PIR enciende una luz
Birdie
Qué hace
El PIR ve movimiento y enciende el LED. Cuando deja de verlo, el LED se apaga. No manda una alarma a ninguna parte: la luz está en la mesa.
Materiales
Raspberry Pi
Protoboard
1 módulo PIR HC-SR501 o similar
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
VCC del PIR al 5 V, físico 2, si el módulo lo pide. GND al físico 14. Salida OUT al GPIO5, físico 29.
LED GPIO17 físico 11 con 330 Ω a GND físico 6. Mide OUT con un polímetro antes de fiarte: si en reposo o en detección pasa de 3,3 V, no lo conectes al GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
MotionSensor de gpiozero. El retardo largo lo marca el potenciómetro del propio módulo, no solo el código.
from gpiozero import LED, MotionSensor
from signal import pause
led = LED(17)
pir = MotionSensor(5)
pir.when_motion = led.on
pir.when_no_motion = led.off
pause()
Mides una distancia en metros con el HC-SR04. El disparo sale directo del GPIO5, que aguanta los 3,3 V. El eco no entra directo: un divisor baja los 5 V del eco antes del GPIO6. El número lo calcula gpiozero a partir del tiempo de vuelo; no es una fórmula de calibración inventada en la ficha.
Materiales
Raspberry Pi
Protoboard
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
cables dupont
Cableado
VCC del HC-SR04 al 5 V, físico 2. GND al físico 14. TRIG al GPIO5, físico 29, sin resistencia.
ECHO → resistencia de 1 kΩ → GPIO6, físico 31. Desde ese nudo del GPIO6, resistencia de 2 kΩ a GND físico 20. Así el pico de 5 V queda cerca de 3,3 V. No puentes el eco al GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DistanceSensor(echo=6, trigger=5). distance está en metros. max_distance evita esperas eternas si no hay eco.
from gpiozero import DistanceSensor
from time import sleep
sensor = DistanceSensor(echo=6, trigger=5, max_distance=2)
try:
while True:
print(round(sensor.distance, 3), "m")
sleep(0.4)
finally:
sensor.close()
Monta el divisor antes de dar corriente: 1 kΩ en serie desde ECHO y 2 kΩ de GPIO6 a masa.
TRIG al físico 29. VCC al físico 2.
Ejecuta y apunta al techo o a una pared a menos de 2 m.
Acerca la mano: el número baja.
Si se queda clavado en el máximo, el eco no llega o el divisor está al revés.
No mires el sensor de frente a otra placa que también emita ultrasonidos.
Circuito
Variantes
max_distance=0.5 si solo te importa el rango de una mesa.
Media de cinco lecturas antes de imprimir, para aquietar el ruido.
Fallos típicos
ECHO directo al GPIO6: 5 V en un pin de 3,3 V.
TRIG y ECHO intercambiados.
Alimentar el HC-SR04 a 3,3 V y esperar el alcance de la hoja: muchos módulos no miden bien así. Esta ficha lo alimenta a 5 V.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 6
Línea buena: 6: ip pd | hi // GPIO6 = input, el eco ya dividido
Línea mala: 6: op dh pn | hi // GPIO6 = output: el eco tiene que entrar, no salir
26 Sensor de lluvia
Birdie
Qué hace
La placa de lluvia, por su salida digital, enciende el LED cuando el comparador del módulo ve agua. No mide milímetros: solo distingue seco y mojado al umbral que tú dejas en el potenciómetro del módulo.
Materiales
Raspberry Pi
Protoboard
1 módulo de lluvia con salida digital, alimentable a 3,3 V
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
VCC del módulo al 3,3 V, físico 1. GND al físico 9. DO al GPIO27, físico 13.
LED GPIO17 físico 11 con 330 Ω a GND físico 6. Si tu módulo solo vive a 5 V, no conectes DO al GPIO: esa salida sería de 5 V. No uses la pata analógica en un GPIO; si la quieres, es cosa del ADS1115, no de esta ficha.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El DO suele ir a nivel bajo cuando detecta. Lo leemos con pull-up y encendemos el LED en ese caso. Comprueba el serigrafiado de tu placa por si es al revés.
from gpiozero import LED, DigitalInputDevice
from time import sleep
led = LED(17)
lluvia = DigitalInputDevice(27, pull_up=True)
try:
while True:
mojado = not lluvia.value
led.value = mojado
print("mojado" if mojado else "seco")
sleep(0.3)
finally:
led.close()
Una sonda capacitiva de suelo entra en el ADS1115. Tú anotas el entero con la tierra seca y el entero con la tierra recién regada. El LED se enciende entre medias, cuando toca avisar. No hay un porcentaje: el script compara enteros que tú has escrito.
Materiales
Raspberry Pi
Protoboard
1 sonda capacitiva de suelo que acepte 3,3 V
1 ADS1115
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
ADS: SDA físico 3, SCL físico 5, VDD 3,3 V físico 1, GND físico 6, ADDR a GND.
Sonda: VCC al mismo 3,3 V, GND común, salida analógica solo a AIN0. LED GPIO17 físico 11 con 330 Ω a GND. El cursor o la salida analógica no van a un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Misma lectura cruda del ADS que en la ficha 14. SECO y MOJADO los cambias tú después de mirar la terminal. I2C tiene que estar activo en config.txt.
import fcntl, os, time
from gpiozero import LED
I2C_SLAVE = 0x0703
SECO = 20000
MOJADO = 8000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
led = LED(17)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
valor = (hi << 8) | lo
if valor & 0x8000:
valor -= 0x10000
return valor
try:
while True:
n = crudo()
# Avisa si estás más seco que el punto medio de tus dos notas.
umbral = (SECO + MOJADO) // 2
led.value = n >= umbral if SECO > MOJADO else n <= umbral
print("entero", n, "umbral", umbral)
time.sleep(1)
finally:
led.close()
Un interruptor reed y un imán en la puerta. Con la puerta cerrada el imán junta el contacto y el LED está apagado. Al abrir, el contacto se separa y el LED se enciende. Es el mismo tipo de lectura que un botón a GND.
Materiales
Raspberry Pi
Protoboard
1 reed de puerta, contacto cerrado con imán
1 imán
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
Un hilo del reed al GPIO23, físico 16. El otro a GND físico 9. LED GPIO17 físico 11 con 330 Ω a GND físico 6.
Nada a 5 V. Pull-up interna. Si tu reed es del tipo abierto con imán, la lógica se invierte: pruébalo con el imán en la mano antes de pegarlo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero ve nivel bajo cuando el reed cierra a masa, que aquí significa puerta cerrada.
from gpiozero import LED, DigitalInputDevice
from time import sleep
led = LED(17)
reed = DigitalInputDevice(23, pull_up=True)
try:
while True:
cerrada = not reed.value
led.value = not cerrada
print("cerrada" if cerrada else "abierta")
sleep(0.2)
finally:
led.close()
Un interruptor de bola cierra o abre según tumba la caja. Si la caja se inclina, pita el zumbador activo y se enciende el LED. Sirve para una maqueta o una tapa, no para un sistema de seguridad de una máquina.
Materiales
Raspberry Pi
Protoboard
1 sensor de inclinación tipo bola
1 zumbador activo
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
Sensor entre GPIO23 físico 16 y GND físico 9. Pull-up interna.
LED GPIO17 físico 11 con 330 Ω a GND físico 6. Zumbador activo: señal GPIO25 físico 22, GND físico 14, VCC al 3,3 V físico 1 si el módulo es de 3,3 V. Nada de 5 V en los GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Mientras el contacto está cerrado se considera inclinada. Si tu sensor es al revés, cambia la comparación, no el cable a 5 V.
from gpiozero import LED, Buzzer, DigitalInputDevice
from time import sleep
led = LED(17)
z = Buzzer(25)
sensor = DigitalInputDevice(23, pull_up=True)
try:
while True:
inclinado = not sensor.value
led.value = inclinado
if inclinado:
z.on()
else:
z.off()
sleep(0.1)
finally:
z.off(); led.close(); z.close()
El módulo de llama solo enciende un LED y un pitido si su salida digital se activa. No abre una válvula, no apaga un fuego y no sustituye a un detector de incendios. Es un sensor de hobby para ver que la entrada cambia.
Materiales
Raspberry Pi
Protoboard
1 módulo detector de llama con salida digital a 3,3 V
1 LED
1 resistencia de 330 Ω
1 zumbador activo
cables dupont
Cableado
VCC del módulo al 3,3 V físico 1. GND al físico 6. DO al GPIO27 físico 13. Si DO puede poner 5 V, no lo conectes.
LED GPIO17 físico 11 con 330 Ω a GND físico 9. Zumbador activo en GPIO25 físico 22, GND físico 14. No hay relé ni nada que controle una llama.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El aviso es local. El script no intenta «gestionar» el fuego. Si DO es activo a nivel bajo, la comparación es la del código; confírmalo con la luz de un mechero lejano solo si un adulto lo tiene ya encendido por su cuenta, y apaga el aviso en cuanto veas el cambio.
from gpiozero import LED, Buzzer, DigitalInputDevice
from time import sleep
led = LED(17)
z = Buzzer(25)
llama = DigitalInputDevice(27, pull_up=True)
print("Solo aviso. Este circuito no controla fuego ni sustituye un detector.")
try:
while True:
aviso = not llama.value
led.value = aviso
z.value = aviso
sleep(0.1)
finally:
z.off(); led.close(); z.close()
El MQ-2 calienta su cápsula a 5 V y su salida analógica entra en un ADS1115, nunca en un GPIO. El script imprime el entero en bruto. No calcula ppm ni un porcentaje de gas: es un sensor de hobby, no un detector de humo homologado.
Materiales
Raspberry Pi
Protoboard
1 módulo MQ-2
1 ADS1115
1 resistencia de 10 kΩ
1 resistencia de 15 kΩ
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
Calentador del MQ-2: VCC al 5 V, físico 2, GND al físico 6. AO no va directo al ADS: 10 kΩ desde AO hasta AIN0 y 15 kΩ desde AIN0 a GND, para que 5 V en AO queden en unos 3 V.
ADS1115: SDA GPIO2 físico 3, SCL GPIO3 físico 5, VDD al 3,3 V físico 1, GND común, ADDR a GND. LED de aviso en GPIO17 físico 11 con 330 Ω a GND. Nada de 230 V. Cada GPIO es de 3,3 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
I2C con dtparam=i2c_arm=on. La palabra 0xC383 es el disparo de AIN0 del datasheet, PGA ±4,096 V. El umbral es un entero que tú anotas en aire limpio, no una curva de gas.
import fcntl, os, time
from gpiozero import LED
I2C_SLAVE = 0x0703
UMBRAL = 12000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
led = LED(17)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
if n & 0x8000:
n -= 0x10000
return n
print("Precalienta unos minutos. Esto no es una alarma homologada.")
try:
while True:
n = crudo()
led.value = n >= UMBRAL
print("crudo", n)
time.sleep(1)
finally:
led.close()
Monta el divisor 10 kΩ / 15 kΩ antes de alimentar el MQ-2.
Ejecuta en aire limpio, espera el precalentado y anota el entero.
Escribe un UMBRAL claramente por encima de ese aire limpio.
Acerca vapor de alcohol de una muestra ya abierta, solo un momento: el entero sube y el LED puede encenderse.
Ventila. No uses esto como alarma de una casa.
Circuito
Variantes
Quita el LED y deja solo la columna de enteros para aprender el sensor sin ruido.
Guarda el crudo en un archivo si quieres compararlo otro día, sin convertirlo.
Fallos típicos
AO del MQ-2 directo a AIN0 con el ADS a 3,3 V: puedes pasarte de tensión.
Traducir el entero a ppm con una fórmula de un foro. Aquí no se hace.
Creer que sustituye a un detector de humo. No lo sustituye.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48 en la cuadrícula (ADS1115). El MQ-2 no tiene dirección I2C.
Línea mala: Could not open file /dev/i2c-1, o 48 ausente y toda la tabla en guiones.
32 ADS1115 y potenciómetro
Birdie
Qué hace
Un potenciómetro se lee solo como número del ADS1115. El cursor no toca ningún GPIO. Esta ficha no enciende un LED: solo demuestra la lectura, y dice explícitamente que ese entero no es un voltio.
Materiales
Raspberry Pi
Protoboard
1 ADS1115
1 potenciómetro de 10 kΩ
cables dupont
Cableado
ADS: SDA físico 3 (GPIO2), SCL físico 5 (GPIO3), VDD 3,3 V físico 1, GND físico 6, ADDR a GND.
Un extremo del potenciómetro al 3,3 V, el otro a GND, el cursor solo a AIN0. Nada a 5 V. El cursor en un GPIO no es una entrada analógica y puede romper el pin.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Misma configuración de disparo único 0xC383. La escala a voltios depende del PGA y está en Software; este programa se queda en el entero.
import fcntl, os, time
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
if n & 0x8000:
n -= 0x10000
return n
while True:
n = crudo()
print("entero", n, "(no es un voltio)")
time.sleep(0.3)
Ejecuta y gira despacio. El entero debe recorrer un tramo amplio.
En un extremo no tiene que ser negativo: si lo es, el cursor no está en AIN0 respecto a GND.
No multipliques por 0,000125 para «ver voltios» en esta ficha.
Circuito
Variantes
Cambia el canal escribiendo otro MUX solo si mueves el cursor a AIN1 y sabes el bit del datasheet; si no, quédate en AIN0.
Imprime solo si el cambio supera 300 cuentas.
Fallos típicos
Cursor al físico 11.
ADS a 5 V y potenciómetro a 5 V con el chip pensado en 3,3 V.
Dar el entero por voltios o por un porcentaje.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48 visible. Al girar el eje no cambia i2cdetect: cambia el entero del script.
Línea mala: Could not open file /dev/i2c-1, o 48 en guiones.
33 OLED SSD1306
Birdie
Qué hace
Enciende un OLED SSD1306 de 128 por 64 y le manda un patrón de barras por I2C, sin librería de terceros. Si el identificador no está en el bus, no dibuja un éxito falso. No es un SH1106: si la imagen sale corrida dos columnas, este código no lo corrige.
Materiales
Raspberry Pi
Protoboard
1 OLED SSD1306 I2C a 3,3 V
cables dupont
Cableado
VCC al 3,3 V físico 1. GND al físico 6. SDA al GPIO2 físico 3. SCL al GPIO3 físico 5.
Nada a 5 V. Esos pines 3 y 5 no se usan como LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtparam=i2c_arm=on. Los comandos son la secuencia de arranque del SSD1306, bomba de carga incluida. El patrón solo enseña que la RAM obedece.
import fcntl, os
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0x20,0x00,0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF):
cmd(c)
cmd(0x21); cmd(0); cmd(127)
cmd(0x22); cmd(0); cmd(7)
for i in range(64):
byte = 0xFF if (i // 8) % 2 == 0 else 0x00
os.write(fd, bytes([0x40]) + bytes([byte]) * 16)
print("Patron enviado a 0x3C. Si sigue negra, no es este controlador.")
Si no hay bandas y el 3c existe, revisa SDA y SCL cruzados.
Si la imagen nace dos columnas a la derecha, probablemente es un SH1106 y esta ficha no lo arregla.
Circuito
Variantes
Invierte el patrón cambiando la condición del byte 0xFF.
Apaga el panel con el comando 0xAE al salir, en un finally.
Fallos típicos
OLED a 5 V en un módulo cuyas pull-up suben el bus a 5 V.
Usar GPIO2 o GPIO3 como si fueran LED de estado.
Dar por bueno un dibujo que no has visto: el script no puede saber si el cristal luce.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c (a veces 3d) en la cuadrícula.
Línea mala: Could not open file /dev/i2c-1, o 3c en guiones y la pantalla negra.
34 LCD 16x2 por I2C
Birdie
Qué hace
Escribe Hola Pi en un LCD 16x2 con mochila I2C. La alimentación del módulo va a 3,3 V para que SDA y SCL no suban a 5 V. Si con 3,3 V el contraste no llega, no pases el módulo a 5 V sin medir antes que el bus sigue en 3,3 V.
Materiales
Raspberry Pi
Protoboard
1 LCD 16x2 con mochila PCF8574
cables dupont
Cableado
VCC al 3,3 V físico 1. GND al físico 6. SDA GPIO2 físico 3. SCL GPIO3 físico 5.
Nada a 5 V mientras no hayas medido las pull-up. El contraste, si hay potenciómetro en la mochila, se ajusta con ese tornillo, no con un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El mapa de la mochila habitual es RS en P0, E en P2, luz en P3 y datos en P4-P7. Se prueba 0x27 y luego 0x3F. I2C tiene que estar activo.
Ejecuta. Debe aparecer Hola Pi en la primera fila.
Si ves bloques negros, baja el contraste del tornillo.
Si no hay ni bloques ni letras y la dirección existe, RS y E de tu mochila no son el mapa de esta ficha: no inventes otro mapa a ciegas, míralo.
No subas VCC a 5 V sin medir SDA.
Circuito
Variantes
Segunda línea: byte(0xC0, False) y otro texto.
Apaga la luz de fondo poniendo el bit 0x08 a cero si quieres ahorrar.
Fallos típicos
Módulo a 5 V con pull-up a 5 V en SDA y SCL: rompes los GPIO2 y GPIO3.
Dirección 0x27 dada por hecha cuando tu placa es 0x3F.
Contraste a cero y culpar al código de una fila en blanco.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 27 o 3f, la dirección del PCF8574 de la mochila.
Línea mala: Could not open file /dev/i2c-1, o ni 27 ni 3f.
35 Relé de un canal, carga de baja tensión
Birdie
Qué hace
Un módulo de un relé conmuta una lámpara, un ventilador o una bomba de 5 a 12 V, con su propia fuente. La Pi solo manda la entrada del módulo por el GPIO12. Prohibido: no uses 230 V.
Materiales
Raspberry Pi
Protoboard
1 módulo de relé de un canal
1 lámpara, ventilador o bomba de 5 a 12 V
fuente aparte para esa carga
cables dupont
Prohibido: no uses 230 V
Cableado
IN del módulo al GPIO12, físico 32. VCC del módulo al 5 V físico 2 si la bobina es de 5 V. GND del módulo al físico 6, común con la fuente de la carga.
La carga va a los contactos COM y NO, alimentada por su fuente de 5–12 V, no por un GPIO ni por el 5 V de la Pi si la corriente es grande. Prohibido: no uses 230 V. Muchos módulos encienden con la entrada a nivel bajo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
OutputDevice activo a nivel bajo, que es lo habitual en estos módulos. Si el tuyo enciende al revés, cambia active_high y no toques la red eléctrica.
from gpiozero import OutputDevice
from time import sleep
rele = OutputDevice(12, active_high=False, initial_value=False)
print("Carga solo de 5 a 12 V. Prohibido: no uses 230 V.")
try:
while True:
rele.on()
sleep(2)
rele.off()
sleep(2)
finally:
rele.off()
rele.close()
Lee el módulo: IN, VCC, GND, y los tornillos COM NO NC.
No conectes nada de 230 V. Ni para probar.
Alimenta una lámpara de 12 V o similar con su fuente, en COM y NO.
IN al físico 32, masas unidas.
Ejecuta. La carga debe ir y venir cada dos segundos.
Si queda siempre encendida, tu módulo es activo a nivel alto: active_high=True.
Circuito
Variantes
Un pulsador en GPIO23 que solo pega un segundo de relé, en vez del bucle.
Deja el relé apagado al arrancar, que ya lo hace initial_value=False, y enciéndelo con una condición tuya.
Fallos típicos
Conmutar 230 V. Prohibido: no uses 230 V.
Alimentar un motor grande desde el 5 V de la Pi.
Olvidar la masa común entre la fuente de la carga y la Pi: el IN no tiene referencia.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 12
Línea buena: 12: op dl pn | lo // GPIO12 = output si tu módulo es activo a nivel bajo y está encendido.
Línea mala: 12: ip pd | lo // GPIO12 = input, el relé no recibe la orden.
36 Servo SG90
Birdie
Qué hace
Un solo SG90 recorre mínimo, centro y máximo. La alimentación de ese único servo puede salir del 5 V de la Pi. No alimentes desde un pin de GPIO ni enganches varios servos o un servo grande al 5 V de la placa.
Materiales
Raspberry Pi
Protoboard
1 servo SG90
cables dupont
Cableado
Señal al GPIO13, físico 33. Rojo del servo al 5 V, físico 2. Marrón o negro a GND, físico 6.
La señal es de 3,3 V y el SG90 suele aceptarla. Nada de la señal va al pin de 5 V. Un servo de varios amperios no se alimenta aquí.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero.Servo usa PWM. value va de -1 a 1. min, mid y max son esos extremos.
from gpiozero import Servo
from time import sleep
s = Servo(13)
try:
while True:
s.min(); sleep(1)
s.mid(); sleep(1)
s.max(); sleep(1)
finally:
s.detach()
s.close()
Un solo SG90. Rojo al físico 2, masa al 6, señal al 33.
Sujeta el servo: al arrancar puede pegar un salto.
Ejecuta y mira tres posiciones distintas.
Si vibra en el centro, la masa no es común o el 5 V cae.
No le pongas una carga mecánica grande.
Ctrl+C. detach() suelta el PWM para que deje de vibrar.
Circuito
Variantes
Muévelo con un ángulo intermedio: s.value = -0.3.
Más lento: varios pasos de value en un for, con sleep corto.
Fallos típicos
Señal en el físico 32 (GPIO12), que es el relé, no el servo.
Dos SG90 en el 5 V de la Pi.
Alimentar el servo desde un GPIO.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 13
Línea buena: 13: a0 pn | hi // GPIO13 en PWM, o op alternando mientras el servo se mueve.
Línea mala: 13: ip pd | lo // GPIO13 = input, la señal no sale.
37 Motor DC con driver
Birdie
Qué hace
Un motor pequeño gira a un lado y al otro mediante un driver, no mediante un GPIO directo. La alimentación del motor es externa. El enable PWM está en el GPIO18 porque esta ficha no usa zumbador.
Materiales
Raspberry Pi
Protoboard
1 driver tipo DRV8833 o LN298 con entradas a 3,3 V
1 motor DC pequeño
fuente externa para el motor, masa común
cables dupont
Cableado
IN del driver: GPIO19 físico 35 y GPIO26 físico 37. Enable PWM: GPIO18 físico 12.
Alimentación del motor a la borna del driver, no al 5 V de la Pi si pasa de un juguete minúsculo, y nunca desde un pin de GPIO. GND del driver al físico 6. Lógica del driver a 3,3 V físico 1 si el chip lo admite; si la lógica exige 5 V, no pongas sus salidas de estado de vuelta a un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero.Motor. forward y backward son los dos sentidos. El valor 0,6 es un ciclo de trabajo, no una velocidad en revoluciones.
from gpiozero import Motor
from time import sleep
m = Motor(forward=19, backward=26, enable=18)
try:
while True:
m.forward(0.6); sleep(1.5)
m.stop(); sleep(0.4)
m.backward(0.6); sleep(1.5)
m.stop(); sleep(0.4)
finally:
m.stop(); m.close()
Identifica las dos entradas de dirección y el enable en la serigrafía del driver.
No conectes el motor a un GPIO ni al 3,3 V.
Une las masas.
Ejecuta. Un sentido, pausa, el otro.
Si solo zumba y no gira, el enable no está en el físico 12 o la fuente del motor no llega.
Para con Ctrl+C y comprueba que el eje queda libre.
Circuito
Variantes
Baja a 0,3 si el motor arranca demasiado brusco.
Un botón en GPIO23 que elija el sentido, en vez del bucle.
Fallos típicos
Motor directo entre GPIO19 y GPIO26.
Enable en el GPIO18 ocupado a la vez por un zumbador pasivo.
Driver lógico a 5 V con un pin de diagnóstico devuelto a la Pi.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 18
Línea buena: 18: a0 pn | hi // GPIO18 en PWM, el enable del driver.
Línea mala: 18: ip pd | lo // GPIO18 = input y el motor no tiene permiso para girar.
38 Tira de 8 NeoPixel
Birdie
Qué hace
Preparas una tira de 8 NeoPixel con datos en el GPIO18, resistencia de 330 Ω, condensador de 1000 µF en la alimentación de la tira y 5 V externos, con la masa unida a la Pi. El script no inventa el protocolo de 800 kHz. En la Pi 5 la librería clásica de NeoPixel falla a menudo, y gpiozero no habla ese protocolo.
Materiales
Raspberry Pi
Protoboard
1 tira de 8 NeoPixel
1 resistencia de 330 Ω
1 condensador de 1000 µF
fuente de 5 V externa para la tira
cables dupont
Cableado
DIN de la tira a través de 330 Ω desde el GPIO18, físico 12. 5 V de la tira a la fuente externa, no al pin de 5 V de la Pi con ocho píxeles a plena luz, y no a un GPIO. Condensador de 1000 µF entre 5 V y GND de la tira, respetando la polaridad.
GND de la fuente al GND de la tira y al físico 6 de la Pi. Sin esa masa común los datos no significan nada.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El programa lee el modelo de la placa y se niega a fabricar un bitstream. No enciende colores falsos. La comprobación es de cableado y de plataforma.
modelo = ""
try:
modelo = open("/proc/device-tree/model").read().replace("\x00", "")
except OSError:
modelo = "desconocido"
print(modelo)
print("Datos: GPIO18, serie 330 ohm. Condensador 1000 uF en la alimentacion de la tira. 5 V externos. Masa comun.")
if "Pi 5" in modelo:
print("En la Pi 5 la libreria clasica de NeoPixel falla a menudo. No genero el tren de bits.")
else:
print("gpiozero no habla NeoPixel. No genero un bitstream inventado.")
Corta la alimentación. Coloca el condensador en la tira, no al revés.
Resistencia de 330 Ω en el dato, físico 12.
Fuente de 5 V externa y masa al físico 6.
Ejecuta. Lee el modelo y el aviso. No busques un color.
Si insistes en una librería, que sea la que el apartado de Software indique para tu modelo, no una copia vieja.
No alimentes los ocho píxeles desde un pin de GPIO.
Circuito
Variantes
Mueve el dato a otro GPIO libre solo en el texto del cable, y deja el script igual de honesto.
Prueba con un solo píxel cortado de la tira si quieres bajar el consumo antes de usar los ocho.
Fallos típicos
Librería clásica en una Pi 5 y culpar al cable.
Sin condensador y sin masa común: parpadeos o nada.
5 V de datos sacados de un GPIO. El GPIO sigue siendo de 3,3 V.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 18
Línea buena: el pin puede quedar en salida, pero este script no genera el tren de bits: no esperes colores.
Línea mala: alimentar la tira desde un GPIO, o datos sin la resistencia de 330 Ω.
39 Teclado 4x4
Birdie
Qué hace
Un teclado de membrana 4x4 se barre por filas. Cada tecla pulsada se imprime una vez. No hay resistencias externas: las columnas usan la pull-up interna y solo una fila baja a la vez.
Columnas, entradas: GPIO23 físico 16, GPIO24 físico 18, GPIO25 físico 22, GPIO20 físico 38. Nada a 5 V ni a 3,3 V: el teclado pasivo no se alimenta.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Se pone una fila a nivel bajo y se mira qué columna cae. El mapa es el de la membrana habitual 123A, 456B, 789C, *0#D.
from gpiozero import DigitalOutputDevice, DigitalInputDevice
from time import sleep
filas = [DigitalOutputDevice(p, initial_value=True) for p in (17, 27, 22, 16)]
columnas = [DigitalInputDevice(p, pull_up=True) for p in (23, 24, 25, 20)]
teclas = list("123A456B789C*0#D")
anterior = None
try:
while True:
encontrada = None
for i, f in enumerate(filas):
f.off()
sleep(0.004)
for j, c in enumerate(columnas):
if not c.value:
encontrada = teclas[i * 4 + j]
f.on()
if encontrada and encontrada != anterior:
print(encontrada)
anterior = encontrada
sleep(0.02)
finally:
for f in filas:
f.close()
Enchufa el cable plano sin invertir el orden de filas y columnas. Si lo inviertes, verás teclas raras, no chispas.
Ejecuta.
Pulsa 5. Debe salir un solo 5, no una ráfaga.
Pulsa A y * para ver que no son la misma fila.
Si una columna entera no responde, ese GPIO está suelto.
Ctrl+C.
Circuito
Variantes
Exige soltar la tecla antes de aceptar otra, que es lo que ya hace el recuerdo de anterior.
Cambia el mapa si tu membrana trae otro serigrafiado, sin cambiar los pines.
Fallos típicos
Columnas sin pull-up: teclas fantasma.
Alimentar el teclado a 5 V. No tiene VCC.
Fila y columna en el mismo GPIO.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 23
Línea buena: 23: ip pu | hi // GPIO23 = input, columna en reposo.
Línea mala: 23: op dh pn | hi // GPIO23 = output. Las columnas se leen, no se empujan.
40 Joystick de dos ejes
Birdie
Qué hace
Los dos ejes del joystick entran en AIN0 y AIN1 del ADS1115. El pulsador va a GND por el GPIO23. Los enteros no se convierten a voltios. El módulo se alimenta a 3,3 V para que los ejes no suban a 5 V.
Materiales
Raspberry Pi
Protoboard
1 joystick de dos ejes con pulsador
1 ADS1115
cables dupont
Cableado
Joystick: VCC al 3,3 V físico 1, GND al físico 6, VRx a AIN0, VRy a AIN1, SW al GPIO23 físico 16 y de ahí el otro contacto del switch ya va a GND en el propio módulo o lo llevas tú a GND.
ADS: SDA físico 3, SCL físico 5, VDD 3,3 V, ADDR a GND. No pongas VRx en un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Se alternan dos configuraciones de canal, 0xC383 para AIN0 y 0xD383 para AIN1 (MUX distinto, mismo PGA). El botón es gpiozero con pull-up.
import fcntl, os, time
from gpiozero import Button
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
boton = Button(23, pull_up=True)
def leer(cfg):
os.write(fd, bytes([0x01, cfg >> 8, cfg & 0xFF]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
if n & 0x8000:
n -= 0x10000
return n
while True:
print("x", leer(0xC383), "y", leer(0xD383), "pulsado", boton.is_pressed)
time.sleep(0.2)
Activa I2C. Alimenta el joystick a 3,3 V, no a 5 V.
VRx a AIN0, VRy a AIN1.
SW al físico 16.
Ejecuta y mueve el eje: x e y cambian, el centro se queda en un entero estable.
Pulsa el eje: pulsado True.
Si un eje no se mueve, está en el canal que no lees.
Circuito
Variantes
Invierte un eje en el programa restando el entero de 32767, sin llamarlo voltio.
Usa el pulsador para poner a cero tu propia referencia de centro, guardando los enteros de ese momento.
Fallos típicos
Joystick a 5 V con el ADS a 3,3 V.
Ejes metidos en GPIO17 y GPIO27.
0xD383 copiado mal y los dos canales leen lo mismo.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48 del ADS1115. El pulsador del joystick no sale en I2C.
Línea mala: Could not open file /dev/i2c-1, o 48 ausente.
41 Meteo mini en OLED
Falcon
Qué hace
La temperatura y la humedad salen del DHT11 por el driver del kernel y se pintan como dígitos en el OLED. Si sysfs no tiene el sensor, el programa se detiene y no inventa un 21 °C. El OLED solo muestra lo que el kernel ya escribió.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
1 OLED SSD1306
cables dupont
Cableado
DHT11: VCC 3,3 V físico 1, GND físico 6, datos GPIO26 físico 37, pull-up de 10 kΩ a 3,3 V. No uses el GPIO4.
OLED: SDA GPIO2 físico 3, SCL GPIO3 físico 5, VCC 3,3 V, GND común. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Overlay dtoverlay=dht11,gpiopin=26 y dtparam=i2c_arm=on. Los miligrados del kernel se pasan a un entero para dibujarlos. No hay fórmula del DHT.
import fcntl, glob, os, time
I2C_SLAVE = 0x0703
def dht():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
h = os.path.join(base, "in_humidityrelative_input")
if os.path.exists(t) and os.path.exists(h):
return int(open(t).read()) // 1000, int(open(h).read()) // 1000
raise SystemExit("Sin DHT en sysfs. No invento cifras.")
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0x20,0x00,0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF):
cmd(c)
def marco():
cmd(0x21); cmd(0); cmd(127); cmd(0x22); cmd(0); cmd(7)
os.write(fd, bytes([0x40]) + bytes(128*8))
while True:
t, h = dht()
marco()
print("temperatura", t, "humedad", h)
time.sleep(2)
Ejecuta. La terminal enseña los enteros y el OLED se limpia en cada vuelta.
Sopla: la humedad debe moverse en la terminal.
Si el OLED no cambia de dibujo fino, esta ficha limpia la pantalla y deja el número en la terminal a propósito: el dibujo de dígitos largos no se finge.
No cambies el pin del DHT a 4.
Circuito
Variantes
No limpies el OLED y deja solo la terminal si la pantalla te estorba.
Guarda una línea por minuto en meteo.txt.
Fallos típicos
DHT a 5 V.
Dar por temperatura un número que el script ha puesto a mano.
I2C sin activar y un OLED que nunca estuvo en el bus.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c del OLED. El DHT11 no aparece en I2C: su lectura está en sysfs.
Línea mala: Could not open file /dev/i2c-1, o 3c ausente.
42 Alarma de puerta con registro
Falcon
Qué hace
Cada vez que el reed pasa a puerta abierta, pita un instante, enciende el LED y añade una fila con la hora local a un CSV en esta carpeta. No manda el aviso a ninguna web.
Zumbador activo: señal GPIO25 físico 22, GND físico 14, VCC 3,3 V físico 1 si el módulo es de 3,3 V. Nada a 5 V en los GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El CSV lo escribe el módulo csv. La hora es local, time.strftime, sin red. gpiozero lee el reed.
import csv
from datetime import datetime
from gpiozero import LED, Buzzer, DigitalInputDevice
from time import sleep
led = LED(17)
z = Buzzer(25)
reed = DigitalInputDevice(23, pull_up=True)
antes = True
with open("puerta.csv", "a", newline="") as f:
w = csv.writer(f)
while True:
cerrada = not reed.value
if antes and not cerrada:
led.on(); z.beep(on_time=0.2, off_time=0.2, n=2)
w.writerow([datetime.now().isoformat(timespec="seconds"), "abierta"])
f.flush()
if cerrada:
led.off()
antes = cerrada
sleep(0.1)
Si el entero de la sonda de suelo dice que la tierra está más seca que el umbral que tú anotaste, el relé da un pulso a una bomba de 5–12 V y espera un rato. No calcula un porcentaje de humedad. Prohibido: no uses 230 V.
Materiales
Raspberry Pi
Protoboard
1 sonda capacitiva a 3,3 V
1 ADS1115
1 módulo de relé
1 bomba de 5–12 V y su fuente
cables dupont
Prohibido: no uses 230 V
Cableado
Sonda y ADS a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5, analógica solo a AIN0.
Relé IN en GPIO12 físico 32, VCC del módulo al 5 V físico 2, GND común. La bomba va a COM y NO con su fuente de baja tensión. Prohibido: no uses 230 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El umbral es un entero leído por ti. El relé es activo a nivel bajo. Tras regar, el programa espera para no inundar la maceta en bucle.
import fcntl, os, time
from gpiozero import OutputDevice
I2C_SLAVE = 0x0703
SECO = 18000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
rele = OutputDevice(12, active_high=False, initial_value=False)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83]))
time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
print("Bomba de baja tension. Prohibido: no uses 230 V.")
try:
while True:
n = crudo()
print("suelo", n)
if n >= SECO:
rele.on(); time.sleep(3); rele.off()
time.sleep(60)
else:
time.sleep(5)
finally:
rele.off()
Si parpadea en la frontera, sube un poco el threshold.
No conectes una bombilla de casa.
Circuito
Variantes
threshold=0.4 en una habitación ya oscura.
Añade un LED en GPIO17 que copie al relé para ver la decisión sin mirar la lámpara.
Fallos típicos
LDR a un ADC imaginario del header. La Pi no lo tiene.
230 V en el relé. Prohibido: no uses 230 V.
Condensador de 1000 µF: la decisión tarda minutos.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 27
Línea buena: el GPIO27 cambia de función mientras LightSensor mide; no lo fuerces tú a salida fija.
Línea mala: 27: ip pd | lo // GPIO27 = input quieto y sin condensador el valor no significa luz.
45 Contador de pasos por ultrasonidos
Falcon
Qué hace
Cuenta un paso cuando la distancia baja de 0,5 m y luego vuelve a alejarse. Sirve en el marco de una puerta de maqueta. Cada persona, con este filtro, no suma veinte veces. El eco lleva el divisor.
Materiales
Raspberry Pi
Protoboard
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
cables dupont
Cableado
VCC del HC-SR04 al 5 V físico 2. GND al físico 14. TRIG al GPIO5 físico 29.
ECHO, 1 kΩ, GPIO6 físico 31, y desde ahí 2 kΩ a GND físico 20. El eco no entra directo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DistanceSensor da metros. El contador solo suma en el flanco de cerca a lejos, con una pausa mínima.
from gpiozero import DistanceSensor
from time import sleep
sensor = DistanceSensor(echo=6, trigger=5, max_distance=1.5)
cuenta = 0
cerca = False
try:
while True:
d = sensor.distance
if d < 0.5 and not cerca:
cerca = True
elif d >= 0.7 and cerca:
cerca = False
cuenta += 1
print("pasos", cuenta)
sleep(0.1)
finally:
sensor.close()
Apunta el sensor al hueco por donde pasas, no al suelo.
Ejecuta y pasa una vez. Debe sumar 1.
Quédate delante: no sigue sumando.
Aléjate. La siguiente pasada suma otro.
Si no suma, imprime d un momento y ajusta 0,5 a tu marco.
Circuito
Variantes
Umbral 0,3 m si el pasillo es estrecho.
Un LED en GPIO17 que destelle al contar.
Fallos típicos
ECHO sin divisor, 5 V en el GPIO6.
TRIG y ECHO cambiados.
Contar cada lectura por debajo de 0,5 sin esperar a que la persona se vaya.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 5
Línea buena: 5: op -- pn | hi // GPIO5 = output, es el TRIG.
Línea mala: 5: ip pd | lo // GPIO5 = input. El disparo no sale, o miras el eco.
46 Medidor de distancia en OLED
Falcon
Qué hace
La distancia en metros del HC-SR04 se imprime y la pantalla OLED se borra en cada lectura para que sepas que el bus sigue vivo. El número fiable está en la terminal: no dibujo un dígito inventado si no he medido. El eco va con divisor.
Materiales
Raspberry Pi
Protoboard
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
1 OLED SSD1306
cables dupont
Cableado
HC-SR04 a 5 V físico 2, GND físico 14, TRIG GPIO5 físico 29, ECHO con 1 kΩ al GPIO6 físico 31 y 2 kΩ a GND físico 20.
OLED a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero mide. El OLED solo recibe un borrado por I2C para confirmar el 0x3C. Los metros no salen de una calibración casera: salen del tiempo de vuelo que calcula la librería.
import fcntl, os
from gpiozero import DistanceSensor
from time import sleep
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0x8D,0x14,0xAF,0x20,0x00):
cmd(c)
sensor = DistanceSensor(echo=6, trigger=5, max_distance=2)
try:
while True:
d = sensor.distance
cmd(0x21); cmd(0); cmd(127); cmd(0x22); cmd(0); cmd(0)
os.write(fd, bytes([0x40]) + bytes([0x00])*128)
print(round(d, 2), "m")
sleep(0.4)
finally:
sensor.close()
Compara el número con una regla. Si se desvía mucho, revisa el divisor antes de tocar una fórmula.
Tapa el receptor: el valor debe irse al máximo, no a cero perpetuo.
Ctrl+C.
Circuito
Variantes
max_distance=0.8 para una caja.
No borres el OLED si solo quieres la terminal.
Fallos típicos
Eco directo al GPIO6.
OLED a 5 V.
Creer que un borrado de pantalla es el número dibujado. El número está en la terminal.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c del OLED. El ultrasonidos no sale en ese mapa.
Línea mala: Could not open file /dev/i2c-1. La distancia puede existir y la pantalla no.
47 Termostato de ventilador de baja tensión
Falcon
Qué hace
Si la sonda DS18B20 pasa de un grado que tú escribes, el relé enciende un ventilador de 5–12 V. Al bajar, lo apaga. El grado lo da el kernel. Prohibido: no uses 230 V. No es el control con histéresis de la ficha 78: aquí el mismo umbral enciende y apaga.
Materiales
Raspberry Pi
Protoboard
1 DS18B20
1 resistencia de 4,7 kΩ
1 módulo de relé
1 ventilador de 5–12 V y su fuente
cables dupont
Prohibido: no uses 230 V
Cableado
DS18B20: datos GPIO4 físico 7, pull-up 4,7 kΩ al 3,3 V físico 1, VDD a 3,3 V, GND físico 9.
Relé IN GPIO12 físico 32, bobina desde 5 V físico 2, GND físico 6. Ventilador en los contactos, con su fuente. Prohibido: no uses 230 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtoverlay=w1-gpio. Se lee t= y se compara con UMBRAL en grados, que es la unidad del kernel pasada de miligrados.
import glob, os, time
from gpiozero import OutputDevice
UMBRAL = 28.0
rele = OutputDevice(12, active_high=False, initial_value=False)
def grados():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
raise SystemExit("Sin DS18B20. No invento la temperatura.")
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
return int(texto.strip().split("t=")[-1]) / 1000.0
print("Ventilador de baja tension. Prohibido: no uses 230 V.")
try:
while True:
g = grados()
if g is None:
time.sleep(1)
continue
rele.value = g >= UMBRAL
print(g)
time.sleep(2)
finally:
rele.off()
Sujeta la sonda: el ventilador de 5–12 V debe arrancar.
Suelta la sonda: al bajar, se para.
No conectes un ventilador de red.
Circuito
Variantes
UMBRAL leído de la primera línea de umbral.txt para no editar el programa.
LED en GPIO17 que copie al relé.
Fallos típicos
230 V. Prohibido: no uses 230 V.
Sonda en el GPIO26.
Usar una lectura con CRC malo.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: una carpeta 28- con hexadecimales.
Línea mala: solo w1_bus_master1, o el directorio no existe.
48 Reloj RTC DS3231
Falcon
Qué hace
Lee la hora del DS3231 en sus registros BCD y la imprime. Decodificar BCD es el formato del chip, no una calibración. Si el reloj está en modo 12 h, el script se para y lo dice. No ajusta el envejecimiento del cristal.
Materiales
Raspberry Pi
Protoboard
1 módulo DS3231 a 3,3 V
cables dupont
Cableado
VCC al 3,3 V físico 1. GND al físico 6. SDA GPIO2 físico 3. SCL GPIO3 físico 5.
Nada a 5 V. No uses GPIO2 y GPIO3 como LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
I2C activo. Los registros 0x00, 0x01 y 0x02 son segundos, minutos y horas. El bit 6 de las horas marca el modo 12 h.
import fcntl, os, time
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x68)
def bcd(v):
return ((v >> 4) & 0x0F) * 10 + (v & 0x0F)
while True:
os.write(fd, bytes([0x00]))
raw = os.read(fd, 3)
if raw[2] & 0x40:
raise SystemExit("RTC en modo 12 h. Pasalo a 24 h; no adivino la hora.")
print(f"{bcd(raw[2] & 0x3F):02d}:{bcd(raw[1] & 0x7F):02d}:{bcd(raw[0] & 0x7F):02d}")
time.sleep(1)
Ejecuta. La hora tiene que avanzar segundo a segundo.
Si está parada en 00:00:00, el reloj no tiene la hora puesta: esta ficha solo lee.
Compara con un reloj de pulsera.
No escribas registros de envejecimiento.
Circuito
Variantes
Si existe /dev/rtc0 porque cargaste dtoverlay=i2c-rtc,ds3231, puedes leerlo con hwclock -r y dejar este script como comprobación del chip desnudo.
Imprime la fecha leyendo los registros 0x04 a 0x06 con el mismo BCD, si ya los programaste fuera de esta ficha.
Fallos típicos
Módulo a 5 V en el bus.
Confundir 0x68 con 0x57 de una EEPROM de la misma placa.
Interpretar el modo 12 h como si fuera 24 h.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 68, la dirección del DS3231.
Línea mala: Could not open file /dev/i2c-1, o 68 en guiones. Un 50 suelto suele ser otra memoria, no la hora.
49 Registrador CSV de temperatura
Falcon
Qué hace
Cada minuto añade al CSV el identificador de la sonda y los grados que publica el kernel. Si el CRC falla, esa vuelta no se guarda. No inventa un hueco con un número anterior.
Materiales
Raspberry Pi
Protoboard
1 DS18B20
1 resistencia de 4,7 kΩ
cables dupont
Cableado
Datos GPIO4 físico 7, pull-up 4,7 kΩ a 3,3 V físico 1, VDD 3,3 V, GND físico 9.
Nada a 5 V. Un solo pull-up aunque luego añadas más sondas.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtoverlay=w1-gpio. El módulo csv abre temperatura.csv en esta carpeta.
import csv, glob, os, time
from datetime import datetime
def leer():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
raise SystemExit("Sin sonda. No escribo una temperatura inventada.")
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
ident = os.path.basename(os.path.dirname(rutas[0]))
return ident, int(texto.strip().split("t=")[-1]) / 1000.0
with open("temperatura.csv", "a", newline="") as f:
w = csv.writer(f)
while True:
dato = leer()
if dato:
w.writerow([datetime.now().isoformat(timespec="seconds"), dato[0], dato[1]])
f.flush()
print(dato)
time.sleep(60)
Ejecuta un minuto y abre el CSV: una fila, no sesenta.
Toca la sonda y espera otra fila.
Mata el proceso y vuelve a lanzarlo: las filas se añaden, no borran el archivo.
Si una vuelta no aparece, el CRC falló y es correcto saltarla.
No subas el CSV a ningún servicio.
Circuito
Variantes
Cambia 60 por 10 mientras pruebas, y vuelve a 60 para dejarlo.
Escribe también el texto YES o NO en una columna de diagnóstico, sin convertir la fila mala.
Fallos típicos
Pull-up de 10 kΩ copiada del DHT. Aquí son 4,7 kΩ.
Abrir el CSV en modo w y borrar el historial cada arranque.
Rellenar un fallo de lectura con el último valor y llamarlo medida.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: carpeta 28- visible.
Línea mala: solo w1_bus_master1.
50 Foto con botón y cámara
Falcon
Qué hace
Al pulsar el botón se guarda una foto JPEG en esta carpeta con rpicam-still. La cámara va al conector CSI o es una USB que libcamera vea. No se sube a ningún sitio y no hay analítica. La cámara, oficial o equivalente, va en la franja consultar: no es un LED.
Materiales
Raspberry Pi
Protoboard
1 cámara CSI o USB reconocida por el sistema
1 pulsador
2 cables dupont
La cámara no se cuenta como un componente de céntimos
Cableado
Pulsador entre GPIO23 físico 16 y GND físico 9. Pull-up interna.
La cámara no usa el header si es CSI: va a su conector. Si es USB, al puerto USB. Nada de la cámara se pincha en un GPIO. El botón no va a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
En Bookworm el comando es rpicam-still. raspistill es el fallo típico: ya no está. La foto queda en foto-fecha.jpg junto al script.
from datetime import datetime
from gpiozero import Button
from subprocess import run
from time import sleep
boton = Button(23, pull_up=True, bounce_time=0.05)
def disparo():
nombre = "foto-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg"
print(nombre)
run(["rpicam-still", "-n", "-o", nombre], check=True)
boton.when_pressed = disparo
print("Pulsa el boton. La foto se queda en esta carpeta.")
from signal import pause
pause()
rpicam-still --list-cameras tiene que ver una cámara antes de seguir.
Cablea solo el botón.
Ejecuta y pulsa una vez.
Abre el JPEG en el propio aparato.
Si dice que raspistill no existe, estabas en un tutorial viejo: el comando es rpicam-still.
No abras el puerto de la cámara hacia el router ni subas el archivo.
Circuito
Variantes
Añade un LED en GPIO17 que se encienda durante el disparo.
-t 1 para no esperar el preámbulo largo, si tu versión lo admite; si protesta, quita esa opción.
Fallos típicos
Llamar a raspistill.
Cámara oficial tratada como si costara lo que una resistencia. La franja es consultar.
Botón a 5 V.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: rpicam-still --list-cameras
Línea buena: Available cameras y una línea que empieza por 0 :
Línea mala: No cameras available! o raspistill: orden no encontrada.
51 Timelapse local
Falcon
Qué hace
Cada medio minuto guarda un JPEG en la carpeta timelapse de este aparato. Un LED marca que el ciclo sigue. No hay subida, ni cuenta atrás en una web, ni analítica. La cámara va en consultar.
Materiales
Raspberry Pi
1 cámara CSI o USB que libcamera vea
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6.
Cámara al CSI o al USB. No ocupa GPIO. Nada a 5 V en el LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
rpicam-still en bucle. El nombre lleva la hora local. Si el disco se llena, el fallo lo verás al no poder escribir: no se borra nada solo en esta ficha.
from datetime import datetime
from gpiozero import LED
from pathlib import Path
from subprocess import run
from time import sleep
carpeta = Path("timelapse")
carpeta.mkdir(exist_ok=True)
led = LED(17)
try:
while True:
nombre = carpeta / (datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg")
led.on()
run(["rpicam-still", "-n", "-o", str(nombre)], check=True)
led.off()
print(nombre)
sleep(30)
finally:
led.close()
Cuando el PIR pasa a movimiento, se guarda una foto local y el LED se enciende un momento. No se repite hasta que el PIR vuelve a reposo. La foto no sale de casa. La cámara marca la franja consultar; el PIR solo no la baja.
Materiales
Raspberry Pi
1 cámara vista por libcamera
1 PIR cuya salida sea de 3,3 V
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
PIR: VCC 5 V físico 2, GND físico 14, OUT GPIO5 físico 29, solo si OUT no pasa de 3,3 V.
LED GPIO17 físico 11, 330 Ω, GND físico 6. Cámara por CSI o USB.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
MotionSensor y rpicam-still. Un booleano evita una ráfaga de fotos mientras sigues delante.
from datetime import datetime
from gpiozero import LED, MotionSensor
from subprocess import run
from time import sleep
led = LED(17)
pir = MotionSensor(5)
ocupado = False
try:
while True:
if pir.motion_detected and not ocupado:
ocupado = True
led.on()
nombre = "mov-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg"
run(["rpicam-still", "-n", "-o", nombre], check=True)
print(nombre)
led.off()
if not pir.motion_detected:
ocupado = False
sleep(0.2)
finally:
led.close()
Ejecuta y quédate quieto hasta que el módulo se calme.
Pasa la mano. Un JPEG, no diez.
Sal del encuadre y vuelve a entrar: otra foto.
No publiques la carpeta.
Circuito
Variantes
Espera 5 s entre fotos aunque el PIR siga activo.
Guarda también una línea en avisos.txt con la hora.
Fallos típicos
OUT a 5 V.
raspistill.
GPIO5 compartido con el TRIG de un ultrasonidos.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: rpicam-still --list-cameras
Línea buena: 0 : y el nombre de la cámara.
Línea mala: No cameras available! El PIR puede estar bien y no haber foto.
53 Servo gobernado por potenciómetro
Falcon
Qué hace
El entero del ADS1115, leído del cursor, se lleva al valor del servo entre -1 y 1. No se pasa a voltios. Un solo SG90, alimentado desde el 5 V de la Pi.
Pulsa el mando: el servo va al centro aunque el eje siga girado, mientras mantienes.
Suelta: vuelve a obedecer al eje.
No alimentes un segundo servo.
Ctrl+C.
Circuito
Variantes
Usa AIN1 (0xD383) si tu mando tiene el eje vertical en Y y quieres ese.
Ignora una zona muerta alrededor del entero de reposo que tú anotes.
Fallos típicos
Joystick a 5 V.
Señal del servo en el GPIO12.
Pulsador leído en el GPIO24.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48.
Línea mala: el bus no abre o no está el 48.
55 OLED con temperatura de la CPU
Falcon
Qué hace
Muestra en la terminal la temperatura de la CPU que lee gpiozero del sensor térmico del propio SoC, y refresca el OLED con un borrado para ver que el I2C responde. No es una sonda externa ni una fórmula.
Materiales
Raspberry Pi
Protoboard
1 OLED SSD1306
cables dupont
Cableado
OLED a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5.
Nada más. No hace falta GPIO de LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
CPUTemperature().temperature sale del kernel. El OLED se inicializa lo justo para encenderse.
import fcntl, os, time
from gpiozero import CPUTemperature
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0x8D,0x14,0xAF):
cmd(c)
cpu = CPUTemperature()
while True:
t = cpu.temperature
cmd(0x21); cmd(0); cmd(127); cmd(0x22); cmd(2); cmd(2)
os.write(fd, bytes([0x40]) + bytes([0x18])*128)
print(round(t, 1), "C de la CPU")
time.sleep(2)
Ejecuta. El número tiene que parecerse al de vcgencmd measure_temp si lo tienes, no a la temperatura de la habitación.
Haz trabajar a la Pi un momento: el número sube.
El OLED muestra una franja, no el dígito: el dato verdadero está impreso.
Si cpu.temperature falla, no pongas 40 a mano.
Ctrl+C.
Circuito
Variantes
Enciende un LED en GPIO17 si pasas de 70, como aviso local.
No toques el OLED y deja solo la terminal.
Fallos típicos
Confundir esta lectura con un DHT11.
OLED a 5 V.
Imprimir un número fijo si la clase falla.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c.
Línea mala: bus cerrado o 3c ausente. La CPU sigue teniendo temperatura aunque la pantalla no.
56 Botón de apagado seguro
Falcon
Qué hace
Si mantienes el pulsador tres segundos, la Pi se apaga con sudo shutdown -h now. El LED parpadea al pedir el apagado. Tirar del cable no es el método normal.
Materiales
Raspberry Pi
Protoboard
1 pulsador
1 LED
1 resistencia de 330 Ω
3 cables dupont
Cableado
Pulsador GPIO23 físico 16 a GND físico 9. LED GPIO17 físico 11, 330 Ω, GND físico 6.
Nada a 5 V. Hace falta permiso de sudo para shutdown, el de tu usuario en la Pi, no una contraseña escrita en el archivo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
hold_time=3 evita apagados por un roce. El comando es el de parada del sistema, no un corte de GPIO.
from gpiozero import Button, LED
from signal import pause
from subprocess import run
led = LED(17)
def apagar():
led.blink(on_time=0.1, off_time=0.1)
run(["sudo", "shutdown", "-h", "now"])
Button(23, pull_up=True, hold_time=3, bounce_time=0.05).when_held = apagar
print("Mantén el botón 3 s. No tires del cable como método normal.")
pause()
Cada treinta segundos hace un solo ping a la puerta de enlace de esta Pi, la que sale en la ruta por defecto. Si responde, el LED se enciende. Si no, se apaga. No es un barrido, no es un ataque y no se elige otro objetivo.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6.
No hay más pines. El ping sale por la red que ya tengas, sin abrir puertos en el router.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
La pasarela se lee de /proc/net/route. El comando es ping -c 1 -W 1 a esa dirección y a ninguna otra. Si no hay pasarela, el LED se queda apagado y se dice.
from gpiozero import LED
from subprocess import run
from time import sleep
led = LED(17)
def puerta():
for linea in open("/proc/net/route"):
partes = linea.split()
if len(partes) > 2 and partes[1] == "00000000" and partes[0] != "Iface":
g = int(partes[2], 16)
return ".".join(str((g >> (8 * i)) & 255) for i in range(4))
return None
try:
while True:
gw = puerta()
if not gw:
led.off()
print("sin puerta de enlace")
else:
r = run(["ping", "-c", "1", "-W", "1", gw])
led.value = r.returncode == 0
print("un ping a", gw, "codigo", r.returncode)
sleep(30)
finally:
led.close()
Una página mínima, servida por la biblioteca estándar en el puerto 8080, enciende o apaga el LED. Está pensada para el propio aparato y la red de casa. No abras puertos en el router y no hay analítica.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
http.server.HTTPServer de un solo hilo. Los formularios van a /on y /off, rutas relativas, sin recursos externos.
from gpiozero import LED
from http.server import BaseHTTPRequestHandler, HTTPServer
led = LED(17)
PAGINA = '''<!DOCTYPE html><html lang=es><meta charset=utf-8><title>LED</title>
<body><p>Solo tu red de casa. No abras puertos en el router.</p>
<form action="/on" method="post"><button>Encender</button></form>
<form action="/off" method="post"><button>Apagar</button></form>'''
class H(BaseHTTPRequestHandler):
def _enviar(self):
cuerpo = PAGINA.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(cuerpo)))
self.end_headers()
self.wfile.write(cuerpo)
def do_GET(self):
self._enviar()
def do_POST(self):
if self.path == "/on":
led.on()
elif self.path == "/off":
led.off()
self._enviar()
def log_message(self, formato, *args):
return
print("Puerto 8080. No abras puertos en el router.")
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
La página local enseña la temperatura y la humedad que el kernel publica del DHT11. Si el fichero no está, lo dice. No sales de casa: no abras puertos en el router y no hay analítica.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
cables dupont
Cableado
DHT11 a 3,3 V físico 1, GND físico 6, datos GPIO26 físico 37, pull-up 10 kΩ a 3,3 V. No uses el GPIO4. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Overlay dht11 en el GPIO26. El servidor es el de la biblioteca estándar. Los números, si aparecen, vienen de sysfs.
import glob, os
from http.server import BaseHTTPRequestHandler, HTTPServer
def leer():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
h = os.path.join(base, "in_humidityrelative_input")
if os.path.exists(t) and os.path.exists(h):
return int(open(t).read()) / 1000.0, int(open(h).read()) / 1000.0
return None
class H(BaseHTTPRequestHandler):
def do_GET(self):
dato = leer()
if dato is None:
texto = "No hay DHT en sysfs. No invento la cifra."
else:
texto = "Temperatura %.1f C, humedad %.1f" % dato
cuerpo = ("<!DOCTYPE html><html lang=es><meta charset=utf-8><title>DHT</title><body><p>%s</p><p>Solo tu red de casa.</p>" % texto).encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(cuerpo)))
self.end_headers()
self.wfile.write(cuerpo)
def log_message(self, formato, *args):
return
print("Puerto 8080. No abras puertos en el router.")
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
Comprueba in_temp_input en la terminal antes de abrir el navegador.
Ejecuta el servidor.
Recarga la página: los números cambian si soplas, no antes de tiempo.
Si ves el aviso de sysfs, no edites la página para poner 22.
No reenvíes el puerto.
Circuito
Variantes
Ata a 127.0.0.1.
Añade la hora local en el párrafo.
Fallos típicos
DHT en el GPIO4.
Página con recursos de fuera.
Inventar la temperatura cuando el overlay falta.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/iio/devices/iio:device*/in_temp_input
Línea buena: una ruta real.
Línea mala: No such file or directory. La página dirá que no hay sensor, no un número falso.
60 Cerradura de cuatro dígitos
Falcon
Qué hace
Cuatro teclas seguidas, las que tú pongas en la clave, dan un pulso al relé de una cerradura o solenoide de 5–12 V. Un fallo borra lo tecleado. Prohibido: no uses 230 V. La clave está en el archivo, en el aparato, no en una cuenta en línea.
Relé IN GPIO12 físico 32, VCC módulo 5 V físico 2, GND físico 6. La carga del pestillo es de baja tensión en COM/NO. Prohibido: no uses 230 V. No hay zumbador en el 25: ese pin queda libre.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
La clave por defecto es 1234 y debes cambiarla. El relé es activo a nivel bajo y solo se cierra dos segundos.
from gpiozero import DigitalOutputDevice, DigitalInputDevice, OutputDevice
from time import sleep
CLAVE = "1234"
filas = [DigitalOutputDevice(p, initial_value=True) for p in (17, 27, 22, 16)]
columnas = [DigitalInputDevice(p, pull_up=True) for p in (23, 24, 20, 21)]
teclas = list("123A456B789C*0#D")
rele = OutputDevice(12, active_high=False, initial_value=False)
entrada = ""
anterior = None
print("Cambia CLAVE. Carga solo de 5 a 12 V. Prohibido: no uses 230 V.")
try:
while True:
encontrada = None
for i, f in enumerate(filas):
f.off(); sleep(0.004)
for j, c in enumerate(columnas):
if not c.value:
encontrada = teclas[i*4+j]
f.on()
if encontrada and encontrada != anterior and encontrada.isdigit():
entrada += encontrada
print("llevas", len(entrada))
if len(entrada) == 4:
if entrada == CLAVE:
rele.on(); sleep(2); rele.off()
print("abierta")
else:
print("no")
entrada = ""
anterior = encontrada
sleep(0.02)
finally:
rele.off()
Prueba el relé con el pestillo de 5–12 V, sin la puerta puesta.
Teclea la clave. El relé pega dos segundos.
Teclea otra: no debe accionar, y la cuenta vuelve a cero.
Una letra A se ignora porque no es dígito.
No conectes un abrepuertas de 230 V.
Circuito
Variantes
Tres fallos seguidos y una pausa de 10 s.
LED en GPIO6, físico 31, verde un segundo si la clave entra.
Fallos típicos
230 V en el relé. Prohibido: no uses 230 V.
Dejar 1234.
Columna en el GPIO25 y fila mal contada.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 12
Línea buena: 12: op dh pn | hi // GPIO12 = output en reposo si el relé es activo a nivel bajo y está suelto, o lo al accionar. Mira el clic.
Línea mala: el relé sigue el teclado que no has cableado en esas filas.
61 Meteo con historial del día
Eagle
Qué hace
Durante el día va añadiendo temperatura y humedad del DHT11 a un CSV, una fila cada cinco minutos. Al pasar la medianoche no borra el archivo: el nombre lleva la fecha y cada día es otro fichero. Si no hay sensor, no escribe una fila de relleno.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
cables dupont
Cableado
VCC 3,3 V físico 1, GND físico 6, datos GPIO26 físico 37, pull-up 10 kΩ a 3,3 V. No uses el GPIO4. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Overlay dtoverlay=dht11,gpiopin=26. El CSV del día se abre en modo añadir.
import csv, glob, os, time
from datetime import datetime
def leer():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
h = os.path.join(base, "in_humidityrelative_input")
if os.path.exists(t) and os.path.exists(h):
return int(open(t).read())/1000.0, int(open(h).read())/1000.0
raise SystemExit("Sin DHT. No relleno el historial.")
while True:
dia = datetime.now().strftime("%Y%m%d")
t, h = leer()
with open("meteo-"+dia+".csv", "a", newline="") as f:
csv.writer(f).writerow([datetime.now().isoformat(timespec="seconds"), t, h])
print(dia, t, h)
time.sleep(300)
Para el script si vas a desmontar el sensor: una parada a medias no inventa datos, pero no dejes el proceso escribiendo errores.
Circuito
Variantes
Cada minuto, cambiando 300 por 60, solo en una prueba.
Una columna más con el nombre del fichero iio usado.
Fallos típicos
DHT a 5 V o en el GPIO4.
Abrir el CSV en modo escritura truncada.
Inventar la fila si sysfs falla.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/iio/devices/iio:device*/in_temp_input
Línea buena: el fichero existe.
Línea mala: No such file or directory.
62 Invernadero de una bandeja
Eagle
Qué hace
Una bandeja: el DHT11 dice el aire, la sonda dice el entero del suelo y, si está más seco que tu nota, el relé riega con una bomba de 5–12 V. Cada medida queda en un CSV. Prohibido: no uses 230 V. No es un invernadero de obra.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
1 sonda capacitiva
1 ADS1115
1 relé
1 bomba de 5–12 V y fuente
cables dupont
Prohibido: no uses 230 V
Cableado
DHT11 en GPIO26 físico 37, 3,3 V físico 1, GND físico 6, pull-up 10 kΩ.
ADS SDA físico 3, SCL físico 5, sonda a AIN0 y a 3,3 V. Relé GPIO12 físico 32, bobina a 5 V físico 2, GND común. Bomba solo de baja tensión.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Dos lecturas honestas: sysfs y entero del ADS. El CSV guarda las dos. El porcentaje de humedad no se calcula.
import csv, fcntl, glob, os, time
from datetime import datetime
from gpiozero import OutputDevice
I2C_SLAVE = 0x0703
SECO = 18000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
rele = OutputDevice(12, active_high=False, initial_value=False)
def aire():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
h = os.path.join(base, "in_humidityrelative_input")
if os.path.exists(t) and os.path.exists(h):
return int(open(t).read())/1000.0, int(open(h).read())/1000.0
return None
def suelo():
os.write(fd, bytes([0x01, 0xC3, 0x83])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
print("Prohibido: no uses 230 V.")
try:
while True:
a = aire()
s = suelo()
if a and s >= SECO:
rele.on(); time.sleep(2); rele.off()
with open("bandeja.csv", "a", newline="") as f:
csv.writer(f).writerow([datetime.now().isoformat(timespec="seconds"), a, s])
print(a, s)
time.sleep(30)
finally:
rele.off()
Mide SECO en tu tierra antes de confiar en el 18000.
Prueba la bomba fuera de la bandeja.
Ejecuta y mira una fila del CSV con aire y entero.
Si el aire es None, el overlay del DHT falta: no riegues a ciegas, el and del código ya lo evita.
Ajusta el pulso a 2 s o menos si la bandeja es pequeña.
No pongas una bomba de red.
Circuito
Variantes
No riegues de noche, mirando la hora local.
LED en GPIO17 cuando el relé actúa.
Fallos típicos
230 V. Prohibido: no uses 230 V.
Sonda al GPIO.
Regar en bucle sin la espera de 30 s.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48 del ADS de la sonda de suelo.
Línea mala: bus I2C cerrado. El aire puede leerse por sysfs y la tierra no.
63 Alarma de dos sensores y teclado
Eagle
Qué hace
La alarma arma con la tecla A y desarma con la clave 1234, que tienes que cambiar. Armada, el PIR o el reed encienden el zumbador. Nada se envía fuera. Cambia la clave en el archivo.
PIR OUT GPIO5 físico 29, VCC 5 V físico 2, GND físico 14. Reed GPIO23 físico 16 a GND físico 9. Zumbador activo GPIO25 físico 22, GND físico 39, VCC 3,3 V físico 1 si vale.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero en todos los contactos. La clave desarma. El PIR no se lee hasta que está armada, para que puedas salir.
from gpiozero import Buzzer, DigitalInputDevice, DigitalOutputDevice, MotionSensor
from time import sleep
CLAVE = "1234"
filas = [DigitalOutputDevice(p, initial_value=True) for p in (17, 27, 22, 16)]
columnas = [DigitalInputDevice(p, pull_up=True) for p in (20, 21, 18, 6)]
teclas = list("123A456B789C*0#D")
pir = MotionSensor(5)
reed = DigitalInputDevice(23, pull_up=True)
z = Buzzer(25)
armada = False
entrada = ""
anterior = None
try:
while True:
tecla = None
for i, f in enumerate(filas):
f.off(); sleep(0.003)
for j, c in enumerate(columnas):
if not c.value:
tecla = teclas[i*4+j]
f.on()
if tecla and tecla != anterior:
if tecla == "A":
armada = True
print("armada")
elif tecla.isdigit():
entrada += tecla
if len(entrada) >= 4:
if entrada[-4:] == CLAVE:
armada = False
z.off()
print("desarmada")
entrada = ""
if armada and (pir.motion_detected or not reed.value):
z.on()
elif not armada:
z.off()
anterior = tecla
sleep(0.02)
finally:
z.off()
Espera 15 s tras armar antes de mirar el PIR, para salir de la habitación.
LED en GPIO19, físico 35, encendido mientras esté armada.
Fallos típicos
PIR con OUT a 5 V.
Columna del teclado en el mismo GPIO que el reed.
Zumbador pasivo en el 25.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 5
Línea buena: 5: ip pd | hi // GPIO5 = input cuando el PIR dispara.
Línea mala: 5: op dh pn | hi // GPIO5 = output.
64 Persiana de maqueta con finales de carrera
Eagle
Qué hace
Un SG90 sube o baja una persiana de maqueta hasta que un final de carrera, arriba o abajo, dice basta. No sigue empujando contra el tope. Es un servo, no un motor de persiana de verdad.
Materiales
Raspberry Pi
Protoboard
1 SG90
2 finales de carrera o microswitch
cables dupont
Cableado
Señal del servo GPIO13 físico 33, 5 V físico 2 de un solo SG90, GND físico 6.
Final de arriba: GPIO23 físico 16 a GND físico 9. Final de abajo: GPIO24 físico 18 a GND físico 14. Pull-up interna.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Si pides subir y el de arriba ya está cerrado, no se mueve. Igual hacia abajo.
from gpiozero import Button, Servo
from time import sleep
servo = Servo(13)
arriba = Button(23, pull_up=True)
abajo = Button(24, pull_up=True)
orden = input("sube o baja: ").strip().lower()
try:
if orden == "sube" and not arriba.is_pressed:
servo.max()
while not arriba.is_pressed:
sleep(0.05)
servo.mid()
elif orden == "baja" and not abajo.is_pressed:
servo.min()
while not abajo.is_pressed:
sleep(0.05)
servo.mid()
else:
print("tope o orden rara")
finally:
servo.detach()
Coloca los dos micros para que cierren justo al final del recorrido, con la mano, antes de automatizar.
Ejecuta y escribe sube.
Tiene que parar al pulsar el de arriba, no seguir.
Repite con baja.
Si no hay pulsación en dos segundos de prueba, para el proceso: el while no tiene otro tope. Añade tú un contador si tu maqueta puede trabarse.
Un solo SG90.
Circuito
Variantes
Timeout de 3 s que llama a detach aunque el micro no llegue.
Botón GPIO25 para repetir la última orden sin escribir.
Fallos típicos
Servo grande en el 5 V de la Pi.
Final de carrera alimentado a 5 V en vez de ir a GND.
Sin detach, el servo sigue haciendo fuerza en el tope.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 23
Línea buena: 23: ip pu | lo // GPIO23 = input cuando el final de carrera cierra a masa.
Línea mala: 23: op dh pn | hi // GPIO23 = output.
65 Robot que evita obstáculos
Eagle
Qué hace
Un carro pequeño mira con el HC-SR04 y, si hay algo a menos de 25 cm, para, gira y sigue. El motor A usa el mapa GPIO19, GPIO26 y enable GPIO18. El motor B va en GPIO20 y GPIO21, digitales, para poder girar. La batería de los motores es aparte. El conjunto del robot, chasis y batería, cae en consultar.
Materiales
Raspberry Pi
1 driver de dos canales que acepte lógica a 3,3 V
2 motores pequeños
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
batería de motores, fuera de la franja de un LED
cables dupont
Cableado
Motor A: GPIO19 físico 35, GPIO26 físico 37, enable GPIO18 físico 12. Motor B: GPIO20 físico 38 y GPIO21 físico 40, sin enable PWM.
HC-SR04 a 5 V físico 2, GND físico 14, TRIG GPIO5 físico 29, ECHO con 1 kΩ al GPIO6 físico 31 y 2 kΩ a GND físico 20. Masa de la batería unida al físico 6. No alimentes los motores desde un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DistanceSensor y un Motor gpiozero más dos salidas para el segundo canal. 0,25 m es una decisión de maqueta, no una calibración del sonido.
from gpiozero import DistanceSensor, Motor, DigitalOutputDevice
from time import sleep
dist = DistanceSensor(echo=6, trigger=5, max_distance=1)
a = Motor(forward=19, backward=26, enable=18)
b_f = DigitalOutputDevice(20)
b_b = DigitalOutputDevice(21)
try:
while True:
if dist.distance < 0.25:
a.stop(); b_f.off(); b_b.off(); sleep(0.2)
a.backward(0.5); b_f.off(); b_b.on(); sleep(0.4)
else:
a.forward(0.5); b_b.off(); b_f.on()
sleep(0.05)
finally:
a.stop(); b_f.off(); b_b.off()
Pon la mano delante: debe retroceder un poco y girar.
Quita la mano: sigue.
Baja el coche al suelo solo cuando el sentido de cada rueda sea el bueno.
Circuito
Variantes
Umbral 0,15 m en un laberinto estrecho.
Para total si la distancia se queda en el máximo, por si el sensor se ha desconectado.
Fallos típicos
ECHO sin divisor.
Motores desde el 5 V de la Pi o desde un GPIO.
Enable del motor y zumbador a la vez en el GPIO18.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 5
Línea buena: 5: op -- pn | hi // GPIO5 = output, TRIG.
Línea mala: 5: ip pd | lo // GPIO5 = input.
66 Seguidor de línea
Eagle
Qué hace
Dos sensores digitales de línea, a 3,3 V, corrigen un carro de dos motores. Si el izquierdo ve la cinta, gira hacia ese lado. El chasis completo va en consultar. No es un vehículo para la calle.
Materiales
Raspberry Pi
2 sensores digitales de línea que trabajen a 3,3 V
1 driver de dos motores
2 motores
batería de motores
cables dupont
Cableado
Sensor izquierdo GPIO23 físico 16, derecho GPIO24 físico 18, ambos con VCC a 3,3 V físico 1 y GND físico 9. Si el módulo solo funciona a 5 V, no conectes su salida al GPIO.
Motores como en la ficha del robot: A en GPIO19 físico 35 y GPIO26 físico 37 con enable GPIO18 físico 12; B en GPIO20 físico 38 y GPIO21 físico 40. Masa de batería al físico 6.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Las salidas digitales se leen con pull-up. Ajusta la comparación si tu módulo da nivel bajo al ver negro.
from gpiozero import DigitalInputDevice, DigitalOutputDevice, Motor
from time import sleep
izq = DigitalInputDevice(23, pull_up=True)
der = DigitalInputDevice(24, pull_up=True)
a = Motor(forward=19, backward=26, enable=18)
bf = DigitalOutputDevice(20)
bb = DigitalOutputDevice(21)
try:
while True:
negro_i = not izq.value
negro_d = not der.value
if negro_i and not negro_d:
a.forward(0.4); bf.off(); bb.on()
elif negro_d and not negro_i:
a.forward(0.4); bb.off(); bf.on()
elif negro_i and negro_d:
a.stop(); bf.off(); bb.off()
else:
a.forward(0.5); bb.off(); bf.on()
sleep(0.02)
finally:
a.stop(); bf.off(); bb.off()
Confirma que la salida del sensor a 3,3 V cambia al pasar de blanco a negro, con el polímetro o con un print, el carro aún en el aire.
Si el sentido de negro está al revés, quita el not.
Pinta una cinta que contraste.
Ejecuta y presenta la cinta.
Debe corregir, no dar vueltas siempre al mismo lado.
Para el proceso antes de coger el coche con las ruedas girando.
Circuito
Variantes
Velocidad 0,3 en una cinta con curvas cerradas.
Paro de un segundo si los dos ven negro, como una meta.
Fallos típicos
Sensor de 5 V directo al GPIO.
Motores alimentados por la Pi.
Enable en el GPIO18 y un zumbador a la vez.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 23
Línea buena: 23: ip pd | hi // GPIO23 = input cuando el sensor izquierdo ve claro u oscuro, según tu módulo, pero como entrada.
Línea mala: 23: op dh pn | hi // GPIO23 = output.
67 Cámara con zona de movimiento
Eagle
Qué hace
Compara dos fotogramas pequeños en RGB, solo el rectángulo central, y si la luz media cambia más de lo que tú anotas, guarda un JPEG y enciende el LED. No es un modelo de visión ni una alarma que salga de casa. La cámara va en consultar.
Materiales
Raspberry Pi
1 cámara que rpicam-still vea
1 LED
1 resistencia de 330 Ω
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6. Cámara al CSI o USB. Nada a 5 V en el LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
rpicam-still --width 32 --height 24 --encoding rgb escribe bytes crudos. El umbral es una diferencia de brillo que tú subes si hay falsos avisos. No hay red neuronal.
from datetime import datetime
from gpiozero import LED
from pathlib import Path
from subprocess import run
led = LED(17)
UMBRAL = 12
def frame(ruta):
run(["rpicam-still", "-n", "--width", "32", "--height", "24", "--encoding", "rgb", "-o", str(ruta)], check=True)
return Path(ruta).read_bytes()
def media(blob):
w, h = 32, 24
s = n = 0
for y in range(6, 18):
for x in range(8, 24):
i = (y * w + x) * 3
s += blob[i] + blob[i+1] + blob[i+2]
n += 3
return s / n
antes = frame("a.rgb")
try:
while True:
ahora = frame("b.rgb")
if abs(media(ahora) - media(antes)) > UMBRAL:
led.on()
nombre = "zona-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg"
run(["rpicam-still", "-n", "-o", nombre], check=True)
print(nombre)
led.off()
antes = ahora
finally:
led.close()
Ejecuta con la escena quieta y sube UMBRAL si salta sola.
Mueve la mano en el centro. Debe guardar un JPEG.
Mueve algo solo en una esquina: el rectángulo central puede no verlo. Eso es la zona, no un fallo de enfoque.
No subas las fotos.
Ctrl+C.
Circuito
Variantes
Rectángulo más bajo, cambiando el range de y.
Sin LED, solo el archivo.
Fallos típicos
raspistill.
Creer que esto reconoce personas. Solo compara brillo.
Pedalear el umbral hasta que cualquier sombra dispare y llamarlo detector.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: rpicam-still --list-cameras
Línea buena: 0 : y un sensor.
Línea mala: No cameras available!
68 Acceso con RFID RC522
Eagle
Qué hace
Habla con un RC522 por SPI, alimentado a 3,3 V y nunca a 5 V, y enseña el registro de versión. Si el módulo Python spidev no está, no inventa un UID: solo dice que el dispositivo existe o no. El RST ocupa el GPIO25, así que no hay zumbador en ese pin; si quieres pitido, va al GPIO18.
Materiales
Raspberry Pi
Protoboard
1 RC522
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
VCC del RC522 al 3,3 V físico 1, nunca al 5 V. GND físico 6. SCK GPIO11 físico 23, MOSI GPIO10 físico 19, MISO GPIO9 físico 21, CE0 GPIO8 físico 24, RST GPIO25 físico 22.
LED de tarjeta vista: GPIO17 físico 11, 330 Ω, GND físico 9. No hay zumbador en el 25.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtparam=spi=on. El registro 0x37 de un MFRC522 genuino responde 0x91 o 0x92. Eso es detección, no una tarjeta inventada.
import os
print("VCC del RC522 a 3,3 V. Nunca a 5 V. RST en GPIO25: no hay zumbador ahi.")
if not os.path.exists("/dev/spidev0.0"):
raise SystemExit("No existe /dev/spidev0.0. No invento un UID.")
try:
import spidev
except ImportError:
raise SystemExit("Falta el modulo spidev. El dispositivo SPI si existe. No invento un UID.")
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 100000
spi.mode = 0
def rd(reg):
return spi.xfer2([((reg << 1) & 0x7E) | 0x80, 0])[1]
ver = rd(0x37)
print("version", hex(ver))
if ver not in (0x91, 0x92):
print("No es la respuesta de version que el datasheet asocia al RC522. No fabrico un UID.")
else:
print("Chip visto. Acerca una tarjeta tuya: el UID completo esta en Software, esta ficha no lo rellena si la trama falla.")
Si imprime otra cosa, no sigas como si la tarjeta se hubiera leído.
El LED de esta ficha no se enciende solo: la detección es el hexadecimales.
No pongas 5 V ni aunque el módulo lo encienda un segundo.
Circuito
Variantes
Zumbador activo en GPIO18, físico 12, un pitido si la versión cuadra. Dilo: el 25 está ocupado por RST.
LED que enciendes tú en el código solo después de ver 0x91 o 0x92, no antes.
Fallos típicos
VCC a 5 V: puedes destruir el lector y, si MISO sube a 5 V, el GPIO.
Zumbador en GPIO25 a la vez que RST.
Inventar un UID cuando el registro de versión no cuadra.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /dev/spidev0.0
Línea buena: el dispositivo existe.
Línea mala: No such file or directory. Falta dtparam=spi=on, o el módulo no está en el bus.
69 Menú de modos en OLED
Eagle
Qué hace
Dos botones recorren tres modos —reposo, aviso y medida— y el modo elegido se imprime y se marca con una barra en el OLED. No cambia hardware peligroso: solo elige un nombre que otro programa tuyo podría obedecer.
Materiales
Raspberry Pi
Protoboard
1 OLED SSD1306
2 pulsadores
cables dupont
Cableado
OLED a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5.
Botón siguiente GPIO23 físico 16 a GND físico 9. Botón aceptar GPIO24 físico 18 a GND físico 14.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
I2C activo. El menú es una lista de Python. El OLED recibe una página distinta según el índice, para que se vea el cambio sin una fuente de texto completa.
import fcntl, os
from gpiozero import Button
from signal import pause
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0x8D,0x14,0xAF,0x20,0x00):
cmd(c)
modos = ["reposo", "aviso", "medida"]
i = 0
def pintar():
cmd(0x21); cmd(0); cmd(127); cmd(0x22); cmd(0); cmd(0)
os.write(fd, bytes([0x40]) + bytes([0xFF if n < (i+1)*40 else 0x00 for n in range(128)]))
print("modo", modos[i])
def siguiente():
global i
i = (i + 1) % len(modos)
pintar()
def aceptar():
print("elegido", modos[i])
Button(23, pull_up=True, bounce_time=0.05).when_pressed = siguiente
Button(24, pull_up=True, bounce_time=0.05).when_pressed = aceptar
pintar()
pause()
No hay relé en esta ficha: elegir aviso no pita solo.
Ctrl+C.
Circuito
Variantes
Tres modos con otros nombres, los de tu maqueta.
Un tercer botón para volver atrás.
Fallos típicos
Botones a 5 V.
OLED a 5 V.
GPIO2 usado como botón y como SDA a la vez.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c.
Línea mala: Could not open file /dev/i2c-1 o sin 3c.
70 Aviso de consumo con sensor de corriente
Eagle
Qué hace
Un sensor de corriente de hobby entra al ADS1115 a través de un divisor, y el LED se enciende si el entero supera el que tú anotaste con la carga de 12 V encendida. No se convierte a amperios. No es el trabajo de un electricista ni una medida certificada.
Materiales
Raspberry Pi
Protoboard
1 sensor de corriente de hobby, tipo módulo de efecto Hall
1 ADS1115
1 resistencia de 10 kΩ
1 resistencia de 15 kΩ
1 LED
1 resistencia de 330 Ω
cables dupont
Cableado
Si el módulo se alimenta a 5 V, VCC al físico 2 y GND al físico 6. Su salida no va directa a AIN0: 10 kΩ hasta AIN0 y 15 kΩ de AIN0 a GND, para no pasar de unos 3 V.
ADS a 3,3 V físico 1, SDA físico 3, SCL físico 5, ADDR a GND. LED GPIO17 físico 11 con 330 Ω. La carga que mides es de baja tensión, la que ya sepas manejar. Prohibido: no uses 230 V en este sensor de hobby.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Entero crudo. UMBRAL lo copias de la terminal con tu lámpara de 12 V encendida, no de una hoja de sensibilidad inventada aquí.
import fcntl, os, time
from gpiozero import LED
I2C_SLAVE = 0x0703
UMBRAL = 8000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
led = LED(17)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
print("Hobby. No es una medida de electricista. Prohibido: no uses 230 V.")
try:
while True:
n = crudo()
led.value = n >= UMBRAL
print("crudo", n)
time.sleep(0.5)
finally:
led.close()
Monta el divisor antes de unir la salida del sensor.
Anota el entero en reposo y el entero con tu carga de baja tensión.
Pon UMBRAL entre los dos, del lado del consumo que quieres avisar.
Ejecuta. El LED sigue a ese cruce.
No dividas el entero por una sensibilidad sacada de memoria.
No pases un cable de 230 V por el sensor.
Circuito
Variantes
Aviso solo si el cruce se mantiene diez lecturas.
Escribe el crudo en un CSV sin columna de amperios.
Fallos típicos
Salida del sensor de 5 V directa al ADS alimentado a 3,3 V.
Llamar amperios al entero.
Usar el montaje en la instalación de casa. Prohibido: no uses 230 V.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48.
Línea mala: sin ADS no hay aviso. No estimes amperios a ojo.
71 Lectura de batería y apagado limpio
Eagle
Qué hace
Un divisor mete en el ADS1115 una fracción de la batería, siempre por debajo de 3,3 V en el pin. Si el entero baja del que tú anotaste con la batería ya justa, la Pi hace shutdown -h now. No calculo porcentaje. La batería que ya tengas no entra en la franja; el ADS y las resistencias, sí. Tirar del cable no es el método.
Materiales
Raspberry Pi
Protoboard
1 ADS1115
1 resistencia de 20 kΩ
1 resistencia de 10 kΩ
cables dupont
La batería, si la compras, puede pasar de esta franja
Cableado
Pack al divisor: positivo, 20 kΩ, nudo AIN0, 10 kΩ, GND del pack y GND físico 6 de la Pi. Con 8,4 V el nudo queda cerca de 2,8 V. Si tu pack es más alto, cambia resistencias para que AIN0 no pase de 3,3 V y vuelve a medirlo con polímetro antes de conectar el ADS.
ADS a 3,3 V físico 1, SDA físico 3, SCL físico 5, ADDR a GND. Nada del positivo del pack va a un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Entero crudo. BAJA es el entero que viste cuando consideraste la batería justa, no una fórmula de química. El apagado es sudo shutdown -h now.
import fcntl, os, time
from subprocess import run
I2C_SLAVE = 0x0703
BAJA = 6000
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
def crudo():
os.write(fd, bytes([0x01, 0xC3, 0x83])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
while True:
n = crudo()
print("crudo", n)
if 0 < n < BAJA:
print("apagado limpio")
run(["sudo", "shutdown", "-h", "now"])
break
time.sleep(10)
Calcula el divisor para tu voltaje máximo y mídelo antes de tocar el ADS.
Anota el entero con la batería en el punto en que quieras parar. Escríbelo en BAJA.
Ejecuta y comprueba que con la batería llena el número queda por encima.
No pruebes el apagado hasta estar seguro del signo de la comparación.
El apagado es el comando del sistema, no retirar el cable.
Si sudo pide clave, arréglalo en el sistema, no la pegues aquí.
Circuito
Variantes
Tres lecturas seguidas por debajo de BAJA antes de apagar.
LED en GPIO17 cuando estás a menos de un 10 por ciento del recorrido entre tu entero lleno y BAJA: ese aviso sigue siendo una comparación de enteros, no un porcentaje de carga química.
Fallos típicos
Pack de 12 V directo a AIN0 o a un GPIO.
Convertir el entero en un tanto por ciento de fábrica.
Cortar la corriente tirando del cable como costumbre.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48.
Línea mala: sin ADS no hay lectura y no debes apagar por un número que no existe.
72 Varios DS18B20 en un bus
Eagle
Qué hace
Varias sondas DS18B20 comparten el mismo GPIO4 y una sola pull-up de 4,7 kΩ. El programa lista cada carpeta 28- y su temperatura de kernel. No supone un orden físico: el nombre de la carpeta es la identidad.
Materiales
Raspberry Pi
Protoboard
2 o más DS18B20
1 resistencia de 4,7 kΩ
cables dupont
Cableado
Todos los datos unidos al GPIO4 físico 7. Una sola resistencia de 4,7 kΩ de esa línea al 3,3 V físico 1. Todos los VDD al 3,3 V. Todos los GND al físico 9.
No pongas una pull-up por sonda. Nada a 5 V. Cables cortos.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtoverlay=w1-gpio. Se saltan las lecturas sin YES.
import glob, os, time
def todas():
salida = []
for ruta in sorted(glob.glob("/sys/bus/w1/devices/28-*/w1_slave")):
ident = os.path.basename(os.path.dirname(ruta))
texto = open(ruta).read()
if "YES" not in texto:
salida.append((ident, None))
continue
salida.append((ident, int(texto.strip().split("t=")[-1]) / 1000.0))
if not salida:
raise SystemExit("Ninguna sonda. No invento la lista.")
return salida
while True:
for ident, g in todas():
print(ident, g)
time.sleep(2)
Ejecuta y calienta solo una con los dedos. Solo esa identidad sube.
Anota qué carpeta es el agua y cuál el aire, en un papel, no en el orden del listado.
Añadir una tercera no cambia el pin.
Circuito
Variantes
Ignora las que devuelven None en vez de imprimirlas.
Escribe un CSV con una columna por identidad.
Fallos típicos
Una pull-up de 4,7 kΩ en cada sonda, todas en paralelo: la línea queda demasiado fuerte o rara.
Esperar que el primer nombre sea siempre la sonda de la izquierda.
Datos en el GPIO26.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: tantas carpetas 28- como sondas hayas puesto.
Línea mala: una sola 28- cuando hay tres sondas, o solo w1_bus_master1.
73 Dos programas que se dejan notas en disco
Eagle
Qué hace
Un programa escribe una nota en un archivo y otro la lee, enciende el LED y la borra. Se excluyen con un candado del sistema para no pisarse la frase. No hay red entre ellos: solo el disco de esta Pi.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
fcntl.flock sobre la nota. Lanzas dos terminales: uno escribe, el otro escucha. El texto de ejemplo es el de abajo; son dos programas en un solo archivo, separados por la variable que elijas al arrancar.
import fcntl, os, sys, time
from gpiozero import LED
RUTA = "/tmp/nota-lesvencimos.txt"
led = LED(17)
def con_candado(modo):
fd = os.open(RUTA, os.O_RDWR | os.O_CREAT, 0o644)
fcntl.flock(fd, fcntl.LOCK_EX)
return fd
if len(sys.argv) > 1 and sys.argv[1] == "escribe":
fd = con_candado("w")
os.lseek(fd, 0, os.SEEK_SET)
os.ftruncate(fd, 0)
os.write(fd, b"hay correo
")
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
print("nota dejada")
else:
print("escuchando")
try:
while True:
fd = con_candado("r")
os.lseek(fd, 0, os.SEEK_SET)
dato = os.read(fd, 200)
if dato.strip():
led.on()
print(dato.decode())
os.ftruncate(fd, 0)
else:
led.off()
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
time.sleep(0.5)
finally:
led.close()
Una página local enciende o apaga cuatro relés de cargas de 5–12 V. El primero es el GPIO12 del mapa. Los otros tres usan GPIO16, GPIO20 y GPIO21, que son los del RGB: no montes el LED RGB a la vez. Prohibido: no uses 230 V. No abras puertos en el router.
Materiales
Raspberry Pi
Protoboard o regleta
4 módulos de relé, o una placa de cuatro canales
cargas de 5–12 V y su fuente
cables dupont
Prohibido: no uses 230 V
Cableado
IN en GPIO12 físico 32, GPIO16 físico 36, GPIO20 físico 38 y GPIO21 físico 40. VCC de la placa de relés al 5 V físico 2 si la bobina es de 5 V. GND físico 6 común con la fuente de las cargas.
Cada carga en sus contactos, solo 5–12 V. El RGB de la ficha 05 no se usa a la vez. Prohibido: no uses 230 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
HTTPServer y cuatro OutputDevice activos a nivel bajo. La página no carga nada de fuera.
from gpiozero import OutputDevice
from http.server import BaseHTTPRequestHandler, HTTPServer
pines = (12, 16, 20, 21)
reles = [OutputDevice(p, active_high=False, initial_value=False) for p in pines]
print("Prohibido: no uses 230 V. No abras puertos en el router. No montes el RGB a la vez.")
class H(BaseHTTPRequestHandler):
def do_GET(self):
cuerpo = b"<!DOCTYPE html><html lang=es><meta charset=utf-8><title>Reles</title><body>"
cuerpo += "<p>Solo tu red de casa. Cargas de 5 a 12 V.</p>".encode()
for i in range(4):
cuerpo += ('<form method=post action="/r/%d"><button>Rele %d</button></form>' % (i, i+1)).encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(cuerpo)))
self.end_headers()
self.wfile.write(cuerpo)
def do_POST(self):
if self.path.startswith("/r/"):
i = int(self.path[3:])
if 0 <= i < 4:
reles[i].toggle()
self.do_GET()
def log_message(self, formato, *a):
return
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
Prueba cada relé con una carga de juguete de 12 V.
Abre el puerto 8080 en el navegador de casa.
Pulsa Relé 1 y escucha el clic.
Comprueba que el RGB no está cableado en 36, 38 y 40.
No reenvíes el puerto.
Circuito
Variantes
Ata el servidor a 127.0.0.1.
Un botón Apagar todos que recorra los cuatro y llame a off().
Fallos típicos
230 V. Prohibido: no uses 230 V.
RGB y relés en los mismos GPIO a la vez.
Página con scripts de fuera.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 12
Línea buena: 12: op dl pn | lo // GPIO12 = output, primer relé.
Línea mala: 12: ip pd | lo // GPIO12 = input.
75 Vigilancia local con límite de disco
Eagle
Qué hace
Saca una foto local cada minuto mientras quede espacio de sobra. Si el disco baja del límite, para y enciende el LED fijo. No sube nada y no abre el router. La cámara va en consultar.
Materiales
Raspberry Pi
1 cámara vista por libcamera
1 LED
1 resistencia de 330 Ω
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6. Cámara al CSI o USB.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
shutil.disk_usage mira el sistema de archivos local. rpicam-still guarda en la carpeta vigilancia.
import shutil
from datetime import datetime
from gpiozero import LED
from pathlib import Path
from subprocess import run
from time import sleep
led = LED(17)
carpeta = Path("vigilancia")
carpeta.mkdir(exist_ok=True)
MINIMO = 500 * 1024 * 1024
try:
while True:
libre = shutil.disk_usage(carpeta).free
if libre < MINIMO:
led.on()
print("disco justo", libre)
break
led.off()
nombre = carpeta / (datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg")
run(["rpicam-still", "-n", "-o", str(nombre)], check=True)
print(nombre, "libre", libre)
sleep(60)
finally:
pass
El TCS3200 se alimenta a 3,3 V para que OUT no suba a 5 V. El script elige filtro y cuenta flancos a ojo de Python durante un instante. Esos pulsos son crudos: no salen un rojo, un verde ni un porcentaje de color.
Materiales
Raspberry Pi
Protoboard
1 módulo TCS3200 que acepte 3,3 V
cables dupont
Cableado
VCC al 3,3 V físico 1, nunca a 5 V si OUT va a un GPIO. GND físico 6. S0 GPIO17 físico 11, S1 GPIO27 físico 13, S2 GPIO22 físico 15, S3 GPIO16 físico 36, OUT GPIO23 físico 16.
Nada de 5 V en OUT.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
S0 alto y S1 bajo piden la escala del 2 por ciento, la más lenta, para que un bucle de Python vea algún flanco. Sigue siendo un conteo de hobby.
from gpiozero import DigitalInputDevice, DigitalOutputDevice
from time import sleep, time
s0 = DigitalOutputDevice(17)
s1 = DigitalOutputDevice(27)
s2 = DigitalOutputDevice(22)
s3 = DigitalOutputDevice(16)
out = DigitalInputDevice(23, pull_up=False)
s0.on(); s1.off()
def pulsos():
n = 0
anterior = out.value
t0 = time()
while time() - t0 < 0.1:
ahora = out.value
if ahora and not anterior:
n += 1
anterior = ahora
return n
def filtro(a, b):
s2.value = a
s3.value = b
sleep(0.05)
return pulsos()
while True:
print("rojo", filtro(0, 0), "verde", filtro(1, 1), "azul", filtro(0, 1), "claro", filtro(1, 0))
sleep(0.5)
Ejecuta y acerca un papel blanco: los cuatro números no tienen por qué parecerse a 255.
Acerca un papel rojo. El conteo del filtro rojo puede subir respecto a los otros. Puede no hacerlo si el sondeo se pierde flancos: dilo, no lo tapes con una fórmula.
No llames a eso RGB calibrado.
Si OUT no late, la escala o el pin están mal.
No pases el módulo a 5 V.
Circuito
Variantes
Ventana de 0,2 s si los números salen siempre a cero.
Guarda los cuatro enteros en un CSV sin columna de color con nombre.
Fallos típicos
Módulo a 5 V y OUT al GPIO23.
Dar un porcentaje de rojo dividiendo los pulsos.
S0 y S1 en apagado: el chip está en power-down y OUT no late.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 23
Línea buena: 23: ip pd | hi // GPIO23 = input, la patilla OUT late.
Línea mala: 23: op dh pn | hi // GPIO23 = output. OUT se cuenta, no se empuja.
77 Aire: gas, temperatura y registro
Eagle
Qué hace
Cada minuto guarda la temperatura del DHT11, si el kernel la tiene, y el entero crudo del MQ-2 a través del ADS. No hay ppm. Es un registro de hobby, no un analizador ni una alarma homologada.
Materiales
Raspberry Pi
Protoboard
1 MQ-2
1 ADS1115
1 resistencia de 10 kΩ
1 resistencia de 15 kΩ
1 DHT11
1 resistencia de 10 kΩ para el DHT
cables dupont
Cableado
MQ-2 a 5 V físico 2 para el calentador, GND físico 6, AO con 10 kΩ a AIN0 y 15 kΩ de AIN0 a GND. ADS a 3,3 V físico 1, SDA físico 3, SCL físico 5.
DHT11 a 3,3 V, datos GPIO26 físico 37, pull-up de 10 kΩ. No uses el GPIO4.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
CSV con la hora, el entero del gas y la temperatura del kernel o la palabra ausente. Nada de porcentaje de gas.
import csv, fcntl, glob, os, time
from datetime import datetime
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
def gas():
os.write(fd, bytes([0x01, 0xC3, 0x83])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
def temp():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
if os.path.exists(t):
return int(open(t).read())/1000.0
return None
print("Hobby. No es una alarma homologada.")
while True:
fila = [datetime.now().isoformat(timespec="seconds"), gas(), temp()]
with open("aire.csv", "a", newline="") as f:
csv.writer(f).writerow(fila)
print(fila)
time.sleep(60)
Precalienta el sensor antes de fiarte de la primera fila.
Comprueba que la temperatura no es None.
Abre aire.csv: tres columnas, la del medio es un entero.
No añadas una columna de ppm.
Ventila la mesa cuando acabes la prueba.
Circuito
Variantes
LED en GPIO17 si el entero sube más de un margen sobre la primera fila, margen que tú fijas.
Fila cada 10 s en una demo.
Fallos típicos
AO sin divisor.
Llamar al entero calidad del aire en porcentaje.
DHT en el GPIO4.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48 del ADS del MQ-2.
Línea mala: sin 48 no hay columna de gas, y no se rellena con un cero que parezca aire limpio.
78 Control de temperatura con histéresis
Eagle
Qué hace
El ventilador de 5–12 V se enciende al pasar de un grado y no se apaga hasta bajar de otro más fresco. Ese hueco es la histéresis: evita que el relé tabletee en el límite. Los grados los da el kernel. Prohibido: no uses 230 V.
Materiales
Raspberry Pi
Protoboard
1 DS18B20
1 resistencia de 4,7 kΩ
1 relé
1 ventilador de 5–12 V y fuente
cables dupont
Prohibido: no uses 230 V
Cableado
Sonda en GPIO4 físico 7, pull-up 4,7 kΩ al 3,3 V físico 1, GND físico 9.
Relé GPIO12 físico 32, bobina 5 V físico 2, GND físico 6. Ventilador solo de baja tensión.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
dtoverlay=w1-gpio. ALTA y BAJA las eliges tú, con BAJA menor que ALTA.
import glob, os, time
from gpiozero import OutputDevice
ALTA = 29.0
BAJA = 27.0
rele = OutputDevice(12, active_high=False, initial_value=False)
encendido = False
def grados():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
raise SystemExit("Sin sonda.")
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
return int(texto.strip().split("t=")[-1]) / 1000.0
print("Prohibido: no uses 230 V.")
try:
while True:
g = grados()
if g is not None:
if not encendido and g >= ALTA:
encendido = True
elif encendido and g <= BAJA:
encendido = False
rele.value = encendido
print(g, "on" if encendido else "off")
time.sleep(2)
finally:
rele.off()
Calienta la sonda por encima de ALTA: el ventilador arranca.
Mientras no baje de BAJA, sigue aunque ya hayas soltado un poco la sonda.
Al cruzar BAJA, para.
Eso es la histéresis, no un retardo al azar.
Nada de 230 V.
Circuito
Variantes
Histéresis de un solo grado si el ventilador es pequeño.
LED en GPIO17 que copie a encendido.
Fallos típicos
ALTA y BAJA iguales: vuelve el tableteo.
230 V. Prohibido: no uses 230 V.
Usar una lectura sin YES.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: una carpeta 28-.
Línea mala: solo w1_bus_master1.
79 Reloj: RTC si no hay red, NTP si la hay
Eagle
Qué hace
Si el sistema dice que el reloj está sincronizado por NTP, se imprime esa hora y la fuente. Si no, se leen los registros BCD del DS3231. No se fabrica un cliente NTP a mano ni se abre un puerto. El modo 12 h se rechaza.
Materiales
Raspberry Pi
Protoboard
1 DS3231 a 3,3 V
cables dupont
Cableado
DS3231: 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5. Nada a 5 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
timedatectl dice si NTP ya sincronizó. El chip se lee solo cuando eso no ocurre. I2C y, si quieres el dispositivo de reloj del kernel, dtoverlay=i2c-rtc,ds3231 en config.txt.
import fcntl, os, time
from datetime import datetime
from subprocess import run
I2C_SLAVE = 0x0703
def ntp_ok():
r = run(["timedatectl", "show", "-p", "NTPSynchronized", "--value"], capture_output=True, text=True)
return r.stdout.strip() == "yes"
def bcd(v):
return ((v >> 4) & 0x0F) * 10 + (v & 0x0F)
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x68)
while True:
if ntp_ok():
print("NTP", datetime.now().isoformat(timespec="seconds"))
else:
os.write(fd, bytes([0x00]))
raw = os.read(fd, 3)
if raw[2] & 0x40:
raise SystemExit("RTC en 12 h. No adivino la hora.")
print("RTC", "%02d:%02d:%02d" % (bcd(raw[2] & 0x3F), bcd(raw[1] & 0x7F), bcd(raw[0] & 0x7F)))
time.sleep(2)
Con la red de casa, timedatectl debe poder decir yes. El script imprime NTP.
Quita el cable de red y espera a que deje de estar sincronizado. Pasa a imprimir RTC.
Si el RTC está en 00:00:00, nadie le puso la hora: esta ficha no la inventa.
Vuelve a conectar la red si quieres.
No abras puertos para forzar NTP.
Circuito
Variantes
Si existe /dev/rtc0, hwclock -r es la lectura del kernel y puedes preferirla al BCD de usuario.
LED en GPIO17 encendido solo en el modo RTC, para ver que estás sin red.
Fallos típicos
RTC a 5 V.
Imprimir NTP cuando timedatectl dice no.
Traducir el modo 12 h sin mirar el bit.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 68 del DS3231.
Línea mala: 68 ausente. Entonces no hay hora de chip que enseñar, aunque el sistema tenga hora por NTP.
80 Vigilante que relanza el programa
Eagle
Qué hace
Un programa pequeño vigila a otro. Si el hijo se cae, espera dos segundos y lo vuelve a lanzar. El LED late en cada vuelta. No es un servicio escondido: lo ves en la terminal. La unidad de systemd, si la quieres al encender, está descrita en Software y no se instala sola desde aquí.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
LED GPIO17 físico 11, 330 Ω, GND físico 6.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El hijo de ejemplo es un Python de una línea que sale a los pocos segundos, para que veas el relanzamiento. Sustituye el comando por el script tuyo. systemd queda como enlace, no como un archivo que yo copie al sistema sin que tú lo pidas en la Pi.
from gpiozero import LED
from subprocess import run
from time import sleep
led = LED(17)
hijo = ["python3", "-c", "import time; print('hijo'); time.sleep(3); raise SystemExit(1)"]
try:
while True:
led.on()
print("lanzo")
codigo = run(hijo).returncode
led.off()
print("salio", codigo, "relanzo en 2 s")
sleep(2)
finally:
led.close()
En una maqueta, el LED de la sala se enciende con un botón, el reed dice si la puerta está abierta, el PIR enciende ese mismo aviso un rato y el DHT11 imprime el aire si el kernel lo ve. Un relé puede mover un ventilador de 5–12 V. Prohibido: no uses 230 V. La caseta de cartón no entra en la cuenta; la electrónica añadida, sí.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
1 pulsador
1 reed
1 PIR de 3,3 V en la salida
1 DHT11
1 resistencia de 10 kΩ
1 relé
1 ventilador de 5–12 V
cables dupont
Prohibido: no uses 230 V
Cableado
LED GPIO17 físico 11 con 330 Ω a GND físico 6. Botón GPIO24 físico 18 a GND físico 14. Reed GPIO23 físico 16 a GND físico 9. PIR OUT GPIO5 físico 29, VCC 5 V físico 2, GND físico 20, solo si OUT es de 3,3 V.
DHT11 datos GPIO26 físico 37, 3,3 V físico 1, GND físico 34, pull-up 10 kΩ. Relé GPIO12 físico 32, bobina 5 V físico 4, GND físico 30. No hay RGB. El ventilador no es de red.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero para luces y contactos. El DHT, si está el overlay, se imprime desde sysfs y si no está se dice, sin cifra falsa. El relé solo actúa si el kernel da una temperatura y pasa de 30.
import glob, os
from gpiozero import LED, Button, DigitalInputDevice, MotionSensor, OutputDevice
from signal import pause
led = LED(17)
luz = Button(24, pull_up=True, bounce_time=0.05)
reed = DigitalInputDevice(23, pull_up=True)
pir = MotionSensor(5)
rele = OutputDevice(12, active_high=False, initial_value=False)
luz.when_pressed = led.toggle
def puerta():
print("puerta", "abierta" if reed.value else "cerrada")
reed.when_deactivated = puerta
reed.when_activated = puerta
pir.when_motion = led.on
print("Prohibido: no uses 230 V.")
def temp():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
if os.path.exists(t):
return int(open(t).read())/1000.0
return None
print("aire", temp() if temp() is not None else "sin DHT, no invento")
pause()
Reparte masas y no juntes 5 V del PIR con un GPIO.
Prueba el botón: el LED cambia.
Abre el reed: la terminal lo dice.
Pasa la mano por el PIR: el LED enciende.
Si temp() es None, arregla el overlay antes de querer el ventilador. El relé de esta versión queda preparado y apagado hasta que amplíes el bucle con el umbral.
Ninguna carga de 230 V en la maqueta.
Circuito
Variantes
Añade en un while el umbral de 30 grados para el relé, con la cifra del kernel.
No uses el PIR y deja la casa solo con puerta y luz.
Fallos típicos
230 V en el relé de la maqueta. Prohibido: no uses 230 V.
DHT en el GPIO4.
PIR a 5 V en el GPIO5.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 29
Línea buena: 29: ip pd | hi // GPIO5 = input con el PIR activo. El físico 29 es el GPIO5.
Línea mala: 29: op dh pn | hi // GPIO5 = output.
82 Invernadero con día, noche e historial
Fénix
Qué hace
La LDR, por tiempo de carga y no por ADC, dice si es de día. La sonda da los grados del kernel. La tierra entra como entero del ADS. Todo se anota en un CSV y, de día y con tierra seca, un relé riega un momento con bomba de 5–12 V. Prohibido: no uses 230 V.
Materiales
Raspberry Pi
Protoboard
1 LDR
1 condensador de 1 µF
1 DS18B20
1 resistencia de 4,7 kΩ
1 sonda de suelo
1 ADS1115
1 relé
1 bomba de 5–12 V
cables dupont
Prohibido: no uses 230 V
Cableado
LDR entre 3,3 V físico 1 y GPIO27 físico 13, condensador a GND físico 9. DS18B20 en GPIO4 físico 7 con 4,7 kΩ.
ADS SDA físico 3, SCL físico 5, sonda a AIN0. Relé GPIO12 físico 32. Masas al físico 6. Bomba de baja tensión.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
LightSensor para el día. Grados solo si hay YES. Entero de suelo sin porcentaje. CSV con las tres cosas.
import csv, fcntl, glob, os, time
from datetime import datetime
from gpiozero import LightSensor, OutputDevice
I2C_SLAVE = 0x0703
SECO = 18000
ldr = LightSensor(27)
rele = OutputDevice(12, active_high=False, initial_value=False)
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
def grados():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
return None
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
return int(texto.strip().split("t=")[-1]) / 1000.0
def suelo():
os.write(fd, bytes([0x01, 0xC3, 0x83])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
print("Prohibido: no uses 230 V.")
try:
while True:
dia = ldr.value > 0.4
g = grados()
s = suelo()
if dia and s >= SECO:
rele.on(); time.sleep(2); rele.off()
with open("invernadero.csv", "a", newline="") as f:
csv.writer(f).writerow([datetime.now().isoformat(timespec="seconds"), "dia" if dia else "noche", g, s])
print(dia, g, s)
time.sleep(60)
finally:
rele.off()
De noche tapa la LDR y comprueba que no riega aunque el entero esté alto.
De día destapa y, con tierra seca, debe haber un pulso corto.
Mira el CSV: la columna de grados es un número o está vacía, nunca un 20 puesto a mano.
La bomba es de 5–12 V.
No dejes el relé enganchado si el proceso muere: el finally lo suelta si sales con Ctrl+C.
Circuito
Variantes
No riegues si g es None.
Umbral de luz 0,2 en un rincón oscuro.
Fallos típicos
230 V. Prohibido: no uses 230 V.
LDR usada como si fuera un voltio en un GPIO.
Dos pull-up en el DS18B20.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: carpeta 28- de la sonda.
Línea mala: solo w1_bus_master1. El día y la noche de la LDR no sustituyen la temperatura.
83 Robot con cámara y mando web local
Fénix
Qué hace
Desde una página de la red de casa mandas adelante, atrás o parar, y pides una foto que se guarda en el aparato. Los motores van con driver y batería aparte. No abras puertos en el router y no hay analítica. La cámara y el chasis van en consultar.
Materiales
Raspberry Pi
1 cámara
1 driver y dos motores pequeños
batería de motores
cables dupont
Cableado
Motor A GPIO19 físico 35, GPIO26 físico 37, enable GPIO18 físico 12. Motor B GPIO20 físico 38 y GPIO21 físico 40.
Masa de la batería al físico 6. Cámara al CSI o USB. No hay DHT en el 26: ese pin es dirección del motor.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
HTTPServer local y rpicam-still. Los botones del formulario son rutas relativas.
from gpiozero import DigitalOutputDevice, Motor
from http.server import BaseHTTPRequestHandler, HTTPServer
from subprocess import run
a = Motor(forward=19, backward=26, enable=18)
bf = DigitalOutputDevice(20)
bb = DigitalOutputDevice(21)
def parar():
a.stop(); bf.off(); bb.off()
class H(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/foto.jpg":
run(["rpicam-still", "-n", "-o", "robot.jpg"], check=False)
data = open("robot.jpg", "rb").read()
self.send_response(200)
self.send_header("Content-Type", "image/jpeg")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
cuerpo = ("<!DOCTYPE html><html lang=es><meta charset=utf-8><title>Robot</title><body>"
"<p>Solo tu red de casa. No abras puertos en el router.</p>"
"<form method=post action=/adelante><button>Adelante</button></form>"
"<form method=post action=/atras><button>Atras</button></form>"
"<form method=post action=/parar><button>Parar</button></form>"
"<img src=foto.jpg alt=foto>").encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(cuerpo)))
self.end_headers()
self.wfile.write(cuerpo)
def do_POST(self):
parar()
if self.path == "/adelante":
a.forward(0.5); bf.on()
elif self.path == "/atras":
a.backward(0.5); bb.on()
self.do_GET()
def log_message(self, formato, *args):
return
print("Puerto 8080 en casa.")
try:
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
finally:
parar()
Abre el puerto 8080 desde otro equipo de casa, no desde la calle.
Adelante, parar, atrás.
Pide la imagen. Se queda en robot.jpg.
No crees un reenvío de puertos.
Circuito
Variantes
Ata a 127.0.0.1 si solo conduces con un monitor en la propia Pi.
Baja el 0,5 si los motores derrapan.
Fallos típicos
Motores desde la Pi.
raspistill.
Exponer el puerto en el router.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: rpicam-still --list-cameras
Línea buena: 0 : y un sensor.
Línea mala: No cameras available! El mando puede mover motores y no haber imagen.
84 Meteo de balcón con registro de un mes
Fénix
Qué hace
Cada hora, durante muchos días, guarda la temperatura del DHT11 si el kernel la tiene y los enteros crudos de presión y temperatura del BMP280. Un archivo por día, pensado para un mes en el balcón del aparato. Los crudos no son hPa ni una segunda temperatura calibrada.
Materiales
Raspberry Pi
Protoboard
1 DHT11
1 resistencia de 10 kΩ
1 BMP280 a 3,3 V
cables dupont
Una caja que proteja la Pi de la lluvia no está en el precio del chip
Protege la electrónica del agua. El balcón no es una excusa para mojar la Pi.
Comprueba id 0x58 o 0x60.
Lanza una vuelta y abre el CSV del día.
La tupla de presión es cruda. No la multipliques.
A la hora siguiente hay otra fila en el mismo archivo.
Al cambiar el día, otro nombre.
Circuito
Variantes
Cada 10 minutos si quieres más puntos y aceptas el tamaño.
Si dht() es None, escribe igual la fila y deja el hueco, que es lo que ya hace.
Fallos típicos
BMP a 5 V.
Convertir el crudo a hPa con una fórmula copiada a medias.
DHT en el GPIO4.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 76 o 77 del BMP280, y el DHT sigue estando en sysfs, no en esta tabla.
Línea mala: sin dirección de presión. No escribas hectopascales inventados.
85 Acceso RFID más teclado y registro
Fénix
Qué hace
El RC522 se detecta por su registro de versión, a 3,3 V. El teclado añade un código. Si la versión cuadra y el código es el tuyo, el relé de un pestillo de 5–12 V da un pulso y se anota una fila. No se inventa un UID. El RST está en GPIO25: el zumbador, si lo pones, va al GPIO18. Prohibido: no uses 230 V.
Materiales
Raspberry Pi
Protoboard
1 RC522
1 teclado 4x4
1 relé
1 pestillo de 5–12 V
cables dupont
Prohibido: no uses 230 V
Cableado
RC522 a 3,3 V físico 1, GND físico 6, SCK GPIO11 físico 23, MOSI GPIO10 físico 19, MISO GPIO9 físico 21, CE0 GPIO8 físico 24, RST GPIO25 físico 22. Nunca 5 V en VCC.
Teclado: filas GPIO17 físico 11, GPIO27 físico 13, GPIO22 físico 15, GPIO16 físico 36. Columnas GPIO23 físico 16, GPIO24 físico 18, GPIO20 físico 38, GPIO21 físico 40. Relé GPIO12 físico 32. Zumbador no va al 25; si lo añades, GPIO18 físico 12.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
SPI activo. CSV local con la hora y si la versión del chip era la esperada. La clave se cambia en el archivo.
import csv, os
from datetime import datetime
from gpiozero import DigitalInputDevice, DigitalOutputDevice, OutputDevice
from time import sleep
CLAVE = "2468"
print("RC522 a 3,3 V. Prohibido: no uses 230 V. Zumbador no en GPIO25.")
rele = OutputDevice(12, active_high=False, initial_value=False)
if not os.path.exists("/dev/spidev0.0"):
raise SystemExit("Sin SPI. No registro un acceso falso.")
try:
import spidev
spi = spidev.SpiDev(); spi.open(0, 0); spi.max_speed_hz = 100000
ver = spi.xfer2([((0x37 << 1) & 0x7E) | 0x80, 0])[1]
except Exception as exc:
raise SystemExit("SPI no usable: %s. No invento UID." % exc)
print("version", hex(ver))
chip_ok = ver in (0x91, 0x92)
filas = [DigitalOutputDevice(p, initial_value=True) for p in (17, 27, 22, 16)]
columnas = [DigitalInputDevice(p, pull_up=True) for p in (23, 24, 20, 21)]
teclas = list("123A456B789C*0#D")
entrada = ""
anterior = None
try:
while True:
tecla = None
for i, f in enumerate(filas):
f.off(); sleep(0.003)
for j, c in enumerate(columnas):
if not c.value:
tecla = teclas[i*4+j]
f.on()
if tecla and tecla != anterior and tecla.isdigit():
entrada += tecla
if len(entrada) >= 4:
ok = chip_ok and entrada[-4:] == CLAVE
with open("accesos.csv", "a", newline="") as fcsv:
csv.writer(fcsv).writerow([datetime.now().isoformat(timespec="seconds"), hex(ver), ok])
if ok:
rele.on(); sleep(2); rele.off()
entrada = ""
anterior = tecla
sleep(0.02)
finally:
rele.off()
Si no es 0x91 ni 0x92, el relé no debe abrir aunque aciertes el teclado.
Si cuadra, teclea la clave y escucha el relé de baja tensión.
Mira accesos.csv.
No pongas 230 V en el pestillo.
Circuito
Variantes
Pitido corto en GPIO18 solo cuando ok es verdadero.
LED en GPIO6, físico 31.
Fallos típicos
RC522 a 5 V.
Zumbador en el GPIO25, que es el RST del lector.
230 V en el pestillo, o anotar el UID a mano en el CSV como si el lector lo hubiera leído.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /dev/spidev0.0
Línea buena: el nodo existe.
Línea mala: No such file or directory.
86 Cuadro de mando: OLED, web, sensores y relés
Fénix
Qué hace
Una página de casa muestra el aire del DHT11 si el kernel lo tiene, enciende dos relés de 5–12 V y el OLED se limpia para confirmar el bus. No abras puertos en el router. Prohibido: no uses 230 V. No hay analítica.
Materiales
Raspberry Pi
Protoboard
1 OLED
1 DHT11
1 resistencia de 10 kΩ
2 módulos de relé
cargas de 5–12 V
cables dupont
Prohibido: no uses 230 V
Cableado
OLED y DHT a 3,3 V físico 1, GND físico 6. SDA físico 3, SCL físico 5. Datos DHT GPIO26 físico 37 con pull-up.
Relés GPIO12 físico 32 y GPIO16 físico 36. Bobinas al 5 V físico 2. El RGB no se monta: el GPIO16 sería su rojo.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Servidor local de la biblioteca estándar. La temperatura solo aparece si existe el fichero del kernel.
import fcntl, glob, os
from gpiozero import OutputDevice
from http.server import BaseHTTPRequestHandler, HTTPServer
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
os.write(fd, bytes([0x00, 0xAF]))
reles = [OutputDevice(p, active_high=False, initial_value=False) for p in (12, 16)]
def aire():
for base in glob.glob("/sys/bus/iio/devices/iio:device*"):
t = os.path.join(base, "in_temp_input")
if os.path.exists(t):
return str(int(open(t).read())/1000.0)
return "sin DHT"
class H(BaseHTTPRequestHandler):
def do_GET(self):
cuerpo = ("<!DOCTYPE html><html lang=es><meta charset=utf-8><title>Mandos</title><body><p>Solo tu red de casa.</p><p>Aire %s</p><form method=post action=/r/0><button>Rele 1</button></form><form method=post action=/r/1><button>Rele 2</button></form>" % aire()).encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(cuerpo)))
self.end_headers()
self.wfile.write(cuerpo)
def do_POST(self):
if self.path.startswith("/r/"):
i = int(self.path[3:])
if 0 <= i < 2:
reles[i].toggle()
self.do_GET()
def log_message(self, formato, *a):
return
print("Prohibido: no uses 230 V.")
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
Tercer relé solo si te queda un GPIO libre que no sea el 2, el 3 ni el 26.
Fallos típicos
230 V. Prohibido: no uses 230 V.
Inventar la temperatura en la página.
Puerto abierto al router.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c del OLED.
Línea mala: Could not open file /dev/i2c-1. La web puede arrancar y la pantalla no.
87 Riego de tres macetas por turnos
Fénix
Qué hace
Tres sondas entran en AIN0, AIN1 y AIN2. Si una está más seca que su entero, su relé riega y las otras esperan: un turno cada vez, para no vaciar la fuente. Bombas o electroválvulas de 5–12 V. Prohibido: no uses 230 V. No hay porcentaje.
Materiales
Raspberry Pi
Protoboard
3 sondas capacitivas a 3,3 V
1 ADS1115
3 relés
3 bombas o válvulas de 5–12 V y fuente
cables dupont
Prohibido: no uses 230 V
Cableado
ADS a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5. Sondas a AIN0, AIN1 y AIN2, alimentadas a 3,3 V.
Relés GPIO12 físico 32, GPIO16 físico 36 y GPIO20 físico 38. Bobinas al 5 V físico 2. No montes el RGB a la vez.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Tres disparos del ADS con MUX distinto: 0xC383, 0xD383 y 0xE383, PGA ±4,096 V del datasheet. El umbral es tuyo.
import fcntl, os, time
from gpiozero import OutputDevice
I2C_SLAVE = 0x0703
SECO = 18000
CFGS = (0xC383, 0xD383, 0xE383)
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x48)
reles = [OutputDevice(p, active_high=False, initial_value=False) for p in (12, 16, 20)]
def canal(cfg):
os.write(fd, bytes([0x01, cfg >> 8, cfg & 0xFF])); time.sleep(0.02)
os.write(fd, bytes([0x00]))
hi, lo = os.read(fd, 2)
n = (hi << 8) | lo
return n - 0x10000 if n & 0x8000 else n
print("Prohibido: no uses 230 V. Un turno cada vez.")
try:
while True:
for i, cfg in enumerate(CFGS):
n = canal(cfg)
print(i, n)
if n >= SECO:
reles[i].on(); time.sleep(2); reles[i].off(); time.sleep(20)
time.sleep(30)
finally:
for r in reles:
r.off()
Mide el entero seco de cada sonda. Si no se parecen, no uses un solo SECO: separa tres constantes.
Prueba cada bomba un segundo, fuera de la maceta.
Ejecuta y confirma que nunca hay dos relés a la vez.
Ajusta los segundos al tamaño de la maceta.
Nada de 230 V.
Ctrl+C suelta los tres.
Circuito
Variantes
Tres umbrales en una tupla.
Saltar la maceta cuyo entero ya está del lado húmedo sin esperar el turno largo.
Fallos típicos
230 V. Prohibido: no uses 230 V.
Sondas a GPIO.
Dos válvulas abiertas porque olvidaste el off.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 48.
Línea mala: sin ADS no riegues a ciegas.
88 Alarma con modos casa, fuera y noche
Fénix
Qué hace
El teclado elige casa, fuera o noche. En casa el PIR no pita. Fuera, pita el PIR o la puerta. De noche, pita la puerta y el PIR solo si quieres, aquí también. Nada sale de la habitación. Cambia la lógica si tu reed cierra al revés, no el voltaje.
PIR GPIO5 físico 29, 5 V físico 2, GND físico 14. Reed GPIO6 físico 31 a GND físico 20. Zumbador GPIO25 físico 22, GND físico 39, VCC 3,3 V físico 1 si vale.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Tecla 1 casa, 2 fuera, 3 noche. gpiozero en PIR, reed y zumbador.
from gpiozero import Buzzer, DigitalInputDevice, DigitalOutputDevice, MotionSensor
from time import sleep
filas = [DigitalOutputDevice(p, initial_value=True) for p in (17, 27, 22, 16)]
columnas = [DigitalInputDevice(p, pull_up=True) for p in (23, 24, 20, 21)]
teclas = list("123A456B789C*0#D")
pir = MotionSensor(5)
reed = DigitalInputDevice(6, pull_up=True)
z = Buzzer(25)
modo = "casa"
anterior = None
try:
while True:
tecla = None
for i, f in enumerate(filas):
f.off(); sleep(0.003)
for j, c in enumerate(columnas):
if not c.value:
tecla = teclas[i*4+j]
f.on()
if tecla and tecla != anterior:
if tecla == "1":
modo = "casa"
elif tecla == "2":
modo = "fuera"
elif tecla == "3":
modo = "noche"
print(modo)
puerta = not reed.value
mover = pir.motion_detected
if modo == "fuera" and (puerta or mover):
z.on()
elif modo == "noche" and puerta:
z.on()
else:
z.off()
anterior = tecla
sleep(0.02)
finally:
z.off()
Elige 3 y abre solo la puerta: pita. El PIR de noche, en este código, no pita; es deliberado.
Vuelve a 1: calla.
Cambia el reed si tu contacto es el inverso.
No conectes esto a una sirena de 230 V.
Circuito
Variantes
En noche, pita también el PIR.
LED en GPIO19, físico 35, con un color de modo que tú acuerdes parpadeando distinto.
Fallos típicos
PIR a 5 V en el GPIO.
Zumbador pasivo.
Modo fuera que no mira la puerta.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 5
Línea buena: 5: ip pd | hi // GPIO5 = input si el PIR ve.
Línea mala: 5: op dh pn | hi // GPIO5 = output.
89 Banco de pruebas de GPIO
Fénix
Qué hace
Un LED con su resistencia de 330 Ω se mueve de pin en pin, solo entre una lista segura, para ver que ese GPIO todavía obedece. El programa se niega a tocar el GPIO2 y el GPIO3, que son I2C, y el 14 y el 15 de la serial. En la Pi 5 no uses RPi.GPIO. gpiozero sí, con el backend lgpio.
Materiales
Raspberry Pi
Protoboard
1 LED
1 resistencia de 330 Ω
2 cables dupont
Cableado
Empieza con el LED en GPIO17 físico 11 y GND físico 6, siempre con 330 Ω.
Cuando el programa lo diga, apaga la Pi o al menos para el script antes de mover el LED a la siguiente pata de la lista. Nunca lo pongas en el físico 1, 2 o 4 de alimentación.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
La lista es 17, 27, 22 y 10. El 10 solo si no tienes SPI en marcha. gpiozero con lgpio. No se importa RPi.GPIO.
from gpiozero import LED
from time import sleep
SEGUROS = (17, 27, 22, 10)
PROHIBIDOS = {2, 3, 14, 15}
for pin in SEGUROS:
if pin in PROHIBIDOS:
raise SystemExit("me niego")
print("Mueve el LED con 330 ohm al GPIO", pin, "y pulsa Enter")
input()
led = LED(pin)
for _ in range(4):
led.on(); sleep(0.2); led.off(); sleep(0.2)
led.close()
print("Hecho. En la Pi 5 no uses RPi.GPIO. gpiozero si, con el backend lgpio.")
Tres líneas de un menú local en el OLED y dos botones para moverte y elegir. No hay servidor, no hay red y no se carga nada de fuera. Sirve para dejar la Pi con una pantalla pequeña y decidir un modo sin teclado de verdad.
Materiales
Raspberry Pi
Protoboard
1 OLED SSD1306
2 pulsadores
cables dupont
Cableado
OLED a 3,3 V físico 1, GND físico 6, SDA físico 3, SCL físico 5.
Botón siguiente GPIO23 físico 16 a GND físico 9. Botón elegir GPIO24 físico 18 a GND físico 14.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
I2C activo. El menú vive en la lista del programa. Elegir solo imprime, para que tú enganches después la acción, sin red.
import fcntl, os
from gpiozero import Button
from signal import pause
I2C_SLAVE = 0x0703
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, 0x3C)
def cmd(b):
os.write(fd, bytes([0x00, b]))
for c in (0xAE,0x8D,0x14,0xAF,0x20,0x00):
cmd(c)
opciones = ["luz", "riego", "foto"]
i = 0
def pintar():
cmd(0x21); cmd(0); cmd(127); cmd(0x22); cmd(1); cmd(1)
os.write(fd, bytes([0x40]) + bytes([0x7E if n//40 == i else 0x00 for n in range(128)]))
print("kiosco", opciones[i])
def mas():
global i
i = (i + 1) % 3
pintar()
def ok():
print("elegido", opciones[i], "sin red")
Button(23, pull_up=True, bounce_time=0.05).when_pressed = mas
Button(24, pull_up=True, bounce_time=0.05).when_pressed = ok
pintar()
pause()
Ejecuta sin cable de red si quieres comprobar que no lo necesita.
Avanza con un botón.
Elige con el otro. La terminal dice la palabra.
No añadas un navegador.
Ctrl+C.
Circuito
Variantes
Las tres palabras son las de tu casa.
Un LED en GPIO17 que enciende solo si eliges luz, y nada más.
Fallos típicos
OLED a 5 V.
Creer que el kiosco necesita salir a la calle.
Botones sin pull-up y selecciones fantasma.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c.
Línea mala: sin 3c no hay kiosco. No abras un navegador a ningún sitio para sustituirlo.
91 Bitácora de cultivo con foto diaria
Fénix
Qué hace
Una vez al día guarda una foto en esta carpeta y una fila de CSV con la hora y los grados del DS18B20, si el kernel los tiene. Si la sonda falla, la fila lo dice y no rellena un número. La cámara no se sube a ningún sitio y va en la franja consultar.
Materiales
Raspberry Pi
1 cámara CSI o USB que libcamera vea
1 DS18B20
1 resistencia de 4,7 kΩ
cables dupont
La cámara no cuenta como un LED
Cableado
Sonda: datos GPIO4 físico 7, pull-up de 4,7 kΩ al 3,3 V físico 1, VDD a 3,3 V, GND físico 9.
La cámara va al conector CSI o al USB, no a un GPIO. Nada a 5 V en la sonda.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
rpicam-still y el bus 1-Wire con dtoverlay=w1-gpio. El bucle duerme hasta el día siguiente. No hace falta red ni un cron para que la ficha funcione, aunque puedas pasarlo a cron más adelante.
import csv, glob, os
from datetime import datetime
from pathlib import Path
from subprocess import run
from time import sleep
carpeta = Path("cultivo")
carpeta.mkdir(exist_ok=True)
def grados():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
return None
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
return int(texto.strip().split("t=")[-1]) / 1000.0
while True:
marca = datetime.now().strftime("%Y%m%d")
foto = carpeta / ("dia-" + marca + ".jpg")
run(["rpicam-still", "-n", "-o", str(foto)], check=True)
g = grados()
with open(carpeta / "bitacora.csv", "a", newline="") as f:
csv.writer(f).writerow([datetime.now().isoformat(timespec="seconds"), foto.name, g if g is not None else "sin sonda"])
print(foto, g)
sleep(24 * 60 * 60)
Lista la cámara y comprueba la carpeta 28- si quieres grados.
Encaja el encuadre de la maceta y no lo muevas.
Ejecuta y espera la primera foto y la primera fila.
Abre el JPEG en el aparato.
Si la columna de grados dice sin sonda, no escribas 20 a mano.
Deja el proceso el día que te interese y páralo con Ctrl+C. No subas la carpeta.
Circuito
Variantes
Intervalo de 60 s solo para probar, y vuelve a las 24 h.
Una nota de texto que lees de nota.txt y añades como cuarta columna.
Fallos típicos
raspistill en vez de rpicam-still.
Contar la cámara oficial como irrisorio.
Inventar la temperatura cuando no hay YES.
Franja de precio
Franja de precio: consultar (>10 €). Material añadido, sin contar la Pi.
Depuración
Comando: rpicam-still --list-cameras
Línea buena: Available cameras y una línea 0 : con el sensor.
Línea mala: No cameras available! o raspistill: orden no encontrada.
92 Semáforo de maqueta que reacciona al tráfico
Fénix
Qué hace
Tres LED hacen de semáforo de maqueta. Si el ultrasonidos ve un objeto cerca, el ciclo no se queda en verde: pasa a ámbar y a rojo, como si llegara un coche. El eco lleva divisor. La distancia en metros la da gpiozero, no una fórmula de esta ficha.
Materiales
Raspberry Pi
Protoboard
3 LED
3 resistencias de 330 Ω
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
cables dupont
Cableado
Rojo GPIO17 físico 11, ámbar GPIO27 físico 13, verde GPIO22 físico 15, cada uno con 330 Ω, cátodos a GND físico 6.
HC-SR04: 5 V físico 2, GND físico 14, TRIG GPIO5 físico 29, ECHO con 1 kΩ al GPIO6 físico 31 y 2 kΩ de ahí a GND físico 20. Nada de 5 V en un GPIO.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DistanceSensor y tres LED. Cerca de 30 cm se considera tráfico. Es un umbral de maqueta.
from gpiozero import DistanceSensor, LED
from time import sleep
rojo, ambar, verde = LED(17), LED(27), LED(22)
dist = DistanceSensor(echo=6, trigger=5, max_distance=1.5)
def solo(luz, segundos):
rojo.off(); ambar.off(); verde.off(); luz.on(); sleep(segundos)
try:
while True:
if dist.distance < 0.3:
solo(ambar, 0.6)
solo(rojo, 3)
else:
solo(verde, 0.4)
finally:
rojo.close(); ambar.close(); verde.close(); dist.close()
Ejecuta con la calle de la maqueta vacía: debe predominar el verde.
Pon un coche de juguete delante del sensor: ámbar y luego rojo.
Retíralo: vuelve el verde.
Si nunca pasa a rojo, imprime la distancia y baja el 0,3 a la medida real.
No alimentes el módulo a 3,3 V esperando el alcance de la hoja.
Circuito
Variantes
Rojo más largo, 6 s, en un cruce lento.
Un cuarto LED en GPIO16 para el peatón, solo si no lo usa otra cosa.
Fallos típicos
ECHO sin las dos resistencias.
TRIG en el físico 31.
LED sin 330 Ω.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 5
Línea buena: 5: op -- pn | hi // GPIO5 = output, el TRIG.
Línea mala: 5: ip pd | lo // GPIO5 = input, o el eco directo sin divisor.
93 Torre I2C: OLED, RTC, presión y luz
Fénix
Qué hace
En el mismo bus I2C conviven el OLED, el DS3231 y el BMP280. La luz no va por I2C: es una LDR con condensador en el GPIO27, al estilo de gpiozero, no un luxómetro. La presión se imprime en crudo. La hora, en BCD. Si el RTC está en 12 h, se para.
Materiales
Raspberry Pi
Protoboard
1 OLED SSD1306
1 DS3231
1 BMP280
1 LDR
1 condensador de 1 µF
cables dupont
Cableado
SDA GPIO2 físico 3 y SCL GPIO3 físico 5, compartidos. Los tres módulos a 3,3 V físico 1 y GND físico 6. Nada a 5 V.
LDR entre el 3,3 V y el GPIO27 físico 13. Condensador de 1 µF del GPIO27 a GND físico 9. No uses el 2 ni el 3 como LED.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
I2C activo. El BMP se reconoce por el registro 0xD0. No se compensan los enteros. La LDR es LightSensor.
import fcntl, os, time
from gpiozero import LightSensor
I2C_SLAVE = 0x0703
ldr = LightSensor(27)
def abrir(addr):
fd = os.open("/dev/i2c-1", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, addr)
return fd
def bcd(v):
return ((v >> 4) & 0x0F) * 10 + (v & 0x0F)
oled = abrir(0x3C)
os.write(oled, bytes([0x00, 0xAF]))
rtc = abrir(0x68)
bmp = None
for addr in (0x76, 0x77):
try:
fd = abrir(addr)
os.write(fd, bytes([0xD0]))
if os.read(fd, 1)[0] in (0x58, 0x60):
bmp = fd
break
os.close(fd)
except OSError:
pass
if bmp is None:
raise SystemExit("Sin BMP280. No invento presion.")
while True:
os.write(rtc, bytes([0x00]))
raw = os.read(rtc, 3)
if raw[2] & 0x40:
raise SystemExit("RTC en 12 h.")
os.write(bmp, bytes([0xF4, 0x25])); time.sleep(0.02)
os.write(bmp, bytes([0xF7]))
d = os.read(bmp, 6)
pres = (d[0] << 12) | (d[1] << 4) | (d[2] >> 4)
print("hora", "%02d:%02d:%02d" % (bcd(raw[2] & 0x3F), bcd(raw[1] & 0x7F), bcd(raw[0] & 0x7F)), "luz", round(ldr.value, 2), "presion cruda", pres)
time.sleep(2)
i2cdetect tiene que mostrar los tres chips antes de lanzar el bucle.
La LDR no aparecerá ahí. Comprueba el condensador.
Ejecuta. Hora, un valor de luz entre 0 y 1, y un entero de presión.
Tapa la LDR: el valor de luz baja. El entero de presión no es un hPa.
Sopla hacia el BMP: el crudo puede moverse. Sigue siendo crudo.
No alimentes ningún módulo de la torre a 5 V.
Circuito
Variantes
Si falta el OLED, quita su abrir() y deja hora y presión en la terminal.
No imprimas la luz si solo quieres la torre I2C estricta, pero entonces esta ficha se queda sin la palabra luz del título.
Fallos típicos
Pull-up del bus a 5 V.
Creer que la LDR está en el 0x23. No lo está.
Convertir el crudo del BMP a hectopascales con una fórmula de esta ficha.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: i2cdetect -y 1
Línea buena: 3c del OLED y 68 del RTC, y 76 o 77 del BMP, en la misma cuadrícula.
Línea mala: Could not open file /dev/i2c-1, o falta una de esas direcciones. La LDR no sale en i2cdetect: no está en el bus.
94 Avisos con acuse de recibo
Fénix
Qué hace
Si el PIR ve movimiento, el LED y el zumbador siguen hasta que pulsas el acuse. Entonces se callan y el CSV guarda la hora del aviso y la hora en que lo confirmaste. Sin acuse no se da el aviso por cerrado.
Materiales
Raspberry Pi
Protoboard
1 PIR con OUT a 3,3 V
1 LED
1 resistencia de 330 Ω
1 zumbador activo
1 pulsador
cables dupont
Cableado
PIR: 5 V físico 2, GND físico 14, OUT GPIO5 físico 29 solo si no pasa de 3,3 V.
LED GPIO17 físico 11 con 330 Ω a GND físico 6. Zumbador activo GPIO25 físico 22, GND físico 39, VCC 3,3 V físico 1 si el módulo lo admite. Acuse: GPIO24 físico 18 a GND físico 9.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
gpiozero y csv. Un aviso abierto no se duplica en el archivo hasta que lo acusas y vuelve a haber calma.
import csv
from datetime import datetime
from gpiozero import Buzzer, Button, LED, MotionSensor
from time import sleep
led = LED(17)
z = Buzzer(25)
pir = MotionSensor(5)
acuse = Button(24, pull_up=True, bounce_time=0.05)
abierto = False
inicio = None
try:
while True:
if pir.motion_detected and not abierto:
abierto = True
inicio = datetime.now().isoformat(timespec="seconds")
led.on(); z.beep(on_time=0.15, off_time=0.4)
print("aviso", inicio)
if abierto and acuse.is_pressed:
z.off(); led.off()
with open("acuses.csv", "a", newline="") as f:
csv.writer(f).writerow([inicio, datetime.now().isoformat(timespec="seconds")])
print("acusado")
abierto = False
sleep(0.3)
sleep(0.05)
finally:
z.off(); led.close()
Tres LED clasifican lo que hay delante: cerca, medio o lejos, con los metros que devuelve gpiozero. No es un modelo que aprenda. Los cortes los escribes tú en metros de maqueta. El eco no entra directo.
Materiales
Raspberry Pi
Protoboard
1 HC-SR04
1 resistencia de 1 kΩ
1 resistencia de 2 kΩ
3 LED
3 resistencias de 330 Ω
cables dupont
Cableado
TRIG GPIO5 físico 29, ECHO con 1 kΩ al GPIO6 físico 31 y 2 kΩ a GND físico 20. VCC del sensor al 5 V físico 2, GND físico 14.
LED cerca GPIO17 físico 11, medio GPIO27 físico 13, lejos GPIO22 físico 15, cada uno con 330 Ω, cátodos a GND físico 6.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
DistanceSensor. Por debajo de 0,2 m es cerca; hasta 0,6 m es medio; más lejos, el tercer LED. max_distance evita esperas infinitas.
from gpiozero import DistanceSensor, LED
from time import sleep
cerca, medio, lejos = LED(17), LED(27), LED(22)
sensor = DistanceSensor(echo=6, trigger=5, max_distance=1.2)
try:
while True:
d = sensor.distance
cerca.off(); medio.off(); lejos.off()
if d < 0.2:
cerca.on(); clase = "cerca"
elif d < 0.6:
medio.on(); clase = "medio"
else:
lejos.on(); clase = "lejos"
print(round(d, 2), clase)
sleep(0.2)
finally:
cerca.close(); medio.close(); lejos.close(); sensor.close()
Si el salto no cae donde quieres, cambia 0,2 y 0,6, que son tuyos.
No conviertas esos metros en otra unidad con un factor inventado: ya vienen de la librería.
Un solo LED cada vez.
Ctrl+C.
Circuito
Variantes
Imprime solo cuando la clase cambia.
Cuarto caso, muy lejos, si d está en el máximo, con el LED de lejos parpadeando.
Fallos típicos
ECHO directo al GPIO6.
Dos LED encendidos porque olvidaste el off.
Llamar a esto inteligencia artificial.
Franja de precio
Franja de precio: poco (≈1–3 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 6
Línea buena: 6: ip pd | hi // GPIO6 = input, eco ya dividido.
Línea mala: 6: op dh pn | hi // GPIO6 = output, o ECHO cableado sin las resistencias.
96 Control de acuario o terrario
Fénix
Qué hace
Pensado como terrario o como acuario de baja tensión: la sonda estanca da los grados del kernel y un relé puede mover un ventilador o una bomba de 5–12 V. Una LDR enciende el LED de día. No conmuto un calentador de red. Prohibido: no uses 230 V. La unión de los cables de la sonda no va al agua si no está aislada.
Materiales
Raspberry Pi
Protoboard
1 DS18B20 impermeable
1 resistencia de 4,7 kΩ
1 LDR
1 condensador de 1 µF
1 LED
1 resistencia de 330 Ω
1 relé
1 ventilador o bomba de 5–12 V y su fuente
cables dupont
Prohibido: no uses 230 V
Cableado
Sonda en GPIO4 físico 7, pull-up 4,7 kΩ al 3,3 V físico 1, GND físico 9. LDR entre 3,3 V y GPIO27 físico 13, condensador a GND físico 20.
LED de día GPIO17 físico 11 con 330 Ω a GND físico 6. Relé GPIO12 físico 32, bobina al 5 V físico 2, GND físico 14. La carga del relé es solo de 5–12 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Overlay w1-gpio. El relé usa histéresis corta para no tabletear. El día es LightSensor, no un luxómetro.
import glob, os, time
from gpiozero import LED, LightSensor, OutputDevice
ALTA, BAJA = 28.0, 26.5
led = LED(17)
ldr = LightSensor(27)
rele = OutputDevice(12, active_high=False, initial_value=False)
on = False
def grados():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
if not rutas:
raise SystemExit("Sin sonda. No invento grados ni enciendo el rele.")
texto = open(rutas[0]).read()
if "YES" not in texto:
return None
return int(texto.strip().split("t=")[-1]) / 1000.0
print("Carga de 5 a 12 V. Prohibido: no uses 230 V.")
try:
while True:
led.value = ldr.value < 0.35
g = grados()
if g is not None:
if not on and g >= ALTA:
on = True
elif on and g <= BAJA:
on = False
rele.value = on
print(g, "rele" if on else "quieto", "luz" if led.value else "dia")
time.sleep(2)
finally:
rele.off(); led.close()
Aísla la unión de la sonda antes de acercarla al agua. La cápsula puede mojarse; el empalme, no, si está desnudo.
No conectes un calentador de acuario de red al relé.
Ejecuta y lee los grados del kernel.
Tapa la LDR: el LED de apoyo enciende. Eso es noche de maqueta, no un ciclo solar medido.
Calienta la sonda por encima de ALTA solo con la mano, con el ventilador de 5–12 V.
Al bajar de BAJA, el relé abre.
Circuito
Variantes
No enciendas el relé de noche, si led.value está activo.
Umbrales más bajos para un terrario fresco.
Fallos típicos
230 V en el relé. Prohibido: no uses 230 V.
Sonda en modo parásito sin el tercer hilo y sin la pull-up fuerte que eso exigiría. Aquí van tres hilos.
Usar el LED de 330 Ω como si alumbrara el acuario entero.
Franja de precio
Franja de precio: bastante (≈6–9 €). Material añadido, sin contar la Pi.
Depuración
Comando: ls /sys/bus/w1/devices
Línea buena: una carpeta 28- de la sonda estanca.
Línea mala: solo w1_bus_master1.
97 Comedero con horario
Fénix
Qué hace
Un script mueve un solo SG90 de abierto a cerrado, una vez, y termina. La hora la pone cron en esta Pi, con una línea que tú escribes en tu crontab. No hay servicio en la nube. Si además tienes el RTC de otra ficha, la hora del sistema puede sostenerse sin red; esta ficha no lee el chip, dispara el servo.
Materiales
Raspberry Pi
Protoboard
1 SG90
cables dupont
Un comedero de maqueta que el servo pueda empujar
Cableado
Señal GPIO13 físico 33. Rojo del servo al 5 V físico 2, un solo SG90. Negro o marrón a GND físico 6.
Nada de la señal al pin de 5 V. No cuelgues el comedero de un servo grande alimentado por la Pi.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
El programa es corto a propósito: cron lo lanza y se acaba. La línea de ejemplo es 0 8 * * * y la ruta de tu archivo. La editas con crontab -e en el aparato.
from gpiozero import Servo
from time import sleep
s = Servo(13)
try:
s.max()
sleep(0.8)
s.min()
sleep(0.8)
finally:
s.detach()
s.close()
Prueba el script a mano: el servo abre y cierra una vez.
crontab -e y una línea con la hora, el minuto y python3 más la ruta del archivo. Ejemplo de minuto y hora: 30 7.
Guarda y sal. No hace falta reiniciar.
Espera a esa hora o cambia el minuto para una prueba.
Mira el syslog o simplemente el comedero.
Quita la línea cuando ya no quieras disparos.
Circuito
Variantes
Dos horas: dos líneas de crontab, el mismo script.
Un botón en GPIO23 que llama al mismo movimiento sin esperar a cron.
Fallos típicos
Dejar el servo con tensión todo el día porque el script no hace detach.
Varios servos en el 5 V de la Pi.
Un cron de otro usuario que no tiene permiso sobre el GPIO.
Franja de precio
Franja de precio: medio (≈3–6 €). Material añadido, sin contar la Pi.
Depuración
Comando: pinctrl get 13
Línea buena: 13: a0 pn | hi // GPIO13 en PWM mientras el servo se mueve.
Línea mala: 13: ip pd | lo // GPIO13 = input. cron puede estar bien y la señal no salir.
98 Maletín de los montajes de Enseñanza
Fénix
Qué hace
El maletín guarda las piezas de las prácticas de Enseñanza y nada más: LED con resistencia, la misma salida encendida desde Python, pulsador y rebote, PWM con brillo y servo, sensor de temperatura, I2C con varios sensores, foto en el aparato, y un sensor que decide y avisa en local. No abre otro curso. Como cierre, no como montaje del maletín, Enseñanza habla del arranque al encender y de la copia de la tarjeta. Esas dos no se cablean aquí.
Materiales
Caja o maletín
Protoboard y cables dupont
LED y resistencias de 330 Ω
1 pulsador
1 SG90
1 DS18B20 y una resistencia de 4,7 kΩ
1 OLED u otro módulo I2C de las prácticas, a 3,3 V
1 cámara si vas a hacer la práctica de foto
La cámara y el conjunto pasan de 10 €: franja consultar. La Pi no entra en la cuenta
Cableado
No los montes todos a la vez. LED de práctica: GPIO17 físico 11, 330 Ω, GND físico 6. Pulsador: GPIO23 físico 16 a GND físico 9. Servo: señal GPIO13 físico 33, 5 V físico 2 solo para un SG90, GND físico 6.
Temperatura: GPIO4 físico 7 con 4,7 kΩ al 3,3 V físico 1. I2C: SDA físico 3 y SCL físico 5, módulos a 3,3 V. La cámara, al CSI o al USB, no al header. Si una práctica usa el físico 11, quita el servo de en medio antes de cambiar de ficha.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
Cada práctica se hace con el código de su lección, no con un programa nuevo de este maletín. gpiozero es el hilo común de las salidas. Este script solo enciende el LED de comprobación de que el maletín está vivo y recuerda que no estás en otro curso.
from gpiozero import LED
from time import sleep
led = LED(17)
print("Maletin de Enseñanza. No es otro curso.")
print("Montajes: LED, salida en Python, pulsador, PWM y servo, temperatura, I2C, foto, aviso local.")
print("Cierre, no montaje: arranque al encender, y copia de la tarjeta.")
try:
for _ in range(3):
led.on(); sleep(0.2); led.off(); sleep(0.2)
finally:
led.close()
Lee el estado real de una lista de GPIO del header y lo imprime. No enciende nada para adornar el panel. Si lgpio no está o no encuentra el chip de la cabecera, se para y lo dice: no rellena una tabla de ceros. En la Pi 5 el chip del header no es el mismo número que en una Pi 4; el script prueba, no adivina.
Materiales
Raspberry Pi
Protoboard vacía o solo con lo que ya tengas puesto y quieras observar
cables solo si vas a leer un botón que ya está cableado
No añadas una Pi nueva a la cuenta: la franja es el material de más, y aquí no hace falta
Cableado
No pongas salidas en este experimento. Si hay un botón, que sea el de siempre: GPIO23 físico 16 a GND físico 9, pull-up.
No leas como entrada un pin que otro programa está usando de salida. No toques el físico 2 ni el 4. GPIO2 y GPIO3 se quedan fuera de la lista porque son I2C.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
lgpio, el mismo backend que gpiozero usa por debajo. Se reclama el pin como entrada un instante, se lee y se suelta. Si el import falla, no hay estado falso.
PINS = (17, 27, 22, 23, 24, 25)
try:
import lgpio
except ImportError:
raise SystemExit("No esta lgpio. No invento el estado de los GPIO.")
def abrir():
for n in (4, 0, 10):
try:
h = lgpio.gpiochip_open(n)
except Exception:
continue
try:
lgpio.gpio_claim_input(h, 17, lgpio.SET_PULL_UP)
lgpio.gpio_free(h, 17)
return h
except Exception:
lgpio.gpiochip_close(h)
raise SystemExit("No encuentro el gpiochip del header. No invento niveles.")
h = abrir()
try:
for pin in PINS:
lgpio.gpio_claim_input(h, pin, lgpio.SET_PULL_UP)
print("GPIO", pin, "nivel", lgpio.gpio_read(h, pin))
lgpio.gpio_free(h, pin)
finally:
lgpio.gpiochip_close(h)
Instala el paquete de la distro si falta: python3-lgpio. No hace falta red si ya está en la imagen.
Ejecuta con la mesa quieta.
Anota el nivel del GPIO23.
Cierra un botón a masa en ese pin, si lo tienes, y ejecuta otra vez: el nivel tiene que cambiar.
Si el script sale diciendo que no hay chip, no completes la tabla a mano.
No reclames el GPIO2.
Circuito
Variantes
Añade el 12 y el 13 solo si no hay un relé o un servo gobernados por otro proceso.
Imprime también el nombre del chip que se abrió, guardando el número del bucle.
Fallos típicos
Dar por bueno un 0 escrito en el código cuando lgpio no importó.
Leer un pin en salida de otro programa y creer que el panel manda.
En la Pi 5 forzar gpiochip0 porque así era en la Pi 4.
Franja de precio
Franja de precio: irrisorio (<1 €). Material añadido, sin contar la Pi.
Depuración
Comando: gpioinfo
Línea buena: una línea del GPIO17 con used o con la función que de verdad tiene, no un texto que te hayas inventado.
Línea mala: gpioinfo: command not found, o un chip que no es el del header y por eso el GPIO17 no existe ahí.
100 Proyecto final: informe offline de un sistema propio
Fénix
Qué hace
Reúne en un HTML local lo que esta Pi puede decir de verdad: temperatura de la CPU, espacio libre en disco y, si hay un DS18B20, sus grados de kernel. Lo que no está, se escribe como ausente. No abre la red, no llama a ninguna analítica y no inventa el sistema que todavía no has construido. Los sensores de las fichas anteriores no se vuelven a comprar para esta: la franja es irrisorio porque el informe no añade un aparato.
Materiales
Raspberry Pi
El sistema que ya hayas montado en otras fichas, si quieres citarlo en el texto
Opcional: un DS18B20 ya cableado como en su ficha, con 4,7 kΩ
No compres otra cámara ni otra Pi para el informe
Cableado
Si no hay sonda, no cablees nada: el informe sale igual.
Si la hay: datos GPIO4 físico 7, pull-up 4,7 kΩ al 3,3 V físico 1, GND físico 9, como en la ficha del DS18B20. No estrenes pines nuevos. Cada GPIO sigue siendo de 3,3 V.
El header de 40 es el mismo en Pi 4 y Pi 5.
Software
cpu de gpiozero, disco de shutil, sonda de sysfs. El HTML se escribe con marcas escapadas en un archivo de esta carpeta. Un CSV hermano guarda la misma fila, por si quieres la tabla pelada.
import csv, glob, html, os, shutil
from datetime import datetime
from gpiozero import CPUTemperature
def sonda():
rutas = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
datos = []
for ruta in rutas:
ident = os.path.basename(os.path.dirname(ruta))
texto = open(ruta).read()
if "YES" not in texto:
datos.append((ident, "CRC malo, no uso la cifra"))
else:
datos.append((ident, str(int(texto.strip().split("t=")[-1]) / 1000.0)))
return datos
cpu = round(CPUTemperature().temperature, 1)
libre = shutil.disk_usage(".").free
sondas = sonda()
ahora = datetime.now().isoformat(timespec="seconds")
with open("informe.csv", "a", newline="") as f:
csv.writer(f).writerow([ahora, cpu, libre, sondas])
partes = ["<!DOCTYPE html><html lang=es><meta charset=utf-8><title>Informe</title><body>",
"<h1>Informe offline</h1>",
"<p>Sin red. Sin analitica. %s</p>" % html.escape(ahora),
"<p>CPU %s C. Disco libre %s bytes.</p>" % (cpu, libre)]
if not sondas:
partes.append("<p>DS18B20 ausente. No invento grados.</p>")
else:
for ident, g in sondas:
partes.append("<p>%s: %s</p>" % (html.escape(ident), html.escape(g)))
partes.append("<p>Completa a mano que fichas usaste. Este archivo no las adivina.</p></body></html>")
open("informe-offline.html", "w").write("".join(partes))
print("escrito informe-offline.html")