icon

UNIHIKER K10 Meets Harry Potter: Build an AI Magic Wand

0 2046 Medium

What if your everyday electronics could turn into a real magic wand? Imagine holding UNIHIKER K10—not just a development board, but a wand that responds to your every flick. With a twist of your wrist, lights glow, hats move, and spells come to life—just like Harry Potter casting charms at Hogwarts. Now, with UNIHIKER K10 and Edge Impulse, you can craft your own AI-powered wand and step into the wizarding world of technology.

Ā 

Ā 

The Magic Awakens: Project Background

Think of the iconic Hogwarts spells: "Lumos" to light up the darkness, "Alohomora" to unlock doors. In this project, we’ll recreate that sense of wonder using AI.

Your AI magic wand will be able to:

- Recognize different wand-waving gestures (circle, triangle, square, etc.)

- Trigger magical effects inside enchanted hats via wireless signals

Whether you’re upgrading your next costume party or simply experience the wonderful combination of AI and hardware, this project lets you experience how AI and hardware weave real magic together.

Magical Principles: Technology Every Muggle Can Master

At Hogwarts, every spell requires the right wand movement, a magical incantation, and a spark of energy. In our maker world, the magic comes from sensors, AI, and wireless signals—each mimicking a part of spellcasting:

Core Magical Components List

To begin your wizardry, gather these magical items:

Note: The ā€œmagic hatsā€ can be replaced with any relay-controlled appliance or device, allowing you to adapt the effect to your own wizarding setup.

HARDWARE LIST
1 Wand Core: UNIHIKER K10
2 Magic Receivers: UNIHIKER K10 (to be hidden in the two magic hats)
1 Lumos Materials: WS2812 LED strip
1 Activation Switch: Button
2 Mechanism Controller: Relay module
1 Magic Storage: 32GB TF card
1 Power Source: CR123A battery holder

Ā 

Magical Training Starts

Ā 

Step 1: Connect Your Wand to Hogwarts

Ā 

šŸ”® Spell: Incantatio Connectum

Ā 

Edge Impulse is like the "Hogwarts School" where we'll teach the wand to recognize different gesture spells. It is a publicly available AI model training platform that allows users to send data from a serial port to a PC and then use the edge impulse cli tool provided by Edge Impulse to forward the serial data to the Edge Impulse platform.

Ā 

1. Preparation:

- Sign up Edge Impulse

- Download Python3

- Download node.js V14 or higher

Ā 

2. Open up powershell as administrator.

Ā 

Ā 

3. Input "npm install -g edge-impulse-cli --force" to install the edge impulse cli tool.

Step 2: Infuse the Wand with Perception

šŸ”® Spell: Sensum Infusium

Now, your wand must feel movement. We'll enable the K10 to sense gestures and forward them to Edge Impulse.

1. Upload the data forward code to K10

- In order for the sensor data to be uploaded to the K10 for training, a data forwarding code needs to be uploaded to the K10.

CODE
#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. Forward the data to Edge Impulse

- Open up power shell, then input the following command to forward data from K10 to the Edge Impulse:

edge-impulse-data-forwarder --frequency 100

- Enter your EdgeImpulse account and go with a name for your magic wand, and finally give each of the five variables output in the above code a different variable name, here I've named it k,x,y,z,v.

Step 3: Teach the Wand Spells

šŸ”® Spell: Cognitio Gestorum

This is the most crucial step! Just as Harry practiced Expelliarmus countless times, your wand needs training to master gestures.

1. Login to your Edge Impulse account, and choose your data collecting device:

2. Most crucial step - data collection

- On the Edge Impulse platform, select the data acquisition section, fill in the label for the motion sensor data and select 2000ms for the sampling duration to start sampling.

- After clicking Start Sampling, you have 2 seconds to wave the K10 in your hand.

- You can wave the K10 to draw circles, triangles, squares and so on. And you need to make sure that you have the same label for the same type of action before waving.

- Strongly recommend that you collect enough data in both the Training and Test datasets. Edge Impulse will use the Training data for training and substitute the Test data into the model for validation.

- After collecting data, you can go to ā€œCreate impulseā€ to set the size and frequency of the eigenvalue acquisition window.

- The eigenvalues can be generated as illustrated in the following figure

- Next, you can enter the ā€œClassfierā€ for model training, you can set the number of training cycles, here I set 100 times, and then select the model version, float32 model will be slightly larger, but the accuracy will be improved a lot.

- Once the training is complete, you can see the accuracy of the trained model on the right side. This accuracy is verified by substituting the model using the test dataset we collected earlier.

- Once you are satisfied with the accuracy, we can export the model and deploy it.

- Click into Depolyment. Select Arduino library, TensorFlow Lite and then build. Finally, an Arduino library will be downloaded.

Step 4: Assemble the Wand and Hats

šŸ”® Spell: Assemblium Magicus

1. Wand Assembly

icon conv&depthwise.zip 27KB Download(3)

- After downloading the library, copy it to the libraries folder of Arduino IDE 1.8.19 and unzip it.

- Then copy the conv.cpp and depthwise_conv.cpp to the library in the following path

src→edge-impulse-sdk→tensorflow→lite→micro→kernels

- Then upload the following code to the magic wand. Because of the Edge Impulse model involved, this code takes a very long time to compile, about 40 minutes or so.

- The library files needed for the build are also attached below. Meanwhile, the Magic Wand K10's screen displays some pictures, which are also placed in the TrasmitterPic folder.

icon RMT&DFRobot_NeoPixel lib.zip 24KB Download(1)
icon TrasmitterPic.zip 11KB Download(1)
CODE
#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;

//Need change the MAC Address into your own K10 MAC Address
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; // ę›“ę–°äøŗå½“å‰ē±»åˆ«ļ¼ˆä»Ž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
}

- After uploading the code, please follow the pictures as shown to install the buttons and WS2812 LED strips.

- 3D print a wand shell for installing the K10. The wand's back cover can accommodate the CR123A battery holder and can be fitted with magnets for easy battery replacement.

icon WandCase.rar 4.72MB Download(7)

2. Hat Assembly

The HAT K10 accepts ESPNOW messages and drives the WS2812 LED strip as well as relays. The relays control the power to the servos. When power is applied, the servos start moving; when power is cut, they stop. If you purchased a hat servo that requires a PWM signal or other signal to drive it, you may need to modify the code below appropriately.

šŸ’” Tip: The servos in the hat I purchased start moving as soon as power is applied, creating a magical effect with minimal wiring. If such hats are hard to find in your region, you can build your own version using any small servo or motor.

CODE
#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;

//Need change the MAC Address in to your own K10 MAC Address
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);
}

- Connect the K10 to your relay module, and use COM/NA to power on and off your HAT motor.

The Final Ritual: Cast Your First Spell!

šŸ”® Spell: Incantatio Finalis

Once everything is ready, power up your wand and hat. Now, wave your magic wand, draw the trained gestures, and watch how the hat responds to your "spells"!

Photo by RDNE Stock project on Pexels

Advanced Magical Ideas

After mastering the basics, expand your spellbook:

- Add new gestures for fresh effects

- Customize your wand shell and hat designs

- Combine with voice recognition for gesture + voice dual casting

Congratulations, wizard! You’ve completed your training—turning ordinary components into extraordinary magical artifacts. But remember: every wizard has their own style. Don’t keep your magic to yourself—share your AI wand with us and inspire more wizards to join the adventure!

---

Github: https://github.com/YeezB/MagicWond

License
All Rights
Reserved
licensBg
0