Ever been woken up by that annoying high-pitched buzz at 3 AM? Not anymore. Meet the Skeeter-Scan 16, a tactical edge-computing project built for the Hackster Holidays Giveaway.
We’ve combined the modular power of the Framework Laptop 16 with a custom ESP32-CAM "Sentinel" to create a real-time, autonomous pest radar.
How it Works:- The Brain (ESP32-CAM): Running Edge-AI to filter wing-beat frequencies and monitor the room via local processing—privacy first! [2, 3]
- The Radar (HC-SR04): An ultrasonic sonar that confirms target distance with precision. [1, 2]
- The Sight (650nm Laser): A low-power red laser module that "paints" the target, so you know exactly where to strike. [1, 3]
- The Interface (Framework + Meta Ray-Ban): Using Autodesk Fusion to map the room's 3D geometry, the system streams a tactical HUD directly to my Meta Ray-Ban Display, turning pest control into a real-life video game. [3, 4]How does the ESP32 distinguish a mosquito from a fan?A: It uses a Narrow-Band FFT filter. Most fans have a consistent low-frequency hum, while mosquitoes have a "wavering" frequency that the ESP32’s dual-core processor can identify.
Q: Why use the Framework 16 for this?A: Tracking multiple targets in 3D space requires significant mathematical throughput. The Framework 16 handles the complex geometry and hosts the Hackster.io project logs while remaining portable enough to move between rooms. Go to Tools > Board and select "AI Thinker ESP32-CAM".Connect the HC-SR04 sensor: VCC to 5V, GND to GND, Trig to GPIO 13, and Echo to GPIO 12.Connect the Laser Module: Red wire to GPIO 4 and black wire to GND.Technical note: GPIO 4 on the ESP32-CAM is also connected to the board's Flash LED. Using this pin will cause the Flash to illuminate along with the laser, creating an even stronger "visual alert" effect in your project!
/*
* SKEETER-SCAN 16 - Tactical Radar Firmware
* Board: ESP32-CAM (O Cérebro)
*/
// Definição dos Pinos (GPIOs)
const int PIN_TRIG = 13; // Trigger do Sensor Ultrassônico
const int PIN_ECHO = 12; // Echo do Sensor Ultrassônico
const int PIN_LASER = 4; // Módulo Laser (Cilíndrico 6mm)
// Configurações do Radar
const int DISTANCIA_ALVO = 50; // Distância de detecção em cm (ajustável)
void setup() {
Serial.begin(115200);
// Configuração dos pinos
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_LASER, OUTPUT);
// Garante que o laser comece desligado
digitalWrite(PIN_LASER, LOW);
Serial.println("SKEETER-SCAN 16: Sistema Iniciado...");
}
void loop() {
// 1. Gera o pulso ultrassônico
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
// 2. Mede o tempo de resposta
long duracao = pulseIn(PIN_ECHO, HIGH);
// 3. Calcula a distância em cm
int distancia = duracao * 0.034 / 2;
// 4. Lógica de Detecção e Mira
if (distancia > 0 && distancia <= DISTANCIA_ALVO) {
Serial.print("ALVO DETECTADO! Distancia: ");
Serial.print(distancia);
Serial.println(" cm");
digitalWrite(PIN_LASER, HIGH); // Ativa a Mira Laser
delay(500); // Mantém o laser aceso por meio segundo
} else {
digitalWrite(PIN_LASER, LOW); // Desliga o Laser
}
delay(100); // Pequena pausa para a próxima varredura
}
V2
/*
* SKEETER-SCAN 16 - Virtual Sweep Upgrade
* Simula a lógica de varredura de 0° a 180° para integração com o HUD
*/
#include <Arduino.h>
const int PIN_TRIG = 13;
const int PIN_ECHO = 12;
const int PIN_LASER = 4;
// Lógica de Varredura Virtual
int anguloAtual = 0;
int direcao = 1; // 1 para aumentar, -1 para diminuir
const int PASSO = 10; // Incremento de 10 graus por leitura
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_LASER, OUTPUT);
Serial.println("SKEETER-SCAN 16: Modo Varredura Virtual Ativo");
}
void loop() {
// 1. Lógica de Movimentação do Ângulo (Simulada)
// Se você adicionar um servo no futuro, o comando 'servo.write(anguloAtual)' viria aqui.
anguloAtual += (direcao * PASSO);
if (anguloAtual >= 180 || anguloAtual <= 0) {
direcao *= -1; // Inverte a direção ao chegar nos limites
}
// 2. Medição Ultrassônica
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duracao = pulseIn(PIN_ECHO, HIGH);
int distancia = duracao * 0.034 / 2;
// 3. Output Formatado para o Framework 16 / HUD
// Enviamos o ângulo e a distância para que o processamento 3D no PC mapeie o arco
Serial.print("ANG:");
Serial.print(anguloAtual);
Serial.print("|DIST:");
Serial.println(distancia);
// 4. Lógica de Disparo (Laser)
// O laser só deve disparar se o alvo estiver no "centro" ou em um ângulo de interesse
if (distancia > 0 && distancia <= 50) {
digitalWrite(PIN_LASER, HIGH);
// No HUD, isso apareceria como um "Lock-on" no ângulo X
Serial.printf("!!! ALVO DETECTADO NO ANGULO %d !!!\n", anguloAtual);
} else {
digitalWrite(PIN_LASER, LOW);
}
delay(50); // Delay menor para uma varredura mais fluida
}
New CPP
C/C++To actually move the sensor without a motor, some makers use multiple sensor or vibration techniques, but the most efficient way to implement "scanning" via pure code on the ESP32-CAM (which is dual-core) is to create a Virtual Sampling Scan.
/*
* SKEETER-SCAN 16 - Virtual Sweep Upgrade
* Simula a lgica de varredura de 0 a 180 para integrao com o HUD
*/
#include <Arduino.h>
const int PIN_TRIG = 13;
const int PIN_ECHO = 12;
const int PIN_LASER = 4;
// Lgica de Varredura Virtual
int anguloAtual = 0;
int direcao = 1; // 1 para aumentar, -1 para diminuir
const int PASSO = 10; // Incremento de 10 graus por leitura
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_LASER, OUTPUT);
Serial.println("SKEETER-SCAN 16: Modo Varredura Virtual Ativo");
}
void loop() {
// 1. Lgica de Movimentao do ngulo (Simulada)
// Se voc adicionar um servo no futuro, o comando 'servo.write(anguloAtual)' viria aqui.
anguloAtual += (direcao * PASSO);
if (anguloAtual >= 180 || anguloAtual <= 0) {
direcao *= -1; // Inverte a direo ao chegar nos limites
}
// 2. Medio Ultrassnica
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duracao = pulseIn(PIN_ECHO, HIGH);
int distancia = duracao * 0.034 / 2;
// 3. Output Formatado para o Framework 16 / HUD
// Enviamos o ngulo e a distncia para que o processamento 3D no PC mapeie o arco
Serial.print("ANG:");
Serial.print(anguloAtual);
Serial.print("|DIST:");
Serial.println(distancia);
// 4. Lgica de Disparo (Laser)
// O laser s deve disparar se o alvo estiver no "centro" ou em um ngulo de interesse
if (distancia > 0 && distancia <= 50) {
digitalWrite(PIN_LASER, HIGH);
// No HUD, isso apareceria como um "Lock-on" no ngulo X
Serial.printf("!!! ALVO DETECTADO NO ANGULO %d !!!\n", anguloAtual);
} else {
digitalWrite(PIN_LASER, LOW);
}
delay(50); // Delay menor para uma varredura mais fluida
}
Skeeter Scan in Python
Pythonimport cv2
import numpy as np
import serial
import time
import threading
import sys
# ==========================================
# CONFIGURAES DO USURIO (AJUSTE AQUI)
# ==========================================
# IP da ESP32-CAM (veja no Monitor Serial ao iniciar o ESP32)
ESP32_IP = "192.168.1.100"
URL_STREAM = f"http://{ESP32_IP}:81/stream"
# Porta Serial (Windows: 'COM3', 'COM4' | Linux/Mac: '/dev/ttyUSB0')
SERIAL_PORT = 'COM3'
BAUD_RATE = 115200
# Parmetros de Deteco (Calibragem)
AREA_MINIMA = 50 # Tamanho mnimo do objeto (filtra rudo)
AREA_MAXIMA = 800 # Tamanho mximo (filtra pessoas/mos)
DISTANCIA_TIRO = 50 # Distncia mxima em cm para disparo
# ==========================================
# VARIVEIS GLOBAIS DE ESTADO
# ==========================================
telemetria = {
"distancia": 0,
"sistema_ativo": True,
"laser_ativo": False,
"lock_visual": False
}
# ==========================================
# THREAD DE COMUNICAO SERIAL
# ==========================================
def serial_comms():
"""Gerencia envio e recebimento de dados com o ESP32 em paralelo."""
try:
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=0.1)
time.sleep(2) # Tempo para estabilizar conexo
print(f"[SERIAL] Conectado na porta {SERIAL_PORT}")
while telemetria["sistema_ativo"]:
# 1. LEITURA (Recebe dados do Sonar)
if ser.in_waiting:
try:
linha = ser.readline().decode('utf-8', errors='ignore').strip()
if "DIST:" in linha:
# Formato esperado: "ANG:0|DIST:45" ou apenas "DIST:45"
partes = linha.split("DIST:")
if len(partes) > 1:
telemetria["distancia"] = int(partes[1])
except ValueError:
pass
# 2. ESCRITA (Envia comando do Laser)
# Protocolo: 'L' = Laser ON, 'O' = Laser OFF
comando = b'L' if telemetria["laser_ativo"] else b'O'
ser.write(comando)
time.sleep(0.05) # Evita sobrecarga da CPU
ser.close()
except Exception as e:
print(f"[ERRO SERIAL] Falha na conexo: {e}")
telemetria["distancia"] = -1
# ==========================================
# VISO COMPUTACIONAL E HUD
# ==========================================
def desenhar_hud(frame, largura, altura):
"""Desenha a interface ttica sobre o vdeo."""
centro_x, centro_y = largura // 2, altura // 2
cor_hud = (0, 255, 0) # Verde Hacker
cor_alerta = (0, 0, 255) # Vermelho
# 1. Retcula Central
if telemetria["laser_ativo"]:
cv2.circle(frame, (centro_x, centro_y), 40, cor_alerta, 2)
cv2.putText(frame, "ENGAGED", (centro_x-35, centro_y-50), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor_alerta, 2)
else:
cv2.circle(frame, (centro_x, centro_y), 20, cor_hud, 1)
cv2.line(frame, (centro_x-30, centro_y), (centro_x+30, centro_y), cor_hud, 1)
cv2.line(frame, (centro_x, centro_y-30), (centro_x, centro_y+30), cor_hud, 1)
# 2. Painel de Dados
cv2.rectangle(frame, (10, 10), (250, 110), (0, 0, 0), -1) # Fundo preto semi-transparente
cv2.rectangle(frame, (10, 10), (250, 110), cor_hud, 1)
cv2.putText(frame, "SKEETER-SCAN V3", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor_hud, 2)
# Status do Sonar
dist = telemetria["distancia"]
txt_dist = f"SONAR: {dist} cm" if dist != -1 else "SONAR: ERRO"
cv2.putText(frame, txt_dist, (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor_hud, 1)
# Status do Sistema
status = "ARMED" if telemetria["lock_visual"] else "SCANNING"
cor_sts = cor_alerta if telemetria["lock_visual"] else cor_hud
cv2.putText(frame, f"STATUS: {status}", (20, 95), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor_sts, 2)
def main():
# Inicia a thread serial
t_serial = threading.Thread(target=serial_comms)
t_serial.daemon = True
t_serial.start()
print(f"[VIDEO] Conectando ao stream: {URL_STREAM}")
cap = cv2.VideoCapture(URL_STREAM)
# Subtrator de fundo para detectar movimento
fgbg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50, detectShadows=False)
while True:
ret, frame = cap.read()
if not ret:
print("[VIDEO] Falha ao capturar frame. Verifique o IP.")
time.sleep(2)
continue
altura, largura, _ = frame.shape
centro_x, centro_y = largura // 2, altura // 2
# --- PROCESSAMENTO DE IMAGEM ---
# Aplica mscara de movimento
mask = fgbg.apply(frame)
_, mask = cv2.threshold(mask, 200, 255, cv2.THRESH_BINARY)
# Limpeza de rudo (eroso e dilatao)
mask = cv2.erode(mask, None, iterations=2)
mask = cv2.dilate(mask, None, iterations=2)
contornos, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
telemetria["lock_visual"] = False
alvo_no_centro = False
for cnt in contornos:
area = cv2.contourArea(cnt)
# Filtra objetos pelo tamanho (mosquito vs ventilador vs pessoa)
if AREA_MINIMA < area < AREA_MAXIMA:
x, y, w, h = cv2.boundingRect(cnt)
# Desenha caixa ao redor do alvo
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)
# Verifica se o alvo est na "zona de tiro" (centro da tela)
zona_tiro_x = (centro_x - 50, centro_x + 50)
zona_tiro_y = (centro_y - 50, centro_y + 50)
centro_obj_x = x + w//2
centro_obj_y = y + h//2
if (zona_tiro_x[0] < centro_obj_x < zona_tiro_x[1]) and \
(zona_tiro_y[0] < centro_obj_y < zona_tiro_y[1]):
alvo_no_centro = True
telemetria["lock_visual"] = True
# --- LGICA DE DISPARO (FUSO DE SENSORES) ---
# Dispara SE: (Alvo Visual no Centro) E (Distncia Sonar < Limite)
dist = telemetria["distancia"]
if alvo_no_centro and (0 < dist <= DISTANCIA_TIRO):
telemetria["laser_ativo"] = True
else:
telemetria["laser_ativo"] = False
# --- RENDERIZAO ---
desenhar_hud(frame, largura, altura)
cv2.imshow("SKEETER-SCAN 16 - CONTROL STATION", frame)
# 'q' para sair
if cv2.waitKey(1) & 0xFF == ord('q'):
telemetria["sistema_ativo"] = False
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()








_3u05Tpwasz.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)
Comments