Kan læse og sende til API

Egentligt initial commit.
Kan læse tag og sende til API
This commit is contained in:
2026-07-15 16:32:34 +02:00
parent a0aa64f1a8
commit e2b9f0dc9e
30 changed files with 2991 additions and 34 deletions

39
.gitignore vendored
View File

@@ -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

10
.vscode/extensions.json vendored Normal file
View File

@@ -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"
]
}

31
data/root_ca.pem Normal file
View File

@@ -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-----

15
include/DeviceIdentity.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <Arduino.h>
#include "FilamentGuardDeviceInfo.h"
class DeviceIdentity
{
public:
FilamentGuardDeviceInfo create(
const String& hostname) const;
private:
String createDeviceId() const;
};

18
include/DeviceSettings.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <Arduino.h>
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);
};

View File

@@ -0,0 +1,20 @@
#pragma once
#include <Arduino.h>
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/";
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
struct FilamentGuardDeviceInfo
{
String deviceId;
String hostname;
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include <Arduino.h>
enum class FilamentGuardTagEventType
{
TagPlaced,
TagRemoved
};
struct FilamentGuardTagEvent
{
String deviceId;
String hostname;
FilamentGuardTagEventType eventType;
String tagUid;
String instanceUuid;
String tagJson;
unsigned long uptimeMilliseconds = 0;
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include <Arduino.h>
#include "FilamentGuardTagEvent.h"
class FilamentGuardTagEventSerializer
{
public:
static String serialize(
const FilamentGuardTagEvent& tagEvent);
private:
static const char* eventTypeToString(
FilamentGuardTagEventType eventType);
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
#include "FilamentGuardDeviceInfo.h"
#include "OpenPrintTagData.h"
struct FilamentGuardTagRead
{
FilamentGuardDeviceInfo device;
OpenPrintTagData tag;
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include <Arduino.h>
#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;
};

View File

@@ -0,0 +1,8 @@
#pragma once
class FirmwareUpdater
{
public:
bool begin();
void checkForUpdate();
};

View File

@@ -0,0 +1,25 @@
#pragma once
#include <Arduino.h>
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;
};

View File

@@ -0,0 +1,73 @@
#pragma once
#include <Arduino.h>
#include <cbor.h>
#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;
};

99
include/Pn5180Reader.h Normal file
View File

@@ -0,0 +1,99 @@
#pragma once
#include <Arduino.h>
#include <PN5180ISO14443.h>
#include <PN5180ISO15693.h>
#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;
};

37
include/README Normal file
View File

@@ -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

8
include/WifiSetup.h Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
class WifiSetup
{
public:
bool connect(const char* hostname);
void resetSettings();
};

46
lib/README Normal file
View File

@@ -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 <Foo.h>
#include <Bar.h>
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

23
platformio.ini Normal file
View File

@@ -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

36
src/DeviceIdentity.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include "DeviceIdentity.h"
#include <Esp.h>
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<uint16_t>(
chipId >> 32),
static_cast<uint32_t>(
chipId));
return String(buffer);
}

159
src/DeviceSettings.cpp Normal file
View File

@@ -0,0 +1,159 @@
#include "DeviceSettings.h"
#include <Preferences.h>
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<unsigned long long>(
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;
}

View File

@@ -0,0 +1,121 @@
#include "FilamentGuardApiClient.h"
#include <HTTPClient.h>
#include <WiFi.h>
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");
}

View File

@@ -0,0 +1,55 @@
#include "FilamentGuardTagEventSerializer.h"
#include <ArduinoJson.h>
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";
}
}

View File

@@ -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<uint8_t>(character) < 0x20)
{
char buffer[7] = {};
std::snprintf(
buffer,
sizeof(buffer),
"\\u%04X",
static_cast<uint8_t>(
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 += ',';
}
}

111
src/FirmwareUpdater.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include "FirmwareUpdater.h"
#include <Arduino.h>
#include <WiFi.h>
#include <SPIFFS.h>
#include <esp32FOTA.hpp>
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<unsigned int>(
(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();
}

951
src/OpenPrintTagParser.cpp Normal file
View File

@@ -0,0 +1,951 @@
#include "OpenPrintTagParser.h"
#include <cstring>
#include <new>
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<size_t>(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<size_t>(
memory[offset]) << 8) |
static_cast<size_t>(
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<size_t>(
ndefMessage[offset]) << 24) |
(static_cast<size_t>(
ndefMessage[offset + 1]) << 16) |
(static_cast<size_t>(
ndefMessage[offset + 2]) << 8) |
static_cast<size_t>(
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<size_t>(
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<uint16_t>(value);
data.hasNominalWeight = true;
break;
}
case FieldActualWeight:
{
uint64_t value = 0;
if (!readUnsignedInteger(
&iterator,
value))
{
return false;
}
data.actualWeightGrams =
static_cast<uint16_t>(value);
data.hasActualWeight = true;
break;
}
case FieldSpoolWeight:
{
uint64_t value = 0;
if (!readUnsignedInteger(
&iterator,
value))
{
return false;
}
data.spoolWeightGrams =
static_cast<uint16_t>(value);
data.hasSpoolWeight = true;
break;
}
default:
if (!skipValue(&iterator))
{
Serial.print(
"CBOR-parser: Felt kunne ikke springes over: ");
Serial.println(
static_cast<unsigned long>(
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;
}

634
src/Pn5180Reader.cpp Normal file
View File

@@ -0,0 +1,634 @@
#include "Pn5180Reader.h"
#include <cstring>
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<size_t>(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<size_t>(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<size_t>(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<int>(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<char>(
data[index]);
if (character >= 32 &&
character <= 126)
{
Serial.print(character);
}
else
{
Serial.print('.');
}
}
Serial.println();
}

61
src/WifiSetup.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "WifiSetup.h"
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiManager.h>
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."
);
}

104
src/main.cpp Normal file
View File

@@ -0,0 +1,104 @@
#include <Arduino.h>
#include <WiFi.h>
#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);
}

11
test/README Normal file
View File

@@ -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