Home

Awesome

The MCCI Catena Arduino Platform Library

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.

GitHub release GitHub commits Build Status

<!-- markdownlint-disable MD033 --> <!-- markdownlint-capture --> <!-- markdownlint-disable --> <!-- TOC depthFrom:2 updateOnSave:true --> <!-- /TOC --> <!-- markdownlint-restore --> <!-- Due to a bug in Markdown TOC, the table is formatted incorrectly if tab indentation is set other than 4. Due to another bug, this comment must be *after* the TOC entry. -->

Overview

Coding Practices

In order to assist people who are not everyday readers and writer of C++, this library adopts some rules.

  1. All names are in the McciCatena namespace.

  2. 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.

  3. We tend to use the m_... prefix on the names of class member fields.

  4. 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.

  5. We tend to name classes starting with a lower-case letter c, i.e., <code><strong>c</strong><em><u>ClassName</u></em></code>. For the Catena... classes, we don't follow this rule, however.

  6. 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.

  7. 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.)

Components

Namespace 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;

Class 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.

Board-specific Classes

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:

ClassHeader FileDescription
Catena4410Catena4410.hFirst generation MCCI systems with BME180
Catena4420Catena4420.hFeather M0 Bluetooth + LoRa Radio Wing
Catena4450Catena4450.hMCCI Catena 4450
Catena4460Catena4460.hMCCI Catena 4460
Catena4470Catena4470.hMCCI Catena 4470
Catena4551Catena4551.hMCCI Catena 4551 first-generation Murata-based board.
Catena4610Catena4610.hMCCI Catena 4610 second-generation Murata-based board with LiPo charging
Catena4611Catena4611.hMCCI Catena 4611 second-generation Murata-based board with fixed Vdd, no charging
Catena4612Catena4612.hMCCI Catena 4612 second-generation Murata-based board with variable Vdd, no charging.
Catena4617Catena4617.hMCCI Catena 4617 second-generation Murata-based board with variable Vdd, no charging
Catena4618Catena4618.hMCCI Catena 4618 second-generation Murata-based board with variable Vdd, no charging.
Catena4630Catena4630.hMCCI Catena 4630 Murata-based board with Air Quality Sensor.
Catena4801Catena4801.hMCCI Catena 4801 Murata-based board with Modbus.
Catena4802Catena4802.hMCCI Catena 4802 Murata-based board with Modbus and Temperature sensor.

Class derivation

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.

<!-- For all these, see assets/CatenaBase.plantuml. for some bizarre reason GitHub won't render these unless image links are http (not https). Each image is structured as a reference to the image, plus a link. Both must be updated; only the link may be https; the image must be http. Also note that the link is SVG, but the image is PNG. -->

STM32 Classes

The first figure just gives relationships; the second has details about the members of each class.

Catena STM32 Class Relationships:

<!-- $enableStm32 = 1, $enableSamd = 0, $enableMembers = 0 -->

Catena STM32 Class Relationships:

Catena STM32 Class Hierarchy (full detail):

<!-- $enableStm32 = 1, $enableSamd = 0, $enableMembers = 1 -->

Catena STM32 Class Hierarchy (full detail):

SAMD Classes

The first figure just gives relationships; the second has details about the members of each class.

Catena SAMD Class Relationships:

<!-- $enableStm32 = 0, $enableSamd = 1, $enableMembers = 0 -->

Catena SAMD Class Relationships:

Catena SAMD Class Hierarchy (full detail):

<!-- $enableStm32 = 0, $enableSamd = 1, $enableMembers = 1 -->

Catena SAMD Class Hierarchy (full detail):

Platform Management

The hardware supported by this platform is generally similar. The architecture allows for the following kinds of variation (as outlined in the class hierarchy):

  1. CPU differences (Cortex M0, RISC-V, etc.)
  2. SOC differences (SAMD21, STM32L0, etc.)
  3. PC-board differences (different sensors, power supply, capabilities)
  4. Optional component population differences (pull-up resistor values, etc.)
  5. Externally-connected sensors (one-wire temperature sensors, etc.)

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.

Platform GUIDs

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

GUIDs for the Catena 461x family

Catena 4610

The Catena 4610 uses a LiPo battery like traditional Feathers, and includes a BME280 temperature/pressure/humidity sensor, and a Si1133 light sensor.

NameGUIDDescription
CATENA_4610_BASE53ca094b-b888-465e-aa0e-e3064ec56d21Base Catena 4610, assuming no modifications or customizations.
CATENA_4610_M1016a5d8d0c-d5ae-4143-adc7-8f84ec56a867Catena 4610 M101, configured for power monitoring and other pulse-input applications.
CATENA_4610_M10218252b1c-3c0d-403e-8012-224d96c5af06Catena 4610 M102, configured for environmental monitoring
CATENA_4610_M103c2cf6cf4-a4c3-4611-941f-6955ffa5bfdcCatena 4610 M103 -- contact MCCI
CATENA_4610_M104bfed4740-a58a-4ef6-933a-09cb22e93d00Catena 4610 M104 -- contact MCCI
Catena 4611

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.

NameGUIDDescription
CATENA_4611_BASE9bb29dca-0685-4837-8182-3dfa309d279fBase Catena 4611, assuming no modifications or customizations.
CATENA_4611_M1014e995471-1570-4767-adae-6657ef871bcdCatena 4611 M101, configured for power monitoring and other pulse-input applications.
CATENA_4611_M102964bcf91-9c45-4386-a6e7-5f2d7c3641efCatena 4611 M102, configured for environmental monitoring.
CATENA_4611_M103c85b27cb-7cf9-4025-92bb-2009c08449e5Catena 4611 M103 -- contact MCCI
CATENA_4611_M104c22be8af-e693-4319-b243-1c2d10197973Catena 4611 M103 -- contact MCCI
Catena 4612

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.

NameGUIDDescription
CATENA_4612_BASE915decfa-d156-4d4f-bac5-70e7724726d8Base Catena 4612, assuming no modifications or customizations.
CATENA_4612_M101d210a354-c49a-4c4f-856a-4b545dcfaa20Catena 4612 M101, configured for power monitoring and other pulse-input applications.
CATENA_4612_M1027fa9709d-17af-463e-ae7f-8210e49acd7aCatena 4612 M102, configured for environmental monitoring.
CATENA_4612_M103ff8b2ac6-75cd-4ed3-980b-50b209e64551Catena 4612 M103 -- contact MCCI
CATENA_4612_M104dea48489-cdac-43f4-b8ad-edb08ce21546Catena 4612 M103 -- contact MCCI
Catena 4617

The Catena 4617 is a variant of the Catena 4612 with a IDT HS3001 temperature/humidity sensor in place of the Bosch BME280.

NameGUIDDescription
CATENA_4617_BASE6767c2f6-d5d5-43f4-81af-db0d4d08815aBase Catena 4617, assuming no modifications or customizations.
Catena 4618

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.

NameGUIDDescription
CATENA_4618_BASEb75ed77b-b06e-4b26-a968-9c15f222dfb2Base Catena 4618, assuming no modifications or customizations.
Catena 4630

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.

NameGUIDDescription
CATENA_4630_BASE17281c12-d78a-4e4f-9c42-c8bbc5499c91Base Catena 4618, assuming no modifications or customizations.

GUIDs for the Catena 4450/4460/4470 family

Catena 4450

The 4450 Feather Wing includes a BME280 temperature/humidity/pressure sensor, and a BH1750 lux sensor.

NameGUIDDescription
CATENA_4450_BASE60480acb-dc5d-4148-b6c9-aca13449cf1dBase Catena 4450, assuming no modifications or customizations.
CATENA_4450_M10182bf2661-70cb-45ae-b620-caf695478bc1Catena 4450 M101, configured for power monitoring and other pulse-input applications.
CATENA_4450_M1022281255e-ac5c-48cb-a263-9dc890d16638Catena 4450 M102, configured for environmental monitoring.
CATENA_4450_M1031fb2506f-0f2a-4310-9e6a-9bc191e0ae12Catena 4450 M103 -- contact MCCI
CATENA_4450_M104a731f637-e3ed-4088-a9a8-f54b6671dcf6Catena 4450 M103 -- contact MCCI
Catena 4460

The 4460 Feather Wing includes a BME680 air quality sensor, and a BH1750 lux sensor.

NameGUIDDescription
CATENA_4460_BASE3037d9be-8ebe-4ae7-970e-91915a2484f8Base Catena 4460, assuming no modifications or customizations.
CATENA_4460_M10131e563d1-0267-43fc-bca0-9a4cb5bfc55aCatena 4460 M101, configured for power monitoring and other pulse-input applications.
CATENA_4460_M102494f3c17-8ac1-4f80-8ecc-ca4dd3dccbdcCatena 4460 M102, configured for environmental monitoring.
CATENA_4460_M103a882186f-f4ab-4ee4-9402-7b628a76d886Catena 4460 M103 -- contact MCCI
CATENA_4460_M104398a9e5a-e22f-4265-9d35-bf45433ddbe3Catena 4460 M103 -- contact MCCI
Catena 4470

The 4470 Feather Wing includes a BME280 temperature/humidity/pressure sensor, a BH1750 lux sensor, and a Modbus/RS-485 interface connected to Serial1.

NameGUIDDescription
CATENA_4470_BASEea8568ec-5dae-46ee-929a-a3f6b00a565eBase Catena 4470, assuming no modifications or customizations.
CATENA_4470_M101dd0a37a6-e469-43ec-b173-fed795129455Catena 4470 M101, configured for power monitoring and other pulse-input applications.

GUIDs for the Catena 480x family

Catena 4801
NameGUIDDescription
CATENA_4801_BASE10ea7e25-a4a4-45fd-8959-c04a6a5d7f95Base Catena 4801, assuming no modifications or customizations.
Catena 4802
NameGUIDDescription
CATENA_4802_BASEdaaf345e-b5d5-4a32-a303-3ac70b81d260Base Catena 4802, assuming no modifications or customizations.

GUIDs for Adafruit Feather M0s

MCCI also uses this library with Feather M0s without MCCI hardware. These GUIDs are useful in that situation.

NameGUIDDescription
FEATHER_M0_LORA_TTNNYCa67ad93c-551a-47d2-9adb-e249b4cf915aFeather M0 LoRa, modified per The Things Network NYC standards -- DIO1 connected to D6.
FEATHER_M0_LORAe2deccc8-55fa-4bd3-94c3-ce66bcd0baacFeather M0 LoRa, but DIO1 connection is not known.
FEATHER_M0_PROTO_WINGLORA_TTNMCCI3bab150f-6e32-4459-a2b6-72aced75059fFeather M0 Proto with a separate LoRa Feather Wing. This is sometimes known as an MCCI Catena 4420.
FEATHER_M0_PROTOf6a15678-c7f3-43f4-ac57-67ef5cf75541A Feather M0 Proto.
FEATHER_M02e6dfed4-f577-47d5-9137-b3e63976ae92Some unspecified member of the Feather M0 family.

Polling Framework

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.

**Pollable framework UML Class Diagram **

Making a class pollable

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:

  1. Change UserClass1 to inherit from McciCatena::cPollableObject, using multiple inheritance if needed. List McciCatena::cPollableObject last. No need to make it public.

  2. 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;
};

Using pollable objects in sketches

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 Machine (FSM) Framework

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:

In addition to the states relevant to your problem, TState must have three distinct values with well-known names.

We'll use the coin-operated turnstile example from Wikipedia to make things more concrete.

Getting ready

The author usually starts by drawing a diagram, labeled with the states and transitions.

For example, here's the turnstile state diagram:

**Coin Operated Turnstile State Diagram

Defining the state 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.
};

Identify the parent class

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 state type to the parent class

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.
  };

};

Define the FSM instance in the parent class

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;
};

Declare a method function in the parent class

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);
};

Implement the FSM dispatch function

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;
}

Implement the FSM initialization

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...
}

The general time/date class 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;

Interval Seconds

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 types

The 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 properties

bool 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 methods

bool 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.

Timekeeping, solar days, leap seconds

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.

  1. Keep interval time interval-preserving, and convert to calendar time as if days were exactly 86,400 seconds long. (GPS is such a time scale.) Differences between instants (in seconds) are in ITU seconds.
  2. Keep interval time interval-preserving, but convert to calendar time accounting for leap seconds (most days are 86,400 seconds long, but some days are 86,399 seconds long, others are 86,401 seconds long). (UTC is such a time scale.) Differences between instants (in seconds) are in ITU seconds.
  3. Make interval time not interval-preserving by considering leap seconds. A day with 86,401 ITU seconds will have two seconds numbered 86,399; a day with 86,399 ITU seconds will not have a second numbered 86,399. Convert to date/time as if days were exactly 86,400 seconds long. The difference between two instants (in interval time) is not guaranteed to be accurate in ITU seconds. This is how POSIX time works.

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.

LoRaWAN Support

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.

  1. 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.
    
  2. Instantiate the global LoRaWAN object, with the name gLoRaWAN:

    Catena::LoRaWAN gLoRaWAN;  // the LoRaWAN function.
    
  3. 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
    }
    

Sending an uplink message

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:

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.

Registering to receive downlink messages

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
  );

LoRaWAN Class Structure

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).

LoRaWAN class structure

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.

FRAM Storage Management

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.)

FRAM Storage Formats

All FRAMs managed by cFram use a common object format on the FRAM, defined by the header file Catena_FramStorage.h.

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_tis 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.

Object Storage Structure
BytesNameTypeDescription
0..3uSizeKeyuint32_tThe 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..19GuidMCCIADK_GUID_WIREthe 16-byte globally-unique ID of the object. This GUID is stored in wire order (big endian).
20Keyuint8_tAn additional byte of name, allowing up to 256 objects to be defined by a single common GUID.
21..23uVer[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.
Bit layout of uSizeKey
BitsNameMaskDescription
15..0SizecFramStorage::SIZE_MASKThe size of the object in "clicks". Each click is four bytes.
30..16DataSizecFramStorage::DATASIZE_MASKThe size of the object's data payload in bytes. This may be zero.
31fNonStandardcFramStorage::NONSTD_MASKIf 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)
The FRAM header object

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.

Adding FRAM objects

  1. 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.

  2. 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 value
    • uSize, 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.
    • Optionally fReplicated (assumed true), which controls whether the replicated data-storage scheme should be used.
  3. Find the table McciCatena::cFramStorage::vItemDefs[] in Catena_FramStorage.cpp, and add your StandardItem value at the appropriate offset.

  4. 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.

    • You may also use gCatena.getFram()->getField(uKey, (uint8_t *)&buffer, sizeof(buffer)).
  5. 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.

    • You may also use gCatena.getFram()->saveField(uKey, (const uint8_t *)&buffer, sizeof(buffer)).

Class hierarchy within the FRAM library

<!-- The following image renders well on GitHub, but doesn't render in the previewer in Visual Studio Code unless you turn on "insecure content". Of course, GitHub rendering is more important, but this is irksome -->

Image of cFram -- see assets/cfram.plantuml

<!-- The following image renders well on GitHub, but doesn't render in the previewer in Visual Studio Code unless you turn on "insecure content". Of course, GitHub rendering is more important, but this is irksome -->

Image of FRAM Storage objects -- see assets/cframstorage.plantuml

Asynchronous Serial Port Command Processing

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

Collecting lines asynchronously from streams

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
    );

The command parser

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 methods for use by functions

Command stream functions may call any of these functions:

Synchronous Command Functions

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).

Asynchronous Command Functions

An asynchronous command function allows for work to continue after the initial function call. The main command function typically has two parts.

  1. 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.

  2. 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()).

Clock Management and Calibration

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.

Watchdog Timer

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.

SafeDelay()

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.

Si1133 driver

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.

ParameterSetterGetterTypeComments
ADC input.setAdcMux().getAdcMux()Catena_Si1133::InputLed_t
Software gain code.setSwGainCode().getSwGainCode()uint8_tThis is log2 of the gain.
Hardware gain code.setHwGainCode().getHwGainCode()uint8_tThis is log2 of the gain.
High range select.setHsig().getHsig()boolHigh range divides gain by 14.5
Interrupt Threshold.setInterruptThreshold().getInterruptThreshold()Catena_Si1133::Threshold_tEither no interrupt, or one of three threshold registers.
Post-shift.setPostShift().getPostShift()uint8_tDivides measurement by 2^n.
Set 24-bit mode.set24bit().get24bit()boolIf true, select 24-bit mode, otherwise 16-bit.
Periodic-mode counter.setCounter().getCounter()Catena_Si1133::CounterSelect_tEither 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 object

Timer objects are used to simplify the implementation of periodic events.

Catena_Timer.h header file and initialization

#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;

cTimer begin() and end()

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();

Checking for cTimer events

To 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 routines

std::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.

Command Summary

Standard commands

The following commands are supported by the Catena command parser.

CommandDescription
echo argswrite arguments to the log stream
helplist 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 resetdynamically restart the system, as if the reset button were pressed
system versiondisplay the board type, and versions of the required libraries. Includes the MCCI Arduino BSP version, if known.

STM32L0 commands

CommandDescription
system calibratecalibrate the system clock and print the result.

FRAM commands

CommandDescription
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.

LoRaWAN commands

The following commands are added by the Catena LoRawAN module.

CommandDescription
lorawan configureDisplay all LoRaWAN parameters.
lorawan configure param [ value ]Display or set a LoRaWAN parameter.
lorawan joinunjoin if joined, then start a new join session.

LoRaWAN Parameters

These parameters are generally not loaded into the LMIC immediately. They are primarily used at boot time and at join time.

CommandTarget device typeDescription
lorawan configureeitherDisplay all the parameters.
lorawan configure deveui [ value ]OTAASet the devEUI for this device to value, a 64-bit EUI given in big-endian (natural) form.
lorawan configure appeui [ value ]OTAASet the AppEUI for this device to value, a 64-bit EUI given in big-endian (natural) form.
lorawan configure appkey [ value ]OTAASet the application key for this device to value, a 128-bit value given in big-endian (natural) form.
lorawan configure nwkskey [ value ]ABPSet 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 ]ABPSet 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 ]eitherSet 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 ]eitherSet the network ID, in big-endian form. For OTAA devices, this reflects the value saved after them most recent join.
lorawan configure fcntup [ value ]eitherthe current uplink frame count, FCntUp in the LoRaWAN spec.
lorawan configure fcntdown [ value ]eitherthe current downlink frame count, FCntDown in the LoRaWAN spec.
lorawan configure join [ value ]eitherif 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.

Adding your own commands

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. */;
// the individual commmands are put in this table
static const cCommandStream::cEntry sMyCommmandTable[] =
  {
    { "one", cmdOne },
    { "two", cmdTwo },
    // other commands go here....
  };
// 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.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;
    }

Example sketches

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.

Board Support Dependencies

Other Libraries and Versions Required

LibraryRecommended VersionMinimum VersionComments
arduino-lmic4.0.03.99.0-3Earlier versions will fail to compile due to missing lmic_pinmap::rxtx_rx_polarity and lmic_pinmap::spi_freq fields.
arduino-lorawan0.9.10.9.1-pre1Needed for bug fixes in session state save/restore.
catena-mcciadk0.2.10.1.2Needed for miscellaneous definitions

Release History

Meta

License

This repository is released under the MIT license. Commercial licenses are also available from MCCI Corporation.

Support Open Source Hardware and Software

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.

Trademarks

MCCI and MCCI Catena are registered trademarks of MCCI Corporation. All other marks are the property of their respective owners.