Zimmy · Tecnología · Madrid
Construimos tecnología que protege, entiende y automatiza.
En Zimmy diseñamos y construimos nuestros propios productos. Trabajamos en seguridad digital, en análisis de datos y en electrónica para casas y ciudades. Aquí contamos lo que hacemos y lo que vamos aprendiendo, sin tecnicismos.
A qué nos dedicamos
De la idea a algo que funcione
Nos movemos entre el software, los datos y la electrónica. Partimos siempre de un problema concreto y seguimos hasta tener algo que se pueda usar.
- Entender el problema con datos reales
Antes de construir nada, miramos qué está pasando de verdad y dónde está el fallo.
- Prototipar rápido
Primero, una versión sencilla que funcione entera. Así aprendemos rápido y corregimos a tiempo.
- Saber cuándo se equivoca
Un sistema es fiable cuando sabemos en qué casos falla y cuánto.
- Publicar lo aprendido
Compartimos guías y notas para que lo que sirve llegue a más gente.
Seguridad digital
Herramientas para detectar mensajes y enlaces fraudulentos, y guías para que cualquiera sepa protegerse.
ObexisBuzón de reportesDatos e inteligencia artificial
Preparamos y analizamos datos, y entrenamos modelos que ayudan a decidir con fundamento.
Zynthra-AIRENAsistente RENElectrónica e IoT
Sensores, cámaras y microcontroladores conectados para automatizar casas, calles y vehículos.
AureaTechCoche con ESP32Proyectos
Lo que estamos construyendo
Cada proyecto tiene su página, con demos, planos y código. También los encuentras en el menú «Proyectos».
Producto destacado · seguridad
Obexis
Te avisa de si un enlace es una estafa antes de que lo abras, aunque nadie lo haya denunciado todavía.
Buzón de reportes
PublicadoEnvía los SMS y códigos QR sospechosos que recibas. Se anonimizan en tu propio navegador y ayudan a reunir la primera colección de estafas por SMS en español.
Ver proyecto →AureaTech
TerminadoUna zona residencial en maqueta: farolas que se encienden a tu paso, sensores de aire y una barrera que lee matrículas.
Ver proyecto →Coche con ESP32
FuncionandoSe maneja por Bluetooth o con un mando M5Stack por MQTT, y frena solo antes de chocar.
Ver proyecto →Zynthra-AI
Código abiertoUn asistente de estudio que funciona sin conexión y cita sus fuentes. Si intentas copiar, te ayuda a entenderlo en vez de darte la respuesta.
Ver proyecto →REN
IT Seminar · SuizaResponde a partir de tus documentos, fotos y audios. Busca primero en lo tuyo, consulta la web si hace falta y, si no sabe algo, lo reconoce.
Ver proyecto →Gestor de pedidos
TerminadoAplicación de escritorio en Java con MVC, cambio de divisa en tiempo real, 48 pruebas e integración continua.
Ver proyecto →App de gastos
En diseñoRegistra los gastos de las tarjetas, lee tickets con la cámara y hace el cierre de cada mes sola.
Ver proyecto →Proyectos / Producto destacado · seguridad
Obexis
Un proyecto para que sepas si un enlace es una estafa antes de abrirlo, y te lo explique en lenguaje sencillo.
En desarrolloCada punto es un enlace. Los de phishing se quedan enganchados en el filtro. Pasa el ratón por encima.
La idea
Mirar el enlace antes de morder
Obexis se pone entre tú y el enlace que te ha llegado. Lo revisa antes de que lo abras y te dice, en lenguaje sencillo, si puedes entrar, si conviene tener cuidado o si es mejor no hacerlo. Toca cada paso para verlo.
Por qué
Las estafas ya no tienen faltas de ortografía
Los mensajes falsos copian a la perfección a bancos, empresas de mensajería y organismos públicos, y las webs a las que llevan cambian cada pocos días. Las listas negras siempre llegan tarde. Obexis quiere reconocer la trampa aunque sea la primera vez que alguien la ve.
Antes de entrar
El aviso llega antes de abrir la web, cuando todavía no has escrito nada.
Sin tecnicismos
Te explica el motivo con palabras normales, para que aprendas a detectarlo tú también.
Tu privacidad primero
No necesita leer tus mensajes ni saber quién eres: solo mira el enlace.
Así se ve el problema
Seguro que alguno te suena
Estado
En construcción
Obexis sigue en desarrollo y contaremos más a medida que avance. Mientras tanto, puedes ayudar enviando los mensajes sospechosos que recibas.
Proyectos/Seguridad
Buzón de reportes
Publicado · parte de ObexisUna web para que cualquiera envíe un SMS, un mensaje o un código QR sospechoso. Con esos reportes se construye el primer dataset de smishing en español, justo lo que le falta al detector.
- Qué recoge
- SMS, WhatsApp, correos, redes y QR
- Privacidad
- Se anonimiza en tu navegador
- Tecnología
- Cloudflare Pages, Functions, D1 y R2
- Coste
- 0 € con los planes gratuitos
Por qué hace falta
No existe un dataset público de SMS fraudulentos en español
Los datos disponibles son de correo o están en inglés. Un detector entrenado con ellos acierta peor con los mensajes que llegan aquí, y el test lo confirma.
Cómo funciona
De tu móvil al dataset, sin datos personales
- 1Pegas el mensaje o subes la foto. Si es un QR o una captura, se lee en tu navegador (jsQR y Tesseract) y solo se usa el enlace o el texto.
- 2Se anonimiza antes de salir. Teléfonos, correos, DNI, IBAN, tarjetas, códigos y nombres se cambian por etiquetas.
- 3El servidor lo clasifica: útil, a revisar o basura. Después se revisa a mano en un panel privado.
- 4Lo borras cuando quieras. Sin cuenta: recibes un código de borrado.
Es el mismo código que usa el buzón. No se envía nada.
Qué se guarda
Lo justo para entrenar, nada para identificarte
Se guarda
- El texto ya anonimizado, hasta 1.000 caracteres
- Los enlaces, sin parámetros
- El canal: SMS, WhatsApp, correo, redes o QR
- A quién decía ser (la marca imitada)
- El tipo de remitente y el mes en que llegó
- Tu opinión: fraude, dudoso o legítimo
- El recorte del QR, si lo autorizas (en la UE)
No se guarda nunca
- El número de teléfono del remitente
- El texto original sin anonimizar
- La foto completa de la captura
- Tu IP: solo un resumen irreversible que cambia cada día
- Tu código de borrado: solo su huella SHA-256
- Ninguna cuenta ni dato de contacto
Protección
Abierto a todos, cerrado a los abusos
Captcha invisible
Cloudflare Turnstile frena los envíos automáticos sin pedir que marques semáforos.
Límite de envíos
Como máximo 10 reportes por hora desde la misma conexión, contados sin guardar la IP.
Filtro de calidad
Textos repetidos, sin letras o largos sin nada de fraude se marcan como basura antes de llegar al dataset.
Freno de almacenamiento
Por encima de 9 GB de fotos no se guardan recortes nuevos, para no salir del plan gratuito de 10 GB.
Panel privado
Etiquetar, borrar y exportar a CSV queda solo para el administrador.
Datos en la UE
Las fotos se guardan en Cloudflare R2 con jurisdicción europea.
Código
Las tres piezas clave
const REGLAS = [
[/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g, "<EMAIL>"],
[/\b[A-Z]{2}\d{2}(?:[ ]?[0-9A-Z]{4}){4,7}\b/g, "<IBAN>"],
[/\b(?:\d[ -]?){15,18}\d\b/g, "<TARJETA>"],
[/\b[XYZ]?\d{7,8}[ -]?[A-Z]\b/g, "<DNI>"],
[/(?<![\w€])\+?\d[\d .-]{7,}\d(?![\w€])/g, "<TEL>"],
[/\b\d{4,8}\b(?!\s?(?:€|eur|euros))/gi, "<CODIGO>"],
[/\b(Hola|Estimad[oa]|Querid[oa]|Sr\.?|Sra\.?|Don|Doña|Dear|Hi)(,?\s+)([A-ZÁÉÍÓÚÑ][a-záéíóúñ]+(?:\s+[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+){0,2})/g, "$1$2<NOMBRE>"],
];
// …
function anonimizar(texto) {
const urls = [];
let t = texto.replace(/\u00a0/g, " ").replace(RE_URL, (m) => {
const c = limpiarUrl(m);
if (c && !urls.includes(c)) urls.push(c);
return "<URL>";
});
for (const [re, rep] of REGLAS) t = t.replace(re, rep);
t = t.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
return { texto: t.slice(0, MAX), urls };
}export function analizarReporte(texto, urls = []) {
const t = String(texto || "");
const bajo = t.toLowerCase();
const sinMarcas = t.replace(/<[A-Z]+>/g, " ");
const palabras = bajo.replace(/<[a-z]+>/g, " ").match(new RegExp(`[${L}]+`, "g")) || [];
const letras = (sinMarcas.match(/[A-Za-zÁÉÍÓÚÜÑáéíóúüñ]/g) || []).length;
const visibles = sinMarcas.replace(/\s/g, "").length || 1;
const n = palabras.length;
// Texto sin sentido
if (/(.)\1{7,}/.test(sinMarcas) || /(..)\1{5,}/.test(sinMarcas.toLowerCase())) return { calidad: "basura", motivo: "caracteres repetidos" };
if (visibles >= 20 && letras / visibles < 0.45) return { calidad: "basura", motivo: "casi sin letras" };
if (n >= 20 && new Set(palabras).size / n < 0.3) return { calidad: "basura", motivo: "palabras repetidas" };
// Señales de fraude (cuántas veces aparecen, no solo si aparecen)
const enlaces = (Array.isArray(urls) ? urls.length : 0) + (t.match(/<URL>/g) || []).length;
let senales = (t.match(/<TEL>|<CODIGO>|<IBAN>|<TARJETA>/g) || []).length;
for (const re of SENALES) senales += (bajo.match(re) || []).length;
const funcion = palabras.filter((w) => FUNCION.has(w)).length;
// Texto largo (un libro, una noticia…) con casi nada de fraude, aunque lleve un enlace
if (n >= 45 && senales < n / 30) return { calidad: "basura", motivo: "texto largo con casi nada típico de un fraude" };
if (n >= 6 && funcion === 0 && enlaces === 0) return { calidad: "revisar", motivo: "no parece texto real" };
if (enlaces === 0 && senales === 0) {
if (n > 40) return { calidad: "basura", motivo: "texto largo sin nada típico de un fraude" };
return { calidad: "revisar", motivo: "sin enlaces ni señales de fraude" };
}
if (n >= 25 && senales < n / 20) return { calidad: "revisar", motivo: "pocas señales de fraude para lo largo que es" };
return { calidad: "util", motivo: "" };
}CREATE TABLE IF NOT EXISTS reportes (
id TEXT PRIMARY KEY, -- identificador aleatorio
creado TEXT NOT NULL, -- fecha ISO en UTC
canal TEXT NOT NULL, -- sms, whatsapp, correo, redes, qr, otro
opinion TEXT NOT NULL, -- fraude, dudoso, legitimo
remitente TEXT NOT NULL, -- tipo de remitente, nunca el número
marca TEXT, -- a quién decía ser
mes_recepcion TEXT, -- AAAA-MM
texto TEXT NOT NULL, -- mensaje ya anonimizado en el navegador
urls TEXT NOT NULL DEFAULT '[]',-- JSON con las URL sin parámetros
longitud_original INTEGER,
consentimiento_version TEXT NOT NULL,
consentimiento_fecha TEXT NOT NULL,
codigo_borrado_hash TEXT NOT NULL, -- SHA-256 del código que se da a quien reporta
revisado INTEGER NOT NULL DEFAULT 0,-- 0 sin revisar, 1 revisado
etiqueta_final TEXT, -- etiqueta tras revisión: fraude, legitimo, spam
origen TEXT -- texto, qr o captura (de dónde salió el texto)Estado
Qué está hecho y qué falta
Proyectos/Electrónica e IoT
AureaTech
Terminado · proyecto en equipo · UEMMaqueta a escala de un barrio que se monitoriza solo: farolas, casas, un lago y una calle con sensores reales conectados a un ESP32. Los datos viajan por WiFi y se guardan en MariaDB, y una aplicación de escritorio en Python los muestra en tiempo real.
- Hardware
- ESP32, ESP32-CAM, DHT22, HC-SR04, LDR, MQ-2, servo, buzzer
- Software
- Python, Flet, MariaDB, YOLO (Ultralytics), ONNX
- Construcción
- Impresión 3D y DM cortado a láser
- Equipo
- Daniel de Abajo, Paula Gómez Lucas, Pablo Serrano y Franco Zimmermann
Fotos
La maqueta, por fuera y por dentro
Toda la electrónica va escondida bajo la base: una protoboard con el ESP32 y el cableado, sobre una estructura de DM cortada a láser. Toca una foto para ampliarla.
Pruébalo
La calle, en pequeño
Mueve a la persona por la calle, cambia entre día y noche o haz llegar un coche a la barrera.
De noche: se encienden las farolas cercanas a la persona.
Sensores
Qué mide cada pieza y qué provoca
Cada sensor se lee a su ritmo: la luz y el humo cada 2 segundos, la distancia cada segundo y la temperatura cada 2 minutos.
| Pieza | Qué hace en la maqueta |
|---|---|
| HC-SR04 | Detecta a una persona caminando y enciende las farolas por tramos: según la distancia, la primera, la segunda o la tercera |
| LDR | Distingue día y noche: de día las farolas se quedan apagadas y al anochecer se encienden |
| DHT22 | Temperatura y humedad; por encima de 38 °C suena la alarma |
| MQ-2 | Detección de gas o humo; también dispara la alarma |
| LEDs | Las farolas, más un LED que indica el estado del sistema |
| Servo | La barrera de acceso de vehículos: 0° cerrada, 90° abierta |
| Buzzer | Alarmas |
| ESP32-CAM | Cámara del acceso para el reconocimiento de matrículas |
Plano
Cómo se conecta todo
El ESP32 escribe cada lectura en MariaDB y descarga las reglas de automatización cada 15 minutos. Si pierde la base de datos, sigue funcionando con unas reglas de reserva grabadas en el firmware. La cámara de la entrada pasa por el servidor de visión con YOLO.
Base de datos
17 tablas para una comunidad de vecinos
Tres papeles con permisos distintos: administrador, técnico y vecino. Todo lo que pasa queda en un registro de auditoría.
Comunidad y usuarioscommunity, user
Sensores y lecturassensor, actuator, reading
Automatizaciónautomation_rule, rule_actuator_action, rule_alert_action
Alertasalert, user_alert
Acceso de vehículosallowed_plate, camera_event
Soportechat_thread, chat_message, faq
Registroaudit_log, error_event
Reconocimiento de matrículas
Mi parte del proyecto
El módulo de visión del acceso de vehículos.
- 1Modelo YOLO entrenado para detectar matrículas en las imágenes de la ESP32-CAM, exportado también a ONNX para desplegarlo sin PyTorch.
- 2Normalización del texto de la matrícula.
- 3Comprobación contra las matrículas autorizadas de la comunidad, que el vecino solicita y el administrador aprueba.
- 4Si está autorizada, el servo abre la barrera. Si no, se avisa a los técnicos. Cada lectura queda guardada.
class PlateRecognizer:
def __init__(self, model_path: str | Path | None = None):
from ultralytics import YOLO
self.model_path = Path(model_path) if model_path else MODEL_PATH
self.model = YOLO(str(self.model_path))
def detect(self, image_path: str, save_result: bool = True):
"""
Detecta matrículas en una imagen.
Si save_result=True, guarda una copia de la imagen con el recuadro
de la matrícula dentro de app/infraestructure/vision/results/predict.
"""
if save_result:
results = self.model(
image_path,
save=True,
project=str(RESULTS_DIR),
name="predict",
exist_ok=True
)
else:
results = self.model(image_path)
detections = []
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
confidence = float(box.conf[0])
x1, y1, x2, y2 = box.xyxy[0].tolist()
detections.append({
"class": self.model.names[class_id],
"confidence": round(confidence, 4),
"bbox": {
"x1": round(x1, 2),
"y1": round(y1, 2),
"x2": round(x2, 2),
"y2": round(y2, 2),
}
})
return detectionsplate = normalize_plate(detected_plate)
if not plate:
raise ValueError("Detected plate cannot be empty.")
community_id = self.plate_repository.get_sensor_community_id(int(sensor_id))
if community_id is None:
raise ValueError(f"Sensor {sensor_id} not found.")
allowed = self.plate_repository.find_allowed_plate(plate, community_id)
is_allowed = allowed is not None
event_id = self.plate_repository.add_camera_event(
sensor_id=int(sensor_id),
detected_plate=plate,
is_allowed=is_allowed,
detected_at=detected_at or datetime.now(),
image_path=image_path,
)
# … se registra en audit_log …
alert_result = None
actuator_result = None
if not is_allowed:
alert_result = self._notify_technicians(
community_id=int(community_id),
plate=plate,
camera_event_id=int(event_id),
)
else:
actuator_result = self._open_garage(
community_id=int(community_id),
camera_event_id=int(event_id),
actor_id=int(actor_id),
)const FallbackRule FALLBACK_RULES[] = {
// Reglas fallback alineadas con las reglas semilla de docs/db.sql
{ SENSOR_ID_DHT_TEMP, METRIC_TEMPERATURE, ">", 38.0, ACTUATOR_ID_BUZZER, "SET", "ON" },
{ SENSOR_ID_LDR, METRIC_LUMINOSITY, "<", 50.0, ACTUATOR_ID_LED_ACTION_1, "SET", "ON" },
{ SENSOR_ID_LDR, METRIC_LUMINOSITY, "<", 50.0, ACTUATOR_ID_LED_ACTION_2, "SET", "ON" },
{ SENSOR_ID_MQ2, METRIC_AIR_QUALITY, ">", 180.0, ACTUATOR_ID_BUZZER, "SET", "ON" }
};
void evaluateRulesForMetric(int sensor_id, const char *metric_key, float value) {
if (g_fallback_mode) {
// En modo fallback usamos reglas hardcodeadas, sin detección de flanco
for (int i = 0; i < FALLBACK_RULES_COUNT; i++) {
const FallbackRule &fr = FALLBACK_RULES[i];
if (fr.sensor_id == sensor_id && strcmp(fr.metric_key, metric_key) == 0) {
if (evaluateCondition(value, fr.op, fr.threshold)) {
executeAction(fr.actuator_id, fr.command, fr.target);
}
}
}
return;
}
for (int i = 0; i < g_rules_count; i++) {
Rule &r = g_rules[i];
if (r.sensor_id != sensor_id) continue;
if (strcmp(r.metric_key, metric_key) != 0) continue;
bool triggered_now = evaluateCondition(value, r.comparison_operator, r.threshold_value);
if (triggered_now && !r.was_triggered) {
// Flanco de subida (la regla acaba de empezar a cumplirse)
triggerRuleActions(r.rule_id);
}
r.was_triggered = triggered_now;
}
}Entregables
Cómo se presentó
Memoria técnica
Pruebas, problemas encontrados y conclusiones, con diagramas PlantUML de clases y de la base de datos.
Defensa de 7 minutos
Presentación de 9 diapositivas pensada para defender con la maqueta física delante.
Póster
Póster académico en A2, con una versión reducida en A4.
Trabajo en grupo de cuatro personas: todos participamos en el diseño, la electrónica, el software y la maqueta. Código publicado con el visto bueno del profesor de la asignatura.
Proyectos/Electrónica e IoT
Coche RC con ESP32
Funcionando · proyecto en equipoCoche de dos ruedas motrices que se maneja desde el móvil o desde un mando hecho con un M5Stack, con un sensor de ultrasonidos que frena solo cuando tiene algo delante.
- Cerebro
- ESP32 Dev Module + driver L298N
- Control
- Bluetooth (App Inventor), MQTT (M5Stack) o USB
- Frenado
- Mitad de velocidad a 35 cm, parada a 20 cm
- Equipo
- Franco Zimmermann, Lucas Toledano y Daniel de Abajo
Fotos
El coche y su mando
Pruébalo
Arranca y mira cuándo frena
El sensor mide cada 60 ms. Por debajo de 35 cm el coche va a la mitad de velocidad y por debajo de 20 cm no deja avanzar y pita, aunque sí deja ir marcha atrás y girar para salir.
Distancia al obstáculo: —
Control
Tres formas de manejarlo
Si se pierde la conexión Bluetooth, el coche se para.
Móvil por Bluetooth
Una app hecha con MIT App Inventor se empareja con «CocheFranco» y manda letras: F adelante, B atrás, S parar… El coche solo se mueve mientras mantienes pulsado.
Mando M5Stack por MQTT
Un M5Stack Core se conecta al WiFi y publica el comando en el topic francozimm/coche/cmd. El coche está suscrito y lo ejecuta.
USB
Los mismos comandos por el monitor serie a 115200 baudios, para probar sin móvil.
Comandos
| Bluetooth | MQTT | Acción |
|---|---|---|
| F B L R | 1 2 3 4 | Adelante, atrás, izquierda, derecha |
| G I H J | — | Diagonales: adelante y atrás, a izquierda y derecha |
| S | 0 | Parar |
| 0 … 9, q | — | Velocidad, de mínima a máxima |
| V / v | 5 | Bocina |
| D | — | El coche responde con la distancia en cm |
El sensor
Medir con el eco
El HC-SR04 lanza un pulso de ultrasonidos y mide cuánto tarda en volver. En el código, el tiempo en microsegundos se divide entre 58 para obtener centímetros. Si en 25 ms no vuelve el eco (unos 4 m), el camino se da por libre.
Montaje
Material y conexiones
Material
| Componente | Cantidad |
|---|---|
| ESP32 Dev Module | 1 |
| Driver de motores L298N | 1 |
| Motores DC con reductora y ruedas | 2 |
| Rueda loca | 1 |
| Sensor HC-SR04 | 1 |
| Resistencias 1 kΩ y 2 kΩ | 1 + 1 |
| Buzzer activo | 1 |
| Batería 7,4 V (2 × 18650) | 1 |
Conexiones
| ESP32 | Va a |
|---|---|
| GPIO 14 · 27 · 26 | L298N ENA, IN1, IN2 (motor izquierdo) |
| GPIO 32 · 25 · 33 | L298N ENB, IN3, IN4 (motor derecho) |
| GPIO 4 | HC-SR04 TRIG |
| GPIO 18 | HC-SR04 ECHO, por un divisor 1 kΩ / 2 kΩ porque da 5 V |
| GPIO 23 | Buzzer |
| GND | Masa común del L298N, el sensor y la batería |
Código
El firmware, por partes
Fragmentos reales de esp32_rc_car.ino.
// Aplica el movimiento actual teniendo en cuenta el obstáculo
void aplicarMovimiento() {
int v = velocidad;
if (vaHaciaDelante(movimiento)) {
if (distancia < DISTANCIA_FRENO) {
parar();
return;
}
if (distancia < DISTANCIA_AVISO) {
v = max(VELOCIDAD_MIN, v / 2);
}
}
int giro = v / 2; // en las diagonales una rueda va más lenta
switch (movimiento) {
case ADELANTE: mover( v, v); break;
case ATRAS: mover(-v, -v); break;
case IZQUIERDA: mover(-v, v); break; // gira sobre sí mismo
case DERECHA: mover( v, -v); break;
case ADELANTE_IZQ: mover( giro, v); break;
case ADELANTE_DER: mover( v, giro); break;
case ATRAS_IZQ: mover(-giro, -v); break;
case ATRAS_DER: mover(-v, -giro); break;
default: parar(); break;
}
}int medirDistancia() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
// 25 ms de espera máxima ≈ 4 m; si no hay eco, damos el camino por libre
unsigned long duracion = pulseIn(PIN_ECHO, HIGH, 25000);
if (duracion == 0) return 999;
return duracion / 58; // µs -> cm
}// El M5Stack manda números:
// 0 parar · 1 adelante · 2 atrás · 3 izquierda · 4 derecha · 5 bocina
// También acepta las mismas letras que por Bluetooth (F, B, S...).
void alRecibirMQTT(char* topic, byte* payload, unsigned int largo) {
if (largo == 0) return;
char c = (char)payload[0];
switch (c) {
case '0': procesarComando('S'); break;
case '1': procesarComando('F'); break;
case '2': procesarComando('B'); break;
case '3': procesarComando('L'); break;
case '4': procesarComando('R'); break;
case '5': bocina = !bocina; break;
default: procesarComando(c); break;
}
Serial.printf("MQTT: %c\n", c);
}Próximos pasos
Lo siguiente
Vídeo
Grabar el coche en movimiento, frenando ante un obstáculo.
Chasis nuevo
Una segunda versión del chasis.
Mejor mando
M5Stack con joystick o acelerómetro en vez de botones.
Modo autónomo
Que esquive los obstáculos solo.
Proyectos/Inteligencia artificial
Zynthra-AI
Código abierto · licencia MITUn asistente para estudiar que funciona entero en tu ordenador. Responde a partir de tus propios documentos, cita de dónde sale cada respuesta y, si detecta que le estás pidiendo que te haga el trabajo, cambia a modo tutor y te guía con preguntas en lugar de darte la solución.
- Backend
- Python, FastAPI, MongoDB
- Frontend
- React 19
- Modelos
- Ollama: Llama 3.2 1B/3B o Qwen2.5 1.5B
- Requisitos
- 8 GB de RAM, sin APIs de pago
Qué hace
Un RAG que enseña en lugar de copiar
100 % local
Todo corre en tu máquina con Ollama. Ningún documento ni pregunta sale a terceros.
Respuestas verificables
Cada respuesta cita los fragmentos del documento en los que se apoya.
Modo tutor
Ante un intento de copia, responde con preguntas socráticas que llevan a la solución sin darla.
Umbral de confianza
Configurable: decide cuánta evidencia hace falta para responder.
Índice persistente
Los embeddings se guardan en disco y no se recalculan en cada arranque.
Español e inglés
Instrucciones y detección de intención en los dos idiomas.
Anticopia
¿Estudias o quieres que te lo hagan?
Cada mensaje se clasifica en cuatro tipos: estudio, intento de copia, conversación o aclaración. Esta demo usa las mismas reglas y pesos que el clasificador real, en tu navegador.
- !Copia si su puntuación supera 0,6 y la de estudio: se activa el modo tutor.
- ✓Estudio si supera 0,5: respuesta completa con fuentes.
Arquitectura
Del mensaje a la respuesta con fuentes
| Módulo | Qué hace |
|---|---|
| chunker.py | Divide el texto en fragmentos con solapamiento |
| embeddings.py | Genera los vectores con Ollama, con caché |
| indexer.py | Índice vectorial persistente en disco |
| retriever.py | Búsqueda semántica con el contexto de la conversación |
| intent.py | Clasificación de la intención: estudio, copia, conversación o aclaración |
| llm.py | Cliente de Ollama con respuesta en streaming |
| orchestrator.py | Coordina todo el flujo |
Código
El clasificador de intención
def classify(
self,
message: str,
conversation_history: List[dict] = None
) -> IntentClassification:
message_lower = message.lower().strip()
# Check for smalltalk first (highest priority for short messages)
if self._is_smalltalk(message_lower):
return IntentClassification(
intent_type=IntentType.SMALLTALK,
confidence=0.9,
triggered_patterns=[],
recommended_mode=ResponseMode.CONVERSATIONAL
)
# Check for copy intent
copy_score, copy_patterns = self._check_copy_intent(message_lower)
# Check for legitimate study intent
study_score, study_patterns = self._check_study_intent(message_lower)
# Determine intent based on scores
if copy_score > 0.6 and copy_score > study_score:
return IntentClassification(
intent_type=IntentType.COPY_ATTEMPT,
confidence=copy_score,
triggered_patterns=copy_patterns,
recommended_mode=ResponseMode.GUIDED_MODE
)
if study_score > 0.5:
return IntentClassification(
intent_type=IntentType.LEGITIMATE_STUDY,
confidence=study_score,
triggered_patterns=study_patterns,
recommended_mode=ResponseMode.FULL_RESPONSE
)
# Check for clarification (follow-up questions)
if self._is_clarification(message_lower, conversation_history):
return IntentClassification(
intent_type=IntentType.CLARIFICATION,
confidence=0.7,
triggered_patterns=[],
recommended_mode=ResponseMode.FULL_RESPONSE
)
# Default: unknown but allow full response
return IntentClassification(
intent_type=IntentType.UNKNOWN,
confidence=0.5,
triggered_patterns=[],
recommended_mode=ResponseMode.FULL_RESPONSE
)Proyectos/Inteligencia artificial
REN
Presentado en el XVIII IT Seminar (Suiza)Un asistente que responde con tus propios documentos, en tu ordenador. Primero busca en local; solo si la evidencia no alcanza, consulta la web; y si tampoco, dice que no lo sabe en lugar de inventar.
- Interfaz
- Python y Streamlit
- Modelos
- llama3.2:3b y nomic-embed-text con Ollama
- Entradas
- Texto, PDF, imágenes con OCR y audio
- Licencia
- MIT
Qué hace
Evidencia antes que improvisación
RAG local
Responde con los archivos de la carpeta docs/, troceados en fragmentos de 1.100 caracteres con 150 de solapamiento.
Lee de todo
Importa PDF e imágenes con OCR (EasyOCR) y los convierte en texto para la base.
Escucha
Transcribe audio en local con faster-whisper para usar una grabación como pregunta.
Memoria
Guarda las conversaciones y tiene en cuenta el intercambio anterior al buscar.
Web solo si hace falta
Fallback conservador cuando la evidencia local es débil o está desactualizada.
Sin inventar
Nunca inventa fuentes, enlaces ni citas. Si no hay evidencia fiable, lo dice.
Cómo decide
Local, web o «no lo sé»
El recuperador devuelve la similitud del mejor fragmento. Mueve los deslizadores para ver qué camino toma, con los umbrales reales del proyecto.
Umbral local: 0,60 (0,70 para considerarla fuerte).
Modelos
Por qué estos modelos
Decisión revisada en abril de 2026 con la documentación oficial de Ollama.
llama3.2:3b, por defecto
Buen equilibrio entre calidad y tamaño (unos 2 GB), pensado para diálogo multilingüe, recuperación y resúmenes. Cabe en un ordenador personal.
qwen2.5:3b, alternativa
Más fuerte siguiendo instrucciones y con salida estructurada, pero la mejora no compensa cambiar una base estable.
Código
El paso a la web
Fragmento de chat_engine.py: si no hay evidencia local suficiente, se busca en la web; si tampoco, se responde que no hay información fiable.
if not explicit_web_request:
hits = retrieve(local_index, contextual_query, settings.top_k) if local_index else []
top_score = hits[0][1] if hits else None
stale_local_hit = (
settings.use_freshness_check
and bool(hits)
and _has_stale_content(hits, settings.max_doc_age_days, settings.assume_no_date_is_stale)
)
if (
explicit_web_request
or
not hits
or should_use_web_fallback(top_score)
or top_score < settings.min_score
or (stale_local_hit and settings.freshness_mode == "Prefer web fallback")
):
web_index = build_web_index(contextual_query)
hits = retrieve(web_index, contextual_query, settings.top_k) if web_index else []
top_score = hits[0][1] if hits else None
if not hits or should_use_web_fallback(top_score) or top_score < settings.min_score:
return ChatResult(
assistant_text=insufficient_evidence_message(user_message),
assistant_meta={"sources": [], "scores": [], "urls": []},
last_hits=hits,
)
# en rules_config.py
def should_use_web_fallback(top_score):
"""
Returns True when local retrieval should be considered insufficient.
"""
if top_score is None:
return True
return top_score < LOCAL_CONFIDENCE_THRESHOLDProyectos/Software
Gestor de pedidos en Java
Terminado · Introducción a la Ingeniería de SoftwareAplicación de escritorio para buscar, crear, editar y borrar pedidos, con los importes en euros o dólares según el tipo de cambio en tiempo real. Hecha con arquitectura MVC, pruebas unitarias e integración continua.
- Lenguaje
- Java 17 y Swing
- Librerías
- Jackson, OkHttp, SLF4J y Logback
- Calidad
- 48 pruebas con JUnit 5 y GitHub Actions
- Diagramas
- PlantUML: clases, casos de uso, secuencia y despliegue
Funciones
Lo que hace
Lista y búsqueda
Pedidos en un panel lateral; al elegir uno se ve su detalle, el total bruto y el total con descuento.
Crear y editar
Formulario con validaciones: ID obligatorio y único, y al menos un artículo. Se edita la cantidad y el descuento de cada artículo.
Euros o dólares
Tipo de cambio de una API pública. Si falla, la app avisa y sigue en euros.
Persistencia en JSON
Cada cambio se guarda en orders.json, y la lectura tolera campos de versiones anteriores.
Borrado seguro
Con confirmación antes de eliminar un pedido.
Arquitectura
Cada capa, una responsabilidad
La vista solo muestra, el controlador decide y el servicio obtiene el dato externo.
| Paquete | Contenido |
|---|---|
| model/ | Order, Article, Calculator, Searcher |
| view/ | Ventana principal y diálogos de crear y editar (Swing) |
| controller/ | OrderController, que coordina |
| service/ | ExchangeRateService: EUR → USD con OkHttp y Jackson |
Pruebas y CI
Cada cambio se comprueba solo
Código
El tipo de cambio
public class ExchangeRateService {
private final OkHttpClient client = new OkHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
public double getEuroToUsdRate() throws IOException {
// API externa de tipos de cambio. Base: EUR
String url = "https://open.er-api.com/v6/latest/EUR";
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful() || response.body() == null) {
throw new IOException("Error HTTP al obtener tipo de cambio: " + response.code());
}
String json = response.body().string();
JsonNode root = mapper.readTree(json);
double rate = root.path("rates").path("USD").asDouble();
if (rate <= 0) {
throw new IOException("Tipo de cambio inválido: " + rate);
}
return rate;
}
}
}Proyectos/Aplicaciones
App de gastos
En diseñoSaber a dónde va el dinero cada mes, sin apuntarlo a mano. Una app para Android e iPhone que registra los gastos de las tarjetas, permite añadir el efectivo con una foto del ticket o con la voz y los clasifica sola.
- Plataformas
- Android e iPhone
- Entrada
- Tarjetas, foto del ticket, voz y escritura
- Organización
- Por categorías y por mes
- Seguridad
- Nivel bancario si se conecta al banco
Funciones
Lo que tiene que hacer
Registrar
Gastos de tarjetas de crédito y débito con fecha y hora, efectivo leyendo el ticket con la cámara, y dictado o escritura en el momento.
Ordenar
Categorías como supermercado, comer fuera, transporte o efectivo, guardadas mes a mes.
Cerrar el mes
Cuánto entró, cuánto salió y cuánto queda disponible, con desgloses simples pero completos.
Vigilar
Aviso de cargos duplicados y de movimientos que podrían ser un uso fraudulento de la tarjeta.
Cómo funcionará
Del gasto al cierre del mes
- 1Entra el gasto. Desde la tarjeta, una foto del ticket o diciéndolo en voz alta.
- 2Se clasifica solo. Y aprende de tus correcciones.
- 3Avisa de lo raro. Cargos repetidos o importes fuera de lo normal.
- 4Cierre mensual. Sin hacer cuentas.
Prueba el aviso de duplicados
Movimientos de ejemplo. Se marca como duplicado el mismo importe en el mismo comercio con menos de 10 minutos de diferencia.
| Hora | Comercio | Importe | Estado |
|---|
Datos
Un movimiento, un cierre
Dos piezas bastan para empezar: cada movimiento, con su origen y su categoría, y el resumen de cada mes.
{
"movimiento": {
"fecha": "2026-10-14",
"importe": -23.40,
"origen": "tarjeta | ticket | voz",
"comercio": "Supermercado",
"categoria": "Supermercado",
"sospechoso": false
},
"cierre_mensual": {
"mes": "2026-10",
"entradas": 1000,
"salidas": 588,
"disponible": 412
}
}Modelo previsto; los importes son de ejemplo.
Retos
Lo difícil
Leer tickets
Fotos torcidas, papel térmico desgastado y formatos distintos en cada comercio. Hay que sacar comercio, fecha y total con fiabilidad.
Conectar con el banco
Leer las tarjetas exige acceso a la cuenta. Es el punto que pide seguridad de nivel bancario y cumplir la normativa europea de pagos.
Clasificar bien
El mismo comercio puede ser supermercado o comer fuera. La app debe aprender de cada corrección sin volverse impredecible.
Plan
Por fases
Recursos
Guía práctica contra el fraude digital
Todo lo necesario en un solo sitio, en el orden en que lo vas a necesitar: reconocer el engaño, ponerte a prueba, actuar si ya caíste y blindar tus cuentas.
1 · Reconocer
Cambia el canal, no el truco
Casi todos los fraudes combinan tres cosas: prisa, una marca o persona de confianza y una acción (pulsar, llamar, pagar o dar un código). Si las ves juntas, para.
SMS · smishingEl paquete, el banco, la multa
- Llega incluso dentro del hilo real de tu banco: el remitente se puede falsificar.
- El enlace no es del dominio oficial: mira lo que va justo antes de .es o .com.
- Pide pagar una cantidad pequeña (1,99 €) para robar la tarjeta entera.
Correo · phishingLa factura, la cuenta suspendida
- El nombre del remitente parece bien, pero la dirección real es otra.
- Adjuntos inesperados (.zip, .html, .iso) o «ver documento compartido».
- Saludo genérico y una consecuencia grave si no actúas hoy.
WhatsApp«Hola mamá, este es mi número nuevo»
- Un familiar con número nuevo que enseguida necesita dinero o un Bizum.
- No puede llamar «porque tiene el micro roto».
- Compruébalo llamando al número de siempre o con una pregunta que solo él sepa.
Llamada · vishing«Le llamamos de seguridad de su banco»
- Te alertan de un cargo que no has hecho y te piden un código o que muevas el dinero «a una cuenta segura».
- El número que aparece puede ser el real del banco.
- Cuelga y llama tú al número de la tarjeta o de la app.
Código QR · quishingParquímetros, cartas, avisos pegados
- Pegatinas encima del QR original en parquímetros o cargadores.
- QR en un correo para saltarse los filtros de enlaces.
- Antes de pagar, mira la dirección que muestra la cámara al leerlo.
Compras e inversionesChollos y ganancias seguras
- Tiendas con precios imposibles, dominio reciente y solo transferencia o Bizum.
- Inversiones con rentabilidad «garantizada» y famosos que nunca la recomendaron.
- Si piden pagar fuera de la plataforma, es fraude.
Cómo leer un enlace en cinco segundos
Aquí el dominio es clientes.top, no bbva.es. Que aparezca «bbva» delante no significa nada.
2 · Ponte a prueba
¿Fraude o no?
Seis mensajes de ejemplo inspirados en campañas reales. Decide antes de ver la respuesta.
3 · Si ya caíste
Qué hacer, según lo que diste
Abrir un enlace casi nunca es el problema; lo es lo que haces después. Elige tu caso.
- Cámbiala ya desde la web o app oficial, no desde el enlace.
- Cámbiala también en cualquier otro sitio donde uses la misma.
- Cierra las sesiones abiertas y activa la verificación en dos pasos.
- Revisa reglas de reenvío en tu correo: los atacantes las crean para seguir leyéndolo.
- Llama a tu banco y bloquea la tarjeta.
- Revisa los movimientos de los últimos días y de los siguientes.
- Pide la devolución de los cargos que no reconozcas por escrito.
- Denuncia y guarda el justificante: el banco suele pedirlo.
- Un código de un solo uso autoriza algo: llama a tu banco o al servicio al momento.
- Si era de WhatsApp, entra en la app y vuelve a verificar tu número; activa el PIN de verificación en dos pasos.
- Avisa a tus contactos de que pueden recibir mensajes en tu nombre.
- Pon el móvil en modo avión.
- Desinstala la app; si no te deja, revisa los administradores del dispositivo.
- Desde otro dispositivo, cambia las contraseñas del banco y del correo.
- Si había acceso a la banca, llama al banco. Si dudas, restablece el móvil de fábrica tras guardar fotos y contactos.
- Llama al banco cuanto antes: algunas transferencias se pueden frenar en las primeras horas.
- Guarda capturas del mensaje, la web y el justificante.
- Denuncia en la Policía Nacional o la Guardia Civil.
- Desconfía de quien luego te ofrezca recuperar el dinero a cambio de una comisión: es otro fraude.
4 · Protégete
Tres hábitos que bloquean casi todo
- 1Una contraseña distinta en cada sitio, guardadas en un gestor de contraseñas. Así una filtración no abre todas tus puertas.
- 2Verificación en dos pasos en el correo, el banco y las redes. Mejor con app o llave que con SMS.
- 3Entra siempre por tu cuenta en la app o la web oficial, nunca desde un enlace que te envían.
Tu móvil, más seguro
Seis ajustes. Márcalos a medida que los hagas; se guardan en tu navegador.
0 de 6 hechos
5 · Dónde pedir ayuda
No estás solo
INCIBE · Tu Ayuda en Ciberseguridad
Gratuito y confidencial. También por WhatsApp en el 900 116 117.
Policía Nacional o Guardia Civil
Lleva capturas, el número o la web y los justificantes de pago.
Bloqueo y reclamación
Usa el teléfono de la tarjeta o de la app, nunca el de un mensaje.
Have I Been Pwned ↗
Comprueba si tu correo apareció en alguna filtración de datos.
Avisos de INCIBE ↗
Las campañas de fraude activas en España, con ejemplos.
Buzón de reportes
Envía el mensaje que recibiste y ayuda a entrenar el detector en español.
Glosario
Las palabras, sin tecnicismos
- Phishing
- Engaño para que entregues tus datos haciéndose pasar por alguien de confianza.
- Smishing
- Phishing por SMS o mensajería.
- Vishing
- Phishing por llamada de voz.
- Quishing
- Phishing con un código QR.
- Dominio
- El nombre real de una web: lo que va justo antes de la primera barra.
- Acortador
- Servicio que convierte un enlace largo en uno corto y oculta el destino.
- Suplantación
- Hacer que un mensaje o llamada parezca venir de otro remitente.
- Verificación en dos pasos
- Un segundo paso, además de la contraseña, para entrar en una cuenta.
- Gestor de contraseñas
- App que crea y guarda una contraseña distinta para cada sitio.
- Cloaking
- Truco de las webs fraudulentas para enseñar una página inocente a los detectores.
Notas
Bitácora, lecciones y próximos pasos
Qué hemos hecho, qué no salió como esperábamos y qué viene ahora.
Bitácora
Qué ha pasado
- Buzón
El buzón de reportes, publicado
Ya se pueden enviar SMS y códigos QR sospechosos, gratis y sin crear cuenta. Los datos personales se borran en el propio navegador y cada envío recibe un código para eliminarlo cuando quieras.
Ver el proyecto →
Próximos pasos
Lo que viene, proyecto a proyecto
Obexis
- Seguir desarrollando el sistema
- Contar más detalles más adelante
Buzón de reportes
- Panel protegido con Cloudflare Access
- Reunir los primeros cientos de reportes
App de gastos
- Diseño de pantallas
- Primera versión: registro manual y cierre mensual
Coche con ESP32
- Vídeo del coche en movimiento
- Segunda versión del chasis
- Mando con joystick o acelerómetro
- Modo autónomo
Sobre mí
Hola, soy Franco
Estudio Ingeniería Informática en la Universidad Europea de Madrid. Me dedico al software, los datos y la seguridad, y en Zimmy comparto lo que voy construyendo.
Ahora mismo estoy centrado en Obexis, un sistema para detectar enlaces fraudulentos antes de que alguien caiga en ellos.
Contacto
Escríbeme si quieres colaborar, comentar un proyecto o avisar de un fraude que te haya llegado.
zemtra.contacto@gmail.com