Skip to content

Creating a basic Module

Will Richards edited this page Sep 2, 2023 · 3 revisions
Author: Will Richards

In Loom each sensor, actuator or piece of hardware is considered a module. This is so all components can be interfaced simultaneously by the manager to allow for unified data manipulation. Note: The Loom Logger will be used in this tutorial so if you haven't already done so please follow this Logger Introduction

Components of a module

There are 5 key components of a module that must be included in some capacity (they can do nothing if they are unused)

virtual void initialize() = 0;                      // Initialize all functionality of the sensor
virtual void measure() = 0;                         // Collect data from the sensor
virtual void package() = 0;                         // Package collected data into JSON document
virtual void power_up() = 0;                        // Power the sensor up and come out of sleep
virtual void power_down() = 0;                      // Power the sensor down to prepare for sleep

Additional functionality is provided to all modules for setting and retrieving the name of the module

void setModuleName(const char* modName)                       // Set the name of the module
virtual const char* getModuleName() { return moduleName; };   // This is a virtual function that can be overridden within your module
virtual void display_data() {};                               // Unused by default but can be overriden to allow device control when display_data is called

/* This is by deafult true for every module but can be set to false in the initialize method to inform other methods to not continue */
bool moduleInitialized = true;                                

Creating our class

For this example we will be using the SDI12 sensor module
Loom_SDI12.h

#include "Module.h"
#include "Loom_Manager.h"


/**
 * Provides both a loomified in addition to a standard reliable library implementation
 * 
 * @author Will Richards
 */ 
class Loom_SDI12 : public Module{
    protected:
        
        
        /* These should be called only by Manager.h */
        void measure() override;                                // Generic Measure Call To Pull Sensor Data
        void package() override;                                // Generic Package Call to Store Sensor Data
        void power_down() override;                             // Power down the module
        void power_up() override;                               // Power up the module

    public: 
        
        Loom_SDI12(Manager& man, const int pinAddr = 11);       // Loomified Constructor

        Loom_SDI12(const int pinAddr = 11);                     // Standard Sensor Interaction Constructor

        void initialize() override;                             // Initialize the sensor interface

        float getTemperature() { return sensorData[0]; };       // Temperature of the soil
        float getDielectricPerm() { return sensorData[1]; };    // Dielectric Permittivity of the soil
        float getConductivity() { return sensorData[2]; };      // Conductivity of the soil

    private:
        Manager* manInst;                                       // Instance of the Manager
        SDI12 sdiInterface;                                     // SDI-12 Library Interface
        float sensorData[3];                                    // Array of floats to store sensor data
};

The code above is a heavily stripped version of the SDI12 library showing only the loomified pieces. Here you can see how each of the pieces in implemented in a broader scope before we rebuild it as an example.

Here we have a basic class structure...
Loom_ExampleModule.h

// Add this line to prevent multiple inclusions of the header file
#pragma once

// Include both the Module and Manager header files
#include "Module.h"
#include "Loom_Manager.h"

/**
 * This is an example Module for the sake of this tutorial
 *
 * @author Will Richards
 */
 class Loom_ExampleModule : public Module {
	 protected:
		 /* These state that we will provide and implementation for these methods in our .cpp file */
		 void initialize() override;           
		 void measure() override;               
		 void package() override;
		 
		 /* While this denotes methods that we don't need to do anything */
		 void power_down() override {};
		 void power_up() override {};
		 
	public:
		/* ALL CONSTRUCTORS MUST INCLUDE A REFRENCE TO THE MANAGER */
		
	    /* This one takes an int named 'pinNum'. Default values should be supplied in almost all cases */
		Loom_ExampleModule(Manager& man, int pinNum = 11);
	
		/* Provide generic getters for module data to allow for independent manipulation */
		int getReading() { return pinReading; };
	
	private:
		Manager* manInst;                       // Instance to the manager that is supplied to the constructor
		int pinNum;                             // Pin number we are using
		
		bool pinReading = false;                // Some data that we will update in the measure function
		
	
 };

This structure provides the protype for our module next we need to add functionality within our Loom_ExampleModule.cpp file.

Loom_ExampleModule.cpp

/* Don't forget to include the header file at the top*/
#include "Loom_ExampleModule.h"

/* Includes the logger component to allow for easy logging*/
#include "Logger.h"

/* 
	Here we have the implementation for the no parameter constructor
	- First in our initialization list we have the module itself, this is required in all modules as it provides the name in this case 'ExampleModule'
	- Next we need to initialize our manager instance by passing the reference into our pointer
*/
Loom_ExampleModule::Loom_ExampleModule(Manager& man): Module("ExampleModule"), manInst(&man){ 

	// Use the value from the constructor, this could also be done in the initialization list
	this->pinNum = pinNum;
	
	/* THIS IS CRITICAL IN EVERY MODULE. This line registers the module with the manager */
        manInst->registerModule(this); 
}

/* 
	This is the piece of the module that is called when `manager.initialize()` is called in your sketch. 
	This should preform any actions required to initialize sensor hardware for taking 
	measurements.
*/
void Loom_ExampleModule::initialize(){

	/* Do initialization stuff.. In this case set our pin 11 to be an input */
	pinMode(pinNum, INPUT);
	
	LOG("Doing init stuff.... successfully initialized!");
}

/*
	This is the piece of the module that is called when `manager.measure()` is called in your sketch. 
	This should request and store any data from perpherials. 
*/
void Loom_ExampleModule::measure(){
	/* Take a reading from the passed in pin */
	LOG("Measuring...");
	pinReading = digitalRead(pinNum);
}

/*
	This is the piece of the module that is called when `manager.packaage()` is called in your sketch. 
	This will then take that data and package it up into the JSON document. 
*/
void Loom_ExampleModule::package(){
	/* Request a new JSON object for our current module to add our measurements to*/
	JsonObject json = manInst->get_data_object(getModuleName());
    
        /* JSON keynames in our case cannot have spaces, this creates a key named 'Pin_State' with the previously read state */
	json["Pin_State"] = pinReading;
}

That's pretty much it! You now have a basic module capable of reading in data from a designated pin and packaging it up into a JSON document