Maker.io main logo

Light Meter Project - Making Dfrobot Unihiker K10 Work with Standard Ard

2025-08-25 | By Mirko Pavleski

License: General Public License Light Arduino ESP32 micro:bit

The other day, I received a shipment with a UNIHIKER K10 development board from DFRobot, which I received as a prize. The device is excellently packaged in a branded box, protected from damage during transportation.

gf

I immediately asked for information because I have no experience with this development board so far. In fact, the device is based on an ESP32-S3 Xtensa LX7 microcontroller and an ILI9341 TFT display with a resolution of 320x240 pixels. The display is protected by a metal frame, and right above it are a light sensor, a temperature and humidity sensor, as well as two microphones for noise reduction. There are two buttons on the side and an edge connector at the bottom for connecting additional modules and sensors. On the back, there is a camera, three I/O connectors, a micro SD slot, a speaker, three RGB LEDs, an accelerometer, and a battery interface.

xdg

First, I made a 3D printed case according to the .stl file provided on the manufacturer's website. The case fits perfectly on the back of the PCB and protects the sensitive components. There are openings for the camera, I/O connectors, USB port, and the buttons are functional.

fgh

After successful boot-up, K10 runs the factory-built-in programs: Face Detection, voice recognition, Sensor mode, and "View Tutorial" mode, which generates the QR code, and, using your cell phone, you can view the tutorial of K10. By pressing the B button, we move through the menu.

 

As for software, the supported environments are MindPlus, Arduino IDE, and MicroPython. Basically, the K10 is designed as a device for exploring: AI (artificial intelligence), IoT, and Python Coding. Even all the support from internet groups and communities relates to these areas. My previous experience has only been with Arduino IDE, so I was pleasantly surprised to read that this board supports it. However, I quickly learned that Arduino IDE support is limited to a single library, "unihiker_k10.h".

zxc

I even contacted the manufacturer regarding support for other standard Arduino libraries, but the answer was that the SDK for Arduino IDE is under development, and is currently recommended to be used only in conjunction with MindPlus.

So I decided to do some research myself and try to install some kind of program using standard libraries. Actually, it's an ESP32 MCU and an ILI9341 driver-based TFT Display, so it seemed logical to me to start with a project that would use some known library, so I chose Bodmer's TFT_eSPI.

First, I downloaded and carefully studied the Unihiker K10 schematic and the way to connect the microcontroller to the Display.

gh

Based on the data from the schematic, I modified User_Setup.h to match the specific connection. Next, I installed support in Arduino IDE for this development board as follows:

We go to File-> Preferences-> Additional boards manager, and enter the given link:

https://downloadcd.dfrobot.com.cn/UNIHIKER/package_unihiker_index.json

kl

Then, in Tools-> Boards Manager, we write Unihiker, and we install the support for Unihiker from DFRobot.

kl

Now we go to Tools -> Unihiker -> Unihiker K10.

hij

With this, the support for this development board in the Arduino IDE is installed.

Now comes the interesting part, which is testing. Of course, to start with, I tried installing one of the examples included in this library, specifically an analog meter. The example installed successfully without errors, but unfortunately, after rebooting, the display remained black. I went back to the schematic again and discovered that the backlight of the display is controlled by a transistor whose base is connected to a special pin on the I/O expansion chip and not directly to the microcontroller, which theoretically means that there is no chance to programmatically turn on the backlight of the display.

jkn

I could probably do it in hardware by directly connecting the ground of the LED pin, but that would be difficult to do, and there was also a chance of damaging the board. I came to the solution by accident, and in the following way:

First, the factory firmware is installed on the board according to the instructions provided on the site. Then, without disconnecting the board from the USB, the indicated example from the TFT-ESPI library is installed, and after uploading, a beautiful analog meter appears on the screen.

fdtg

I assume that the same procedure works on other display libraries such as Adafruit ILI9341.

After this, it was logical to try to activate some sensor from the board, and its values would be displayed on the analog meter. I decided on the LTR303 light sensor located just above the display.

kl

In this case, I use the Adafruit library designed for this type of sensor. I took most of the program from the example in the library, which concerns drawing and moving the needle of the analog meter. The other part uses the Adafruit library to read the values ​​received by the sensor. To my surprise, this library also functions quite normally on the K10 development board.

The device is very practical and displays several parameters. The main part is the analog meter, and it displays the relative light intensity received by the sensor on a scale from 0 to 100.

bnjk

This value is numerically displayed on the lower left part of the instrument. Below the instrument are two important values: the raw value provided by the sensor and the range in which the instrument is currently operating. The value of the light intensity can be easily expressed in Lux units using a simple formula provided by the sensor manufacturer.

j

The real raw values for the intensity of light from complete darkness to direct sunlight range from 0 to several tens of thousands, so if the entire range were displayed on this scale, the resolution would be extremely low. Therefore, for greater precision and flexibility of the device, I introduced auto-range min-max values, which are based on the light intensity at the moment the device is turned on. If the intensity at a certain moment exceeds the limits of the set range, it will automatically change. Let's see how it looks in reality.

UPDATE:

In the meantime, I found a solution to the problem with the backlight on the display. I also included the library "unihiker_k10.h" and initialized the backlight in the setup. So now the program just needs to be installed on the board, regardless of which program was previously installed. Also, now, after turning off the board from the power supply and turning it back on, the program is activated, along with the backlight turned on.

nmn

And finally, a short conclusion. This project demonstrates that despite limited official Arduino support, the Unihiker K10's ESP32-S3 foundation allows determined makers to leverage standard Arduino libraries for creative projects. The successful integration of TFT_eSPI and Adafruit sensor libraries proves this AI-focused board has untapped potential for traditional microcontroller applications.

Copy Code
/*UNIHIKER K10 Light Meter
by mircemk, August 2025
*/

#include <TFT_eSPI.h>
#include <SPI.h>
#include "Adafruit_LTR329_LTR303.h"
#include "unihiker_k10.h"

#define M_SIZE 1.3333
TFT_eSPI tft = TFT_eSPI();
#define TFT_GREY 0x5AEB
#define TFT_LIGHTPINK 0xFDB8
#define TFT_GOLD 0xFEA0
#define TFT_LIGHTGREEN 0x9772
#define TFT_LIGHTSALMON 0xFD0F
#define INFO_PANEL_HEIGHT M_SIZE*30
#define FOOTER_HEIGHT M_SIZE*20  // Height for the version footer

UNIHIKER_K10 k10;
uint8_t      screen_dir=2;

// Light sensor
Adafruit_LTR303 ltr;

// Meter variables
float ltx = 0;
uint16_t osx = M_SIZE*120, osy = M_SIZE*120;
uint32_t updateTime = 0;
int old_analog = -999;

// Light sensor variables
uint16_t minLight = 0;
uint16_t maxLight = 1000;
uint32_t lastSensorRead = 0;
const uint32_t SENSOR_READ_INTERVAL = 100;

void setup(void) {
  Serial.begin(115200);

      k10.begin();
    k10.initScreen(screen_dir);
  
  if (!ltr.begin()) {
    Serial.println("Couldn't find LTR sensor!");
    while (1) delay(10);
  }
  
  ltr.setGain(LTR3XX_GAIN_1);
  ltr.setIntegrationTime(LTR3XX_INTEGTIME_50);
  ltr.setMeasurementRate(LTR3XX_MEASRATE_50);
  
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  analogMeter();
  drawInfoPanel(0);
  drawFooter();
  updateTime = millis();
}

void loop() {
  uint16_t visible_plus_ir, infrared;
  bool valid;
  
  if (millis() - lastSensorRead > SENSOR_READ_INTERVAL) {
    lastSensorRead = millis();
    
    if (ltr.newDataAvailable()) {
      valid = ltr.readBothChannels(visible_plus_ir, infrared);
      if (valid) {
        if (visible_plus_ir < minLight) minLight = visible_plus_ir;
        if (visible_plus_ir > maxLight) maxLight = visible_plus_ir;
        
        int mappedValue = map(visible_plus_ir, minLight, maxLight, 0, 100);
        mappedValue = constrain(mappedValue, 0, 100);
        
        plotNeedle(mappedValue, 10);
        drawInfoPanel(visible_plus_ir);
      }
    }
  }
}

void drawInfoPanel(uint16_t lightValue) {
  // Clear and draw panel
  tft.fillRect(0, M_SIZE*126, M_SIZE*239, INFO_PANEL_HEIGHT, TFT_YELLOW);
  tft.drawRect(0, M_SIZE*126, M_SIZE*239, INFO_PANEL_HEIGHT, TFT_BLACK);
  
  // Set larger font (Font 4)
  tft.setTextColor(TFT_BLACK, TFT_YELLOW);
  tft.setTextFont(4); // Larger font
  
  // Format and display text
  char infoText[40];
  snprintf(infoText, sizeof(infoText), "RAW:%-5d  RNG:%d-%d", 
           lightValue, minLight, maxLight);
  
  // Center the text in the panel
  int16_t textWidth = tft.textWidth(infoText, 4);
  int16_t xPos = (M_SIZE*239 - textWidth) / 2;
  tft.setCursor(xPos, M_SIZE*133);
  tft.print(infoText);
}

void drawFooter() {
  // Sky blue footer
  tft.fillRect(0, M_SIZE*126 + INFO_PANEL_HEIGHT, M_SIZE*239, FOOTER_HEIGHT, TFT_CYAN);
  
  // Centered version text
  tft.setTextColor(TFT_BLACK, TFT_CYAN);
  tft.setTextFont(2);  // Medium-sized font
  
  const char* versionText = "Light Intensity Meter V1.0 by mircemk";
  int16_t textWidth = tft.textWidth(versionText, 2);
  int16_t xPos = (M_SIZE*239 - textWidth) / 2;
  int16_t yPos = M_SIZE*126 + INFO_PANEL_HEIGHT + (FOOTER_HEIGHT/2 - 8);  // Vertically centered
  
  tft.setCursor(xPos, yPos);
  tft.print(versionText);
}



// #########################################################################
//  Draw the analogue meter on the screen
// #########################################################################
void analogMeter() {
  // Meter outline
  tft.fillRect(0, 0, M_SIZE*239, M_SIZE*126, TFT_LIGHTSALMON);
  tft.fillRect(5, 3, M_SIZE*230, M_SIZE*119, TFT_WHITE);

  tft.setTextColor(TFT_BLACK);  // Text colour

  // Draw ticks every 5 degrees from -50 to +50 degrees (100 deg. FSD swing)
  for (int i = -50; i < 51; i += 5) {
    // Long scale tick length
    int tl = 15;

    // Coodinates of tick to draw
    float sx = cos((i - 90) * 0.0174532925);
    float sy = sin((i - 90) * 0.0174532925);
    uint16_t x0 = sx * (M_SIZE*100 + tl) + M_SIZE*120;
    uint16_t y0 = sy * (M_SIZE*100 + tl) + M_SIZE*140;
    uint16_t x1 = sx * M_SIZE*100 + M_SIZE*120;
    uint16_t y1 = sy * M_SIZE*100 + M_SIZE*140;

    // Coordinates of next tick for zone fill
    float sx2 = cos((i + 5 - 90) * 0.0174532925);
    float sy2 = sin((i + 5 - 90) * 0.0174532925);
    int x2 = sx2 * (M_SIZE*100 + tl) + M_SIZE*120;
    int y2 = sy2 * (M_SIZE*100 + tl) + M_SIZE*140;
    int x3 = sx2 * M_SIZE*100 + M_SIZE*120;
    int y3 = sy2 * M_SIZE*100 + M_SIZE*140;

    // Green zone limits
    if (i >= 0 && i < 25) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_GREEN);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_GREEN);
    }

    // Orange zone limits
    if (i >= 25 && i < 50) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE);
    }

    // Short scale tick length
    if (i % 25 != 0) tl = 8;

    // Recalculate coords incase tick lenght changed
    x0 = sx * (M_SIZE*100 + tl) + M_SIZE*120;
    y0 = sy * (M_SIZE*100 + tl) + M_SIZE*140;
    x1 = sx * M_SIZE*100 + M_SIZE*120;
    y1 = sy * M_SIZE*100 + M_SIZE*140;

    // Draw tick
    tft.drawLine(x0, y0, x1, y1, TFT_BLACK);

    // Check if labels should be drawn, with position tweaks
    if (i % 25 == 0) {
      // Calculate label positions
      x0 = sx * (M_SIZE*100 + tl + 10) + M_SIZE*120;
      y0 = sy * (M_SIZE*100 + tl + 10) + M_SIZE*140;
      switch (i / 25) {
        case -2: tft.drawCentreString("0", x0, y0 - 12, 2); break;
        case -1: tft.drawCentreString("25", x0, y0 - 9, 2); break;
        case 0: tft.drawCentreString("50", x0, y0 - 7, 2); break;
        case 1: tft.drawCentreString("75", x0, y0 - 9, 2); break;
        case 2: tft.drawCentreString("100", x0, y0 - 12, 2); break;
      }
    }

    // Now draw the arc of the scale
    sx = cos((i + 5 - 90) * 0.0174532925);
    sy = sin((i + 5 - 90) * 0.0174532925);
    x0 = sx * M_SIZE*100 + M_SIZE*120;
    y0 = sy * M_SIZE*100 + M_SIZE*140;
    // Draw scale arc, don't draw the last part
    if (i < 50) tft.drawLine(x0, y0, x1, y1, TFT_BLACK);
  }

  tft.drawString("Light", M_SIZE*(5 + 230 - 40), M_SIZE*(119 - 20), 2); // Units at bottom right
  tft.drawCentreString("Light", M_SIZE*120, M_SIZE*70, 4); // Comment out to avoid font 4
  tft.drawRect(5, 3, M_SIZE*230, M_SIZE*119, TFT_BLACK); // Draw bezel line

  plotNeedle(0, 0); // Put meter needle at 0
}

// #########################################################################
// Update needle position
// #########################################################################
void plotNeedle(int value, byte ms_delay) {
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  char buf[8]; dtostrf(value, 4, 0, buf);
  tft.drawRightString(buf, M_SIZE*40, M_SIZE*(119 - 20), 2);

  if (value < -10) value = -10; // Limit value to emulate needle end stops
  if (value > 110) value = 110;

  // Move the needle until new value reached
  while (!(value == old_analog)) {
    if (old_analog < value) old_analog++;
    else old_analog--;

    if (ms_delay == 0) old_analog = value; // Update immediately if delay is 0

    float sdeg = map(old_analog, -10, 110, -150, -30); // Map value to angle
    // Calcualte tip of needle coords
    float sx = cos(sdeg * 0.0174532925);
    float sy = sin(sdeg * 0.0174532925);

    // Calculate x delta of needle start (does not start at pivot point)
    float tx = tan((sdeg + 90) * 0.0174532925);

    // Erase old needle image
    tft.drawLine(M_SIZE*(120 + 20 * ltx - 1), M_SIZE*(140 - 20), osx - 1, osy, TFT_WHITE);
    tft.drawLine(M_SIZE*(120 + 20 * ltx), M_SIZE*(140 - 20), osx, osy, TFT_WHITE);
    tft.drawLine(M_SIZE*(120 + 20 * ltx + 1), M_SIZE*(140 - 20), osx + 1, osy, TFT_WHITE);

    // Re-plot text under needle
    tft.setTextColor(TFT_BLACK);
    tft.drawCentreString("Light", M_SIZE*120, M_SIZE*70, 4);

    // Store new needle end coords for next erase
    ltx = tx;
    osx = M_SIZE*(sx * 98 + 120);
    osy = M_SIZE*(sy * 98 + 140);

    // Draw the needle in the new postion
    tft.drawLine(M_SIZE*(120 + 20 * ltx - 1), M_SIZE*(140 - 20), osx - 1, osy, TFT_RED);
    tft.drawLine(M_SIZE*(120 + 20 * ltx), M_SIZE*(140 - 20), osx, osy, TFT_MAGENTA);
    tft.drawLine(M_SIZE*(120 + 20 * ltx + 1), M_SIZE*(140 - 20), osx + 1, osy, TFT_RED);

    // Slow needle down slightly as it approaches new postion
    if (abs(old_analog - value) < 10) ms_delay += ms_delay / 5;

    // Wait before next update
    delay(ms_delay);
  }
}

Mfr Part # DFR0992-EN
UNIHIKER K10 AI EDUCATIONAL TOOL
DFRobot
$237.90
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.