Rhasspy control of Matrix LEDs?

I am using a Matrix Voice on a Rpi4B. I would like to have the Matrix LEDs respond in sync with Rhasspy: one blink on wake, swirling LEDs while recording, and another blink on finish or error. Has someone already written something like this? Thanks.

Do you have the esp32 version or not?

For the esp32 version, I have a nice repo you can find here:

Also, there is hermes led control:

Maybe you can find something you need with those repo’s

1 Like

I do have the ESP version but I’m using it attached to an Rpi4B so I think I will try the hermes control. from glancing over the wiki it appears to be a python3 program, perhaps running in a virtual environment, the uses MQTT messages to control the LEDs. Do I have that right?

I do not use the Hermes Led Control, but I think you are correct.

If installing HermesLedControl, do NOT let it update your drivers - it claims to have more up-to-date drivers, but they are not !

Do you mean the part where the script asks if you want to install/configure the matrix voice?

My experience is with HLC and reSpeaker 2- and 4-mic HATs - not with ESP/matrix.

I know that seeed stopped maintaining their drivers about 3 years ago, and the official work-around was to downgrade Raspbian to the kernel from back then. A user named HinTak made an updated driver, and I guess this is what HLC was offering to install - but shortly after I got my reSpeaker HinTak was maintaining the official driver.

Install the appropriate drivers, and test. Then install HLC without updating your device drivers.

Using rhasspy-satellite, are the matrix kernels still required to reside on the rpi, or are they loaded onto the ESP32? I ask because the HA OS uses Alpine Linux and it seems that the matrix kernels are incompatible with that OS.

Once the first flashing is done, you can remove the device from the Pi and use OTA for updates.
It is not possible to use a Pi with HA OS installed on to do the first flashing, you need to follow the get started for the MV: https://github.com/Romkabouter/ESP32-Rhasspy-Satellite/blob/master/matrixvoice.md

I have two cards, one with HA OS and the other with HA and Rhasspy in docker containers. So, could I flash the matrix from the docker card initially, then run Rhasspy and HA from the OS card with the matrix separate?

Yes, that is indeed possible.

Consider those files a construction kit, not a finished solution. Search for everloop in relation to matrix. They already provided a means to achieve what you want. I just adapted it for my purposes.

/etc/systemd/system/voiceAssistantLED.service

[Unit]
Description=voiceAssistantLED
After=network.target

[Service]
ExecStart=/home/pi/lite_js/voiceAssistantLED.sh start
ExecStop=/home/pi/lite_js/voiceAssistantLED.sh stop
WorkingDirectory=/home/pi/lite_js/
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi

[Install]
WantedBy=multi-user.target

/home/pi/lite_js/voiceAssistantLED.sh

#!/bin/bash
export NVM_DIR="/home/pi/.nvm"
bash $NVM_DIR/nvm.sh;
export PATH=/home/pi/.nvm/versions/node/v12.10.0/bin/:$PATH
LOGFILE=/mnt/fileserver_logs/voiceAssistantLed.log

startScript()
{
        echo $(date) >> $LOGFILE
        /usr/bin/node /home/pi/lite_js/assistant.js 2>&1 | tee -a $LOGFILE
}

stopScript()
{
        echo $(date) >> $LOGFILE
        PID=$(ps -ef|grep voiceAssistantLED.sh|grep -v grep|awk '{print $2}')
        LENGTH=$(echo ${#PID})
        if [ "$LENGTH" -gt 0 ];
        then
                echo "Process IDs of running instance: $PID" >> $LOGFILE
                echo "Killing daemon..." >> $LOGFILE
                for process in "$PID"
                do
                        kill $process >> $LOGFILE 2>&2
                done;
                echo "Daemon stopped." >> $LOGFILE 2>&2
        else
                echo "Daemon was not running." >> $LOGFILE 2>&1
        fi
}

status()
{
        echo $(date) >> $LOGFILE
        PID=$(ps -ef|grep voiceAssistantLED.sh|grep -v grep|awk '{print $2}')
        LENGTH=$(echo ${#PID})
        if [ "$LENGTH" -gt 0 ];
        then
                echo "Daemon is running. Process IDs: $PID" >> $LOGFILE 2>&1
                exit 0
        else
                echo "Daemon is not running." >> $LOGFILE 2>&1
                exit 1
        fi

}

case "$1" in
  start)
        echo "" > $LOGFILE
        echo $(date) >> $LOGFILE
        echo -n "Starting voice assistant status LED script...\n" >> $LOGFILE 2>&1
        startScript
    ;;
  stop)
        echo $(date) >> $LOGFILE
        echo -n "Stopping voice assistant status LED script...\n" >> $LOGFILE 2>&1
        stopScript
    ;;
  restart|force-reload)
        echo "" > $LOGFILE
        echo $(date) >> $LOGFILE
        stopScript
        startScript
    ;;
  status)
        status
        ;;
  *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
    ;;
esac

assistant.js
The distinction between different satellites is required because if it’s not done and you have multiple satellites - all would light up when you actually only address a single one of them (so also if they are in different rooms).

/////////////
//VARIABLES//
/////////////
var localSiteId;// = "satellite02";

var mqtt = require('mqtt');
const fs = require('fs');
var client  = mqtt.connect('mqtts://mqtt.mydomainname', { port: 8883, ca: [ fs.readFileSync('/home/pi/mydomainname_root_cert_base64.crt') ], });
//var client  = mqtt.connect('mqtt://mqtt.mydomainname', { port: 1883 });
//var relay = require('./relayLightLite.js');
//var everloop = require('./everloopLightLite.js');
//var everloop = require('./everloop.js');
var ledPattern = require('./ledPattern.js');
var snipsUserName = 'YOUR_SNIPS_USERNAME';
//var wakeword = 'hermes/hotword/default/detected';
var statusWakeword = 'hermes/hotword/porcupine/detected';
var statusSessionEnd = 'hermes/dialogueManager/sessionEnded';
var statusProcessing = 'hermes/asr/textCaptured';
//var statusProcessing = 'hermes/dialogueManager/processing';
var statusSuccess = 'hermes/dialogueManager/success';
var statusError = 'hermes/dialogueManager/error';
var lightState = 'hermes/intent/'+snipsUserName+':Lights';

fs.readFile('./satelliteName.txt', function (err, data) {
      if (err) {
         return console.error(err);
      }
      localSiteId = data.toString().trim();
      console.log("SatelliteID: " + localSiteId);
   });

ledPattern.blankLeds();

//////////////
//ON CONNECT//
//////////////
client.on('connect', function() {
   console.log('Connected to MQTT server\n');
   client.subscribe(statusWakeword);
   console.log('Subscribed to topic ' + statusWakeword);
   client.subscribe(statusSessionEnd);
   console.log('Subscribed to topic ' + statusSessionEnd);
   client.subscribe(statusSuccess);
   console.log('Subscribed to topic ' + statusSuccess);
   client.subscribe(statusError);
   console.log('Subscribed to topic ' + statusError);
   client.subscribe(statusProcessing);
   console.log('Subscribed to topic ' + statusProcessing);
});
//////////////
//ON MESSAGE//
//////////////
client.on('message', function(topic,message) {
   var message = JSON.parse(message);
//   console.log('Message received for topic: ' + topic);
   switch(topic)
   {

       case statusWakeword:
           if(message.siteId == localSiteId)
           {
                console.log('Wakeword detected');
                ledPattern.processingPulse();
           }
       break;

       case statusSessionEnd:
           if(message.siteId == localSiteId)
           {
                console.log('Session ended.\n');
               ledPattern.stopProcessing();
               ledPattern.blankLeds();
           }
       break;

       case statusProcessing:
           if(message.siteId == localSiteId)
           {
                console.log('Processing reported.');
                ledPattern.processingWheel();
           }
       break;

       case statusSuccess:
           if(message.siteId == localSiteId)
           {
                console.log('Success reported.');
                ledPattern.success();
           }
       break;

       case statusError:
           if(message.siteId == localSiteId)
           {
                console.log('Error reported.');
                ledPattern.error();
           }
       break;

       default:
               ledPattern.stopProcessing();
               ledPattern.blankLeds();
               break;
   }
});

ledPattern.js

const matrix = require("@matrix-io/matrix-lite");
var matrix_device_leds = 0;// Holds amount of LEDs on MATRIX device
var ledMethods = {};// Declaration of method controls at the end
var waitingToggle = false;
var counter = 0;
var minimumUnit=50;
var blinkInterval=250;
var stopRequested=false;

var colorSuccess="#00CC00";
var colorError="#FF0000";
var colorProcessing="#0000ff";

function Sleep(milliseconds) {
   return new Promise(resolve => setTimeout(resolve, milliseconds));
}

ledMethods.stopProcessing = async function()
{
        stopRequested=true;
        await Sleep(minimumUnit+5);
        ledMethods.blankLeds();
}

ledMethods.blankLeds = async function()
{
        matrix.led.set({});
}

ledMethods.processingPulse = async function()
{
        ledMethods.stopProcessing();
        await Sleep(minimumUnit+5);
        stopRequested=false;

        while(!stopRequested)
        {
                matrix.led.set({
                        b: (Math.round((Math.sin(counter) + 1) * 100) + 10),// Math used to make pulsing effect
                });
                counter = counter + 0.2;
                await Sleep(minimumUnit);
        }

        matrix.led.set({});
}

ledMethods.processingWheel = async function()
{
        ledMethods.stopProcessing();
        await Sleep(minimumUnit+5);

        stopRequested=false;
        var ledArray = [];
        ledArray.length = matrix.led.length;

        while(!stopRequested)
        {
                for(i=0; i<matrix.led.length; i++)
                {
                        if(stopRequested)        // Will lead to a faster exit than the surrounding while true loop
                                return;

                        ledArray = [];

                        ledArray[getAbsolutePin(i)] = colorProcessing;
                        ledArray[getAbsolutePin(i+1)] = colorProcessing;
                        ledArray[getAbsolutePin(i+2)] = colorProcessing;
                        ledArray[getAbsolutePin(i+3)] = colorProcessing;

                        ledArray[getAbsolutePin(i+6)] = colorProcessing;
                        ledArray[getAbsolutePin(i+7)] = colorProcessing;
                        ledArray[getAbsolutePin(i+8)] = colorProcessing;
                        ledArray[getAbsolutePin(i+9)] = colorProcessing;

                        ledArray[getAbsolutePin(i+12)] = colorProcessing;
                        ledArray[getAbsolutePin(i+13)] = colorProcessing;
                        ledArray[getAbsolutePin(i+14)] = colorProcessing;
                        ledArray[getAbsolutePin(i+15)] = colorProcessing;

                        matrix.led.set(ledArray);
                        await Sleep(200);
                }
        }

        matrix.led.set({});
}

ledMethods.processingCircle = async function()
{
        ledMethods.stopProcessing();
        await Sleep(minimumUnit+5);


        stopRequested=false;
        var ledArray = [];
        ledArray.length = matrix.led.length;

        while(!stopRequested)
        {
                for(i=0; i<matrix.led.length; i++)
                {
                        if(stopRequested)        // Will lead to a faster exit than the surrounding while true loop
                                return;

                        ledArray = [];

                        ledArray[getAbsolutePin(i)] = colorProcessing;
                        ledArray[getAbsolutePin(i+1)] = colorProcessing;
                        ledArray[getAbsolutePin(i+2)] = colorProcessing;

                        matrix.led.set(ledArray);
                        await Sleep(300);
                }
        }

        matrix.led.set({});
}

ledMethods.success = async function()
{
        ledMethods.stopProcessing();

        ledArray = [];

        for(i=0; i<matrix.led.length; i++)
        {
                ledArray[i] = colorSuccess;
                matrix.led.set(ledArray);
                await Sleep(minimumUnit);
        }

        for(i=0; i<3; i++)
        {
                matrix.led.set(colorSuccess);
                await Sleep(blinkInterval);
                matrix.led.set({});
                await Sleep(blinkInterval);
        }

        matrix.led.set({});
}

ledMethods.error = async function()
{
        ledMethods.stopProcessing();

        ledArray = [];

        for(i=0; i<matrix.led.length; i++)
        {
                ledArray[i] = colorError;
                matrix.led.set(ledArray);
                await Sleep(minimumUnit);
        }

        for(i=0; i<3; i++)
        {
                matrix.led.set(colorError);
                await Sleep(blinkInterval);
                matrix.led.set({});
                await Sleep(blinkInterval);
        }

        matrix.led.set({});
}


function getAbsolutePin(relativePin)
{
        var result = relativePin;

        while(result >= matrix.led.length)
        {
                result = result-matrix.led.length;
        }

        while(result < 0)
        {
                result = result+matrix.led.length;
        }

        return result;
}

//console.log("LED amount: " + matrix.led.length);
//matrix.led.set(['red', 'gold', 'purple', {}, 'black', '#6F41C1', 'blue', {g:255}]);
//matrix.led.set(['red', 'gold', 'purple', {}, 'black', '#6F41C1', 'blue', {g:255}]);
//matrix.led.set({});

//process.argv.forEach(function (val, index, array) {
//  console.log(index + ': ' + val);
//});

if(process.argv.length >= 3)
{
        if(process.argv[2].length > 0)
        {
                switch(process.argv[2])
                {
                        case "success":
                                ledMethods.success();
                                break;
                        case "error":
                                ledMethod.error();
                                break;
                        case "processingPulse":
                                ledMethods.processingPulse();
                                break;
                        case "processingCircle":
                                ledMethods.processingCircle();
                                break;
                        case "processingWheel":
                                ledMethods.processingWheel();
                                break;
                        case "off":
                                ledMethods.blankLeds();
                                break;
                        default:
                                break;
                }
        }
}

module.exports = ledMethods;