This library provides a simple-to-use framework for taking advantage of many of the features of the MCCI Catena® Arduino products.
Apologies: This document is a work in progress, and is published in this intermediate form in hopes that it will still be better than nothing.
McciCatena
Catena
and header file Catena.h
McciCatena::cDate
cTimer
Timer object
Catena_functional.h
cDownload
cBootloaderApi
cSerial
In order to assist people who are not everyday readers and writer of C++, this library adopts some rules.
All names are in the McciCatena
namespace.
In classes with elaborate hierarchy, we normally define a private synonym of Super
which refers to the parent class. This is done so that we can change parent/child relationships without breaking code.
We tend to use the m_...
prefix on the names of class member fields.
We tend to use this->m_...
to refer to class members (rather than omitting this->
). We do this for emphasis, and to avoid visual ambiguity.
We tend to name classes starting with a lower-case letter c
, i.e., cClassName
. For the Catena...
classes, we don't follow this rule, however.
We don't use most of the standard C++ library (because of the frequent use of exceptions), nor do we use exceptions in our own code. The exception framework tends to be inefficient, and it's a source of coding problems because the error paths are not directly visible.
However, we do take advantage of some of the C++-11 header files, such as <functional>
, <type_traits>
, and <cstdint>
. (Sometimes we have to do extra work for this.)
McciCatena
Unless otherwise specified, all symbols are defined inside namespace McciCatena
. Usually sketches begin with something like this:
#include <Catena.h>
//... other includes
using namespace McciCatena;
Catena
and header file Catena.h
Catena.h
is the main header file for the library. It uses the #defines
injected by board.txt
and platform.txt
from the Arduino environment to create a class named Catena
derived from the Catena...
class that is specific to the board for which the software is being built. This allows examples to be source-compatible, no matter which Catena is our target.
Catena.h
defines the class Catena
in terms on one of the following classes based on the setting of the BSP:
The known classes and header files are:
Class | Header File | Description |
---|---|---|
Catena4410 |
Catena4410.h |
First generation MCCI systems with BME180 |
Catena4420 |
Catena4420.h |
Feather M0 Bluetooth + LoRa Radio Wing |
Catena4450 |
Catena4450.h |
MCCI Catena 4450 |
Catena4460 |
Catena4460.h |
MCCI Catena 4460 |
Catena4470 |
Catena4470.h |
MCCI Catena 4470 |
Catena4551 |
Catena4551.h |
MCCI Catena 4551 first-generation Murata-based board. |
Catena4610 |
Catena4610.h |
MCCI Catena 4610 second-generation Murata-based board with LiPo charging |
Catena4611 |
Catena4611.h |
MCCI Catena 4611 second-generation Murata-based board with fixed Vdd, no charging |
Catena4612 |
Catena4612.h |
MCCI Catena 4612 second-generation Murata-based board with variable Vdd, no charging. |
Catena4617 |
Catena4617.h |
MCCI Catena 4617 second-generation Murata-based board with variable Vdd, no charging |
Catena4618 |
Catena4618.h |
MCCI Catena 4618 second-generation Murata-based board with variable Vdd, no charging. |
Catena4630 |
Catena4630.h |
MCCI Catena 4630 Murata-based board with Air Quality Sensor. |
Catena4801 |
Catena4801.h |
MCCI Catena 4801 Murata-based board with Modbus. |
Catena4802 |
Catena4802.h |
MCCI Catena 4802 Murata-based board with Modbus and Temperature sensor. |
The following figures gives the class derivation hierarchy for the board classes.
The tree is too big to show in one diagram here. So we split according to the two families: STM32-based CPUs, and SAMD-based CPUS.
The first figure just gives relationships; the second has details about the members of each class.
Catena STM32 Class Relationships:
Catena STM32 Class Hierarchy (full detail):
The first figure just gives relationships; the second has details about the members of each class.
Catena SAMD Class Relationships:
Catena SAMD Class Hierarchy (full detail):
The hardware supported by this platform is generally similar. The architecture allows for the following kinds of variation (as outlined in the class hierarchy):
Items 1-3 are to some degree known at compile time, based on the Catena model chosen. However, it's inconvenient to update the BSP for every possible modification, so we allow some variation at run time, guided by the contents of FRAM.
The system is identified to the software by a platform object, of type CATENA_PLATFORM
. Several platform objects are built into the firmware image, based on the known variations for component population and external sensors. The appropriate platform object is located at boot time by the Catena Arduino Platform framework. Some values representing possibly variation are stored as PlatformFlags
in the CATENA_PLATFORM
. This variable is of typePLATFORM_FLAGS
.
Each CATENA_PLATFORM
has a unique identification. This is a 128-bit binary number called a GUID (or UUID), generated by MCCI during the system design process. The platform GUIDs are defined in the header file Catena_Guids.h
. For convenience, here's a table of the known GUIDs.
All of these names begin with the string GUID_HW_
, so we omit that from the tables below.
The M101 and M102 designations are used by the Catena-Sketches family of applications to determine what external sensors are available. This lets them avoid trying to poll external hardware unless the appropriate platform configuration is set. The well known configurations are:
The designations "M103" and "M104" are reserved for use by MCCI.
For boards with FRAM, the appropriate platform GUID should be selected and programmed into FRAM using the command system configure platformguid
, followed by the GUID value. For boards without FRAM, the library has provisions for tying the GUID to the CPU serial number. Contact MCCI for details.
The tables below were generated from Catena_Guids.h
using a script, and then hand annotated. The script is a one-line shell command using awk
:
awk '/^[/][/]/ {
s = $2; gsub(/[{}]/, "", s);
getline;
g = $2; gsub(/\(f\)$/, "", g);
gsub(/^GUID_HW_/, "", g);
printf("`%s` | `%s` |\n", g, tolower(s));
}' src/Catena_Guids.h | LC_ALL=C sort
The Catena 4610 uses a LiPo battery like traditional Feathers, and includes a BME280 temperature/pressure/humidity sensor, and a Si1133 light sensor.
Name | GUID | Description |
---|---|---|
CATENA_4610_BASE |
53ca094b-b888-465e-aa0e-e3064ec56d21 |
Base Catena 4610, assuming no modifications or customizations. |
CATENA_4610_M101 |
6a5d8d0c-d5ae-4143-adc7-8f84ec56a867 |
Catena 4610 M101, configured for power monitoring and other pulse-input applications. |
CATENA_4610_M102 |
18252b1c-3c0d-403e-8012-224d96c5af06 |
Catena 4610 M102, configured for environmental monitoring |
CATENA_4610_M103 |
c2cf6cf4-a4c3-4611-941f-6955ffa5bfdc |
Catena 4610 M103 -- contact MCCI |
CATENA_4610_M104 |
bfed4740-a58a-4ef6-933a-09cb22e93d00 |
Catena 4610 M104 -- contact MCCI |
The 4611 uses a boost regulator that is either on or fully off, controlled by the enable pin. It's therefore a hybrid between the 4610 (which uses a battery charger switch controlled by the enable pin), and the 4612 (which instead uses the switch to jump from raw Vbat to regulated 3.3V). The 4611 is available by special order from MCCI. Generally, MCCI uses the 4612 instead.
Name | GUID | Description |
---|---|---|
CATENA_4611_BASE |
9bb29dca-0685-4837-8182-3dfa309d279f |
Base Catena 4611, assuming no modifications or customizations. |
CATENA_4611_M101 |
4e995471-1570-4767-adae-6657ef871bcd |
Catena 4611 M101, configured for power monitoring and other pulse-input applications. |
CATENA_4611_M102 |
964bcf91-9c45-4386-a6e7-5f2d7c3641ef |
Catena 4611 M102, configured for environmental monitoring. |
CATENA_4611_M103 |
c85b27cb-7cf9-4025-92bb-2009c08449e5 |
Catena 4611 M103 -- contact MCCI |
CATENA_4611_M104 |
c22be8af-e693-4319-b243-1c2d10197973 |
Catena 4611 M103 -- contact MCCI |
The 4612 runs off an unregulated battery supply, with the option of a boost regulator that can bring the system voltage up to 3.3V.
Name | GUID | Description |
---|---|---|
CATENA_4612_BASE |
915decfa-d156-4d4f-bac5-70e7724726d8 |
Base Catena 4612, assuming no modifications or customizations. |
CATENA_4612_M101 |
d210a354-c49a-4c4f-856a-4b545dcfaa20 |
Catena 4612 M101, configured for power monitoring and other pulse-input applications. |
CATENA_4612_M102 |
7fa9709d-17af-463e-ae7f-8210e49acd7a |
Catena 4612 M102, configured for environmental monitoring. |
CATENA_4612_M103 |
ff8b2ac6-75cd-4ed3-980b-50b209e64551 |
Catena 4612 M103 -- contact MCCI |
CATENA_4612_M104 |
dea48489-cdac-43f4-b8ad-edb08ce21546 |
Catena 4612 M103 -- contact MCCI |
The Catena 4617 is a variant of the Catena 4612 with a IDT HS3001 temperature/humidity sensor in place of the Bosch BME280.
Name | GUID | Description |
---|---|---|
CATENA_4617_BASE |
6767c2f6-d5d5-43f4-81af-db0d4d08815a |
Base Catena 4617, assuming no modifications or customizations. |
The Catena 4618 is a variant of the Catena 4612 with a Sensirion SHT31-DIS-F temperature/humidity sensor in place of the Bosch BME280.
Name | GUID | Description |
---|---|---|
CATENA_4618_BASE |
b75ed77b-b06e-4b26-a968-9c15f222dfb2 |
Base Catena 4618, assuming no modifications or customizations. |
The Catena 4630 is a variant of the Catena 4610. It deletes the Si1131 light sensor, and adds an IDT ZMOD4410 atmospheric gas sensor, plus a connection for an external Plantower PMS7003 PM2.5/dust sensor.
Name | GUID | Description |
---|---|---|
CATENA_4630_BASE |
17281c12-d78a-4e4f-9c42-c8bbc5499c91 |
Base Catena 4618, assuming no modifications or customizations. |
The 4450 Feather Wing includes a BME280 temperature/humidity/pressure sensor, and a BH1750 lux sensor.
Name | GUID | Description |
---|---|---|
CATENA_4450_BASE |
60480acb-dc5d-4148-b6c9-aca13449cf1d |
Base Catena 4450, assuming no modifications or customizations. |
CATENA_4450_M101 |
82bf2661-70cb-45ae-b620-caf695478bc1 |
Catena 4450 M101, configured for power monitoring and other pulse-input applications. |
CATENA_4450_M102 |
2281255e-ac5c-48cb-a263-9dc890d16638 |
Catena 4450 M102, configured for environmental monitoring. |
CATENA_4450_M103 |
1fb2506f-0f2a-4310-9e6a-9bc191e0ae12 |
Catena 4450 M103 -- contact MCCI |
CATENA_4450_M104 |
a731f637-e3ed-4088-a9a8-f54b6671dcf6 |
Catena 4450 M103 -- contact MCCI |
The 4460 Feather Wing includes a BME680 air quality sensor, and a BH1750 lux sensor.
Name | GUID | Description |
---|---|---|
CATENA_4460_BASE |
3037d9be-8ebe-4ae7-970e-91915a2484f8 |
Base Catena 4460, assuming no modifications or customizations. |
CATENA_4460_M101 |
31e563d1-0267-43fc-bca0-9a4cb5bfc55a |
Catena 4460 M101, configured for power monitoring and other pulse-input applications. |
CATENA_4460_M102 |
494f3c17-8ac1-4f80-8ecc-ca4dd3dccbdc |
Catena 4460 M102, configured for environmental monitoring. |
CATENA_4460_M103 |
a882186f-f4ab-4ee4-9402-7b628a76d886 |
Catena 4460 M103 -- contact MCCI |
CATENA_4460_M104 |
398a9e5a-e22f-4265-9d35-bf45433ddbe3 |
Catena 4460 M103 -- contact MCCI |
The 4470 Feather Wing includes a BME280 temperature/humidity/pressure sensor, a BH1750 lux sensor, and a Modbus/RS-485 interface connected to Serial1
.
Name | GUID | Description |
---|---|---|
CATENA_4470_BASE |
ea8568ec-5dae-46ee-929a-a3f6b00a565e |
Base Catena 4470, assuming no modifications or customizations. |
CATENA_4470_M101 |
dd0a37a6-e469-43ec-b173-fed795129455 |
Catena 4470 M101, configured for power monitoring and other pulse-input applications. |
Name | GUID | Description |
---|---|---|
CATENA_4801_BASE |
10ea7e25-a4a4-45fd-8959-c04a6a5d7f95 |
Base Catena 4801, assuming no modifications or customizations. |
Name | GUID | Description |
---|---|---|
CATENA_4802_BASE |
daaf345e-b5d5-4a32-a303-3ac70b81d260 |
Base Catena 4802, assuming no modifications or customizations. |
MCCI also uses this library with Feather M0s without MCCI hardware. These GUIDs are useful in that situation.
Name | GUID | Description |
---|---|---|
FEATHER_M0_LORA_TTNNYC |
a67ad93c-551a-47d2-9adb-e249b4cf915a |
Feather M0 LoRa, modified per The Things Network NYC standards -- DIO1 connected to D6. |
FEATHER_M0_LORA |
e2deccc8-55fa-4bd3-94c3-ce66bcd0baac |
Feather M0 LoRa, but DIO1 connection is not known. |
FEATHER_M0_PROTO_WINGLORA_TTNMCCI |
3bab150f-6e32-4459-a2b6-72aced75059f |
Feather M0 Proto with a separate LoRa Feather Wing. This is sometimes known as an MCCI Catena 4420. |
FEATHER_M0_PROTO |
f6a15678-c7f3-43f4-ac57-67ef5cf75541 |
A Feather M0 Proto. |
FEATHER_M0 |
2e6dfed4-f577-47d5-9137-b3e63976ae92 |
Some unspecified member of the Feather M0 family. |
When composing software from components, it's inconvenient and bug-prone to have to manually edit the Arduino loop()
function to poll each component.
To compensate, the Catena platform defines a framework for polling, and allows components to register to be polled.
The foundation of the framework is cPollableInterface
, an abstract class. Any object inheriting from cPollableInterface
will provide a poll()
method; this provides a standard way to poll an object.
Pollable objects are managed via a central instance of cPollingEngine
, which works on objects that are derived from cPollableObject
. You create your pollable class by arranging for it to inherit from cPollableObject
; then at run time you arrange to register and normally created by arranging for them to inherit from cPollableObject
. This adds a few tracking fields to your class, and makes them available to the cPollingEngine
when you register the object with the polling engine.
The abstract relationships are shown below.
Let's say that UserClass1
exists and has the following definition.
class UserClass1 : public ParentClass {
public:
// constructor
UserClass1() {}
void begin();
};
To make UserClass1
pollable, you change the class declaration as follows:
Change UserClass1
to inherit from McciCatena::cPollableObject
, using multiple inheritance if needed. List McciCatena::cPollableObject
last. No need to make it public
.
Declare a new public virtual method void poll()
. We recommend that you use the override
keyword to make it clear that this is an override for a method declared in the parent class.
You also, of course, must supply an implementation of UserClass::poll()
.
Here's an example of the transformed UserClass1
:
class UserClass1 : public ParentClass, McciCatena::cPollableObject {
public:
// constructor
UserClass1() {}
void begin();
// <<Pollable>> requirements:
virtual void poll() override;
};
Each instance of your pollable class (in our example, each instance of type UserClass1
) must be registered with a polling engine. The most convenient polling engine to use is the once provided by the CatenaBase
class, which is normally instantiated as gCatena
. Simply call gCatena.registerObject()
to register your object with the Catena polling engine. So for example:
#include <Catena.h>
// create the gCatena instance.
McciCatena::Catena gCatena;
// create an instance of my object.
UserClass1 myUserObject;
void setup() {
// conventionally, we call gCatena.begin() first:
gCatena.begin();
// now register the object.
gCatena.registerObject(&myUserObject);
}
void loop() {
// poll all the objects registered with gCatena.
gCatena.poll();
// do any other work...
}
If you're not using the full Catena
class framework, you still can use polling; just declare your own cPollingEngine
instance. For example:
#include <Arduino.h>
#include <Catena_PollableInterface.h>
// create the polling engine instance.
McciCatena::cPollingEngine gPollingEngine;
// create an instance of my object.
UserClass1 myUserObject;
void setup() {
// conventionally, we call gPollingEngine.begin() first:
gPollingEngine.begin();
// now register the object.
gPollingEngine.registerObject(&myUserObject);
}
void loop() {
// poll all the objects registered with gCatena.
gPollingEngine.poll();
// do any other work...
}
Finite state machines are very useful when implementing non-blocking asynchronous programs, or modeling external hardware that changes state independently of the system.
We've ported an implementation of MCCI's standard FSM approach. It's good for Mealy or Moore designs, and is intended to be easy to implement and maintain. This version is C++ oriented; it assumes that each FSM instance is associated with a C++ class object (the "parent" object). The work of implementing the FSM is divided between the parent object and the FSM object. The FSM object does all the bookkeeping and handling of corner conditions; the parent object provides a method function, which the FSM calls whenever it seems appropriate to do so.
Here's what you need:
enum class
type with all of your states, and a couple of distinguished states. This type is referred to as TState
; andclass
type (referred to as TParent
), containing...TParent::someName(TState currentState, bool fEntry) -> TState
.In addition to the states relevant to your problem, TState
must have three distinct values with well-known names.
TState::stNoChange
does not correspond to any state. Instead, it's the value returned by the dispatch routine when the state is not to be changed. (Returning the current state will cause the FSM to transition from the current state to the current state.)
TState::stInitial
is the initial state of your FSM.
TState::stFinal
is the final state of your FSM. Once the FSM reaches this state, it will remain there until another call to fsm.init()
is made.
We'll use the coin-operated turnstile example from Wikipedia to make things more concrete.
The author usually starts by drawing a diagram, labeled with the states and transitions.
For example, here's the turnstile state diagram:
enum class
Define an enum class
as follows:
enum class MyStateEnum {
stNoChange = 0, // this name must be present: indicates "no change of state"
stInitial, // this name must be presnt: it's the starting state.
// use-case-specific states
// ...
stFinal, // this name must be present, it's the terminal state.
};
For example, for the turnstile diagram:
enum class State {
stNoChange = 0, // this name must be present: indicates "no change of state"
stInitial, // this name must be presnt: it's the starting state.
stLocked,
stUnlocked,
stFinal, // this name must be present, it's the terminal state.
};
This means finding the class name for the class that is going to contain this FSM. One class can contain many FSMs, but each FSM class has only one parent class.
For our example, we'll say that the class modeling turnstiles is Turnstile
.
Add the type you defined above to the parent class. For example:
class Turnstile {
// states for FSM
enum class State {
stNoChange = 0, // this name must be present: indicates "no change of state"
stInitial, // this name must be presnt: it's the starting state.
stLocked,
stUnlocked,
stFinal, // this name must be present, it's the terminal state.
};
};
Add an FSM instance as a member of the parent class. It's up to you whether to make it public
, private
, or protected
.
class Turnstile {
// states for FSM
enum class State {
stNoChange = 0, // this name must be present: indicates "no change of state"
stInitial, // this name must be presnt: it's the starting state.
stLocked,
stUnlocked,
stFinal, // this name must be present, it's the terminal state.
};
// the FSM instance
McciCatena::cFSM<Turnstile, State> m_fsm;
};
Finally, we have to declare a method function that the FSM can call. Extending the turnstile example again:
class Turnstile {
// states for FSM
enum class State {
stNoChange = 0, // this name must be present: indicates "no change of state"
stInitial, // this name must be presnt: it's the starting state.
stLocked,
stUnlocked,
stFinal, // this name must be present, it's the terminal state.
};
// the FSM instance
McciCatena::cFSM<Turnstile, State> m_fsm;
// the FSM dispatch function called by this->m_fsm.
State fsmDispatch(State currentState, bool fEntry);
};
Your FSM dispatch function will look like this.
void Turnstile::fsmDispatch(Turnstile::State currentState, bool fEntry) {
State newState = State::stNoChange;
switch (currentState) {
case State::stInitial:
if (fEntry) {
// entry is not considered in this state, always move on.
}
digitalWrite(LOCK, 1);
pinMode(LOCK, OUTPUT);
newState = State::stLocked;
break;
case State::stLocked:
if (fEntry) {
digitalWrite(LOCK, 1);
}
if (this->m_evShutdown) {
this->m_evShutdown = false;
newState = State::stFinal;
} else if (this->m_evCoin) {
this->m_evCoin = false;
newState = State::stUnlocked;
} else if (this->m_evPush) {
this->m_evPush = false;
// stay in this state.
} else {
// stay in this state.
}
break;
case State::stUnlocked:
if (fEntry) {
digitalWrite(LOCK, 0);
}
if (this->m_evShutdown) {
this->m_evShutdown = false;
newState = State::stFinal;
} else if (this->m_evCoin) {
this->m_evCoin = false;
// stay in this state.
} else if (this->m_evPush) {
this->m_evPush = false;
newState = State::stLocked;
} else {
// stay in this state.
}
break;
case State::stFinal:
// by policy, we idle with the turnstile locked.
digitalWrite(LOCK, 1);
// stay in this state.
break;
default:
// the default means unknown state.
// transition to locket.
newState = State::stLocked;
break;
}
return newState;
}
Somewhere in your initialization for Turnstile
, add the following code. For example, if Turnstile
has a Turnstile::begin()
method, you could write:
void Turnstile::begin() {
// other init code...
// set up FSM
this->m_fsm.init(*this, fsmDispatch);
// remaining init code...
}
McciCatena::cDate
When logging data, we frequently need to keep time on a scale that is correlated with other devices. Although the Arduino environment provides interval times based on the seconds()
, millis()
and micros()
APIs, there's no built-in concept of calendar time. The cDate
class provides calendar time objects and the ability to perform conversions between calendar time and interval time. The cDate
object is also an important component for clock drivers.
#include <Catena_Date.h>
// allocate a date object, initially invalid
McciCatena::cDate myDate;
It's common to compare intervals and transmit timestamps using a simple up-counter. Traditional Posix systems count seconds since 1970-01-01 00:00:00Z; GPS systems count seconds since 1980-01-06 00:00:00Z. These base times are commonly called "epochs". Times can be (theoretically) in the past (negative) or future (positive) relative to the epoch. For a variety of reasons, we call times based on the Posix epoch "Common times"; we call times based on the GPS epoch "GPS times". The types CommonTime_t
and GpsTime_t
are used to record times in common and GPS times. Both are of type std::int64_t
. Both have a range of designated values that are valid; this range is chosen to allow any valid CommonTime_t
to be converted to GpsTime_t
and vice versa.
constexpr cDate::CommonTime_t cDate::kCommonTimeInvalid;
constexpr cDate::CommonTime_t cDate::kGpsTimeInvalid;
constexpr bool cDate::isCommonTimeValid(CommonTime_t);
constexpr bool cDate::isGpsTimeValid(GpsTime_t);
constexpr cDate::CommonTime_t cDate::getCommonTime(GpsTime_t);
constexpr cDate::GpsTime_t cDate::getGpsTime(CommonTime_t);
Many numerical values of std::int64_t
are not valid times. The library uses kCommonInvalidTime
and kGpsInvalidTime
when it needs to create an invalid time, but it (and clients of the library) should use isCommonTimeValid()
or isGpsTimeValid()
to check whether a given time is in fact valid.
getCommonTime()
and getGpsTime()
convert valid times between the two systems, handling invalid cases.
cDate
calendar typesThe types cDate::Year_t
, cDate::Month_t
, cDate::Day_t
, cDate::Hour_t
, cDate::Minute_t
, cDate::Second_t
are used to represent years (from 0 to 65535), months (from 1 to 12), days (from 1 to 28, 29, 30, or 31, depending on the month and year), hours (0 to 23), minutes (0 to 59), and seconds (0 to 59). The year zero corresponds to ISO-8601 year zero. We use, technically speaking, a proleptic Gregorian calendar with astronomical year numbering (i.e., year zero).
cDate
propertiesbool cDate::isValid() const;
This function returns true
if the entries in the cDate
object are valid, false
otherwise.
cDate::Year_t cDate::year() const;
cDate::Monty_t cDate::month() const;
cDate::Day_t cDate::day() const;
cDate::Hour_t cDate::hour() const;
cDate::Minute_t cDate::minute() const;
cDate::Second_t cDate::second() const;
These functions return the various fields of the date.
cDate::CommonTime_t cDate::getCommonTime() const;
cDate::GpsTime_t cDate::getGpsTime() const;
These functions return the CommonTime_t
or GpsTime_t
equivalent of the date object.
cDate
methodsbool cDate::setDate(Year_t y, Month_t m, Day_t d);
Set the date portion of the cDate
instance (only if a valid date is passed). Return true
if and only if the date was updated.
bool cDate::setTime(Hour_t h, Minute_t m, Second_t s);
Set the time portion of the cDate
instance (only if a valid time is passed). Return true
if and only if the time was updated. Time is set in zone UTC+0
.
bool cDate::setCommonTime(CommonTime_t commonTime);
bool cDate::setGpsTime(GpsTime_t gpsTime);
Set the date and time of the cDate
instance from the common or GPS time stamp. Returns false
if the incoming timestamp is invalid, or if the specified time is out of range.
This section is provided for background, and can be skipped if you're not interested in the theory behind the implementation.
Timekeeping is a thorny topic for scientific investigations, because one day is not exactly 86,400 seconds long. Obviously, the difference between two instants, measured in seconds, is independent of calendar system, but converting the time of each instant into ISO date and time is not independent of the calendar. Worse is that computing systems (e.g. POSIX-based systems) focus more on easy, deterministic conversion from a time serial number to UTC time, and so assume that there are exactly 86400 seconds/day. In UTC time, the solar calendar date is paramount; leap-seconds are inserted or deleted as needed to keep UTC mean solar noon aligned with astronomical mean solar noon.
In effect, the computer observes a sequence of seconds. We need to correlate them to calendar time, and we need to interpret know the interval between instances. Let's call the sequence of seconds interval time, as opposed to calendar time.
Let's also define an important property of sequences of seconds -- "interval-preserving" sequences are those in which, if T1 and T2 are interval second numbers, (T2 - T1) is equal to the number of ITU seconds between the times T1 and T2.
Real-time calendar clocks typically measure intervals using a mixed-radix system (year/month/day hour:minute:second). This looks much like UTC calendar time, but in fact doesn't include leap seconds, and is a pure interval counter (with inconvenient arithmetic).
There are (at least) three ways of relating interval time to calendar time.
Steve Allen's website has a number of good discussions, including:
The onboard real-time clocks provided by various Catena platforms count intervals in "calendar" time, and are set by people (again in "calendar" time) from watches etc. that run from UTC or a derivative. We avoid the additional complication of local time zones by assuming that the user will use GMT (UTC+0, or "Zulu" time) We will assume that the user can input the time in Zulu time and that the battery-backed RTC is recording time in Zulu time.
Therefore, we use a timescale that simply states that days have 86,400 seconds. In effect, we choose option 1 above. In our applications, we think that this will be good enough. If we ever start to use LoRaWAN ("GPS") time, we assume that the network will be able to send us the information needed to convert to calendar time as needed. This may add a little complication but it's future complication and we'll deal with all this if the need arises.
The Catena Arduino Platform includes C++ wrappers for LoRaWAN support, based on the MCCI version of the Arduino LMIC library and MCCI's Arduino LoRaWAN library. It includes command processing from the Serial
console for run-time (not compile-time) provisioning, and uses the non-volatile storage provided by the Catena FRAM to store connection parameters and uplink/downlink counts.
The Catena::LoRaWAN
class is derived from the Arduino_LoRaWAN
class defined by <Arduino_LoRaWAN.h>
.
The example catena_hello_lora.ino
is a complete working example.
To use LoRaWAN in a sketch, do the following.
Instantiate the global Catena object, with the name gCatena
.
#include <Catena.h>
using namespace McciCatena; // to save typing
Catena gCatena; // instantiate the Catena platform object.
Instantiate the global LoRaWAN object, with the name gLoRaWAN
:
Catena::LoRaWAN gLoRaWAN; // the LoRaWAN function.
In your setup function, initialize gCatena, gLoRaWAN, and register gLoRaWAN as a pollable object (see Polling Framework).
void setup() {
// other things
// set up Catena platform.
gCatena.begin();
// set up LoRaWAN
gLoRaWAN.begin(&gCatena);
gCatena.registerObject(&gLoRaWAN);
// other things
}
Use the Catena::LoRaWAN::SendBuffer()
method to send an uplink message. Usually it's best to send it with an asynchronous callback, so that's what we'll show.
Definitions:
typedef void (Arduino_LoRaWAN::SendBufferCbFn)(
void *pClientData,
bool fSuccess
);
bool Catena::LoRaWAN::SendBuffer(
const std::uint8_t *pUplinkBuffer,
size_t nBuffer,
Arduino_LoRaWAN::SendBufferCbFn *pDoneFn,
void *pClientData,
bool fConfirmed,
std::uint8_t port
);
SendBuffer
attempts to start the transmission of a buffer. This attempt might fail for several reasons, for example:
If the transmission is not accepted, SendBuffer()
returns false
.
If the transmission is accepted, then the following steps are taken:
pDoneFn
and pClientData
are saved internally for use when the transmission completes.pUplinkBuffer
is copied into an internal buffer (so you can immediately start reusing the buffer in your own code).port
. If fConfirmed
is true
, a confirmed (acknowledged) uplink is used; otherwise unconfirmed uplinks are used.true
).When the transmission attempt finishes, the LoRaWAN subsystem calls pDoneFn(pClientData, fSuccess)
, with fSuccess
true if the uplink seemed to be successful. Success means different things in different circumstances.
fSuccess
will be false.Receiving a message is a somewhat passive operation. The client registers a callback with gLoRaWAN
; later, whenever a downlink message is received, the client's callback is called.
typedef void Arduino_LoRaWAN::ReceivePortBufferCbFn(
void *pCtx,
uint8_t uPort,
const uint8_t *pBuffer,
size_t nBuffer
);
void Arduino_LoRaWAN::SetReceiveBufferBufferCb(
ReceivePortBufferCbFn *pReceivePortBufferFn,
void *pCtx
);
In order to allow code to be portable across networks and regions, we've done a lot of work with abstraction classes. If you're curious, here's a somewhat simplified diagram (click on the diagram to get an enlarged SVG version).
As the diagram shows, Catena::LoRaWAN objects are primarily Arduino_LoRaWAN
derivatives, but they also can be viewed as McciCatena::cPollableObject
instances, and therefore can participate in polling.
Many MCCI Catena models include FRAM storage for keeping data across power cycles without worrying about the limited write-tolerance of EEPROM or flash. (FRAM, or ferro-electric RAM, is essentially non-volatile memory that can be freely written. Flash EPROM and EEPROM can be written, but tend to have non-local error properties and limited write durability. They are good for storing code, but troublesome for storing counters, because a location must be updated each time a counter is written.)
The abstract class cFram
is used to represent a FRAM-based storage element. It is abstract in that is uses several virtual methods that must be supplied by the concrete class that represents the specific FRAM chip. (For example, cFram2K
represents a 2k by 8 FRAM.)
All FRAMs managed by cFram
use a common object format on the FRAM, defined by the header file Catena_FramStorage.h
.
Storage is viewed as a linear sequence of objects.
Each object uses a common format.
Each object consists of a common 24-byte header followed by a variable-length storage field.
Objects are always a multiple of 4 bytes long.
Objects are identified by "globally unique ID" (or GUID) and "key" (an 8-bit value). GUIDs are 16-byte values, generated by a standard algorithm with low likelihood of collision. We considered using one GUID for each object, but that would consume a lot of room in system flash memory. So instead, we use an extra one-byte key, which allows most objects to share a common GUID. This approach allows for more space-efficient code on systems with limited system memory.
Each standard object contains a data payload. For any given object, the payload size is fixed when the object is created.
Objects normally contain two payload slots. The slots are written alternately (so that the old version is always available). A voting scheme is used to determine which slot is currently live. Three bytes are used for storing the "current" slot indicator, and are updated only after the new data have been written. A system interruption before the second byte of the trio is written will cause the system to use the old value after recovering from the problem; a system interruption after the second byte of the trio is written will cause the system to use the new value.
The first uint32_t
of an object records the overall size of the object, and the size of each data payload slot. Objects are always required to be a multiple of 4 bytes long, so the size is recorded as a count of uint32_t
values. Objects are allowed to be up to 2^18 bytes long. Data payload fields are specified in bytes, and are limited to [0..32767] bytes.
There is an escape clause. If bit 31 of the first uint32_t
is set, the object is not "standard". In such a case, the contents of the object after the standard header cannot be used for a standard data payload (as defined above). This may be desirable payloads that are written only once, when the FRAM is initialized; but it leaves redundancy management to the client.
This format is summarized in the following tables.
Bytes | Name | Type | Description |
---|---|---|---|
0..3 | uSizeKey |
uint32_t |
The size of the overall object, and the size of a datum within the object. This item is stored in little-endian format. The bit layout is shown below. |
4..19 | Guid |
MCCIADK_GUID_WIRE |
the 16-byte globally-unique ID of the object. This GUID is stored in wire order (big endian). |
20 | Key |
uint8_t |
An additional byte of name, allowing up to 256 objects to be defined by a single common GUID. |
21..23 | uVer[3] |
uint8_t[3] |
Array of current slot indicators. Normally these are all identical and either 0x00 or 0x01. However, after a system upset, it is possible that these will not be the same. If uVer[0] is equal to uVer[1] , then the slot is selected by the value of these bytes. Otherwise, the slot is selected by the value of uVer[3] . |
24..size-1 | - | - | Reserved space for the data payload. Slot zero starts at byte 24 and runs for the number of data bytes defined by bits 30..16 of uSizeKey . Slot one starts immediately after slot zero. |
uSizeKey
Bits | Name | Mask | Description |
---|---|---|---|
15..0 | Size |
cFramStorage::SIZE_MASK |
The size of the object in "clicks". Each click is four bytes. |
30..16 | DataSize |
cFramStorage::DATASIZE_MASK |
The size of the object's data payload in bytes. This may be zero. |
31 | fNonStandard |
cFramStorage::NONSTD_MASK |
If zero, the object's payload uses the redundant scheme described above; the payload size is necessarily limited to 32767 byes. If non-zero, the object's payload uses a client-supplied encoding and representation; but can use up to 256 k bytes (since the object size can represent up to 256 k bytes) |
An FRAM store managed by this library is expected to begin with a header object. A header object is identified by the well-known GUID {1DE7CDCD-0647-4B3C-A18D-8138A3D9613F}
and the key kHeader
(zero).
The header object carries a single 4-byte (uint32_t
) payload, which is interpreted as the end-of-storage address -- the offset of the first byte on the FRAM that is not used for object storage. If an object is added to the store, this pointer is updated after the new object object has been fully committed. The new object is not permanently committed until the end-of-storage pointer is atomically updated.
Determine the GUID and key you want to use. If you are adding the item as part of the Catena library, you can use the GUID GUID_FRAM_CATENA_V1(WIRE)
, {1DE7CDCD-0647-4B3C-A18D-8138A3D9613F}
; add the key to McciCatena::cFramStorage::StandardKeys
, defined in Catena_FramStorage.h
.
There is no presentable way to use a non-standard GUID; several changes must be made in Catena_Fram.cpp
to enable this.
Ultimately, the metadata for your new object is represented by a 32-bit value of type cFramStorage::StandardItem
. The constructor takes three (optionally four) arguments:
uKey
, the 8-bit key valueuSize
, the 16-bit object size. (If your object is variable size, you must specify a maximum size, and the actual size of the object must be represented as part of the object data somehow.)fNumber
, a Boolean value. If true, then the value represents a little-endian value; if false, big-endian. This is used for displays and the command interpreter.fReplicated
(assumed true
), which controls whether the replicated data-storage scheme should be used.Find the table McciCatena::cFramStorage::vItemDefs[]
in Catena_FramStorage.cpp
, and add your StandardItem
value at the appropriate offset.
To query the value of your object, you can use gCatena.getFram()->getField(uKey, Value)
; this is a templated function which will set Value according toe the current value stored for uKey
.
gCatena.getFram()->getField(uKey, (uint8_t *)&buffer, sizeof(buffer))
.To set the value of your object, you can use gCatena.getFram()->saveField(uKey, Value)
; this is a templated function which will write Value to the object identified by uKey
.
gCatena.getFram()->saveField(uKey, (const uint8_t *)&buffer, sizeof(buffer))
.The Catena Arduino platform provides both an asynchronous command-line collection object and a full command parser.
The Catena::begin()
method normally creates a command parser instance that's linked to a command parser instance. For
The header file Catena_StreamLineCollector.h
defines the class cStreamLineCollector
. This class is a cPollableObject
, and as such is polled automatically by the governing cPollingEngine
. A read is launched by calling cStreamLineCollector::readAsync()
, passing a callback function, a buffer (base and size), and a context handle. When a command has been accumulated, the specified callback function is called according to the following prototype:
typedef void (cStreamLineCollector::ReadCompleteCbFn)(
void* pCtx,
cStreamLineCollector::ErrorCode uStatus,
uint8_t *pBuffer,
size_t nBuffer
);
pCtx
is the user-supplied context parameter passed to cStreamLineCollector::readAsync
.uStatus
indicates whether the read was successful, and gives a rough idea of the failure reason if not.pBuffer
points to the first byte of data. This might be nullptr
in case of error, and it might be different than the user's original buffer pointer.nBuffer
is passed as the actual number of data bytes in the buffer. In case of error, nBuffer
will be zero.A command parser is initialized with a reference to a cStreamLineCollector
instance and a convenience reference to the governing cCatena
instance. It is initialized with
bool cCommandParser::begin(cStreamLineCollector *pStream, cCatena *pCatena)`
The command parser works by parsing the input line into words, and then finding the command in command tables, which the client registers at run time using the following function:
void cCommandParser::registerCommands(cDispatch *pDispatch, void *pContext);
Multiple command tables can be registered dynamically; this allows modules to add commands as they are initialized. There's no need to edit a central command table.
The command tables consist of a top-level cCommandParser::cDispatch
instance. This is not a const
-- it has bookkeeping entries to help with building the tables at runtime without requiring malloc()
. The dispatch instance points in turn to a
static cCommandStream::cDispatch myTable(/* cCommandStream::cEntry * */&table, sizeof(table));
or
static cCommandStream::cDispatch myTable(/* cCommandStream::cEntry * */&table, sizeof(table), "groupname");
In the first case, the commands are each entered into the top-level name space. In the second case, a top-level command named groupname
is entered, and each of the commands in the table is entered as a secondary command.
The command tables themselves are simple arrays of name/function pointer pairs.
static cCommandStream::CommandFn function1, function2 /*, etc. */;
static const cCommandStream::cEntry table[] = {
"cmd1", function1,
"cmd2", function2,
// ...
};
The signature of each function is:
cCommandStream::CommandStatus function1(
cCommandStream *pThis,
void *pContext,
int argc,
char **argv
);
pThis
points to the parent cCommandStream
instance. pContext
is the user data from the relevant cCommandStream::cDispatch
object. argc
and argv
are very much like the command arguments to a C main()
function. argv[0]
is the matching command, and argv[1..argc-1]
are the parsed arguments from the command line.
A command function may operate synchronously or asynchronously.
Command stream functions may call any of these functions:
pThis->printf()
formats results to pass back to the command source.pThis->getuint32()
scans an argument and converts to uint32_t
.pThis->completeCommand(CommandStatus)
signals the completion of an asynchronous command.A synchronous command function does all of its work in the initial function call, and returns a status code. The status code can be any value except CommandStatus::kPending
. Synchronous commands must not call pThis->completeCommand(CommandStatus)
.
An asynchronous command function allows for work to continue after the initial function call. The main command function typically has two parts.
The first part of the command is normally coded synchronously; it checks parameters, etc., and returns non-kPending
status. In this part of the command, there's no chance of pThis->completeCommand
being called.
The second part of the command is coded asynchronously. The asynchronous paths each call pThis->completeCommand()
when all work has been done. Once the function has established at least one asynchronous completion path, the main function must return kPending
(and must ensure that all the completion paths call completeCommand()
).
On some platforms, the system clock needs to be calibrated explicitly in order for the real-time ticks from micros()
and millis()
to be accurate. Do this by calling uint32_t gCatena.CalibrateSystemClock()
. This function updates the clock calibration, and returns a platform-specific value indicative of the calibration. On platforms that don't support (or that don't need) calibration, a dummy implementation is provided that returns 0.
The independent watchdog is used to detect and resolve malfunctions due to software failures. It triggers a reset sequence when it is not refreshed within the expected time-window (we use 26 seconds time-window). Along with the watchdog timer, we use SafeDelay()
function.
It serves as an alternative to the Arduino delay()
function. Its purpose is to refresh the watchdog time-window, thus preventing any potential resets during delay operations within the application. Like Arduino delay()
, it accepts milliseconds as a parameter.
The library includes a simple driver for the SiLabs 1133 light sensor found on many Catena boards.
The header file is Catena_Si1133.h
. It defines the class Catena_Si1133
.
The constructor, Catena_Si1133()
, takes no arguments.
Call Catena_Si1133::begin()
prior to using the sensor, and Catena_Si1133::end()
to shut it down.
Catena_Si1133::configure()
configures one channel of the sensor. It has one of two forms. The original form allows you to choose from pre-configured measurement profiles. Prototype:
bool configure(uint8_t uChannel, uint8_t uMode, uMeasurementCount = 0);
Up to six channels may be configured. uMode
is one of the following: CATENA_SI1133_MODE_NotUsed
to configure a channel as not used, CATENA_SI1133_MODE_SmallIR
to use the small IR sensor, CATENA_SI1133_MODE_MediumIR
to use the medium IR sensor, CATENA_SI1133_MODE_LargeIR
to use the large IR sensor, CATENA_SI1133_MODE_White
to use the regular white sensor,
CATENA_SI1133_MODE_LargeWhite
to use the large white sensor,
CATENA_SI1133_MODE_UV
to use the ultraviolet sensor,
CATENA_SI1133_MODE_UVDeep
to use the deep UV sensor.
uMeasurementCount
is zero to use the channel in forced mode, and non-zero to have the channel run in autonomous mode.
An advanced form provided complete flexibility. A special object is defined, Catena_Si1133::ChannelConfiguration_t
, which can represent all aspects of a measurement. To set up a measurement configuration, write something like this:
auto const measConfig = Catena_Si1133::ChannelConfiguration_t()
.setAdcMux(Catena_Si1133::InputLed_t::LargeWhite)
.setSwGainCode(7)
.setHwGainCode(4)
.setPostShift(1)
.set24bit(true)
.setCounter(Catena_Si1133::ChannelConfiguration_t::CounterSelect_t::MeasCount2);
This creates a value, measConfig
, that defines a measurement of the large-white LED, with software gain 7, hardware gain 4, and a post-shift of 1. The measurement, if periodic, will be driven by counter 2. (In forced mode, the counter settings are ignored.)
A number of methods allow you to modify and query ChannelConfiguration_t
values.
Parameter | Setter | Getter | Type | Comments |
---|---|---|---|---|
ADC input | .setAdcMux() |
.getAdcMux() |
Catena_Si1133::InputLed_t |
|
Software gain code | .setSwGainCode() |
.getSwGainCode() |
uint8_t |
This is log2 of the gain. |
Hardware gain code | .setHwGainCode() |
.getHwGainCode() |
uint8_t |
This is log2 of the gain. |
High range select | .setHsig() |
.getHsig() |
bool |
High range divides gain by 14.5 |
Interrupt Threshold | .setInterruptThreshold() |
.getInterruptThreshold() |
Catena_Si1133::Threshold_t |
Either no interrupt, or one of three threshold registers. |
Post-shift | .setPostShift() |
.getPostShift() |
uint8_t |
Divides measurement by 2^n. |
Set 24-bit mode | .set24bit() |
.get24bit() |
bool |
If true, select 24-bit mode, otherwise 16-bit. |
Periodic-mode counter | .setCounter() |
.getCounter() |
Catena_Si1133::CounterSelect_t |
Either no counter, or one of three measurement counters. |
To define a channel using a ChannelConfiguration_t
object, call the following method:
bool configure(uint8_t uChannel, ChannelConfiguration_t config, uMeasurementCount);
In the advanced method, uChannel
and uMeasurementCount
have the same meaning as in the pre-configured method.
bool start(bool fOneTime = false);
Start a repeated or one-time measurement.
bool isOneTimeReady();
Return true
if a one-time measurement is complete. All the measurements must have completed.
uint32_t readChannelData(uint8_t uChannel = 0);
Read the value for the specified channel.
void readMultiChannelData(uint16_t *pChannelData, uint32_t nChannel);
void readMultiChannelData(uint32_t *pChannelData, uint32_t nChannel);
Read nChannel
channels of data, starting from channel 0, into the table at pChannelData
. If all measurements are 16 bits, then the 16-bit form may be used. If any measurement is 24 bit, then the 32-bit form should be used.
cTimer
Timer objectTimer objects are used to simplify the implementation of periodic events.
#include <Catena_Timer.h>
This header file contains all the definitions for the cTimer
class.
The constructor takes no arguments. To create a timer named myTimer
, write:
McciCatena::cTimer myTimer;
Timers are initially stopped. To start a timer, call:
bool cTimer::begin(std::uint32_t nMillis);
This method initializes the timer to run with a period of nMillis
milliseconds. The timer automatically restarts each time the period elapses; so it's like a clock that ticks every nMillis
milliseconds.
To stop a timer, call:
void cTimer::end();
cTimer
eventsTo check whether a timer has ticked, call one of the following:
bool cTimer::isready();
std::uint32_t cTimer::readTicks();
std::uint32_t cTimer::peekTicks() const;
std::uint32_t cTimer::getRemaining() const;
isready()
returns true if the timer has ticked at least once since the last time isready()
or readTicks()
was called. readTicks()
returns the number of ticks that have occurred since the last time readTicks()
or isready()
was called. Both of these, in effect, reset the tick counter.
peekTicks()
returns the number of ticks since the last call to readTicks()
or isready()
, but doesn't reset the tick counter.
getRemaining()
returns the number of milliseconds remaining in the current timer cycle.
cTimer
Utility routinesstd::uint32_t cTimer::getInterval() const;
std::uint32_t cTimer::setInterval(std::uint32_t nMillis);
void cTimer::retrigger();
getInterval()
returns the number of milliseconds per timer tick.
setInterval()
changes the timer period to a new value, but doesn't change the base time of the current period. If the period is lengthened, then the next tick occurs relative to the base time plus the new period. If the period is shortened, ticks will immediately occur to cover any ticks between the base time of the period and now.
retrigger()
sets the base of the current period to the current time, and resets any pending ticks.
Catena_functional.h
This wrapper allows the C++ <functional>
header file to be used with Arduino code.
The technical problem is that the arduino.h
header file defines min()
and max()
macros. This causes problems with parsing the <functional>
header file, at least with GCC.
The solution is a hack: undefine min()
prior to including <functional>
, and then redefine them using the well-known definitions.
cDownload
This class may be instantiated as a general-purpose wrapper for supporting the MCCI Trusted Bootloader on STM32L0 platforms. It has two primary entry points. cDownload::evStartSerialDownload()
starts a download over a serial port, which is assumed to support 8-bit clean data transport. cDownload::evStart()
starts an abstract download; the client supplies a cDownload:Request_t
object which includes callbacks for fetching data. An instance of the cBootloaderApi
is also required.
cBootloaderApi
This class may be instantiated as a singleton to provide the interface to the Trusted Bootloader. It allows access to the APIs exported by the bootloader, as well as to the application structures that accompany the bootloader.
cSerial
This class provides an abstract wrapper for various Arduino Serial-like classes so that a single pointer can be used to refer to any of the possibilities (hardware serial, USB serial, software serial, etc.) It's intended primarily for use by the downloader, so doesn't provide 100% of the Serial
-like methods.
The following commands are supported by the Catena command parser.
Command | Description |
---|---|
echo args |
write arguments to the log stream |
help |
list the known commands |
system configure operatingflags [ uint32 ] |
display or set the operating flags for this system. |
system configure platformguid [ hexGuid ] |
display or set the platform GUID for this system |
system configure syseui [ eui64 ] |
display or set the system serial number, a 64-bit number. |
system reset |
dynamically restart the system, as if the reset button were pressed |
system version |
display the board type, and versions of the required libraries. Includes the MCCI Arduino BSP version, if known. |
Command | Description |
---|---|
system calibrate |
calibrate the system clock and print the result. |
Command | Description |
---|---|
fram reset [ hard ] |
reset the contents of the FRAM. A soft reset assumes that the data structures are correct, and resets values to defaults. A hard reset invalidates the FRAM, so that the next boot will fully reconstruct it. |
fram dump [ base [ len ] ] |
dump the contents of FRAM, starting at base for len bytes. If len is absent, a length of 32 bytes is assumed. If base is also absent, then 32 bytes are dumped starting at byte zero. |
The following commands are added by the Catena LoRawAN module.
Command | Description |
---|---|
lorawan configure |
Display all LoRaWAN parameters. |
lorawan configure param [ value ] |
Display or set a LoRaWAN parameter. |
lorawan join |
unjoin if joined, then start a new join session. |
These parameters are generally not loaded into the LMIC immediately. They are primarily used at boot time and at join time.
Command | Target device type | Description |
---|---|---|
lorawan configure |
either | Display all the parameters. |
lorawan configure deveui [ value ] |
OTAA | Set the devEUI for this device to value, a 64-bit EUI given in big-endian (natural) form. |
lorawan configure appeui [ value ] |
OTAA | Set the AppEUI for this device to value, a 64-bit EUI given in big-endian (natural) form. |
lorawan configure appkey [ value ] |
OTAA | Set the application key for this device to value, a 128-bit value given in big-endian (natural) form. |
lorawan configure nwkskey [ value ] |
ABP | Set the network session key for this device (the network session key) to value. For OTAA devices, this reflects the value saved after them most recent join. |
lorawan configure appskey [ value ] |
ABP | Set the application session key for this device (the application session key) to value. For OTAA devices, this reflects the value saved after them most recent join. |
lorawan configure devaddr [ value ] |
either | Set the device address, a 32-bit number, in big-endian form. Setting devaddr to zero on an OTAA device will cause the LMIC to try to rejoin after the next restart. For OTAA devices, this reflects the value saved after them most recent join. |
lorawan configure netid _[ value ] |
either | Set the network ID, in big-endian form. For OTAA devices, this reflects the value saved after them most recent join. |
lorawan configure fcntup [ value ] |
either | the current uplink frame count, FCntUp in the LoRaWAN spec. |
lorawan configure fcntdown [ value ] |
either | the current downlink frame count, FCntDown in the LoRaWAN spec. |
lorawan configure join [ value ] |
either | if zero, the provisioning data will not be loaded into the LMIC at startup. Older versions of the arduino-lorawan might still allow transmits to cause the device to start trying to join, but it will use invalid credentials. |
Here's a step-by-step procedure. There's a fully worked example, catena_usercommand
.
#include <Catena_CommandStream.h>
// for simplicity, we always assume this:
using namespace McciCatena;
// forward references to the command functions
cCommandStream::CommandFn cmdOne, cmdTwo /*, .. etc. */;
static
, but that's not required.// the individual commmands are put in this table
static const cCommandStream::cEntry sMyCommmandTable[] =
{
{ "one", cmdOne },
{ "two", cmdTwo },
// other commands go here....
};
const
, because internal fields are used for linkage (to avoid run-time memory allocation in the library).// a top-level structure wraps the above and connects to the system table
// it optionally includes a "first word" so you can for sure avoid name clashes
// with commands defined by the framework.
static cCommandStream::cDispatch sMyCommands(
sMyCommmandTable, // this is the pointer to the table
sizeof(sMyCommmandTable), // this is the size of the table
"application" // this is the "first word" for all the commands
// in this table. If nullptr, then the commands
// are added to the main table.
);
gCatena
names the global top-level object.gCatena.addCommands(
// app dispatch table, passed by reference
sMyCommands,
// optionally a context pointer using static_cast<void *>().
// normally only libraries (needing to be reentrant) need
// to use the context pointer.
nullptr
);
// process the command "application one"
// argv[0] is "one" (the matching word)
// argv[1..argc-1] are the arguments, if any
cCommandStream::CommandStatus cmdOne(
cCommandStream *pThis,
void *pContext,
int argc,
char **argv
)
{
// output your response using pThis->printf(), so that if there
// are multiple command sources, the answers will go to the right
// place.
pThis->printf("Hello, world!\n");
return cCommandStream::CommandStatus::kSuccess;
}
catena_hello
This is a very simple sketch without LoRaWAN support. It shows the minimal boilerplate needed to use this library. Although it's not obvious, while looping, the program automatically flashes the LED and accepts commands from the console.
catena_hello_lora
This sketch adds LoRaWAN uplink to the basic hello-world application. If the LoRaWAN system is provisioned, the app transmits a single message to port 16, containing the bytes 0xCA
, 0xFE
, 0xBA
, and 0xBE
, in sequence.
If the LoRaWAN system is not provisioned, the application enters an idle loop; you can use the LoRaWAN commands to set things up.
catena_usercommand
This sketch is very similar to catena_hello
. It shows how to add a user-defined command, application hello
, that prints "Hello, world!
".
catena_fsm
This sketch demonstrates the use of the Catena FSM class to implement the Turnstile
example described in Finite State Machine Framework.
Library | Recommended Version | Minimum Version | Comments |
---|---|---|---|
arduino-lmic |
4.0.0 | 3.99.0-3 | Earlier versions will fail to compile due to missing lmic_pinmap::rxtx_rx_polarity and lmic_pinmap::spi_freq fields. |
arduino-lorawan |
0.9.1 | 0.9.1-pre1 | Needed for bug fixes in session state save/restore. |
catena-mcciadk |
0.2.1 | 0.1.2 | Needed for miscellaneous definitions |
v0.23.0 includes the following changes.
v0.22.0 includes the following changes.
v0.21.2 includes the following changes, non breaking, all bug fixes.
v0.21.1 includes the following changes; only #305 is an API extension and it's non-breaking.
v0.21.0 includes the following changes.
gCatena.poll()
friendly.Catena_Mx25v8035f::programPage()
.gpCatenaBase
in favor of the static CatenaBase::pCatenaBase
.v0.20.1 includes the following changes.
cTotalizer::setDebounce()
, allowing the debounce time to be adjusted from 50ms. This can also be done in the constructor. (Version 0.20.0.30.)v0.20.0 includes the following changes.
Catena_Mb85rc64ta::read()
and write()
to read/write > 255 bytes (version 0.19.0.30).AdcStart()
before each AdcGetValue()
to read channel value (version 0.19.0.10).v0.19.0 includes the following changes.
lorawan configure
to display all the parameters.v0.18.1 includes the following changes.
v0.18.0 includes the following changes.
library.properties
.cDate
class and parser (0.17.0.30).Arduino_LoRaWAN_network
consistently as base for LoRaWAN
classes. Requires Arduino_LoRaWAN 0.6.0.20 or later. (0.17.0.20)Catena_Si1133
cannot be copied or moved. (0.17.0.11)McciCatena::cTimer
class to simplify timing. (0.17.0.10)v0.17.0 includes the following changes
CatenaSTM32L0::Sleep()
timing.system version
command.ttnctl
, update comments. See mcci-catena-provision for USB-based provisioning, using a variant of the same script.system calibrate
command. Version 0.16.0.50.v0.16.0 includes the following changes.
gLog.isenabled()
declaration.v0.15.0 includes the following changes.
TxBuffer_t
for usability.CatenaWingFram2k
, and add API to get configured clock rate.__HAL_PWR_CLEAR_FLAG()
in STM32 SleepForAlarm(). #150 change STM32 Sleep() to request STOP mode instead of STANDBY mode.lorawan join
command.catena_hello_lora
example.cFramStorage::setCurrent()
.CATENA_ARDUINO_PLATFORM_VERSION
to allow careful clients to #error
rather than die with obscure compile problems.cCommandStream::getuint32()
.ARDUINO_LORAWAN_VERSION
.system reset
command.v0.14.0 (2019-02-10) includes changes for the following issues.
v0.13.0 incorporates recent bug fixes and enhancements. We added our own implementation of the RTC class (issue #86. We updated the UML docs (issue #111). We refactored the STM32 classes (issue #99 and #103). A few other minor changes (754f4b and 71d45d0).
v0.12.0 adds support for the 4610, 4611, 4612, and 4801, and relies on the LMIC 2.3 pre-integrated pin map feature to simplify maintenance and make things more structures. We added more UML diagrams to document the class hierarchy. Now depends on MCCI SAMD BSP 1.2.0 and STM32 2.0.0. A common Catena::Sleep()
method was added for architecture-neutral low-power standby (issue #83). Added experimental machineQ network support. Various minor bug fixes and enhancements.
v0.11.0 adds a flash driver for the Catena 4470, adds a flash object for storing BME680 calibration data, and fixes bugs.
v0.10.0 adds explicit support for the Catena 4470, and fixes minor issues.
v0.9.0 adds explicit support for the Catena 4460.
v0.8.1 corrects an issue with the STM32 library (some things were here that belonged in the main Arduino libraries).
v0.8.0 has some minor changes (add the Catena4551 m101/m102/m103/m104 platform, add the Catena4450 m103/m104), and a flag change which effectively changes the API (hence the bump). We add CatenaBase::fHasLuxS1113
, which indicates the presence of a SI 1113 Lux sensor (as distinct from the BH1750 or the TSL2561 lux sensor used in the Catena4410). Further, we correct the platform flags for the 4551, as it doesn't have an I2C mux. Also incorporates some minor bug fixes for USB serial.
v0.7.0 is a major refactoring adding support for the Catena 4551
, which is based on the STM32L0. Although we think that there are no breaking changes, there might be a few, especially if code relied on structured defined internally to the MCCI-Catena-Arduino library Catena...
classes.
This repository is released under the MIT license. Commercial licenses are also available from MCCI Corporation.
MCCI invests time and resources providing this open source code, please support MCCI and open-source hardware by purchasing products from MCCI, Adafruit and other open-source hardware/software vendors!
For information about MCCI's products, please visit store.mcci.com.
MCCI and MCCI Catena are registered trademarks of MCCI Corporation. All other marks are the property of their respective owners.