Writing Sparrow Applications
At the highest level, the Sparrow application firmware can be conceptualized as being mostly asleep, with a scheduled interval of activity. During the active interval, the application firmware will be polled by the underlying firmware at a given frequency. This polling is invoked by an internal scheduler/state machine and operates through a series of callbacks.
Architecture
Layer Diagram
|- app -|- app -|- app -|
|------ scheduler ------|
|-- application host ---|------- gateway -------|
|------------------ framework ------------------|
|---------------- util & timer -----------------|
|-------------------- core ---------------------|
|--------------------- HAL ---------------------|
Files Critical to Abstraction:
Application/config_notecard.h
Provides
#define
variables that can be used to force override Notecard specific settings.Application/Framework/sched.h
Declares application callback signatures and the scheduled application configuration structure,
schedAppConfig
.Application/Framework/sched.c
Contains application array, implements the Sparrow task scheduler functions, and performs application callbacks (”Application Host”).
Application/Gateway/auth.c
Used by the gateway to receive/process Notes by adding additional information before forwarding to the Notecard.
Application/Sensor/init.c
Implements the weakly linked
schedAppInit()
function, which loads the example applications into the “Application Host”.
Application Host
The Application Host tracks the state of each application running in the system, and invokes the Scheduler to schedule work for each of those applications.
Application Model
An application predominantly exists in two main states, activated and deactivated. The application model is designed around low power, and as a result, the application is expected to spend the vast majority of its time deactivated. The application will be activated on a parameterized period, and when active, the “run loop” will execute at a parameterized polling period. As such, the two main levers are activationPeriodSecs
and pollPeriodSecs
.
An application “runs” inside its callbacks activateFn()
, interruptFn()
, pollFn()
, and responseFn()
. The closest approximation to a run loop would be the intermittent calls to pollFn()
at pollIntervalSecs
. The state of the application will be passed in as a parameter to pollFn()
.
Scheduler
The scheduler cycles through the states of each application hosted by the application host.
When an application is booted, the first call to pollFn()
will enter the initialization state, STATE_ONCE
. After being initialized, the application will be an active state, STATE_ACTIVATED
. While active, the polling function is called at the specified interval.
Unless additional states are supplied, an application will typically cycle through STATE_ACTIVATED
, STATE_SENDING_REQUEST
, and STATE_RECEIVING_RESPONSE
. The application will remain active until it sends a message using the noteSendToGatewayAsync()
function, or until it manually requests deactivation by setting the scheduler's state to STATE_DEACTIVATED
.
Application Host States
State Transition APIs
Additional states can be submitted to the Application Host (scheduler), via the following APIs.
API | Takes Effect |
---|---|
| immediately |
| immediately (ISR safe) |
| upon subsequent call to |
System States
The system states used by the state machine are represented by the following constants, which are all negative integers:
STATE_UNDEFINED
STATE_ONCE
(initialization)STATE_ACTIVATED
STATE_DEACTIVATED
STATE_SENDING_REQUEST
STATE_RECEIVING_RESPONSE
User-defined States
User-defined states are used to extend the Application Host system states for application specific purposes. These custom states are passed into the caller as a parameter to the polling callback function.
User-defined application states MUST be integers >= 0.
WARNING: Negative numbers are RESERVED for the system states and CANNOT be used for user-defined, application states.
Application
Application Registration
To register an application, you must provide the schedAppConfig
to the schedAppInit()
function in the framework. The standard pattern is to make an initialization function (e.g. myAppInit()
) that can be called from init.c
.
The result returned from the call to schedAppInit()
is an application identifier.
NOTE: In order to interact with the Scheduler, you must know your application ID,
appID
. This can and should be captured when you first register your application with the scheduler. However, theappID
is also provided as a form of context with each callback invocation.
schedAppConfig
// App Configuration definition
typedef struct {
// General
const char *name;
// How often we get activated
uint32_t activationPeriodSecs;
// While app is active, how often it's polled
uint32_t pollIntervalSecs;
// Handlers
schedActivateFunc activateFn;
schedInterruptFunc interruptFn;
schedPollFunc pollFn;
schedResponseFunc responseFn;
// Application Context
void *appContext;
} schedAppConfig;
Name Field
name
You can name your sensor anything you like. This name typically appears in system level logs, describing framework interactions with your application.
Timing Fields
activationPeriodSecs
The activation period is the time that passes between activations. Each time the scheduler awakens an application from it's deactivated state, it will invoke the
.activateFn
function. The primary concern addressed by the activation period is power management. The longer the activation period, the more time spent sleeping and better battery performance a Sparrow node can achieve.pollIntervalSecs
The polling function is only called when the application is in an active state, and the poll interval is frequency at which the
.pollFn
function is invoked. At a high level, the polling function should be thought of as a "check-in" from the underlying state machine to the main control body of the application logic. Ideally, you would not request the poll function to be invoked faster than the rate of change associated with whatever external factors for which the application has been designed (i.e. there is no reason to pay for the poll unless it is able to capture or respond to some change in state).
Callbacks
.activateFn
-typedef bool (*schedActivateFunc) (int appID);
Called on activation; you may return
false
to cancel any given activation.WARNING: This method must NOT send messages to the gateway; only local operations are allowed.
.interruptFn
-typedef void (*schedInterruptFunc) (int appID, uint16_t pins);
A shared ISR that is called for ANY interrupt on ALL applications; the
pins
parameter indicatesexti
lines (https://wiki.st.com/stm32mcu/wiki/EXTI_feature_overview ) that changed. Due to the shared nature of the pin, you must filter to the pin you are expecting to handle in your application.Example: Filtering on the
PAIR
buttonif (!(pins & BUTTON1_Pin)) { return; }
.pollFn
-typedef void (*schedPollFunc) (int appID, int state);
Called repeatedly while activated. This function implements the application's state machine, where negative states are reserved. Only when this function sends a message using the
noteSendToGatewayAsync()
function, or manually submitsSTATE_DEACTIVATE
to the scheduler, will the application will be deactivated..responseFn
-typedef void (*schedResponseFunc) (int appID, J *rsp);
Called after an application sent a Notecard request and is asynchronously receiving a reply. This will be called when a response comes back or when it times out; if timeout the
rsp
field will beNULL
.
Callback Timing
Here is a diagram to illustrate the callback timing of a typical Sparrow application that was expecting a response from the Notecard.
Application Context
appContext
Application context exists in two forms, which can work in tandem.
static
global variables (singleton model)
When application context is supplied as static global variables, then it is available to everything in the containing .c
file. This is suitable for most single purpose applications (e.g. an interface to specific hardware, performs a unique operation, etc.). To use a static global variable, you only need to define a variable as static
in the global space of your application’s .c
file.appContext
- portable struct (multiple instances)
However, there are several instances where a portable struct can facilitate code reuse and enable higher level abstractions (e.g. using one source file to interface to an array of identical sensors, enabling a context based language, etc.). To use a portable struct, you would define it in your application’s .c
file, allocate it from the heap in an initialization function, and provide a pointer to the appContext
field of the application configuration struct, schedAppConfig
.
Console Logging
Console logging can be performed via the APP_PRINTF()
function.
WARNING: The maximum number of characters is 90.
Dynamic Queue File Naming
Applications can take advantage of dynamic queue file naming, to create node and/or application specific queue files. For example, *#myapp.qo
will evaluate to 012345678901234567890123#myapp.qo
. The Gateway will automatically transform the *
into the node id associated with the originating Sparrow board.
*
is a special character that will be replaced with the Sparrow node's ID by the Gateway.
Note Submission
Notes are submitted to the Gateway using the following API:
void noteSendToGatewayAsync(J *req, bool responseExpected)
The function has two parameters. The first is the JSON representation of the Note you would like to submit to the Notecard attached to the Sparrow Gateway. The second indicates whether or not a response is requested.
Invoking this API causes a cascade of changes to be made to the applications runtime.
The current state is send to
STATE_SENDING_REQUEST
and if you indicated you were expecting a response, the state will change toSTATE_RECEIVING_RESPONSE
after sending the request.
However, both the success and failure states are set toSTATE_DEACTIVATED
, which means whether your application succeeds or fails at it’s attempt to send the Note (or receive a response), it will ultimately become deactivated. If you wish to alter the success and failure states, thenschedSetCompletionState()
can be called promptly afternoteSendToGatewayAsync()
has returned.If a response has been requested, then the application will continue running - blocking the main thread of execution. This, in turn, prevents other applications from running.
Note Tracking
If you have requested a response, then an arbitrary number can be added to the id
tag of the Note, which allows you to match the response with the original request. When "id"
is supplied to a Note, then the response (which comes as the rsp
parameter of diagResponse()
callback) will contain a matching "id"
tag.
NOTE: Responses are expected to be used sparingly, as they work against the spirit of the typical LoRa communications model as well as the core of the Sparrow application design pattern. Responses can be an invaluable tool during the early development of a Sparrow application, but they should be removed (unless critical to the application) before an application is considered ready for production.
GPIO Special Interactions/Behaviors
Analog Pins
To enable the analog pins on the header rail (i.e. A1
, A2
, A3
), the macro Ax_ENABLE
where x
corresponds to the desired analog pin (e.g. A1_ENABLE
).
Digital Pins
RED
- This pin is required to enable the evaluation of several internal variables and values. As such, this pin is in use by the system outside thepollFn
related to your application. The implication is that this pin cannot be used as an interrupt or other function that would require it to operate in a continuous fashion.
Integrating a Custom Sensor into CMake
To add an application to the Sparrow firmware, you will be able to do everything from the sparrow-application
folder by following these steps:
Create a new folder for your application in the
sparrow-application
folder.Create an application module (
.c
/.h
files) in your new folder.Multiple working examples exist, each in their own folder, listed under the
sparrow-application
folder.Update
sparrow-application/CMakeLists.txt
to build your source file (.c
) and include your header (.h
)Each of the samples is also built by this
CMakeLists.txt
file.
Collecting Logs
Use an STLINK-V3MINI to connect to your device, you can use a terminal emulator to view the debugging output on the serial port that appears on your computer.
STLINK-V3MINI
Connect your computer to STLINK-V3MINI using a USB A-to-Micro, then connect the STLINK-V3MINI to your device using the Cortex debug connector.
Sparrow Rail Pinout (v1.1)
Pin # | Pin Name | Description |
| Pin # | Pin Name | Description |
---|---|---|---|---|---|---|
NRST | RST# | RESET Button |
| PA7 | MOSI | Main Out / Secondary In |
PH3-BOOT0 | BOOT | BOOT Button |
| PA6 | MISO | Main In / Secondary Out |
VSS_EP | GND | Ground |
| PA5 | SCK | SPI Clock |
PA2 | LPTX | Low-Power UART Transmit |
| PA4 | CS | Chip Select |
PA3 | LPRX | Low-Power UART Receive |
| PA11 | SDA | I2C Data |
PB2 | A1 | Analog Pin 1 |
| PA12 | SCL | I2C Clock |
PA10 | A2 | Analog Pin 2 |
| PA1 | BLUE | Blue LED |
PA15 | A3 | Analog Pin 3 |
| PA0 | RED | Red LED |
PA13 | SWDIO | Single-Wire Debug I/O |
| PB12 | GREEN | Green LED |
PA14 | SWCLK | Single-Wire Debug Clock |
| PB6 | RX | UART Receive |
PC13 | BTN# | PAIR Button |
| PB7 | TX | UART Transmit |
VDD | <VIO | Logic-level Voltage |
| -- | VBAT | Direct Battery Voltage |