2025-09-16 19:24:27 +09:00
|
|
|
/*
|
|
|
|
|
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: MIT
|
|
|
|
|
*/
|
|
|
|
|
/*
|
|
|
|
|
Simple example using M5UnitUnified for UnitFinger2
|
|
|
|
|
*/
|
|
|
|
|
#include <M5Unified.h>
|
|
|
|
|
#include <M5UnitUnified.h>
|
|
|
|
|
#include <M5UnitUnifiedFINGER.h>
|
|
|
|
|
#include <M5Utility.hpp>
|
|
|
|
|
|
|
|
|
|
using namespace m5::unit::finger2;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
auto& lcd = M5.Display;
|
|
|
|
|
|
|
|
|
|
m5::unit::UnitUnified Units;
|
|
|
|
|
m5::unit::UnitFinger2 unit;
|
|
|
|
|
|
|
|
|
|
void print_exists_users()
|
|
|
|
|
{
|
|
|
|
|
uint16_t num{};
|
|
|
|
|
if (unit.wakeup() && unit.readValidTemplates(num)) {
|
|
|
|
|
uint8_t table[32]{};
|
|
|
|
|
M5.Log.printf("=== Exists templates %u/%u ===\n", num, unit.capacity());
|
|
|
|
|
if (unit.readIndexTable(table)) {
|
|
|
|
|
for (uint16_t id = 0; id < 32 * 8; ++id) {
|
|
|
|
|
uint32_t idx = id >> 3;
|
|
|
|
|
uint32_t shift = id & 0x07;
|
|
|
|
|
if (table[idx] & (1U << shift)) {
|
|
|
|
|
M5.Log.printf("page:%03u\n", id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Example of registration like as autoEnroll
|
|
|
|
|
// Registered IDs will be overwritten.
|
|
|
|
|
// Existing templates will not be considered.
|
|
|
|
|
void register_finger()
|
|
|
|
|
{
|
|
|
|
|
static bool highlow{};
|
|
|
|
|
|
|
|
|
|
if (unit.wakeup()) {
|
|
|
|
|
// Available lowest/highest page_id
|
2025-09-17 19:33:47 +09:00
|
|
|
uint16_t page{0xFFFF};
|
|
|
|
|
if (!(highlow == false ? unit.findLowestAvailablePage(page) : unit.findHighestAvailablePage(page)) ||
|
|
|
|
|
page == 0xFFFF) {
|
2026-04-07 23:00:38 +09:00
|
|
|
M5_LOGE("Failed or Invalid page");
|
2025-09-16 19:24:27 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
M5.Log.printf("Try register to %u\n", page);
|
|
|
|
|
|
|
|
|
|
bool detected{};
|
|
|
|
|
uint8_t cnt{};
|
|
|
|
|
while (cnt < 5) {
|
|
|
|
|
// Wait touch finger
|
|
|
|
|
lcd.fillScreen(TFT_ORANGE);
|
|
|
|
|
lcd.drawString("Please touch finger", 0, 16);
|
|
|
|
|
M5.Log.printf("Please touch finger(%u)\n", cnt + 1);
|
|
|
|
|
|
|
|
|
|
auto timeout_at = m5::utility::millis() + 5000;
|
|
|
|
|
bool touched{};
|
|
|
|
|
do {
|
|
|
|
|
touched = unit.capture(detected, true /*for enroll */) && detected;
|
|
|
|
|
m5::utility::delay(1);
|
|
|
|
|
} while (!touched && m5::utility::millis() <= timeout_at);
|
|
|
|
|
if (!touched) {
|
|
|
|
|
M5_LOGE("Finger undetected %u", cnt + 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate characteristic (1-5)
|
|
|
|
|
if (!unit.generateCharacteristic(cnt + 1)) {
|
|
|
|
|
M5_LOGE("Failed to GenerateCharacteristic %u", cnt + 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait release finger
|
|
|
|
|
if (cnt < 4) {
|
|
|
|
|
M5.Speaker.tone(3000, 20);
|
|
|
|
|
lcd.fillScreen(TFT_BLUE);
|
|
|
|
|
lcd.drawString("Please release finger", 0, 16);
|
|
|
|
|
M5.Log.printf("Please release finger(%u)\n", cnt + 1);
|
|
|
|
|
|
|
|
|
|
auto timeout_at = m5::utility::millis() + 5000;
|
|
|
|
|
bool released{};
|
|
|
|
|
do {
|
|
|
|
|
released = unit.capture(detected, true) && !detected;
|
|
|
|
|
m5::utility::delay(1);
|
|
|
|
|
} while (!released && m5::utility::millis() <= timeout_at);
|
|
|
|
|
if (!released) {
|
|
|
|
|
M5_LOGE("Finger not released %u", cnt + 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++cnt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate template(merge 1-5)
|
|
|
|
|
lcd.fillScreen(TFT_DARKGREEN);
|
|
|
|
|
if (!unit.generateTemplate()) {
|
|
|
|
|
M5_LOGE("Failed to generateTemplate");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store template
|
|
|
|
|
if (!unit.storeTemplate(page)) {
|
|
|
|
|
M5_LOGE("Failed to storeTemplate");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
highlow = !highlow;
|
|
|
|
|
M5.Log.printf("Registered: %u\n", page);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1:1
|
|
|
|
|
void match_finger(const uint16_t page)
|
|
|
|
|
{
|
|
|
|
|
bool detected{};
|
|
|
|
|
if (unit.capture(detected) && detected && unit.generateCharacteristic(1) && unit.loadTemplate(2, page)) {
|
|
|
|
|
bool matched{};
|
|
|
|
|
uint16_t score{};
|
|
|
|
|
if (unit.match(matched, score)) {
|
|
|
|
|
M5.Log.printf("<1:1>%u %s, Score:%u\n", page, matched ? "Matched" : "Unmatched", score);
|
|
|
|
|
} else {
|
2026-04-07 23:00:38 +09:00
|
|
|
M5_LOGE("Failed to match");
|
2025-09-16 19:24:27 +09:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
M5_LOGE("Failed to any function");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1:N
|
|
|
|
|
uint16_t search_finger()
|
|
|
|
|
{
|
|
|
|
|
bool detected{};
|
|
|
|
|
// search is performed on the specified buffer
|
|
|
|
|
if (unit.capture(detected) && detected && unit.generateCharacteristic(1)) {
|
|
|
|
|
bool matched{};
|
|
|
|
|
uint16_t page{};
|
|
|
|
|
uint16_t score{};
|
2026-04-07 23:00:38 +09:00
|
|
|
if (unit.search(matched, page, score, 1 /* buffer id */)) {
|
2025-09-16 19:24:27 +09:00
|
|
|
M5.Log.printf("<1:N> %s Page:%u Score:%u\n", matched ? "Found" : "Not found", page, score);
|
|
|
|
|
return matched ? page : 0xFFFF;
|
|
|
|
|
} else {
|
|
|
|
|
M5_LOGE("Failed to search");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
M5_LOGE("Failed to capture/generate");
|
|
|
|
|
}
|
|
|
|
|
return 0xFFFF;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1:N
|
|
|
|
|
uint16_t search_now_finger()
|
|
|
|
|
{
|
|
|
|
|
bool detected{};
|
2026-04-07 23:00:38 +09:00
|
|
|
// searchNow is performed on the last generated characteristic
|
2025-09-16 19:24:27 +09:00
|
|
|
if (unit.capture(detected) && detected && unit.generateCharacteristic(2)) {
|
|
|
|
|
bool matched{};
|
|
|
|
|
uint16_t page{};
|
|
|
|
|
uint16_t score{};
|
|
|
|
|
if (unit.searchNow(matched, page, score)) {
|
|
|
|
|
M5.Log.printf("<1:N now> %s Page:%u Score:%u\n", matched ? "Found" : "Not found", page, score);
|
|
|
|
|
return matched ? page : 0xFFFF;
|
|
|
|
|
} else {
|
|
|
|
|
M5_LOGE("Failed to search");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
M5_LOGE("Failed to capture/generate");
|
|
|
|
|
}
|
|
|
|
|
return 0xFFFF;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void setup()
|
|
|
|
|
{
|
|
|
|
|
M5.begin();
|
2026-04-07 23:00:38 +09:00
|
|
|
M5.setTouchButtonHeightByRatio(100);
|
2025-09-16 19:24:27 +09:00
|
|
|
|
|
|
|
|
// The screen shall be in landscape mode
|
|
|
|
|
if (lcd.height() > lcd.width()) {
|
|
|
|
|
lcd.setRotation(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pin_num_in = M5.getPin(m5::pin_name_t::port_c_rxd);
|
|
|
|
|
auto pin_num_out = M5.getPin(m5::pin_name_t::port_c_txd);
|
|
|
|
|
if (pin_num_in < 0 || pin_num_out < 0) {
|
|
|
|
|
M5_LOGW("PortC is not available");
|
2026-04-07 23:00:38 +09:00
|
|
|
// NanoC6: Ex_I2C.setPort() registers m5gfx::i2c on GROVE pins;
|
|
|
|
|
// Wire.end() alone won't release it, causing dual-driver conflict on uart_driver_install
|
|
|
|
|
if (M5.getBoard() == m5::board_t::board_M5NanoC6) {
|
|
|
|
|
M5.Ex_I2C.release();
|
|
|
|
|
}
|
2025-09-16 19:24:27 +09:00
|
|
|
Wire.end();
|
|
|
|
|
pin_num_in = M5.getPin(m5::pin_name_t::port_a_pin1);
|
|
|
|
|
pin_num_out = M5.getPin(m5::pin_name_t::port_a_pin2);
|
|
|
|
|
}
|
|
|
|
|
M5_LOGI("getPin: %d,%d", pin_num_in, pin_num_out);
|
|
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
|
#if defined(CONFIG_IDF_TARGET_ESP32C6)
|
|
|
|
|
auto& s = Serial1;
|
|
|
|
|
#elif SOC_UART_NUM > 2
|
|
|
|
|
auto& s = Serial2;
|
|
|
|
|
#elif SOC_UART_NUM > 1
|
|
|
|
|
auto& s = Serial1;
|
|
|
|
|
#else
|
|
|
|
|
#error "Not enough Serial"
|
|
|
|
|
#endif
|
|
|
|
|
// clang-format on
|
|
|
|
|
s.begin(115200, SERIAL_8N1, pin_num_in, pin_num_out);
|
|
|
|
|
|
|
|
|
|
if (!Units.add(unit, s) || !Units.begin()) {
|
|
|
|
|
M5_LOGE("Failed to begin");
|
2026-04-07 23:00:38 +09:00
|
|
|
lcd.fillScreen(TFT_RED);
|
2025-09-16 19:24:27 +09:00
|
|
|
while (true) {
|
|
|
|
|
m5::utility::delay(10000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 23:00:38 +09:00
|
|
|
M5_LOGI("M5UnitUnified initialized");
|
2025-09-16 19:24:27 +09:00
|
|
|
M5_LOGI("%s", Units.debugInfo().c_str());
|
|
|
|
|
|
|
|
|
|
lcd.fillScreen(TFT_DARKGREEN);
|
|
|
|
|
lcd.setFont(&fonts::AsciiFont8x16);
|
|
|
|
|
|
|
|
|
|
print_exists_users();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop()
|
|
|
|
|
{
|
|
|
|
|
M5.update();
|
|
|
|
|
Units.update();
|
|
|
|
|
|
|
|
|
|
// Search,Match
|
2026-04-07 23:00:38 +09:00
|
|
|
if (M5.BtnA.wasClicked()) {
|
2025-09-16 19:24:27 +09:00
|
|
|
M5.Speaker.tone(2000, 20);
|
|
|
|
|
auto page = search_finger();
|
|
|
|
|
if (page != 0xFFFF) {
|
|
|
|
|
auto page2 = search_now_finger();
|
|
|
|
|
if (page2 != 0xFFFF) {
|
|
|
|
|
match_finger(page);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register
|
2026-04-07 23:00:38 +09:00
|
|
|
if (M5.BtnA.wasHold()) {
|
2025-09-16 19:24:27 +09:00
|
|
|
M5.Speaker.tone(4000, 20);
|
|
|
|
|
register_finger();
|
|
|
|
|
lcd.fillScreen(TFT_DARKGREEN);
|
|
|
|
|
}
|
|
|
|
|
}
|