Initial Commit

This commit is contained in:
2025-07-01 18:20:48 +02:00
commit e80990c521
12 changed files with 858 additions and 0 deletions

400
src/main.cpp Normal file
View File

@@ -0,0 +1,400 @@
#undef ARDUINO_M5STACK_FIRE
#define ARDUINO_M5STACK_Paper
#define YAML_DISABLE_CJSON
#include <ArduinoJson.h>
#include <ArduinoYaml.h>
#include <WiFi.h>
#include <M5EPD.h>
#include <NTPClient.h>
#include <HTTPClient.h>
#include <PubSubClient.h>
// https://github.com/Netthaw/TOTP-MCU
#include "totp.h"
#define LGFX_M5PAPER
#define LGFX_USE_V1
#define LGFX_AUTODETECT
#include <LovyanGFX.hpp>
static LGFX gfx;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP);
constexpr float FONT_SIZE_NORMAL = 4.0;
constexpr float FONT_SIZE_LARGE = 8.0;
static YAMLNode *SETTINGS = nullptr;
static String oldinput = "";
static String input = "";
static int last_index = -1;
static bool is_disarmed = false;
static const int WIFI_CONNECT_RETRY_MAX = 30;
static const float_t BUTTON_WIDTH = 115;
static const float_t BUTTON_HEIGHT = 115;
void drawLockState(String state)
{
state.replace("_", " ");
gfx.setTextSize(FONT_SIZE_NORMAL);
gfx.setColor(TFT_WHITE);
gfx.fillRect(405, 155, gfx.fontWidth() * 10, gfx.fontHeight());
gfx.setColor(TFT_BLACK);
gfx.setCursor(405, 155);
state[0] = toupper(state[0]);
gfx.printf("%s", state);
}
void mqtt_callback(char *topic, byte *payload, unsigned int length)
{
String state;
for (int i = 0; i < length; i++)
{
state += (const char)payload[i];
}
is_disarmed = state == "disarmed";
Serial.printf("%d: %s - %s\n", length, topic, state);
drawLockState(state);
}
WiFiClient wifiClient;
PubSubClient *client = nullptr;
void cls()
{
gfx.setEpdMode(epd_mode_t::epd_quality);
gfx.fillScreen(TFT_WHITE);
gfx.waitDisplay();
gfx.fillScreen(TFT_WHITE);
gfx.setEpdMode(epd_mode_t::epd_fast);
gfx.setColor(TFT_BLACK);
gfx.waitDisplay();
}
void drawButton(int index, String label)
{
float_t x = (125 * (index % 3)) + 15;
float_t y = (125 * (index / 3)) + 20;
gfx.setCursor(x + (115 / 2) - (gfx.fontWidth() / 2), y + (115 / 2) - (gfx.fontHeight() / 2));
gfx.print(label);
gfx.setColor(TFT_BLACK);
gfx.drawFastHLine(x, y, BUTTON_WIDTH);
gfx.drawFastHLine(x, y + BUTTON_HEIGHT, BUTTON_WIDTH);
gfx.drawFastVLine(x, y, BUTTON_HEIGHT);
gfx.drawFastVLine(x + BUTTON_WIDTH, y, BUTTON_HEIGHT);
}
void drawButtons()
{
gfx.setTextSize(FONT_SIZE_NORMAL);
for (int i = 0; i <= 11; i++)
{
String label = String(i + 1);
if (i == 9)
{
label = String("<");
}
if (i == 10)
{
label = String("0");
}
if (i == 11)
{
label = String(">");
}
drawButton(i, label);
}
}
void drawInputField()
{
int x = 405;
int y = 20;
gfx.setColor(TFT_BLACK);
gfx.drawFastHLine(x, y, 520);
gfx.drawFastHLine(x, y + 115, 520);
gfx.drawFastVLine(x, y, 115);
gfx.drawFastVLine(x + 520, y, 115);
}
void drawInput()
{
int x = 405;
int y = 20;
gfx.setColor(TFT_WHITE);
gfx.fillRect(x + 1, y + 1, 519, 114);
gfx.setColor(TFT_BLACK);
gfx.setEpdMode(epd_mode_t::epd_text);
gfx.setColor(TFT_BLACK);
gfx.setTextSize(FONT_SIZE_LARGE);
gfx.setCursor(x + (520 / 2) - ((gfx.fontWidth() * input.length()) / 2), y + (115 / 2) - (gfx.fontHeight() / 2));
String display = "";
for (int i = 0; i < input.length(); i++)
{
display += "*";
}
gfx.print(display);
gfx.setEpdMode(epd_mode_t::epd_fast);
}
void setupTime()
{
timeClient.begin();
}
void send(String state)
{
if (SETTINGS == nullptr)
{
return;
}
if (client->connect(SETTINGS->gettext("mqtt:ident"), SETTINGS->gettext("mqtt:username"), SETTINGS->gettext("mqtt:password")))
{
client->unsubscribe(SETTINGS->gettext("mqtt:state_topic"));
client->subscribe(SETTINGS->gettext("mqtt:state_topic"));
client->publish(SETTINGS->gettext("mqtt:command_topic"), state.c_str());
}
}
void lock()
{
if (is_disarmed)
{
send("ARM_AWAY");
}
}
void unlock()
{
send("DISARM");
}
bool isConnecting = false;
void resetWifi()
{
if (!SETTINGS)
{
return;
}
if (isConnecting)
{
return;
}
isConnecting = true;
drawLockState("connecting");
WiFi.begin(SETTINGS->gettext("wifi:ssid"), SETTINGS->gettext("wifi:password"));
if (WiFi.isConnected())
{
WiFi.disconnect();
}
Serial.print("Connecting to Wi-Fi network");
for (int cnt_retry = 0; cnt_retry < WIFI_CONNECT_RETRY_MAX && !WiFi.isConnected();
cnt_retry++)
{
delay(500);
Serial.print(".");
}
Serial.println("");
if (WiFi.isConnected())
{
Serial.print("Local IP: ");
Serial.println(WiFi.localIP());
isConnecting = false;
}
else
{
drawLockState("failed to connect");
isConnecting = false;
return;
}
IPAddress ip;
if (!ip.fromString(SETTINGS->gettext("mqtt:hostname")))
{
drawLockState("failed to parse mqtt ip");
isConnecting = false;
return;
}
client = new PubSubClient(ip, 1883, mqtt_callback, wifiClient);
if (client->connect(SETTINGS->gettext("mqtt:ident"), SETTINGS->gettext("mqtt:username"), SETTINGS->gettext("mqtt:password")))
{
client->unsubscribe(SETTINGS->gettext("mqtt:state_topic"));
client->subscribe(SETTINGS->gettext("mqtt:state_topic"));
}
}
bool initSD()
{
if (!SD.exists("/settings.yml"))
{
Serial.println("settings.yml not found");
drawLockState("settings.yml not found");
return false;
}
File settingsFile = SD.open("/settings.yml");
YAMLNode node = YAMLNode::loadStream(settingsFile);
settingsFile.close();
SETTINGS = new YAMLNode(node);
return true;
}
void initTOTP()
{
uint8_t *base32_key = new uint8_t[20];
const char *hmac = ((String)SETTINGS->gettext("totp:hmac")).c_str();
for (int i = 0; i < 20; i++)
{
base32_key[i] = (uint8_t)hmac[i];
}
TOTP(base32_key, 20, 30);
}
void setup()
{
M5.begin();
gfx.init();
gfx.setEpdMode(epd_mode_t::epd_fast);
gfx.setRotation(1);
cls();
drawButtons();
drawInputField();
drawInput();
if (!initSD())
return;
resetWifi();
sleep(1);
setupTime();
initTOTP();
lock();
}
int index(int x, int y)
{
for (int index = 0; index <= 11; index++)
{
float_t cx = (125 * (index % 3)) + 15;
float_t cy = (125 * (index / 3)) + 20;
if (x > cx && x < cx + BUTTON_WIDTH && y > cy && y < cy + BUTTON_HEIGHT)
{
return index;
}
}
return -1;
}
void submit(String code)
{
uint32_t newCode = getCodeFromTimestamp(timeClient.getEpochTime());
uint32_t input = code.toInt();
if (newCode == input)
{
unlock();
return;
}
lock();
}
void checkForInput()
{
if (M5.TP.available())
{
M5.TP.update();
if (M5.TP.isFingerUp())
{
last_index = -1;
return;
}
tp_finger_t fingerItem = M5.TP.readFinger(0);
int i = index(fingerItem.x, fingerItem.y);
if (last_index != i)
{
last_index = i;
if (i == 9)
{
input = input.substring(0, input.length() - 1);
}
else if (i == 10)
{
input += String(0);
}
else if (i == 11)
{
// submit
submit(input);
input = "";
}
else
{
input += String(i + 1);
}
}
}
}
unsigned long lastBtnPressed = millis();
void checkForButton()
{
M5.BtnP.read();
if (M5.BtnP.isPressed() && millis() - lastBtnPressed > 1000)
{
lastBtnPressed = millis(); // try and debouce, not really working i guess
resetWifi();
}
}
void loop()
{
checkForButton();
timeClient.update();
if (client != nullptr)
{
client->loop();
}
checkForInput();
if (oldinput != input)
{
oldinput = input;
drawInput();
}
}

202
src/sha1.cpp Normal file
View File

@@ -0,0 +1,202 @@
#include <string.h>
#include "sha1.h"
#define SHA1_K0 0x5a827999
#define SHA1_K20 0x6ed9eba1
#define SHA1_K40 0x8f1bbcdc
#define SHA1_K60 0xca62c1d6
union _buffer
{
uint8_t b[BLOCK_LENGTH];
uint32_t w[BLOCK_LENGTH / 4];
} buffer;
union _state
{
uint8_t b[HASH_LENGTH];
uint32_t w[HASH_LENGTH / 4];
} state;
uint8_t bufferOffset;
uint32_t byteCount;
uint8_t keyBuffer[BLOCK_LENGTH];
uint8_t innerHash[HASH_LENGTH];
uint8_t sha1InitState[] = {
0x01, 0x23, 0x45, 0x67, // H0
0x89, 0xab, 0xcd, 0xef, // H1
0xfe, 0xdc, 0xba, 0x98, // H2
0x76, 0x54, 0x32, 0x10, // H3
0xf0, 0xe1, 0xd2, 0xc3 // H4
};
void init(void)
{
memcpy(state.b, sha1InitState, HASH_LENGTH);
byteCount = 0;
bufferOffset = 0;
}
uint32_t rol32(uint32_t number, uint8_t bits)
{
return ((number << bits) | (uint32_t)(number >> (32 - bits)));
}
void hashBlock()
{
uint8_t i;
uint32_t a, b, c, d, e, t;
a = state.w[0];
b = state.w[1];
c = state.w[2];
d = state.w[3];
e = state.w[4];
for (i = 0; i < 80; i++)
{
if (i >= 16)
{
t = buffer.w[(i + 13) & 15] ^ buffer.w[(i + 8) & 15] ^ buffer.w[(i + 2) & 15] ^ buffer.w[i & 15];
buffer.w[i & 15] = rol32(t, 1);
}
if (i < 20)
{
t = (d ^ (b & (c ^ d))) + SHA1_K0;
}
else if (i < 40)
{
t = (b ^ c ^ d) + SHA1_K20;
}
else if (i < 60)
{
t = ((b & c) | (d & (b | c))) + SHA1_K40;
}
else
{
t = (b ^ c ^ d) + SHA1_K60;
}
t += rol32(a, 5) + e + buffer.w[i & 15];
e = d;
d = c;
c = rol32(b, 30);
b = a;
a = t;
}
state.w[0] += a;
state.w[1] += b;
state.w[2] += c;
state.w[3] += d;
state.w[4] += e;
}
void addUncounted(uint8_t data)
{
buffer.b[bufferOffset ^ 3] = data;
bufferOffset++;
if (bufferOffset == BLOCK_LENGTH)
{
hashBlock();
bufferOffset = 0;
}
}
void write(uint8_t data)
{
++byteCount;
addUncounted(data);
return;
}
void writeArray(uint8_t *buffer, uint8_t size)
{
while (size--)
{
write(*buffer++);
}
}
void pad()
{
// Implement SHA-1 padding (fips180-2 <20><>5.1.1)
// Pad with 0x80 followed by 0x00 until the end of the block
addUncounted(0x80);
while (bufferOffset != 56)
addUncounted(0x00);
// Append length in the last 8 bytes
addUncounted(0); // We're only using 32 bit lengths
addUncounted(0); // But SHA-1 supports 64 bit lengths
addUncounted(0); // So zero pad the top bits
addUncounted(byteCount >> 29); // Shifting to multiply by 8
addUncounted(byteCount >> 21); // as SHA-1 supports bitstreams as well as
addUncounted(byteCount >> 13); // byte.
addUncounted(byteCount >> 5);
addUncounted(byteCount << 3);
}
uint8_t *result(void)
{
// Pad to complete the last block
pad();
// Swap byte order back
uint8_t i;
for (i = 0; i < 5; i++)
{
uint32_t a, b;
a = state.w[i];
b = a << 24;
b |= (a << 8) & 0x00ff0000;
b |= (a >> 8) & 0x0000ff00;
b |= a >> 24;
state.w[i] = b;
}
// Return pointer to hash (20 characters)
return state.b;
}
#define HMAC_IPAD 0x36
#define HMAC_OPAD 0x5c
void initHmac(const uint8_t *key, uint8_t keyLength)
{
uint8_t i;
memset(keyBuffer, 0, BLOCK_LENGTH);
if (keyLength > BLOCK_LENGTH)
{
// Hash long keys
init();
for (; keyLength--;)
write(*key++);
memcpy(keyBuffer, result(), HASH_LENGTH);
}
else
{
// Block length keys are used as is
memcpy(keyBuffer, key, keyLength);
}
// Start inner hash
init();
for (i = 0; i < BLOCK_LENGTH; i++)
{
write(keyBuffer[i] ^ HMAC_IPAD);
}
}
uint8_t *resultHmac(void)
{
uint8_t i;
// Complete inner hash
memcpy(innerHash, result(), HASH_LENGTH);
// Calculate outer hash
init();
for (i = 0; i < BLOCK_LENGTH; i++)
write(keyBuffer[i] ^ HMAC_OPAD);
for (i = 0; i < HASH_LENGTH; i++)
write(innerHash[i]);
return result();
}

16
src/sha1.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef __SHA1_H__
#define __SHA1_H__
#include <inttypes.h>
#define HASH_LENGTH 20
#define BLOCK_LENGTH 64
void init(void);
void initHmac(const uint8_t *secret, uint8_t secretLength);
uint8_t *result(void);
uint8_t *resultHmac(void);
void write(uint8_t);
void writeArray(uint8_t *buffer, uint8_t size);
#endif

76
src/totp.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include "totp.h"
#include "sha1.h"
uint8_t *_hmacKey;
uint8_t _keyLength;
uint8_t _timeZoneOffset = 0;
uint32_t _timeStep;
// Init the library with the private key, its length and the timeStep duration
void TOTP(uint8_t *hmacKey, uint8_t keyLength, uint32_t timeStep)
{
_hmacKey = hmacKey;
_keyLength = keyLength;
_timeStep = timeStep;
}
void setTimezone(uint8_t timezone)
{
_timeZoneOffset = timezone;
}
uint32_t TimeStruct2Timestamp(struct tm time)
{
// time.tm_mon -= 1;
// time.tm_year -= 1900;
return mktime(&(time)) - (_timeZoneOffset * 3600) - 2208988800;
}
// Generate a code, using the timestamp provided
uint32_t getCodeFromTimestamp(uint32_t timeStamp)
{
uint32_t steps = timeStamp / _timeStep;
return getCodeFromSteps(steps);
}
// Generate a code, using the timestamp provided
uint32_t getCodeFromTimeStruct(struct tm time)
{
return getCodeFromTimestamp(TimeStruct2Timestamp(time));
}
// Generate a code, using the number of steps provided
uint32_t getCodeFromSteps(uint32_t steps)
{
// STEP 0, map the number of steps in a 8-bytes array (counter value)
uint8_t _byteArray[8];
_byteArray[0] = 0x00;
_byteArray[1] = 0x00;
_byteArray[2] = 0x00;
_byteArray[3] = 0x00;
_byteArray[4] = (uint8_t)((steps >> 24) & 0xFF);
_byteArray[5] = (uint8_t)((steps >> 16) & 0xFF);
_byteArray[6] = (uint8_t)((steps >> 8) & 0XFF);
_byteArray[7] = (uint8_t)((steps & 0XFF));
// STEP 1, get the HMAC-SHA1 hash from counter and key
initHmac(_hmacKey, _keyLength);
writeArray(_byteArray, 8);
uint8_t *_hash = resultHmac();
// STEP 2, apply dynamic truncation to obtain a 4-bytes string
uint32_t _truncatedHash = 0;
uint8_t _offset = _hash[20 - 1] & 0xF;
uint8_t j;
for (j = 0; j < 4; ++j)
{
_truncatedHash <<= 8;
_truncatedHash |= _hash[_offset + j];
}
// STEP 3, compute the OTP value
_truncatedHash &= 0x7FFFFFFF; // Disabled
_truncatedHash %= 1000000;
return _truncatedHash;
}

13
src/totp.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef __TOTP_H__
#define __TOTP_H__
#include <inttypes.h>
#include "time.h"
void TOTP(uint8_t *hmacKey, uint8_t keyLength, uint32_t timeStep);
void setTimezone(uint8_t timezone);
uint32_t getCodeFromTimestamp(uint32_t timeStamp);
uint32_t getCodeFromTimeStruct(struct tm time);
uint32_t getCodeFromSteps(uint32_t steps);
#endif