Version 1 færdig

Læser og kalder API som det skal.
OTA virker
Opsætning af wifi via ESP32's eget hotspot virker

Mangler at få lavet skrivning, men det bliver v2.
This commit is contained in:
2026-07-27 14:33:06 +02:00
parent c0c59e1ad7
commit d757e13ce6
14 changed files with 1426 additions and 310 deletions

View File

@@ -1,2 +1,34 @@
# filamentguard_esp32
## Build-miljoeer
`dev` er standard og bruger:
```text
http://192.168.1.6:5011
```
Byg og upload dev-firmware:
```powershell
pio run -e dev -t upload
```
`prod` bruges til OTA-firmware og bruger:
```text
http://192.168.3.2:9090
```
Byg prod-firmware til OTA:
```powershell
pio run -e prod
```
OTA-binaren ligger derefter i:
```text
.pio/build/prod/firmware.bin
```

View File

@@ -2,6 +2,18 @@
#include <Arduino.h>
#ifndef FILAMENT_GUARD_API_ENV
#define FILAMENT_GUARD_API_ENV 0
#endif
#if FILAMENT_GUARD_API_ENV == 0
#define FILAMENT_GUARD_API_BASE_URL "http://192.168.1.6:5011"
#elif FILAMENT_GUARD_API_ENV == 1
#define FILAMENT_GUARD_API_BASE_URL "http://192.168.3.2:9090"
#else
#error "FILAMENT_GUARD_API_ENV skal vaere 0 (dev) eller 1 (prod)."
#endif
class FilamentGuardApiClient
{
public:
@@ -20,11 +32,16 @@ public:
private:
static constexpr const char* TagReadEndpointUrl =
"http://192.168.1.6:5011/api/Tag/detected";
FILAMENT_GUARD_API_BASE_URL
"/api/Tag/detected";
static constexpr const char* TagEventEndpointUrl =
"http://192.168.1.6:5011/api/Tag/removed";
FILAMENT_GUARD_API_BASE_URL
"/api/Tag/removed";
static constexpr const char* LogEndpointUrl =
"http://192.168.1.6:5011/api/Log/create";
FILAMENT_GUARD_API_BASE_URL
"/api/Log/create";
};
#undef FILAMENT_GUARD_API_BASE_URL

View File

@@ -7,6 +7,7 @@
struct FilamentGuardTagRead
{
String readerId;
FilamentGuardDeviceInfo device;
OpenPrintTagData tag;
};

View File

@@ -1,7 +1,6 @@
#pragma once
#include <Arduino.h>
#include <PN5180ISO14443.h>
#include <PN5180ISO15693.h>
#include "FilamentGuardApiClient.h"
@@ -12,11 +11,18 @@
#include "OpenPrintTagData.h"
#include "OpenPrintTagParser.h"
#ifndef FILAMENT_GUARD_NFC_DIAGNOSTICS
#define FILAMENT_GUARD_NFC_DIAGNOSTICS 0
#endif
class Pn5180Reader
{
public:
explicit Pn5180Reader(
FilamentGuardLogger& logger);
Pn5180Reader(
FilamentGuardLogger& logger,
uint8_t nssPin,
uint8_t busyPin,
uint8_t resetPin);
void begin();
@@ -24,24 +30,37 @@ public:
const FilamentGuardDeviceInfo& deviceInfo);
void printVersionInformation();
void checkForTag();
const String& getReaderId() const;
bool scanForTag(
uint8_t* uid,
uint16_t& signalStrength);
bool registerTag(
const uint8_t* uid,
uint8_t uidLength);
bool isActiveTag(
const uint8_t* uid,
uint8_t uidLength) const;
bool hasActiveTag() const;
void markTagPresent();
bool markTagMissing();
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;
@@ -49,6 +68,7 @@ private:
FilamentGuardLogger& logger;
FilamentGuardDeviceInfo deviceInfo;
String readerId;
uint8_t lastUid[10] = {};
uint8_t lastUidLength = 0;
@@ -56,11 +76,22 @@ private:
TagProtocol activeProtocol = TagProtocol::None;
uint8_t missingScanCount = 0;
bool checkIso14443Tag();
bool checkIso15693Tag();
#if FILAMENT_GUARD_NFC_DIAGNOSTICS
unsigned long nextDiagnosticAt = 0;
#endif
void handleTagMissing();
bool performInventory(
uint8_t* uid);
bool readIso15693BlockRange(
const uint8_t* uid,
uint8_t firstBlock,
uint8_t blockCount,
uint8_t* blockData,
uint8_t blockSize);
bool isSameUid(
const uint8_t* uid,
uint8_t uidLength) const;
@@ -77,8 +108,8 @@ private:
uint8_t uidLength,
bool reverseOrder) const;
void dumpIso15693Tag(
uint8_t* uid,
bool dumpIso15693Tag(
const uint8_t* uid,
uint8_t uidLength);
void logTagRead(

Binary file not shown.

Binary file not shown.

View File

@@ -8,7 +8,10 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
[platformio]
default_envs = dev
[env]
platform = espressif32
board = esp32dev
framework = arduino
@@ -19,5 +22,20 @@ board_build.filesystem = spiffs
lib_deps =
tzapu/WiFiManager
chrisjoyce911/esp32FOTA
https://github.com/ATrappmann/PN5180-Library.git
https://github.com/wilson-elechouse/PN5180_ELECHOUSE.git#c413f0a91e8efa317256afb1e3bba074d3f122dc
soburi/TinyCBOR@^0.5.3-arduino2
[env:dev]
build_src_filter =
+<*>
-<pn5180_probe.cpp>
build_flags =
-D FILAMENT_GUARD_API_ENV=0
[env:prod]
build_src_filter =
+<*>
-<pn5180_probe.cpp>
build_flags =
-D FILAMENT_GUARD_API_ENV=1

View File

@@ -2,52 +2,55 @@
#include <HTTPClient.h>
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <new>
namespace
{
constexpr uint16_t HttpTimeoutMilliseconds =
15000;
5000;
constexpr UBaseType_t ApiQueueLength =
24;
constexpr uint32_t ApiWorkerStackSize =
4096;
struct ApiRequest
{
String endpointUrl;
String json;
String description;
bool reportSuccess = true;
};
QueueHandle_t apiQueue = nullptr;
TaskHandle_t apiWorkerHandle = nullptr;
bool postJson(
const char* endpointUrl,
const String& json,
const char* description,
int* returnedResponseCode = nullptr,
String* returnedResponseBody = nullptr)
const ApiRequest& request)
{
if (returnedResponseCode != nullptr)
{
*returnedResponseCode = 0;
}
if (returnedResponseBody != nullptr)
{
returnedResponseBody->clear();
}
if (WiFi.status() != WL_CONNECTED)
{
Serial.print(description);
Serial.print(
request.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))
if (!httpClient.begin(
request.endpointUrl))
{
Serial.print(description);
Serial.print(
request.description);
Serial.println(
" kunne ikke initialiseres.");
@@ -62,12 +65,8 @@ namespace
HttpTimeoutMilliseconds);
const int responseCode =
httpClient.POST(json);
if (returnedResponseCode != nullptr)
{
*returnedResponseCode = responseCode;
}
httpClient.POST(
request.json);
if (responseCode <= 0)
{
@@ -75,16 +74,14 @@ namespace
httpClient.errorToString(
responseCode);
if (returnedResponseBody != nullptr)
{
*returnedResponseBody = errorMessage;
}
Serial.print(
request.description);
Serial.print(description);
Serial.print(
" fejlede. HTTPClient-fejl: ");
Serial.println(errorMessage);
Serial.println(
errorMessage);
httpClient.end();
@@ -94,24 +91,27 @@ namespace
const String responseBody =
httpClient.getString();
if (returnedResponseBody != nullptr)
{
*returnedResponseBody = responseBody;
}
const bool successful =
responseCode >= 200 &&
responseCode < 300;
if (!successful)
if (!successful ||
request.reportSuccess)
{
Serial.print(description);
Serial.print(
" blev afvist. HTTP-status: ");
request.description);
Serial.println(responseCode);
Serial.print(
successful
? " sendt. HTTP-status: "
: " blev afvist. HTTP-status: ");
if (!responseBody.isEmpty())
Serial.println(
responseCode);
}
if (!successful &&
!responseBody.isEmpty())
{
Serial.println(
"Svar fra API:");
@@ -119,12 +119,144 @@ namespace
Serial.println(
responseBody);
}
}
httpClient.end();
return successful;
}
void apiWorkerTask(
void*)
{
while (true)
{
ApiRequest* request = nullptr;
if (xQueueReceive(
apiQueue,
&request,
portMAX_DELAY) != pdTRUE)
{
continue;
}
if (request == nullptr)
{
continue;
}
postJson(
*request);
delete request;
}
}
bool ensureApiWorkerStarted()
{
if (apiQueue != nullptr)
{
return true;
}
apiQueue =
xQueueCreate(
ApiQueueLength,
sizeof(ApiRequest*));
if (apiQueue == nullptr)
{
Serial.println(
"API-koen kunne ikke oprettes.");
return false;
}
const BaseType_t taskCreated =
xTaskCreate(
apiWorkerTask,
"filamentguard-api",
ApiWorkerStackSize,
nullptr,
1,
&apiWorkerHandle);
if (taskCreated != pdPASS)
{
Serial.println(
"API-worker kunne ikke startes.");
vQueueDelete(
apiQueue);
apiQueue = nullptr;
return false;
}
return true;
}
bool enqueueJson(
const char* endpointUrl,
const String& json,
const char* description,
const bool reportSuccess = true)
{
if (json.isEmpty())
{
Serial.print(
description);
Serial.println(
" blev ikke lagt i ko: JSON-data er tomme.");
return false;
}
if (!ensureApiWorkerStarted())
{
return false;
}
ApiRequest* request =
new (std::nothrow) ApiRequest;
if (request == nullptr)
{
Serial.println(
"Der er ikke hukommelse til en API-anmodning.");
return false;
}
request->endpointUrl =
endpointUrl;
request->json =
json;
request->description =
description;
request->reportSuccess =
reportSuccess;
if (xQueueSend(
apiQueue,
&request,
0) != pdTRUE)
{
Serial.println(
"API-koen er fuld.");
delete request;
return false;
}
return true;
}
}
bool FilamentGuardApiClient::sendTagRead(
@@ -132,12 +264,23 @@ bool FilamentGuardApiClient::sendTagRead(
int& responseCode,
String& responseBody) const
{
return postJson(
const bool queued =
enqueueJson(
TagReadEndpointUrl,
json,
"tagregistrering",
&responseCode,
&responseBody);
"tagregistrering");
responseCode =
queued
? 202
: 0;
responseBody =
queued
? "{\"queued\":true}"
: "{\"queued\":false}";
return queued;
}
bool FilamentGuardApiClient::sendTagEvent(
@@ -145,19 +288,31 @@ bool FilamentGuardApiClient::sendTagEvent(
int& responseCode,
String& responseBody) const
{
return postJson(
const bool queued =
enqueueJson(
TagEventEndpointUrl,
json,
"tagfjernelse",
&responseCode,
&responseBody);
"tagfjernelse");
responseCode =
queued
? 202
: 0;
responseBody =
queued
? "{\"queued\":true}"
: "{\"queued\":false}";
return queued;
}
bool FilamentGuardApiClient::sendLogEntry(
const String& json) const
{
return postJson(
return enqueueJson(
LogEndpointUrl,
json,
"log");
"log",
false);
}

View File

@@ -9,6 +9,16 @@ String FilamentGuardTagReadSerializer::serialize(
json += '{';
json += "\"reader\":{";
appendStringProperty(
json,
"id",
tagRead.readerId,
false);
json += "},";
json += "\"device\":{";
appendStringProperty(

View File

@@ -11,7 +11,7 @@ namespace
"filamentguard";
constexpr const char* FirmwareVersion =
"1.0.1";
"1.0.2";
constexpr const char* ManifestUrl =
"https://ota-filamentguard.maximuss.dk/firmware/manifest.json";

View File

@@ -2,16 +2,42 @@
#include <cstring>
namespace
{
// Antal gange getInventory forsoeges pr. scan, foer vi giver op.
// ISO15693 er slot-baseret og et enkelt scan kan sagtens misse
// et svag-koblet tag i sit slot. Retries er standardpraksis.
constexpr uint8_t MaxInventoryAttempts = 3;
constexpr uint16_t InventoryRetryDelayMilliseconds = 10;
// Tid RF-feltet skal vaere aktivt foer inventory sendes, saa
// passive tags naar at faa opladet deres kondensator.
constexpr uint16_t RfSettleDelayMilliseconds = 20;
bool isValidIso15693Uid(
const uint8_t* uid)
{
if (uid == nullptr)
{
return false;
}
// ISO15693-UID'er leveres LSB foerst af PN5180. Den sidste byte
// skal derfor vaere ISO/IEC 15963-markoeren 0xE0. Kontrollen
// afviser blandt andet bibliotekets falske nul-UID.
return uid[7] == 0xE0;
}
}
Pn5180Reader::Pn5180Reader(
FilamentGuardLogger& newLogger)
FilamentGuardLogger& newLogger,
const uint8_t nssPin,
const uint8_t busyPin,
const uint8_t resetPin)
: iso15693Reader(
NssPin,
BusyPin,
ResetPin),
iso14443Reader(
NssPin,
BusyPin,
ResetPin),
nssPin,
busyPin,
resetPin),
logger(newLogger)
{
}
@@ -19,10 +45,66 @@ Pn5180Reader::Pn5180Reader(
void Pn5180Reader::begin()
{
iso15693Reader.begin();
iso14443Reader.begin();
iso15693Reader.reset();
iso15693Reader.setupRF();
uint8_t dieIdentifier[16] = {};
if (!iso15693Reader.readEEprom(
DIE_IDENTIFIER,
dieIdentifier,
sizeof(dieIdentifier)))
{
logger.error(
"NfcReader",
"PN5180 reader-ID kunne ikke laeses.");
return;
}
bool allZero = true;
bool allOnes = true;
for (const uint8_t value :
dieIdentifier)
{
allZero =
allZero &&
value == 0x00;
allOnes =
allOnes &&
value == 0xFF;
}
if (allZero ||
allOnes)
{
logger.error(
"NfcReader",
"PN5180 returnerede et ugyldigt reader-ID.");
return;
}
readerId.reserve(
sizeof(dieIdentifier) *
2);
for (const uint8_t value :
dieIdentifier)
{
if (value < 0x10)
{
readerId += '0';
}
readerId += String(
value,
HEX);
}
readerId.toUpperCase();
}
void Pn5180Reader::setDeviceInfo(
@@ -68,7 +150,9 @@ void Pn5180Reader::printVersionInformation()
}
const String versionData =
String("{\"productVersion\":\"") +
String("{\"readerId\":\"") +
readerId +
"\",\"productVersion\":\"" +
productVersion[1] +
"." +
productVersion[0] +
@@ -88,164 +172,149 @@ void Pn5180Reader::printVersionInformation()
versionData);
}
void Pn5180Reader::checkForTag()
const String& Pn5180Reader::getReaderId() const
{
bool tagFound = false;
if (activeProtocol ==
TagProtocol::Iso14443A)
{
tagFound =
checkIso14443Tag();
}
else if (activeProtocol ==
TagProtocol::Iso15693)
{
tagFound =
checkIso15693Tag();
}
else
{
tagFound =
checkIso14443Tag();
if (!tagFound)
{
tagFound =
checkIso15693Tag();
}
return readerId;
}
if (tagFound)
bool Pn5180Reader::scanForTag(
uint8_t* uid,
uint16_t& signalStrength)
{
missingScanCount = 0;
return;
}
signalStrength = 0;
handleTagMissing();
}
bool Pn5180Reader::checkIso14443Tag()
{
uint8_t uid[10] = {};
iso14443Reader.reset();
iso14443Reader.setupRF();
if (!iso14443Reader.isCardPresent())
if (uid == nullptr)
{
return false;
}
const uint8_t uidLength =
iso14443Reader.readCardSerial(
uid);
std::memset(
uid,
0,
8);
if (uidLength == 0)
iso15693Reader.reset();
const bool rfReady =
iso15693Reader.setupRF();
if (rfReady)
{
// Giv det passive tag tid til at blive forsynet efter skiftet
// mellem de to antenners RF-felter.
delay(RfSettleDelayMilliseconds);
}
const ISO15693ErrorCode result =
rfReady
? (performInventory(uid)
? ISO15693_EC_OK
: EC_NO_CARD)
: ISO15693_EC_UNKNOWN_ERROR;
#if FILAMENT_GUARD_NFC_DIAGNOSTICS
if (static_cast<long>(
millis() -
nextDiagnosticAt) >= 0)
{
nextDiagnosticAt =
millis() +
2000;
Serial.print(
"[Debug] [NfcReader] readerId=");
Serial.print(
readerId);
Serial.print(
", rfReady=");
Serial.print(
rfReady
? "true"
: "false");
Serial.print(
", inventoryResult=");
Serial.print(
static_cast<int>(
result));
Serial.print(
", rawUid=");
for (uint8_t index = 0;
index < 8;
++index)
{
if (uid[7 - index] < 0x10)
{
Serial.print('0');
}
Serial.print(
uid[7 - index],
HEX);
if (index < 7)
{
Serial.print(':');
}
}
Serial.println();
}
#endif
if (result != ISO15693_EC_OK ||
!isValidIso15693Uid(uid))
{
iso15693Reader.setRF_off();
return false;
}
if (activeProtocol ==
TagProtocol::Iso14443A)
uint32_t rfStatus = 0;
if (iso15693Reader.readRegister(
RF_STATUS,
&rfStatus))
{
return isSameUid(
uid,
uidLength);
signalStrength =
static_cast<uint16_t>(
rfStatus &
0x03FF);
}
if (activeProtocol !=
TagProtocol::None)
{
return false;
}
rememberUid(
uid,
uidLength,
TagProtocol::Iso14443A);
const String tagData =
String("{\"protocol\":\"ISO14443A\",") +
"\"tagUid\":\"" +
formatUid(
uid,
uidLength,
false) +
"\"}";
logger.info(
"NfcReader",
"ISO14443A-tag fundet.",
tagData);
iso15693Reader.setRF_off();
return true;
}
bool Pn5180Reader::checkIso15693Tag()
bool Pn5180Reader::registerTag(
const uint8_t* uid,
const uint8_t uidLength)
{
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 !=
if (uid == nullptr ||
uidLength != 8 ||
!isValidIso15693Uid(uid) ||
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)
if (!iso15693Reader.setupRF())
{
iso15693Reader.setRF_off();
return false;
}
if (std::memcmp(
uid,
confirmedUid,
uidLength) != 0)
{
return false;
}
rememberUid(
uid,
uidLength,
TagProtocol::Iso15693);
delay(RfSettleDelayMilliseconds);
const String tagData =
String("{\"protocol\":\"ISO15693\",") +
String("{\"readerId\":\"") +
readerId +
"\",\"protocol\":\"ISO15693\"," +
"\"tagUid\":\"" +
formatUid(
uid,
@@ -258,18 +327,60 @@ bool Pn5180Reader::checkIso15693Tag()
"ISO15693-tag fundet.",
tagData);
dumpIso15693Tag(
if (!dumpIso15693Tag(
uid,
uidLength);
uidLength))
{
iso15693Reader.setRF_off();
delay(50);
return false;
}
iso15693Reader.reset();
iso15693Reader.setupRF();
rememberUid(
uid,
uidLength,
TagProtocol::Iso15693);
iso15693Reader.setRF_off();
return true;
}
bool Pn5180Reader::isActiveTag(
const uint8_t* uid,
const uint8_t uidLength) const
{
return activeProtocol ==
TagProtocol::Iso15693 &&
isSameUid(
uid,
uidLength);
}
bool Pn5180Reader::hasActiveTag() const
{
return activeProtocol !=
TagProtocol::None;
}
void Pn5180Reader::markTagPresent()
{
missingScanCount = 0;
}
bool Pn5180Reader::markTagMissing()
{
const bool wasActive =
activeProtocol !=
TagProtocol::None;
handleTagMissing();
return wasActive &&
activeProtocol ==
TagProtocol::None;
}
void Pn5180Reader::handleTagMissing()
{
if (activeProtocol ==
@@ -286,18 +397,16 @@ void Pn5180Reader::handleTagMissing()
return;
}
const bool reverseOrder =
activeProtocol ==
TagProtocol::Iso15693;
const String tagUid =
formatUid(
lastUid,
lastUidLength,
reverseOrder);
true);
const String tagData =
String("{\"tagUid\":\"") +
String("{\"readerId\":\"") +
readerId +
"\",\"tagUid\":\"" +
tagUid +
"\"}";
@@ -307,7 +416,9 @@ void Pn5180Reader::handleTagMissing()
tagData);
const String eventJson =
String("{\"device\":{") +
String("{\"reader\":{\"id\":\"") +
readerId +
"\"},\"device\":{" +
"\"deviceId\":\"" +
deviceInfo.deviceId +
"\",\"hostname\":\"" +
@@ -326,28 +437,274 @@ void Pn5180Reader::handleTagMissing()
responseCode,
responseBody);
const String responseMessage =
String("Tag removed API-svar. HTTP-status: ") +
responseCode;
if (successful)
{
logger.info(
"Api",
responseMessage,
"Tagfjernelse lagt i API-ko.",
responseBody);
}
else
{
logger.error(
"Api",
responseMessage,
"Tagfjernelse kunne ikke laegges i API-ko.",
responseBody);
}
clearLastUid();
}
bool Pn5180Reader::performInventory(
uint8_t* uid)
{
if (uid == nullptr)
{
return false;
}
ISO15693ErrorCode lastResult =
ISO15693_EC_UNKNOWN_ERROR;
for (uint8_t attempt = 0;
attempt < MaxInventoryAttempts;
++attempt)
{
std::memset(
uid,
0,
8);
lastResult =
iso15693Reader.getInventory(
uid);
if (lastResult == ISO15693_EC_OK &&
isValidIso15693Uid(uid))
{
#if FILAMENT_GUARD_NFC_DIAGNOSTICS
if (attempt > 0)
{
Serial.print(
"[Debug] [NfcReader] readerId=");
Serial.print(
readerId);
Serial.print(
", inventory succeeded on attempt ");
Serial.println(
attempt + 1);
}
#endif
return true;
}
if (attempt + 1 < MaxInventoryAttempts)
{
delay(InventoryRetryDelayMilliseconds);
}
}
#if FILAMENT_GUARD_NFC_DIAGNOSTICS
if (lastResult != EC_NO_CARD)
{
Serial.print(
"[Debug] [NfcReader] readerId=");
Serial.print(
readerId);
Serial.print(
", inventory failed after ");
Serial.print(
MaxInventoryAttempts);
Serial.print(
" attempts, lastResult=");
Serial.println(
static_cast<int>(lastResult));
}
#endif
return false;
}
bool Pn5180Reader::readIso15693BlockRange(
const uint8_t* uid,
const uint8_t firstBlock,
const uint8_t blockCount,
uint8_t* blockData,
const uint8_t blockSize)
{
constexpr uint8_t maximumBlocksPerRequest = 8;
constexpr uint16_t maximumResponseLength =
1 +
maximumBlocksPerRequest *
4 +
2;
constexpr uint32_t receiveErrorMask =
(1UL << 16) |
(1UL << 17) |
(1UL << 18);
if (uid == nullptr ||
blockData == nullptr ||
blockCount == 0 ||
blockCount >
maximumBlocksPerRequest ||
blockSize != 4)
{
return false;
}
uint8_t command[12] =
{
0x22,
static_cast<uint8_t>(
blockCount == 1
? 0x20
: 0x23),
0, 0, 0, 0, 0, 0, 0, 0,
firstBlock,
static_cast<uint8_t>(
blockCount - 1)
};
std::memcpy(
command + 2,
uid,
8);
const uint8_t commandLength =
blockCount == 1
? 11
: 12;
if (!iso15693Reader.clearIRQStatus(
0xFFFFFFFF) ||
!iso15693Reader.sendData(
command,
commandLength))
{
return false;
}
const unsigned long startedWaiting =
millis();
uint32_t irqStatus = 0;
do
{
irqStatus = 0;
if (!iso15693Reader.readRegister(
IRQ_STATUS,
&irqStatus))
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
if ((irqStatus &
GENERAL_ERROR_IRQ_STAT) != 0)
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
if ((irqStatus &
RX_IRQ_STAT) != 0)
{
break;
}
delay(1);
}
while (millis() -
startedWaiting <
150);
if ((irqStatus &
RX_IRQ_STAT) == 0 ||
(irqStatus &
RX_SOF_DET_IRQ_STAT) == 0)
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
uint32_t rxStatus = 0;
if (!iso15693Reader.readRegister(
RX_STATUS,
&rxStatus) ||
(rxStatus &
receiveErrorMask) != 0)
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
const uint16_t responseLength =
static_cast<uint16_t>(
rxStatus &
0x01FF);
const uint16_t dataLength =
static_cast<uint16_t>(
blockCount) *
blockSize;
const uint16_t minimumResponseLength =
1 +
dataLength;
if (responseLength <
minimumResponseLength ||
responseLength >
minimumResponseLength +
2 ||
responseLength >
maximumResponseLength)
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
uint8_t response[
maximumResponseLength] = {};
if (!iso15693Reader.readData(
responseLength,
response))
{
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
return false;
}
iso15693Reader.clearIRQStatus(
0xFFFFFFFF);
if ((response[0] &
0x01) != 0)
{
return false;
}
std::memcpy(
blockData,
response + 1,
dataLength);
return true;
}
bool Pn5180Reader::isSameUid(
const uint8_t* uid,
const uint8_t uidLength) const
@@ -461,8 +818,8 @@ String Pn5180Reader::formatUid(
return result;
}
void Pn5180Reader::dumpIso15693Tag(
uint8_t* uid,
bool Pn5180Reader::dumpIso15693Tag(
const uint8_t* uid,
const uint8_t uidLength)
{
constexpr uint8_t blockSize = 4;
@@ -474,8 +831,6 @@ void Pn5180Reader::dumpIso15693Tag(
numberOfBlocks;
uint8_t memory[memorySize] = {};
bool allBlocksRead = true;
const String memoryData =
String("{\"blockSizeBytes\":") +
blockSize +
@@ -490,50 +845,63 @@ void Pn5180Reader::dumpIso15693Tag(
"Laeser ISO15693-hukommelse.",
memoryData);
for (uint8_t blockNumber = 0;
blockNumber < numberOfBlocks;
++blockNumber)
constexpr uint8_t blocksPerRequest = 8;
for (uint8_t firstBlock = 0;
firstBlock < numberOfBlocks;
firstBlock += blocksPerRequest)
{
uint8_t* blockData =
uint8_t* destination =
memory +
static_cast<size_t>(
firstBlock) *
blockSize;
if (readIso15693BlockRange(
uid,
firstBlock,
blocksPerRequest,
destination,
blockSize))
{
continue;
}
for (uint8_t offset = 0;
offset < blocksPerRequest;
++offset)
{
const uint8_t blockNumber =
firstBlock +
offset;
if (!readIso15693BlockRange(
uid,
blockNumber,
1,
memory +
static_cast<size_t>(
blockNumber) *
blockSize;
const ISO15693ErrorCode readResult =
iso15693Reader.readSingleBlock(
uid,
blockNumber,
blockData,
blockSize);
if (readResult !=
ISO15693_EC_OK)
blockSize,
blockSize))
{
allBlocksRead = false;
const String errorData =
String("{\"blockNumber\":") +
blockNumber +
",\"errorCode\":" +
static_cast<int>(
readResult) +
"}";
logger.error(
"NfcReader",
"ISO15693-blok kunne ikke laeses.",
errorData);
}
}
if (!allBlocksRead)
{
logger.error(
"NfcReader",
"OpenPrintTag-parseren blev ikke startet, fordi en eller flere blokke ikke kunne laeses.");
"OpenPrintTag-parseren blev ikke startet, fordi hukommelsen ikke kunne laeses komplet.");
return;
return false;
}
}
}
logger.info(
@@ -551,7 +919,7 @@ void Pn5180Reader::dumpIso15693Tag(
"NfcReader",
"OpenPrintTag-data kunne ikke fortolkes.");
return;
return false;
}
openPrintTagData.tagUid =
@@ -562,6 +930,9 @@ void Pn5180Reader::dumpIso15693Tag(
FilamentGuardTagRead tagRead;
tagRead.readerId =
readerId;
tagRead.device =
deviceInfo;
@@ -573,6 +944,8 @@ void Pn5180Reader::dumpIso15693Tag(
sendTagRead(
tagRead);
return true;
}
void Pn5180Reader::logTagRead(
@@ -604,22 +977,18 @@ void Pn5180Reader::sendTagRead(
responseCode,
responseBody);
const String message =
String("Tag detected API-svar. HTTP-status: ") +
responseCode;
if (successful)
{
logger.info(
"Api",
message,
"Tagregistrering lagt i API-ko.",
responseBody);
}
else
{
logger.error(
"Api",
message,
"Tagregistrering kunne ikke laegges i API-ko.",
responseBody);
}
}

View File

@@ -11,7 +11,57 @@
namespace
{
constexpr const char* FirmwareVersion =
"1.0.1";
"1.0.2";
constexpr uint8_t UidLength = 8;
struct ReaderPins
{
uint8_t nssPin;
uint8_t busyPin;
uint8_t resetPin;
};
// Pin-tabel for alle PN5180-laesere. Hver raekke er \u00e9n laeser
// (NSS, BUSY, RESET). For at tilfoeje/fjerne en laeser: aendr denne
// liste OG readers[]-instansen nedenfor tilsvarende.
// Faelles SPI-bus: SCK=18, MISO=19, MOSI=23.
constexpr ReaderPins readerPins[] =
{
{ 5, 16, 17 }, // Reader 1
{ 27, 26, 25 }, // Reader 2
// Forbered til indx-8 (4 laesere) - aktiver n\u00e5r hardwaren er klar:
// { 32, 33, 4 }, // Reader 3
// { 14, 13, 21 }, // Reader 4
};
constexpr size_t ReaderCount =
sizeof(readerPins) /
sizeof(readerPins[0]);
void prepareReaderPins(
const ReaderPins& pins)
{
pinMode(
pins.nssPin,
OUTPUT);
digitalWrite(
pins.nssPin,
HIGH);
pinMode(
pins.busyPin,
INPUT);
pinMode(
pins.resetPin,
OUTPUT);
digitalWrite(
pins.resetPin,
HIGH);
}
}
FilamentGuardLogger logger;
@@ -22,7 +72,15 @@ DeviceIdentity deviceIdentity;
WifiSetup wifiSetup(logger);
FirmwareUpdater firmwareUpdater(logger);
Pn5180Reader pn5180Reader(logger);
// PN5180-instanser. Antal og indeks skal matche readerPins[] ovenfor.
// Tilfoej/fjern raekker parallelt med readerPins for at aendre antal laesere.
Pn5180Reader readers[ReaderCount] =
{
{ logger, readerPins[0].nssPin, readerPins[0].busyPin, readerPins[0].resetPin },
{ logger, readerPins[1].nssPin, readerPins[1].busyPin, readerPins[1].resetPin },
// { logger, readerPins[2].nssPin, readerPins[2].busyPin, readerPins[2].resetPin },
// { logger, readerPins[3].nssPin, readerPins[3].busyPin, readerPins[3].resetPin },
};
FilamentGuardDeviceInfo deviceInfo;
@@ -43,10 +101,44 @@ void setup()
logger.info(
"NfcReader",
"Initialiserer PN5180.");
String("Initialiserer ") +
ReaderCount +
" PN5180-laesere.");
pn5180Reader.begin();
pn5180Reader.printVersionInformation();
// Deaktiver alle moduler, foer den faelles SPI-bus startes. Ellers kan
// et endnu ikke initialiseret modul svare samtidig med det aktive modul.
for (size_t index = 0;
index < ReaderCount;
++index)
{
prepareReaderPins(
readerPins[index]);
}
for (size_t index = 0;
index < ReaderCount;
++index)
{
const String readerLabel =
String("PN5180 nr. ") +
(index + 1);
logger.info(
"NfcReader",
String("Initialiserer ") +
readerLabel +
".");
readers[index].begin();
logger.info(
"NfcReader",
String("Laeser versionsoplysninger fra ") +
readerLabel +
".");
readers[index].printVersionInformation();
}
const String hostname =
deviceSettings.getOrCreateHostname();
@@ -58,8 +150,13 @@ void setup()
logger.setDeviceInfo(
deviceInfo);
pn5180Reader.setDeviceInfo(
for (size_t index = 0;
index < ReaderCount;
++index)
{
readers[index].setDeviceInfo(
deviceInfo);
}
const String deviceData =
String("{\"deviceId\":\"") +
@@ -91,12 +188,147 @@ void setup()
logger.info(
"NfcReader",
"Klar til at laese ISO15693-tags.");
String("Alle ") +
ReaderCount +
" PN5180-laesere er klar.");
}
void loop()
{
pn5180Reader.checkForTag();
// Scan alle readers hver cyklus \u2014 hver reader arbejder uafhaengigt.
// Multi-reader use case (fx indx-8): hver spole-position skal kunne
// have sit eget tag samtidig.
uint8_t uids[ReaderCount][UidLength] = {};
uint16_t signalStrengths[ReaderCount] = {};
bool tagFound[ReaderCount] = {};
for (size_t index = 0;
index < ReaderCount;
++index)
{
tagFound[index] =
readers[index].scanForTag(
uids[index],
signalStrengths[index]);
}
// Fase 1: For hver reader med en aktiv tag, opdater tilstedevaerelsen.
for (size_t index = 0;
index < ReaderCount;
++index)
{
if (!readers[index].hasActiveTag())
{
continue;
}
if (tagFound[index] &&
readers[index].isActiveTag(
uids[index],
UidLength))
{
readers[index].markTagPresent();
}
else
{
readers[index].markTagMissing();
}
}
// Fase 2: For readers UDEN aktiv tag der fandt en tag, vaelg den
// med hoejest signalstyrke og registrer der. Undg\u00e5 UIDs der allerede
// er aktive p\u00e5 en anden reader (samme tag i overlappende felter).
int selectedIndex = -1;
uint16_t bestSignal = 0;
for (size_t index = 0;
index < ReaderCount;
++index)
{
if (readers[index].hasActiveTag())
{
continue;
}
if (!tagFound[index])
{
continue;
}
bool alreadyActiveElsewhere = false;
for (size_t other = 0;
other < ReaderCount;
++other)
{
if (other == index)
{
continue;
}
if (readers[other].isActiveTag(
uids[index],
UidLength))
{
alreadyActiveElsewhere = true;
break;
}
}
if (alreadyActiveElsewhere)
{
continue;
}
if (signalStrengths[index] >= bestSignal)
{
bestSignal =
signalStrengths[index];
selectedIndex =
static_cast<int>(index);
}
}
if (selectedIndex >= 0)
{
String selectionData =
String("{\"selectedReaderId\":\"") +
readers[selectedIndex].getReaderId() +
"\",\"readers\":[";
for (size_t index = 0;
index < ReaderCount;
++index)
{
if (index > 0)
{
selectionData += ",";
}
selectionData += "{\"index\":";
selectionData += index + 1;
selectionData += ",\"found\":";
selectionData += tagFound[index]
? "true"
: "false";
selectionData += ",\"agc\":";
selectionData += signalStrengths[index];
selectionData += "}";
}
selectionData += "]}";
logger.info(
"NfcReader",
"Naermeste PN5180-laeser valgt.",
selectionData);
readers[selectedIndex].registerTag(
uids[selectedIndex],
UidLength);
}
logger.process();
delay(200);

130
src/pn5180_probe.cpp Normal file
View File

@@ -0,0 +1,130 @@
#include <Arduino.h>
#include <PN5180ISO15693.h>
#ifndef PN5180_PROBE_READER
#define PN5180_PROBE_READER 1
#endif
namespace
{
constexpr uint8_t Reader1NssPin = 5;
constexpr uint8_t Reader1BusyPin = 16;
constexpr uint8_t Reader1ResetPin = 17;
constexpr uint8_t Reader2NssPin = 27;
constexpr uint8_t Reader2BusyPin = 26;
constexpr uint8_t Reader2ResetPin = 25;
#if PN5180_PROBE_READER == 1
PN5180ISO15693 reader(
Reader1NssPin,
Reader1BusyPin,
Reader1ResetPin);
#else
PN5180ISO15693 reader(
Reader2NssPin,
Reader2BusyPin,
Reader2ResetPin);
#endif
void printUid(
const uint8_t* uid)
{
for (int8_t index = 7;
index >= 0;
--index)
{
if (uid[index] < 0x10)
{
Serial.print('0');
}
Serial.print(
uid[index],
HEX);
if (index > 0)
{
Serial.print(':');
}
}
}
}
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println();
Serial.print(
"[Probe] Starter isoleret PN5180 reader ");
Serial.println(
PN5180_PROBE_READER);
reader.begin();
reader.reset();
uint8_t productVersion[2] = {};
if (reader.readEEprom(
PRODUCT_VERSION,
productVersion,
sizeof(productVersion)))
{
Serial.print(
"[Probe] PN5180 svarer. Produktversion: ");
Serial.print(
productVersion[1]);
Serial.print('.');
Serial.println(
productVersion[0]);
}
else
{
Serial.println(
"[Probe] FEJL: PN5180 svarer ikke paa SPI.");
}
}
void loop()
{
uint8_t uid[8] = {};
reader.reset();
if (!reader.setupRF())
{
Serial.println(
"[Probe] FEJL: RF kunne ikke startes.");
delay(1000);
return;
}
delay(20);
const ISO15693ErrorCode result =
reader.getInventory(
uid);
reader.setRF_off();
if (result == ISO15693_EC_OK)
{
Serial.print(
"[Probe] TAG FUNDET: ");
printUid(
uid);
Serial.println();
}
else
{
Serial.print(
"[Probe] Intet tag. Fejlkode: ");
Serial.println(
static_cast<int>(
result));
}
delay(500);
}

121
tools/test-api.ps1 Normal file
View File

@@ -0,0 +1,121 @@
param(
[string]$BaseUrl = "http://192.168.3.2:9090"
)
$ErrorActionPreference = "Stop"
function Invoke-FilamentGuardPost {
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[object]$Payload
)
$url = $BaseUrl.TrimEnd("/") + $Path
$json = $Payload | ConvertTo-Json -Depth 10 -Compress
Write-Host ""
Write-Host "=== $Name ===" -ForegroundColor Cyan
Write-Host "POST $url"
Write-Host $json
try {
$response = Invoke-WebRequest `
-Uri $url `
-Method Post `
-ContentType "application/json; charset=utf-8" `
-Body $json `
-UseBasicParsing
Write-Host "HTTP $([int]$response.StatusCode)" -ForegroundColor Green
if ($response.Content) {
Write-Host $response.Content
}
}
catch {
$statusCode = 0
$responseBody = ""
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
try {
$stream = $_.Exception.Response.GetResponseStream()
$reader = [System.IO.StreamReader]::new($stream)
$responseBody = $reader.ReadToEnd()
$reader.Dispose()
}
catch {
$responseBody = ""
}
}
Write-Host "HTTP $statusCode" -ForegroundColor Red
if ($responseBody) {
Write-Host $responseBody
}
else {
Write-Host $_.Exception.Message
}
}
}
$deviceId = "ESP32-API-TEST"
$hostname = "filamentguard-api-test"
$tagUid = "E0:04:01:23:45:67:89:AB"
$tagRead = @{
reader = @{
id = "00112233445566778899AABBCCDDEEFF"
}
device = @{
deviceId = $deviceId
hostname = $hostname
}
tag = @{
tagUid = $tagUid
instanceUuid = "00000000-0000-0000-0000-000000000001"
materialName = "API test PLA"
materialType = "PLA"
brand = "Test"
colorHex = "#FF0000"
actualWeightGrams = 1000
nominalWeightGrams = 1000
spoolWeightGrams = 200
consumedWeightGrams = 0
remainingWeightGrams = 1000
hasActualWeight = $true
hasNominalWeight = $true
hasSpoolWeight = $true
hasConsumedWeight = $true
}
}
$logEntry = @{
deviceId = $deviceId
hostname = $hostname
firmwareVersion = "1.0.2"
level = "Info"
category = "ApiTest"
message = "Ekstern API-test"
data = '{"source":"test-api.ps1"}'
tagUid = $tagUid
uptimeMilliseconds = 12345
}
Invoke-FilamentGuardPost `
-Name "Tagregistrering" `
-Path "/api/Tag/detected" `
-Payload $tagRead
Invoke-FilamentGuardPost `
-Name "Logoprettelse" `
-Path "/api/Log/create" `
-Payload $logEntry