from machine import Pin, ADC, PWM, I2C
from time import sleep, ticks_ms
# ==================================================
# SIMULACION DIDACTICA DE SOLAR TRACKER
# MicroPython + ESP32 + Wokwi
# ==================================================
# En esta version:
# FOCO_H simula la posicion horizontal del foco/sol
# FOCO_V simula la posicion vertical del foco/sol
# RADIACION simula la intensidad solar disponible
foco_h = ADC(Pin(32)) # Posicion horizontal del foco
foco_v = ADC(Pin(33)) # Posicion vertical del foco
radiacion = ADC(Pin(36)) # Intensidad solar simulada
# Servomotores
servo_vertical = PWM(Pin(18), freq=50)
servo_horizontal = PWM(Pin(19), freq=50)
# LED que representa el foco/sol
led_foco = Pin(2, Pin.OUT)
# Pantalla LCD I2C
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
# Configuracion ADC
for sensor in [foco_h, foco_v, radiacion]:
sensor.atten(ADC.ATTN_11DB)
sensor.width(ADC.WIDTH_12BIT)
# ==================================================
# CLASE LCD I2C
# ==================================================
class LCD_I2C:
def __init__(self, i2c, addr=0x27):
self.i2c = i2c
self.addr = addr
self.backlight = 0x08
self.init_lcd()
def write_byte(self, data):
self.i2c.writeto(self.addr, bytes([data | self.backlight]))
def pulse_enable(self, data):
self.write_byte(data | 0x04)
sleep(0.001)
self.write_byte(data & ~0x04)
sleep(0.001)
def send_nibble(self, data):
self.write_byte(data)
self.pulse_enable(data)
def send_byte(self, data, mode=0):
high = mode | (data & 0xF0)
low = mode | ((data << 4) & 0xF0)
self.send_nibble(high)
self.send_nibble(low)
def command(self, cmd):
self.send_byte(cmd, 0)
def write_char(self, char):
self.send_byte(ord(char), 1)
def init_lcd(self):
sleep(0.05)
self.send_nibble(0x30)
sleep(0.005)
self.send_nibble(0x30)
sleep(0.001)
self.send_nibble(0x30)
self.send_nibble(0x20)
self.command(0x28)
self.command(0x0C)
self.command(0x06)
self.clear()
def clear(self):
self.command(0x01)
sleep(0.002)
def move_to(self, col, row):
row_offsets = [0x00, 0x40]
self.command(0x80 | (col + row_offsets[row]))
def putstr(self, text):
for char in text:
self.write_char(char)
lcd = LCD_I2C(i2c, 0x27)
# ==================================================
# VARIABLES DEL TRACKER
# ==================================================
LIGHT_THRESHOLD = 150
pos_vertical = 90
pos_horizontal = 90
UPPER_LIMIT_POS = 160
LOWER_LIMIT_POS = 20
last_time = ticks_ms()
lcd_interval = 1500
pantalla_estado = 0
# ==================================================
# FUNCIONES
# ==================================================
def map_value(x, in_min, in_max, out_min, out_max):
return int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
def set_servo_angle(servo, angle):
angle = max(0, min(180, angle))
min_us = 500
max_us = 2500
pulse_us = map_value(angle, 0, 180, min_us, max_us)
servo.duty_ns(pulse_us * 1000)
def calcular_ldr_virtuales(valor_h, valor_v):
# valor_h bajo = foco hacia la izquierda
# valor_h alto = foco hacia la derecha
# valor_v bajo = foco hacia abajo
# valor_v alto = foco hacia arriba
luz_izquierda = 4095 - valor_h
luz_derecha = valor_h
luz_abajo = 4095 - valor_v
luz_arriba = valor_v
ldr_tl = (luz_izquierda + luz_arriba) // 2
ldr_tr = (luz_derecha + luz_arriba) // 2
ldr_bl = (luz_izquierda + luz_abajo) // 2
ldr_br = (luz_derecha + luz_abajo) // 2
return ldr_tl, ldr_tr, ldr_bl, ldr_br
def mover_tracker(avg_top, avg_bottom, avg_left, avg_right):
global pos_vertical, pos_horizontal
direccion_vertical = "Centro"
direccion_horizontal = "Centro"
# Movimiento vertical
if (avg_top - avg_bottom) > LIGHT_THRESHOLD and pos_vertical < UPPER_LIMIT_POS:
pos_vertical += 1
set_servo_angle(servo_vertical, pos_vertical)
direccion_vertical = "Arriba"
elif (avg_bottom - avg_top) > LIGHT_THRESHOLD and pos_vertical > LOWER_LIMIT_POS:
pos_vertical -= 1
set_servo_angle(servo_vertical, pos_vertical)
direccion_vertical = "Abajo"
# Movimiento horizontal
if (avg_left - avg_right) > LIGHT_THRESHOLD and pos_horizontal < UPPER_LIMIT_POS:
pos_horizontal += 1
set_servo_angle(servo_horizontal, pos_horizontal)
direccion_horizontal = "Izquierda"
elif (avg_right - avg_left) > LIGHT_THRESHOLD and pos_horizontal > LOWER_LIMIT_POS:
pos_horizontal -= 1
set_servo_angle(servo_horizontal, pos_horizontal)
direccion_horizontal = "Derecha"
return direccion_vertical, direccion_horizontal
def calcular_corriente(valor_radiacion, valor_h, valor_v):
# Radiacion maxima simulada entre 0 y 1 A
corriente_maxima = valor_radiacion / 4095
# Convertimos posicion de foco a angulo esperado del tracker
objetivo_h = map_value(valor_h, 0, 4095, LOWER_LIMIT_POS, UPPER_LIMIT_POS)
objetivo_v = map_value(valor_v, 0, 4095, LOWER_LIMIT_POS, UPPER_LIMIT_POS)
error_h = abs(objetivo_h - pos_horizontal)
error_v = abs(objetivo_v - pos_vertical)
error_total = error_h + error_v
# Mientras mas cerca esta el panel del foco, mayor corriente
factor_alineacion = 1 - (error_total / 280)
if factor_alineacion < 0:
factor_alineacion = 0
corriente = corriente_maxima * factor_alineacion
return corriente, factor_alineacion
# ==================================================
# INICIO
# ==================================================
set_servo_angle(servo_vertical, pos_vertical)
set_servo_angle(servo_horizontal, pos_horizontal)
led_foco.on()
lcd.clear()
lcd.move_to(0, 0)
lcd.putstr("Solar Tracker")
lcd.move_to(0, 1)
lcd.putstr("Version 2")
sleep(2)
# ==================================================
# BUCLE PRINCIPAL
# ==================================================
while True:
# Lectura de posicion del foco
valor_h = foco_h.read()
valor_v = foco_v.read()
valor_r = radiacion.read()
# Calculo de LDRs virtuales
tl, tr, bl, br = calcular_ldr_virtuales(valor_h, valor_v)
# Promedios de luz
avg_top = (tl + tr) // 2
avg_bottom = (bl + br) // 2
avg_left = (tl + bl) // 2
avg_right = (tr + br) // 2
# Movimiento del tracker
dir_v, dir_h = mover_tracker(avg_top, avg_bottom, avg_left, avg_right)
# Calculo de corriente simulada
corriente, alineacion = calcular_corriente(valor_r, valor_h, valor_v)
# Mostrar informacion cada cierto tiempo
if ticks_ms() - last_time > lcd_interval:
last_time = ticks_ms()
pantalla_estado = (pantalla_estado + 1) % 3
lcd.clear()
if pantalla_estado == 0:
lcd.move_to(0, 0)
lcd.putstr("Corriente:")
lcd.move_to(0, 1)
lcd.putstr("{:.2f} A".format(corriente))
elif pantalla_estado == 1:
lcd.move_to(0, 0)
lcd.putstr("Dir:")
lcd.move_to(5, 0)
lcd.putstr(dir_h[:10])
lcd.move_to(0, 1)
lcd.putstr(dir_v[:16])
else:
lcd.move_to(0, 0)
lcd.putstr("Servo H:{} ".format(pos_horizontal))
lcd.move_to(0, 1)
lcd.putstr("Servo V:{} ".format(pos_vertical))
print("====== DATOS DE SIMULACION ======")
print("FOCO_H:", valor_h, "FOCO_V:", valor_v, "RADIACION:", valor_r)
print("LDR TL:", tl, "TR:", tr, "BL:", bl, "BR:", br)
print("Promedio arriba:", avg_top, "abajo:", avg_bottom)
print("Promedio izquierda:", avg_left, "derecha:", avg_right)
print("Movimiento horizontal:", dir_h)
print("Movimiento vertical:", dir_v)
print("Servo horizontal:", pos_horizontal)
print("Servo vertical:", pos_vertical)
print("Alineacion:", "{:.2f}".format(alineacion))
print("Corriente simulada:", "{:.2f} A".format(corriente))
print("=================================")
sleep(0.03)