Application Introduction
The Diagnostic Application is designed to help new developers to the Sparrow Application ecosystem understand and trace the behaviors of an application. The Diagnostic application uses all features (albeit trivially) to demonstrate their usage, and generates logs with calling parameters for each callback it enters. This gives the developer a feel for the rhythm and flow of a Sparrow Application.
Expected Behavior
The Diagnostic application has a period of 10 minutes, which means it will wake from “sleep” every 10 minutes to perform work. While awake, it has a polling interval of 5 seconds, which means it will invoke the polling callback every 5 seconds. When a Sparrow running the Diagnostic application is connected to a terminal, it will create an output log as it enters each callback.
The Diagnostic will create a template, register it with the Notecard attached to the Sparrow Gateway and parse the response. Once it has successfully registered a template, the application then performs “real work” by making system calls to collect the runtime memory usage of the Sparrow Runtime, including all applications.
Output to Notehub
The Diagnostic application will generate a Note with the following details. The Note will appear in a Notefile prefixed with the device id (e.g. 012345678901234567890123
) and suffixed with #diag.qo
.
"body": { "mem.alloc.bytes":928, "mem.free.bytes":960, "mem.heap.bytes":53488, "voltage":3.306 }
This Note can be viewed on Notehub.io, by looking for the project associated with the Notecard attached to the Sparrow Gateway.
Logging Output
The logs generated by the Diagnostic application are verbose by design.
In the logs below, you can see the application send a request to register a template (8
), receive a response that the registration was successful (43
), generate a diagnostic report (46
), and finally receive a response of success (73
).
More importantly, you can see the application progress through the callbacks:
diagActivate
- (1
)diagPoll
- (4
,6
,16
,20
,31
,38
)diagResponse
- (35
)diagPoll
- (38
,49
,53
,65
)diagResponse
- (69
)diagPoll
- (72
)
diag: Entered application callback function: diagActivate appId: 3 diagnostic activated with 600s activation period and 5s poll interval diag: Entered application callback function: diagPoll appId: 3 state: ONCE diag: Entered application callback function: diagPoll appId: 3 state: ACTIVATED diag: template registration request 240036 note.template diagnostic now SENDING_REQUEST 240036 to gateway:5 sensor sending request (152) 240036 to gateway:5 sending ( (152/152) at txp:-17 240036 to gateway:5 waiting 2s to transmit (slot 0s-20s in 20s window) diagnostic state will be set to 1 on success, or 0 on error sched: sleeping 3s (next transmit window in 1s) diag: Entered application callback function: diagPoll appId: 3 state: SENDING_REQUEST sched: sleeping 5s 240036 to gateway:5 waiting for message from gateway diag: Entered application callback function: diagPoll appId: 3 state: SENDING_REQUEST sched: sleeping 4s (next transmit window in 12s) 240036 fm gateway:5 ack received ATP: buffered sample 2 ATP: rssi/snr:-34/7 txp:-17 ATP: | [3] - - - - - - - - - - - - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - diagnostic now RECEIVING_RESPONSE (waiting for response) 240036 fm gateway:5 waiting for response from gateway 240036 fm gateway:5 now receiving response from gateway 240036 fm gateway:5 sending ACK (0/0) at txp:-17 diag: Entered application callback function: diagPoll appId: 3 state: RECEIVING_RESPONSE sched: sleeping 4s (next transmit window in 8s) diagnostic now 1 (response completed) diag: Entered application callback function: diagResponse appId: 3 rsp: {"id":2,"bytes":32} diag: SUCCESSFUL template registration diag: Entered application callback function: diagPoll appId: 3 state: STATE_DIAG_CHECK diag: generating diagnostic report 240036 note.add diagnostic now SENDING_REQUEST 240036 to gateway:6 sensor sending request (159) 240036 to gateway:6 sending ( (159/159) at txp:-17 240036 to gateway:6 waiting 3s to transmit (slot 0s-20s in 20s window) diagnostic state will be set to 1 on success, or 0 on error diag: note request sent sched: sleeping 4s (next transmit window in 3s) diag: Entered application callback function: diagPoll appId: 3 state: SENDING_REQUEST sched: sleeping 4s 240036 to gateway:6 waiting for message from gateway diag: Entered application callback function: diagPoll appId: 3 state: SENDING_REQUEST sched: sleeping 4s (next transmit window in 13s) 240036 fm gateway:6 ack received ATP: buffered sample 3 ATP: would decrease power but it's already bottomed-out at -17db ATP: rssi/snr:-44/5 avg:-37/6 txp:-17 ATP: | [5] - - - - - - - - - - - - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - diagnostic now RECEIVING_RESPONSE (waiting for response) 240036 fm gateway:6 waiting for response from gateway 240036 fm gateway:6 now receiving response from gateway 240036 fm gateway:6 sending ACK (0/0) at txp:-17 diag: Entered application callback function: diagPoll appId: 3 state: RECEIVING_RESPONSE sched: sleeping 4s (next transmit window in 8s) diagnostic now 1 (response completed) diag: Entered application callback function: diagResponse appId: 3 rsp: {"template":true} diag: SUCCESSFUL Note submission diag: Entered application callback function: diagPoll appId: 3 state: STATE_DIAG_CHECK diagnostic now DEACTIVATED (diag: completed) diagnostic deactivated
Implementation Details
Defines
Custom Application States
#define STATE_DIAG_ABORT 0 #define STATE_DIAG_CHECK 1 #define STATE_DIAG_ISR_XFER 2
STATE_DIAG_ABORT
- This state is reserved for application specific error conditions, and will allow for graceful deactivation of the application.STATE_DIAG_CHECK
- When this state is received by the polling callback and the application has not be marked for deactivation, it will generate a diagnostic report, send the results to the Gateway Notecard, and mark itself ready for deactivation. Otherwise, if this state is received by the polling callback and the application has already been marked for deactivation then it will log a completion message and immediately deactivate.STATE_DIAG_ISR_XFER
- This state is set from within the ISR, and it allows the polling callback to know the interrupt was invoked.
Application Specific Values
#define REQUESTID_DIAGNOTE 9171979 #define REQUESTID_TEMPLATE 19790917
REQUESTID_DIAGNOTE
- An arbitrary number to match a diagnostic Note response and request.REQUESTID_TEMPLATE
- An arbitrary number to match the template response and request.
#define APPLICATION_NOTEFILE "*#diag.qo"
APPLICATION_NOTEFILE
- The dynamic filename of the application specific queue.NOTE: The Gateway will replace
*
with the originating node's ID. The resulting transformation will resemble012345678901234567890123#diag.qo
.
Array and Counter Limits
#define ISR_MAX_CALL_RETENTION 8 // Must be a power of 2 #define ISR_COUNTER_MASK ~(ISR_MAX_CALL_RETENTION-1)
ISR_MAX_CALL_RETENTION
- The maximum number of ISRs that will be recorded.ISR_COUNTER_MASK
- The mask will limit the value of the counter.
Structures
isrParameters
- Since anAPP_PRINT()
statement cannot be used from within an interrupt service routine, this structure is used to collect the calling parameters of the ISR for later processing.typedef struct isrParameters { int appID; uint16_t pins; } isrParameters;
appID
- The application ID passed to the ISR callbackpins
- The pin(s) prompting the ISR invocation.
applicationContext
- A rather trivial example of application context, but never the less ALL the application state is stored in a single structure which is shared between callbacks.typedef struct applicationContext { // ISR call ring-buffer volatile size_t isrCount; volatile bool isrOverflow; isrParameters isrParams[ISR_MAX_CALL_RETENTION]; // Application status bool templateRegistered; // Only `true` once we've successfully registered the template bool done; } applicationContext;
isrCount
- The number of times the ISR was invoked.isrOverflow
- Indicates whether the ISR was invoked more thanISR_MAX_CALL_RETENTION
times.isrParams
- An array dedicated to recording the calling parameters of each ISR invocation.templateRegistered
- Provides a mechanism for the polling callback to only attempt to register a template once.done
- Provides a mechanism for the polling callback to indicate when a note has been submitted to the Notecard, so it will know when to deactivate itself.
Global Static Variables (a.k.a. Singleton Application Specific Context)
none
Callbacks
bool diagActivate(int appID, void *appContext)
// Load Application Context applicationContext *ctx = appContext;
This line casts the context, so it can be used in subsequent operations.
APP_PRINTF("diag: Entered application callback function: diagActivate\r\n\tappId: %d\r\n", appID);
This line logs the entry into the function along with the calling parameters.
ctx->done = false;
The function resets the
done
state, so the application will progress until it has generated a Note or has failed during execution.// Success return true;
The function returns true, so the application will progress to the polling callback
void diagISR(int appID, uint16_t pins, void *appContext);
// Load Application Context applicationContext *ctx = appContext;
This line casts the context, so it can be used in subsequent operations.
/* * This callback function is executed directly from the ISR. * Only perform ISR sensitive operations and exit quickly. */ ctx->isrParams[ctx->isrCount].appID = appID; ctx->isrParams[ctx->isrCount].pins = pins; ctx->isrCount++; ctx->isrCount = (ISR_COUNTER_MASK & ctx->isrCount); ctx->isrOverflow = (ctx->isrOverflow || !ctx->isrCount);
The function executes inside an actual ISR, so there is no time to perform expensive operations (like logging). Therefore, it collects the calling parameters and updates metadata. This is necessary to track how many times the interrupt has been called, so it may log the details outside the ISR.
if ((pins & BUTTON1_Pin) && !schedIsActive(appID)) { schedActivateNowFromISR(appID, true, STATE_DIAG_ISR_XFER); }
These lines filter to ensure the ISR only responds to the
PAIR
button (BUTTON1_Pin
), and checks with the scheduler that the application is not already in an active state. If these conditions are met, then it will request the scheduler to schedule the application for immediate activation instead of respecting the activation period.
The logs generated by the ISR look like this:diag: ISR callback function called <5> times. diag: call 0: appId: 3 pins: 128 diag: call 1: appId: 3 pins: 128 diag: call 2: appId: 3 pins: 8192 diag: call 3: appId: 3 pins: 128 diag: call 4: appId: 3 pins: 128
Since the parameter collection is performed outside the filter, the ISR callback is invoked multiple times. However, an ISR is only significant to this application when
pin
equals 8192 (which is 0x2000 orBUTTON1_Pin
). As you can see, there are other invocations wherepin
equals 128. Those invocations are related to the PIR application interrupts, and are ignored by this application.void diagPoll(int appID, int state, void *appContext);
// Load Application Context applicationContext *ctx = appContext;
This line casts the context, so it can be used in subsequent operations.
APP_PRINTF("diag: Entered application callback function: diagPoll\r\n\tappId: %d\tstate: %s\r\n", appID, diagStateName(state));
This line logs the entry into the function along with the calling parameters.
case STATE_ACTIVATED: if (!ctx->templateRegistered) { registerNotefileTemplate(); schedSetCompletionState(appID, STATE_DIAG_CHECK, STATE_DIAG_ABORT); } else { schedSetState(appID, STATE_DIAG_CHECK, "diag: process diagnostics"); } break;
This case handles the newly activated state. It will first check to see if a template has been registered in a previous invocation of the polling callback.
If a template has not been registered, then it will attempt to do so. If the request is delivered successfully, then the state will be changed to the next state in the progression, to process the diagnostic values. Otherwise, if the message fails to be delivered, then the state will be set to the ABORT state.
If a template has already been registered on a previous invocation, then the state will be changed to the next state, to process diagnostic values.case STATE_DIAG_ISR_XFER: APP_PRINTF("diag: Transfered from application ISR callback function.\r\n"); APP_PRINTF("diag: ISR callback function called %s <%d> times.\r\n", (ctx->isrOverflow ? "more than" : ""), (ctx->isrOverflow ? ISR_MAX_CALL_RETENTION : ctx->isrCount)); if (ctx->isrOverflow) { ctx->isrCount = ISR_MAX_CALL_RETENTION; } for (size_t i = 0 ; i < ctx->isrCount ; ++i) { APP_PRINTF("diag: call %d:\tappId: %d\tpins: %d\r\n", i, ctx->isrParams[i].appID, ctx->isrParams[i].pins); } resetIsrValues(ctx); // fall through and report diagnostics
This case handles a transfer from the interrupt routine. It will print the invocation, along with the parameters, of of each recorded ISR, up to
ISR_MAX_CALL_RETENTION
times. Once it has exhausted it’s cache, it will reset all values related to the ISR. Instead of exiting, it will fall through into the next case…case STATE_DIAG_CHECK: if (ctx->done) { schedSetState(appID, STATE_DEACTIVATED, "diag: completed successfully"); break; } schedSetCompletionState(appID, STATE_DIAG_CHECK, STATE_DIAG_ABORT); addNote(true); break;
This case handles the main objective of the application, the collection of diagnostics and sending a Note to the Notehub. It will check for the
done
flag, which is only set once a Note has been successfully received by the Notecard. Ifdone
is set, then the application will log a success message and request to be deactivated. If thedone
flag has not been set, thenschedSetCompletionState
is called to instruct the scheduler what to do when the request to add a Note either succeeds or fails. As you can see above, if the request is successful, then it will return to this state, but thedone
flag will be true, and the app will request deactivation. However, if it were to fail, then the application will proceed to the ABORT state. Finally, the request to add a Note is made.case STATE_DIAG_ABORT: schedSetState(appID, STATE_DEACTIVATED, "diag: aborted due to failure!"); break;
This case is reserved for fail cases encountered during the application processing. It request for the application to be deactivated and generates a log indicating the failure.
void diagResponse(int appID, J *rsp, void *appContext);
// Load Application Context applicationContext *ctx = appContext;
This line casts the context, so it can be used in subsequent operations.
APP_PRINTF("diag: Entered application callback function: diagResponse\r\n\tappId: %d", appID); char *json_string = JConvertToJSONString(rsp); APP_PRINTF("\trsp: %s\r\n", json_string); free(json_string);
These lines log the entry into the function along with the calling parameters. Special accommodation is made for the JSON response.
APP_PRINTF
is limited to 90 characters, so two calls are required to reliably log the parameters.// See if there's an error char *err = JGetString(rsp, "err"); if (err[0] != '\0') { APP_PRINTF("diag: app error response: %d\r\n", err); schedSetState(appID, STATE_DIAG_ABORT, "diag: aborting..."); return; }
These lines will peek inside the response JSON to see if an error message is present. If so, the error message will be logged and the application will proceed to the ABORT state.
// Flash the LED if this is a response to this specific ping request switch (JGetInt(rsp, "id")) { case REQUESTID_DIAGNOTE: ctx->done = true; APP_PRINTF("diag: SUCCESSFUL Note submission\r\n"); break; case REQUESTID_TEMPLATE: ctx->templateRegistered = true; APP_PRINTF("diag: SUCCESSFUL template registration\r\n"); break; default: APP_PRINTF("diag: received unexpected response\r\n"); }
This switch statement is designed to handle responses to successful requests. The switch operates on the unique identifiers given to each request. If the ID of a diagnostic Note is encountered, the
done
flag is set and a success log is generated. If the ID associated with a template request is encountered, then the flag indicating the template was successfully recorded is set and a log is generated. If neither of these IDs are present, then a log is generated indicating the response was not recognized.
Other Functions
External
bool diagInit(void);
APP_PRINTF("diag: Initializing application...\r\n");
This line logs the entry into the function.
bool result = false;
This line initializes the result of the function to false, which means “Failed to init”.
// Allocate and initialize application context applicationContext *ctx = (applicationContext *)malloc(sizeof(applicationContext)); ctx->templateRegistered = false; ctx->done = false; resetIsrValues(ctx);
These lines allocate and initialize the values of the portable application context.
// Register the application schedAppConfig config = { .name = "diagnostic", .activationPeriodSecs = 60 * 10, .pollPeriodSecs = 5, .activateFn = diagActivate, .interruptFn = diagISR, .pollFn = diagPoll, .responseFn = diagResponse, .appContext = ctx, };
These lines initialize the application configuration that will be passed to the scheduler.
if (schedRegisterApp(&config) < 0) { // Failure result = false; } else { // Success result = true; }
These lines will attempt to register the application with the scheduler. On success, an application identifier greater than or equal to zero is returned, otherwise a negative value is returned to indicate registration failure. For basic usage, it is unnecessary to capture the application identifier, because it is returned as the first parameter of each of the callbacks registered by the application.
return result;
This line returns the result to the caller.
Internal
static void addNote(bool immediate);
static const char * diagStateName (int state);
switch (state) { case STATE_DIAG_ABORT: return "STATE_DIAG_ABORT"; case STATE_DIAG_CHECK: return "STATE_DIAG_CHECK"; case STATE_DIAG_ISR_XFER: return "STATE_DIAG_ISR_XFER"; default: { static char undefined_state[20]; schedStateName(state, undefined_state, sizeof(undefined_state)); return undefined_state; } }
The function translates the
int
value of an application specific state into aconst char *
description of the state. In the default case, where a state is unrecognized, it will defer to the analogous system API, which will either transcribe the state or simply print the integer's string representation.static bool registerNotefileTemplate();
static inline void resetIsrValues(applicationContext *ctx);
APP_PRINTF("diag: resetting ISR values\r\n");
This line logs the entry into the function.
ctx->isrCount = 0; ctx->isrOverflow = false; for (size_t i = 0 ; i < ISR_MAX_CALL_RETENTION ; ++i) { ctx->isrParams[i].appID = 0; ctx->isrParams[i].pins = 0; }
These lines will reset all metadata and parameters related to the ISR.