summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFlorian Baumann <derflob@derflob.de>2020-12-19 06:10:35 +0100
committerFlorian Baumann <derflob@derflob.de>2020-12-19 06:11:34 +0100
commitba808a30e30e0c2bab85c85fa6d11b3a3b15f6fd (patch)
treeab1a84a313191e7e51d862ee964f078804a73e77
downloadRJ45EEPROMKeyHolder-ba808a30e30e0c2bab85c85fa6d11b3a3b15f6fd.tar.gz
RJ45EEPROMKeyHolder-ba808a30e30e0c2bab85c85fa6d11b3a3b15f6fd.tar.bz2
initial commit
-rw-r--r--.gitignore5
-rw-r--r--.travis.yml67
-rw-r--r--.vscode/extensions.json7
-rw-r--r--include/README39
-rw-r--r--include/config.h10
-rw-r--r--include/const.h22
-rw-r--r--include/eeprom_key.h34
-rw-r--r--include/pins.h25
-rw-r--r--include/publish.h19
-rw-r--r--include/types.h30
-rw-r--r--include/utils.h10
-rw-r--r--lib/README46
-rw-r--r--platformio.ini23
-rw-r--r--src/eeprom_key.cpp317
-rw-r--r--src/main.cpp338
-rw-r--r--src/publish.cpp194
-rw-r--r--src/utils.cpp21
-rw-r--r--test/README11
18 files changed, 1218 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89cc49c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7c486f1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,67 @@
+# Continuous Integration (CI) is the practice, in software
+# engineering, of merging all developer working copies with a shared mainline
+# several times a day < https://docs.platformio.org/page/ci/index.html >
+#
+# Documentation:
+#
+# * Travis CI Embedded Builds with PlatformIO
+# < https://docs.travis-ci.com/user/integration/platformio/ >
+#
+# * PlatformIO integration with Travis CI
+# < https://docs.platformio.org/page/ci/travis.html >
+#
+# * User Guide for `platformio ci` command
+# < https://docs.platformio.org/page/userguide/cmd_ci.html >
+#
+#
+# Please choose one of the following templates (proposed below) and uncomment
+# it (remove "# " before each line) or use own configuration according to the
+# Travis CI documentation (see above).
+#
+
+
+#
+# Template #1: General project. Test it using existing `platformio.ini`.
+#
+
+# language: python
+# python:
+# - "2.7"
+#
+# sudo: false
+# cache:
+# directories:
+# - "~/.platformio"
+#
+# install:
+# - pip install -U platformio
+# - platformio update
+#
+# script:
+# - platformio run
+
+
+#
+# Template #2: The project is intended to be used as a library with examples.
+#
+
+# language: python
+# python:
+# - "2.7"
+#
+# sudo: false
+# cache:
+# directories:
+# - "~/.platformio"
+#
+# env:
+# - PLATFORMIO_CI_SRC=path/to/test/file.c
+# - PLATFORMIO_CI_SRC=examples/file.ino
+# - PLATFORMIO_CI_SRC=path/to/test/directory
+#
+# install:
+# - pip install -U platformio
+# - platformio update
+#
+# script:
+# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..0f0d740
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ // See http://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "platformio.platformio-ide"
+ ]
+}
diff --git a/include/README b/include/README
new file mode 100644
index 0000000..194dcd4
--- /dev/null
+++ b/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/include/config.h b/include/config.h
new file mode 100644
index 0000000..2c2b331
--- /dev/null
+++ b/include/config.h
@@ -0,0 +1,10 @@
+#ifndef __KH_CONFIG_H__
+#define __KH_CONFIG_H__
+
+#define STA_SSID "SSID"
+#define STA_PASSWD "PASSWORD"
+#define HOSTNAME "RJ45EEPROMKeyHolder"
+
+#define MQTT_HOST "10.0.0.1"
+
+#endif
diff --git a/include/const.h b/include/const.h
new file mode 100644
index 0000000..3a4415a
--- /dev/null
+++ b/include/const.h
@@ -0,0 +1,22 @@
+#ifndef __KH_CONST_H__
+#define __KH_CONST_H__
+
+#define NUM_KEY_SOCKETS 12
+
+#define EEPROM_SIZE (1536 / 8)
+
+#define MAGIC_BYTE_OFFSET 0
+#define FEATURE_MASK_OFFSET 2
+#define FEATURE_MASK_KEY_NAME 0
+#define KEY_NAME_OFFSET 42
+#define KEY_NAME_MAX 33
+
+#define PAYLOAD_HOME "home"
+#define PAYLOAD_NOT_HOME "not_home"
+
+#define PAYLOAD_LIGHT_ON "ON"
+#define PAYLOAD_LIGHT_OFF "OFF"
+
+#define MAX_KNOWN_KEYS 127
+
+#endif \ No newline at end of file
diff --git a/include/eeprom_key.h b/include/eeprom_key.h
new file mode 100644
index 0000000..e2783a4
--- /dev/null
+++ b/include/eeprom_key.h
@@ -0,0 +1,34 @@
+#ifndef __KH_EEPROM_KEY_H__
+#define __KH_EEPROM_KEY_H__
+
+#include "types.h"
+#include "const.h"
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define KK_PREFNAME "known-keys"
+
+extern eeprom_key_t *known_keys[MAX_KNOWN_KEYS];
+extern uint8_t known_key_length;
+
+void read_eeprom(uint8_t index, uint8_t address, uint8_t *buffer, size_t size);
+void write_eeprom(uint8_t index, uint8_t address, uint8_t *buffer, size_t size);
+
+void set_socket(eeprom_key_socket_t *socket, eui48_t *identifier);
+void unset_socket(eeprom_key_socket_t *socket);
+
+bool detect_key(uint8_t index, eui48_t *identifier);
+
+bool check_feature_mask(eeprom_key_t *key, uint8_t bit);
+bool check_feature_mask(eeprom_key_socket_t *socket, uint8_t bit);
+void set_feature_mask(eeprom_key_socket_t *socket, uint8_t bit);
+void unset_feature_mask(eeprom_key_socket_t *socket, uint8_t bit);
+
+void load_known_keys();
+bool is_known(eui48_t *id);
+eeprom_key_t *get_key(eui48_t *id);
+eeprom_key_t *ensure_key(eui48_t *id);
+eeprom_key_t *add_known_key(eui48_t *id);
+
+#endif \ No newline at end of file
diff --git a/include/pins.h b/include/pins.h
new file mode 100644
index 0000000..dfb6647
--- /dev/null
+++ b/include/pins.h
@@ -0,0 +1,25 @@
+#ifndef __PINS_H__
+#define __PINS_H__
+
+#define LED_REG_SCLK 25
+#define LED_REG_RCLK 26
+
+#define LED_REG0 12
+#define LED_REG1 14
+#define LED_REG2 27
+
+#define LED_STATUS 32
+
+#define SPI_SCK 18
+#define SPI_MISO 19
+#define SPI_MOSI 23
+#define SPI_CS 5
+
+#define CS_MULT_ENABLE 15
+
+#define CS_SEL0 17
+#define CS_SEL1 21
+#define CS_SEL2 4
+#define CS_SEL3 16
+
+#endif \ No newline at end of file
diff --git a/include/publish.h b/include/publish.h
new file mode 100644
index 0000000..86a19e9
--- /dev/null
+++ b/include/publish.h
@@ -0,0 +1,19 @@
+#ifndef __KH_PUBLISH_H__
+#define __KH_PUBLISH_H__
+
+#include "types.h"
+
+#include <PubSubClient.h>
+#include <stdint.h>
+
+extern PubSubClient mqtt;
+
+void publish_key_discovery(eeprom_key_t *key);
+void publish_key_state(eeprom_key_t *key, bool home, int8_t holder);
+void publish_key_attributes(eeprom_key_t *key, int8_t holder);
+void publish_key_light_state(eeprom_key_t *key, bool enabled);
+
+void publish_holder_sensor();
+void publish_holder_key_count(uint8_t count);
+
+#endif \ No newline at end of file
diff --git a/include/types.h b/include/types.h
new file mode 100644
index 0000000..9277e72
--- /dev/null
+++ b/include/types.h
@@ -0,0 +1,30 @@
+#ifndef __KH_TYPES_H__
+#define __KH_TYPES_H__
+
+#include "const.h"
+#include <stdint.h>
+
+typedef struct {
+ uint8_t vendor[3];
+ uint8_t device[3];
+} eui48_t;
+
+typedef struct {
+ eui48_t id;
+
+ uint16_t feature_mask;
+ char name[KEY_NAME_MAX];
+
+ bool loaded = false;
+} eeprom_key_t;
+
+typedef struct {
+ uint8_t index;
+
+ bool plugged = false;
+ bool flashing = false;
+
+ eeprom_key_t *key;
+} eeprom_key_socket_t;
+
+#endif \ No newline at end of file
diff --git a/include/utils.h b/include/utils.h
new file mode 100644
index 0000000..c7f2534
--- /dev/null
+++ b/include/utils.h
@@ -0,0 +1,10 @@
+#ifndef __KH_UTILS_H__
+#define __KH_UTILs_H__
+
+#include "types.h"
+#include <Arduino.h>
+
+String get_tracker_id(eui48_t id);
+String repr(eeprom_key_t *key);
+
+#endif \ No newline at end of file
diff --git a/lib/README b/lib/README
new file mode 100644
index 0000000..6debab1
--- /dev/null
+++ b/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+| |
+| |--Bar
+| | |--docs
+| | |--examples
+| | |--src
+| | |- Bar.c
+| | |- Bar.h
+| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+| |
+| |--Foo
+| | |- Foo.c
+| | |- Foo.h
+| |
+| |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+ |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+ ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..6be480f
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,23 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:esp32dev]
+platform = espressif32
+board = esp32dev
+framework = arduino
+lib_deps =
+ PubSubClient
+ ArduinoJson
+
+monitor_speed = 115200
+monitor_port = /dev/ttyUSB0
+monitor_filters = esp32_exception_decoder, default
+upload_port = /dev/ttyUSB0
+
diff --git a/src/eeprom_key.cpp b/src/eeprom_key.cpp
new file mode 100644
index 0000000..2b91a7d
--- /dev/null
+++ b/src/eeprom_key.cpp
@@ -0,0 +1,317 @@
+#include "eeprom_key.h"
+
+#include "pins.h"
+#include "utils.h"
+#include "publish.h"
+
+#include <Arduino.h>
+#include <Preferences.h>
+#include <SPI.h>
+
+eeprom_key_t *known_keys[MAX_KNOWN_KEYS];
+uint8_t known_key_length = 0;
+
+static Preferences prefs;
+
+void load_known_keys()
+{
+ const size_t key_size = sizeof(eeprom_key_t);
+ prefs.begin(KK_PREFNAME);
+
+ known_key_length = prefs.getUChar("known-keys");
+ for (int i = 0; i < known_key_length; i++) {
+ eeprom_key_t *key = (eeprom_key_t *)malloc(key_size);
+
+ prefs.getBytes(String(i).c_str(), key, key_size);
+
+ known_keys[i] = key;
+
+ Serial.printf("Loaded #%d key: %s\n", i, repr(key).c_str());
+ }
+
+ prefs.end();
+}
+
+eeprom_key_t *get_key(eui48_t *id)
+{
+ if (known_key_length == 0)
+ load_known_keys();
+
+ for (uint8_t i = 0; i < known_key_length; i++) {
+ if (!memcmp(&(known_keys[i]->id), id, sizeof(eui48_t)))
+ return known_keys[i];
+ }
+
+ return NULL;
+}
+
+eeprom_key_t *ensure_key(eui48_t *id)
+{
+ eeprom_key_t *key = get_key(id);
+ if (key == NULL)
+ return add_known_key(id);
+ else
+ return key;
+}
+
+bool is_known_key(eui48_t *id)
+{
+ return get_key(id) != NULL;
+}
+
+eeprom_key_t *add_known_key(eui48_t *id)
+{
+ if (known_key_length == 0)
+ load_known_keys();
+
+ prefs.begin(KK_PREFNAME, false);
+
+ eeprom_key_t *key = (eeprom_key_t *)malloc(sizeof(eeprom_key_t));
+ memset(key, 0, sizeof(eeprom_key_t));
+ memcpy(&key->id, id, sizeof(eui48_t));
+
+ known_keys[known_key_length] = key;
+ prefs.putBytes(String(known_key_length).c_str(), key, sizeof(eeprom_key_t));
+
+ known_key_length++;
+ prefs.putUChar("known-keys", known_key_length);
+
+ Serial.printf("Saved #%d key: %s\n", known_key_length, repr(key).c_str());
+
+ prefs.end();
+
+ return key;
+}
+
+void set_key_multiplexer(uint8_t index)
+{
+ uint8_t multiplexer_index = index;
+ if (index == 6)
+ multiplexer_index = 7;
+ else if (index == 7)
+ multiplexer_index = 6;
+
+ digitalWrite(CS_SEL0, (multiplexer_index >> 0) & 1);
+ digitalWrite(CS_SEL1, (multiplexer_index >> 1) & 1);
+ digitalWrite(CS_SEL2, (multiplexer_index >> 2) & 1);
+ digitalWrite(CS_SEL3, (multiplexer_index >> 3) & 1);
+}
+
+void read_eeprom(uint8_t index, uint8_t address, uint8_t *buffer, size_t size)
+{
+ set_key_multiplexer(index);
+ digitalWrite(CS_MULT_ENABLE, LOW);
+
+ SPI.beginTransaction(SPISettings(5000000, SPI_MSBFIRST, SPI_MODE0));
+ digitalWrite(SPI_CS, LOW);
+
+ SPI.transfer(0x03);
+ SPI.transfer(address);
+ SPI.transfer(buffer, size);
+
+ digitalWrite(SPI_CS, HIGH);
+ SPI.endTransaction();
+
+ digitalWrite(CS_MULT_ENABLE, HIGH);
+}
+
+void unlock_write_protection()
+{
+ SPI.beginTransaction(SPISettings(5000000, SPI_MSBFIRST, SPI_MODE0));
+ digitalWrite(SPI_CS, LOW);
+
+ Serial.println("Unlocking write protect");
+ SPI.transfer(0x06);
+
+ digitalWrite(SPI_CS, HIGH);
+ SPI.endTransaction();
+ delay(5);
+}
+
+void lock_write_protection()
+{
+ SPI.beginTransaction(SPISettings(5000000, SPI_MSBFIRST, SPI_MODE0));
+ digitalWrite(SPI_CS, LOW);
+
+ Serial.println("Locking write protect");
+ SPI.transfer(0x04);
+
+ digitalWrite(SPI_CS, HIGH);
+ SPI.endTransaction();
+ delay(5);
+}
+
+void write_eeprom(uint8_t index, uint8_t address, uint8_t *buffer, size_t size)
+{
+ set_key_multiplexer(index);
+ digitalWrite(CS_MULT_ENABLE, LOW);
+
+ bool ongoing_transfer = false;
+ Serial.println("Starting Write: " + String((char *)buffer));
+ for (size_t offset = 0; offset < size; offset++) {
+ // start writing new page
+ if (!ongoing_transfer) {
+ unlock_write_protection();
+
+ Serial.printf("Nothing ongoing: %d %d ", address, offset);
+ SPI.beginTransaction(SPISettings(5000000, SPI_MSBFIRST, SPI_MODE0));
+ digitalWrite(SPI_CS, LOW);
+
+ SPI.transfer(0x02);
+ SPI.transfer(address + offset);
+ ongoing_transfer = true;
+ }
+
+ SPI.transfer(*(buffer + offset));
+
+ // check if end of page was reached.
+ if (((address + offset) & 0x0F) == 0x0F) {
+ Serial.println(" Page written.");
+ ongoing_transfer = false;
+ digitalWrite(SPI_CS, HIGH);
+ SPI.endTransaction();
+ delay(5);
+ }
+ }
+
+ digitalWrite(SPI_CS, HIGH);
+ SPI.endTransaction();
+
+ lock_write_protection();
+
+ digitalWrite(CS_MULT_ENABLE, HIGH);
+}
+
+eui48_t read_identifier(uint8_t index)
+{
+ uint8_t buffer[6] = {};
+ eui48_t identifier = {};
+
+ read_eeprom(index, 0xFA, buffer, 6);
+ memcpy(identifier.vendor, buffer, 3);
+ memcpy(identifier.device, buffer + 3, 3);
+
+ return identifier;
+}
+
+bool check_feature_mask(eeprom_key_t *key, uint8_t bit)
+{
+ return !!(key->feature_mask & (1 >> bit));
+}
+
+bool check_feature_mask(eeprom_key_socket_t *socket, uint8_t bit)
+{
+ return check_feature_mask(socket->key, bit);
+}
+
+void set_feature_mask(eeprom_key_socket_t *socket, uint8_t bit)
+{
+ if (!check_feature_mask(socket, bit)) {
+ socket->key->feature_mask |= (1 >> bit);
+ write_eeprom(socket->index, FEATURE_MASK_OFFSET, (uint8_t *)&(socket->key->feature_mask), 2);
+ }
+}
+
+void unset_feature_mask(eeprom_key_socket_t *socket, uint8_t bit)
+{
+ if (check_feature_mask(socket, bit)) {
+ socket->key->feature_mask &= ~(1 >> bit);
+ write_eeprom(socket->index, FEATURE_MASK_OFFSET, (uint8_t *)&(socket->key->feature_mask), 2);
+ }
+}
+
+void format_eeprom(eeprom_key_socket_t *socket)
+{
+ void *buffer = malloc(EEPROM_SIZE);
+
+ memset(buffer, 0, EEPROM_SIZE);
+
+ ((uint8_t *)buffer)[0] = 42;
+ ((uint8_t *)buffer)[1] = 21;
+
+ write_eeprom(socket->index, 0, (uint8_t *)buffer, EEPROM_SIZE);
+
+ free(buffer);
+}
+
+void read_key_data(eeprom_key_socket_t *socket)
+{
+ uint8_t magic_bytes[2];
+
+ uint8_t tries = 0;
+ while (tries < 5) {
+ read_eeprom(socket->index, MAGIC_BYTE_OFFSET, magic_bytes, 2);
+ if (magic_bytes[0] == 42 && magic_bytes[1] == 21)
+ break;
+
+ Serial.printf("Bad Magic Bytes (%d:%d) on %d - %s (%d/5)\n", magic_bytes[0], magic_bytes[1], socket->index, repr(socket->key).c_str(), tries);
+
+ tries++;
+ delay(25);
+ }
+
+ if (tries == 5)
+ format_eeprom(socket);
+
+ read_eeprom(socket->index, FEATURE_MASK_OFFSET, (uint8_t *)&socket->key->feature_mask, 2);
+
+ if (check_feature_mask(socket, FEATURE_MASK_KEY_NAME))
+ read_eeprom(socket->index, KEY_NAME_OFFSET, (uint8_t *)socket->key->name, KEY_NAME_MAX - 1);
+ else
+ memcpy(socket->key->name, get_tracker_id(socket->key->id).c_str(), 16);
+
+ socket->key->loaded = true;
+
+ Serial.printf("Read Key Data on %d: %s\n", socket->index, repr(socket->key).c_str());
+}
+
+void set_socket(eeprom_key_socket_t *socket, eui48_t *identifier)
+{
+ socket->key = ensure_key(identifier);
+
+ socket->flashing = false;
+ socket->plugged = true;
+
+ bool already_seen = socket->key->loaded;
+ read_key_data(socket);
+
+ if (!already_seen)
+ publish_key_discovery(socket->key);
+
+ publish_key_state(socket->key, true, socket->index + 1);
+ publish_key_light_state(socket->key, false);
+}
+
+void unset_socket(eeprom_key_socket_t *socket)
+{
+ if (socket->plugged) {
+ publish_key_state(socket->key, false, 0);
+ publish_key_light_state(socket->key, false);
+ }
+
+ socket->flashing = false;
+ socket->plugged = false;
+ socket->key = NULL;
+}
+
+bool detect_key(uint8_t index, eui48_t *identifier)
+{
+ eui48_t addr_a, addr_b = {};
+ uint8_t any = 0;
+
+ addr_a = read_identifier(index);
+ addr_b = read_identifier(index);
+
+ if (!memcmp(&addr_a, &addr_b, sizeof(eui48_t)))
+ for (uint8_t i = 0; i < 3; i++)
+ any |= addr_a.vendor[i] | addr_a.device[i];
+
+ any &= (addr_a.vendor[0] != 0xff && addr_a.vendor[0] != 0x00);
+
+ if (any)
+ memcpy(identifier, &addr_a, sizeof(eui48_t));
+ else
+ memset(identifier, 0, sizeof(eui48_t));
+
+ return !!any;
+} \ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..3fee7e2
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,338 @@
+#include "config.h"
+#include "const.h"
+#include "pins.h"
+#include "types.h"
+#include "publish.h"
+#include "eeprom_key.h"
+#include "utils.h"
+
+#include <SPI.h>
+#include <WiFi.h>
+#include <PubSubClient.h>
+#include <ArduinoJson.h>
+
+const char *ssid = STA_SSID;
+const char *password = STA_PASSWD;
+
+const char *mqtt_server = MQTT_HOST;
+
+WiFiClient mqtt_wifi_client;
+PubSubClient mqtt(mqtt_wifi_client);
+
+eeprom_key_socket_t holder[NUM_KEY_SOCKETS] = {};
+
+volatile enum kh_states {
+ KH_SETUP,
+ KH_CONNECTING_WIFI,
+ KH_CONNECTING_MQTT,
+ KH_PUBLISHING_CONFIG,
+ KH_UPDATING_KEY_STATE,
+ KH_ERROR,
+} state;
+
+void update_leds_sockets();
+
+void mqtt_callback(char* topic, byte* payload, unsigned int length);
+
+void setup_wifi()
+{
+ WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info) {
+ Serial.println("WiFi.connected & got IP");
+ mqtt.connect(HOSTNAME);
+
+ state = KH_CONNECTING_MQTT;
+
+ }, WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
+
+ WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info) {
+ Serial.printf("WiFi disconnected, ");
+ Serial.printf("seems unexpected, resetting.\n");
+
+ WiFi.begin(ssid, password);
+ state = KH_CONNECTING_WIFI;
+
+ }, WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
+
+ WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info) {
+ Serial.printf("WifiEvent %d\n", event);
+ });
+}
+
+void setup_mqtt()
+{
+ mqtt.setServer(mqtt_server, 1883);
+ mqtt.setCallback(mqtt_callback);
+}
+
+void led_task(void *parameter)
+{
+ while (1) {
+ switch (state) {
+ case KH_SETUP:
+ case KH_CONNECTING_WIFI:
+ case KH_CONNECTING_MQTT:
+ digitalWrite(LED_STATUS, (millis() % 1000 < 500) ? HIGH : LOW);
+ break;
+ case KH_PUBLISHING_CONFIG:
+ digitalWrite(LED_STATUS, (millis() % 500 < 250) ? HIGH : LOW);
+ break;
+ case KH_UPDATING_KEY_STATE:
+ digitalWrite(LED_STATUS, HIGH);
+ break;
+ case KH_ERROR:
+ default:
+ digitalWrite(LED_STATUS, (millis() % 100 < 50) ? HIGH : LOW);
+ }
+ update_leds_sockets();
+ vTaskDelay(25 / portTICK_PERIOD_MS);
+ }
+}
+
+void update_key_status(bool send_discovery = false)
+{
+ eeprom_key_socket_t *socket = NULL;
+ eui48_t identifier = {};
+
+ bool changed = false;
+
+ for (uint8_t index = 0; index < NUM_KEY_SOCKETS; index++) {
+ socket = &holder[index];
+
+ if (detect_key(index, &identifier)) {
+ if (!socket->plugged || memcmp(&identifier, &socket->key->id, sizeof(eui48_t))) {
+ if (socket->plugged)
+ Serial.printf("Found changed key on %d: %s -> %s\n", index, repr(socket->key).c_str(), get_tracker_id(identifier).c_str());
+ else
+ Serial.printf("Found changed key on %d: unplugged -> %s\n", index, get_tracker_id(identifier).c_str());
+
+ changed = true;
+
+ unset_socket(socket);
+ set_socket(socket, &identifier);
+ }
+ } else {
+ if (socket->plugged) {
+ Serial.printf("Unplugged key on %d was %s\n", index, repr(socket->key).c_str());
+
+ changed = true;
+
+ unset_socket(socket);
+ }
+ }
+ }
+
+ if (changed) {
+ uint8_t key_count = 0;
+ for (uint8_t index = 0; index < NUM_KEY_SOCKETS; index++)
+ if (holder[index].plugged)
+ key_count++;
+
+ publish_holder_key_count(key_count);
+ }
+}
+
+void update_leds_sockets()
+{
+ auto status = (millis() % 1000 < 800) ? 85 : 170;
+ static uint8_t led_mapping[8] = {7, 0, 1, 2, 3, 6, 5, 4};
+
+ for (int8_t i = 7; i >= 0; i--) {
+ uint8_t led_offset = led_mapping[i];
+
+ if (led_offset % 2 == 0) {
+ // yellow LEDs
+ digitalWrite(LED_REG0, ((status >> led_offset) & 1) & holder[led_offset / 2].flashing);
+ digitalWrite(LED_REG1, ((status >> led_offset) & 1) & holder[4 + led_offset / 2].flashing);
+ digitalWrite(LED_REG2, ((status >> led_offset) & 1) & holder[8 + led_offset / 2].flashing);
+ } else {
+ // green LEDs
+ digitalWrite(LED_REG0, holder[led_offset / 2].plugged);
+ digitalWrite(LED_REG1, holder[4 + led_offset / 2].plugged);
+ digitalWrite(LED_REG2, holder[8 + led_offset / 2].plugged);
+ }
+
+ digitalWrite(LED_REG_SCLK, HIGH);
+ delayMicroseconds(10);
+ digitalWrite(LED_REG_SCLK, LOW);
+ }
+
+ digitalWrite(LED_REG_RCLK, HIGH);
+ delayMicroseconds(10);
+ digitalWrite(LED_REG_RCLK, LOW);
+
+}
+
+eeprom_key_socket_t *match_key_id(String key_id)
+{
+ eui48_t identifier = {};
+
+ char * end;
+ identifier.vendor[0] = strtol(key_id.substring(0, 2).c_str(), &end, 16);
+ identifier.vendor[1] = strtol(key_id.substring(2, 4).c_str(), &end, 16);
+ identifier.vendor[2] = strtol(key_id.substring(4, 6).c_str(), &end, 16);
+ identifier.device[0] = strtol(key_id.substring(6, 8).c_str(), &end, 16);
+ identifier.device[1] = strtol(key_id.substring(8, 10).c_str(), &end, 16);
+ identifier.device[2] = strtol(key_id.substring(10, 12).c_str(), &end, 16);
+
+ for (uint8_t i = 0; i < NUM_KEY_SOCKETS; i++) {
+ eeprom_key_socket_t *socket = &holder[i];
+
+ if (socket->key && !memcmp(&identifier, &socket->key->id, sizeof(eui48_t))) {
+ Serial.printf("%s at %i %s\n", key_id.c_str(), i, repr(socket->key).c_str());
+
+ return socket;
+ }
+ }
+
+ return NULL;
+}
+
+void mqtt_callback_toggle(char* topic, byte* payload, unsigned int length)
+{
+ String prefix = String("homeassistant/light/") + HOSTNAME + "/";
+ unsigned int start = prefix.length() + String("key-").length();
+ String key_id = String(topic).substring(start, start + 12);
+
+ eeprom_key_socket_t *socket = match_key_id(key_id);
+
+ if (socket) {
+ socket->flashing = !memcmp(payload, PAYLOAD_LIGHT_ON, 2);
+ publish_key_light_state(socket->key, socket->flashing);
+ }
+}
+
+void mqtt_callback_name(char* topic, byte* payload, unsigned int length)
+{
+ String prefix = String("homeassistant/device_tracker/") + HOSTNAME + "/";
+ unsigned int start = prefix.length() + String("key-").length();
+ String key_id = String(topic).substring(start, start + 12);
+
+ eeprom_key_socket_t *socket = match_key_id(key_id);
+
+ if (socket) {
+ memset(socket->key->name, '\0', KEY_NAME_MAX);
+
+ if (length >= KEY_NAME_MAX - 1) {
+ length = KEY_NAME_MAX - 1;
+ }
+
+ memcpy(socket->key->name, payload, length);
+ Serial.printf("Writing name %s\n", socket->key->name);
+
+ write_eeprom(socket->index, KEY_NAME_OFFSET, (uint8_t *)socket->key->name, KEY_NAME_MAX);
+
+ if (length > 0) {
+ set_feature_mask(socket, FEATURE_MASK_KEY_NAME);
+
+ } else {
+ unset_feature_mask(socket, FEATURE_MASK_KEY_NAME);
+
+ memcpy(socket->key->name, get_tracker_id(socket->key->id).c_str(), 16);
+ }
+
+ publish_key_discovery(socket->key);
+ }
+}
+
+void mqtt_callback(char* topic, byte* payload, unsigned int length)
+{
+ Serial.printf("mqtt message on topic: %s\n", topic);
+
+ if (String(topic).endsWith(String("/toggle")))
+ mqtt_callback_toggle(topic, payload, length);
+ else if (String(topic).endsWith(String("/name")))
+ mqtt_callback_name(topic, payload, length);
+}
+
+void setup()
+{
+ Serial.begin(115200);
+
+ pinMode(LED_REG_SCLK, OUTPUT);
+ pinMode(LED_REG_RCLK, OUTPUT);
+ digitalWrite(LED_REG_SCLK, LOW);
+ digitalWrite(LED_REG_RCLK, LOW);
+
+ pinMode(LED_REG0, OUTPUT);
+ pinMode(LED_REG1, OUTPUT);
+ pinMode(LED_REG2, OUTPUT);
+ digitalWrite(LED_REG0, LOW);
+ digitalWrite(LED_REG1, LOW);
+ digitalWrite(LED_REG2, LOW);
+
+ pinMode(SPI_CS, OUTPUT);
+ pinMode(CS_MULT_ENABLE, OUTPUT);
+ pinMode(CS_SEL0, OUTPUT);
+ pinMode(CS_SEL1, OUTPUT);
+ pinMode(CS_SEL2, OUTPUT);
+ pinMode(CS_SEL3, OUTPUT);
+ digitalWrite(SPI_CS, HIGH);
+ digitalWrite(CS_MULT_ENABLE, HIGH);
+ digitalWrite(CS_SEL0, LOW);
+ digitalWrite(CS_SEL1, LOW);
+ digitalWrite(CS_SEL2, LOW);
+ digitalWrite(CS_SEL3, LOW);
+
+ pinMode(LED_STATUS, OUTPUT);
+
+ SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
+ digitalWrite(SPI_CS, HIGH);
+
+ for (uint8_t index = 0; index < NUM_KEY_SOCKETS; index++)
+ holder[index].index = index;
+
+ xTaskCreate(led_task, "LEDTask", 1 << 10, NULL, 1, NULL);
+
+ setup_wifi();
+ setup_mqtt();
+
+ WiFi.begin(ssid, password);
+ state = KH_CONNECTING_WIFI;
+}
+
+void loop()
+{
+ mqtt.loop();
+
+ if (state == KH_CONNECTING_MQTT) {
+ if (mqtt.connected()) {
+
+ mqtt.subscribe((String("homeassistant/light/") + HOSTNAME + String("/+/toggle")).c_str());
+ mqtt.subscribe((String("homeassistant/device_tracker/") + HOSTNAME + String("/+/name")).c_str());
+
+ state = KH_PUBLISHING_CONFIG;
+ }
+ }
+
+ if (state == KH_PUBLISHING_CONFIG) {
+ publish_holder_sensor();
+
+ if (known_key_length == 0)
+ load_known_keys();
+
+ update_key_status();
+
+ for (uint8_t i = 0; i < known_key_length; i++) {
+ eeprom_key_t *key = known_keys[i];
+ if (!key->loaded) {
+ publish_key_discovery(key);
+ publish_key_state(key, false, 0);
+ }
+ }
+
+ state = KH_UPDATING_KEY_STATE;
+ }
+
+ if (state == KH_UPDATING_KEY_STATE) {
+ if (!mqtt.connected() && WiFi.isConnected()) {
+ mqtt.connect(HOSTNAME);
+ state = KH_CONNECTING_MQTT;
+ } else {
+ update_key_status();
+ }
+ }
+
+ if (state == KH_ERROR) {
+
+ }
+}
diff --git a/src/publish.cpp b/src/publish.cpp
new file mode 100644
index 0000000..46b3ea9
--- /dev/null
+++ b/src/publish.cpp
@@ -0,0 +1,194 @@
+#include "publish.h"
+
+#include "const.h"
+#include "config.h"
+#include "utils.h"
+
+#include <SPI.h>
+#include <WiFi.h>
+#include <PubSubClient.h>
+#include <ArduinoJson.h>
+
+String get_base_topic(String sensor_type, String entity_id)
+{
+ String topic = String("homeassistant/") + sensor_type + "/" + String(HOSTNAME) + "/" + entity_id;
+
+ return topic;
+}
+
+String get_base_topic(String sensor_type, eui48_t id)
+{
+ return get_base_topic(sensor_type, get_tracker_id(id));
+}
+
+void get_device(JsonObject *dev)
+{
+ (*dev)["name"] = HOSTNAME;
+ (*dev)["identifiers"] = HOSTNAME;
+ (*dev)["manufacturer"] = "Flobs";
+ (*dev)["model"] = "RJ45EEPROMKeyHolder v2.1";
+}
+
+void get_key_device(JsonObject *dev, eeprom_key_t *key)
+{
+ String tracker_id = get_tracker_id(key->id);
+ if (key->loaded)
+ (*dev)["name"] = key->name;
+ else
+ (*dev)["name"] = tracker_id;
+ (*dev)["identifiers"] = tracker_id;
+ (*dev)["manufacturer"] = "Flobs";
+ (*dev)["model"] = "RJ45EEPROMKey v2.0";
+ (*dev)["via_device"] = HOSTNAME;
+}
+
+void publish_key_device_tracker(eeprom_key_t *key)
+{
+ String base_topic = get_base_topic(String("device_tracker"), key->id);
+ String tracker_id = get_tracker_id(key->id);
+
+ StaticJsonDocument<512> config;
+ config["unique_id"] = tracker_id;
+ if (key->loaded)
+ config["name"] = key->name;
+ else
+ config["name"] = tracker_id;
+ config["state_topic"] = base_topic + "/state";
+ config["json_attributes_topic"] = base_topic + "/attributes";
+
+ JsonObject dev = config.createNestedObject("device");
+ get_key_device(&dev, key);
+
+ size_t message_size = measureJson(config);
+ mqtt.beginPublish((base_topic + "/config").c_str(), message_size, true);
+ serializeJson(config, mqtt);
+ mqtt.endPublish();
+}
+
+void publish_key_light(eeprom_key_t *key)
+{
+ String base_topic = get_base_topic(String("light"), key->id);
+ String tracker_id = get_tracker_id(key->id);
+
+ StaticJsonDocument<512> config;
+ config["unique_id"] = tracker_id;
+ if (key->loaded)
+ config["name"] = key->name;
+ else
+ config["name"] = tracker_id;
+ config["state_topic"] = base_topic + "/state";
+ config["command_topic"] = base_topic + "/toggle";
+ config["json_attributes_topic"] = base_topic + "/attributes";
+
+ JsonObject dev = config.createNestedObject("device");
+ get_key_device(&dev, key);
+
+ size_t message_size = measureJson(config);
+ mqtt.beginPublish((base_topic + "/config").c_str(), message_size, true);
+ serializeJson(config, mqtt);
+ mqtt.endPublish();
+}
+
+void publish_key_device_tracker_plugged(eeprom_key_t *key, const char *payload, const char *type)
+{
+ String base_topic = get_base_topic(String("device_automation"), key->id);
+ String tracker_id = get_tracker_id(key->id);
+
+ mqtt.publish((base_topic + "-" + type + "/config").c_str(), "");
+
+ /* triggers don't work as expected for now, needs more looking into.
+
+ StaticJsonDocument<512> config;
+ config["automation_type"] = "trigger";
+ config["payload"] = payload;
+ config["topic"] = get_base_topic(String("device_tracker"), key->id) + "/state";
+ config["type"] = type;
+ config["subtype"] = key->name;
+
+ JsonObject dev = config.createNestedObject("device");
+ get_key_device(&dev, key);
+
+ size_t message_size = measureJson(config);
+ mqtt.beginPublish((base_topic + "-" + type + "/config").c_str(), message_size, true);
+ serializeJson(config, mqtt);
+ mqtt.endPublish();
+ */
+}
+
+void publish_holder_sensor()
+{
+ String base_topic = get_base_topic(String("sensor"), HOSTNAME + String("_key_count"));
+
+ StaticJsonDocument<512> config;
+ config["unique_id"] = HOSTNAME + String("_key_count");
+ config["name"] = "Key Count";
+ config["icon"] = "mdi:pound";
+ config["state_topic"] = base_topic + "/state";
+
+ JsonObject dev = config.createNestedObject("device");
+ get_device(&dev);
+
+ size_t message_size = measureJson(config);
+ mqtt.beginPublish((base_topic + "/config").c_str(), message_size, true);
+ serializeJson(config, mqtt);
+ mqtt.endPublish();
+}
+
+void publish_key_discovery(eeprom_key_t *key)
+{
+ Serial.printf("Publish discovery: %s\n", repr(key).c_str());
+ publish_key_device_tracker(key);
+ publish_key_light(key);
+
+ publish_key_device_tracker_plugged(key, PAYLOAD_HOME, "plugged");
+ publish_key_device_tracker_plugged(key, PAYLOAD_NOT_HOME, "unplugged");
+
+ publish_holder_sensor();
+}
+
+void publish_key_state(eeprom_key_t *key, bool home, int8_t holder)
+{
+ Serial.printf("Publish Key State %s home:%d holder:%d\n", repr(key).c_str(), home, holder);
+
+ String topic = get_base_topic(String("device_tracker"), key->id);
+
+ const char *state = home ? PAYLOAD_HOME: PAYLOAD_NOT_HOME;
+
+ mqtt.publish((topic + "/state").c_str(), state, true);
+
+ publish_key_attributes(key, holder);
+}
+
+void publish_key_attributes(eeprom_key_t *key, int8_t holder)
+{
+ String topic = get_base_topic(String("device_tracker"), key->id);
+
+ StaticJsonDocument<512> attributes;
+ if (holder > 0)
+ attributes["holder"] = holder;
+ else
+ attributes["holder"] = (char *)0;
+
+ size_t message_size = measureJson(attributes);
+ mqtt.beginPublish((topic + "/attributes").c_str(), message_size, true);
+ serializeJson(attributes, mqtt);
+ mqtt.endPublish();
+}
+
+void publish_key_light_state(eeprom_key_t *key, bool enabled)
+{
+ String topic = get_base_topic(String("light"), key->id);
+
+ const char *state = enabled ? PAYLOAD_LIGHT_ON: PAYLOAD_LIGHT_OFF;
+
+ mqtt.publish((topic + "/state").c_str(), state, true);
+}
+
+void publish_holder_key_count(uint8_t count)
+{
+ Serial.printf("Publish Holder Key Count: %d\n", count);
+
+ String topic = get_base_topic(String("sensor"), HOSTNAME + String("_key_count"));
+
+ mqtt.publish((topic + "/state").c_str(), String(count).c_str(), true);
+} \ No newline at end of file
diff --git a/src/utils.cpp b/src/utils.cpp
new file mode 100644
index 0000000..669203b
--- /dev/null
+++ b/src/utils.cpp
@@ -0,0 +1,21 @@
+#include "utils.h"
+
+#include "const.h"
+#include "eeprom_key.h"
+
+String get_tracker_id(eui48_t id)
+{
+ char tracker_id[17] = {'\0'};
+
+ sprintf(tracker_id, "key-%02x%02x%02x%02x%02x%02x", id.vendor[0], id.vendor[1], id.vendor[2], id.device[0], id.device[1], id.device[2]);
+
+ return String(tracker_id);
+}
+
+String repr(eeprom_key_t *key)
+{
+ if (key->loaded && check_feature_mask(key, FEATURE_MASK_KEY_NAME))
+ return get_tracker_id(key->id) + " (" + String(key->name) + ")";
+ else
+ return get_tracker_id(key->id);
+} \ No newline at end of file
diff --git a/test/README b/test/README
new file mode 100644
index 0000000..df5066e
--- /dev/null
+++ b/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PIO Unit Testing and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PIO Unit Testing:
+- https://docs.platformio.org/page/plus/unit-testing.html