From 356d7de2cfbe325322d0f4d10cd44494025b64c4 Mon Sep 17 00:00:00 2001 From: BotoX Date: Tue, 6 Jun 2023 19:34:45 +0200 Subject: [PATCH] first commit --- Makefile | 9 + app/FingerLogic.cpp | 598 +++++++++++++++++++++++++++++++++++ app/FingerLogic.h | 105 ++++++ app/FingerPrint.cpp | 267 ++++++++++++++++ app/FingerPrint.h | 163 ++++++++++ app/FingerPrint_API.cpp | 101 ++++++ app/FingerPrint_AsyncAPI.cpp | 510 +++++++++++++++++++++++++++++ app/Settings.cpp | 118 +++++++ app/Settings.h | 38 +++ app/application.cpp | 54 ++++ app/main.cpp | 462 +++++++++++++++++++++++++++ app/main.h | 65 ++++ app/utils.cpp | 75 +++++ app/utils.h | 8 + component.mk | 4 + files/.gitkeep | 0 files/bootstrap.min.css.gz | Bin 0 -> 18466 bytes files/bootstrap.min.js.gz | Bin 0 -> 12913 bytes files/favicon.ico | Bin 0 -> 1150 bytes files/index.html | 25 ++ files/jquery-3.2.1.min.js.gz | Bin 0 -> 29995 bytes files/main.css.gz | Bin 0 -> 653 bytes files/main.js.gz | Bin 0 -> 742 bytes files/nunjucks.min.js.gz | Bin 0 -> 22603 bytes files/popper.min.js.gz | Bin 0 -> 6963 bytes files/settings.conf | 25 ++ files/templates.js.gz | Bin 0 -> 4065 bytes files/wifi-sprites.png | Bin 0 -> 1815 bytes out/.gitignore | 2 + 29 files changed, 2629 insertions(+) create mode 100644 Makefile create mode 100644 app/FingerLogic.cpp create mode 100644 app/FingerLogic.h create mode 100644 app/FingerPrint.cpp create mode 100644 app/FingerPrint.h create mode 100644 app/FingerPrint_API.cpp create mode 100644 app/FingerPrint_AsyncAPI.cpp create mode 100644 app/Settings.cpp create mode 100644 app/Settings.h create mode 100644 app/application.cpp create mode 100644 app/main.cpp create mode 100644 app/main.h create mode 100644 app/utils.cpp create mode 100644 app/utils.h create mode 100644 component.mk create mode 100644 files/.gitkeep create mode 100644 files/bootstrap.min.css.gz create mode 100644 files/bootstrap.min.js.gz create mode 100644 files/favicon.ico create mode 100644 files/index.html create mode 100644 files/jquery-3.2.1.min.js.gz create mode 100644 files/main.css.gz create mode 100644 files/main.js.gz create mode 100644 files/nunjucks.min.js.gz create mode 100644 files/popper.min.js.gz create mode 100644 files/settings.conf create mode 100644 files/templates.js.gz create mode 100644 files/wifi-sprites.png create mode 100644 out/.gitignore diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ff51b6c --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +##################################################################### +#### Please don't change this file. Use component.mk instead #### +##################################################################### + +ifndef SMING_HOME +$(error SMING_HOME is not set: please configure it as an environment variable) +endif + +include $(SMING_HOME)/project.mk diff --git a/app/FingerLogic.cpp b/app/FingerLogic.cpp new file mode 100644 index 0000000..c0ac909 --- /dev/null +++ b/app/FingerLogic.cpp @@ -0,0 +1,598 @@ +#include +#include +#include "utils.h" +#include "main.h" +#include "FingerPrint.h" +#include "FingerLogic.h" + +CFingerLogic::CFingerLogic(CMain *pMain) +{ + m_pFingerPrint = NULL; + m_State = STATE_INITIAL; + m_Finger = false; + m_Power = false; + m_pMain = pMain; + + memset(&m_FingerParams, 0, sizeof(m_FingerParams)); + memset(m_aFingerSlots, 0, sizeof(m_aFingerSlots)); + m_FingerSlotsAdded = 0; + + m_PowerOffTimer.initializeMs(1000, TimerDelegate(&CFingerLogic::PowerOff, this)); +} + +void CFingerLogic::Init(CFingerPrint *pFingerPrint) +{ + m_pFingerPrint = pFingerPrint; + + InitFinger(); +} + +void CFingerLogic::SetState(FingerLogicState state) +{ + m_State = state; + + if(m_State == STATE_READY || m_State == STATE_ERROR) + m_PowerOffTimer.start(false); + else + { + m_PowerOffTimer.stop(); + PowerOn(); + } +} + +void CFingerLogic::PowerOff() +{ + if(!m_Power) + return; + + debugf("PowerOff()"); + m_Power = false; + Main().FingerEnable(false); + wifi_set_sleep_type(LIGHT_SLEEP_T); + system_soft_wdt_feed(); +} + +void CFingerLogic::PowerOn() +{ + if(m_Power) + return; + + debugf("PowerOn()"); + m_Power = true; + Main().FingerEnable(true); + wifi_set_sleep_type(MODEM_SLEEP_T); + delayMilliseconds(100); +} + +void CFingerLogic::OnFingerInterrupt(bool finger) +{ + debugf("OnFingerInterrupt: %s", finger ? "DOWN" : "UP"); + m_Finger = finger; + + if(finger) + { + if(m_State == STATE_READY) + VerifyFinger(); + else if(m_State == STATE_ENROLL_WAITFINGER1 || m_State == STATE_ENROLL_WAITFINGER2) + EnrollFinger_OnFinger(); + } +} + +bool CFingerLogic::FingerSlot(uint16_t index) +{ + if(index > m_FingerSlotsAdded) + return false; + + return m_aFingerSlots[index / 8] & (1 << (index % 8)); +} + +bool CFingerLogic::FingerSlot(uint16_t index, bool value) +{ + if(index > m_FingerSlotsAdded) + return false; + + if(value) + m_aFingerSlots[index / 8] |= (1 << (index % 8)); + else + m_aFingerSlots[index / 8] &= ~(1 << (index % 8)); + return true; +} + +int CFingerLogic::FirstFreeFingerSlot() +{ + for(int i = 0; i < m_FingerSlotsAdded / 8; i++) + { + if(m_aFingerSlots[i] != 0xFF) + { + uint8_t val = m_aFingerSlots[i]; + for(int j = 0; j < 8; j++) + { + if(!(val & (1 << j))) + return (i * 8) + j; + } + } + } + + return -1; +} + + +void CFingerLogic::InitFinger() +{ + if(m_State > STATE_INIT_VERIFYPASSWORD) + return; + + SetState(STATE_INIT_VERIFYPASSWORD); + FingerPrint().AsyncVerifyPassword((VerifyPasswordCallback)InitFinger_OnVerifyPassword, this); + + m_Timer.initializeMs(100, TimerDelegate(&CFingerLogic::InitFinger, this)).start(); +} + +void CFingerLogic::InitFinger_OnVerifyPassword(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + pThis->m_Timer.stop(); + debugf("InitFinger_OnVerifyPassword: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + pThis->SetState(STATE_INIT_READSYSTEMPARAMETERS); + pThis->FingerPrint().AsyncReadSystemParameters((ReadSystemParametersCallback)InitFinger_OnReadSystemParameters, pThis); + } + else + pThis->SetState(STATE_ERROR); +} + +void CFingerLogic::InitFinger_OnReadSystemParameters(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, CFingerSystemParameters *param) +{ + debugf("InitFinger_OnReadSystemParameters: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + memcpy(&pThis->m_FingerParams, param, sizeof(CFingerSystemParameters)); + debugf("statusRegister: %d", param->statusRegister); + debugf("systemID: %d", param->systemID); + debugf("storageCapacity: %d", param->storageCapacity); + debugf("securityLevel: %d", param->securityLevel); + debugf("deviceAddress: %X", param->deviceAddress); + debugf("packetLength: %d", param->packetLength); + debugf("baudRate: %d", param->baudRate); + + pThis->SetState(STATE_INIT_READTEMPLATEMAP); + pThis->FingerPrint().AsyncReadTemplateMap((ReadTemplateMapCallback)InitFinger_OnReadTemplateMap, pThis, 0); + } + else + pThis->SetState(STATE_ERROR); +} + +void CFingerLogic::InitFinger_OnReadTemplateMap(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, uint8_t *pData, uint16_t dataLen) +{ + debugf("InitFinger_OnReadTemplateMap: (%d) %s (%p, %d)", error, errorStr, pData, dataLen); + + if(error == ERROR_OK) + { + memcpy(&pThis->m_aFingerSlots[pThis->m_FingerSlotsAdded/8], pData, dataLen); + pThis->m_FingerSlotsAdded += dataLen * 8; + + if(pThis->m_FingerSlotsAdded < pThis->m_FingerParams.storageCapacity) + { + uint8_t page = pThis->m_FingerSlotsAdded / 8 / dataLen; + pThis->FingerPrint().AsyncReadTemplateMap((ReadTemplateMapCallback)InitFinger_OnReadTemplateMap, pThis, page); + } + else + { + pThis->InitFinger_VerifyTemplates(); + } + } +} + +void CFingerLogic::InitFinger_VerifyTemplates() +{ + SetState(STATE_INIT_VERIFYTEMPLATES); + + HashMap &fingerMap = Main().Settings().m_FingerPrints; + uint16_t fingerCount = fingerMap.count(); + + // Check consistency (1) + for(uint16_t i = 0; i < fingerCount; i++) + { + uint16_t id = fingerMap.keyAt(i); + if(!FingerSlot(id)) + { + SetState(STATE_ERROR); + FingerPrint().EmptyDatabase(); + fingerMap.clear(); + Main().Settings().Save(); + debugf("InitFinger_VerifyTemplates: INCONSITENCY(1) AT SLOT %d !!!", id); + return; + } + } + + // Check consistency (2) + for(uint16_t id = 0; id < m_FingerSlotsAdded; id++) + { + if(FingerSlot(id) && !fingerMap.contains(id)) + { + SetState(STATE_ERROR); + FingerPrint().EmptyDatabase(); + fingerMap.clear(); + Main().Settings().Save(); + debugf("InitFinger_VerifyTemplates: INCONSITENCY(2) AT SLOT %d !!!", id); + return; + } + } + + if(!fingerCount) + { + SetState(STATE_READY); + return; + } + + m_iBuffer = 0; + uint16_t position = fingerMap.keyAt(m_iBuffer); + FingerPrint().AsyncLoadTemplate((LoadTemplateCallback)InitFinger_OnLoadTemplate, this, position, 0x01); +} + +void CFingerLogic::InitFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("InitFinger_OnLoadTemplate: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + pThis->FingerPrint().AsyncDownloadCharacteristics((DownloadCharacteristicsCallback)InitFinger_OnDownloadCharacteristics, pThis, 0x01); + } + else + pThis->SetState(STATE_ERROR); +} + +void CFingerLogic::InitFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen) +{ + debugf("InitFinger_OnDownloadCharacteristics: (%d) %s (%p, %d)", error, errorStr, pChar, charLen); + + if(error == ERROR_OK) + { + Crypto::Sha256 ctx; + ctx.update(pChar, charLen); + uint8_t *digest = ctx.getHash().data(); + + const CSettings::CFingerPrint &finger = pThis->Main().Settings().m_FingerPrints.valueAt(pThis->m_iBuffer); + + char aHexDigest1[SHA256_SIZE*2+1]; + char aHexDigest2[SHA256_SIZE*2+1]; + bytes2hex(digest, sizeof(digest), aHexDigest1, sizeof(aHexDigest1)); + bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest2, sizeof(aHexDigest2)); + + debugf("Index: %d -> Num: %d \"%s\" (%s ?= %s)", pThis->m_iBuffer, finger.m_FingerNum, finger.m_aLabel, aHexDigest1, aHexDigest2); + + if(memcmp(digest, finger.m_aDigest, SHA256_SIZE) != 0) + { + pThis->SetState(STATE_ERROR); + debugf("InitFinger_VerifyTemplates: DIVERGENT DIGEST AT SLOT %d !!!", pThis->m_iBuffer); + return; + } + + pThis->m_iBuffer++; + if(pThis->m_iBuffer >= pThis->Main().Settings().m_FingerPrints.count()) + { + debugf("InitFinger_VerifyTemplates: DONE! Verified %d templates.", pThis->m_iBuffer); + pThis->SetState(STATE_READY); + return; + } + + uint16_t position = pThis->Main().Settings().m_FingerPrints.keyAt(pThis->m_iBuffer); + pThis->FingerPrint().AsyncLoadTemplate((LoadTemplateCallback)InitFinger_OnLoadTemplate, pThis, position, 0x01); + } + else + pThis->SetState(STATE_ERROR); +} + + +void CFingerLogic::VerifyFinger() +{ + if(m_State != STATE_READY) + return; + + SetState(STATE_VERIFY_READIMAGE); + FingerPrint().AsyncReadImage((ReadImageCallback)VerifyFinger_OnReadImage, this); +} + +void CFingerLogic::VerifyFinger_OnReadImage(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("VerifyFinger_OnReadImage: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + pThis->SetState(STATE_VERIFY_CONVERTIMAGE); + pThis->FingerPrint().AsyncConvertImage((ConvertImageCallback)VerifyFinger_OnConvertImage, pThis, 0x01); + } + else + { + pThis->SetState(STATE_READY); + if(pThis->m_Finger) + pThis->VerifyFinger(); + } +} + +void CFingerLogic::VerifyFinger_OnConvertImage(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("VerifyFinger_OnConvertImage: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + pThis->SetState(STATE_VERIFY_SEARCHTEMPLATE); + pThis->FingerPrint().AsyncSearchTemplate((SearchTemplateCallback)VerifyFinger_OnSearchTemplate, pThis, 0x01, 0, pThis->m_FingerParams.storageCapacity); + } + else + { + pThis->SetState(STATE_READY); + if(pThis->m_Finger) + pThis->VerifyFinger(); + } +} + +void CFingerLogic::VerifyFinger_OnSearchTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t position, int16_t score) +{ + debugf("VerifyFinger_OnSearchTemplate: (%d) %s (%d, %d)", error, errorStr, position, score); + + if(error == ERROR_OK) + { + pThis->m_iBuffer = position; + pThis->SetState(STATE_VERIFY_LOADTEMPLATE); + pThis->FingerPrint().AsyncLoadTemplate((LoadTemplateCallback)VerifyFinger_OnLoadTemplate, pThis, position, 0x01); + } + else + pThis->SetState(STATE_READY); +} + +void CFingerLogic::VerifyFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("VerifyFinger_OnLoadTemplate: (%d) %s", error, errorStr); + + if(error == ERROR_OK) + { + pThis->SetState(STATE_VERIFY_DOWNLOADCHARACTERISTICS); + pThis->FingerPrint().AsyncDownloadCharacteristics((DownloadCharacteristicsCallback)VerifyFinger_OnDownloadCharacteristics, pThis, 0x01); + } + else + pThis->SetState(STATE_READY); +} + +void CFingerLogic::VerifyFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen) +{ + debugf("VerifyFinger_OnDownloadCharacteristics: (%d) %s (%p, %d)", error, errorStr, pChar, charLen); + + if(error == ERROR_OK) + { + Crypto::Sha256 ctx; + ctx.update(pChar, charLen); + uint8_t *digest = ctx.getHash().data(); + + char aHexDigest[SHA256_SIZE*2+1]; + bytes2hex(digest, sizeof(digest), aHexDigest, sizeof(aHexDigest)); + + debugf("Finger hexdigest: %s", aHexDigest); + + pThis->Main().OnFingerVerified(pThis->m_iBuffer, digest); + } + + pThis->SetState(STATE_READY); +} + + +bool CFingerLogic::EnrollFinger(bool cancel) +{ + if(cancel) + { + if(m_State < STATE_ENROLL_WAITFINGER1 || m_State > STATE_ENROLL_DOWNLOADCHARACTERISTICS) + return false; + + SetState(STATE_READY); + return true; + } + + if(m_State != STATE_READY) + return false; + + SetState(STATE_ENROLL_WAITFINGER1); + if(m_Finger) + EnrollFinger_OnFinger(); + + return true; +} + +void CFingerLogic::EnrollFinger_OnFinger() +{ + if(m_State == STATE_ENROLL_WAITFINGER1) + { + SetState(STATE_ENROLL_READIMAGE1); + FingerPrint().AsyncReadImage((ReadImageCallback)EnrollFinger_OnReadImage1, this); + } + else if(m_State == STATE_ENROLL_WAITFINGER2) + { + SetState(STATE_ENROLL_READIMAGE2); + FingerPrint().AsyncReadImage((ReadImageCallback)EnrollFinger_OnReadImage2, this); + } +} + +void CFingerLogic::EnrollFinger_OnReadImage1(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnReadImage1: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_READIMAGE1) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_CONVERTIMAGE1); + pThis->FingerPrint().AsyncConvertImage((ConvertImageCallback)EnrollFinger_OnConvertImage1, pThis, 0x01); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER1); + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Error while scanning finger: %s
Lift your finger and try again.", errorStr); + pThis->Main().EnrollMessage(aBuf); + } +} + +void CFingerLogic::EnrollFinger_OnConvertImage1(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnConvertImage1: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_CONVERTIMAGE1) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_SEARCHTEMPLATE); + pThis->FingerPrint().AsyncSearchTemplate((SearchTemplateCallback)EnrollFinger_OnSearchTemplate, pThis, 0x01, 0, pThis->m_FingerParams.storageCapacity); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER1); + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Error while analyzing finger: %s
Lift your finger and try again.", errorStr); + pThis->Main().EnrollMessage(aBuf); + } +} + +void CFingerLogic::EnrollFinger_OnSearchTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t position, int16_t score) +{ + debugf("EnrollFinger_OnSearchTemplate: (%d) %s (%d, %d)", error, errorStr, position, score); + if(pThis->m_State != STATE_ENROLL_SEARCHTEMPLATE) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_READY); + pThis->Main().EnrollMessage("Aborting: This finger is already enrolled!", true); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER2); + pThis->Main().EnrollMessage("Finger scanned. Lift finger and place it on the sensor again to enroll."); + } +} + +void CFingerLogic::EnrollFinger_OnReadImage2(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnReadImage2: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_READIMAGE2) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_CONVERTIMAGE2); + pThis->FingerPrint().AsyncConvertImage((ConvertImageCallback)EnrollFinger_OnConvertImage2, pThis, 0x02); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER2); + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Error while scanning finger: %s
Lift your finger and try again.", errorStr); + pThis->Main().EnrollMessage(aBuf); + } +} + +void CFingerLogic::EnrollFinger_OnConvertImage2(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnConvertImage2: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_CONVERTIMAGE2) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_COMPARE); + pThis->FingerPrint().AsyncCompareCharacteristics((CompareCharacteristicsCallback)EnrollFinger_OnCompareCharacteristics, pThis); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER2); + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Error while analyzing finger: %s
Lift your finger and try again.", errorStr); + pThis->Main().EnrollMessage(aBuf); + } +} + +void CFingerLogic::EnrollFinger_OnCompareCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t score) +{ + debugf("EnrollFinger_OnCompareCharacteristics: (%d) %s (%d)", error, errorStr, score); + if(pThis->m_State != STATE_ENROLL_COMPARE) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_CREATETEMPLATE); + pThis->FingerPrint().AsyncCreateTemplate((CreateTemplateCallback)EnrollFinger_OnCreateTemplate, pThis); + } + else + { + pThis->SetState(STATE_ENROLL_WAITFINGER1); + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Error while comparing fingers: %s
Lift your finger and try again.", errorStr); + pThis->Main().EnrollMessage(aBuf); + } +} + +void CFingerLogic::EnrollFinger_OnCreateTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnCreateTemplate: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_CREATETEMPLATE) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_STORETEMPLATE); + pThis->m_iBuffer = pThis->FirstFreeFingerSlot(); + pThis->FingerPrint().AsyncStoreTemplate((StoreTemplateCallback)EnrollFinger_OnStoreTemplate, pThis, pThis->m_iBuffer, 0x01); + } + else + { + pThis->SetState(STATE_READY); + } +} + +void CFingerLogic::EnrollFinger_OnStoreTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, uint16_t positionNumber) +{ + debugf("EnrollFinger_OnStoreTemplate: (%d) %s (%d)", error, errorStr, positionNumber); + if(pThis->m_State != STATE_ENROLL_STORETEMPLATE) return; + + if(error == ERROR_OK) + { + pThis->FingerSlot(positionNumber, true); + pThis->SetState(STATE_ENROLL_LOADTEMPLATE); + pThis->FingerPrint().AsyncLoadTemplate((LoadTemplateCallback)EnrollFinger_OnLoadTemplate, pThis, positionNumber, 0x01); + } + else + { + pThis->SetState(STATE_READY); + } +} + +void CFingerLogic::EnrollFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr) +{ + debugf("EnrollFinger_OnLoadTemplate: (%d) %s", error, errorStr); + if(pThis->m_State != STATE_ENROLL_LOADTEMPLATE) return; + + if(error == ERROR_OK) + { + pThis->SetState(STATE_ENROLL_DOWNLOADCHARACTERISTICS); + pThis->FingerPrint().AsyncDownloadCharacteristics((DownloadCharacteristicsCallback)EnrollFinger_OnDownloadCharacteristics, pThis, 0x01); + } + else + { + pThis->SetState(STATE_READY); + } +} + +void CFingerLogic::EnrollFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen) +{ + debugf("EnrollFinger_OnDownloadCharacteristics: (%d) %s (%p, %d)", error, errorStr, pChar, charLen); + if(pThis->m_State != STATE_ENROLL_DOWNLOADCHARACTERISTICS) return; + + if(error == ERROR_OK) + { + Crypto::Sha256 ctx; + ctx.update(pChar, charLen); + uint8_t *digest = ctx.getHash().data(); + + Serial1.printf("NEW Finger %d hexdigest: ", pThis->m_iBuffer); + for(uint8_t i = 0; i < sizeof(digest); i++) + Serial1.printf("%x", digest[i]); + Serial1.printf("\n"); + + pThis->Main().OnFingerEnrolled(pThis->m_iBuffer, digest); + } + + pThis->SetState(STATE_READY); +} diff --git a/app/FingerLogic.h b/app/FingerLogic.h new file mode 100644 index 0000000..ddc5081 --- /dev/null +++ b/app/FingerLogic.h @@ -0,0 +1,105 @@ +#ifndef FINGERLOGIC_H +#define FINGERLOGIC_H + +#include +#include "FingerPrint.h" + +enum FingerLogicState +{ + STATE_ERROR = -1, + STATE_INITIAL = 0, + + STATE_INIT_VERIFYPASSWORD, + STATE_INIT_READSYSTEMPARAMETERS, + STATE_INIT_READTEMPLATEMAP, + STATE_INIT_VERIFYTEMPLATES, + + STATE_READY, + + STATE_VERIFY_READIMAGE, + STATE_VERIFY_CONVERTIMAGE, + STATE_VERIFY_SEARCHTEMPLATE, + STATE_VERIFY_LOADTEMPLATE, + STATE_VERIFY_DOWNLOADCHARACTERISTICS, + + STATE_ENROLL_WAITFINGER1, + STATE_ENROLL_READIMAGE1, + STATE_ENROLL_CONVERTIMAGE1, + STATE_ENROLL_SEARCHTEMPLATE, + STATE_ENROLL_WAITFINGER2, + STATE_ENROLL_READIMAGE2, + STATE_ENROLL_CONVERTIMAGE2, + STATE_ENROLL_COMPARE, + STATE_ENROLL_CREATETEMPLATE, + STATE_ENROLL_STORETEMPLATE, + STATE_ENROLL_LOADTEMPLATE, + STATE_ENROLL_DOWNLOADCHARACTERISTICS +}; + +class CFingerLogic +{ +public: + CFingerLogic(class CMain *pMain); + void Init(CFingerPrint *pFingerPrint); + void OnFingerInterrupt(bool finger); + + bool FingerSlot(uint16_t index); + bool FingerSlot(uint16_t index, bool value); + int FirstFreeFingerSlot(); + + void InitFinger(); + static void InitFinger_OnVerifyPassword(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void InitFinger_OnReadSystemParameters(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, CFingerSystemParameters *param); + static void InitFinger_OnGetTemplates(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void InitFinger_OnReadTemplateMap(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, uint8_t *pData, uint16_t dataLen); + void InitFinger_VerifyTemplates(); + static void InitFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void InitFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen); + + void VerifyFinger(); + static void VerifyFinger_OnReadImage(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void VerifyFinger_OnConvertImage(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void VerifyFinger_OnSearchTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t position, int16_t score); + static void VerifyFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void VerifyFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen); + + bool EnrollFinger(bool cancel=false); + static void EnrollFinger_OnReadImage1(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnConvertImage1(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnSearchTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t position, int16_t score); + void EnrollFinger_OnFinger(); + static void EnrollFinger_OnReadImage2(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnConvertImage2(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnCompareCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int16_t score); + static void EnrollFinger_OnCreateTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnStoreTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, uint16_t positionNumber); + static void EnrollFinger_OnLoadTemplate(CFingerLogic *pThis, FingerPrintError error, const char *errorStr); + static void EnrollFinger_OnDownloadCharacteristics(CFingerLogic *pThis, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen); + + CMain &Main() { return *m_pMain; } + CFingerPrint &FingerPrint() { return *m_pFingerPrint; } + +private: + bool m_Power; + void PowerOff(); + void PowerOn(); + void SetState(FingerLogicState state); + Timer m_PowerOffTimer; + + CMain *m_pMain; + CFingerPrint *m_pFingerPrint; + FingerLogicState m_State; + bool m_Finger; + + CFingerSystemParameters m_FingerParams; + uint8_t m_aFingerSlots[1792/8]; + uint16_t m_FingerSlotsAdded; + + int32_t m_iBuffer; + + Timer m_Timer; + void *m_fnUserCallback; + void *m_pUserData; +}; + +#endif diff --git a/app/FingerPrint.cpp b/app/FingerPrint.cpp new file mode 100644 index 0000000..5509565 --- /dev/null +++ b/app/FingerPrint.cpp @@ -0,0 +1,267 @@ +#include +#include "FingerPrint.h" + +static struct CFingerErrorsExplain +{ + uint8_t error; + const char *str; +} gs_FingerErrors[] = { + {ERROR_OK, "ERROR_OK: command execution complete"}, + {ERROR_COMMUNICATION, "ERROR_COMMUNICATION: error when receiving data package"}, + {ERROR_NOFINGER, "ERROR_NOFINGER: no finger on the sensor"}, + {ERROR_READIMAGE, "ERROR_READIMAGE: fail to enroll the finger"}, + {ERROR_MESSYIMAGE, "ERROR_MESSYIMAGE: fail to generate character file due to the over-disorderly fingerprint image"}, + {ERROR_FEWFEATUREPOINTS, "ERROR_FEWFEATUREPOINTS: fail to generate character file due to lackness of character point or over-smallness of fingerprint image"}, + {ERROR_NOTMATCHING, "ERROR_NOTMATCHING: finger doesn't match"}, + {ERROR_NOTEMPLATEFOUND, "ERROR_NOTEMPLATEFOUND: fail to find the matching finger"}, + {ERROR_CHARACTERISTICSMISMATCH, "ERROR_CHARACTERISTICSMISMATCH: fail to combine the character files"}, + {ERROR_INVALIDPOSITION, "ERROR_INVALIDPOSITION: addressing PageID is beyond the finger library"}, + {ERROR_LOADTEMPLATE, "ERROR_LOADTEMPLATE: error when reading template from library or the template is invalid"}, + {ERROR_UPLOADTEMPLATE, "ERROR_UPLOADTEMPLATE: error when uploading template"}, + {ERROR_PACKETRESPONSEFAIL, "ERROR_PACKETRESPONSEFAIL: Module can't receive the following data packages"}, + {ERROR_UPLOADIMAGE, "ERROR_UPLOADIMAGE: error when uploading image"}, + {ERROR_DELETETEMPLATE, "ERROR_DELETETEMPLATE: fail to delete the template"}, + {ERROR_CLEARDATABASE, "ERROR_CLEARDATABASE: fail to clear finger library"}, + {ERROR_WRONGPASSWORD, "ERROR_WRONGPASSWORD: wrong password"}, + {ERROR_INVALIDIMAGE, "ERROR_INVALIDIMAGE: fail to generate the image for the lackness of valid primary image"}, + {ERROR_FLASH, "ERROR_FLASH: error when writing flash"}, + {ERROR_NODEF, "ERROR_NODEF: No definition error"}, + {ERROR_INVALIDREGISTER, "ERROR_INVALIDREGISTER: invalid register number"}, + {ERROR_INCORRECTREGISTERCONF, "ERROR_INCORRECTREGISTERCONF: incorrect configuration of register"}, + {ERROR_INVALIDNOTEPADPAGE, "ERROR_INVALIDNOTEPADPAGE: wrong notepad page number"}, + {ERROR_COMMUNICATIONPORT, "ERROR_COMMUNICATIONPORT: fail to operate the communication port"} +}; + +const char *CFingerPrint::ExplainFingerError(uint8_t error) +{ + CFingerErrorsExplain *pFound = NULL; + for(int i = 0; i < sizeof(gs_FingerErrors) / sizeof(*gs_FingerErrors); i++) + { + if(error == gs_FingerErrors[i].error) + { + pFound = &gs_FingerErrors[i]; + break; + } + } + + if(pFound) + return pFound->str; + + return "Unknown error."; +} + +CFingerPrint::CFingerPrint() +{ + m_RecvState = RECV_DROP; +} + +void CFingerPrint::Init(HardwareSerial &serial, uint32_t address, uint32_t password) +{ + if(m_pSerial) + return; + + m_pSerial = &serial; + m_Address = address; + m_Password = password; + + m_pSerial->onDataReceived(StreamDataReceivedDelegate(&CFingerPrint::OnData, this)); +} + +void CFingerPrint::OnData(Stream &stream, char arrivedChar, unsigned short availableCharsCount) +{ + if(m_RecvState == RECV_DONE) + return; + + if(m_RecvState == RECV_DROP) + { + while(stream.available()) + stream.read(); + + return; + } + + // RECV_WAITING + while(stream.available()) + { + uint8_t cur = stream.read(); + + if(m_RecvIndex == 0 && cur != 0xEF || m_RecvIndex == 1 && cur != 0x01) + { + debugf("skip garbage header at %d: %X", m_RecvIndex, cur); + m_RecvIndex = 0; + continue; + } + + m_aRecvBuffer[m_RecvIndex++] = cur; + + // Packet could be complete (the minimal packet size is 12 bytes) + if(m_RecvIndex >= 12) + { + // Check the packet header (redundant) + uint16_t header = m_aRecvBuffer[0] << 8 | m_aRecvBuffer[1]; + if(header != 0xEF01) + { + debugf("wrong header: %X", header); + m_RecvIndex = 0; + continue; + } + + // Calculate packet payload length + uint16_t length = m_aRecvBuffer[7] << 8 | m_aRecvBuffer[8]; + + // Check if the packet is still fully received + // Condition: index counter < packet payload length + packet frame + if(m_RecvIndex < length + 9) + continue; + + // At this point the packet should be fully received + uint8_t ident = m_aRecvBuffer[6]; + + // Calculate checksum: + // Checksum = packet type (1 byte) + packet length RAW!! (2 bytes) + packet payload (n bytes) + uint16_t calcChecksum = ident; + const uint16_t tmp = 7 + length; + for(uint16_t i = 7; i < tmp; i++) + calcChecksum += m_aRecvBuffer[i]; + + // Checksum in received data package + uint16_t recvChecksum = m_aRecvBuffer[m_RecvIndex - 2] << 8 | m_aRecvBuffer[m_RecvIndex - 1]; + + if(calcChecksum != recvChecksum) + { + debugf("checksum!!: %X != %X", calcChecksum, recvChecksum); + m_RecvIndex = 0; + continue; + } + + m_RecvIndex = 0; + m_RecvState = RECV_DONE; + + (this->*m_fnRecvCallback)((FingerPrintIdent)ident, &m_aRecvBuffer[9], length - 2); + } + } +} + +int CFingerPrint::Recv(FingerPrintIdent *pIdent, uint8_t *pData, uint16_t maxLength, const int maxTime) +{ + m_RecvState = RECV_DONE; + m_RecvIndex = 0; + + debugf("Start manual recv:"); + int Ret = -9999; + + int timeout = maxTime; + while(true) + { + while(!m_pSerial->available()) + { + if(--timeout == 0) + return Ret; + delay(1); + } + + uint8_t cur = m_pSerial->read(); + m_aRecvBuffer[m_RecvIndex++] = cur; + + // Packet could be complete (the minimal packet size is 12 bytes) + if(m_RecvIndex >= 12) + { + // Check the packet header + uint16_t header = m_aRecvBuffer[0] << 8 | m_aRecvBuffer[1]; + if(header != 0xEF01) + { + debugf("wrong header: %X", header); + m_RecvIndex = 0; + return Ret; + } + + // Calculate packet payload length + uint16_t length = m_aRecvBuffer[7] << 8 | m_aRecvBuffer[8]; + + // Check if the packet is still fully received + // Condition: index counter < packet payload length + packet frame + if(m_RecvIndex < length + 9) + continue; + + // At this point the packet should be fully received + uint8_t ident = m_aRecvBuffer[6]; + + // Calculate checksum: + // Checksum = packet type (1 byte) + packet length RAW! (2 bytes) + packet payload (n bytes) + uint16_t calcChecksum = ident + length; + uint16_t tmp = 9 + length - 2; + for(uint16_t i = 9; i < tmp; i++) + calcChecksum += m_aRecvBuffer[i]; + + // Checksum in received data package + uint16_t recvChecksum = m_aRecvBuffer[m_RecvIndex - 2] << 8 | m_aRecvBuffer[m_RecvIndex - 1]; + + if(calcChecksum != recvChecksum) + { + debugf("checksum!!: %X != %X", calcChecksum, recvChecksum); + m_RecvIndex = 0; + return Ret; + } + + length -= 2; + Ret = 0; + + if(pIdent) + *(uint8_t *)pIdent = ident; + + if(pData) + { + memcpy(pData, &m_aRecvBuffer[9], min(length, maxLength)); + Ret = (int)maxLength - length; // >= 0 = OK + } + + break; + } + } + + debugf("End recv. (%dms)", maxTime - timeout); + m_RecvIndex = 0; + + return Ret; +} + +int CFingerPrint::Write(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_WAITING; + + // Header + const uint16_t Header = 0xEF01; + m_pSerial->write(Header >> 8 & 0xFF); + m_pSerial->write(Header >> 0 & 0xFF); + + // Address + m_pSerial->write(m_Address >> 24 & 0xFF); + m_pSerial->write(m_Address >> 16 & 0xFF); + m_pSerial->write(m_Address >> 8 & 0xFF); + m_pSerial->write(m_Address >> 0 & 0xFF); + + // Package identifier + m_pSerial->write(ident); + + // Package length + length += 2; // Checksum + m_pSerial->write(length >> 8 & 0xFF); + m_pSerial->write(length >> 0 & 0xFF); + + // Checksum + uint16_t Checksum = ident + length; + + // Data + length -= 2; + for(uint16_t i = 0; i < length; i++) + { + m_pSerial->write(pData[i]); + Checksum += pData[i]; + } + + // Checksum + m_pSerial->write(Checksum >> 8 & 0xFF); + m_pSerial->write(Checksum >> 0 & 0xFF); + + return length; +} diff --git a/app/FingerPrint.h b/app/FingerPrint.h new file mode 100644 index 0000000..2bccf4b --- /dev/null +++ b/app/FingerPrint.h @@ -0,0 +1,163 @@ +#ifndef FINGERPRINT_H +#define FINGERPRINT_H + +// Data package identifier +enum FingerPrintIdent +{ + IDENT_COMMAND = 0x01, // Command packet + IDENT_DATA = 0x02, // Data packet; + // Data packet shall not appear alone in executing processs, + // must follow command packet or acknowledge packet. + IDENT_ACK = 0x07, // Acknowledge packet + IDENT_ENDOFDATA = 0x08, // End of Data packet +}; + +enum FingerPrintCommand +{ + COMMAND_GENIMG = 0x01, // Collect finger image + COMMAND_IMG2TZ = 0x02, // To generate character file from image + COMMAND_MATCH = 0x03, // Carry out precise matching of two templates + COMMAND_SEARCH = 0x04, // Search library finger + COMMAND_REGMODEL = 0x05, // To combine character files and generate template + COMMAND_STORE = 0x06, // To store template; + COMMAND_LOADCHAR = 0x07, // to read/load template + COMMAND_UPCHAR = 0x08, // to upload template + COMMAND_DOWNCHR = 0x09, // to download template + COMMAND_UPIMAGE = 0x0A, // To upload image + COMMAND_DOWNIMAGE = 0x0B, // To download image + COMMAND_DELETCHAR = 0x0C, // to delete tempates + COMMAND_EMPTY = 0x0D, // to empty the library + COMMAND_SETSYSPARA = 0x0E, // To set system Parameter + COMMAND_READSYSPARA = 0x0F, // To read Parameter + COMMAND_SETPWD = 0x12, // To set password + COMMAND_VFYPWD = 0x13, // To verify password + COMMAND_GETRANDOMCODE = 0x14, // to get random code + COMMAND_SETADDER = 0x15, // To set device address + COMMAND_CONTROL = 0x17, // Port control + COMMAND_WRITENOTEPAD = 0x18, // to write note pad + COMMAND_READNOTEPAD = 0x19, // To read note pad + COMMAND_HISPEEDSEARCH = 0x1B, // Search the library fastly + COMMAND_TEMPLATENUM = 0x1D, // To read finger template numbers + COMMAND_READCONLIST = 0x1F, // To read finger template index table +}; + +enum FingerPrintError +{ + ERROR_OK = 0x00, // command execution complete + ERROR_COMMUNICATION = 0x01, // error when receiving data package + ERROR_NOFINGER = 0x02, // no finger on the sensor + ERROR_READIMAGE = 0x03, // fail to enroll the finger + ERROR_MESSYIMAGE = 0x06, // fail to generate character file due to the over-disorderly fingerprint image + ERROR_FEWFEATUREPOINTS = 0x07, // fail to generate character file due to lackness of character point or over-smallness of fingerprint image + ERROR_NOTMATCHING = 0x08, // finger doesn't match + ERROR_NOTEMPLATEFOUND = 0x09, // fail to find the matching finger + ERROR_CHARACTERISTICSMISMATCH = 0x0A, // fail to combine the character files + ERROR_INVALIDPOSITION = 0x0B, // addressing PageID is beyond the finger library + ERROR_LOADTEMPLATE = 0x0C, // error when reading template from library or the template is invalid + ERROR_UPLOADTEMPLATE = 0x0D, // error when uploading template + ERROR_PACKETRESPONSEFAIL = 0x0E, // Module can't receive the following data packages + ERROR_UPLOADIMAGE = 0x0F, // error when uploading image + ERROR_DELETETEMPLATE = 0x10, // fail to delete the template + ERROR_CLEARDATABASE = 0x11, // fail to clear finger library + ERROR_WRONGPASSWORD = 0x13, // wrong password + ERROR_INVALIDIMAGE = 0x15, // fail to generate the image for the lackness of valid primary image + ERROR_FLASH = 0x18, // error when writing flash + ERROR_NODEF = 0x19, // No definition error + ERROR_INVALIDREGISTER = 0x1A, // invalid register number + ERROR_INCORRECTREGISTERCONF = 0x1B, // incorrect configuration of register + ERROR_INVALIDNOTEPADPAGE = 0x1C, // wrong notepad page number + ERROR_COMMUNICATIONPORT = 0x1D, // fail to operate the communication port +}; + +enum RecvStates +{ + RECV_DONE = 0, + RECV_WAITING = 1, + RECV_DROP = 2 +}; + +struct CFingerSystemParameters +{ + uint16_t statusRegister; + uint16_t systemID; + uint16_t storageCapacity; + uint16_t securityLevel; + uint32_t deviceAddress; + uint16_t packetLength; + uint16_t baudRate; +}; + +class CFingerPrint; +typedef void (CFingerPrint::*RecvCallback)(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + +typedef void (*VerifyPasswordCallback)(void *pUser, FingerPrintError error, const char *errorStr); +typedef void (*ReadSystemParametersCallback)(void *pUser, FingerPrintError error, const char *errorStr, CFingerSystemParameters *param); +typedef void (*ReadImageCallback)(void *pUser, FingerPrintError error, const char *errorStr); +typedef void (*ConvertImageCallback)(void *pUser, FingerPrintError error, const char *errorStr); +typedef void (*SearchTemplateCallback)(void *pUser, FingerPrintError error, const char *errorStr, int16_t position, int16_t score); +typedef void (*CompareCharacteristicsCallback)(void *pUser, FingerPrintError error, const char *errorStr, int16_t score); +typedef void (*CreateTemplateCallback)(void *pUser, FingerPrintError error, const char *errorStr); +typedef void (*DownloadCharacteristicsCallback)(void *pUser, FingerPrintError error, const char *errorStr, int8_t *pChar, uint16_t charLen); +typedef void (*LoadTemplateCallback)(void *pUser, FingerPrintError error, const char *errorStr); +typedef void (*StoreTemplateCallback)(void *pUser, FingerPrintError error, const char *errorStr, uint16_t positionNumber); +typedef void (*ReadTemplateMapCallback)(void *pUser, FingerPrintError error, const char *errorStr, uint8_t *pData, uint16_t dataLen); + +class CFingerPrint +{ +public: + CFingerPrint(); + void Init(HardwareSerial &serial, uint32_t address, uint32_t password); + + static const char *ExplainFingerError(uint8_t error); + + int VerifyPassword(); + int ReadSystemParameters(uint8_t aResponse[17]); + int DeleteTemplate(uint16_t positionStart, uint16_t count); + int EmptyDatabase(); + + int AsyncVerifyPassword(VerifyPasswordCallback fnCallback, void *pUser); + int AsyncReadSystemParameters(ReadSystemParametersCallback fnCallback, void *pUser); + int AsyncReadImage(ReadImageCallback fnCallback, void *pUser); + int AsyncConvertImage(ConvertImageCallback fnCallback, void *pUser, uint8_t numCharBuffer); + int AsyncSearchTemplate(SearchTemplateCallback fnCallback, void *pUser, uint8_t numCharBuffer, uint16_t positionStart, uint16_t numTemplates); + int AsyncCompareCharacteristics(CompareCharacteristicsCallback fnCallback, void *pUser); + int AsyncCreateTemplate(CreateTemplateCallback fnCallback, void *pUser); + int AsyncDownloadCharacteristics(DownloadCharacteristicsCallback fnCallback, void *pUser, uint8_t numCharBuffer); + int AsyncLoadTemplate(LoadTemplateCallback fnCallback, void *pUser, uint16_t positionNumber, uint8_t numCharBuffer); + int AsyncStoreTemplate(StoreTemplateCallback fnCallback, void *pUser, uint16_t positionNumber, uint8_t numCharBuffer); + int AsyncReadTemplateMap(ReadTemplateMapCallback fnCallback, void *pUser, uint8_t numPage); + +private: + void OnAsyncVerifyPassword(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncReadSystemParameters(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncReadImage(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncConvertImage(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncSearchTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncCompareCharacteristics(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncCreateTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncDownloadCharacteristics(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncLoadTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncStoreTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + void OnAsyncReadTemplateMap(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + + void OnData(Stream &stream, char arrivedChar, unsigned short availableCharsCount); + int Write(FingerPrintIdent ident, uint8_t *pData, uint16_t length); + int Recv(FingerPrintIdent *pIdent, uint8_t *pData, uint16_t maxLength, const int maxTime=100); + + uint32_t m_Address; + uint32_t m_Password; + HardwareSerial *m_pSerial; + + uint8_t m_aRecvBuffer[2+4+1+2+256+2]; + uint16_t m_RecvIndex; + + volatile RecvStates m_RecvState; + RecvCallback m_fnRecvCallback; + void *m_fnUserCallback; + void *m_pUserData; + + uint8_t m_aBuffer[1024]; + int32_t m_iBuffer; +}; + +#endif diff --git a/app/FingerPrint_API.cpp b/app/FingerPrint_API.cpp new file mode 100644 index 0000000..a2ab40b --- /dev/null +++ b/app/FingerPrint_API.cpp @@ -0,0 +1,101 @@ +#include +#include "FingerPrint.h" + +int CFingerPrint::VerifyPassword() +{ + uint8_t aPayload[] = { + COMMAND_VFYPWD, + (uint8_t)(m_Password >> 24 & 0xFF), + (uint8_t)(m_Password >> 16 & 0xFF), + (uint8_t)(m_Password >> 8 & 0xFF), + (uint8_t)(m_Password >> 0 & 0xFF), + }; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + FingerPrintIdent ident; + uint8_t aResponse[1]; + int ret = Recv(&ident, aResponse, sizeof(aResponse)); + m_RecvState = RECV_DROP; + + if(ret < 0) + return ret; + + if(ident != IDENT_ACK) + return ERROR_COMMUNICATION; + + uint8_t error = aResponse[0]; + return error; +} + +int CFingerPrint::ReadSystemParameters(uint8_t aResponse[17]) +{ + uint8_t aPayload[] = { + COMMAND_READSYSPARA, + }; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + FingerPrintIdent ident; + int ret = Recv(&ident, aResponse, 17); + m_RecvState = RECV_DROP; + + if(ret < 0) + return ret; + + if(ident != IDENT_ACK) + return ERROR_COMMUNICATION; + + uint8_t error = aResponse[0]; + return error; +} + +int CFingerPrint::DeleteTemplate(uint16_t positionStart, uint16_t count) +{ + uint8_t aPayload[] = { + COMMAND_DELETCHAR, + (uint8_t)(positionStart >> 8 & 0xFF), + (uint8_t)(positionStart >> 0 & 0xFF), + (uint8_t)(count >> 8 & 0xFF), + (uint8_t)(count >> 0 & 0xFF), + }; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + FingerPrintIdent ident; + uint8_t aResponse[1]; + int ret = Recv(&ident, aResponse, sizeof(aResponse)); + m_RecvState = RECV_DROP; + + if(ret < 0) + return ret; + + if(ident != IDENT_ACK) + return ERROR_COMMUNICATION; + + uint8_t error = aResponse[0]; + return error; +} + +int CFingerPrint::EmptyDatabase() +{ + uint8_t aPayload[] = { + COMMAND_EMPTY, + }; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + FingerPrintIdent ident; + uint8_t aResponse[1]; + int ret = Recv(&ident, aResponse, sizeof(aResponse)); + m_RecvState = RECV_DROP; + + if(ret < 0) + return ret; + + if(ident != IDENT_ACK) + return ERROR_COMMUNICATION; + + uint8_t error = aResponse[0]; + return error; +} diff --git a/app/FingerPrint_AsyncAPI.cpp b/app/FingerPrint_AsyncAPI.cpp new file mode 100644 index 0000000..8fcbe50 --- /dev/null +++ b/app/FingerPrint_AsyncAPI.cpp @@ -0,0 +1,510 @@ +#include +#include "FingerPrint.h" + +int CFingerPrint::AsyncVerifyPassword(VerifyPasswordCallback fnCallback, void *pUser) +{ + uint8_t aPayload[] = { + COMMAND_VFYPWD, + (uint8_t)(m_Password >> 24 & 0xFF), + (uint8_t)(m_Password >> 16 & 0xFF), + (uint8_t)(m_Password >> 8 & 0xFF), + (uint8_t)(m_Password >> 0 & 0xFF), + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncVerifyPassword; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncVerifyPassword(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((VerifyPasswordCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!"); + return; + } + + if(length != 1) + { + ((VerifyPasswordCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!"); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((VerifyPasswordCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr); +} + + +int CFingerPrint::AsyncReadSystemParameters(ReadSystemParametersCallback fnCallback, void *pUser) +{ + uint8_t aPayload[] = { + COMMAND_READSYSPARA, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncReadSystemParameters; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncReadSystemParameters(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((ReadSystemParametersCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", NULL); + return; + } + + if(length != 1 + sizeof(CFingerSystemParameters)) + { + ((ReadSystemParametersCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!", NULL); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + CFingerSystemParameters *param = (CFingerSystemParameters *)m_aBuffer; + param->statusRegister = pData[1] << 8 | pData[2]; + param->systemID = pData[3] << 8 | pData[4]; + param->storageCapacity = pData[5] << 8 | pData[6]; + param->securityLevel = pData[7] << 8 | pData[8]; + param->deviceAddress = pData[9] << 24 | pData[10] << 16 | pData[11] << 8 | pData[12]; + param->packetLength = pData[13] << 8 | pData[14]; + param->baudRate = pData[15] << 8 | pData[16]; + + ((ReadSystemParametersCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, param); +} + + +int CFingerPrint::AsyncReadImage(ReadImageCallback fnCallback, void *pUser) +{ + uint8_t aPayload[] = { + COMMAND_GENIMG, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncReadImage; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncReadImage(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((ReadImageCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!"); + return; + } + + if(length != 1) + { + ((ReadImageCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!"); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((ReadImageCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr); +} + + +int CFingerPrint::AsyncConvertImage(ConvertImageCallback fnCallback, void *pUser, uint8_t numCharBuffer) +{ + if(numCharBuffer != 0x01 && numCharBuffer != 0x02) + return -1; + + uint8_t aPayload[] = { + COMMAND_IMG2TZ, + numCharBuffer, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncConvertImage; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncConvertImage(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((ConvertImageCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!"); + return; + } + + if(length != 1) + { + ((ConvertImageCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!"); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((ConvertImageCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr); +} + + +int CFingerPrint::AsyncSearchTemplate(SearchTemplateCallback fnCallback, void *pUser, uint8_t numCharBuffer, uint16_t positionStart, uint16_t numTemplates) +{ + if(numCharBuffer != 0x01 && numCharBuffer != 0x02) + return -1; + + uint8_t aPayload[] = { + COMMAND_SEARCH, + numCharBuffer, + (uint8_t)(positionStart >> 8 & 0xFF), + (uint8_t)(positionStart >> 0 & 0xFF), + (uint8_t)(numTemplates >> 8 & 0xFF), + (uint8_t)(numTemplates >> 0 & 0xFF), + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncSearchTemplate; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncSearchTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + int16_t position = -1; + int16_t score = -1; + + if(ident != IDENT_ACK) + { + ((SearchTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", position, score); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + if(error == ERROR_OK) + { + if(length != 5) + { + ((SearchTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!", position, score); + return; + } + + position = pData[1] << 8 | pData[2]; + score = pData[3] << 8 | pData[4]; + } + + ((SearchTemplateCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, position, score); +} + + +int CFingerPrint::AsyncCompareCharacteristics(CompareCharacteristicsCallback fnCallback, void *pUser) +{ + uint8_t aPayload[] = { + COMMAND_MATCH, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncCompareCharacteristics; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncCompareCharacteristics(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + int16_t score = -1; + + if(ident != IDENT_ACK) + { + ((CompareCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", score); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + if(error == ERROR_OK) + { + if(length != 3) + { + ((CompareCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!", score); + return; + } + + score = pData[1] << 8 | pData[2]; + } + + ((CompareCharacteristicsCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, score); +} + + +int CFingerPrint::AsyncCreateTemplate(CreateTemplateCallback fnCallback, void *pUser) +{ + uint8_t aPayload[] = { + COMMAND_REGMODEL, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncCreateTemplate; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncCreateTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((CreateTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!"); + return; + } + + if(length != 1) + { + ((CreateTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!"); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((CreateTemplateCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr); +} + + +int CFingerPrint::AsyncDownloadCharacteristics(DownloadCharacteristicsCallback fnCallback, void *pUser, uint8_t numCharBuffer) +{ + if(numCharBuffer != 0x01 && numCharBuffer != 0x02) + return -1; + + uint8_t aPayload[] = { + COMMAND_UPCHAR, + numCharBuffer, + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncDownloadCharacteristics; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + m_iBuffer = -1; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncDownloadCharacteristics(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + if(m_iBuffer == -1) + { + if(ident != IDENT_ACK) + { + m_RecvState = RECV_DROP; + ((DownloadCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", NULL, 0); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + if(error != ERROR_OK) + { + m_RecvState = RECV_DROP; + ((DownloadCharacteristicsCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, NULL, 0); + return; + } + + m_iBuffer = 0; + m_RecvState = RECV_WAITING; + return; + } + + if(ident != IDENT_DATA && ident != IDENT_ENDOFDATA) + { + m_RecvState = RECV_DROP; + ((DownloadCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no data packet", NULL, 0); + return; + } + + if(sizeof(m_aBuffer) - m_iBuffer < length) + { + m_RecvState = RECV_DROP; + ((DownloadCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Not enough space to store received data", NULL, 0); + return; + } + + memcpy(&m_aBuffer[m_iBuffer], pData, length); + m_iBuffer += length; + + if(ident == IDENT_ENDOFDATA) + { + m_RecvState = RECV_DROP; + ((DownloadCharacteristicsCallback)m_fnUserCallback)(m_pUserData, ERROR_OK, ExplainFingerError(ERROR_OK), (int8_t *)m_aBuffer, m_iBuffer); + return; + } + + m_RecvState = RECV_WAITING; +} + + +int CFingerPrint::AsyncLoadTemplate(LoadTemplateCallback fnCallback, void *pUser, uint16_t positionNumber, uint8_t numCharBuffer) +{ + if(numCharBuffer != 0x01 && numCharBuffer != 0x02) + return -1; + + uint8_t aPayload[] = { + COMMAND_LOADCHAR, + numCharBuffer, + (uint8_t)(positionNumber >> 8 & 0xFF), + (uint8_t)(positionNumber >> 0 & 0xFF), + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncLoadTemplate; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncLoadTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((LoadTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!"); + return; + } + + if(length != 1) + { + ((LoadTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!"); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((LoadTemplateCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr); +} + + +int CFingerPrint::AsyncStoreTemplate(StoreTemplateCallback fnCallback, void *pUser, uint16_t positionNumber, uint8_t numCharBuffer) +{ + if(numCharBuffer != 0x01 && numCharBuffer != 0x02) + return -1; + + uint8_t aPayload[] = { + COMMAND_STORE, + numCharBuffer, + (uint8_t)(positionNumber >> 8 & 0xFF), + (uint8_t)(positionNumber >> 0 & 0xFF), + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncStoreTemplate; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + m_iBuffer = positionNumber; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncStoreTemplate(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((StoreTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", m_iBuffer); + return; + } + + if(length != 1) + { + ((StoreTemplateCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "Incorrect data length!", m_iBuffer); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + ((StoreTemplateCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, m_iBuffer); +} + + +int CFingerPrint::AsyncReadTemplateMap(ReadTemplateMapCallback fnCallback, void *pUser, uint8_t numPage) +{ + uint8_t aPayload[] = { + COMMAND_READCONLIST, + numPage + }; + + m_fnRecvCallback = &CFingerPrint::OnAsyncReadTemplateMap; + m_fnUserCallback = (void *)fnCallback; + m_pUserData = pUser; + + Write(IDENT_COMMAND, aPayload, sizeof(aPayload)); + + return 0; +} + +void CFingerPrint::OnAsyncReadTemplateMap(FingerPrintIdent ident, uint8_t *pData, uint16_t length) +{ + m_RecvState = RECV_DROP; + + if(ident != IDENT_ACK) + { + ((ReadTemplateMapCallback)m_fnUserCallback)(m_pUserData, ERROR_COMMUNICATION, "The received packet is no ack packet!", NULL, 0); + return; + } + + uint8_t error = pData[0]; + const char *errorStr = ExplainFingerError(error); + + if(error != ERROR_OK) + { + ((ReadTemplateMapCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, NULL, 0); + return; + } + + ((ReadTemplateMapCallback)m_fnUserCallback)(m_pUserData, (FingerPrintError)error, errorStr, &pData[1], length - 1); +} diff --git a/app/Settings.cpp b/app/Settings.cpp new file mode 100644 index 0000000..4a7fa0d --- /dev/null +++ b/app/Settings.cpp @@ -0,0 +1,118 @@ +#include +#include +#include "utils.h" +#include "Settings.h" + +CSettings::CSettings() +{ + strcpy(m_aUsername, "admin"); + strcpy(m_aPassword, "admin"); + strcpy(m_aSSID, ""); + strcpy(m_aPSK, ""); + m_DHCP = true; + strcpy(m_aHostname, "safeweb"); + m_Address = IPAddress(0, 0, 0, 0); + m_Netmask = IPAddress(0, 0, 0, 0); + m_Gateway = IPAddress(0, 0, 0, 0); + memset(m_aLockCode, 0, sizeof(m_aLockCode)); +} + +bool CSettings::Exists() +{ + return fileExist(APP_SETTINGS_FILE); +} + +bool CSettings::Load() +{ + if(!Exists()) + { + Save(); + return true; + } + + uint32_t size = fileGetSize(APP_SETTINGS_FILE); + char *pData = new char[size + 1]; + fileGetContent(APP_SETTINGS_FILE, pData, size + 1); + + DynamicJsonDocument doc(1024); + if(deserializeJson(doc, pData)) + { + delete[] pData; + return false; + } + + strncpy(m_aUsername, doc["username"], sizeof(m_aUsername)); + strncpy(m_aPassword, doc["password"], sizeof(m_aPassword)); + + JsonObject network = doc["network"]; + strncpy(m_aSSID, network["ssid"], sizeof(m_aSSID)); + strncpy(m_aPSK, network["passphrase"], sizeof(m_aPSK)); + m_DHCP = network["dhcp"]; + strncpy(m_aHostname, network["hostname"], sizeof(m_aHostname)); + m_Address = network["address"].as(); + m_Netmask = network["netmask"].as(); + m_Gateway = network["gateway"].as(); + + JsonArray lockCode = doc["lock_code"]; + uint8_t tmp = min(lockCode.size(), sizeof(m_aLockCode)); + for(uint8_t i = 0; i < tmp; i++) + m_aLockCode[i] = lockCode[i]; + + JsonArray fingerprints = doc["fingerprints"]; + uint16_t sz = fingerprints.size(); + m_FingerPrints.allocate(sz); + for(uint16_t i = 0; i < sz; i++) + { + JsonObject obj = fingerprints[i]; + CFingerPrint finger; + + finger.m_FingerNum = obj["num"]; + strncpy(finger.m_aLabel, obj["label"], sizeof(finger.m_aLabel)); + hex2bytes(obj["digest"].as(), finger.m_aDigest, sizeof(finger.m_aDigest)); + + m_FingerPrints[finger.m_FingerNum] = finger; + } + + delete[] pData; + return true; +} + +void CSettings::Save() +{ + DynamicJsonDocument doc(1024); + + doc["username"] = m_aUsername; + doc["password"] = m_aPassword; + + JsonObject network = doc.createNestedObject("network"); + network["ssid"] = m_aSSID; + network["passphrase"] = m_aPSK; + network["dhcp"] = m_DHCP; + network["hostname"] = m_aHostname; + network["address"] = m_Address.toString(); + network["netmask"] = m_Netmask.toString(); + network["gateway"] = m_Gateway.toString(); + + JsonArray lockCode = doc.createNestedArray("lock_code"); + for(uint8_t i = 0; i < sizeof(m_aLockCode); i++) + lockCode.add(m_aLockCode[i]); + + JsonArray fingerprints = doc.createNestedArray("fingerprints"); + uint16_t tmp = m_FingerPrints.count(); + for(uint16_t i = 0; i < tmp; i++) + { + JsonObject obj = fingerprints.createNestedObject(); + const CFingerPrint &finger = m_FingerPrints.valueAt(i); + + obj["num"] = finger.m_FingerNum; + obj["label"] = String(finger.m_aLabel); + + char aHexDigest[SHA256_SIZE*2+1]; + bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest)); + obj["digest"] = String(aHexDigest); + } + + String docString; + serializeJsonPretty(doc, docString); + fileSetContent(APP_SETTINGS_FILE, docString); +} diff --git a/app/Settings.h b/app/Settings.h new file mode 100644 index 0000000..d5a71fb --- /dev/null +++ b/app/Settings.h @@ -0,0 +1,38 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#define APP_SETTINGS_FILE "settings.conf" + +class CSettings +{ +public: + CSettings(); + + bool Exists(); + bool Load(); + void Save(); + + char m_aUsername[32]; + char m_aPassword[32]; + + char m_aSSID[32+1]; + char m_aPSK[64+1]; + + bool m_DHCP; + char m_aHostname[64]; + IpAddress m_Address; + IpAddress m_Netmask; + IpAddress m_Gateway; + + uint8_t m_aLockCode[8]; + + struct CFingerPrint + { + uint16_t m_FingerNum; + char m_aLabel[32]; + uint8_t m_aDigest[SHA256_SIZE]; + }; + HashMap m_FingerPrints; +}; + +#endif diff --git a/app/application.cpp b/app/application.cpp new file mode 100644 index 0000000..7dc4bbb --- /dev/null +++ b/app/application.cpp @@ -0,0 +1,54 @@ +#include +#include "FingerPrint.h" +#include "main.h" + +HardwareSerial Serial1(UART_ID_1); +NtpClient ntpClient("at.pool.ntp.org", 3600); + +void IRAM_ATTR OnFingerInterrupt() +{ + // LOW = FINGER, HIGH = NO FINGER + bool status = digitalRead(FINGER_DETECT_PIN); + + g_Main.OnFingerInterrupt(!status); +} + +void ready() +{ + debugf("READY!"); + + gpio_pin_wakeup_enable(GPIO_ID_PIN(FINGER_DETECT_PIN), GPIO_PIN_INTR_LOLEVEL); + + g_Main.Init(Serial); + + attachInterrupt(FINGER_DETECT_PIN, OnFingerInterrupt, CHANGE); +} + +void init() +{ + // for fingerprint module + Serial.systemDebugOutput(false); + Serial.begin(115200, SERIAL_8N1, SERIAL_FULL); + Serial.flush(); // TODO: Full flush? + + // for debug messages + Serial1.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY); + Serial1.systemDebugOutput(true); + + // p-channel FET to turn on fingerprint module + pinMode(FINGER_ENABLE_PIN, OUTPUT_OPEN_DRAIN); + digitalWrite(FINGER_ENABLE_PIN, 1); + + // communication with safe lock + pinMode(SAFELOCK_DATA_PIN, OUTPUT_OPEN_DRAIN); + digitalWrite(SAFELOCK_DATA_PIN, 1); + + pinMode(FINGER_DETECT_PIN, INPUT); + pinMode(SAFELOCK_DETECT_PIN, INPUT); + pinMode(DOOR_DETECT_PIN, INPUT); + + // mount spiffs + spiffs_mount(); + + System.onReady(ready); +} diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..3fd98fd --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,462 @@ +#include +#include +#include +#include "utils.h" +#include "main.h" + +CMain g_Main; + +CMain::CMain() : m_FingerLogic(this) +{ +} + +static void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway) +{ + debugf("GOTIP - IP: %s, MASK: %s, GW: %s\n", ip.toString().c_str(), mask.toString().c_str(), + gateway.toString().c_str()); +} + +static void STADisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) +{ + debugf("DISCONNECT - SSID: %s, REASON: %d", ssid.c_str(), WifiEvents.getDisconnectReasonDesc(reason).c_str()); +} + +void CMain::Init(HardwareSerial &serial) +{ + m_Settings.Load(); + //m_Settings.Save(); + + WifiAccessPoint.enable(false); + if(m_Settings.m_aSSID[0]) + { + debugf("Station: %s", m_Settings.m_aSSID); + WifiStation.enable(true); + WifiStation.config(m_Settings.m_aSSID, m_Settings.m_aPSK); + if(m_Settings.m_DHCP && m_Settings.m_aHostname[0]) + WifiStation.setHostname(m_Settings.m_aHostname); + if(!m_Settings.m_DHCP && !m_Settings.m_Address.isNull()) + WifiStation.setIP(m_Settings.m_Address, m_Settings.m_Netmask, m_Settings.m_Gateway); + WifiStation.connect(); + } + else + { + debugf("Access Point 'admin': %s", m_Settings.m_aPassword); + WifiAccessPoint.config("safeweb", m_Settings.m_aPassword, AUTH_WPA2_PSK); + WifiAccessPoint.enable(true); + } + + WifiEvents.onStationGotIP(STAGotIP); + WifiEvents.onStationDisconnect(STADisconnect); + + m_FTP.listen(21); + m_FTP.addUser(m_Settings.m_aUsername, m_Settings.m_aPassword); + + m_HttpServer.listen(80); + m_HttpServer.setBodyParser("application/json", bodyToStringParser); + + m_HttpServer.paths.set("/api", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/dashboard", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/fingerprint", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/fingerprint/label", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/fingerprint/delete", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/fingerprint/enroll", HttpPathDelegate(&CMain::HttpOnApi, this)); + m_HttpServer.paths.set("/api/unlock", HttpPathDelegate(&CMain::HttpOnApi, this)); + + m_HttpServer.paths.setDefault(HttpPathDelegate(&CMain::HttpOnFile, this)); + + m_FingerPrint.Init(serial, 0xFFFFFFFF, 0x00000000); + m_FingerLogic.Init(&m_FingerPrint); +} + +bool CMain::HttpAuthorized(HttpRequest &request, HttpResponse &response) +{ + String auth = request.getHeader("Authorization"); + if(auth.startsWith("Basic ")) + { + int headerLength = auth.length() - 6; + if(headerLength <= 64) + { + auth = base64_decode(auth.c_str() + 6, headerLength); + if(auth) + { + int sep = auth.indexOf(':'); + if(sep != -1) + { + String username = auth.substring(0, sep); + String password = auth.substring(sep + 1); + if(username == m_Settings.m_aUsername && password == m_Settings.m_aPassword) + return true; + } + } + } + } + + response.code = HTTP_STATUS_UNAUTHORIZED; + response.setHeader("WWW-Authenticate", "Basic realm=\"safeweb\""); + response.setHeader("401 Wrong credentials", "Authentication required"); + response.setHeader("Connection", "close"); + return false; +} + +void CMain::HttpOnApi(HttpRequest &request, HttpResponse &response) +{ + if(!HttpAuthorized(request, response)) + return; + + String path = request.uri.Path; + if(path.length() < 6) + { + response.code = HTTP_STATUS_NOT_FOUND; + return; + } + String endpoint = path.substring(5); + + DynamicJsonDocument jsonReq(1024); + deserializeJson(jsonReq, request.getBody()); + DynamicJsonDocument jsonResp(1024); + + response.setAllowCrossDomainOrigin("*"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if(request.method == HTTP_OPTIONS) + { + response.code = HTTP_STATUS_OK; + return; + } + + + HttpStatus status = HandleApi(request.method, endpoint, jsonReq, jsonResp); + if(status != HTTP_STATUS_OK) + { + response.code = status; + return; + } + + String respString; + serializeJson(jsonResp, respString); + response.setContentType(MIME_JSON); + response.sendString(respString); +} + +HttpStatus CMain::HandleApi(HttpMethod method, String endpoint, JsonDocument &req, JsonDocument &resp) +{ + String test; + serializeJsonPretty(req, test); + + debugf("request: %s\n", test.c_str()); + + if(endpoint == "dashboard") + { + if(method != HTTP_GET) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + JsonObject lock = resp.createNestedObject("lock"); + lock["locked"] = true; + + JsonObject door = resp.createNestedObject("door"); + door["closed"] = true; + + JsonObject battery = resp.createNestedObject("battery"); + debugf("adc: %d\n", system_adc_read()); + battery["voltage"] = (system_adc_read() / 1024.f) / 0.23; + battery["color"] = "success"; + if(battery["voltage"] < 3.6f) + battery["color"] = "warning"; + + JsonObject network = resp.createNestedObject("network"); + if(WifiStation.isEnabled()) + { + network["type"] = "Station"; + network["ssid"] = WifiStation.getSSID(); + network["channel"] = WifiStation.getChannel(); + network["dhcp"] = WifiStation.isEnabledDHCP(); + network["rssi"] = WifiStation.getRssi(); + network["signal"] = Rssi2Quality(WifiStation.getRssi()); + network["address"] = WifiStation.getIP().toString(); + } + else + { + network["type"] = "Access Point"; + network["ssid"] = WifiAccessPoint.getSSID(); + network["channel"] = WifiStation.getChannel(); + network["dhcp"] = true; + network["address"] = WifiAccessPoint.getIP().toString(); + } + } + else if(endpoint == "unlock") + { + if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + LockUnlock(); + resp["success"] = "true"; + } + else if(endpoint == "fingerprint") + { + if(method != HTTP_GET) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + JsonArray fingerprints = resp.createNestedArray("fingerprints"); + uint16_t tmp = Settings().m_FingerPrints.count(); + for(uint16_t i = 0; i < tmp; i++) + { + JsonObject obj = fingerprints.createNestedObject(); + const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(i); + + obj["num"] = finger.m_FingerNum; + obj["label"] = String(finger.m_aLabel); + + char aHexDigest[SHA256_SIZE*2+1]; + bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest)); + obj["digest"] = String(aHexDigest); + } + } + else if(endpoint == "fingerprint/label") + { + if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + if(!req.containsKey("index") || !req.containsKey("label")) + return HTTP_STATUS_BAD_REQUEST; + + int index = req["index"].as() - 1; + String label = req["label"]; + + if(index < 0 || index >= Settings().m_FingerPrints.count()) + return HTTP_STATUS_BAD_REQUEST; + + CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index); + strncpy(finger.m_aLabel, label.c_str(), sizeof(finger.m_aLabel)); + Settings().Save(); + + resp["success"] = true; + } + else if(endpoint == "fingerprint/delete") + { + if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + if(!req.containsKey("index")) + return HTTP_STATUS_BAD_REQUEST; + + int index = req["index"].as() - 1; + + if(index < 0 || index >= Settings().m_FingerPrints.count()) + return HTTP_STATUS_BAD_REQUEST; + + const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(index); + FingerPrint().DeleteTemplate(finger.m_FingerNum, 1); + Settings().m_FingerPrints.removeAt(index); + Settings().Save(); + + resp["success"] = true; + } + else if(endpoint == "fingerprint/enroll") + { + if(method != HTTP_POST) return HTTP_STATUS_METHOD_NOT_ALLOWED; + + if(req.containsKey("cancel")) + { + if(req["cancel"].as() && m_Enrolling) + { + resp["success"] = FingerLogic().EnrollFinger(false); + m_Enrolling = false; + m_Enrolled = false; + } + } + else + { + if(!req.containsKey("enrolling")) + return HTTP_STATUS_BAD_REQUEST; + + bool enrolling = req["enrolling"]; + if(enrolling && !m_Enrolling) + { + if(m_Enrolled) + { + resp["done"] = true; + resp["message"] = m_EnrollMessage; + m_Enrolled = false; + } + else + { + resp["error"] = m_EnrollMessage; + } + } + else if(enrolling && m_Enrolling) + { + resp["done"] = false; + resp["message"] = m_EnrollMessage; + } + else // if(!enrolling) + { + if(!req.containsKey("label")) + return HTTP_STATUS_BAD_REQUEST; + + if(!m_Enrolling) + { + FingerLogic().EnrollFinger(); + m_Enrolling = true; + m_Enrolled = false; + m_EnrollLabel = String(req["label"].as()); + resp["done"] = false; + m_EnrollMessage = "Started enroll process. Please place your finger on the sensor."; + resp["message"] = m_EnrollMessage; + } + else + { + resp["done"] = true; + m_Enrolled = false; + resp["message"] = m_EnrollMessage; + } + } + } + } + else + { + return HTTP_STATUS_NOT_FOUND; + } + + return HTTP_STATUS_OK; +} + +void CMain::HttpOnFile(HttpRequest &request, HttpResponse &response) +{ + if(!HttpAuthorized(request, response)) + return; + + String file = request.uri.Path; + + if(file == "/") + file = "index.html"; + if(file[0] == '/') + file = file.substring(1); + if(file[0] == '.') + { + response.code = HTTP_STATUS_FORBIDDEN; + return; + } + + response.setCache(86400, true); // It's important to use cache for better performance. + response.sendFile(file); +} + +void CMain::OnFingerInterrupt(bool finger) +{ + m_FingerLogic.OnFingerInterrupt(finger); +} + +void CMain::FingerEnable(bool enable) +{ + const int pin = FINGER_ENABLE_PIN; + digitalWrite(pin, !enable); +} + +void CMain::LockSendBytes(uint8_t *pBytes, uint8_t len) +{ + const int pin = SAFELOCK_DATA_PIN; + + // Init + digitalWrite(pin, 0); + delayMicroseconds(10 * 1000); + digitalWrite(pin, 1); + delayMicroseconds(10 * 1000); + + // Send data, calculate checksum and send checksum + uint8_t chk = 0x00; + for(uint8_t i = 0; i <= len; i++) + { + uint8_t byte; + if(i == len) + { + byte = chk; + } + else + { + byte = pBytes[i]; + chk ^= byte; + } + + for(int8_t j = 0; j < 8; j++) + { + digitalWrite(pin, 0); + delayMicroseconds(100); + digitalWrite(pin, 1); + + uint8_t val = byte & (1 << j); + if(val) + delayMicroseconds(300); + else + delayMicroseconds(100); + } + + digitalWrite(pin, 0); + delayMicroseconds(100); + digitalWrite(pin, 1); + delayMicroseconds(8); + } +} + +void CMain::LockSendCode(uint8_t pass[8]) +{ + uint8_t packet[9] = {0x51}; + memcpy(&packet[1], pass, 8); + LockSendBytes(packet, sizeof(packet)); +} + +void CMain::LockUnlock() +{ + LockSendCode(Settings().m_aLockCode); +} + +void CMain::EnrollMessage(const char *msg, bool error) +{ + m_EnrollMessage = msg; + if(error) + m_Enrolling = false; +} + +void CMain::OnFingerVerified(uint16_t fingerNum, uint8_t digest[SHA256_SIZE]) +{ + int fingerIndex = Settings().m_FingerPrints.indexOf(fingerNum); + if(fingerIndex == -1) + { + debugf("OnFingerVerified: fingerIndex == -1"); + return; + } + + const CSettings::CFingerPrint &finger = Settings().m_FingerPrints.valueAt(fingerIndex); + if(memcmp(digest, finger.m_aDigest, SHA256_SIZE) != 0) + { + debugf("OnFingerVerified: SHA256 mismatch"); + return; + } + + LockUnlock(); + + debugf("OnFingerVerified: OK!!!"); +} + +void CMain::OnFingerEnrolled(uint16_t fingerNum, uint8_t digest[SHA256_SIZE]) +{ + CSettings::CFingerPrint finger; + finger.m_FingerNum = fingerNum; + strncpy(finger.m_aLabel, m_EnrollLabel.c_str(), sizeof(finger.m_aLabel)); + memcpy(finger.m_aDigest, digest, SHA256_SIZE); + + char aHexDigest[SHA256_SIZE*2+1]; + bytes2hex(finger.m_aDigest, sizeof(finger.m_aDigest), aHexDigest, sizeof(aHexDigest)); + debugf("OnFingerEnrolled: \"%s\"", aHexDigest); + + Serial1.printf("(sz: %d) Finger hexdigest: ", sizeof(finger.m_aDigest)); + for(uint8_t i = 0; i < sizeof(finger.m_aDigest); i++) + Serial1.printf("%x", finger.m_aDigest[i]); + Serial1.printf("\n"); + + Settings().m_FingerPrints[fingerNum] = finger; + Settings().Save(); + m_Enrolled = true; + m_Enrolling = false; + m_EnrolledFinger = finger; + + char aBuf[512]; + m_snprintf(aBuf, sizeof(aBuf), "Successfully enrolled new finger \"%s\".", finger.m_aLabel); + EnrollMessage(aBuf); + + debugf("OnFingerEnrolled: OK!!!"); +} diff --git a/app/main.h b/app/main.h new file mode 100644 index 0000000..3e6143a --- /dev/null +++ b/app/main.h @@ -0,0 +1,65 @@ +#ifndef MAIN_H +#define MAIN_H + +#define FINGER_DETECT_PIN 4 +#define FINGER_ENABLE_PIN 5 + +#define SAFELOCK_DATA_PIN 12 +#define SAFELOCK_DETECT_PIN 14 + +#define DOOR_DETECT_PIN 13 + +#include +#include "Settings.h" +#include "FingerPrint.h" +#include "FingerLogic.h" + +class CMain +{ +public: + CMain(); + void Init(HardwareSerial &serial); + + void OnFingerInterrupt(bool finger); + + void FingerEnable(bool enable); + + void LockUnlock(); + void EnrollMessage(const char *msg, bool error=false); + + void OnFingerVerified(uint16_t fingerNum, uint8_t digest[SHA256_SIZE]); + void OnFingerEnrolled(uint16_t fingerNum, uint8_t digest[SHA256_SIZE]); + + CSettings &Settings() { return m_Settings; } + CFingerPrint &FingerPrint() { return m_FingerPrint; } + CFingerLogic &FingerLogic() { return m_FingerLogic; } + +private: + bool HttpAuthorized(HttpRequest &request, HttpResponse &response); + void HttpOnApi(HttpRequest &request, HttpResponse &response); + void HttpOnFile(HttpRequest &request, HttpResponse &response); + + HttpStatus HandleApi(HttpMethod method, String endpoint, JsonDocument &req, JsonDocument &resp); + +private: + void LockSendBytes(uint8_t *pBytes, uint8_t len); + void LockSendCode(uint8_t code[8]); + + CSettings m_Settings; + CFingerPrint m_FingerPrint; + CFingerLogic m_FingerLogic; + + FtpServer m_FTP; + HttpServer m_HttpServer; + + String m_EnrollMessage; + String m_EnrollLabel; + CSettings::CFingerPrint m_EnrolledFinger; + bool m_Enrolling; + bool m_Enrolled; +}; + +extern CMain g_Main; +extern HardwareSerial Serial1; + +#endif diff --git a/app/utils.cpp b/app/utils.cpp new file mode 100644 index 0000000..d082b36 --- /dev/null +++ b/app/utils.cpp @@ -0,0 +1,75 @@ +#include +#include "utils.h" + +static inline uint8_t _char2byte(char c) +{ + if('0' <= c && c <= '9') return (uint8_t)(c - '0'); + if('A' <= c && c <= 'F') return (uint8_t)(c - 'A' + 10); + if('a' <= c && c <= 'f') return (uint8_t)(c - 'a' + 10); + return 0xFF; +} + +int hex2bytes(const char *str, uint8_t *bytes, int32_t length) +{ + int result; + if(!str || !bytes || length <= 0) + return -1; + + for(result = 0; *str; result++) + { + uint8_t msn = _char2byte(*str++); + if(msn == 0xFF) return -1; + + uint8_t lsn = _char2byte(*str++); + if(lsn == 0xFF) return -1; + + uint8_t bin = (msn << 4) + lsn; + + if(length-- <= 0) + return -1; + + *bytes++ = bin; + } + + return result; +} + +void bytes2hex(const uint8_t *bytes, int32_t length, char *str, int32_t strLength) +{ + const char binHex[] = "0123456789ABCDEF"; + + if(!str || strLength < 3) + return; + *str = 0; + + if(!bytes || length <= 0 || strLength <= 2 * length) + { + strncpy(str, "ERR", strLength); + return; + } + + for(; length > 0; length--, strLength -= 2) + { + uint8_t byte = *bytes++; + + *str++ = binHex[(byte >> 4) & 0x0F]; + *str++ = binHex[byte & 0x0F]; + } + + if(strLength-- <= 0) + return; + + *str++ = 0; +} + +int Rssi2Quality(sint8 rssi) +{ + if(rssi >= -100 && rssi <= -80) + return 1; + else if(rssi > -80 && rssi <= -65) + return 2; + else if(rssi > -65 && rssi < -50) + return 3; + else + return 4; +} diff --git a/app/utils.h b/app/utils.h new file mode 100644 index 0000000..d0ed894 --- /dev/null +++ b/app/utils.h @@ -0,0 +1,8 @@ +#ifndef UTILS_H +#define UTILS_H + +int hex2bytes(const char *str, uint8_t *bytes, int32_t length); +void bytes2hex(const uint8_t *bytes, int32_t length, char *str, int32_t strLength); +int Rssi2Quality(sint8 rssi); + +#endif diff --git a/component.mk b/component.mk new file mode 100644 index 0000000..dcb4953 --- /dev/null +++ b/component.mk @@ -0,0 +1,4 @@ +ARDUINO_LIBRARIES := ArduinoJson6 + +HWCONFIG = spiffs +SPIFF_FILES = files diff --git a/files/.gitkeep b/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/files/bootstrap.min.css.gz b/files/bootstrap.min.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..71634cbfdeba63409e3c25fa5ca092c3541fafb3 GIT binary patch literal 18466 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 + + + + + + Safe control panel + + + + + + + +
+ + + + + + + + + + diff --git a/files/jquery-3.2.1.min.js.gz b/files/jquery-3.2.1.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..269d2de4f5eef069a9295ae87775c8c6548cacdb GIT binary patch literal 29995 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

`v;-!AF5jB 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 GQUL&A)>~%) literal 0 HcmV?d00001 diff --git a/files/main.css.gz b/files/main.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..772ab62f41e4f23ee429ec74b99f68e1037d9858 GIT binary patch literal 653 zcmV;80&@KyiwFq7QPEif18rexZZ2bUa{!%H+fL&!5PeR*VkCsbqIF0wAPCZGe_%gg zADwuTT6OKnp3(y9zwbC{mTqYZY#%CnJaf+RT%4P0_~o7ul-pRSoF%jbC024B5H<)4 z$%hhZy+s99K9$k~EhK>sFw1T|u7IR5aD9`OPCdhGnzT|WZ5u(ZE%rW-=`Kxgu3@8V z4M8_n8i+_7l|>szm9knk-mR@FVC~ISG>g!BO&GG)Hs8QH;ju+Yk7=TGXpMX>KwG0N ziqg1h8Cg0wF^7K;MeBkl?Q8)#2Jk?4(Gmcf}dAsY?W z%$9fDUHSXvi_li#wtxrz9mtyG`Q@AD{Vo#L#%&5%Xr<702^e&}9ujv=c?i!4`Q-50 zJ^f1hMn|D*a({)rspVDah}^1Q`#v@<3AZGq;baw1@nE25W6eV%f_}lsx3AA2?*}OT zbX2Cw8RrYQn9XLRSHJ6Yjx=?ehz*uY%~Gsh$^PzPDFS(ey2oqII#t@NjT&>Iygp~eT0^eq2(vg=I7D!?BO(8e{hU; z`w`mx7}|Y@_H&3`;&big3d6g+GeLg&og0+@Vk^Ka3_;|j4Yl*!j-iqUe_!R<{2T1j zWcq<0=O+5GfUgjzNphN!@0gqy#U4ADj)Fuz=Rx)!yZ&i<{|2QO+?fq}{tQ?Of@EnB nNI~l{at?!?$E=UIvKv1rq?vFSjgq#PO~|dO)I(DV zZdDJ)Yrv_oSGJb~CH(h{AG<6hG^y0fu6Aa=nQxxkuPjH_8NpotvfJHIgIHw@S8m4F zrKFBu2BAw|HdxJ}_ z--TCr?nd3tLZ~G-r4h=#&Any(UiK}>|L5P`L9ZIpQquyWO-rE^Hwbe@=aNH%oT+&5 z?mK9cdV&Tx`>>RHPUXiW%)D2zXrO0jsFzeka#H&7uGL^`0SI?^x3^u=8C!UcFzn? z4-6wC!(C+*T2SB{q6I%%?xRdz?sw-%(uxVn!|4i1K)CQu(c12NulR?I2l!s7Lf;Pw z41TtP&1NeK3)0@ECT+Lp5m7{HM!gO3NMftGzgTLC&+EB$r{l-Mv@5+YbZiK`fZ)*$ z7!Z0#AEuCDi60T8Y{m0fR}=tXP-+{F1n}I0DkkV+m_%g`*}N^ zWg{;DP>wv(e|YdLTHIN!vPPIAAC1J^#bO)pPf#mM{NOba=S948b zHtFKMOy;dLzI}an7JsGSThG-72sFnO(prdRWjNg7x@i_>#)rS>^Bg@QYPX8z;nX1* z_t+b7LugUl2RHLUSbP*^HYmTk)-2bDpG>ESa YYz*;_Q@!h+oLNuh86X&wfw%_%02@qdNdN!< literal 0 HcmV?d00001 diff --git a/files/nunjucks.min.js.gz b/files/nunjucks.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..ff6749fbe9f1f06e4ec5b8624ef14ff949665c21 GIT binary patch literal 22603 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 T?(ZAUG&lYSaWU*ETv`ACg9F$c literal 0 HcmV?d00001 diff --git a/files/wifi-sprites.png b/files/wifi-sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..863d0d7f28d60e96bcc36f55a461e79816598c21 GIT binary patch literal 1815 zcmV+y2k7{TP)6Fgv!#X>kAm03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00w(WL_t(|+U=cRh#l1( z$3MR_GxzS^-E1P72Kx}J0WHMpV{03%s6?q%QJd7N;Dav}M8v-MAoQhAZE0;OQWPoD z(o{+<3X1>02f;r?8>0>W0g0kU4T;%Z*}Z#b=A2(2b`!n#&dlAc&61q`ecpEd?3wSJ zGv7J){C;ze5F-y=qNHt5t=_MJ4ecqWZ+cW>l)v?{pX;~Qoyknu9om;kLZP0Zffuj% zb(H$MfBv@0IolDbmZz$OcOR#7$NN$0KYZQ=+PZRk?S54yf9wg)_~30xqcnilB<i_(^7b_pUO`Haxfu1jbP-lVo;G;EwOcK^KI@;T`u5H9%fVzBdcNo!8sJEM7m;o4u zVFq9rh8cii7-j&5VI1S+j{^Mty5$XN-t_M%W4iq9$BN4u4>@~cBm1v?AWHqcXYW$V z9@ND2QH5-r_T|5eQvd9ozfg+2@faB!XWj2tmeXQah| zxfl{0o|3V&#cZ@cxKa`lkWkSBas+X-lX$cG2mzcnHIQXZ=3$(o2B0V*1gg?IJd4{) z4H4gN#V_{thV|>B{y?x+X{_Em$!>vRm;o4uVFq9rh8cii7-j&5VZ_bKqX2)oOq+wc zN=s7mTc3(Espr9MN^4^AliVQ$LdmAdO??zWC`sqKxZ&^Ix~>p<<3U34>F0BqA(?f-Mu9c;}k?qtxHMe~YT`H6EiH zn7H#ml==^yb)Hg=m|8vr&iG`%L}>t@bdOMkvSD3$VWI3r>w?q8Jb5U|DeGE4AI=|& zR=<`DAg(!VA0qxvzbL@D5e68i{uCf_35&Jws9uUez@^dFe}Y@mUzOJ&Kp-8J^>(T= z>fN^1C+djQ0A0VkHHnNjsYhwjuS@lB6LlDo8h~LKW&nm^m;o4uv8D__vf-Ft_EH(d2h6Y;& zU6BbS;_wY4*TZ2QX?(&|wDy{-9%fy-@}WIaf1d?6JlKBh`1*3fKsoi{-2ac^tg10C z!>DzzJuS=Jzn*?|a@^N0ZUr4~cl_6>$MJGY0Wt*fIXE}eXpc3N(7z`|C_}#VmL5Jw zRmsveMPEgg_>k8u4B{Y^v<50BMJPgVsj*j-gOVlv<)9w#dsIa(1c4C9vhgpssfUo) zl;J=&RCK?T68z%CCQYHf!SdCk<)}w&@Lrb9=ZG-ac?k#KVByi9G{)fc!0!HY zK7NVrBUfqDqaQwd{VP>jBF>>rp+4V#;r@;PGJWk%m4?V!@#=s2{?4C#nwg!qG-@ed z+taUQ-&@Sw@H1`7LnvSA*I&N8o9U~ruRTYA^7Xv{{13zzfC_tQll1@q002ovPDHLk FV1ixcc)kDt literal 0 HcmV?d00001 diff --git a/out/.gitignore b/out/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/out/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore