• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blues / note-c / 21049432804

15 Jan 2026 11:04PM UTC coverage: 92.697% (-0.3%) from 92.961%
21049432804

Pull #217

github

web-flow
Merge c6385c24b into f00106160
Pull Request #217: feat: NoteConnect

1180 of 1367 branches covered (86.32%)

Branch coverage included in aggregate %.

0 of 7 new or added lines in 1 file covered. (0.0%)

2412 of 2508 relevant lines covered (96.17%)

79.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.43
/n_request.c
1
/*!
2
 @file n_request.c
3

4
 Written by Ray Ozzie and Blues Inc. team.
5

6
 Copyright (c) 2019 Blues Inc. MIT License. Use of this source code is
7
 governed by licenses granted by the copyright holder including that found in
8
 the
9
 <a href="https://github.com/blues/note-c/blob/master/LICENSE">LICENSE</a>
10
 file.
11
 */
12

13
#include "n_lib.h"
14

15
#include <string.h>
16

17
static const int RETRY_DELAY_MS = 500;
18

19
// A value that optionally overrides CARD_INTER_TRANSACTION_TIMEOUT_SEC
20
uint32_t cardTransactionTimeoutOverrideSecs = 0;
21

22
// For flow tracing
23
static int suppressShowTransactions = 0;
24

25
// Flag that gets set whenever an error occurs that should force a reset
26
NOTE_C_STATIC bool resetRequired = true;
27

28
// CRC data
29
#ifndef NOTE_C_LOW_MEM
30
static uint16_t seqNo = 0;
31
#define CRC_FIELD_LENGTH        22  // ,"crc":"SSSS:CCCCCCCC"
32
#define CRC_FIELD_NAME_OFFSET   1
33
#define CRC_FIELD_NAME_TEST     "\"crc\":\""
34
#define ERR_FIELD_NAME_TEST     "\"err\":\""
35
NOTE_C_STATIC int32_t _crc32(const void* data, size_t length);
36
NOTE_C_STATIC char * _crcAdd(char *json, uint16_t seqno);
37
NOTE_C_STATIC bool _crcError(char *json, uint16_t shouldBeSeqno);
38

39
NOTE_C_STATIC bool notecardFirmwareSupportsCrc = false;
40
#endif // !NOTE_C_LOW_MEM
41

42
/*!
43
 @internal
44

45
 @brief Create a JSON object containing an error message.
46

47
 Create a dynamically allocated `J` object containing a single string field
48
 "err" whose value is the passed in error message.
49

50
 @param id The "id" from the original request that resulted in an error
51
 @param errmsg The error message.
52

53
 @returns A `J` object with the "err" field populated.
54
 */
55
NOTE_C_STATIC J * _errDoc(uint32_t id, const char *errmsg)
36✔
56
{
57
    J *rspdoc = JCreateObject();
36✔
58
    if (rspdoc != NULL) {
36✔
59
        JAddStringToObject(rspdoc, c_err, errmsg);
33✔
60
        JAddStringToObject(rspdoc, "src", "note-c");
33✔
61
        if (id) {
33✔
62
            JAddIntToObject(rspdoc, "id", id);
10✔
63
        }
64
        if (suppressShowTransactions == 0) {
33✔
65
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, "[ERROR] ");
32✔
66
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, "{\"err\":\"");
32✔
67
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, errmsg);
32✔
68
            _DebugWithLevelLn(NOTE_C_LOG_LEVEL_ERROR, "\",\"src\":\"note-c\"}");
32✔
69
        }
70
    } else {
71
        NOTE_C_LOG_ERROR("Failed to allocate error document!");
3✔
72
    }
73

74
    return rspdoc;
36✔
75
}
76

77
/*!
78
 @brief Resume showing transaction details.
79
 */
80
void _noteResumeTransactionDebug(void)
12✔
81
{
82
    suppressShowTransactions--;
12✔
83
}
12✔
84

85
/*!
86
 @brief Suppress showing transaction details.
87
 */
88
void _noteSuspendTransactionDebug(void)
12✔
89
{
90
    suppressShowTransactions++;
12✔
91
}
12✔
92

93
/*!
94
 @internal
95

96
 @brief Calculate the transaction timeout.
97

98
  When note.add or web.* requests are used to transfer binary data, the
99
  time to complete the transaction can vary depending on the size of
100
  the payload and network conditions. Therefore, it's possible for
101
  these transactions to timeout prematurely.
102

103
  The algorithm executes the following logic:
104
  - If the request is a `note.add`, set the timeout value to the
105
    value of the "milliseconds" parameter, if it exists. If it
106
    doesn't, use the "seconds" parameter. If that doesn't exist,
107
    use the standard timeout of `CARD_INTER_TRANSACTION_TIMEOUT_SEC`.
108
  - If the request is a `web.*`, follow the same logic, but instead
109
    of using the standard timeout, use the Notecard timeout of 90
110
    seconds for all `web.*` transactions.
111

112
 @param req The request object.
113
 @param isReq Whether the request is a regular request or a command.
114

115
 @returns The timeout in milliseconds.
116
 */
117
NOTE_C_STATIC uint32_t _noteTransaction_calculateTimeoutMs(J *req, bool isReq)
47✔
118
{
119
    uint32_t result = ((CARD_INTER_TRANSACTION_TIMEOUT_SEC - 1) * 1000);
47!
120

121
    // Interrogate the request
122
    if (JContainsString(req, (isReq ? "req" : "cmd"), "note.add")) {
47✔
123
        if (JIsPresent(req, "milliseconds")) {
29✔
124
            NOTE_C_LOG_DEBUG("Using `milliseconds` parameter value for "
4✔
125
                             "timeout.");
126
            result = JGetInt(req, "milliseconds");
4✔
127
        } else if (JIsPresent(req, "seconds")) {
25✔
128
            NOTE_C_LOG_DEBUG("Using `seconds` parameter value for timeout.");
2✔
129
            result = (JGetInt(req, "seconds") * 1000);
2✔
130
        }
131
    } else if (JContainsString(req, (isReq ? "req" : "cmd"), "web.")) {
18✔
132
        NOTE_C_LOG_DEBUG("web.* request received.");
8✔
133
        if (JIsPresent(req, "milliseconds")) {
8✔
134
            NOTE_C_LOG_DEBUG("Using `milliseconds` parameter value for "
4✔
135
                             "timeout.");
136
            result = JGetInt(req, "milliseconds");
4✔
137
        } else if (JIsPresent(req, "seconds")) {
4✔
138
            NOTE_C_LOG_DEBUG("Using `seconds` parameter value for timeout.");
2✔
139
            result = (JGetInt(req, "seconds") * 1000);
2✔
140
        }
141
    }
142

143
    // Add one second to the timeout, to provide time for the Notecard to
144
    // timeout first, then report the timeout to the host, when applicable.
145
    result += 1000;
47✔
146

147
    return result;
47✔
148
}
149

150
/*!
151
 @brief Suppress showing transaction details.
152
 */
153
void NoteSuspendTransactionDebug(void)
2✔
154
{
155
    _noteSuspendTransactionDebug();
2✔
156
}
2✔
157

158
/*!
159
 @brief Resume showing transaction details.
160
 */
161
void NoteResumeTransactionDebug(void)
2✔
162
{
163
    _noteResumeTransactionDebug();
2✔
164
}
2✔
165

166
uint32_t NoteSetRequestTimeout(uint32_t overrideSecs)
12✔
167
{
168
    uint32_t previous = CARD_INTER_TRANSACTION_TIMEOUT_SEC;
12✔
169
    cardTransactionTimeoutOverrideSecs = overrideSecs;
12✔
170
    return previous;
12✔
171
}
172

173
J *NoteNewRequest(const char *request)
75✔
174
{
175
    J *reqdoc = JCreateObject();
75✔
176
    if (reqdoc != NULL) {
75✔
177
        JAddStringToObject(reqdoc, c_req, request);
74✔
178
    }
179
    return reqdoc;
75✔
180
}
181

182
J *NoteNewCommand(const char *request)
16✔
183
{
184
    J *reqdoc = JCreateObject();
16✔
185
    if (reqdoc != NULL) {
16✔
186
        JAddStringToObject(reqdoc, c_cmd, request);
15✔
187
    }
188
    return reqdoc;
16✔
189
}
190

191
bool NoteRequest(J *req)
4✔
192
{
193
    J *rsp = NoteRequestResponse(req);
4✔
194
    if (rsp == NULL) {
4✔
195
        return false;
2✔
196
    }
197

198
    // Check for a transaction error, and exit
199
    bool success = JIsNullString(rsp, c_err);
2✔
200
    JDelete(rsp);
2✔
201

202
    return success;
2✔
203
}
204

205
bool NoteRequestWithRetry(J *req, uint32_t timeoutSeconds)
7✔
206
{
207
#pragma GCC diagnostic push
208
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
209
    J *rsp = NoteRequestResponseWithRetry(req, timeoutSeconds);
7✔
210
#pragma GCC diagnostic pop
211
    // If there is no response return false
212
    if (rsp == NULL) {
7✔
213
        return false;
4✔
214
    }
215

216
    // Check for a transaction error, and exit
217
    bool success = JIsNullString(rsp, c_err);
3✔
218
    JDelete(rsp);
3✔
219

220
    return success;
3✔
221
}
222

223
J *NoteRequestResponse(J *req)
8✔
224
{
225
    // Exit if null request. This allows safe execution of the form
226
    // NoteRequestResponse(NoteNewRequest("xxx"))
227
    if (req == NULL) {
8✔
228
        return NULL;
3✔
229
    }
230
    // Execute the transaction
231
    J *rsp = NoteTransaction(req);
5✔
232
    // Free the request and exit
233
    JDelete(req);
5✔
234
    return rsp;
5✔
235
}
236

237
J *NoteRequestResponseWithRetry(J *req, uint32_t timeoutSeconds)
23✔
238
{
239
    // Exit if null request. This allows safe execution of the form
240
    // NoteRequestResponse(NoteNewRequest("xxx"))
241
    if (req == NULL) {
23✔
242
        return NULL;
2✔
243
    }
244

245
    J *rsp;
246

247
    // Calculate expiry time in milliseconds
248
    uint32_t startMs = _GetMs();
21✔
249
    uint32_t timeoutMs = timeoutSeconds * 1000;
21✔
250

251
    while(true) {
252
        // Execute the transaction
253
        rsp = NoteTransaction(req);
31✔
254

255
        // Loop if there is no response, or if there is an io error
256
        if ((rsp == NULL) || (JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported))) {
31✔
257

258
            // Free error response
259
            if (rsp != NULL) {
17✔
260
                JDelete(rsp);
6✔
261
                rsp = NULL;
6✔
262
            }
263
        } else {
264

265
            // Exit loop on non-null response without io error
266
            break;
267
        }
268

269
        // Exit loop on timeout
270
        if (_GetMs() - startMs >= timeoutMs) {
17✔
271
            break;
7✔
272
        }
273
    }
274

275
    // Free the request
276
    JDelete(req);
21✔
277

278
    // Return the response
279
    return rsp;
21✔
280
}
281

282
char * NoteRequestResponseJSON(const char *reqJSON)
41✔
283
{
284
    const uint32_t transactionTimeoutMs = (CARD_INTER_TRANSACTION_TIMEOUT_SEC * 1000);
41!
285
    char *rspJSON = NULL;
41✔
286
    char *allocatedJSON = NULL; // required to free the string if it is not newline-terminated
41✔
287
    bool isCmdPipeline = false;
41✔
288

289
    if (reqJSON == NULL) {
41✔
290
        return NULL;
2✔
291
    }
292

293
    // Make sure that we get access to the Notecard before transacting.
294
    if (!_TransactionStart(transactionTimeoutMs)) {
39✔
295
        return NULL;
1✔
296
    }
297

298
    _LockNote();
38✔
299

300
    // Manually tokenize the string to search for multiple embedded
301
    // commands (cannot use strtok)
302
    for (;;) {
4✔
303
        const char *endPtr;
304
        const char * const newlinePtr = strchr(reqJSON, '\n');
42✔
305

306
        // If string is not newline-terminated, then allocate a new
307
        // string and terminate it
308
        if (NULL == newlinePtr) {
42✔
309
            // All JSON strings should be newline-terminated to meet the
310
            // specification, however this is required to ensure backward
311
            // compatibility with the previous implementation.
312
            const size_t allocLen = strlen(reqJSON);
15✔
313
            if (0 == allocLen) {
15✔
314
                NOTE_C_LOG_ERROR(ERRSTR("request: jsonbuf zero length", c_bad));
2✔
315
                break;
2✔
316
            }
317

318
            NOTE_C_LOG_WARN(ERRSTR("Memory allocation due to malformed request (not newline-terminated)", c_bad));
13✔
319
            allocatedJSON = _Malloc(allocLen + 2);  // +2 for newline and null-terminator
13✔
320
            if (allocatedJSON == NULL) {
13✔
321
                NOTE_C_LOG_ERROR(ERRSTR("request: jsonbuf malloc failed", c_mem));
3✔
322
                break;
3✔
323
            }
324

325
            memcpy(allocatedJSON, reqJSON, allocLen);
10✔
326
            allocatedJSON[allocLen] = '\n';
10✔
327
            allocatedJSON[allocLen + 1] = '\0';
10✔
328
            reqJSON = allocatedJSON;
10✔
329
            endPtr = &allocatedJSON[allocLen];
10✔
330
        } else {
331
            isCmdPipeline = !(strlen(newlinePtr) == 1);
27✔
332
            endPtr = newlinePtr;
27✔
333
        }
334
        const size_t reqLen = ((endPtr - reqJSON) + 1);
37✔
335

336
        bool isCmd = false;
37✔
337
        if (strstr(reqJSON, "\"cmd\":") != NULL) {
37✔
338
            // Only call `JParse()` after verifying the provided request
339
            // appears to contain a command (i.e. we find `"cmd":`).
340
            J *jsonObj = JParse(reqJSON);
20✔
341
            if (!jsonObj) {
20✔
342
                // Invalid JSON.
343
                if (NULL == newlinePtr) {
6✔
344
                    _Free((void *)reqJSON);
3✔
345
                }
346
                break;
6✔
347
            }
348
            isCmd = JIsPresent(jsonObj, "cmd");
14✔
349
            JDelete(jsonObj);
14✔
350
        }
351

352
        if (!isCmd) {
31✔
353
            const char *errstr = _Transaction(reqJSON, reqLen, &rspJSON, transactionTimeoutMs);
18✔
354
            if (errstr != NULL) {
18✔
355
                NOTE_C_LOG_ERROR(errstr);
9✔
356

357
                // Extract ID from the request JSON, if present
358
                uint32_t id = 0;
9✔
359
                J *req = JParse(reqJSON);
9✔
360
                if (req != NULL) {
9✔
361
                    id = JGetInt(req, "id");
5✔
362
                    JDelete(req);
5✔
363
                }
364

365
                // Use _errDoc() to create a well-formed JSON error string
366
                J *errdoc = _errDoc(id, errstr);
9✔
367
                if (errdoc != NULL) {
9✔
368
                    char *errdocJSON = JPrintUnformatted(errdoc);
8✔
369
                    JDelete(errdoc);
8✔
370
                    if (errdocJSON != NULL) {
8✔
371
                        uint32_t errdocJSONLen = strlen(errdocJSON);
7✔
372
                        rspJSON = (char *) _Malloc(errdocJSONLen+2);
7✔
373
                        if (rspJSON != NULL) {
7✔
374
                            memcpy(rspJSON, errdocJSON, errdocJSONLen);
6✔
375
                            rspJSON[errdocJSONLen++] = '\n';
6✔
376
                            rspJSON[errdocJSONLen] = '\0';
6✔
377
                        }
378
                        _Free((void *)errdocJSON);
7✔
379
                    }
380
                }
381
            }
382
            if (allocatedJSON) {
18✔
383
                _Free((void *)allocatedJSON);
3✔
384
                allocatedJSON = NULL;
3✔
385
            }
386
            break;
18✔
387
        } else {
388
            // If it's a command, the Notecard will not respond, so we pass
389
            // NULL for the response parameter.
390
            const char *errstr = _Transaction(reqJSON, reqLen, NULL, transactionTimeoutMs);
13✔
391
            reqJSON = (endPtr + 1);  // Move to the next command in the pipeline
13✔
392
            if (errstr != NULL) {
13✔
393
                NOTE_C_LOG_ERROR(errstr);
2✔
394
            }
395
        }
396

397
        // Clean up if we allocated a new string
398
        (void)isCmdPipeline;
399
        if (allocatedJSON) {
13✔
400
            _Free((void *)allocatedJSON);
4✔
401
            break;
4✔
402
        }
403
        if (!isCmdPipeline) {
9✔
404
            break;
5✔
405
        }
406
    } // for(;;)
407

408
    _UnlockNote();
38✔
409
    _TransactionStop();
38✔
410

411
    return rspJSON;
38✔
412
}
413

414
J *NoteTransaction(J *req)
51✔
415
{
416
    return _noteTransactionShouldLock(req, true);
51✔
417
}
418

419
/**************************************************************************/
420
/*!
421
  @brief Same as `NoteTransaction`, but takes an additional parameter that
422
  indicates if the Notecard should be locked.
423
  @param   req
424
  The `J` cJSON request object.
425
  @param   lockNotecard
426
  Set to `true` if the Notecard should be locked and `false` otherwise.
427
  @returns a `J` cJSON object with the response, or NULL if there is
428
  insufficient memory.
429
*/
430
/**************************************************************************/
431
J *_noteTransactionShouldLock(J *req, bool lockNotecard)
54✔
432
{
433
    // Validate in case of memory failure of the requestor
434
    if (req == NULL) {
54✔
435
        NOTE_C_LOG_ERROR(ERRSTR("NULL request", c_bad));
1✔
436
        return NULL;
1✔
437
    }
438

439
    // Serialize the JSON request
440
    char *json = JPrintUnformatted(req); // `json` allocated, must be freed
53✔
441
    if (json == NULL) {
53✔
442
        _TransactionStop();
2✔
443
        NOTE_C_LOG_ERROR(ERRSTR("failed to serialize JSON request", c_mem));
2✔
444
        return NULL;
2✔
445
    }
446

447
    // Determine the request or command type
448
    const char * const reqApi = JGetString(req, "req");
51✔
449
    const bool reqFound = reqApi[0];  // test for non-empty string
51✔
450
    const char * const cmdApi = JGetString(req, "cmd");
51✔
451
    const bool cmdFound = cmdApi[0];  // test for non-empty string
51✔
452

453
    // If neither `"req"` nor `"cmd"` are found, then we have an error
454
    // condition. If both are present, then we have undefined behavior.
455
    if (!reqFound && !cmdFound) {
51✔
456
        _Free(json);
1✔
457
        NOTE_C_LOG_ERROR(ERRSTR("neither req nor cmd found in API invocation (invalid JSON)", c_bad));
1✔
458
        return NULL;
1✔
459
    } else if (reqFound && cmdFound) {
50✔
460
        _Free(json);
1✔
461
        NOTE_C_LOG_ERROR(ERRSTR("both req and cmd present in API invocation (undefined behavior)", c_bad));
1✔
462
        return NULL;
1✔
463
    }
464

465
    // Extract the ID of the request so that errors can be returned with the same ID
466
    const uint32_t id = JGetInt(req, "id");
49✔
467

468
    // Ensure the Notecard is ready
469
    if (!_TransactionStart(CARD_INTER_TRANSACTION_TIMEOUT_SEC * 1000)) {
49!
470
        _Free(json);
2✔
471
        const char *errStr = ERRSTR("Notecard not ready (CTX/RTX) {io}", c_ioerr);
2✔
472
        if (cmdFound) {
2✔
473
            NOTE_C_LOG_ERROR(errStr);
1✔
474
            return NULL;
1✔
475
        }
476
        return _errDoc(id, errStr);
1✔
477
    }
478

479
    // Inject the user agent object only when we're doing a `hub.set` and
480
    // specifying the product UID together. The goal is to only piggyback
481
    // user agent data when the host is initializing the Notecard, as opposed
482
    // to every time the host does a `hub.set` to change mode.
483
#ifndef NOTE_DISABLE_USER_AGENT
484
    if (!JIsPresent(req, "body") && JContainsString(req, (reqFound ? "req" : "cmd"), "hub.set") && JIsPresent(req, "product")) {
47!
485
        J *body = NoteUserAgent();
3✔
486
        if (body != NULL) {
3✔
487
            JAddItemToObject(req, "body", body);
2✔
488
            NOTE_C_LOG_DEBUG("Added user-agent to request");
2✔
489
        } else {
490
            NOTE_C_LOG_ERROR(ERRSTR("Failed to add user-agent to request", c_mem));
1✔
491
        }
492
    }
493
#endif
494

495
    // Calculate the transaction timeout based on the parameters in the request.
496
    const uint32_t transactionTimeoutMs = _noteTransaction_calculateTimeoutMs(req, reqFound);
47✔
497

498
#ifndef NOTE_C_LOW_MEM
499
    /*
500
    * Add a CRC value, so the request may be retried if it is received
501
    * in a corrupted state.
502
    *
503
    * NOTE: This can only performed on requests, because commands do not have a
504
    *       'response channel'. As such, we have no ability to understand if a
505
    *       command failed and should be retried. A sequence number is included
506
    *       as part of the CRC data, so that two identical but separate requests
507
    *       are not mistaken as the same request being retried.
508
    *
509
    *   req   cmd  response
510
    *  found found expected
511
    *  ----- ----- --------
512
    *    0     0    ERROR
513
    *    0     1      0
514
    *    1     0      1
515
    *    1     1    1 (UB)
516
    */
517
    bool crcAddedToRequest = false;
47✔
518
    if (reqFound) {
47✔
519
        char *newJson = _crcAdd(json, seqNo);
34✔
520
        if (newJson != NULL) {
34✔
521
            _Free(json);
33✔
522
            json = newJson;
33✔
523
            crcAddedToRequest = true;
33✔
524
        }
525
    }
526
#endif // !NOTE_C_LOW_MEM
527

528
    // Take the lock on the Notecard.  This is required to ensure that we don't
529
    // have multiple threads trying to access the Notecard at the same time.
530
    if (lockNotecard) {
47✔
531
        _LockNote();
45✔
532
    }
533

534
    // If a reset of the I/O interface is required for any reason, do it now.
535
    if (resetRequired) {
47✔
536
        NOTE_C_LOG_DEBUG("Resetting Notecard I/O Interface...");
20✔
537
        if ((resetRequired = !_Reset())) {
20✔
538
            if (lockNotecard) {
2!
539
                _UnlockNote();
2✔
540
            }
541
            _Free(json);
2✔
542
            _TransactionStop();
2✔
543
            const char *errStr = ERRSTR("failed to reset Notecard interface {io}", c_iobad);
2✔
544
            if (cmdFound) {
2✔
545
                NOTE_C_LOG_ERROR(errStr);
1✔
546
                return NULL;
1✔
547
            }
548
            return _errDoc(id, errStr);
1✔
549
        }
550
    }
551

552
    // If we're performing retries, this is where we come back to
553
    // after a failed transaction.
554
    const char *errStr = NULL;
45✔
555
    char *rspJsonStr = NULL;
45✔
556
    J *rsp = NULL;
45✔
557
    bool isHeartbeat = false;
45✔
558
    for (uint8_t lastRequestRetries = 0; lastRequestRetries <= CARD_REQUEST_RETRIES_ALLOWED; ++lastRequestRetries) {
153✔
559
        // free on retry
560
        if (rsp != NULL) {
137✔
561
            JDelete(rsp);
17✔
562
        }
563

564
        // reset variables
565
        errStr = NULL;
137✔
566
        rspJsonStr = NULL;
137✔
567
        rsp = NULL;
137✔
568

569
        // Trace request unless suppressed
570
        if (!isHeartbeat && suppressShowTransactions == 0) {
137✔
571
            NOTE_C_LOG_INFO(json);
124✔
572
        }
573

574
        // In-place replacement of NULL-terminator with a newline character.
575
        // The Notecard expects a newline-terminated string to understand the
576
        // end of the request.
577
        const size_t jsonLen = strlen(json);
137✔
578
        json[jsonLen] = '\n';
137✔
579

580
        size_t jsonTxLen;
581
        if (isHeartbeat) {
137✔
582
            // Heartbeat responses have no request
583
            jsonTxLen = 0;
12✔
584
        } else {
585
            jsonTxLen = (jsonLen + 1);
125✔
586
        }
587

588
        // Perform the transaction
589
        if (cmdFound) {
137✔
590
            errStr = _Transaction(json, jsonTxLen, NULL, transactionTimeoutMs);
12✔
591
            // break;  // No response expected for commands and no ability to retry.
592
        } else {
593
            errStr = _Transaction(json, jsonTxLen, &rspJsonStr, transactionTimeoutMs);
125✔
594
        }
595

596
        // Restore NULL-terminator
597
        json[jsonLen] = '\0';
137✔
598

599
        ////////////////////////
600
        // Request retry logic
601
        ////////////////////////
602

603
        // Handle transaction errors
604
        if (errStr != NULL) {
137✔
605
            _Free(rspJsonStr);
7✔
606
            // If there's an I/O error on the transaction, retry
607
            if (NoteErrorContains(errStr, c_ioerr)) {
7✔
608
                NOTE_C_LOG_WARN(ERRSTR("retrying... transaction failure", c_iobad));
6✔
609
                resetRequired = !_Reset();
6✔
610
                _DelayMs(RETRY_DELAY_MS);
6✔
611
                continue;  // I/O error, retry
6✔
612
            } else {
613
                NOTE_C_LOG_DEBUG(ERRSTR("transaction failure", c_bad));
1✔
614
                break;  // Fatal error, do not retry
1✔
615
            }
616
        } else if (cmdFound) {
130✔
617
            NOTE_C_LOG_DEBUG("Command successfully sent to Notecard");
12✔
618
            break;  // No response expected and no further ability to retry.
12✔
619
        }
620

621
        // Inspect the Notecard Response
622
        if (rspJsonStr == NULL) {
118✔
623
            // If the response is NULL, then we have a timeout or other error
624
            errStr = ERRSTR("response expected, but response is NULL {io}", c_ioerr);
54✔
625
            NOTE_C_LOG_WARN(ERRSTR("retrying... no response", c_iobad));
54✔
626
            _DelayMs(RETRY_DELAY_MS);
54✔
627
            continue;  // I/O error, retry
54✔
628
        }
629

630
#ifndef NOTE_C_LOW_MEM
631
        // If we sent a CRC in the request, examine the response JSON to see if
632
        // it has a CRC error.  Note that the CRC is stripped from the
633
        // rspJsonStr as a side-effect of this method.
634
        if (crcAddedToRequest && _crcError(rspJsonStr, seqNo)) {
64✔
635
            _Free(rspJsonStr);
6✔
636
            errStr = ERRSTR("CRC error {io}", c_iobad);
6✔
637
            NOTE_C_LOG_WARN(ERRSTR("retrying... CRC error", c_iobad));
6✔
638
            _DelayMs(RETRY_DELAY_MS);
6✔
639
            continue;
6✔
640
        }
641
#endif // !NOTE_C_LOW_MEM
642

643
        // Error types
644
        bool isBadBin = false;
58✔
645
        bool isIoError = false;
58✔
646
        isHeartbeat = false;
58✔
647

648
        // Error detection / classification
649
        rsp = JParse(rspJsonStr);
58✔
650
        if (rsp != NULL) {
58✔
651
            isBadBin = JContainsString(rsp, c_err, c_badbinerr);
34✔
652
            isIoError = JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported);
34!
653
            isHeartbeat = JContainsString(rsp, c_err, c_heartbeat);
34✔
654
        } else {
655
            // Failed to parse response as JSON
656
            isIoError = true;
24✔
657
#ifndef NOTE_C_LOW_MEM
658
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, "[ERROR] ");
24✔
659
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, "invalid JSON {io}: ");
24✔
660
            _DebugWithLevel(NOTE_C_LOG_LEVEL_ERROR, rspJsonStr);
24✔
661
#else
662
            NOTE_C_LOG_ERROR(c_ioerr);
663
#endif // !NOTE_C_LOW_MEM
664
        }
665

666
        // Error handling
667
        if (isHeartbeat) {
58✔
668
            // Heartbeat responses are not traditional errors, log and resume waiting
669
            _Free(rspJsonStr);
12✔
670
            const char * const status = JGetString(rsp, c_status);
12✔
671
            NOTE_C_LOG_DEBUG(ERRSTR(status, c_heartbeat));
12✔
672
#ifdef NOTE_C_HEARTBEAT_CALLBACK
673
            if (_noteHeartbeat(status)) {
674
                errStr = ERRSTR("host abandoned transaction {heartbeat}", c_heartbeat);
675
                NoteResetRequired();
676
                break;
677
            }
678
#else
679
            (void)status; // avoid unused variable warning when NOTE_C_LOW_MEM defined
680
#endif
681
            --lastRequestRetries; // Heartbeats do not count against retry limit
12✔
682
            continue;
12✔
683
        } else if (isIoError || isBadBin) {
46✔
684
            if (rsp != NULL) {
32✔
685
                NOTE_C_LOG_ERROR(JGetString(rsp, c_err));
8✔
686
            }
687
            if (isBadBin) {
32✔
688
                NOTE_C_LOG_DEBUG("{bad-bin} errors not eligible for retry");
2✔
689
                break;
2✔
690
            } else {
691
                _Free(rspJsonStr);
30✔
692
                errStr = ERRSTR("corrupt response {io}", c_ioerr);
30✔
693
                NOTE_C_LOG_WARN(ERRSTR("retrying... corrupt response", c_iobad));
30✔
694
                _DelayMs(RETRY_DELAY_MS);
30✔
695
                continue;
30✔
696
            }
697
        }
698

699
        // Transaction completed
700
        break;
14✔
701
    } // end of retry loop
702

703
    // Free the original serialized JSON request
704
    _Free(json);
45✔
705

706
#ifndef NOTE_C_LOW_MEM
707
    // Request processing complete, regardless of success or error.
708
    // Now, advance the request sequence number.
709
    seqNo++;
45✔
710
#endif // !NOTE_C_LOW_MEM
711

712
    // Return an empty object (with no err field) when no response is expected
713
    if (cmdFound) {
45✔
714
        if (lockNotecard) {
12!
715
            _UnlockNote();
12✔
716
        }
717
        _TransactionStop();
12✔
718
        return JCreateObject();
12✔
719
    }
720

721
    // Handle error condition
722
    if (errStr != NULL) {
33✔
723
        if (rsp != NULL) {
17✔
724
            JDelete(rsp);
1✔
725
            rsp = NULL;
1✔
726
        }
727
        NoteResetRequired(); // queue up a reset
17✔
728
        J *errRsp = _errDoc(id, errStr);
17✔
729
        if (lockNotecard) {
17!
730
            _UnlockNote();
17✔
731
        }
732
        _TransactionStop();
17✔
733
        return errRsp;
17✔
734
    }
735

736
    // Log and discard the response JSON
737
    if (suppressShowTransactions == 0) {
16✔
738
        NOTE_C_LOG_INFO(rspJsonStr);
15✔
739
    }
740
    _Free(rspJsonStr);
16✔
741

742
    // Release the Notecard lock
743
    if (lockNotecard) {
16✔
744
        _UnlockNote();
14✔
745
    }
746

747
    // Inform the Notecard that the transaction is complete.
748
    // This allows the Notecard (ESP) to drop into low power mode.
749
    _TransactionStop();
16✔
750

751
    // Done
752
    return rsp;
16✔
753
}
754

755
/*!
756
 @brief Mark that a reset will be required before doing further I/O on a given
757
        port.
758
 */
759
void NoteResetRequired(void)
22✔
760
{
761
    resetRequired = true;
22✔
762
}
22✔
763

NEW
764
bool NoteConnect(uint32_t timeoutSeconds)
×
765
{
NEW
766
    bool result = false;
×
767

NEW
768
    for (
×
NEW
769
        uint32_t startMs = _GetMs(), timeoutMs = (timeoutSeconds * 1000) ;
×
NEW
770
        (result = NoteReset()) && ((_GetMs() - startMs) < timeoutMs) ;
×
NEW
771
        _DelayMs(RETRY_DELAY_MS)
×
772
    );
773

NEW
774
    return result;
×
775
}
776

777
bool NoteReset(void)
5✔
778
{
779
    _LockNote();
5✔
780
    resetRequired = !_Reset();
5✔
781
    _UnlockNote();
5✔
782
    return !resetRequired;
5✔
783
}
784

785
bool NoteErrorContains(const char *errstr, const char *errtype)
20✔
786
{
787
    return (strstr(errstr, errtype) != NULL);
20✔
788
}
789

790
void NoteErrorClean(char *errbuf)
5✔
791
{
792
    while (true) {
5✔
793
        char *end = &errbuf[strlen(errbuf)+1];
10✔
794
        char *beginBrace = strchr(errbuf, '{');
10✔
795
        if (beginBrace == NULL) {
10✔
796
            break;
4✔
797
        }
798
        if (beginBrace>errbuf && *(beginBrace-1) == ' ') {
6✔
799
            beginBrace--;
4✔
800
        }
801
        char *endBrace = strchr(beginBrace, '}');
6✔
802
        if (endBrace == NULL) {
6✔
803
            break;
1✔
804
        }
805
        char *afterBrace = endBrace + 1;
5✔
806
        memmove(beginBrace, afterBrace, end-afterBrace);
5✔
807
    }
808
}
5✔
809

810
#ifndef NOTE_C_LOW_MEM
811

812
/*!
813
 @brief Convert a hex string to a 64-bit unsigned integer.
814

815
 @param p The hex string to convert.
816
 @param maxLen The length of the hex string.
817

818
 @returns The converted number.
819
 */
820
uint64_t _n_atoh(char *p, int maxLen)
6✔
821
{
822
    uint64_t n = 0;
6✔
823
    char *ep = p+maxLen;
6✔
824

825
    while (p < ep) {
42✔
826
        char ch = *p++;
36✔
827
        bool digit = (ch >= '0' && ch <= '9');
36!
828
        bool lcase = (ch >= 'a' && ch <= 'f');
36!
829
        bool space = (ch == ' ');
36✔
830
        bool ucase = (ch >= 'A' && ch <= 'F');
36!
831
        if (!digit && !lcase && !space && !ucase) {
36!
832
            break;
×
833
        }
834
        n *= 16;
36✔
835
        if (digit) {
36✔
836
            n += ch - '0';
20✔
837
        } else if (lcase) {
16!
838
            n += 10 + (ch - 'a');
×
839
        } else if (ucase) {
16!
840
            n += 10 + (ch - 'A');
16✔
841
        }
842
    }
843

844
    return (n);
6✔
845
}
846

847
static uint32_t lut[16] = {
848
    0x00000000, 0x1DB71064, 0x3B6E20C8, 0x26D930AC, 0x76DC4190, 0x6B6B51F4,
849
    0x4DB26158, 0x5005713C, 0xEDB88320, 0xF00F9344, 0xD6D6A3E8, 0xCB61B38C,
850
    0x9B64C2B0, 0x86D3D2D4, 0xA00AE278, 0xBDBDF21C
851
};
852

853
/*!
854
 @brief Compute the CRC32 of the passed in buffer.
855

856
 Small lookup-table half-byte CRC32 algorithm. See
857
 https://create.stephan-brumme.com/crc32/#half-byte
858

859
 @param data The buffer.
860
 @param length The length of the buffer.
861

862
 @returns The CRC32 of the buffer.
863
 */
864
NOTE_C_STATIC int32_t _crc32(const void* data, size_t length)
15✔
865
{
866
    uint32_t previousCrc32 = 0;
15✔
867
    uint32_t crc = ~previousCrc32;
15✔
868
    unsigned char* current = (unsigned char*) data;
15✔
869

870
    while (length--) {
430✔
871
        crc = lut[(crc ^  *current      ) & 0x0F] ^ (crc >> 4);
415✔
872
        crc = lut[(crc ^ (*current >> 4)) & 0x0F] ^ (crc >> 4);
415✔
873
        current++;
415✔
874
    }
875

876
    return ~crc;
15✔
877
}
878

879
/*!
880
 @brief Append a "crc" field to the passed in JSON buffer.
881

882
 The "crc" field has a value of the form "SSSS:CCCCCCCC", where SSSS is the
883
 passed in sequence number and CCCCCCCC is the CRC32.
884

885
 @param json The JSON buffer to both add the CRC32 to and to compute the
886
        CRC32 over.
887
 @param seqno A 16-bit sequence number to include as a part of the CRC.
888

889
 @returns A dynamically-allocated version of the passed in buffer with the CRC
890
          field added or NULL on error.
891
 */
892
NOTE_C_STATIC char *_crcAdd(char *json, uint16_t seqno)
15✔
893
{
894

895
    // Allocate a block the size of the input json plus the size of
896
    // the field to be added.  Note that the input JSON ends in '"}' and
897
    // this will be replaced with a combination of 4 hex digits of the
898
    // seqno plus 8 hex digits of the CRC32, and the '}' will be
899
    // transformed into ',"crc":"SSSS:CCCCCCCC"}' where SSSS is the
900
    // seqno and CCCCCCCC is the CRC32.  Note that the comma is
901
    // replaced with a space if the input json doesn't contain
902
    // any fields, so that we always return compliant JSON.
903

904
    size_t jsonLen = strlen(json);
15✔
905

906
    // Minimum JSON is "{}" and must end with a closing "}".
907
    if (jsonLen < 2 || json[jsonLen-1] != '}') {
15✔
908
        return NULL;
2✔
909
    }
910
    char *newJson = (char *) _Malloc(jsonLen+CRC_FIELD_LENGTH+1);
13✔
911
    if (newJson == NULL) {
13✔
912
        return NULL;
1✔
913
    }
914

915
    bool isEmptyObject = (memchr(json, ':', jsonLen) == NULL);
12✔
916
    size_t newJsonLen = jsonLen-1;
12✔
917

918
    memcpy(newJson, json, newJsonLen);
12✔
919
    newJson[newJsonLen++] = (isEmptyObject ? ' ' : ',');    // Replace }
12✔
920
    newJson[newJsonLen++] = '"';                            // +1
12✔
921
    newJson[newJsonLen++] = 'c';                            // +2
12✔
922
    newJson[newJsonLen++] = 'r';                            // +3
12✔
923
    newJson[newJsonLen++] = 'c';                            // +4
12✔
924
    newJson[newJsonLen++] = '"';                            // +5
12✔
925
    newJson[newJsonLen++] = ':';                            // +6
12✔
926
    newJson[newJsonLen++] = '"';                            // +7
12✔
927
    _n_htoa16(seqno, (uint8_t *) &newJson[newJsonLen]);
12✔
928
    newJsonLen += 4;                                        // +11
12✔
929
    newJson[newJsonLen++] = ':';                            // +12
12✔
930
    _n_htoa32(_crc32(json, jsonLen), &newJson[newJsonLen]);
12✔
931
    newJsonLen += 8;                                        // +20
12✔
932
    newJson[newJsonLen++] = '"';                            // +21
12✔
933
    newJson[newJsonLen++] = '}';                            // +22 == CRC_FIELD_LENGTH
12✔
934
    newJson[newJsonLen] = '\0';                             // null-terminated as it came in
12✔
935

936
    return newJson;
12✔
937
}
938

939
/*!
940
 @brief Check the passed in JSON for CRC and sequence number errors.
941

942
 If the calculated CRC32 doesn't match what's in the buffer, that's an error. If
943
 the sequence number in the buffer doesn't match shouldBeSeqno, that's an error.
944
 If there is no CRC field in the buffer, that's not an error UNLESS this
945
 function has been given a buffer with a CRC field before. In this case, the
946
 lack of a CRC field is an error.
947

948
 @param json The JSON buffer for which the CRC should be checked. Note that the
949
        CRC is stripped from the input JSON regardless of whether or not there
950
        was an error.
951
 @param shouldBeSeqno The expected sequence number.
952

953
 @returns `true` if there's an error and `false` otherwise.
954

955
 @note The CRC is stripped from the input JSON regardless of whether or not
956
        there was an error.
957
 */
958
NOTE_C_STATIC bool _crcError(char *json, uint16_t shouldBeSeqno)
14✔
959
{
960
    // Trim whitespace (specifically "\r\n") for CRC calculation
961
    size_t jsonLen = strlen(json);
14✔
962
    while (jsonLen > 0 && json[jsonLen - 1] <= ' ') {
16✔
963
        jsonLen--;
2✔
964
    }
965

966
    // Ignore CRC check if the response contains an error
967
    // A valid JSON string begins with "{". Therefore, the presence of an error
968
    // message in a well-formed JSON string MUST result in a non-zero response.
969
    if (strstr(json, ERR_FIELD_NAME_TEST)) {
14✔
970
        return false;
2✔
971
    }
972

973
    // Skip if invalid JSON or is too short to contain a CRC parameter
974
    // Minimum length is "{}" (2 bytes) + CRC_FIELD_LENGTH
975
    // Valid JSON ends with a closing "}"
976
    if ((jsonLen < (CRC_FIELD_LENGTH + 2)) || (json[jsonLen - 1] != '}')) {
12✔
977
        return false;
7✔
978
    }
979

980
    // See if it has a compliant CRC field
981
    size_t fieldOffset = ((jsonLen - 1) - CRC_FIELD_LENGTH);
5✔
982
    if (memcmp(&json[fieldOffset + CRC_FIELD_NAME_OFFSET], CRC_FIELD_NAME_TEST, ((sizeof(CRC_FIELD_NAME_TEST) - 1)) != 0)) {
5✔
983
        // If we've seen a CRC before, we should see one every time
984
        return notecardFirmwareSupportsCrc;
2✔
985
    }
986

987
    // If we get here, we've seen at least one CRC from the Notecard, so we should expect it.
988
    notecardFirmwareSupportsCrc = true;
3✔
989

990
    // Extract the CRC field and the sequence number
991
    char *p = &json[fieldOffset + CRC_FIELD_NAME_OFFSET + (sizeof(CRC_FIELD_NAME_TEST) - 1)];
3✔
992
    uint16_t actualSeqno = (uint16_t) _n_atoh(p, 4);
3✔
993
    uint32_t actualCrc32 = (uint32_t) _n_atoh(p+5, 8);
3✔
994
    json[fieldOffset++] = '}';
3✔
995
    json[fieldOffset] = '\0';
3✔
996
    uint32_t shouldBeCrc32 = _crc32(json, fieldOffset);
3✔
997

998
    return (shouldBeSeqno != actualSeqno || shouldBeCrc32 != actualCrc32);
3✔
999
}
1000

1001
#endif // !NOTE_C_LOW_MEM
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc