diff --git a/README.md b/README.md index 183d7a0..1fde9ae 100644 --- a/README.md +++ b/README.md @@ -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 +``` + diff --git a/include/FilamentGuardApiClient.h b/include/FilamentGuardApiClient.h index df19c35..99859ff 100644 --- a/include/FilamentGuardApiClient.h +++ b/include/FilamentGuardApiClient.h @@ -2,6 +2,18 @@ #include +#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 diff --git a/include/FilamentGuardTagRead.h b/include/FilamentGuardTagRead.h index 76627ff..3339b15 100644 --- a/include/FilamentGuardTagRead.h +++ b/include/FilamentGuardTagRead.h @@ -7,6 +7,7 @@ struct FilamentGuardTagRead { + String readerId; FilamentGuardDeviceInfo device; OpenPrintTagData tag; -}; \ No newline at end of file +}; diff --git a/include/Pn5180Reader.h b/include/Pn5180Reader.h index edfbc60..0421e01 100644 --- a/include/Pn5180Reader.h +++ b/include/Pn5180Reader.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #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( diff --git a/ota/bridge/filamentguard-1.0.3.bin b/ota/bridge/filamentguard-1.0.3.bin new file mode 100644 index 0000000..7f966d3 Binary files /dev/null and b/ota/bridge/filamentguard-1.0.3.bin differ diff --git a/ota/downgrade/filamentguard-0.5.0.bin b/ota/downgrade/filamentguard-0.5.0.bin new file mode 100644 index 0000000..5ef6439 Binary files /dev/null and b/ota/downgrade/filamentguard-0.5.0.bin differ diff --git a/platformio.ini b/platformio.ini index f98c351..c7d79eb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 = + +<*> + - +build_flags = + -D FILAMENT_GUARD_API_ENV=0 + +[env:prod] +build_src_filter = + +<*> + - +build_flags = + -D FILAMENT_GUARD_API_ENV=1 + diff --git a/src/FilamentGuardApiClient.cpp b/src/FilamentGuardApiClient.cpp index 8c854a3..3be9a38 100644 --- a/src/FilamentGuardApiClient.cpp +++ b/src/FilamentGuardApiClient.cpp @@ -2,52 +2,55 @@ #include #include +#include +#include +#include +#include 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,37 +91,172 @@ 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( - "Svar fra API:"); + Serial.println( + responseCode); + } - Serial.println( - responseBody); - } + if (!successful && + !responseBody.isEmpty()) + { + Serial.println( + "Svar fra API:"); + + 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( - TagReadEndpointUrl, - json, - "tagregistrering", - &responseCode, - &responseBody); + const bool queued = + enqueueJson( + TagReadEndpointUrl, + json, + "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( - TagEventEndpointUrl, - json, - "tagfjernelse", - &responseCode, - &responseBody); + const bool queued = + enqueueJson( + TagEventEndpointUrl, + json, + "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); } diff --git a/src/FilamentGuardTagReadSerializer.cpp b/src/FilamentGuardTagReadSerializer.cpp index 01ec177..4a25406 100644 --- a/src/FilamentGuardTagReadSerializer.cpp +++ b/src/FilamentGuardTagReadSerializer.cpp @@ -9,6 +9,16 @@ String FilamentGuardTagReadSerializer::serialize( json += '{'; + json += "\"reader\":{"; + + appendStringProperty( + json, + "id", + tagRead.readerId, + false); + + json += "},"; + json += "\"device\":{"; appendStringProperty( @@ -232,4 +242,4 @@ void FilamentGuardTagReadSerializer::appendBooleanProperty( { json += ','; } -} \ No newline at end of file +} diff --git a/src/FirmwareUpdater.cpp b/src/FirmwareUpdater.cpp index 77c1d3b..afc859b 100644 --- a/src/FirmwareUpdater.cpp +++ b/src/FirmwareUpdater.cpp @@ -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"; @@ -160,4 +160,4 @@ void FirmwareUpdater::checkForUpdate() "Downloader og installerer firmware."); firmwareUpdate.execOTA(); -} \ No newline at end of file +} diff --git a/src/Pn5180Reader.cpp b/src/Pn5180Reader.cpp index 5bb3f25..a23c617 100644 --- a/src/Pn5180Reader.cpp +++ b/src/Pn5180Reader.cpp @@ -2,16 +2,42 @@ #include +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(); - } - } - - if (tagFound) - { - missingScanCount = 0; - return; - } - - handleTagMissing(); + return readerId; } -bool Pn5180Reader::checkIso14443Tag() +bool Pn5180Reader::scanForTag( + uint8_t* uid, + uint16_t& signalStrength) { - uint8_t uid[10] = {}; + signalStrength = 0; - iso14443Reader.reset(); - iso14443Reader.setupRF(); - - if (!iso14443Reader.isCardPresent()) + if (uid == nullptr) { 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( + std::memset( uid, - uidLength, - TagProtocol::Iso14443A); + 0, + 8); - const String tagData = - String("{\"protocol\":\"ISO14443A\",") + - "\"tagUid\":\"" + - formatUid( - uid, - uidLength, - false) + - "\"}"; + iso15693Reader.reset(); - logger.info( - "NfcReader", - "ISO14443A-tag fundet.", - tagData); + 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( + 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( + 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; + } + + uint32_t rfStatus = 0; + + if (iso15693Reader.readRegister( + RF_STATUS, + &rfStatus)) + { + signalStrength = + static_cast( + rfStatus & + 0x03FF); + } + + iso15693Reader.setRF_off(); return true; } -bool Pn5180Reader::checkIso15693Tag() +bool Pn5180Reader::registerTag( + const uint8_t* uid, + const uint8_t uidLength) { - uint8_t uid[8] = {}; + if (uid == nullptr || + uidLength != 8 || + !isValidIso15693Uid(uid) || + activeProtocol != + TagProtocol::None) + { + return false; + } iso15693Reader.reset(); - iso15693Reader.setupRF(); - const ISO15693ErrorCode result = - iso15693Reader.getInventory( - uid); - - if (result != ISO15693_EC_OK) + if (!iso15693Reader.setupRF()) { + iso15693Reader.setRF_off(); + 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); + 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)) + { + iso15693Reader.setRF_off(); + + return false; + } + + rememberUid( uid, - uidLength); + uidLength, + TagProtocol::Iso15693); - delay(50); - - iso15693Reader.reset(); - iso15693Reader.setupRF(); + 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(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( + blockCount == 1 + ? 0x20 + : 0x23), + 0, 0, 0, 0, 0, 0, 0, 0, + firstBlock, + static_cast( + 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( + rxStatus & + 0x01FF); + const uint16_t dataLength = + static_cast( + 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( - blockNumber) * + firstBlock) * blockSize; - const ISO15693ErrorCode readResult = - iso15693Reader.readSingleBlock( + if (readIso15693BlockRange( uid, - blockNumber, - blockData, - blockSize); - - if (readResult != - ISO15693_EC_OK) + firstBlock, + blocksPerRequest, + destination, + blockSize)) { - allBlocksRead = false; - - const String errorData = - String("{\"blockNumber\":") + - blockNumber + - ",\"errorCode\":" + - static_cast( - readResult) + - "}"; - - logger.error( - "NfcReader", - "ISO15693-blok kunne ikke laeses.", - errorData); + continue; } - } - if (!allBlocksRead) - { - logger.error( - "NfcReader", - "OpenPrintTag-parseren blev ikke startet, fordi en eller flere blokke ikke kunne laeses."); + for (uint8_t offset = 0; + offset < blocksPerRequest; + ++offset) + { + const uint8_t blockNumber = + firstBlock + + offset; - return; + if (!readIso15693BlockRange( + uid, + blockNumber, + 1, + memory + + static_cast( + blockNumber) * + blockSize, + blockSize)) + { + const String errorData = + String("{\"blockNumber\":") + + blockNumber + + "}"; + + logger.error( + "NfcReader", + "ISO15693-blok kunne ikke laeses.", + errorData); + + logger.error( + "NfcReader", + "OpenPrintTag-parseren blev ikke startet, fordi hukommelsen ikke kunne laeses komplet."); + + 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); } } diff --git a/src/main.cpp b/src/main.cpp index fa8d84a..6db7e42 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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( - deviceInfo); + for (size_t index = 0; + index < ReaderCount; + ++index) + { + readers[index].setDeviceInfo( + deviceInfo); + } const String deviceData = String("{\"deviceId\":\"") + @@ -91,13 +188,148 @@ 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(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); -} \ No newline at end of file +} diff --git a/src/pn5180_probe.cpp b/src/pn5180_probe.cpp new file mode 100644 index 0000000..230f8d0 --- /dev/null +++ b/src/pn5180_probe.cpp @@ -0,0 +1,130 @@ +#include +#include + +#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( + result)); + } + + delay(500); +} diff --git a/tools/test-api.ps1 b/tools/test-api.ps1 new file mode 100644 index 0000000..d587c42 --- /dev/null +++ b/tools/test-api.ps1 @@ -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