Você já sonhou em ter uma varinha mágica capaz de reconhecer gestos? Como a varinha do Harry Potter.

Com o UNIHIKER K10, a plataforma Edge Impulse e alguns componentes eletrônicos, você pode criar uma varinha que reconhece gestos usando IA! Este projeto utiliza o acelerômetro embutido do K10 para treinar um modelo de machine learning e fazer inferências em tempo real, além de empregar o ESP NOW para comunicação entre múltiplos dispositivos UNIHIKER K10.
Prepare-se para mergulhar em um mundo onde gestos controlam a magia – ou, pelo menos, dispositivos inteligentes!

ℹ️ Antes de começarmos...
⚠️ Atenção: a plataforma Edge Impulse ainda não possui interface em português. Sugerimos utilizar extensões de tradução automática do navegador — como o tradutor integrado do Google Chrome
Passo 1: Preparando o ambiente com Edge Impulse
O Edge Impulse é uma plataforma gratuita para criar modelos de IA. Com ela, você pode capturar dados via porta serial, enviá-los para um computador e depois usar o Edge Impulse CLI para processá-los diretamente na plataforma.
Passos necessários no computador:
- Registrar-se no Edge Impulse – Crie uma conta gratuita em https://edgeimpulse.com/
- Instalar Python 3 – Baixe a versão mais recente em https://www.python.org/
- Instalar Node.js (v14 ou superior) – Disponível em https://nodejs.org/en
- Terminal PowerShell com permissões de administrador
1. Abra o PowerShell como administrador: Clique com o botão direito no ícone do PowerShell no Menu Iniciar. Selecione "Executar como administrador".
2. Execute o comando de instalação: npm install -g edge-impulse-cli --force
Passo 2: Enviando dados do K10 para o Edge Impulse
1. Para enviar os dados do sensor para o K10 para treinamento, é necessário enviar o código de reencaminhamento de dados para o K10.
#include "unihiker_k10.h"
volatile float mind_n_test;
UNIHIKER_K10 k10;
void setup() {
k10.begin();
Serial.begin(9600);
}
void loop() {
if ((k10.isGesture(TiltForward))) {
mind_n_test = 1;
}
else {
if ((k10.isGesture(TiltBack))) {
mind_n_test = 2;
}
else {
if ((k10.isGesture(TiltLeft))) {
mind_n_test = 3;
}
else {
if ((k10.isGesture(TiltRight))) {
mind_n_test = 4;
}
else {
if ((k10.isGesture(ScreenUp))) {
mind_n_test = 5;
}
else {
if ((k10.isGesture(ScreenDown))) {
mind_n_test = 6;
}
else {
mind_n_test = 0;
}
}
}
}
}
}
Serial.print(mind_n_test);
Serial.print(",");
Serial.print((k10.getAccelerometerX()));
Serial.print(",");
Serial.print((k10.getAccelerometerY()));
Serial.print(",");
Serial.print((k10.getAccelerometerZ()));
Serial.print(",");
Serial.println((k10.getStrength()));
delay(100);
}
2. No PowerShell, execute o seguinte comando para enviar dados do K10 para o Edge Impulse:
edge-impulse-data-forwarder --frequency 100

3. Insira sua conta EdgeImpulse e nomeie sua varinha mágica. Por fim, atribua nomes diferentes às cinco variáveis de saída do código acima. Aqui, eu as nomeei k, x, y, z e v.

📊 Passo 3: Coleta de dados e treinamento do modelo
1. Faça login na sua conta Edge Impulse e selecione o seu dispositivo de coleta de dados.

2. Selecione "Data acquisition", preencha a etiqueta dos dados do sensor de movimento, selecione o tempo de amostragem de 2000 ms e comece a amostragem. Após clicar em "Start sampling", você terá 2 segundos para agitar o K10 em suas mãos. Você pode agitar o K10 para desenhar círculos, triângulos, quadrados, etc. Antes de agitar, certifique-se de que o mesmo tipo de operação corresponda à mesma "Label".

Dica: É altamente recomendável coletar dados suficientes nos conjuntos de dados de treinamento e teste. O Edge Impulse usará os dados de treinamento para treinar e substituirá os dados de teste no modelo para validação.

Passo 4: Criando e treinando o modelo
1. Após coletar os dados, é possível acessar a janela de configuração "Create Impulse" para definir o tamanho e a frequência da coleta de valores característicos.

Os valores característicos podem ser gerados conforme mostrado na figura abaixo.


2. Em seguida, você pode entrar no "Classfier" para treinar o modelo. É possível definir o número de ciclos de treinamento. Aqui, defini 100 ciclos e selecionei a versão do modelo. O modelo float32 é um pouco maior, mas a precisão é muito maior.

3. Após a conclusão do treinamento, você poderá ver a precisão do modelo de treinamento no lado direito. Essa precisão é verificada substituindo o modelo pelo conjunto de dados de teste que coletamos anteriormente. Quando estiver satisfeito com a precisão, podemos exportar o modelo e implantá-lo. Clique em "Deployment". Selecione a "Arduino library", "TensorFlow Lite", em seguida, "Build". Uma biblioteca Arduino seria baixada.

Passo 5: Código
1. Depois de baixar o repositório, copie-o para a pasta libraries do Arduino IDE 1.8.19 e descompacte-o.
Copie os arquivos conv.cpp e depthwise_conv.cpp para:
src → edge-impulse-sdk → tensorflow → lite → micro → kernels

2. Carregue o código a seguir no Magic Wand. Devido ao modelo Edge Impulse, o tempo de compilação pode levar até 40 minutos.
👇 Acompanham os arquivos de biblioteca necessários para a compilação, bem como o material gráfico exibido na tela do Magic Wand K10.
#include <esp_now.h>
#include <WiFi.h>
#include "unihiker_k10.h"
#include <DFRobot_NeoPixel.h>
#include <magic-xhl_inferencing.h> //Need to change to your own library
UNIHIKER_K10 k10;
DFRobot_NeoPixel neoPixel_P1;
uint8_t screen_dir=2;
//MAC
uint8_t MAC1[] = {0x7C, 0xDF, 0xA1, 0xFE, 0xEF, 0xC4};//Magic wand mac address
uint8_t MAC0[] = {0x7C, 0xDF, 0xA1, 0xFD, 0x67, 0xB8};//First HAT mac address
uint8_t MAC2[] = {0x68, 0xB6, 0xB3, 0x22, 0x06, 0x34};//Second HAT mac address
typedef struct struct_message {
uint8_t ID;
char data[50];
} struct_message;
struct_message sendData;
struct_message recvData;
esp_now_peer_info_t peerInfo;
int x,y,z,v;
int mind_n_test = 0;
int i = 0;
int max_probability_class = 0;
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
if(status == ESP_NOW_SEND_SUCCESS){
Serial.print("Send Success to ");
Serial.println(macStr);
}else{
Serial.print("Send Fail to ");
Serial.println(macStr);
}
}
// Callback when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *Data, int len) {
memcpy(&recvData, Data, sizeof(recvData));
Serial.println("=========");
Serial.print("Bytes received: ");
Serial.println(len);
Serial.println(recvData.ID);
Serial.println(recvData.data);
Serial.println("---------");
}
static float features[100];
int featureIndex = 0;
int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) {
memcpy(out_ptr, features + offset, length * sizeof(float));
return 0;
}
void print_inference_result(ei_impulse_result_t result);
void setup()
{
Serial.begin(9600);
k10.begin();
k10.initScreen(screen_dir);
k10.creatCanvas();
k10.setScreenBackground(0xFFFFFF);
k10.rgb->write(-1, 0xFF0000);
k10.initSDFile();
k10.canvas->canvasDrawImage(0, 0, "S:/Fail.png");
k10.canvas->updateCanvas();
neoPixel_P1.begin(2, 7);
pinMode(1, INPUT);
WiFi.mode(WIFI_STA);
//Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing");
return;
}
//Register the send callback function
esp_now_register_send_cb(OnDataSent);
peerInfo.channel = 0;
peerInfo.encrypt = false;
//Register MAC0 devices
memcpy(peerInfo.peer_addr, MAC0, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer0");
return;
}
//Register MAC2 devices
memcpy(peerInfo.peer_addr, MAC2, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer0");
return;
}
//Register the receive callback function
esp_now_register_recv_cb(OnDataRecv);
sendData.ID = 1;
k10.rgb->write(-1, 0x000000);
}
void loop()
{
if (digitalRead(1)) {
Serial.println("====================");
k10.canvas->canvasDrawImage(0, 0, "S:/Effective.png");
k10.canvas->updateCanvas();
featureIndex = 0;
neoPixel_P1.setRangeColor(0, 13, 0xFF0000);
delay(100);
while (featureIndex < 100) {
if ((k10.isGesture(TiltForward))) {
mind_n_test = 1;
}
else {
if ((k10.isGesture(TiltBack))) {
mind_n_test = 2;
}
else {
if ((k10.isGesture(TiltLeft))) {
mind_n_test = 3;
}
else {
if ((k10.isGesture(TiltRight))) {
mind_n_test = 4;
}
else {
if ((k10.isGesture(ScreenUp))) {
mind_n_test = 5;
}
else {
if ((k10.isGesture(ScreenDown))) {
mind_n_test = 6;
}
else {
mind_n_test = 0;
}
}
}
}
}
}
// Store data in the features array
//Serial.print(features[featureIndex]);Serial.print(",");
features[featureIndex++] = mind_n_test;
//Serial.print(features[featureIndex]);Serial.print(",");
features[featureIndex++] = k10.getAccelerometerX();
//Serial.print(features[featureIndex]);Serial.print(",");
features[featureIndex++] = k10.getAccelerometerY();
//Serial.print(features[featureIndex]);Serial.print(",");
features[featureIndex++] = k10.getAccelerometerZ();
//Serial.print(features[featureIndex]);Serial.print(",");
features[featureIndex++] = k10.getStrength();
delay(100);
}
ei_printf("Edge Impulse standalone inferencing (Arduino)\n");
if (sizeof(features) / sizeof(float) != EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
ei_printf("The size of your 'features' array is not correct. Expected %lu items, but had %lu\n",
EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, sizeof(features) / sizeof(float));
delay(1000);
return;
}
ei_impulse_result_t result = { 0 };
signal_t features_signal;
features_signal.total_length = sizeof(features) / sizeof(features[0]);
features_signal.get_data = &raw_feature_get_data;
EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false /* debug */);
if (res != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", res);
return;
}
ei_printf("run_classifier returned: %d\r\n", res);
print_inference_result(result);
//neoPixel_P1.setRangeColor(0, 6, 0x00FF00);
// for (int index = 0; index < 7; index++) {
// neoPixel_P1.shift(1);
// delay(20);
// }
if(max_probability_class == 1){
strcpy(sendData.data, "Action_1");
for (int i = 0; i < 7; i++) {
neoPixel_P1.setRangeColor(i, i, 0x00FF00);
neoPixel_P1.setRangeColor(13-i, 13-i, 0x00FF00);
delay(200);
}
esp_now_send(0, (uint8_t *)&sendData, sizeof(sendData));
k10.canvas->canvasDrawImage(0, 0, "S:/Action_1.png");
Serial.println("esp_now_send");
}else if(max_probability_class == 2){
strcpy(sendData.data, "Action_2");
for (int i = 0; i < 7; i++) {
neoPixel_P1.setRangeColor(i, i, 0x00FF00);
neoPixel_P1.setRangeColor(13-i, 13-i, 0x00FF00);
delay(20);
}
esp_now_send(0, (uint8_t *)&sendData, sizeof(sendData));
k10.canvas->canvasDrawImage(0, 0, "S:/Action_2.png");
Serial.println("esp_now_send");
}else if(max_probability_class == 3){
strcpy(sendData.data, "Action_3");
for (int i = 0; i < 7; i++) {
neoPixel_P1.setRangeColor(i, i, 0x00FF00);
neoPixel_P1.setRangeColor(13-i, 13-i, 0x00FF00);
delay(20);
}
esp_now_send(0, (uint8_t *)&sendData, sizeof(sendData));
k10.canvas->canvasDrawImage(0, 0, "S:/Action_3.png");
Serial.println("esp_now_send");
}
k10.canvas->updateCanvas();
delay(2000);
k10.canvas->canvasDrawImage(0, 0, "S:/Fail.png");
k10.canvas->updateCanvas();
neoPixel_P1.clear();
}
}
void print_inference_result(ei_impulse_result_t result) {
ei_printf("Timing: DSP %d ms, inference %d ms, anomaly %d ms\r\n",
result.timing.dsp,
result.timing.classification,
result.timing.anomaly);
#if EI_CLASSIFIER_OBJECT_DETECTION == 1
ei_printf("Object detection bounding boxes:\r\n");
for (uint32_t i = 0; i < result.bounding_boxes_count; i++) {
ei_impulse_result_bounding_box_t bb = result.bounding_boxes[i];
if (bb.value == 0) {
continue;
}
ei_printf(" %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
bb.label,
bb.value,
bb.x,
bb.y,
bb.width,
bb.height);
}
#else
ei_printf("Predictions:\r\n");
float max_value = 0.0;
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
ei_printf(" %s: ", ei_classifier_inferencing_categories[i]);
ei_printf("%.5f\r\n", result.classification[i].value);
if (result.classification[i].value > max_value) {
max_value = result.classification[i].value;
max_probability_class = i + 1;
}
}
ei_printf("Max Probability Class: %d\n", max_probability_class);
#endif
#if EI_CLASSIFIER_HAS_ANOMALY
ei_printf("Anomaly prediction: %.3f\r\n", result.anomaly);
#endif
}
Após carregar o código, instale o botão e a fita de LED WS2812 conforme mostrado na imagem.

Passo 6: Código e montagem do “Chapéu Mágico” (receptor)
Outro K10 servirá como receptor de mensagens ESPNOW e acionará a faixa de luz LED WS2812 e o relé. O relé controla a alimentação do servidor, e o servidor que comprei no chapéu começa a funcionar assim que é ligado.
Dica: se você comprou um servidor de chapéu que precisa de sinal PWM ou outro sinal para funcionar, talvez seja necessário modificar o código abaixo adequadamente.
#include <esp_now.h>
#include <WiFi.h>
#include "unihiker_k10.h"
//On black hat and orange hat K10 upload need to open the corresponding macro definition, comment out another macro definition
#define blackHAT
//#define orangeHAT
UNIHIKER_K10 k10;
uint8_t screen_dir=2;
static int status = 0;
float startTime = 0;
float endTime = 0;
//MAC
uint8_t MAC1[] = {0x7C, 0xDF, 0xA1, 0xFE, 0xEF, 0xC4};//Magic Wand Mac Address
uint8_t MAC0[] = {0x7C, 0xDF, 0xA1, 0xFD, 0x67, 0xB8};//First hat Mac Address
uint8_t MAC2[] = {0x68, 0xB6, 0xB3, 0x22, 0x06, 0x34};//Orange hat Mac Address
typedef struct struct_message {
uint8_t ID;
char data[50];
} struct_message;
struct_message sendData;
struct_message recvData;
esp_now_peer_info_t peerInfo;
int recv_action;
// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
if(status == ESP_NOW_SEND_SUCCESS){
Serial.print("Send Success to ");
Serial.println(macStr);
}else{
Serial.print("Send Fail to ");
Serial.println(macStr);
}
}
/*
Callback when data is received
ACTION 1 means circle, first hat move
ACTION 2 means shake, both hat stop
ACTION 3 means triangle, second hat move
The two HAT's K10's need to be uploaded with different programs, the program switching is achieved by the #define macro definition at the beginning of that program
*/
void OnDataRecv(const uint8_t * mac, const uint8_t *Data, int len) {
memcpy(&recvData, Data, sizeof(recvData));
Serial.println("=========");
Serial.print("Bytes received: ");
Serial.println(len);
Serial.println(recvData.ID);
Serial.println(recvData.data);
if (String(recvData.data) == "Action_1") {
recv_action = 1;
k10.canvas->canvasText("Action_1", 0, 0, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
#ifdef blackHAT
if(status = 0)
{
digitalWrite(P0, HIGH);
status = 1;
}
else if(status = 1)
{
digitalWrite(P0, LOW);
delay(1000);
digitalWrite(P0, HIGH);
status = 1;
}
#endif
} else if (String(recvData.data) == "Action_2") {
recv_action = 2;
k10.canvas->canvasText("Action_2", 0, 0, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
digitalWrite(P0, LOW);
status = 0;
} else if (String(recvData.data) == "Action_3") {
recv_action = 3;
k10.canvas->canvasText("Action_3", 0, 0, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
#ifdef orangeHAT
if(status = 0)
{
digitalWrite(P0, HIGH);
status = 1;
}
else if(status = 1)
{
digitalWrite(P0, LOW);
delay(1000);
digitalWrite(P0, HIGH);
}
status = 0;
#endif
}
Serial.println(recv_action);
k10.canvas->updateCanvas();
Serial.println("---------");
}
void setup() {
Serial.begin(9600);
k10.begin();
pinMode(P0, OUTPUT);
k10.initScreen(screen_dir);
k10.creatCanvas();
k10.setScreenBackground(0xFFFFFF);
k10.rgb->write(-1, 0xFF0000);
WiFi.mode(WIFI_STA);
//Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing");
return;
}
esp_now_register_send_cb(OnDataSent);
peerInfo.channel = 0;
peerInfo.encrypt = false;
memcpy(peerInfo.peer_addr, MAC1, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer0");
return;
}
//注册接收回调函数
esp_now_register_recv_cb(OnDataRecv);
k10.rgb->write(-1, 0x000000);
delay(3000);
digitalWrite(P0, HIGH);
delay(1000);
digitalWrite(P0, LOW);
}
void loop() {
if(digitalRead(P0) == HIGH)
{
endTime = millis();
if(endTime - startTime >= 15000)
{
startTime = endTime;
delay(500);
digitalWrite(P0, LOW);
delay(1000);
digitalWrite(P0, HIGH);
}
}
else if(digitalRead(P0) == LOW)
{
startTime = millis();
}
Serial.print("Start Time: ");
Serial.println(startTime);
Serial.print("End Time: ");
Serial.println(endTime);
}
Conecte o K10 ao seu módulo relé e use COM/NA para ligar e desligar o motor HAT.

Passo 7: Imprima o corpo da varinha em 3D
Imprima em 3D um dispositivo varinha mágica para instalar o K10. A parte traseira do dispositivo varinha mágica está equipada com uma tampa que pode ser usada para instalar uma caixa de bateria CR123A. A tampa pode ser equipada com ímãs para ativar a função de magnetização do compartimento da bateria.
Passo 8 Ligue os chapéus e a varinha e, em seguida, agite a varinha.
Pressione o botão na Varinha Mágica e desenhe um triângulo em dois segundos. Quando a Varinha Mágica reconhecer o triângulo - movimento de desenho. A faixa de luz fica verde e o Chapéu Mágico balança de um lado para o outro.
Se você balançar a Varinha Mágica horizontalmente, o Chapéu Mágico vai parar de balançar.
Quer ajuda para adaptar esse projeto? Tem dúvidas na instalação?
💬 Entre no grupo do WhatsApp — estamos sempre à disposição para ajudá-lo!
