MicroSat Satellite#
Name |
MicroSat |
---|---|
Subtitle |
A Minimalist Implementation of a Constellation Satellite for Microcontrollers |
Language |
C++ |
Website |
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 boardesp32-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 controlleron_launch
- launching of the satellite, readying it for runningon_land
- landing the satellite, i.e. shutting down any controlled deviceson_start
- starting a new runon_stop
- stopping the current runon_interrupt
- autonomous reaction to issues with other satellites in the Constellationon_failure
- autonomous reaction to an error in this satelliterunning
- the loop method called for every microcontroller main loop iteration when in running stateon_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 nameslib
contains support libraries for this project, namely:CHIRP
contains a full-featured implementation of the CHIRP protocol, capable of receiving and sending beaconsCHPBeater
contains transmitter and receiver modules for Constellation heartbeatsCMDPPublisher
contains the CMDP library for publishing log messages and statistics to the Constellation networkCSCPServer
contains the command server component listening and reacting to CSCP commands of the ConstellationSatellite
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 classplatformio.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 are0
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 commandSize
25
, twenty-five bytes of message bodyMessage 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.