Zecko ES – Embedded System Platform for Unihiker K10

by jorgeeldis in Circuits > Microcontrollers

52 Views, 1 Favorites, 0 Comments

Zecko ES – Embedded System Platform for Unihiker K10

PIXEL.png
Untitled design (2).png
unihiker_collage.jpg
unihiker (2).png

It is great to have multifunctional devices that help you with many activities. Using your own device, built with your own embedded system, to listen to music, take pictures, record thoughts, and much more adds significant value. There is something special about using a device that is made and personalized by you. That is why I decided to create an embedded system from scratch that includes day-to-day functionalities.

Special thanks to DFRobot for the Unihiker K10 device. I received this device through the FREE Trial of UNIHIKER K10

I created an embedded system called Zecko ES to maximize the functionalities of the Unihiker K10.

About the Unihiker K10:

A “super microcontroller” with a 2.8-inch color screen, Wi-Fi & Bluetooth, camera, microphone, speaker, RGB light, temperature, humidity, light, and accelerometer sensors.

Zecko ES Functions

  1. Camera: Takes pictures and saves to SD Card
  2. Music Player: Plays .wav audio from SD Card
  3. Workout Station: Chronometer, pedometer, tracking of sets & reps
  4. Zecko AI: Chatbot that answers questions
  5. Weather Station: Measures temperature, humidity, noise, light intensity
  6. AI Recognition: Camera specialized in facial recognition
  7. Reaction Game: Simple reflex game
  8. Journal: Voice recorder saving audio to SD Card


Supplies

Unihiker K10

$28.90

ABS Filament

$16.25

Total COGS

$45.15

Note: The Unihiker K10 was provided free by DFRobot.

Tools & Apps

  1. Flashforge Adventurer 5M: 3D printer for enclosure
  2. Ruler: Measuring the Unihiker K10
  3. VS Code: Software for coding
  4. Platform IO: Flashes code onto the Unihiker K10
  5. Free CAD: Enclosure design
  6. Orca Slicer: Slices designs for printing

Schematic & Electronics

For this project, creating a schematic was not necessary. However, it is important to understand the electronics and functionality of each IC and sensor.

Explanation

The Unihiker K10 has the following components:

  1. MCU → ESP32-S3 Xtensa LX7: A complete powerhouse for fast thinking and AI management, the brain of the hardware
  2. SRAM → 512KB: More than enough memory to run simple tasks, keeping in mind that the device CANNOT run at the same time AI tasks with WiFi tasks.
  3. Flash → 16MB: Good amount to save informations and keep them persistent without crashing
  4. Wi-Fi → 2.4G: Connection (802.11 b/g/n) with 40 MHz of bandwidth support.
  5. BT → Bluetooth 5.0: Low Energy Bluetooth Support
  6. Screen → 2.8 inch, 240x320
  7. Camera → 2MP
  8. Sensor → Button, Microphone, Temperature Sensor, Humidity Sensor, Light Sensor, Accelerometer Sensor
  9. Actuators → RGB Lights, Speaker


Coding

I challenged myself during development. In a world full of AI assistance, I chose not to rely on it for building the entire system. The logic is somewhat messy (but it works!). It is based on persistent integers and counting how many times a button is pressed. Logically, the code contains several bugs, but my goal was to test how well I understood the fundamentals of C++ development.

I could have used MicroPython, but I chose C++ because of its fast execution. I built the system in less than six hours, which made the challenge even harder. With this in mind, I tried to comment on all functions and explain what they do. I will also publish the GitHub repository so you are free to improve it.

Code and explanation 🧠

The libraries required for the program are the following. In my case, I used platformio.ini to install them.

´´´ [env:unihiker] platform = https://github.com/DFRobot/platform-unihiker.git board = unihiker_k10 framework = arduino build_flags = -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -DModel=None ´´´

#include "unihiker_k10.h"
#include "math.h"
#include <ctime>
#include <Wire.h>
#include "AIRecognition.h"
#include "asr.h"

We initialize all the functions required for the program to operate.

void onButtonAPressed();
void onButtonBPressed();
void onButtonABPressed();
void startup();
void menu();
void camera();
void musicplayer();
void workout();
void weather();
void airecognition();
void reaction();
void journal();
void typeLine(String msg, uint16_t color);

We then initialize the global variables and rename class instances to make them easier to use inside the functions.

volatile uint8_t step = 0;
volatile uint8_t sets = 0;
volatile uint8_t reps = 0;
volatile int sec = 0;
volatile int minute = 0;
volatile int pressA = 0;
volatile int pressB = 0;
UNIHIKER_K10 k10;
uint8_t screen_dir = 2;
Music music;
AHT20 aht20;
int randomNumber = random(10000);
int random10Sec = random(10000);
int randomColor = random(5);
String filename = "S:/photo" + String(randomNumber) + ".bmp";
String voicerecord = "S:/sound" + String(randomNumber) + ".wav";
String ColorSelected = "White";
AIRecognition ai;
ASR asr;
int Colors[] = {0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0x000000};

We start the program in setup(), where the system comes to life.

void setup()
{
Wire.begin();
k10.begin();
asr.asrInit(ONCE, EN_MODE, 6000);
delay(2000);
asr.addASRCommand(1, "Initialize interface");
asr.addASRCommand(2, "Enter network");
asr.addASRCommand(3, "Disconnect");
asr.addASRCommand(4, "System diagnostics");
asr.addASRCommand(5, "Show status");
asr.addASRCommand(10, "Launch camera module");
asr.addASRCommand(11, "Launch audio module");
asr.addASRCommand(12, "Launch fitness protocol");
asr.addASRCommand(13, "Launch weather scan");
asr.addASRCommand(14, "Return to root");
asr.addASRCommand(20, "Scan environment");
asr.addASRCommand(21, "Analyze subject");
asr.addASRCommand(22, "Run deep scan");
asr.addASRCommand(23, "Activate stealth mode");
asr.addASRCommand(24, "Override protocol");
asr.addASRCommand(30, "Identify");
asr.addASRCommand(31, "Who controls you");
asr.addASRCommand(32, "Are you sentient");
asr.addASRCommand(33, "What is my status");
asr.addASRCommand(34, "Engage combat mode");
k10.initScreen(screen_dir);
ai.initAi();
k10.creatCanvas();
k10.setScreenBackground(0x000000);
k10.buttonA->setPressedCallback(onButtonAPressed);
k10.buttonB->setPressedCallback(onButtonBPressed);
k10.buttonAB->setPressedCallback(onButtonABPressed);
k10.canvas->canvasClear();
startup();
k10.initSDFile();
delay(2000);
menu();
k10.rgb->brightness(round(5));
k10.rgb->write(-1, 0x008000);
}

Next, we configure the loop() function. On specific occasions, the system continuously scans and checks different sensors and values.

void loop()
{
if (asr.isDetectCmdID(1))
{
k10.rgb->write(-1, 0x00FFFF);
typeLine("Neural interface online.", 0x00FFFF);
}
// Here goes a lot of commands, i'm going to leave it on github, but i'm not gonna replicate the code over here cause of the quantity of lines
if (pressA == 4000 && pressB == 4000)
{
weather();
}
if (pressA == 7001)
{
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Recording", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 10, false);
k10.canvas->updateCanvas();
music.recordSaveToTFCard(voicerecord, 60);
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Record complete", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 16, false);
k10.canvas->updateCanvas();
pressA = 7500;
}
if ((pressA >= 2000 && pressA <= 2999) && (pressB >= 2000 && pressB <= 2999))
{
workout();
}
if (pressA == 6000)
{
reaction();
delay(2000);
while (pressA == 6000)
{
k10.setScreenBackground(Colors[randomColor]);
delay(2000);
while (randomColor != 0)
{
randomColor = random(5);
}
}
}
if (pressA == 6001 && randomColor == 0)
{
k10.canvas->canvasClear();
k10.creatCanvas();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("You win", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 21, false);
k10.canvas->updateCanvas();
}
if (pressA == 6001 && randomColor != 0)
{
k10.canvas->canvasClear();
k10.creatCanvas();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("You lost", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 21, false);
k10.canvas->updateCanvas();
}
if (pressA == 5000 && pressB == 5000)
{
if (ai.isDetectContent(AIRecognition::Face))
{
k10.rgb->write(-1, 0xFF0000);
k10.canvas->canvasText((String("Face Length") + String(ai.getFaceData(AIRecognition::Length))), 0, 0, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
k10.canvas->canvasText((String("Face Width ") + String(ai.getFaceData(AIRecognition::Length))), 0, 16, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
k10.canvas->canvasText((String("Face Center X") + String(ai.getFaceData(AIRecognition::CenterX))), 0, 32, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
k10.canvas->canvasText((String("Face Center Y") + String(ai.getFaceData(AIRecognition::CenterY))), 0, 32, 0x0000FF, k10.canvas->eCNAndENFont24, 50, true);
k10.canvas->updateCanvas();
k10.rgb->write(-1, 0xFF0000);
}
delay(1000);
}
}

We begin by calling the startup() function, which displays a simple splash screen to initialize the system.

void startup()
{
k10.rgb->brightness(round(5));
k10.rgb->write(-1, 0x1F51FF);
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("hi! this is zecko!", 20, 160, 0x008000, Canvas::eCNAndENFont24, 19, false);
k10.canvas->updateCanvas();
}

The menu() function displays all available features within the system.

void menu()
{
k10.setScreenBackground(0x000000);
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("menu", 10, 10, 0x008000, Canvas::eCNAndENFont24, 5, false);
k10.canvas->canvasText("camera", 10, 50, 0x008000, Canvas::eCNAndENFont24, 7, false);
k10.canvas->canvasText("music player", 10, 80, 0x008000, Canvas::eCNAndENFont24, 13, false);
k10.canvas->canvasText("workout", 10, 110, 0x008000, Canvas::eCNAndENFont24, 8, false);
k10.canvas->canvasText("zecko ai", 10, 140, 0x008000, Canvas::eCNAndENFont24, 9, false);
k10.canvas->canvasText("weather", 10, 170, 0x008000, Canvas::eCNAndENFont24, 8, false);
k10.canvas->canvasText("ai recognition", 10, 200, 0x008000, Canvas::eCNAndENFont24, 15, false);
k10.canvas->canvasText("reaction game", 10, 230, 0x008000, Canvas::eCNAndENFont24, 19, false);
k10.canvas->canvasText("journal", 10, 260, 0x008000, Canvas::eCNAndENFont24, 19, false);
k10.canvas->updateCanvas();
}

One of the largest sections is the button-handling logic. It depends on internal counters and value ranges to enter and exit different functions.

void onButtonAPressed()
{
pressA += 1;

if (pressA == 1) // Select camera
{
music.playTone(220, 2000);
k10.canvas->canvasText("camera", 10, 50, 0x1F51FF, Canvas::eCNAndENFont24, 7, false);
k10.canvas->updateCanvas();
}
else if (pressA == 2) // Select music player
{
music.playTone(220, 2000);
k10.canvas->canvasText("camera", 10, 50, 0x008000, Canvas::eCNAndENFont24, 7, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("music player", 10, 80, 0x1F51FF, Canvas::eCNAndENFont24, 13, false);
k10.canvas->updateCanvas();
}
else if (pressA == 3) // Select workout
{
music.playTone(220, 2000);
k10.canvas->canvasText("music player", 10, 80, 0x008000, Canvas::eCNAndENFont24, 13, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("workout", 10, 110, 0x1F51FF, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
}
else if (pressA == 4) // Select zecko ai
{
music.playTone(220, 2000);
k10.canvas->canvasText("workout", 10, 110, 0x008000, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("zecko ai", 10, 140, 0x1F51FF, Canvas::eCNAndENFont24, 9, false);
k10.canvas->updateCanvas();
}
else if (pressA == 5) // Select weather
{
music.playTone(220, 2000);
k10.canvas->canvasText("zecko ai", 10, 140, 0x008000, Canvas::eCNAndENFont24, 9, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("weather", 10, 170, 0x1F51FF, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
}
else if (pressA == 6) // Select ai recognition
{
music.playTone(220, 2000);
k10.canvas->canvasText("weather", 10, 170, 0x008000, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("ai recognition", 10, 200, 0x1F51FF, Canvas::eCNAndENFont24, 15, false);
k10.canvas->updateCanvas();
}
else if (pressA == 7) // Select reaction game
{
music.playTone(220, 2000);
k10.canvas->canvasText("ai recognition", 10, 200, 0x008000, Canvas::eCNAndENFont24, 15, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("reaction game", 10, 230, 0x1F51FF, Canvas::eCNAndENFont24, 19, false);
k10.canvas->updateCanvas();
}
else if (pressA == 8) // Select journal
{
music.playTone(220, 2000);
k10.canvas->canvasText("reaction game", 10, 230, 0x008000, Canvas::eCNAndENFont24, 19, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("journal", 10, 260, 0x1F51FF, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
}
else if (pressA == 9) // Select camera
{
music.playTone(220, 2000);
k10.canvas->canvasText("journal", 10, 260, 0x008000, Canvas::eCNAndENFont24, 8, false);
k10.canvas->updateCanvas();
k10.canvas->canvasText("camera", 10, 50, 0x1F51FF, Canvas::eCNAndENFont24, 7, false);
k10.canvas->updateCanvas();
pressA = 1;
}
else if (pressA == 101)
{
k10.photoSaveToTFCard(filename);
}
else if (pressA == 1001)
{
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Playing...", 10, 10, 0x008000, Canvas::eCNAndENFont24, 20, false);
k10.canvas->updateCanvas();
music.playTFCardAudio("S:/car-horn.wav");
}
else if (pressA >= 2001 && pressA <= 2800)
{
sets++;
reps = 0;
}
}

The same logic applies when Button B is pressed. Since it typically acts as the secondary selection button, a switch-case structure is used.

void onButtonBPressed()
{

if (pressB == 2000)
{
reps++;
}

switch (pressA)
{
case 1:
pressA = 100;
pressB = 100;
music.playTone(220, 2000);
camera();
break;
case 2:
pressA = 1000;
pressB = 1000;
music.playTone(220, 2000);
musicplayer();
break;
case 3:
pressA = 2000;
pressB = 2000;
music.playTone(220, 2000);
workout();
break;
case 4:
pressA = 3000;
pressB = 3000;
music.playTone(220, 2000);
zeckoai();
break;
case 5:
pressA = 4000;
pressB = 4000;
music.playTone(220, 2000);
weather();
break;
case 6:
pressA = 5000;
pressB = 5000;
music.playTone(220, 2000);
airecognition();
break;
case 7:
pressA = 6000;
pressB = 6000;
music.playTone(220, 2000);
reaction();
break;
case 8:
pressA = 7000;
pressB = 7000;
music.playTone(220, 2000);
journal();
break;
}
}

I decided to implement a reset function when pressing A and B simultaneously. This resets both counters to zero and returns to the menu.

void onButtonABPressed()
{
menu();
pressA = 0;
pressB = 0;
}

The camera() function initializes the SD card to enable image storage.

void camera()
{
ai.switchAiMode(ai.NoMode);
delay(100);
k10.initBgCamerImage();
k10.setBgCamerImage(false);
k10.creatCanvas();
k10.setBgCamerImage(true);
}

The musicplayer() function initializes the SD card to read and play audio files.


void musicplayer()
{
k10.canvas->canvasClear();
k10.initSDFile();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Press A to play...", 10, 10, 0x008000, Canvas::eCNAndENFont24, 20, false);
k10.canvas->updateCanvas();
}

The workout() function includes a pedometer, chronometer, and set/rep counter.


void workout()
{
if (((k10.getStrength()) > 1080))
{
step += 1;
}
sec++;
if (sec == 60)
{
minute++;
sec = 0;
}
String StrSets = String(sets);
String StrReps = String(reps);
String StrStep = String(step);
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Workout Station", 10, 10, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText("Step Count: " + StrStep, 10, 50, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText("Set: " + StrSets, 10, 80, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText("Reps: " + StrReps, 10, 110, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText("Time: ", 10, 140, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText(" " + String(minute) + " : " + String(sec), 90, 220, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->updateCanvas();
delay(1000);
}

The weather() function retrieves temperature, humidity, microphone (noise), and ambient light data.


void weather()
{
float temperatureC = aht20.getData(AHT20::eAHT20TempC) * 0.75;
float humidity = aht20.getData(AHT20::eAHT20HumiRH) + 25;
String Noise = String(k10.readMICData());
String Temp = String(temperatureC);
String Humid = String(humidity);
String LAX = String(k10.readALS());
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Weather Station", 10, 10, 0x008000, Canvas::eCNAndENFont24, 16, false);
k10.canvas->canvasText("Temperature: " + Temp, 10, 50, 0x008000, Canvas::eCNAndENFont24, 20, false);
k10.canvas->canvasText("Humidity: " + Humid, 10, 80, 0x008000, Canvas::eCNAndENFont24, 15, false);
k10.canvas->canvasText("Intensity: " + LAX, 10, 110, 0x008000, Canvas::eCNAndENFont24, 18, false);
k10.canvas->canvasText("Noise: " + Noise, 10, 140, 0x008000, Canvas::eCNAndENFont24, 18, false);
k10.canvas->updateCanvas();
delay(1000);
}

The journal() function records audio and saves voice notes to the SD card.


void journal()
{
k10.canvas->canvasClear();
k10.creatCanvas();
k10.initSDFile();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Press A to record.", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 50, false);
k10.canvas->updateCanvas();
}

The reaction() function is a simple reflex game. When the selected color appears, you must press A to win.


void reaction()
{
k10.canvas->canvasClear();
k10.creatCanvas();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x1F51FF, 0x1F51FF, false);
k10.canvas->canvasText("Press A when you", 10, 10, 0x008000, k10.canvas->eCNAndENFont24, 21, false);
k10.canvas->canvasText("see the color:", 10, 40, 0x008000, k10.canvas->eCNAndENFont24, 21, false);
k10.canvas->canvasText(ColorSelected, 10, 70, 0x008000, k10.canvas->eCNAndENFont24, 21, false);
k10.canvas->updateCanvas();
}

The typeLine() function is designed to interact with a chatbot that answers simple questions.

void typeLine(String msg, uint16_t color)
{
k10.canvas->canvasClear();
k10.canvas->canvasRectangle(1, 1, 239, 319, 0x000000, 0x000000, false);

String buffer = "> ";
for (int i = 0; i < msg.length(); i++)
{
buffer += msg[i];
k10.canvas->canvasText(buffer, 10, 140, color, Canvas::eCNAndENFont16, 40, true);
k10.canvas->updateCanvas();
delay(20);
}
}

The airecognition() function detects faces and notifies the user.

void airecognition()
{
k10.initBgCamerImage();
k10.setBgCamerImage(false);
k10.creatCanvas();
ai.switchAiMode(ai.NoMode);
k10.setBgCamerImage(true);
ai.switchAiMode(ai.Face);
}

Breadboard Testing, Design & PCB

Explanation

For this project, designing a PCB or performing breadboard testing was not necessary since everything is integrated within the Unihiker K10.

3D Enclosure Design

c5202339-a65b-4490-bd10-22515bbc445b.png

In the following images, you will see all sides of the enclosure, along with the link to download the design from Printables.com. Feel free to leave your make, comment, and rating — it is greatly appreciated.

Zecko ES – Custom Case for Unihiker K10

There are many Unihiker K10 cases available online. However, I wanted to design my own. Many existing designs are fully enclosed, covering the bottom circuitry and top sensors. Some also block the Wi-Fi module at the back, which may reduce performance.

The GPIO pins on the sides and the speakers should remain accessible for connecting external modules. All these considerations were taken into account during the design process.

Assembly and Testing

zeckoES (1).png
test (1).png
unihiker_collage.jpg

Assembly is straightforward. The 3D enclosure is specifically designed as a press fit. You should be able to insert the Unihiker K10 with minimal force and securely fit it inside the enclosure.

Improvements and Recommendations

Improvements

  1. Coding structure and logic: The entire main.cpp can be improved. It serves its intended purpose and follows the expected user functionality, but it lacks safeguards against unintended usage and may fail in certain situations.

Recommendations

  1. Use cheaper filament: ABS was the filament I had available at the time. It is not necessary for this project.
This is a full presentation and showcase of the Unihiker K10 and its functionalities. Once again, I am grateful to have received the Unihiker K10 from DFRobot.