diff --git a/.gitignore b/.gitignore index e257658..89cc49c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,5 @@ -# ---> C++ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/data/root_ca.pem b/data/root_ca.pem new file mode 100644 index 0000000..b85c803 --- /dev/null +++ b/data/root_ca.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/include/DeviceIdentity.h b/include/DeviceIdentity.h new file mode 100644 index 0000000..ae31d3b --- /dev/null +++ b/include/DeviceIdentity.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "FilamentGuardDeviceInfo.h" + +class DeviceIdentity +{ +public: + FilamentGuardDeviceInfo create( + const String& hostname) const; + +private: + String createDeviceId() const; +}; \ No newline at end of file diff --git a/include/DeviceSettings.h b/include/DeviceSettings.h new file mode 100644 index 0000000..c005765 --- /dev/null +++ b/include/DeviceSettings.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class DeviceSettings +{ +public: + String getHostname(); + String getOrCreateHostname(); + + bool setHostname(const String& hostname); + bool clearHostname(); + +private: + static String createDefaultHostname(); + static String normalizeHostname(const String& hostname); + static bool isValidHostname(const String& hostname); +}; \ No newline at end of file diff --git a/include/FilamentGuardApiClient.h b/include/FilamentGuardApiClient.h new file mode 100644 index 0000000..a0f42d3 --- /dev/null +++ b/include/FilamentGuardApiClient.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class FilamentGuardApiClient +{ +public: + bool sendTagRead( + const String& json) const; + + bool sendTagEvent( + const String& json) const; + +private: + static constexpr const char* TagReadEndpointUrl = + "http://filamentguard.maximuss.dk/"; + + static constexpr const char* TagEventEndpointUrl = + "http://filamentguard.maximuss.dk/"; +}; \ No newline at end of file diff --git a/include/FilamentGuardDeviceInfo.h b/include/FilamentGuardDeviceInfo.h new file mode 100644 index 0000000..a9b48ed --- /dev/null +++ b/include/FilamentGuardDeviceInfo.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +struct FilamentGuardDeviceInfo +{ + String deviceId; + String hostname; +}; \ No newline at end of file diff --git a/include/FilamentGuardTagEvent.h b/include/FilamentGuardTagEvent.h new file mode 100644 index 0000000..fb167b1 --- /dev/null +++ b/include/FilamentGuardTagEvent.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +enum class FilamentGuardTagEventType +{ + TagPlaced, + TagRemoved +}; + +struct FilamentGuardTagEvent +{ + String deviceId; + String hostname; + + FilamentGuardTagEventType eventType; + + String tagUid; + String instanceUuid; + + String tagJson; + + unsigned long uptimeMilliseconds = 0; +}; \ No newline at end of file diff --git a/include/FilamentGuardTagEventSerializer.h b/include/FilamentGuardTagEventSerializer.h new file mode 100644 index 0000000..fe803e6 --- /dev/null +++ b/include/FilamentGuardTagEventSerializer.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "FilamentGuardTagEvent.h" + +class FilamentGuardTagEventSerializer +{ +public: + static String serialize( + const FilamentGuardTagEvent& tagEvent); + +private: + static const char* eventTypeToString( + FilamentGuardTagEventType eventType); +}; \ No newline at end of file diff --git a/include/FilamentGuardTagRead.h b/include/FilamentGuardTagRead.h new file mode 100644 index 0000000..76627ff --- /dev/null +++ b/include/FilamentGuardTagRead.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "FilamentGuardDeviceInfo.h" +#include "OpenPrintTagData.h" + +struct FilamentGuardTagRead +{ + FilamentGuardDeviceInfo device; + OpenPrintTagData tag; +}; \ No newline at end of file diff --git a/include/FilamentGuardTagReadSerializer.h b/include/FilamentGuardTagReadSerializer.h new file mode 100644 index 0000000..599ce04 --- /dev/null +++ b/include/FilamentGuardTagReadSerializer.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "FilamentGuardTagRead.h" + +class FilamentGuardTagReadSerializer +{ +public: + String serialize( + const FilamentGuardTagRead& tagRead) const; + +private: + String escapeJson( + const String& value) const; + + void appendStringProperty( + String& json, + const char* propertyName, + const String& value, + bool addComma = true) const; + + void appendUnsignedProperty( + String& json, + const char* propertyName, + uint16_t value, + bool addComma = true) const; + + void appendBooleanProperty( + String& json, + const char* propertyName, + bool value, + bool addComma = true) const; +}; \ No newline at end of file diff --git a/include/FirmwareUpdater.h b/include/FirmwareUpdater.h new file mode 100644 index 0000000..bb5c317 --- /dev/null +++ b/include/FirmwareUpdater.h @@ -0,0 +1,8 @@ +#pragma once + +class FirmwareUpdater +{ +public: + bool begin(); + void checkForUpdate(); +}; \ No newline at end of file diff --git a/include/OpenPrintTagData.h b/include/OpenPrintTagData.h new file mode 100644 index 0000000..2e26fbe --- /dev/null +++ b/include/OpenPrintTagData.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +struct OpenPrintTagData +{ + String tagUid; + String instanceUuid; + + String materialName; + String materialType; + String brand; + String colorHex; + + uint16_t actualWeightGrams = 0; + uint16_t nominalWeightGrams = 0; + uint16_t spoolWeightGrams = 0; + uint16_t consumedWeightGrams = 0; + uint16_t remainingWeightGrams = 0; + + bool hasActualWeight = false; + bool hasNominalWeight = false; + bool hasSpoolWeight = false; + bool hasConsumedWeight = false; +}; \ No newline at end of file diff --git a/include/OpenPrintTagParser.h b/include/OpenPrintTagParser.h new file mode 100644 index 0000000..42950e1 --- /dev/null +++ b/include/OpenPrintTagParser.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include "OpenPrintTagData.h" + +class OpenPrintTagParser +{ +public: + bool parse( + const uint8_t* memory, + size_t memoryLength, + OpenPrintTagData& data); + +private: + static constexpr uint8_t NdefTlvType = 0x03; + static constexpr uint8_t TerminatorTlvType = 0xFE; + + static constexpr const char* ExpectedMimeType = + "application/vnd.openprinttag"; + + bool findNdefMessage( + const uint8_t* memory, + size_t memoryLength, + const uint8_t*& ndefMessage, + size_t& ndefMessageLength) const; + + bool parseNdefRecord( + const uint8_t* ndefMessage, + size_t ndefMessageLength, + const uint8_t*& payload, + size_t& payloadLength) const; + + bool mimeTypeMatches( + const uint8_t* mimeType, + size_t mimeTypeLength) const; + + bool parseCborPayload( + const uint8_t* payload, + size_t payloadLength, + OpenPrintTagData& data) const; + + bool parseMainData( + const uint8_t* mainData, + size_t mainDataLength, + OpenPrintTagData& data) const; + + bool readUnsignedInteger( + CborValue* value, + uint64_t& result) const; + + bool readTextString( + CborValue* value, + String& result) const; + + bool readByteString( + CborValue* value, + uint8_t* buffer, + size_t bufferCapacity, + size_t& bytesRead) const; + + bool skipValue( + CborValue* value) const; + + String formatUuid( + const uint8_t* bytes, + size_t length) const; + + String formatColor( + const uint8_t* bytes, + size_t length) const; +}; \ No newline at end of file diff --git a/include/Pn5180Reader.h b/include/Pn5180Reader.h new file mode 100644 index 0000000..c5b9f10 --- /dev/null +++ b/include/Pn5180Reader.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +#include "FilamentGuardApiClient.h" +#include "FilamentGuardDeviceInfo.h" +#include "FilamentGuardTagRead.h" +#include "FilamentGuardTagReadSerializer.h" +#include "OpenPrintTagData.h" +#include "OpenPrintTagParser.h" + +class Pn5180Reader +{ +public: + Pn5180Reader(); + + void begin(); + + void setDeviceInfo( + const FilamentGuardDeviceInfo& deviceInfo); + + void printVersionInformation(); + void checkForTag(); + +private: + enum class TagProtocol : uint8_t + { + None, + Iso14443A, + Iso15693 + }; + + static constexpr uint8_t NssPin = 5; + static constexpr uint8_t BusyPin = 16; + static constexpr uint8_t ResetPin = 17; + + static constexpr uint8_t RequiredMissingScans = 3; + + PN5180ISO15693 iso15693Reader; + PN5180ISO14443 iso14443Reader; + + OpenPrintTagParser openPrintTagParser; + FilamentGuardTagReadSerializer tagReadSerializer; + FilamentGuardApiClient apiClient; + + FilamentGuardDeviceInfo deviceInfo; + + uint8_t lastUid[10] = {}; + uint8_t lastUidLength = 0; + + TagProtocol activeProtocol = TagProtocol::None; + uint8_t missingScanCount = 0; + + bool checkIso14443Tag(); + bool checkIso15693Tag(); + + void handleTagMissing(); + + bool isSameUid( + const uint8_t* uid, + uint8_t uidLength) const; + + void rememberUid( + const uint8_t* uid, + uint8_t uidLength, + TagProtocol protocol); + + void clearLastUid(); + + void printUid( + const uint8_t* uid, + uint8_t uidLength, + bool reverseOrder) const; + + String formatUid( + const uint8_t* uid, + uint8_t uidLength, + bool reverseOrder) const; + + void dumpIso15693Tag( + uint8_t* uid, + uint8_t uidLength); + + void printTagRead( + const FilamentGuardTagRead& tagRead) const; + + void printTagReadJson( + const FilamentGuardTagRead& tagRead) const; + + void sendTagRead( + const FilamentGuardTagRead& tagRead) const; + + void printBlock( + uint8_t blockNumber, + const uint8_t* data, + uint8_t blockSize) const; +}; \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +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 convention is to give header files names that end with `.h'. + +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/WifiSetup.h b/include/WifiSetup.h new file mode 100644 index 0000000..c06c492 --- /dev/null +++ b/include/WifiSetup.h @@ -0,0 +1,8 @@ +#pragma once + +class WifiSetup +{ +public: + bool connect(const char* hostname); + void resetSettings(); +}; \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /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 the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for 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 + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by 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..f98c351 --- /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 +monitor_speed = 115200 +upload_port = COM3 +monitor_port = COM3 +board_build.filesystem = spiffs +lib_deps = + tzapu/WiFiManager + chrisjoyce911/esp32FOTA + https://github.com/ATrappmann/PN5180-Library.git + soburi/TinyCBOR@^0.5.3-arduino2 diff --git a/src/DeviceIdentity.cpp b/src/DeviceIdentity.cpp new file mode 100644 index 0000000..1957d9a --- /dev/null +++ b/src/DeviceIdentity.cpp @@ -0,0 +1,36 @@ +#include "DeviceIdentity.h" + +#include + +FilamentGuardDeviceInfo DeviceIdentity::create( + const String& hostname) const +{ + FilamentGuardDeviceInfo deviceInfo; + + deviceInfo.deviceId = + createDeviceId(); + + deviceInfo.hostname = + hostname; + + return deviceInfo; +} + +String DeviceIdentity::createDeviceId() const +{ + const uint64_t chipId = + ESP.getEfuseMac(); + + char buffer[19] = {}; + + std::snprintf( + buffer, + sizeof(buffer), + "ESP32-%04X%08X", + static_cast( + chipId >> 32), + static_cast( + chipId)); + + return String(buffer); +} \ No newline at end of file diff --git a/src/DeviceSettings.cpp b/src/DeviceSettings.cpp new file mode 100644 index 0000000..d5800c0 --- /dev/null +++ b/src/DeviceSettings.cpp @@ -0,0 +1,159 @@ +#include "DeviceSettings.h" + +#include + +namespace +{ + constexpr const char* PreferencesNamespace = "device"; + constexpr const char* HostnameKey = "hostname"; +} + +String DeviceSettings::getHostname() +{ + Preferences preferences; + + if (!preferences.begin(PreferencesNamespace, true)) + { + Serial.println("Kunne ikke åbne indstillinger."); + return ""; + } + + const String hostname = + preferences.getString(HostnameKey, ""); + + preferences.end(); + + return hostname; +} + +String DeviceSettings::getOrCreateHostname() +{ + String hostname = getHostname(); + + if (isValidHostname(hostname)) + { + return hostname; + } + + hostname = createDefaultHostname(); + + if (!setHostname(hostname)) + { + Serial.println("Kunne ikke gemme standard-hostname."); + } + + return hostname; +} + +bool DeviceSettings::setHostname(const String& hostname) +{ + const String normalizedHostname = + normalizeHostname(hostname); + + if (!isValidHostname(normalizedHostname)) + { + Serial.println("Ugyldigt hostname."); + return false; + } + + Preferences preferences; + + if (!preferences.begin(PreferencesNamespace, false)) + { + Serial.println("Kunne ikke åbne indstillinger."); + return false; + } + + const size_t written = + preferences.putString( + HostnameKey, + normalizedHostname + ); + + preferences.end(); + + return written > 0; +} + +bool DeviceSettings::clearHostname() +{ + Preferences preferences; + + if (!preferences.begin(PreferencesNamespace, false)) + { + return false; + } + + const bool removed = + preferences.remove(HostnameKey); + + preferences.end(); + + return removed; +} + +String DeviceSettings::createDefaultHostname() +{ + const uint64_t chipId = + ESP.getEfuseMac(); + + char hostname[32]; + + snprintf( + hostname, + sizeof(hostname), + "filamentguard-%06llx", + static_cast( + chipId & 0xFFFFFF + ) + ); + + return String(hostname); +} + +String DeviceSettings::normalizeHostname( + const String& hostname +) +{ + String normalized = hostname; + + normalized.trim(); + normalized.toLowerCase(); + + return normalized; +} + +bool DeviceSettings::isValidHostname( + const String& hostname +) +{ + const size_t length = hostname.length(); + + if (length == 0 || length > 32) + { + return false; + } + + if (hostname[0] == '-' || + hostname[length - 1] == '-') + { + return false; + } + + for (size_t index = 0; index < length; index++) + { + const char character = hostname[index]; + + const bool valid = + (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || + character == '-'; + + if (!valid) + { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/src/FilamentGuardApiClient.cpp b/src/FilamentGuardApiClient.cpp new file mode 100644 index 0000000..8672161 --- /dev/null +++ b/src/FilamentGuardApiClient.cpp @@ -0,0 +1,121 @@ +#include "FilamentGuardApiClient.h" + +#include +#include + +namespace +{ + bool postJson( + const char* endpointUrl, + const String& json, + const char* description) + { + if (WiFi.status() != WL_CONNECTED) + { + Serial.print(description); + Serial.println( + " blev ikke sendt: Wi-Fi er ikke forbundet."); + + return false; + } + + if (json.isEmpty()) + { + Serial.print(description); + Serial.println( + " blev ikke sendt: JSON-data er tomme."); + + return false; + } + + HTTPClient httpClient; + + if (!httpClient.begin(endpointUrl)) + { + Serial.print(description); + Serial.println( + " kunne ikke initialiseres."); + + return false; + } + + httpClient.addHeader( + "Content-Type", + "application/json"); + + Serial.println(); + Serial.print("Sender "); + Serial.print(description); + Serial.print(" til: "); + Serial.println(endpointUrl); + + const int responseCode = + httpClient.POST(json); + + if (responseCode <= 0) + { + Serial.print(description); + Serial.print( + " fejlede. HTTPClient-fejl: "); + + Serial.println( + httpClient.errorToString( + responseCode)); + + httpClient.end(); + + return false; + } + + const String responseBody = + httpClient.getString(); + + Serial.print("HTTP-status: "); + Serial.println(responseCode); + + if (!responseBody.isEmpty()) + { + Serial.println("Svar fra API:"); + Serial.println(responseBody); + } + + const bool successful = + responseCode >= 200 && + responseCode < 300; + + if (successful) + { + Serial.print(description); + Serial.println( + " blev sendt korrekt."); + } + else + { + Serial.print(description); + Serial.println( + " blev afvist af API'et."); + } + + httpClient.end(); + + return successful; + } +} + +bool FilamentGuardApiClient::sendTagRead( + const String& json) const +{ + return postJson( + TagReadEndpointUrl, + json, + "tagregistrering"); +} + +bool FilamentGuardApiClient::sendTagEvent( + const String& json) const +{ + return postJson( + TagEventEndpointUrl, + json, + "taghændelse"); +} \ No newline at end of file diff --git a/src/FilamentGuardTagEventSerializer.cpp b/src/FilamentGuardTagEventSerializer.cpp new file mode 100644 index 0000000..588a185 --- /dev/null +++ b/src/FilamentGuardTagEventSerializer.cpp @@ -0,0 +1,55 @@ +#include "FilamentGuardTagEventSerializer.h" + +#include + +String FilamentGuardTagEventSerializer::serialize( + const FilamentGuardTagEvent& tagEvent) +{ + JsonDocument document; + + document["deviceId"] = + tagEvent.deviceId; + + document["hostname"] = + tagEvent.hostname; + + document["eventType"] = + eventTypeToString( + tagEvent.eventType); + + document["tagUid"] = + tagEvent.tagUid; + + document["instanceUuid"] = + tagEvent.instanceUuid; + + document["tagJson"] = + tagEvent.tagJson; + + document["uptimeMilliseconds"] = + tagEvent.uptimeMilliseconds; + + String json; + + serializeJson( + document, + json); + + return json; +} + +const char* FilamentGuardTagEventSerializer::eventTypeToString( + const FilamentGuardTagEventType eventType) +{ + switch (eventType) + { + case FilamentGuardTagEventType::TagPlaced: + return "TagPlaced"; + + case FilamentGuardTagEventType::TagRemoved: + return "TagRemoved"; + + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/FilamentGuardTagReadSerializer.cpp b/src/FilamentGuardTagReadSerializer.cpp new file mode 100644 index 0000000..01ec177 --- /dev/null +++ b/src/FilamentGuardTagReadSerializer.cpp @@ -0,0 +1,235 @@ +#include "FilamentGuardTagReadSerializer.h" + +String FilamentGuardTagReadSerializer::serialize( + const FilamentGuardTagRead& tagRead) const +{ + String json; + + json.reserve(768); + + json += '{'; + + json += "\"device\":{"; + + appendStringProperty( + json, + "deviceId", + tagRead.device.deviceId); + + appendStringProperty( + json, + "hostname", + tagRead.device.hostname, + false); + + json += "},"; + + json += "\"tag\":{"; + + appendStringProperty( + json, + "tagUid", + tagRead.tag.tagUid); + + appendStringProperty( + json, + "instanceUuid", + tagRead.tag.instanceUuid); + + appendStringProperty( + json, + "materialName", + tagRead.tag.materialName); + + appendStringProperty( + json, + "materialType", + tagRead.tag.materialType); + + appendStringProperty( + json, + "brand", + tagRead.tag.brand); + + appendStringProperty( + json, + "colorHex", + tagRead.tag.colorHex); + + appendUnsignedProperty( + json, + "actualWeightGrams", + tagRead.tag.actualWeightGrams); + + appendUnsignedProperty( + json, + "nominalWeightGrams", + tagRead.tag.nominalWeightGrams); + + appendUnsignedProperty( + json, + "spoolWeightGrams", + tagRead.tag.spoolWeightGrams); + + appendUnsignedProperty( + json, + "consumedWeightGrams", + tagRead.tag.consumedWeightGrams); + + appendUnsignedProperty( + json, + "remainingWeightGrams", + tagRead.tag.remainingWeightGrams); + + appendBooleanProperty( + json, + "hasActualWeight", + tagRead.tag.hasActualWeight); + + appendBooleanProperty( + json, + "hasNominalWeight", + tagRead.tag.hasNominalWeight); + + appendBooleanProperty( + json, + "hasSpoolWeight", + tagRead.tag.hasSpoolWeight); + + appendBooleanProperty( + json, + "hasConsumedWeight", + tagRead.tag.hasConsumedWeight, + false); + + json += '}'; + + json += '}'; + + return json; +} + +String FilamentGuardTagReadSerializer::escapeJson( + const String& value) const +{ + String escaped; + + escaped.reserve( + value.length() + 16); + + for (size_t index = 0; + index < value.length(); + ++index) + { + const char character = + value[index]; + + switch (character) + { + case '"': + escaped += "\\\""; + break; + + case '\\': + escaped += "\\\\"; + break; + + case '\b': + escaped += "\\b"; + break; + + case '\f': + escaped += "\\f"; + break; + + case '\n': + escaped += "\\n"; + break; + + case '\r': + escaped += "\\r"; + break; + + case '\t': + escaped += "\\t"; + break; + + default: + if (static_cast(character) < 0x20) + { + char buffer[7] = {}; + + std::snprintf( + buffer, + sizeof(buffer), + "\\u%04X", + static_cast( + character)); + + escaped += buffer; + } + else + { + escaped += character; + } + + break; + } + } + + return escaped; +} + +void FilamentGuardTagReadSerializer::appendStringProperty( + String& json, + const char* propertyName, + const String& value, + bool addComma) const +{ + json += '"'; + json += propertyName; + json += "\":\""; + json += escapeJson(value); + json += '"'; + + if (addComma) + { + json += ','; + } +} + +void FilamentGuardTagReadSerializer::appendUnsignedProperty( + String& json, + const char* propertyName, + uint16_t value, + bool addComma) const +{ + json += '"'; + json += propertyName; + json += "\":"; + json += String(value); + + if (addComma) + { + json += ','; + } +} + +void FilamentGuardTagReadSerializer::appendBooleanProperty( + String& json, + const char* propertyName, + bool value, + bool addComma) const +{ + json += '"'; + json += propertyName; + json += "\":"; + json += value + ? "true" + : "false"; + + if (addComma) + { + json += ','; + } +} \ No newline at end of file diff --git a/src/FirmwareUpdater.cpp b/src/FirmwareUpdater.cpp new file mode 100644 index 0000000..ffc0ffb --- /dev/null +++ b/src/FirmwareUpdater.cpp @@ -0,0 +1,111 @@ +#include "FirmwareUpdater.h" + +#include +#include +#include +#include + +namespace +{ + constexpr const char* FirmwareType = "filamentguard"; + constexpr const char* FirmwareVersion = "1.0.1"; + + constexpr const char* ManifestUrl = + "https://ota-filamentguard.maximuss.dk/firmware/manifest.json"; + + esp32FOTA firmwareUpdate( + FirmwareType, + FirmwareVersion + ); + + CryptoFileAsset rootCertificate( + "/root_ca.pem", + &SPIFFS + ); +} + +bool FirmwareUpdater::begin() +{ + Serial.println("Starter firmware-opdatering..."); + + if (!SPIFFS.begin(false)) + { + Serial.println("Kunne ikke starte SPIFFS."); + return false; + } + + if (!SPIFFS.exists("/root_ca.pem")) + { + Serial.println("Certifikatet /root_ca.pem findes ikke."); + return false; + } + + firmwareUpdate.setManifestURL(ManifestUrl); + firmwareUpdate.setRootCA(&rootCertificate); + + firmwareUpdate.setProgressCb( + [](size_t progress, size_t total) + { + if (total == 0) + { + return; + } + + const unsigned int percentage = + static_cast( + (progress * 100) / total + ); + + Serial.printf( + "\rFirmware download: %u%%", + percentage + ); + + if (progress == total) + { + Serial.println(); + } + } + ); + + Serial.print("Firmwaretype: "); + Serial.println(FirmwareType); + + Serial.print("Firmwareversion: "); + Serial.println(FirmwareVersion); + + Serial.print("Manifest: "); + Serial.println(ManifestUrl); + + Serial.println("Firmware-opdatering er klar."); + + return true; +} + +void FirmwareUpdater::checkForUpdate() +{ + if (WiFi.status() != WL_CONNECTED) + { + Serial.println( + "Firmwarekontrol sprunget over: Wi-Fi er ikke forbundet." + ); + + return; + } + + Serial.println("Kontrollerer efter ny firmware..."); + + const bool updateAvailable = + firmwareUpdate.execHTTPcheck(); + + if (!updateAvailable) + { + Serial.println("Ingen ny firmware fundet."); + return; + } + + Serial.println("Ny firmware fundet."); + Serial.println("Downloader og installerer firmware..."); + + firmwareUpdate.execOTA(); +} \ No newline at end of file diff --git a/src/OpenPrintTagParser.cpp b/src/OpenPrintTagParser.cpp new file mode 100644 index 0000000..64e14f3 --- /dev/null +++ b/src/OpenPrintTagParser.cpp @@ -0,0 +1,951 @@ +#include "OpenPrintTagParser.h" + +#include +#include + +namespace +{ + constexpr uint64_t FieldInstanceUuid = 0; + constexpr uint64_t FieldMaterialName = 10; + constexpr uint64_t FieldBrand = 11; + constexpr uint64_t FieldNominalWeight = 16; + constexpr uint64_t FieldActualWeight = 17; + constexpr uint64_t FieldSpoolWeight = 18; + constexpr uint64_t FieldPrimaryColor = 19; + constexpr uint64_t FieldMaterialType = 52; + + constexpr uint64_t MetadataAuxOffset = 2; + + bool readAuxOffset( + const uint8_t* payload, + size_t payloadLength, + size_t& auxOffset) + { + CborParser parser; + CborValue root; + CborValue iterator; + + const CborError parserResult = + cbor_parser_init( + payload, + payloadLength, + 0, + &parser, + &root); + + if (parserResult != CborNoError || + !cbor_value_is_map(&root)) + { + return false; + } + + if (cbor_value_enter_container( + &root, + &iterator) != CborNoError) + { + return false; + } + + while (!cbor_value_at_end(&iterator)) + { + if (!cbor_value_is_unsigned_integer( + &iterator)) + { + return false; + } + + uint64_t key = 0; + + if (cbor_value_get_uint64( + &iterator, + &key) != CborNoError) + { + return false; + } + + if (cbor_value_advance( + &iterator) != CborNoError) + { + return false; + } + + if (cbor_value_at_end(&iterator)) + { + return false; + } + + if (key == MetadataAuxOffset) + { + if (!cbor_value_is_unsigned_integer( + &iterator)) + { + return false; + } + + uint64_t value = 0; + + if (cbor_value_get_uint64( + &iterator, + &value) != CborNoError) + { + return false; + } + + if (value >= payloadLength) + { + return false; + } + + auxOffset = + static_cast(value); + + return true; + } + + if (cbor_value_advance( + &iterator) != CborNoError) + { + return false; + } + } + + return false; + } +} + +bool OpenPrintTagParser::parse( + const uint8_t* memory, + size_t memoryLength, + OpenPrintTagData& data) +{ + data = OpenPrintTagData{}; + + if (memory == nullptr || + memoryLength == 0) + { + Serial.println( + "OpenPrintTag-parser: Hukommelsesbufferen er tom."); + + return false; + } + + const uint8_t* ndefMessage = nullptr; + size_t ndefMessageLength = 0; + + if (!findNdefMessage( + memory, + memoryLength, + ndefMessage, + ndefMessageLength)) + { + Serial.println( + "OpenPrintTag-parser: Ingen gyldig NDEF-meddelelse fundet."); + + return false; + } + + const uint8_t* payload = nullptr; + size_t payloadLength = 0; + + if (!parseNdefRecord( + ndefMessage, + ndefMessageLength, + payload, + payloadLength)) + { + Serial.println( + "OpenPrintTag-parser: NDEF-recorden kunne ikke fortolkes."); + + return false; + } + + if (!parseCborPayload( + payload, + payloadLength, + data)) + { + Serial.println( + "OpenPrintTag-parser: CBOR-data kunne ikke fortolkes."); + + return false; + } + + const uint16_t startingWeight = + data.hasActualWeight + ? data.actualWeightGrams + : data.nominalWeightGrams; + + if (startingWeight == 0) + { + data.remainingWeightGrams = 0; + } + else if (data.consumedWeightGrams >= startingWeight) + { + data.remainingWeightGrams = 0; + } + else + { + data.remainingWeightGrams = + startingWeight - + data.consumedWeightGrams; + } + + Serial.println(); + Serial.println("OpenPrintTag-data:"); + + Serial.print("Instance UUID: "); + Serial.println(data.instanceUuid); + + Serial.print("Producent: "); + Serial.println(data.brand); + + Serial.print("Materiale: "); + Serial.println(data.materialName); + + Serial.print("Materialetype: "); + Serial.println(data.materialType); + + Serial.print("Farve: "); + Serial.println(data.colorHex); + + if (data.hasNominalWeight) + { + Serial.print("Nominel vaegt: "); + Serial.print(data.nominalWeightGrams); + Serial.println(" g"); + } + + if (data.hasActualWeight) + { + Serial.print("Faktisk startvaegt: "); + Serial.print(data.actualWeightGrams); + Serial.println(" g"); + } + + if (data.hasSpoolWeight) + { + Serial.print("Spolevaegt: "); + Serial.print(data.spoolWeightGrams); + Serial.println(" g"); + } + + Serial.print("Forbrugt vaegt: "); + Serial.print(data.consumedWeightGrams); + Serial.println(" g"); + + Serial.print("Resterende vaegt: "); + Serial.print(data.remainingWeightGrams); + Serial.println(" g"); + + return true; +} + +bool OpenPrintTagParser::findNdefMessage( + const uint8_t* memory, + size_t memoryLength, + const uint8_t*& ndefMessage, + size_t& ndefMessageLength) const +{ + size_t offset = 4; + + while (offset < memoryLength) + { + const uint8_t tlvType = + memory[offset++]; + + if (tlvType == 0x00) + { + continue; + } + + if (tlvType == TerminatorTlvType) + { + return false; + } + + if (offset >= memoryLength) + { + return false; + } + + size_t tlvLength = + memory[offset++]; + + if (tlvLength == 0xFF) + { + if (offset + 2 > memoryLength) + { + return false; + } + + tlvLength = + (static_cast( + memory[offset]) << 8) | + static_cast( + memory[offset + 1]); + + offset += 2; + } + + if (offset + tlvLength > + memoryLength) + { + return false; + } + + if (tlvType == NdefTlvType) + { + ndefMessage = + memory + offset; + + ndefMessageLength = + tlvLength; + + return true; + } + + offset += tlvLength; + } + + return false; +} + +bool OpenPrintTagParser::parseNdefRecord( + const uint8_t* ndefMessage, + size_t ndefMessageLength, + const uint8_t*& payload, + size_t& payloadLength) const +{ + if (ndefMessage == nullptr || + ndefMessageLength < 3) + { + return false; + } + + size_t offset = 0; + + const uint8_t header = + ndefMessage[offset++]; + + const bool messageBegin = + (header & 0x80) != 0; + + const bool messageEnd = + (header & 0x40) != 0; + + const bool chunked = + (header & 0x20) != 0; + + const bool shortRecord = + (header & 0x10) != 0; + + const bool idLengthPresent = + (header & 0x08) != 0; + + const uint8_t typeNameFormat = + header & 0x07; + + if (!messageBegin || + !messageEnd || + chunked || + typeNameFormat != 0x02) + { + return false; + } + + if (offset >= ndefMessageLength) + { + return false; + } + + const size_t mimeTypeLength = + ndefMessage[offset++]; + + if (shortRecord) + { + if (offset >= ndefMessageLength) + { + return false; + } + + payloadLength = + ndefMessage[offset++]; + } + else + { + if (offset + 4 > + ndefMessageLength) + { + return false; + } + + payloadLength = + (static_cast( + ndefMessage[offset]) << 24) | + (static_cast( + ndefMessage[offset + 1]) << 16) | + (static_cast( + ndefMessage[offset + 2]) << 8) | + static_cast( + ndefMessage[offset + 3]); + + offset += 4; + } + + size_t idLength = 0; + + if (idLengthPresent) + { + if (offset >= ndefMessageLength) + { + return false; + } + + idLength = + ndefMessage[offset++]; + } + + if (offset + mimeTypeLength > + ndefMessageLength) + { + return false; + } + + const uint8_t* mimeType = + ndefMessage + offset; + + if (!mimeTypeMatches( + mimeType, + mimeTypeLength)) + { + return false; + } + + offset += mimeTypeLength; + + if (offset + idLength > + ndefMessageLength) + { + return false; + } + + offset += idLength; + + if (offset + payloadLength > + ndefMessageLength) + { + return false; + } + + payload = + ndefMessage + offset; + + return true; +} + +bool OpenPrintTagParser::mimeTypeMatches( + const uint8_t* mimeType, + size_t mimeTypeLength) const +{ + const size_t expectedLength = + std::strlen(ExpectedMimeType); + + return mimeTypeLength == expectedLength && + std::memcmp( + mimeType, + ExpectedMimeType, + expectedLength) == 0; +} + +bool OpenPrintTagParser::parseCborPayload( + const uint8_t* payload, + size_t payloadLength, + OpenPrintTagData& data) const +{ + size_t auxOffset = 0; + + if (!readAuxOffset( + payload, + payloadLength, + auxOffset)) + { + Serial.println( + "CBOR-parser: Aux-offset kunne ikke laeses."); + + return false; + } + + CborParser metadataParser; + CborValue metadataValue; + + if (cbor_parser_init( + payload, + payloadLength, + 0, + &metadataParser, + &metadataValue) != CborNoError) + { + Serial.println( + "CBOR-parser: Metadata kunne ikke startes."); + + return false; + } + + /* + * Metadata er selv en CBOR-map. Hele mappen skal + * springes over, før adressen til main-data kan findes. + */ + if (!skipValue(&metadataValue)) + { + Serial.println( + "CBOR-parser: Metadata kunne ikke springes over."); + + return false; + } + + const uint8_t* mainData = + cbor_value_get_next_byte( + &metadataValue); + + if (mainData == nullptr || + mainData <= payload || + mainData >= payload + auxOffset) + { + Serial.println( + "CBOR-parser: Main-data har et ugyldigt offset."); + + return false; + } + + const size_t mainOffset = + static_cast( + mainData - payload); + + const size_t mainDataLength = + auxOffset - mainOffset; + + return parseMainData( + mainData, + mainDataLength, + data); +} + +bool OpenPrintTagParser::parseMainData( + const uint8_t* mainData, + size_t mainDataLength, + OpenPrintTagData& data) const +{ + CborParser parser; + CborValue root; + CborValue iterator; + + if (cbor_parser_init( + mainData, + mainDataLength, + 0, + &parser, + &root) != CborNoError || + !cbor_value_is_map(&root)) + { + Serial.println( + "CBOR-parser: Main-data er ikke en map."); + + return false; + } + + if (cbor_value_enter_container( + &root, + &iterator) != CborNoError) + { + return false; + } + + while (!cbor_value_at_end(&iterator)) + { + uint64_t fieldId = 0; + + if (!readUnsignedInteger( + &iterator, + fieldId)) + { + return false; + } + + switch (fieldId) + { + case FieldInstanceUuid: + { + uint8_t bytes[16] = {}; + size_t bytesRead = 0; + + if (!readByteString( + &iterator, + bytes, + sizeof(bytes), + bytesRead)) + { + return false; + } + + data.instanceUuid = + formatUuid( + bytes, + bytesRead); + + break; + } + + case FieldMaterialName: + if (!readTextString( + &iterator, + data.materialName)) + { + return false; + } + break; + + case FieldBrand: + if (!readTextString( + &iterator, + data.brand)) + { + return false; + } + break; + + case FieldMaterialType: + if (!readTextString( + &iterator, + data.materialType)) + { + return false; + } + break; + + case FieldPrimaryColor: + { + uint8_t bytes[4] = {}; + size_t bytesRead = 0; + + if (!readByteString( + &iterator, + bytes, + sizeof(bytes), + bytesRead)) + { + return false; + } + + data.colorHex = + formatColor( + bytes, + bytesRead); + + break; + } + + case FieldNominalWeight: + { + uint64_t value = 0; + + if (!readUnsignedInteger( + &iterator, + value)) + { + return false; + } + + data.nominalWeightGrams = + static_cast(value); + + data.hasNominalWeight = true; + break; + } + + case FieldActualWeight: + { + uint64_t value = 0; + + if (!readUnsignedInteger( + &iterator, + value)) + { + return false; + } + + data.actualWeightGrams = + static_cast(value); + + data.hasActualWeight = true; + break; + } + + case FieldSpoolWeight: + { + uint64_t value = 0; + + if (!readUnsignedInteger( + &iterator, + value)) + { + return false; + } + + data.spoolWeightGrams = + static_cast(value); + + data.hasSpoolWeight = true; + break; + } + + default: + if (!skipValue(&iterator)) + { + Serial.print( + "CBOR-parser: Felt kunne ikke springes over: "); + + Serial.println( + static_cast( + fieldId)); + + return false; + } + break; + } + } + + return true; +} + +bool OpenPrintTagParser::readUnsignedInteger( + CborValue* value, + uint64_t& result) const +{ + if (value == nullptr || + !cbor_value_is_unsigned_integer(value)) + { + return false; + } + + if (cbor_value_get_uint64( + value, + &result) != CborNoError) + { + return false; + } + + return cbor_value_advance(value) == + CborNoError; +} + +bool OpenPrintTagParser::readTextString( + CborValue* value, + String& result) const +{ + if (value == nullptr || + !cbor_value_is_text_string(value)) + { + return false; + } + + size_t textLength = 0; + + if (cbor_value_calculate_string_length( + value, + &textLength) != CborNoError) + { + return false; + } + + char* buffer = + new (std::nothrow) + char[textLength + 1]; + + if (buffer == nullptr) + { + return false; + } + + size_t bufferLength = + textLength + 1; + + CborValue nextValue; + + const CborError copyResult = + cbor_value_copy_text_string( + value, + buffer, + &bufferLength, + &nextValue); + + if (copyResult != CborNoError) + { + delete[] buffer; + return false; + } + + buffer[textLength] = '\0'; + result = buffer; + + delete[] buffer; + + *value = nextValue; + + return true; +} + +bool OpenPrintTagParser::readByteString( + CborValue* value, + uint8_t* buffer, + size_t bufferCapacity, + size_t& bytesRead) const +{ + if (value == nullptr || + !cbor_value_is_byte_string(value)) + { + return false; + } + + size_t byteLength = 0; + + if (cbor_value_calculate_string_length( + value, + &byteLength) != CborNoError || + byteLength > bufferCapacity) + { + return false; + } + + size_t bufferLength = + byteLength; + + CborValue nextValue; + + const CborError copyResult = + cbor_value_copy_byte_string( + value, + buffer, + &bufferLength, + &nextValue); + + if (copyResult != CborNoError) + { + return false; + } + + bytesRead = bufferLength; + *value = nextValue; + + return true; +} + +bool OpenPrintTagParser::skipValue( + CborValue* value) const +{ + if (value == nullptr || + !cbor_value_is_valid(value)) + { + return false; + } + + if (cbor_value_is_array(value) || + cbor_value_is_map(value)) + { + CborValue child; + + if (cbor_value_enter_container( + value, + &child) != CborNoError) + { + return false; + } + + while (!cbor_value_at_end(&child)) + { + if (!skipValue(&child)) + { + return false; + } + } + + return cbor_value_leave_container( + value, + &child) == + CborNoError; + } + + return cbor_value_advance(value) == + CborNoError; +} + +String OpenPrintTagParser::formatUuid( + const uint8_t* bytes, + size_t length) const +{ + if (bytes == nullptr || + length != 16) + { + return String(); + } + + String result; + result.reserve(36); + + for (size_t index = 0; + index < length; + ++index) + { + if (bytes[index] < 0x10) + { + result += '0'; + } + + result += String( + bytes[index], + HEX); + + if (index == 3 || + index == 5 || + index == 7 || + index == 9) + { + result += '-'; + } + } + + result.toLowerCase(); + + return result; +} + +String OpenPrintTagParser::formatColor( + const uint8_t* bytes, + size_t length) const +{ + if (bytes == nullptr || + length != 4) + { + return String(); + } + + String result = "#"; + result.reserve(9); + + for (size_t index = 0; + index < length; + ++index) + { + if (bytes[index] < 0x10) + { + result += '0'; + } + + result += String( + bytes[index], + HEX); + } + + result.toUpperCase(); + + return result; +} \ No newline at end of file diff --git a/src/Pn5180Reader.cpp b/src/Pn5180Reader.cpp new file mode 100644 index 0000000..2bdb992 --- /dev/null +++ b/src/Pn5180Reader.cpp @@ -0,0 +1,634 @@ +#include "Pn5180Reader.h" + +#include + +Pn5180Reader::Pn5180Reader() + : iso15693Reader(NssPin, BusyPin, ResetPin), + iso14443Reader(NssPin, BusyPin, ResetPin) +{ +} + +void Pn5180Reader::begin() +{ + iso15693Reader.begin(); + iso14443Reader.begin(); + + iso15693Reader.reset(); + iso15693Reader.setupRF(); +} + +void Pn5180Reader::setDeviceInfo( + const FilamentGuardDeviceInfo& newDeviceInfo) +{ + deviceInfo = newDeviceInfo; +} + +void Pn5180Reader::printVersionInformation() +{ + uint8_t productVersion[2] = {}; + uint8_t firmwareVersion[2] = {}; + uint8_t eepromVersion[2] = {}; + + const bool productRead = + iso15693Reader.readEEprom( + PRODUCT_VERSION, + productVersion, + sizeof(productVersion)); + + const bool firmwareRead = + iso15693Reader.readEEprom( + FIRMWARE_VERSION, + firmwareVersion, + sizeof(firmwareVersion)); + + const bool eepromRead = + iso15693Reader.readEEprom( + EEPROM_VERSION, + eepromVersion, + sizeof(eepromVersion)); + + Serial.println(); + Serial.println("PN5180 versionsoplysninger:"); + + if (!productRead || + !firmwareRead || + !eepromRead) + { + Serial.println( + "Kunne ikke laese versionsoplysninger fra PN5180."); + + return; + } + + Serial.printf( + "Produktversion: %u.%u\n", + productVersion[1], + productVersion[0]); + + Serial.printf( + "Firmwareversion: %u.%u\n", + firmwareVersion[1], + firmwareVersion[0]); + + Serial.printf( + "EEPROM-version: %u.%u\n", + eepromVersion[1], + eepromVersion[0]); +} + +void Pn5180Reader::checkForTag() +{ + bool tagFound = false; + + if (activeProtocol == TagProtocol::Iso14443A) + { + tagFound = checkIso14443Tag(); + } + else if (activeProtocol == TagProtocol::Iso15693) + { + tagFound = checkIso15693Tag(); + } + else + { + tagFound = checkIso14443Tag(); + + if (!tagFound) + { + tagFound = checkIso15693Tag(); + } + } + + if (tagFound) + { + missingScanCount = 0; + return; + } + + handleTagMissing(); +} + +bool Pn5180Reader::checkIso14443Tag() +{ + uint8_t uid[10] = {}; + + iso14443Reader.reset(); + iso14443Reader.setupRF(); + + if (!iso14443Reader.isCardPresent()) + { + return false; + } + + const uint8_t uidLength = + iso14443Reader.readCardSerial(uid); + + if (uidLength == 0) + { + return false; + } + + if (activeProtocol == TagProtocol::Iso14443A) + { + return isSameUid( + uid, + uidLength); + } + + if (activeProtocol != TagProtocol::None) + { + return false; + } + + rememberUid( + uid, + uidLength, + TagProtocol::Iso14443A); + + Serial.print( + "ISO14443A-tag fundet | UID: "); + + printUid( + uid, + uidLength, + false); + + Serial.println(); + + return true; +} + +bool Pn5180Reader::checkIso15693Tag() +{ + uint8_t uid[8] = {}; + + iso15693Reader.reset(); + iso15693Reader.setupRF(); + + const ISO15693ErrorCode result = + iso15693Reader.getInventory(uid); + + if (result != ISO15693_EC_OK) + { + return false; + } + + constexpr uint8_t uidLength = 8; + + if (activeProtocol == TagProtocol::Iso15693) + { + return isSameUid( + uid, + uidLength); + } + + if (activeProtocol != TagProtocol::None) + { + return false; + } + + uint8_t confirmedUid[uidLength] = {}; + + delay(20); + + iso15693Reader.reset(); + iso15693Reader.setupRF(); + + const ISO15693ErrorCode confirmationResult = + iso15693Reader.getInventory( + confirmedUid); + + if (confirmationResult != ISO15693_EC_OK) + { + return false; + } + + if (std::memcmp( + uid, + confirmedUid, + uidLength) != 0) + { + return false; + } + + rememberUid( + uid, + uidLength, + TagProtocol::Iso15693); + + Serial.print( + "ISO15693-tag fundet | UID: "); + + printUid( + uid, + uidLength, + true); + + Serial.println(); + + dumpIso15693Tag( + uid, + uidLength); + + delay(50); + + iso15693Reader.reset(); + iso15693Reader.setupRF(); + + return true; +} + +void Pn5180Reader::handleTagMissing() +{ + if (activeProtocol == TagProtocol::None) + { + return; + } + + ++missingScanCount; + + if (missingScanCount < + RequiredMissingScans) + { + return; + } + + Serial.println("NFC-tag fjernet."); + + clearLastUid(); +} + +bool Pn5180Reader::isSameUid( + const uint8_t* uid, + uint8_t uidLength) const +{ + if (uid == nullptr || + uidLength != lastUidLength) + { + return false; + } + + return std::memcmp( + lastUid, + uid, + uidLength) == 0; +} + +void Pn5180Reader::rememberUid( + const uint8_t* uid, + uint8_t uidLength, + TagProtocol protocol) +{ + std::memset( + lastUid, + 0, + sizeof(lastUid)); + + std::memcpy( + lastUid, + uid, + uidLength); + + lastUidLength = uidLength; + activeProtocol = protocol; + missingScanCount = 0; +} + +void Pn5180Reader::clearLastUid() +{ + std::memset( + lastUid, + 0, + sizeof(lastUid)); + + lastUidLength = 0; + activeProtocol = TagProtocol::None; + missingScanCount = 0; +} + +void Pn5180Reader::printUid( + const uint8_t* uid, + uint8_t uidLength, + bool reverseOrder) const +{ + Serial.print( + formatUid( + uid, + uidLength, + reverseOrder)); +} + +String Pn5180Reader::formatUid( + const uint8_t* uid, + uint8_t uidLength, + bool reverseOrder) const +{ + if (uid == nullptr || + uidLength == 0) + { + return String(); + } + + String result; + + result.reserve( + static_cast(uidLength) * 3 - 1); + + for (uint8_t index = 0; + index < uidLength; + ++index) + { + const uint8_t uidIndex = + reverseOrder + ? uidLength - 1 - index + : index; + + if (uid[uidIndex] < 0x10) + { + result += '0'; + } + + result += String( + uid[uidIndex], + HEX); + + if (index < uidLength - 1) + { + result += ':'; + } + } + + result.toUpperCase(); + + return result; +} + +void Pn5180Reader::dumpIso15693Tag( + uint8_t* uid, + uint8_t uidLength) +{ + constexpr uint8_t blockSize = 4; + constexpr uint8_t numberOfBlocks = 80; + + constexpr size_t memorySize = + static_cast(blockSize) * + numberOfBlocks; + + uint8_t memory[memorySize] = {}; + bool allBlocksRead = true; + + Serial.println(); + Serial.println( + "ISO15693 hukommelsesoplysninger:"); + + Serial.print("Blokstoerrelse: "); + Serial.print(blockSize); + Serial.println(" bytes"); + + Serial.print("Antal blokke: "); + Serial.println(numberOfBlocks); + + Serial.print("Samlet hukommelse: "); + Serial.print(memorySize); + Serial.println(" bytes"); + + Serial.println(); + + for (uint8_t blockNumber = 0; + blockNumber < numberOfBlocks; + ++blockNumber) + { + uint8_t* blockData = + memory + + static_cast(blockNumber) * + blockSize; + + const ISO15693ErrorCode readResult = + iso15693Reader.readSingleBlock( + uid, + blockNumber, + blockData, + blockSize); + + if (readResult != ISO15693_EC_OK) + { + allBlocksRead = false; + + Serial.print("Blok "); + Serial.print(blockNumber); + + Serial.print( + " kunne ikke laeses. Fejlkode: "); + + Serial.println( + static_cast(readResult)); + } + } + + Serial.println(); + + if (!allBlocksRead) + { + Serial.println( + "OpenPrintTag-parseren blev ikke startet, " + "fordi en eller flere blokke ikke kunne laeses."); + + return; + } + + Serial.println( + "Fortolker OpenPrintTag-data..."); + + OpenPrintTagData openPrintTagData; + + if (!openPrintTagParser.parse( + memory, + memorySize, + openPrintTagData)) + { + Serial.println( + "OpenPrintTag-data kunne ikke fortolkes."); + + return; + } + + openPrintTagData.tagUid = + formatUid( + uid, + uidLength, + true); + + FilamentGuardTagRead tagRead; + + tagRead.device = + deviceInfo; + + tagRead.tag = + openPrintTagData; + + printTagRead(tagRead); + printTagReadJson(tagRead); + sendTagRead(tagRead); +} + +void Pn5180Reader::printTagRead( + const FilamentGuardTagRead& tagRead) const +{ + Serial.println(); + Serial.println( + "FilamentGuard tagregistrering:"); + + Serial.print("Device ID: "); + Serial.println( + tagRead.device.deviceId); + + Serial.print("Hostname: "); + Serial.println( + tagRead.device.hostname); + + Serial.print("Tag UID: "); + Serial.println( + tagRead.tag.tagUid); + + Serial.print("Instance UUID: "); + Serial.println( + tagRead.tag.instanceUuid); + + Serial.print("Producent: "); + Serial.println( + tagRead.tag.brand); + + Serial.print("Materiale: "); + Serial.println( + tagRead.tag.materialName); + + Serial.print("Materialetype: "); + Serial.println( + tagRead.tag.materialType); + + Serial.print("Farve: "); + Serial.println( + tagRead.tag.colorHex); + + if (tagRead.tag.hasNominalWeight) + { + Serial.print("Nominel vaegt: "); + Serial.print( + tagRead.tag.nominalWeightGrams); + + Serial.println(" g"); + } + + if (tagRead.tag.hasActualWeight) + { + Serial.print("Faktisk startvaegt: "); + Serial.print( + tagRead.tag.actualWeightGrams); + + Serial.println(" g"); + } + + if (tagRead.tag.hasSpoolWeight) + { + Serial.print("Spolevaegt: "); + Serial.print( + tagRead.tag.spoolWeightGrams); + + Serial.println(" g"); + } + + Serial.print("Forbrugt vaegt: "); + Serial.print( + tagRead.tag.consumedWeightGrams); + + Serial.println(" g"); + + Serial.print("Resterende vaegt: "); + Serial.print( + tagRead.tag.remainingWeightGrams); + + Serial.println(" g"); + + Serial.println(); +} + +void Pn5180Reader::printTagReadJson( + const FilamentGuardTagRead& tagRead) const +{ + const String json = + tagReadSerializer.serialize( + tagRead); + + Serial.println( + "FilamentGuard JSON:"); + + Serial.println(json); + Serial.println(); +} + +void Pn5180Reader::sendTagRead( + const FilamentGuardTagRead& tagRead) const +{ + const String json = + tagReadSerializer.serialize( + tagRead); + + apiClient.sendTagRead(json); +} + +void Pn5180Reader::printBlock( + uint8_t blockNumber, + const uint8_t* data, + uint8_t blockSize) const +{ + Serial.print("Blok "); + + if (blockNumber < 10) + { + Serial.print('0'); + } + + Serial.print(blockNumber); + Serial.print(": "); + + for (uint8_t index = 0; + index < blockSize; + ++index) + { + if (data[index] < 0x10) + { + Serial.print('0'); + } + + Serial.print( + data[index], + HEX); + + if (index < blockSize - 1) + { + Serial.print(' '); + } + } + + Serial.print(" | "); + + for (uint8_t index = 0; + index < blockSize; + ++index) + { + const char character = + static_cast( + data[index]); + + if (character >= 32 && + character <= 126) + { + Serial.print(character); + } + else + { + Serial.print('.'); + } + } + + Serial.println(); +} \ No newline at end of file diff --git a/src/WifiSetup.cpp b/src/WifiSetup.cpp new file mode 100644 index 0000000..951ff1b --- /dev/null +++ b/src/WifiSetup.cpp @@ -0,0 +1,61 @@ +#include "WifiSetup.h" + +#include +#include +#include + +bool WifiSetup::connect(const char* hostname) +{ + WiFi.mode(WIFI_OFF); + delay(100); + + WiFi.setHostname(hostname); + WiFi.mode(WIFI_STA); + + WiFiManager wifiManager; + + wifiManager.setConfigPortalTimeout(180); + + Serial.print("Hostname: "); + Serial.println(hostname); + + Serial.println("Forbinder til Wi-Fi..."); + + const bool connected = + wifiManager.autoConnect( + "FilamentGuard-Setup" + ); + + if (!connected) + { + Serial.println( + "Wi-Fi-opsætning fejlede eller fik timeout." + ); + + return false; + } + + Serial.println("Wi-Fi forbundet."); + + Serial.print("Hostname: "); + Serial.println(WiFi.getHostname()); + + Serial.print("SSID: "); + Serial.println(WiFi.SSID()); + + Serial.print("IP-adresse: "); + Serial.println(WiFi.localIP()); + + return true; +} + +void WifiSetup::resetSettings() +{ + WiFiManager wifiManager; + + wifiManager.resetSettings(); + + Serial.println( + "Gemte Wi-Fi-oplysninger er slettet." + ); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..833e35a --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include "DeviceIdentity.h" +#include "DeviceSettings.h" +#include "FilamentGuardDeviceInfo.h" +#include "FirmwareUpdater.h" +#include "Pn5180Reader.h" +#include "WifiSetup.h" + +DeviceSettings deviceSettings; +DeviceIdentity deviceIdentity; +WifiSetup wifiSetup; +FirmwareUpdater firmwareUpdater; +Pn5180Reader pn5180Reader; + +FilamentGuardDeviceInfo deviceInfo; + +void setup() +{ + Serial.begin(115200); + delay(1000); + + Serial.println(); + Serial.println("FilamentGuard starter..."); + Serial.println("Firmwareversion: 1.0.1"); + Serial.println("OTA-testen lykkedes."); + + Serial.println(); + Serial.println("Initialiserer PN5180..."); + + pn5180Reader.begin(); + pn5180Reader.printVersionInformation(); + + const String hostname = + deviceSettings.getOrCreateHostname(); + + deviceInfo = + deviceIdentity.create(hostname); + + pn5180Reader.setDeviceInfo( + deviceInfo); + + Serial.println(); + Serial.println("Enhedsidentitet:"); + + Serial.print("Device ID: "); + Serial.println( + deviceInfo.deviceId); + + Serial.print("Hostname: "); + Serial.println( + deviceInfo.hostname); + + if (!wifiSetup.connect( + deviceInfo.hostname.c_str())) + { + Serial.println( + "Kunne ikke forbinde til Wi-Fi."); + + Serial.println( + "Genstarter om 5 sekunder..."); + + delay(5000); + ESP.restart(); + } + + Serial.print( + "Wi-Fi forbundet | Hostname: "); + + Serial.print( + WiFi.getHostname()); + + Serial.print(" | SSID: "); + Serial.print(WiFi.SSID()); + + Serial.print(" | IP: "); + Serial.print(WiFi.localIP()); + + Serial.print(" | Signal: "); + Serial.print(WiFi.RSSI()); + + Serial.println(" dBm"); + + if (!firmwareUpdater.begin()) + { + Serial.println( + "Firmware-opdatering kunne ikke startes."); + } + else + { + firmwareUpdater.checkForUpdate(); + } + + Serial.println(); + Serial.println( + "Klar til at laese ISO15693-tags."); +} + +void loop() +{ + pn5180Reader.checkForTag(); + delay(200); +} \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner 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 PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html