MicroSat Satellite#

Name

MicroSat

Subtitle

A Minimalist Implementation of a Constellation Satellite for Microcontrollers

Language

C++

Website

constellation/satellites/microsat

This is repository contains a minimalist implementation of a Constellation satellite for micro controllers of the ESP family.

Limitations#

This satellite runs on a microcontroller and therefore comes with a few restrictions compared to a fully-featured Constellation Satellite, namely:

  • As there is only a single thread, all code runs synchronously. This has some implications, most prominently that there are no transitional states of the FSM. When initialing a transition, the corresponding action is directly executed and an answer to the controller is only provided once the target steady state has been reached. For implementing MicroSats this means that these transitions should be reasonably short.

  • Due to the limited resources of the microcontroller, MicroSat comes with some restrictions on the total number of connections it can maintain for each of the communication protocols. More precisely, only 8 CMDP clients, 8 CSCP controllers and 16 CHP heartbeat subscribers can be connected.

  • MicroSat has some restrictions in receiving information: It does not provide an interface to subscribe to CMDP messages from other hosts, and can track the heartbeats of a maximum of 16 CHP hosts.

  • Since the microcontroller will run during the entire period of it being powered and there is no shutdown mechanism, the MicroSat will never send DEPART messages over CHIRP will its services will simply seize to be.

Building and flashing the code#

The project is built using PlatformIO, which manages all dependent libraries as well as the Xtensa toolkit. Install and Platformio and build MicroSat as follows:

$ pip install platformio

# Build the project - all environments
$ pio run

# Build one specific environment of the repository
$ pio run -e nodemcuv2

# Build and upload the firmware for a specific environment:
$ pio run -e nodemcuv2 -t upload

# Attach to serial port to see debug messages:
$ pio device monitor -e nodemcuv2

There are two environments defined:

  • nodemcuv2 matches the ESP8266 microcontroller’s NodeMCUv2 development board

  • esp32-poe-iso matches the ESP32 microcontroller on the Olimex POE ISO board

Before compilation, the desired satellite name as well as WiFi credentials have to be added to the include/config.h file.

MicroSat is compatible with standard Constellation implementations.

Implementing a MicroSat Satellite#

MicroSat satellites should be derived as child class from the Satellite base class provided within this repository. They should implement the relevant FSM transition commands:

  • on_initialize - initialization transition after receiving configuration from the controller

  • on_launch - launching of the satellite, readying it for running

  • on_land - landing the satellite, i.e. shutting down any controlled devices

  • on_start - starting a new run

  • on_stop - stopping the current run

  • on_interrupt - autonomous reaction to issues with other satellites in the Constellation

  • on_failure - autonomous reaction to an error in this satellite

  • running - the loop method called for every microcontroller main loop iteration when in running state

  • on_reconfigure - reconfiguration of the satellite, this is an optional transition which allows in-flight reconfiguration

It is recommended to reuse the provided main.cpp file since it starts all relevant services in the appropriate order and e.g. registers services with the CHIRP manager for network discovery.

Repository structure & description of support libraries#

This is a PlatformIO project. Accordingly, the repository is structured as follows:

  • include/config.h is the configuration file which contains e.g WiFi credentials and host names

  • lib contains support libraries for this project, namely:

    • CHIRP contains a full-featured implementation of the CHIRP protocol, capable of receiving and sending beacons

    • CHPBeater contains transmitter and receiver modules for Constellation heartbeats

    • CMDPPublisher contains the CMDP library for publishing log messages and statistics to the Constellation network

    • CSCPServer contains the command server component listening and reacting to CSCP commands of the Constellation

    • Satellite contains the main satellite and finite state machine libraries which combine the above components to a functioning Constellation satellite.

    • ZMTPSocket contains the very basic implementation of the ZMTP protocol for message exchange with ZeroMQ sockets

  • src/ contains the main executable, as well as an example for a user satellite class

  • platformio.ini contains the build parameters and platform description

Announcing Services to the Constellation via CHIRP#

This is an (almost complete) implementation of the Constellation Host Identification & Reconnaissance Protocol (CHIRP). It can send OFFER beacons, both upon startup and when receiving REQUEST beacons from other CHIRP hosts. It can also register remote services when receiving OFFER beacons and execute callback functions upon reception. On startup it will also send REQUEST beacons for all services with a registered callback function to enable late-joiner discovery.

One important limitation of microcontrollers is that they do not have a “clean shutdown” mechanism. The code will simply run until power is cut or the controller is reset via its hardware reset pin. Hence, this CHIRP-implementation cannot send DEPART beacons when a service is not available anymore.

The CHIRP library can be used as follows:

// UDP service
WiFiUDP chirpUDP;
// CHIRP service, groupname is the name of the Constellation, canonicalname the canonical identifier of this satellite:
CHIRPManager chirper(chirpUDP, groupname, canonicalname);

void setup() {
    // Start the CHIRP manager:
    chirper.begin();

    // Register a service - this also sends the OFFER CHIRP beacon.
    chirper.RegisterService(CHIRPManager::ServiceIdentifier::MONITORING, CMDPPublisher::getPort());

    // Register a callback for a remote service - this also sends the corresponding REQUEST beacon:
    chirper.RegisterCallback(CHIRPManager::ServiceIdentifier::HEARTBEAT, satellite.getHeartbeatCallback());
}

void loop() {
    // Process pending CHIRP requests:
    chirper.update();
}

Logging to Serial or Network with the CMDPPublisher#

This a limited implementation of the Constellation Monitoring Distribution Protocol (CMDP) v1. It uses the ZMTPSocket implementation described in detail below. In addition to be able to logging via the CMDP protocol to the network, it also can log to the serial port of the device. It is limited to publishing messages and cannot subscribe and receive messages from other CMDP hosts. There should only be one CMDPPublisher active, hence the library is implemented as a static instance which can be set up and initialized as:

void setup() {
    // Canonical Constellation name of this machine:
    CMDPPublisher::init(canonical_name);
    // Starting the publisher
    CMDPPublisher::begin();
}

This code will allocate an ephemeral port for the communication and start a server listening on the socket. The port number can be obtained via CMDPPublisher::getPort() e.g. for passing it to a CHIRP publisher.

The implementation provides logging macros which will automatically assemble CMDP messages and send them to all connected CMDP clients with corresponding subscriptions. CMDP messages below the lowest log subscription of any client are not assembled at all.

void loop() {
    // Needs to be called on every loop iteration to process new clients and subscriptions/unsubscriptions:
    CMDPPublisher::update();

    // Logging a message:
    LOG(INFO, "This is a log message that will be distributed via CMDP");

    // Optionally, a logger topic can be set:
    LOG(WARNING, "SENSOR", "Something is up with the sensor reading");

    // For TRACE log levels, code location information is automatically attached
    LOG(TRACE, "There is a bug at this location that needs fixing");

    // A separate macro is provided for logging only locally to the serial interface
    LOGS(TRACE, "This message is only logged locally to the serial interface, not over network");
}

ZMTPSocket#

Since there is no full implementation of the ZMQ library available for micro controller platforms, this code implements a simplified version of the ZMTP 3.0 protocol “manually”. Not all valid socket combinations are currently supported by this implementation, and command messages as well as regular traffic has to be manually decoded and checked by classes using the ZMTPSocket class as parent.

Child classes using the ZMTPSocket as parent need to provide the desired socket types for the local and remote peer via the constructor, e.g. the following code for a REP socket which accepts connections from remote REQ sockets:

MyService::MyService() : ZMTPSocket(Type::REQ, Type::REP) {}

Protocol information#

This code only works for ZMTP v3 subscribers. Connections are set up as follows:

  • The peer connects.

  • The host sends the signature bits of the greeting and reads back the peer greeting from the socket. The signature for ZMTP 3.0 is the byte sequence 0xFF, 0, 0, 0, 0, 0, 0, 0, 1, 0x7F.

  • The host sends its greeting version and reads the peer version from the socket. The version consists of two bytes, one for the major and one for the minor version, hence 3, 0.

  • The host sends the remainder of the greeting sequence (mechanism, as-server flag, filler bytes) and reads the peer greeting from the socket. The connection mechanism chosen is the NULL security mechanism, hence the byte pattern is 'N', 'U', 'L', 'L', 0. Both as-server and filler flags are 0 and fill the entire greeting to a total length of 64 bytes.

After the host and peer have agreed on their greeting procedure, the handshake must be performed. The handshake is already a valid ZMQ message and consists of:

  • Flags (one byte)

  • Size of the message body (one byte for short messages)

  • Message body

Reading a message from the socket therefore always first reads two bytes (flag and size) and then N bytes according to the size read. The flags indicate e.g. SNDMORE (0x1, more frames to follow in this multipart message), or COMMAND (0x4).

The handshake looks as follows:

  • Flags 0x4 to indicate that this is a command

  • Size 25, twenty-five bytes of message body

  • Message body:

    • 1 byte command size (5) followed by the command (R, E, A, D, Y)

    • 1 byte property name size (11) followed by the property name (Socket-Type)

    • 3 bytes of filler (0, 0, 0), property value size and property value.

The property value represents the socket type and can be PUB, SUB, REQ, REP (value size 3) or PULL, PUSH (value size 4). After the host has sent its own handshake message, it reads the handshake message from the peer and verifies that the socket-type matches (valid socket pairing according to ZMTP).

With this sequence, a connection is established and messages can be exchanged between the connected peers.