1

I know the topic has been covered a few times however I have attempted, or at least tried virtually all solutions however being a fairly new python basher I've not been able to get any of the previous solutions to work.

The basic premise of the script is that its subscribed to a MQTT broker and waiting for commands, the single action commands work 100%, however one of the commands required a loop to run indefinitely until another command is received, thus the most appropriate solution was to run the "loop" in a separate thread while the main subscriber loop continues to "listen" for the next command.

Everything is working 95%, the "static" commands come through and the tasks is run fine, then when the "mtg" command comes through it actions the thread and the loop runs 100%, however this is where it falls down, when the next command is received I can confirm the "if" statement processes the command as it prints the message to the console, but the thread.stop() is not run, or it may be run but it does not terminate the thread --- I'm pulling my hair out trying to figure it out.

Some code:

from sys import exit
import blinkt
import threading
import time

MQTT_SERVER = '192.168.x.x'
MQTT_PORT = 1883
MQTT_TOPIC = 'mytopic'

REDS = [0, 0, 0, 0, 0, 16, 64, 255, 64, 16, 0, 0, 0, 0, 0, 0]

start_time = time.time()

class task(threading.Thread):

     def __init__(self):
         threading.Thread.__init__(self)
         self.kill = threading.Event()
         self.event = threading.Event()
         self._stop = threading.Event()

     def run(self):
#        while not self.kill.is_set(): 
         while True:
             if self.stopped():
                return
             self.start_run()

     def stop(self):
#        self.event.set()
         self._stop.set()

     def stopped(self):
         return self._stop.isSet()

     def start_run(self):
#         while True: <-- no longer needed as the run method loops the process. 
             delta = (time.time() - start_time) * 16
             offset = int(abs((delta % len(REDS)) - blinkt.NUM_PIXELS))

             for i in range(blinkt.NUM_PIXELS):
                 blinkt.set_pixel(i, REDS[offset + i], 0, 0)

             blinkt.show()
             time.sleep(0.1)

def on_connect(client, userdata, flags, rc):
    print('Connected with result code ' + str(rc))
    client.subscribe(MQTT_TOPIC)

def on_message(client, userdata, msg):

    data = msg.payload
    if type(data) is bytes:
        data = data.decode('utf-8')
    data = data.split(',')
    command = data.pop(0)

    if command == 'clr' and len(data) == 0:
        blinkt.clear()
        blinkt.show()
        t1.stop()      #<--- I've tried a few ways to get the task to stop when the "clr" command is recieved
        task.stop()
        return

    if command == 'rgb' and len(data) == 4: #<-- This code block works fine, msg arrives and LEDs are set correctly
        try:
            pixel = data.pop(0)

            if pixel == '*':
                pixel = None
            else:
                pixel = int(pixel)
                if pixel > 7:
                    print('Pixel out of range: ' + str(pixel))
                    return

            r, g, b = [int(x) & 0xff for x in data]

            print(command, pixel, r, g, b)

        except ValueError:
            print('Malformed command: ' + str(msg.payload))
            return
        if pixel is None:
            for x in range(blinkt.NUM_PIXELS):
                blinkt.set_pixel(x, r, g, b)
        else:
            blinkt.set_pixel(pixel, r, g, b)
        blinkt.show()
        return


    if command == 'mtg' and len(data) == 0:
        print(command)
        t1 = task()
        t1.start()   #<-- Here is where the Thread is called to start and seems to run ok
        return

blinkt.set_clear_on_exit()

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_SERVER, MQTT_PORT, 60)
client.loop_forever()
Bobbo_sa
  • 13
  • 1
  • 4

2 Answers2

2

Python program raising exceptions in a python thread

import threading 
import ctypes 
import time 

class thread_with_exception(threading.Thread):

    def __init__(self, name): 
    threading.Thread.__init__(self) 
    self.name = name 

def run(self): 

    # target function of the thread class 
    try: 
        while True: 
            print('running ' + self.name) 
    finally: 
        print('ended') 

def get_id(self): 

    # returns id of the respective thread 
    if hasattr(self, '_thread_id'): 
        return self._thread_id 
    for id, thread in threading._active.items(): 
        if thread is self: 
            return id

def raise_exception(self): 
    thread_id = self.get_id() 
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 
          ctypes.py_object(SystemExit)) 
    if res > 1: 
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) 
        print('Exception raise failure') 

t1 = thread_with_exception('Thread 1') 
t1.start() 
time.sleep(2) 
t1.raise_exception() 
t1.join() 
Kaushik
  • 21
  • 2
  • **ctypes.c_long(** required for Linux else Windows only ! `res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit)) print(f"{self.name} terminated") if res > 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), 0) ` credit to Andrew Abrahamowicz [here](https://code.activestate.com/recipes/496960-thread2-killable-threads). – social Oct 31 '22 at 21:58
1

Your t1 variable is a local, so it gets lost when you exit the on_message function. Also, you're conflating the class task and the instance t1 (task.stop() won't work).

For a quick fix, declare t1 = None as a global, then add global t1 to your on_message function...

However, I'd consider refactoring things so there's an always-running thread to command the Blinkt!, and the MQTT message handler simply sets its state accordingly – something like this. Dry-coded, obviously, so there may be some silliness.

from sys import exit
import blinkt
import threading
import time

MQTT_SERVER = "192.168.x.x"
MQTT_PORT = 1883
MQTT_TOPIC = "mytopic"

REDS = [0, 0, 0, 0, 0, 16, 64, 255, 64, 16, 0, 0, 0, 0, 0, 0]

start_time = time.time()


class BlinktManager(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.stop_event = threading.Event()
        self.mode = None

    def run(self):
        while not self.stop_event.isSet():
            self.tick()
            self.stop_event.wait(0.1)  # instead of sleep

    def tick(self):
        if self.mode == "reds":
            self._tick_reds()

    def _tick_reds(self):
        delta = (time.time() - start_time) * 16
        offset = int(
            abs((delta % len(REDS)) - blinkt.NUM_PIXELS)
        )

        for i in range(blinkt.NUM_PIXELS):
            blinkt.set_pixel(i, REDS[offset + i], 0, 0)

        blinkt.show()

    def clear(self):
        self.mode = None
        blinkt.clear()
        blinkt.show()

    def set_all_pixels(self, r, g, b):
        self.mode = None
        for x in range(blinkt.NUM_PIXELS):
            blinkt.set_pixel(x, r, g, b)
        blinkt.show()

    def set_pixel(self, x, r, g, b):
        self.mode = None
        blinkt.set_pixel(x, r, g, b)
        blinkt.show()

    def begin_reds(self):
        self.mode = "reds"


def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe(MQTT_TOPIC)


def on_message(client, userdata, msg):
    data = msg.payload
    if type(data) is bytes:
        data = data.decode("utf-8")
    data = data.split(",")
    command = data.pop(0)

    if command == "clr" and len(data) == 0:
        blinkt_manager.clear()

    if command == "rgb" and len(data) == 4:
        x = data[0]
        r, g, b = [int(x) & 0xFF for x in data[1:]]
        if x == "*":
            blinkt_manager.set_all_pixels(r, g, b)
        else:
            # TODO: error handling
            blinkt_manager.set_pixel(int(x), r, g, b)

    if command == "mtg" and len(data) == 0:
        blinkt_manager.begin_reds()


blinkt.set_clear_on_exit()

blinkt_manager = BlinktManager()
blinkt_manager.start()

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_SERVER, MQTT_PORT, 60)
client.loop_forever()
AKX
  • 152,115
  • 15
  • 115
  • 172
  • Considering you called that "Dry coded" is amazing, my attempt at task.stop() was flawed from the beginning, i was trying all sorts without sufficient understanding of what the previous example was doing. I've seen a new light on your way of doing this, and of course besides a missing import from my side, it works barring a minor issue that I'm adding to light up the rest of the lights on the strip. – Bobbo_sa May 06 '20 at 08:31
  • "a new light", heh. ;-) Anyway, glad I could help! – AKX May 06 '20 at 08:34