Manually Configured Device - Best Practice

I've set-up a Raspberry Pi to be a manually configured thing as described here. The example works fine and demonstrates how to connect to the cloud and interact with cloud variables. That's all good; mission accomplished.

However, in the example the client() blocks completely the script which has left me wondering how best to structure a script to contain some user defined functionality.

The best I could come up with is to put client() on its own thread to leave the main thread for my stuff as in the script below.

I'm new to multi-threaded scripting and was wondering if communicating between the threads as I have is unconditionally safe. It does seem to work, but is it correct? Could it be done better?

I'd very much like to hear the opinion of someone with experience of threads.

I'd also be interested to get other ideas as to how a script might be structured. I've loosely followed the standard Arduino approach of a set-up and a loop.

"""
    A script that implements a very simplistic model of a heating system
    to demonstrate how a script of a manually configured Thing might be 
    structured.
"""

import time
import logging
import threading
import sys
sys.path.append("lib")
from arduino_iot_cloud import ArduinoCloudClient

DEVICE_ID  = b"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
SECRET_KEY = b"YYYYYYYYYYYYYYYYYYYYYYY"

# These variables hold the state of the 
temperature = 0.0
heating_switch = False
thermostat = 0.0

def logging_func():
    logging.basicConfig(
        datefmt="%H:%M:%S",
        format="%(asctime)s.%(msecs)03d %(message)s",
        level=logging.INFO,
    )   

# 
def on_heating_switch_changed(client, value):
    """ This function is executed each time the "heating_switch" variable changes """
    global heating_switch
    
    heating_switch = value
    
    if value:
        logging.info("Heating Switched On")
        
    else:
        logging.info("Heating Switched Off")

def on_thermostat_changed(client, value):
    """ This function is executed whenever the "thermostat" variable value changes.
        Simply copy the Cloud value to the python variable of the same name
    """
    global thermostat
    thermostat = value
    logging.info("thermostat = {}".format(thermostat))
    
def client_thread_func(client):
    """ This behaviour of the client thread is defined in this function """ 
    client.start()


if __name__ == "__main__":

    ##################################################################################################
    # Set up stuff here
    ##################################################################################################
    logging_func()
    client = ArduinoCloudClient(device_id=DEVICE_ID, username=DEVICE_ID, password=SECRET_KEY)
    
    client.register("temperature")
    client.register("heating_switch", value=None, on_write=on_heating_switch_changed)
    client.register("thermostat", value=None, on_write=on_thermostat_changed)
    
    client_thread_func = threading.Thread(target=client_thread_func, args=(client,), daemon=True)
    client_thread_func.start()
    
    ##################################################################################################
    # Execution loop - Do interesting stuff here
    ##################################################################################################
    while True:
    
        # Simplistic model of heating
        if heating_switch:
            if temperature < thermostat:
                temperature += 0.1
            else:
                temperature -= 0.1
            
            # Send Python temperature value to the Cloud variable of the same name
            client["temperature"] = temperature
            # Write back value to make graph more readable
            client["thermostat"] = thermostat 
        
        logging.info("temperature = {}".format(temperature))
        logging.info("thermostat  = {}".format(thermostat))
        
        time.sleep(1)
        pass
    
    

I'm not an expert on this, but was curious about it and asked ChatGPT for some insights :blush: . This is the reply:

In the provided script, there are a few issues that might cause the client() function to block the script. Let's analyze the script and identify potential problems:

Client Initialization: The script uses the ArduinoCloudClient class from the arduino_iot_cloud library. The client is initialized and started using a separate thread (client_thread_func). However, it seems that the client_thread_func is being overwritten and not executed as intended.

Here's the relevant part of the code:

python

client_thread_func = threading.Thread(target=client_thread_func, args=(client,), daemon=True)
client_thread_func.start()

It appears that the variable client_thread_func is being overwritten in the next line and might not be pointing to the correct function. As a result, the thread might not be started as expected.

Temperature Update Logic: The script includes a loop that simulates the behavior of a heating system. However, the loop continuously increments or decrements the temperature and then updates the temperature and thermostat values in the client. This loop is running indefinitely and without any form of user interaction or interruption, causing the script to be stuck in this loop.

python

while True:
    # Simplistic model of heating
    if heating_switch:
        if temperature < thermostat:
            temperature += 0.1
        else:
            temperature -= 0.1
        
        # Send Python temperature value to the Cloud variable of the same name
        client["temperature"] = temperature
        # Write back value to make graph more readable
        client["thermostat"] = thermostat 
    
    logging.info("temperature = {}".format(temperature))
    logging.info("thermostat  = {}".format(thermostat))
    
    time.sleep(1)

To address these issues and prevent the client() function from blocking the script, you should consider the following steps:

Rename Variable: Avoid reusing the name client_thread_func for both the target function and the thread. Choose a different name for the thread variable to avoid overwriting the function reference.

python

client_thread = threading.Thread(target=client_thread_func, args=(client,), daemon=True)
client_thread.start()

User Interaction: Add a way to control the script execution, either by accepting user input or by using a condition that can be toggled. This will allow you to start and stop the simulation as needed.

Graceful Exit: Implement a mechanism to exit the script gracefully when desired. This could involve catching a specific keyboard interrupt signal (Ctrl+C) to terminate the script and stop the client thread.

By addressing these issues, you can structure the script to contain user-defined functionality while preventing the client from blocking the execution.

Let me know if this helps :sweat_smile:

1 Like

Ha! That's one approach I hadn't thought of. Very Modern!

As someone one takes his homework seriously, I had a look at the response.

ChatGPT rightly picked up on the fact it's not much of a thermostat. To be fair that was not the intention, but it's impressive that it came to that conclusion.

It also picked up on the badly named client thread, which should have been client_thread and not client_thread_func. I think the only reason client_thread_func doesn't get overwritten is because of Python namespaces; global versus local. But again, it's an impressive analysis.

It's very interesting to see what ChatGPT made of the problem. While it didn't come up with any concrete blockers, it did come up with some valid points. On another occasion it might just hit the nail on the head.

Thanks for taking the time to look at this.

When I get time I'll put together a stress test and leave it running to see what happens.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.