Particle Argon Multithreading

A recent major project required the creation of a system that would be capable of collecting data from a wide range of sensors, acting upon that data and also allowing the handling of requests from a user in real-time.

I decided to build what I labelled the ‘Smart Greenhouse’. Simply put it was a small greenhouse contained in a fish tank which contained various sensors for temperature, humidity, soil moisture, etc. It also had a dashboard for the user so they could see historical data, set thresholds which the system would respond to (i.e. the moisture level which triggers watering, or temperature that triggers venting) and also manually trigger venting and watering via overrides in real-time.

Smart Greenhouse project and dashboard

Without getting too much into creation of the actual project itself, I’d like to focus on the implementation of multithreading to provide the required project functionality. Slight caveat, whilst Particle’s Argon and Photon development boards both allow multithreading, it’s not officially supported.

The Demonstration Project

I’ve created a simple demonstration project which shows multithreading in use. Using the same setup as my controlling leds from the internet project, three LEDs are connected to the Argon, with each being fired on their own thread.

Argon Setup

Overview of Project

The project code can be found here. I will conduct a brief overview of parts of interest below. The first section of interest is the multithreading setup which begins on line 16.

//Multithreading
SYSTEM_THREAD(ENABLED); //multi threading, system runs on its own thread
void preSetup(); // startup function to register mutex before setup is called
STARTUP(preSetup()); // mutex must be initialised before setup
void thread1Function(void *param); // declaration of thread1 function
void thread2Function(void *param); // declaration of thread2 function
Thread thread1; // thread 1
Thread thread2; // thread 2
os_mutex_t mutex; // mutex object

The second line enables the system thread, this is what allows multithreading. A placeholder for the preSetup function is then created, this is imperative for the creation of the mutex object. Basically when the Argon first powers on all threads are started, meaning that without calling a special startup function the mutex object is referenced before it’s instantiated. To get around this the preSetup function is called first, allowing for the creation of the object. Finally declarations for the 2 extra threads are made.

// System setup function
void setup() {
    // SERIAL
    Serial.begin(9600); // Starts the serial communication

    // Cloud Function
    Particle.function("callThread2Function", callThread2Function); // allows realtime calling to thread 2

    // Multithreading
    thread1 = Thread("thread1", thread1Function); // instantiation of thread 1
    thread2 = Thread("thread2", thread2Function); // instantiation of thread 2

    // setup pins
    pinMode(redLED, OUTPUT);
    pinMode(yellowLED, OUTPUT);
    pinMode(greenLED, OUTPUT);
}

Line 37 is used to create the cloud function that will be called to unlock the mutex, allowing the third thread to execute. Lines 40 & 41 reference the functions each thread will run. These functions will continue to execute whilst the device is powered on.

// System loop function
void loop() {
    // system thread
    flashLED(greenLED, 3); // flash green led 3 times
    delay(3000); // wait 3 seconds
}

Line 49-54 covers the default loop function, this is executed on the system thread and merely calls the flashLED helper function to flash the green LED three times before halting for three seconds.

// Calls flashLED every X seconds determined by varDelay variable
void thread1Function(void *param) {
	while(true) {
        // do stuff
        flashLED(yellowLED, 5); // flash yellow led 5 times
        os_thread_delay_until(&lastThreadTime, varDelay);	// Delay X seconds between cycles
	}
	// You must not return from the thread function, once thread starts it should never end...
}

This is the function for thread 1. A delay is used to halt execution of the thread for a determined amount of time (signified by the varDelay variable) before calling flashLED to flash the yellow LED five times.

void thread2Function(void *param) {
	while(true) {
        os_mutex_lock(mutex); // lock mutex, code runs once
        WITH_LOCK(Serial) { // lock serial to thread
            Serial.println("Function 2 called");
        }
        flashLED(redLED, 1); // flash red led 1 time
	}
	// You must not return from the thread function, once thread starts it should never end...
}

// function triggered via cloud, unlocks mutex which triggers thread2Function
int callThread2Function(String arg) {
  Serial.println("MUTEX UNLOCK");
  os_mutex_unlock(mutex);
  return 1;
}

Thread 2’s function uses a mutex object to halt execution. The mutex is unlocked by a call to the cloud function aptly named callThread2Function. Once the mutex is unlocked flashLED is called again this this time referencing the red LED and only flashing a single time.

// Flash passed in LED x amount of times
void flashLED(int targetLED, int cycles){
    
    for(int x = 0; x < cycles; x++){
        digitalWrite(targetLED, 1); // turn on
        delay(500); // delay half a second
        digitalWrite(targetLED, 0); // turn off
        delay(500); // delay half a second
    }
}

Finally the simple helper function flashLED which takes two parameters, the target LED and the number of cycles.

Demonstration of Project

Quick demonstration video of the project