All the different parts are easily combined in the main file of the Arduino sketch. It starts with some includes and declarations.

#include <SPI.h>

#include "AudioPlayer.h"
#include "SDCard.h"
#include "Timer.h"
#include "LEDController.h"
#include "MotionSensor.h"

using namespace lr;

// Motion callback.
void onMotion(const unsigned long currentTime, MotionSensor::Status status);

/// The logic state.
enum LogicState : uint8_t {
	/// If the board is waiting for the sensor.
	WaitForSensor,
	/// If the board is idle
	IdleState,
	/// If the board is in error state, only the LED is controlled.
	ErrorState,
	/// If the board is in alarm state, the voice is played.
	AlarmState,
} logicState;

/// The controller for the LED
LEDController ledController; 

/// The motion sensor.
MotionSensor motionSensor;

There is the include #include : The SPI class is not used in the main file, but the include also makes sure the files for the SPI libraries are compiled and linked into the project.

Next I include all headers of my classes into the project and import everything from my own namespace lr into the global namespace.

I have to declare the method onMotion here (forward declaration), because I use them as callback in the setup() method which is defined before the onMotion method.

The enumeration LogicState is the overall logic state of my project. This states are similar to the sensor states, but represent it in a more general way.

Next there are two global variables to create instances of the LED controller and motion sensor.

void setup() {
	// Set the initial state.
	logicState = WaitForSensor;

	// setup the LED controller
	ledController.setup();

	// setup the motion sensor.
	motionSensor.setup();
	motionSensor.setCallback(&onMotion);

	// Initialize the serial interface
	Serial.begin(115200);
	Serial.println(F("Starting..."));

	// Initialize the audio player
	if (!audioPlayer.initialize()) {
		Serial.println(F("Error on initialize."));
		Serial.flush();
		signalError();
		return;
	}

	Serial.println(F("Success!"));
	Serial.flush();
}

The setup() method initializes all components and writes messages to the serial interface.

void loop() {
	// Get the current time for this loop iteration.
	const unsigned long currentTime = millis();
	// Control the LED
	ledController.loop(currentTime);
	// Deactivate everything if there is an error.
	if (logicState != ErrorState) {
		// Check the motion sensor.
		motionSensor.loop(currentTime);
		// If the board goes into alarm state, play the sound.
		// This will block the loop, until the sound finishes.
		if (logicState == AlarmState) {
			if (!audioPlayer.play("voice.snd")) {
				Serial.println(F("Error on play."));
				Serial.flush();
				return;
			}
			// Go back into idle state after the sound.
			// The sensor might stay in alarm state for a while.
			logicState = IdleState;
		}
	}
}

The loop calls the loop methods of my components, depending on the state. In the Error state, only the loop method of the LED controller is called to keep the red LED blinking. Everything else is ignored.

If the overall logic switches into the alarm state, the voice audio is played. This is a blocking call. After playing the voice, the loop goes back into idle state.

void onMotion(const unsigned long currentTime, MotionSensor::Status status)
{
	if (status == MotionSensor::WaitStablilize) {
		Serial.println(F("Wait until the sensor is ready."));
		Serial.flush();
		ledController.setState(LEDController::Orange, LEDController::BlinkSlow);
	} else if (status == MotionSensor::Idle) {
		Serial.println(F("Sensor is in idle state."));
		Serial.flush();
		ledController.setState(LEDController::Green, LEDController::FlashVerySlow);
		logicState = IdleState; // Ready to observe.
	} else if (status == MotionSensor::Alarm) {
		Serial.println(F("Sensor alarm."));
		Serial.flush();
		ledController.setState(LEDController::Red, LEDController::On);
		logicState = AlarmState; // Activate the alarm and play a sound.
	}
}

The onMotion callback is called if the state of the motion sensor changes. Depending on the state, the LED color and state is changed and the overall state of the controller is changed too.

void signalError() {
	ledController.setState(LEDController::Red, LEDController::BlinkFast);
	logicState = ErrorState; // Deactivate everything except the LED blinking.
}

The signalError() method is called to put the controller into the error state. The LED is changed into red blinking, and the logic state is set to ErrorState. Only a reset of the board can end the error state.

Conclusion

Because all the complexity is wrapped into components with simple interfaces, the main file of the project looks very clean and should be easy to understand. Imagine the chaos if every detail from these classes would be put into one single file.

Continue here: From the Prototype to the Final Device