diff --git a/n_request.c b/n_request.c
index 0be148fb..bd989bab 100644
--- a/n_request.c
+++ b/n_request.c
@@ -527,13 +527,22 @@ J *_noteTransactionShouldLock(J *req, bool lockNotecard)
_LockNote();
}
- // Make sure that if anything was pending in the notecard's receive buffer,
- // that it is line-terminated so that the request we're about to send
- // begins on a new line.
+ // If a reset of the I/O interface is required for any reason, do it now.
if (resetRequired) {
NOTE_C_LOG_DEBUG("Resetting Notecard I/O Interface...");
- _Reset();
- resetRequired = false;
+ if ((resetRequired = !_Reset())) {
+ if (lockNotecard) {
+ _UnlockNote();
+ }
+ _Free(json);
+ _TransactionStop();
+ const char *errStr = ERRSTR("failed to reset Notecard interface {io}", c_iobad);
+ if (cmdFound) {
+ NOTE_C_LOG_ERROR(errStr);
+ return NULL;
+ }
+ return _errDoc(id, errStr);
+ }
}
// If we're performing retries, this is where we come back to
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f4a0c38a..c6fd18f7 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -168,6 +168,7 @@ add_test(NoteNewRequest_test)
add_test(NotePayload_test)
add_test(NotePayloadRetrieveAfterSleep_test)
add_test(NotePayloadSaveAndSleep_test)
+add_test(NotePing_test)
add_test(NotePrint_test)
add_test(NotePrintf_test)
add_test(NotePrintln_test)
diff --git a/test/src/NotePing_test.cpp b/test/src/NotePing_test.cpp
new file mode 100644
index 00000000..1803e844
--- /dev/null
+++ b/test/src/NotePing_test.cpp
@@ -0,0 +1,259 @@
+/*!
+ * @file NotePing_test.cpp
+ *
+ * Written by the Blues Inc. team.
+ *
+ * Copyright (c) 2026 Blues Inc. MIT License. Use of this source code is
+ * governed by licenses granted by the copyright holder including that found in
+ * the
+ * LICENSE
+ * file.
+ *
+ */
+
+#include
+#include
+
+#include "n_lib.h"
+
+#include
+#include
+
+DEFINE_FFF_GLOBALS
+FAKE_VALUE_FUNC(bool, _noteTransactionStart, uint32_t)
+FAKE_VOID_FUNC(_noteTransactionStop)
+FAKE_VOID_FUNC(_noteLockNote)
+FAKE_VOID_FUNC(_noteUnlockNote)
+FAKE_VALUE_FUNC(const char *, _noteJSONTransaction, const char *, size_t, char **, uint32_t)
+FAKE_VALUE_FUNC(bool, _noteSerialAvailable)
+FAKE_VALUE_FUNC(char, _noteSerialReceive)
+FAKE_VALUE_FUNC(bool, _noteHardReset)
+
+extern volatile int hookActiveInterface;
+
+namespace
+{
+
+enum class PingResponse {
+ Echo,
+ WrongNonce,
+ Error,
+ InvalidJson,
+ NoResponse,
+ TransactionError,
+};
+
+PingResponse pingResponse = PingResponse::Echo;
+uint32_t currentMs = 1000;
+size_t serialBytesRemaining = 0;
+size_t serialBytesRemainingAtTransaction = 0;
+size_t lastRequestLength = 0;
+uint32_t lastTransactionTimeoutMs = 0;
+bool lastRequestEndedWithNewline = false;
+bool lastRequestHadCrc = false;
+
+char *copyString(const char *src)
+{
+ const size_t len = strlen(src);
+ char *dst = static_cast(malloc(len + 1));
+ if (dst != NULL) {
+ memcpy(dst, src, len + 1);
+ }
+ return dst;
+}
+
+uint32_t getMs()
+{
+ return currentMs;
+}
+
+void delayMs(uint32_t ms)
+{
+ currentMs += ms;
+}
+
+bool serialAvailable()
+{
+ return serialBytesRemaining > 0;
+}
+
+char serialReceive()
+{
+ if (serialBytesRemaining > 0) {
+ --serialBytesRemaining;
+ }
+ return 'x';
+}
+
+char *makeResponse(const char *text)
+{
+ J *rsp = JCreateObject();
+ if (rsp == NULL) {
+ return NULL;
+ }
+ JAddStringToObject(rsp, "text", text);
+ char *json = JPrintUnformatted(rsp);
+ JDelete(rsp);
+ return json;
+}
+
+const char *pingTransaction(const char *request, size_t reqLen, char **response, uint32_t timeoutMs)
+{
+ lastRequestLength = reqLen;
+ lastTransactionTimeoutMs = timeoutMs;
+ lastRequestEndedWithNewline = (reqLen > 0 && request[reqLen - 1] == '\n');
+ lastRequestHadCrc = (strstr(request, "\"crc\"") != NULL);
+ serialBytesRemainingAtTransaction = serialBytesRemaining;
+
+ if (pingResponse == PingResponse::TransactionError) {
+ return ERRSTR("transaction failed {io}", c_ioerr);
+ }
+ if (response == NULL || pingResponse == PingResponse::NoResponse) {
+ return NULL;
+ }
+ if (pingResponse == PingResponse::InvalidJson) {
+ *response = copyString("not-json");
+ return NULL;
+ }
+ if (pingResponse == PingResponse::Error) {
+ *response = copyString("{\"err\":\"failed\"}");
+ return NULL;
+ }
+
+ char *requestCopy = static_cast(malloc(reqLen + 1));
+ if (requestCopy == NULL) {
+ return ERRSTR("malloc failed {mem}", c_mem);
+ }
+ memcpy(requestCopy, request, reqLen);
+ requestCopy[reqLen] = '\0';
+ if (reqLen > 0 && requestCopy[reqLen - 1] == '\n') {
+ requestCopy[reqLen - 1] = '\0';
+ }
+
+ J *req = JParse(requestCopy);
+ free(requestCopy);
+ if (req == NULL) {
+ return ERRSTR("parse failed {bad}", c_bad);
+ }
+
+ const char *nonce = JGetString(req, "text");
+ *response = makeResponse(pingResponse == PingResponse::WrongNonce ? "wrong" : nonce);
+ JDelete(req);
+
+ return NULL;
+}
+
+void resetTestState(void)
+{
+ RESET_FAKE(_noteTransactionStart);
+ RESET_FAKE(_noteTransactionStop);
+ RESET_FAKE(_noteLockNote);
+ RESET_FAKE(_noteUnlockNote);
+ RESET_FAKE(_noteJSONTransaction);
+ RESET_FAKE(_noteSerialAvailable);
+ RESET_FAKE(_noteSerialReceive);
+ RESET_FAKE(_noteHardReset);
+
+ pingResponse = PingResponse::Echo;
+ currentMs = 1000;
+ serialBytesRemaining = 0;
+ serialBytesRemainingAtTransaction = 0;
+ lastRequestLength = 0;
+ lastTransactionTimeoutMs = 0;
+ lastRequestEndedWithNewline = false;
+ lastRequestHadCrc = false;
+ resetRequired = false;
+
+ NoteSetFnDefault(malloc, free, delayMs, getMs);
+ RESET_FAKE(_noteLockNote);
+ RESET_FAKE(_noteUnlockNote);
+
+ hookActiveInterface = NOTE_C_INTERFACE_SERIAL;
+ _noteTransactionStart_fake.return_val = true;
+ _noteJSONTransaction_fake.custom_fake = pingTransaction;
+ _noteSerialAvailable_fake.custom_fake = serialAvailable;
+ _noteSerialReceive_fake.custom_fake = serialReceive;
+}
+
+SCENARIO("NotePing")
+{
+ resetTestState();
+
+ SECTION("returns true when echo response matches the nonce") {
+ CHECK(NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ CHECK(lastRequestLength > 0);
+ CHECK(lastRequestEndedWithNewline);
+ CHECK(!lastRequestHadCrc);
+ CHECK(lastTransactionTimeoutMs == 500);
+ CHECK(_noteTransactionStart_fake.call_count == 1);
+ CHECK(_noteTransactionStop_fake.call_count == 1);
+ CHECK(_noteLockNote_fake.call_count == 1);
+ CHECK(_noteUnlockNote_fake.call_count == 1);
+ }
+
+ SECTION("returns false when the echoed nonce does not match") {
+ pingResponse = PingResponse::WrongNonce;
+
+ CHECK(!NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ }
+
+ SECTION("returns false when the response contains an error") {
+ pingResponse = PingResponse::Error;
+
+ CHECK(!NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ }
+
+ SECTION("returns false when the response is not valid JSON") {
+ pingResponse = PingResponse::InvalidJson;
+
+ CHECK(!NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ }
+
+ SECTION("returns false when the transaction fails and does not retry") {
+ pingResponse = PingResponse::TransactionError;
+
+ CHECK(!NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ }
+
+ SECTION("returns false when no response is received and does not retry") {
+ pingResponse = PingResponse::NoResponse;
+
+ CHECK(!NotePing());
+ CHECK(_noteJSONTransaction_fake.call_count == 1);
+ }
+
+ SECTION("does not reset even when resetRequired is set") {
+ resetRequired = true;
+ pingResponse = PingResponse::TransactionError;
+
+ CHECK(!NotePing());
+ CHECK(_noteHardReset_fake.call_count == 0);
+ CHECK(resetRequired == true);
+ }
+
+ SECTION("returns false without locking or transacting when transaction start fails") {
+ _noteTransactionStart_fake.return_val = false;
+
+ CHECK(!NotePing());
+ CHECK(_noteLockNote_fake.call_count == 0);
+ CHECK(_noteJSONTransaction_fake.call_count == 0);
+ CHECK(_noteTransactionStop_fake.call_count == 0);
+ }
+
+ SECTION("drains serial input before sending the ping") {
+ serialBytesRemaining = 3;
+
+ CHECK(NotePing());
+ CHECK(_noteSerialReceive_fake.call_count == 3);
+ CHECK(serialBytesRemainingAtTransaction == 0);
+ }
+
+ resetTestState();
+}
+
+}