🐍 CircuitPython is the easiest way to program microcontrollers now celebrating over 200 boards!🐍
0

Node-Red, Pi and interfacing Adafruit hardware
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

Re: Node-Red, Pi and interfacing Adafruit hardware

by iwbaxter on Wed Apr 07, 2021 10:26 pm

So, things have changed....

I had thought that the SGP30 was a good tool, it was not the tool I needed. I substituted the SCD30 breakout, but then it added some "complications":

    1. I would prefer to stick as close to the Adafruit python code as much as possible.
    2. This meant I had to get rid of the "Python3 Node" approach, and hopefully not modifying their libraries.

So, the question was, how to do it? There is a Node-Red Daemon which does redirect stdout (the output from Python's print command), stdin (keyboard input) and stderr (error and debugger output). It actually took me some time to use it, but now I love it.

So, once you have the SCD30 working properly, the dashboard, would look like this:
Capture6.JPG
Capture6.JPG (57.43 KiB) Viewed 759 times


The flow looks like this:
Capture7.JPG
Capture7.JPG (76.92 KiB) Viewed 759 times


The Daemon noe is configured like this:
Capture8.JPG
Capture8.JPG (50.09 KiB) Viewed 759 times


The SCD30_test.py code is:

Code: Select all | TOGGLE FULL SIZE
The SCD30 code (requiring no modification to the Adafruit libraries) is:
# SPDX-FileCopyrightText: 2020 by Bryan Siepert, written for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
import time
import board
import busio
import adafruit_scd30
from adafruit_extended_bus import ExtendedI2C as I2C
 
# Create library object using our Bus I2C port
# i2c = busio.I2C(board.SCL, board.SDA)
i2c = I2C(3)
scd = adafruit_scd30.SCD30(i2c)

# Report the temp offset
# print("Temp offset ", scd.temperature_offset)
# Adjust the Temp down by 2.1 degree C
# NewOffset = 1.6
# scd.temperature_offset = NewOffset

# Set the forced_recalibration_reference
# scd.forced_recalibration_reference = 409

while True:
    # since the measurement interval is long (2+ seconds) we check for new data before reading
    # the values, to ensure current readings.
    if scd.data_available:
        CO2 = str(scd.CO2)
        print("CO2|"+CO2)
        print("Temp|"+str(scd.temperature))
        print("Humid|"+str(scd.relative_humidity))
        with open("/home/pi/SCD30.txt", 'w') as file:
          file.write(CO2 + "\n")
    time.sleep(0.5)


So, the next challenge is to turn the output, which I "standardized" to the form of <topic>|<payload", a string seperated with the unix pipe character -> "|"

I did that with the "working" Python3 function node code, it is *such* an easy hack that provides a ton of utility.

Fixes for python 3.5 by zewelor · Pull Request #3 · dejanrizvan/node-red-contrib-python3-function · GitHub
Replace contents of
/home/pi/.node-red/node_modules/node-red-contrib-python3-function/lib/python3-function.js
Line 116:
return cls(**json.loads(json_string.decode('utf-8')))
Line 158:
msg = json.loads(raw_msg.decode('utf-8'))

So, now I get a delimited string into the Python script and can turn it into a proper Python dictionary with the stdOut to Msg node (all python) :

Code: Select all | TOGGLE FULL SIZE
# Released under Beer License
# Clear any leading or trailing whitespace
OrigStr = msg['payload']
# get rid of carriage returnsand leading whitespace
OrigStr = OrigStr.lstrip()
OrigStr = OrigStr.replace('\r','')
if len(OrigStr) >6:
  # Save only the first line
  TmpStr = OrigStr.partition("\n")[0]
  TmpStr = TmpStr.replace('\n','')
  # Turn it into an array
  TmpArray = TmpStr.split("|")
  try:
    # Form the msg object
    msg = {
      "topic": TmpArray[0],
      "payload": round(float(TmpArray[1]),2)
    }
    # return the msg
    return [msg,None]
  except:
    # Form the msg object
    msg = {
      "topic": "Debug",
      "payload": OrigStr
    }
    # return the msg
    return [None,msg]
else:
  # return the msg
  return [None,None]

iwbaxter
 
Posts: 19
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

by iwbaxter on Wed Apr 07, 2021 10:27 pm

So, now that this is into messages, we can direct the data to the appropriate nodes for processing. We do this with a Switch node we call "Split MSG", configured as such:

CaptureA.JPG
CaptureA.JPG (67.03 KiB) Viewed 755 times


And after that, it's what you make it in Node Red...

iwbaxter
 
Posts: 19
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

by franklin97355 on Thu Apr 08, 2021 1:29 pm

@iwbaxter thanks for all the work you have been doing, it's fantastic. Contact support@adafruit.com ...attn phil and we will see if we can reward you for your work.

franklin97355
 
Posts: 21792
Joined: Mon Apr 21, 2008 2:33 pm
Location: Lacomb, OR.

Re: Node-Red, Pi and interfacing Adafruit hardware

by iwbaxter on Sun Apr 11, 2021 5:08 pm

Well, another weekend, and another improvement. I decided to tackle the SGP30/BME280 combination. The BME280, I took back to the orginal from Adafruit's library. I reversed one change to the SGP30 Library (putting back the commented iaq_init in the class initialization code), and left these additions:

Code: Select all | TOGGLE FULL SIZE
    @property
    # pylint: disable=invalid-name
    def featureset(self):
        """SGP30 Featureset"""
        return self.get_iaq_featureset()
       
@property
    # pylint: disable=invalid-name
    def inceptive_baseline(self):
        """SGP30 Inceptive TVOC Baseline"""
        return self.get_iaq_inceptive_baseline()[0]

    @property
    # pylint: disable=invalid-name
    def measure(self):
        """Get both TVOC and CO2"""
        return self.iaq_measure()         

    @property
    # pylint: disable=invalid-name
    def baselines(self):
        """TVOC and Carbon Dioxide Equivalent baseline values"""
        return self.get_iaq_baseline()

    def get_iaq_featureset(self):
        """Get the SGP30 featureset"""
        # name, command, signals, delay
        return self._run_profile(["iaq_featureset", [0x20, 0x2F], 1, 0.01])

    def get_iaq_inceptive_baseline(self):
        """Retreive the IAQ inceptive baseline for TVOC"""
        # name, command, signals, delay
        return self._run_profile(["iaq_get_inceptive_baseline", [0x20, 0xb3], 1, 0.01])

    def set_iaq_TVOC_baseline(self, TVOC):  # pylint: disable=invalid-name
        """Set the baseline for TVOC"""
        if TVOC == 0:
            raise RuntimeError("Invalid baseline")
        buffer = []
        for value in [TVOC]:
            arr = [value >> 8, value & 0xFF]
            arr.append(self._generate_crc(arr))
            buffer += arr
        self._run_profile(["iaq_set_TVOC_baseline", [0x20, 0x77] + buffer, 0, 0.01])


I then removed the Python function node and created a Node-Red Node Daemon node:

Capture1.JPG
Capture1.JPG (47.36 KiB) Viewed 117 times


The code for the SGP30_BME280_test.py script is:

Code: Select all | TOGGLE FULL SIZE
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
""" Example for using the SGP30 and BME280 with CircuitPython and the Adafruit library"""
import os
from os import path
import time
import board
import busio
import math
from adafruit_extended_bus import ExtendedI2C as I2C
import adafruit_sgp30
import adafruit_bme280

from datetime import datetime
# Create library object on our I2C port
# Device is /dev/i2c-3
i2c = I2C(3)
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
##
# INTERACTIVE - Uncomment code if running on command line
#
# Get the featureset
#print("Featureset is: " + str(sgp30.featureset))
# Get the inceptive Baseline
#Inceptive = sgp30.inceptive_baseline
#print("Inceptive TVOC baseline is: " + hex(Inceptive))
##
#
#
# Get the SGP serial number and turn it into a string
SGPSerial = "-".join([hex(i) for i in sgp30.serial]).replace("0x","")
# Create the Serial specific Baseline file name
BaseFile = "/home/pi/SGP30_" + SGPSerial + ".txt"
# Check for and load last baseline date and values
if path.exists(BaseFile):
  with open(BaseFile, 'r') as file:
    lst = [line.rstrip() for line in file]
  # Last time we started a calibration sequence
  LastCalibration = datetime.strptime(lst[0], '%Y-%m-%d %H:%M:%S')
  # Last time was saved the baselines
  LastSave = datetime.strptime(lst[1], '%Y-%m-%d %H:%M:%S')
  # Load the existing baseline values
  eCO2Base = int(lst[2],16)
  TVOCBase = int(lst[3],16)
else:
  # Set these to invalid dates
  LastInit = datetime.strptime("1970-01-01 00:00:00", '%Y-%m-%d %H:%M:%S')
  LastCalibration = datetime.strptime("1970-01-01 00:00:00", '%Y-%m-%d %H:%M:%S')
  LastSave = datetime.strptime("1970-01-01 00:00:00", '%Y-%m-%d %H:%M:%S')
  # Set invalid baseline values
  eCO2Base = 0
  TVOCBase = 0
# Check for an old baseline
BaselineOld = datetime.now() - LastSave
# Divide by 24 Hours * 60 minutes * 60 seconds to get days
BaselineOld = BaselineOld.total_seconds() / (24*60*60)
# 7 or more days old is not valid
if BaselineOld < 7:
  # Initialize the SGP30
  sgp30.iaq_init()
  # Set the Baseline
  sgp30.set_iaq_baseline(eCO2Base, TVOCBase)
  # Do not reset the Last Calibration Date
else:
  # The SGP is going to calibrate
  LastCalibration = datetime.now()
# Begin processing
elapsed_sec = 0
while True:
  # Read the sensors
  thistemp = bme280.temperature
  thisrh = bme280.relative_humidity
  thispressure = bme280.pressure
  thisalt  = bme280.altitude
  # calc the absolute humidity in g/mg3
  """https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/"""
  absTemperature = thistemp + 273.15;
  absHumidity = 6.112;
  absHumidity *= math.exp((17.67 * thistemp) / (243.5 + thistemp));
  absHumidity *= thisrh;
  absHumidity *= 2.1674;
  absHumidity /= absTemperature;
  # According to the chip manual and all sources, this must be g/m3
  sgp30.set_iaq_humidity(absHumidity)
  # Test to see if we can read both at the same time
  readings = sgp30.measure
  eCO2 = readings[0]
  TVOC = readings[1]
  # Every twentieth iteration, output the Baselines
  if elapsed_sec > 20:
    elapsed_sec = 0
    # Get how many hours since the last calibration
    CalibOld = datetime.now() - LastCalibration
    # Divide by 60 minutes * 60 seconds to get hours
    CalibOld = CalibOld.total_seconds() / 3600   
    # Only check and save the Baseline if it has been 12 hours since the last calibration
    if CalibOld > 12:
      # Get how many minutes since the last baseline save
      BaselineOld = datetime.now() - LastSave
      # Divide by 60 seconds to get minutes
      BaselineOld = BaselineOld.total_seconds() / 60     
      # Only retrieve and save if it's been an hour
      if BaselineOld > 59:
        # get the existing baseline from the SGP30
        readings = sgp30.baselines
        eCO2Base = readings[0]
        TVOCBase = readings[1]
        LastSave = datetime.now()
        with open(BaseFile, 'w') as file:
          file.write(datetime.strftime(LastCalibration, '%Y-%m-%d %H:%M:%S') + "\n")
          file.write(datetime.strftime(LastSave, '%Y-%m-%d %H:%M:%S') + "\n")
          file.write(hex(eCO2Base) + "\n")
          file.write(hex(TVOCBase) + "\n")
  # Output what we have
  print("Temp|"+str(thistemp))
  time.sleep(.1)
  print("RelHum|"+str(thisrh))
  time.sleep(.1)
  print("AbsHum|"+str(absHumidity))
  time.sleep(.1)
  print("Alt|"+str(thisalt))
  time.sleep(.1)
  print("Press|"+str(thispressure))
  time.sleep(.1)
  print("eCO2|"+str(eCO2))
  time.sleep(.1)
  print("TVOC|"+str(TVOC))
  # Sleep a minimum of 1 second
  time.sleep(.5)
  # Increment our counter
  elapsed_sec += 1


Now, about this code - it is set to use the device(s) on I2C Bus 3, so you may want to change that if you use the code.

There is a .1 second sleep between output so that the lines do not "bleed" together. I found that to be a problem, I may have to alter the SCD30 code for the same issue, although I have not detected it to date.

The other thing is about baselines and calibration. If no baseline, or an old baseline file exists at the time of the deployment of the flow (/home/pi/SGP30_<Serial>.txt) then the SGP30 will start auto calibration. This will take about 12 hours to complete and should be performed in a "clean" environment where there is no added CO2, etc. Once the calibration is complete, the script will update the Baselines every hour or so.

The structure of this file is simple:
Line 1 - Date and time of last calibration start
Line 2 - Date and time of last save
Line 3 - eCO2 Baseline
Line 4 - TVOC Baseline

Again, I added my little "friend", the Python3 function node "2 msg" with the following code:

Code: Select all | TOGGLE FULL SIZE
# Released under Beer License
# Clear any leading or trailing whitespace
OrigStr = msg['payload']
# get rid of carriage returnsand leading whitespace
OrigStr = OrigStr.lstrip()
OrigStr = OrigStr.replace('\r','')
if len(OrigStr) >6:
  # Save only the first line
  TmpStr = OrigStr.partition("\n")[0]
  TmpStr = TmpStr.replace('\n','')
  # Turn it into an array
  TmpArray = TmpStr.split("|")
  try:
    # Form the msg object
    msg = {
      "topic": TmpArray[0],
      "payload": round(float(TmpArray[1]),2)
    }
    # return the msg
    return [msg,None]
  except:
    # Form the msg object
    msg = {
      "topic": "Debug",
      "payload": OrigStr
    }
    # return the msg
    return [None,msg]
else:
  # return the msg
  return [None,None]


And of course, followed it with the "Switch" node:

Capture2.JPG
Capture2.JPG (36.78 KiB) Viewed 117 times


This is the resulting dashboard (I had to hide the soil sensors):

Capture3.JPG
Capture3.JPG (53.58 KiB) Viewed 117 times

iwbaxter
 
Posts: 19
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

by iwbaxter on Sun Apr 11, 2021 5:11 pm

So, continuing, the flow looks like this:

Capture4.JPG
Capture4.JPG (83.47 KiB) Viewed 117 times


And here is the JSON so you can import it all into Node-Red:

Code: Select all | TOGGLE FULL SIZE
[{"id":"6e4792e2.d1413c","type":"debug","z":"394fcece.136712","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":910,"y":420,"wires":[]},{"id":"9948d897.00ab78","type":"catch","z":"394fcece.136712","name":"","scope":null,"uncaught":false,"x":740,"y":420,"wires":[["6e4792e2.d1413c"]]},{"id":"336a5d64.79aa02","type":"ui_gauge","z":"394fcece.136712","name":"","group":"54991333.36cc3c","order":3,"width":3,"height":3,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"20","max":"30","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"25","seg2":"27","x":730,"y":100,"wires":[]},{"id":"8d5a4f65.5ecd4","type":"ui_gauge","z":"394fcece.136712","name":"","group":"54991333.36cc3c","order":1,"width":3,"height":3,"gtype":"gage","title":"Humid","label":"","format":"{{value}}","min":"20","max":"60","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"31","seg2":"52","x":730,"y":220,"wires":[]},{"id":"b5770088.dc9d3","type":"ui_gauge","z":"394fcece.136712","name":"","group":"df17b484.5087f8","order":1,"width":3,"height":3,"gtype":"gage","title":"CO2 PPM","label":"ppm","format":"{{value}}","min":"400","max":"2000","colors":["#e6e600","#00b500","#e60000"],"seg1":"900","seg2":"1700","x":740,"y":340,"wires":[]},{"id":"7a68b6ba.b07c68","type":"smooth","z":"394fcece.136712","name":"Calc Min","property":"payload","action":"min","count":"360","round":"2","mult":"single","reduce":false,"x":740,"y":60,"wires":[["fa5ce8a2.520e98"]]},{"id":"fa5ce8a2.520e98","type":"change","z":"394fcece.136712","name":"Min","rules":[{"t":"change","p":"topic","pt":"msg","from":"Temp","fromt":"str","to":"Min","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":60,"wires":[["b570ad02.c65f4"]]},{"id":"101b9d27.13f603","type":"smooth","z":"394fcece.136712","name":"Calc Min","property":"payload","action":"min","count":"360","round":"2","mult":"single","reduce":false,"x":740,"y":180,"wires":[["40c8e56d.30d48c"]]},{"id":"40c8e56d.30d48c","type":"change","z":"394fcece.136712","name":"Min","rules":[{"t":"change","p":"topic","pt":"msg","from":"RelHum","fromt":"str","to":"Min","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":180,"wires":[["582e9472.71b22c"]]},{"id":"ee67fbe.e613308","type":"smooth","z":"394fcece.136712","name":"Calc Max","property":"payload","action":"max","count":"360","round":"2","mult":"single","reduce":false,"x":740,"y":20,"wires":[["7b15ae1f.411f5"]]},{"id":"7b15ae1f.411f5","type":"change","z":"394fcece.136712","name":"Max","rules":[{"t":"change","p":"topic","pt":"msg","from":"Temp","fromt":"str","to":"Max","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":20,"wires":[["b570ad02.c65f4"]]},{"id":"96d969c6.be2b98","type":"smooth","z":"394fcece.136712","name":"Calc Max","property":"payload","action":"max","count":"360","round":"2","mult":"single","reduce":false,"x":740,"y":140,"wires":[["b0771170.d405c"]]},{"id":"b0771170.d405c","type":"change","z":"394fcece.136712","name":"Max","rules":[{"t":"change","p":"topic","pt":"msg","from":"RelHum","fromt":"str","to":"Max","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":140,"wires":[["582e9472.71b22c"]]},{"id":"be0dce00.c5e13","type":"smooth","z":"394fcece.136712","name":"Calc Max","property":"payload","action":"max","count":"360","round":"","mult":"single","reduce":false,"x":740,"y":260,"wires":[["bf763bc4.c0bc18"]]},{"id":"e166f98e.7d66f8","type":"smooth","z":"394fcece.136712","name":"Calc Min","property":"payload","action":"min","count":"360","round":"","mult":"single","reduce":false,"x":740,"y":300,"wires":[["2042f190.5d2bce"]]},{"id":"bf763bc4.c0bc18","type":"change","z":"394fcece.136712","name":"Max","rules":[{"t":"change","p":"topic","pt":"msg","from":"CO2","fromt":"str","to":"Max","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":260,"wires":[["d234387f.02bb28"]]},{"id":"2042f190.5d2bce","type":"change","z":"394fcece.136712","name":"Min","rules":[{"t":"change","p":"topic","pt":"msg","from":"CO2","fromt":"str","to":"Min","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":870,"y":300,"wires":[["d234387f.02bb28"]]},{"id":"24b8446a.9a74ac","type":"delay","z":"394fcece.136712","name":"5 sec","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":590,"y":300,"wires":[["be0dce00.c5e13","e166f98e.7d66f8","d234387f.02bb28"]]},{"id":"d6f0e66d.b954d8","type":"delay","z":"394fcece.136712","name":"5 sec","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":590,"y":60,"wires":[["ee67fbe.e613308","7a68b6ba.b07c68","b570ad02.c65f4"]]},{"id":"51fc4b4e.8edf44","type":"delay","z":"394fcece.136712","name":"5 sec","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":590,"y":180,"wires":[["96d969c6.be2b98","101b9d27.13f603","582e9472.71b22c"]]},{"id":"b2a8bc76.aacca","type":"daemon","z":"394fcece.136712","name":"SGP30 BME280","command":"/usr/bin/python3","args":"/home/pi/Documents/SGP30_BME280_test.py","autorun":true,"cr":false,"redo":false,"op":"string","closer":"SIGKILL","x":140,"y":80,"wires":[["671787f5.117a08"],["1db214d.bcbedeb"],[]]},{"id":"671787f5.117a08","type":"python3-function","z":"394fcece.136712","name":"2 msg","func":"# Released under Beer License\n# Clear any leading or trailing whitespace\nOrigStr = msg['payload']\n# get rid of carriage returnsand leading whitespace\nOrigStr = OrigStr.lstrip()\nOrigStr = OrigStr.replace('\\r','')\nif len(OrigStr) >6:\n  # Save only the first line\n  TmpStr = OrigStr.partition(\"\\n\")[0]\n  TmpStr = TmpStr.replace('\\n','')\n  # Turn it into an array\n  TmpArray = TmpStr.split(\"|\")\n  try:\n    # Form the msg object\n    msg = {\n      \"topic\": TmpArray[0],\n      \"payload\": round(float(TmpArray[1]),2)\n    }\n    # return the msg\n    return [msg,None]\n  except:\n    # Form the msg object\n    msg = {\n      \"topic\": \"Debug\",\n      \"payload\": OrigStr\n    }\n    # return the msg\n    return [None,msg]\nelse:\n  # return the msg\n  return [None,None]","outputs":2,"x":270,"y":200,"wires":[["af7974d1.62fb98"],["1db214d.bcbedeb"]]},{"id":"1db214d.bcbedeb","type":"debug","z":"394fcece.136712","name":"Debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":380,"wires":[]},{"id":"af7974d1.62fb98","type":"switch","z":"394fcece.136712","name":"Split msg","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"Temp","vt":"str"},{"t":"eq","v":"RelHum","vt":"str"},{"t":"eq","v":"eCO2","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":420,"y":180,"wires":[["d6f0e66d.b954d8","336a5d64.79aa02"],["51fc4b4e.8edf44","8d5a4f65.5ecd4"],["24b8446a.9a74ac","b5770088.dc9d3"]]},{"id":"b570ad02.c65f4","type":"ui_chart","z":"394fcece.136712","name":"Temp","group":"54991333.36cc3c","order":4,"width":9,"height":3,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"20","ymax":"30","removeOlder":"2","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":1010,"y":40,"wires":[[]]},{"id":"582e9472.71b22c","type":"ui_chart","z":"394fcece.136712","name":"Humid","group":"54991333.36cc3c","order":2,"width":9,"height":3,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"20","ymax":"60","removeOlder":"2","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":1010,"y":160,"wires":[[]]},{"id":"d234387f.02bb28","type":"ui_chart","z":"394fcece.136712","name":"CO2","group":"df17b484.5087f8","order":2,"width":9,"height":3,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"400","ymax":"2000","removeOlder":"2","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":1010,"y":280,"wires":[[]]},{"id":"54991333.36cc3c","type":"ui_group","name":"Environment (BME280)","tab":"fc7ad6e.40b8028","order":5,"disp":true,"width":12,"collapse":true},{"id":"df17b484.5087f8","type":"ui_group","name":"Gasses (SGP30)","tab":"fc7ad6e.40b8028","order":6,"disp":true,"width":12,"collapse":true},{"id":"fc7ad6e.40b8028","type":"ui_tab","name":"Grow Tent","icon":"dashboard","order":2,"disabled":false,"hidden":false}]


Next weekend, I'll tackle the Stemma soil sensors, hopefully it removes some of the errors and issues I have seen. IMHO, it will.

iwbaxter
 
Posts: 19
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

by iwbaxter on Sun Apr 18, 2021 3:41 pm

As promised... I converted the Stemma stuff in Node-Red to run using the Node-Red-Node-Daemon.

First, here is the configuration for the Daemon node:

Capture1.JPG
Capture1.JPG (47.15 KiB) Viewed 47 times


Of course I run this through the "2 msg" python function node, it's in my previous posts so no need to expand on it.

This runs into a "Switch" node, here's the first bit of configuration:

Capture2.JPG
Capture2.JPG (57.05 KiB) Viewed 47 times


And here is what the flow looks like:

Capture3.JPG
Capture3.JPG (66.08 KiB) Viewed 47 times


Here's the JSON for the flow:

Code: Select all | TOGGLE FULL SIZE
[{"id":"24b1a656.02afaa","type":"ui_gauge","z":"394fcece.136712","name":"","group":"dba29111.3e4d1","order":1,"width":3,"height":2,"gtype":"gage","title":"Moisture","label":"","format":"{{value}}","min":"500","max":"1500","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"800","seg2":"1200","x":660,"y":400,"wires":[]},{"id":"27db4506.9fd68a","type":"ui_gauge","z":"394fcece.136712","name":"","group":"47b7ae38.45e89","order":1,"width":3,"height":2,"gtype":"gage","title":"Moisture","label":"","format":"{{value}}","min":"500","max":"1500","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"800","seg2":"1200","x":660,"y":480,"wires":[]},{"id":"df24129e.1f131","type":"ui_gauge","z":"394fcece.136712","name":"","group":"dba29111.3e4d1","order":2,"width":3,"height":2,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"15","max":"35","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"22","seg2":"30","x":650,"y":360,"wires":[]},{"id":"9403a49a.00c4a8","type":"ui_gauge","z":"394fcece.136712","name":"","group":"47b7ae38.45e89","order":2,"width":3,"height":2,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"15","max":"35","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"22","seg2":"30","x":650,"y":440,"wires":[]},{"id":"ff82b221.79417","type":"ui_gauge","z":"394fcece.136712","name":"","group":"d8d02957.9818d8","order":2,"width":3,"height":2,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"15","max":"35","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"22","seg2":"30","x":650,"y":520,"wires":[]},{"id":"4cbfc0a0.e5c9","type":"ui_gauge","z":"394fcece.136712","name":"","group":"d8d02957.9818d8","order":1,"width":3,"height":2,"gtype":"gage","title":"Moisture","label":"","format":"{{value}}","min":"500","max":"1500","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"800","seg2":"1200","x":660,"y":560,"wires":[]},{"id":"83eae28f.89158","type":"ui_gauge","z":"394fcece.136712","name":"","group":"d3f34506.4545f8","order":2,"width":3,"height":2,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"15","max":"35","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"22","seg2":"30","x":650,"y":600,"wires":[]},{"id":"d8a4dda3.7f88a","type":"ui_gauge","z":"394fcece.136712","name":"","group":"d3f34506.4545f8","order":1,"width":3,"height":2,"gtype":"gage","title":"Moisture","label":"","format":"{{value}}","min":"500","max":"1500","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"800","seg2":"1200","x":660,"y":640,"wires":[]},{"id":"e8e94eae.794e6","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"2","mult":"single","reduce":false,"x":520,"y":360,"wires":[["df24129e.1f131"]]},{"id":"4753f2fe.a1a53c","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"2","mult":"single","reduce":false,"x":520,"y":440,"wires":[["9403a49a.00c4a8"]]},{"id":"fc4d3fbf.a0637","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"2","mult":"single","reduce":false,"x":520,"y":520,"wires":[["ff82b221.79417"]]},{"id":"14608945.67ddf7","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"2","mult":"single","reduce":false,"x":520,"y":600,"wires":[["83eae28f.89158"]]},{"id":"a05772bb.fee55","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"0","mult":"single","reduce":false,"x":520,"y":400,"wires":[["24b1a656.02afaa"]]},{"id":"bd339164.261b","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"0","mult":"single","reduce":false,"x":520,"y":480,"wires":[["27db4506.9fd68a"]]},{"id":"13969075.d5af1","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"0","mult":"single","reduce":false,"x":520,"y":560,"wires":[["4cbfc0a0.e5c9"]]},{"id":"b9d5fe7d.6b5f7","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"10","round":"0","mult":"single","reduce":false,"x":520,"y":640,"wires":[["d8a4dda3.7f88a"]]},{"id":"64faace2.687684","type":"daemon","z":"394fcece.136712","name":"Stemma","command":"/usr/bin/python3","args":"/home/pi/Documents/Stemma_Joined.py","autorun":true,"cr":false,"redo":false,"op":"string","closer":"SIGKILL","x":160,"y":380,"wires":[["744b8184.758cb"],["b9acf6f1.af02b8"],[]]},{"id":"293cd8b5.9fcfa8","type":"switch","z":"394fcece.136712","name":"Split msg","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"0x36temp","vt":"str"},{"t":"eq","v":"0x36moist","vt":"str"},{"t":"eq","v":"0x37temp","vt":"str"},{"t":"eq","v":"0x37moist","vt":"str"},{"t":"eq","v":"0x38temp","vt":"str"},{"t":"eq","v":"0x38moist","vt":"str"},{"t":"eq","v":"0x39temp","vt":"str"},{"t":"eq","v":"0x39moist","vt":"str"}],"checkall":"true","repair":false,"outputs":8,"x":300,"y":520,"wires":[["e8e94eae.794e6"],["a05772bb.fee55"],["4753f2fe.a1a53c"],["bd339164.261b"],["fc4d3fbf.a0637"],["13969075.d5af1"],["14608945.67ddf7"],["b9d5fe7d.6b5f7"]]},{"id":"744b8184.758cb","type":"python3-function","z":"394fcece.136712","name":"2 msg","func":"# Released under Beer License\n# Clear any leading or trailing whitespace\nOrigStr = msg['payload']\n# get rid of carriage returnsand leading whitespace\nOrigStr = OrigStr.lstrip()\nOrigStr = OrigStr.replace('\\r','')\nif len(OrigStr) >6:\n  # Save only the first line\n  TmpStr = OrigStr.partition(\"\\n\")[0]\n  TmpStr = TmpStr.replace('\\n','')\n  # Turn it into an array\n  TmpArray = TmpStr.split(\"|\")\n  try:\n    # Form the msg object\n    msg = {\n      \"topic\": TmpArray[0],\n      \"payload\": round(float(TmpArray[1]),2)\n    }\n    # return the msg\n    return [msg,None]\n  except:\n    # Form the msg object\n    msg = {\n      \"topic\": \"Debug\",\n      \"payload\": OrigStr\n    }\n    # return the msg\n    return [None,msg]\nelse:\n  # return the msg\n  return [None,None]\n","outputs":2,"x":150,"y":440,"wires":[["293cd8b5.9fcfa8"],["b9acf6f1.af02b8"]]},{"id":"dba29111.3e4d1","type":"ui_group","name":"(x36) Soil Left Rear","tab":"fc7ad6e.40b8028","order":1,"disp":true,"width":6,"collapse":true},{"id":"47b7ae38.45e89","type":"ui_group","name":"(x37) Soil Right Rear ","tab":"fc7ad6e.40b8028","order":2,"disp":true,"width":6,"collapse":true},{"id":"d8d02957.9818d8","type":"ui_group","name":"(0x38) Soil Left Front","tab":"fc7ad6e.40b8028","order":3,"disp":true,"width":6,"collapse":true},{"id":"d3f34506.4545f8","type":"ui_group","name":"(0x39) Soil Right Front","tab":"fc7ad6e.40b8028","order":4,"disp":true,"width":6,"collapse":true},{"id":"fc7ad6e.40b8028","type":"ui_tab","name":"Grow Tent","icon":"dashboard","order":2,"disabled":false,"hidden":false}]


And finally, the code for the Stemma_Joined.py script:

Code: Select all | TOGGLE FULL SIZE
import time
from board import SCL, SDA
import busio
from adafruit_extended_bus import ExtendedI2C as I2C
from adafruit_seesaw.seesaw import Seesaw

# Device is /dev/i2c-1
i2c = I2C(1)

# Set up the Stemmas by specifying a temp correction,
# use a 0 (or float), do not delete the entry unless the
# device does not exist.
#
TempCorrection = {
  0x36 : 0,
  0x37 : 0,
  0x38 : 0,
  0x39 : 0
}
#
# Create an empty dictionary to hold the devices
Devices = {}
# Initialize the objects
for device_address in sorted(TempCorrection):
  Devices[device_address] = Seesaw(i2c, addr=device_address)
#
# Give the devices .5 sec to stabilize
time.sleep(.5)
#
# Now we can loop and query them
while (True):
  for device_address, device in Devices.items():
    # read moisture level through capacitive touch pad
    touch = device.moisture_read()
    # read temperature from the temperature sensor
    temp = device.get_temp()
    temp = temp + TempCorrection[device_address]
    # Output what we got
    print(hex(device_address) + "temp|" + str(temp))
    time.sleep(.1)
    print(hex(device_address) + "moist|" + str(touch))
    time.sleep(.1)
  # Give the devices time to stabilize
  time.sleep(2)
 


Some notes on this Python code:
1. we can apply a temperature adjustment to each individual stemma soil sensor, it's configured in the code, just set the value as desired (decimals and negatives allowed):

TempCorrection = {
0x36 : 0,
0x37 : 0,
0x38 : 0,
0x39 : 0
}

2. This Python dictionary is used to populate and create the devices, so don't go deleting any device addresses for those you want to have reporting, just leave them 0.

# Create an empty dictionary to hold the devices
Devices = {}
# Initialize the objects
for device_address in sorted(TempCorrection):
Devices[device_address] = Seesaw(i2c, addr=device_address)
#

3. The output is a string like this:
0x36temp|20.05
0x36moist|1014

Of course, this gets turned into a message with the topic "0x36temp" or "0x36moist" and the value as the payload.

4. The sleep(.1) between the two print statements is to make sure each line of data is seperated from the last.

iwbaxter
 
Posts: 19
Joined: Fri Feb 19, 2021 12:49 am

Please be positive and constructive with your questions and comments.