Growing stuff and doing things

dstroy0

Zeroes and Ones
Glad you got her figured. Funny, how they can grow like rampant weeds ,,, and then full stop.

,,, the asshole in me almost suggested
" have you tried turning on a light ?" ?
,,, cause indeed- they looked like they were just sitting there.
My brand of humour is slightly offensive in person, and often translates worse as written word.

Impetuous cads, thinking they could pull one over on Destroy.


I freely admit that I do not a have a complete understanding of all the chemistry involved,,,

,,, so I will start with what I DO know.
I have very hard tapwater.
No urea involved, although this product somehow " busts up " the liquid rock, and makes it drinkable for the plants.
View attachment 63750
Like I say, I dont understand the exact action, but consider,,,
If you were accidentally use this product with your RO supply,,,could you level the playing field ( reset ) with the addition of some hard water ?

At any rate, you deserve a fine harvest. Keep up the good work.

I read a bit about urea and calcium ammonium nitrate and when it's a good idea to use higher percentages of ammonium and whatnot. It's a good idea to have some in hydroponic nutrient solution because it acts as a buffer. Too much is bad.


 

dstroy0

Zeroes and Ones
Things haven't gotten worse, definitely still have mg def, but think that I'm on the right track cause the P def quit creeping up. They're eating about 0.6-0.1EC/day at 1.8EC full strength jacks 3.6-2.4-1.2 with 1ml protekt, roots look fine.

I switched to a different brand of epsom last week, I was using care one from stop and shop, I picked up some from walgreens, much smaller crystals that dissolve faster.

I bought some calimagic to add in as well starting today as soon as I make enough RO I'll change the whole thing.

With jacks full strength, calimagic@1.25ml/gal, and protekt@1ml/gal my final EC is 2.1. Which is INSANE.

A quart of calimagic will last about 18 weeks if I use 52.5ml/week (about a dollar a week for what I paid, which is pretty expensive compared to the other stuff I'm using).

So it's been a few weeks of the jacks brand calcium nitrate, one week of a different brand of epsom, and I'm using calimagic with this res change today. Come on and grow normal!!!!!!!!!!!!!!!
 

Buck5050

Underground Chucker
Things haven't gotten worse, definitely still have mg def, but think that I'm on the right track cause the P def quit creeping up. They're eating about 0.6-0.1EC/day at 1.8EC full strength jacks 3.6-2.4-1.2 with 1ml protekt, roots look fine.

I switched to a different brand of epsom last week, I was using care one from stop and shop, I picked up some from walgreens, much smaller crystals that dissolve faster.

I bought some calimagic to add in as well starting today as soon as I make enough RO I'll change the whole thing.

With jacks full strength, calimagic@1.25ml/gal, and protekt@1ml/gal my final EC is 2.1. Which is INSANE.

A quart of calimagic will last about 18 weeks if I use 52.5ml/week (about a dollar a week for what I paid, which is pretty expensive compared to the other stuff I'm using).

So it's been a few weeks of the jacks brand calcium nitrate, one week of a different brand of epsom, and I'm using calimagic with this res change today. Come on and grow normal!!!!!!!!!!!!!!!
I'm so curious to see how this works out for you. I thought about adding in some bottled cal-mag in with the Jacks myself as well. I had a full array of deficiencies across multiple pHenos some looking cal and more looking mag. The odd thing is I do have a couple that looks great with the standard 321 mixes and is growing good. Lower the EC was the key to me getting mine on the right track. I do think I'll have to build back up for optimal conditions with Jacks but at the moment I am at 1.5.
 

dstroy0

Zeroes and Ones
I adapted a watchdog timer to the teensy 4.1


watchdog.cpp
C++:
#ifdef __IMXRT1062__

#include "watchdog.h"
#include <stdint.h> //typedefs
#include <imxrt.h>  //macro definitions

// 4 seconds timeout
#define WDTO 4 //seconds

uint8_t timeoutval = (WDTO - 0.5f) / 0.5f;

void watchdog_init() {

  CCM_CCGR3 |= CCM_CCGR3_WDOG1(3);  // enable WDOG1 clocks
  WDOG1_WMCR = 0;                   // disable power down PDE
  WDOG1_WCR |= WDOG_WCR_SRS | WDOG_WCR_WT(timeoutval);
  WDOG1_WCR |= WDOG_WCR_WDE | WDOG_WCR_WDT | WDOG_WCR_SRE;

}

void HAL_watchdog_refresh() {
  // Watchdog refresh sequence
  WDOG1_WSR = 0x5555;
  WDOG1_WSR = 0xAAAA;
}

#endif // __IMXRT1062__
watchdog.h
Code:
#pragma once

/**
 * HAL Watchdog for Teensy 4.0 (IMXRT1062DVL6A) / 4.1 (IMXRT1062DVJ6A)
 */

void watchdog_init();

void HAL_watchdog_refresh();
 

dstroy0

Zeroes and Ones
hacked read/write anything into adafruit's fram library:

Adafruit_FRAM_I2C.h
C++:
/**************************************************************************/
/*!
    @file     Adafruit_FRAM_I2C.h
    @author   KTOWN (Adafruit Industries)

    Software License Agreement (BSD License)

    Copyright (c) 2013, Adafruit Industries
    All rights reserved.
 *
 *     Adafruit invests time and resources providing this open source code,
 *  please support Adafruit and open-source hardware by purchasing products from
 *     Adafruit!
 *
 *
 *    BSD license (see license.txt)
*/
/**************************************************************************/
#ifndef _ADAFRUIT_FRAM_I2C_H_
#define _ADAFRUIT_FRAM_I2C_H_

#if ARDUINO >= 100
#include <Arduino.h>
#else
#include <WProgram.h>
#endif

#include <Wire.h>

#define MB85RC_DEFAULT_ADDRESS                                                 \
  (0x50)                       ///<* 1010 + A2 + A1 + A0 = 0x50 default */
#define MB85RC_SLAVE_ID (0xF8) ///< SLAVE ID

/*!
 *    @brief  Class that stores state and functions for interacting with
 *            I2C FRAM chips
 */
class Adafruit_FRAM_I2C {
public:
  Adafruit_FRAM_I2C(void);

  boolean begin(uint8_t addr = MB85RC_DEFAULT_ADDRESS);
  void write8(uint16_t framAddr, uint8_t value);
  uint8_t read8(uint16_t framAddr);
  void getDeviceID(uint16_t *manufacturerID, uint16_t *productID);

  //write anything to FRAM
  template <class T> uint16_t writeAnything(uint16_t ee, const T& value)
  {
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++) {
      Adafruit_FRAM_I2C::write8(ee++, *p++);
    }
    return i;
  }

  //read anything from FRAM
  template <class T> uint16_t readAnything(uint16_t ee, T& value)
  {
    byte* p = (byte*)(void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++) {
      *p++ = Adafruit_FRAM_I2C::read8(ee++);    
    } 
    return i;
  }

private:
  uint8_t i2c_addr;
  boolean _framInitialised;
};


#endif

I need it because I write/read the controller config to FRAM and writing an entire class member by member is asking for trouble later on when I inevitably change what is in the config class. Just a couple hundred members, so nothing insane but I would hate to write it one byte at a time with unions or something.....eeewwwww f keeping track of all that noise.

templates are special bits of code that can use generic types, which means that I don't have to repeat the code for every type (variables of different types can be different sizes in memory) or different classes (made of various types/different amounts of members) I want to use.
 

dstroy0

Zeroes and Ones
working some more on the network handler for the new controller

C++:
class RF24PacketTimer
{
  public:
    uint32_t time_since_print[NUM_NODES + 1] = {0};
    uint32_t time_between_packets[NUM_NODES + 1] = {0};
    //how long since we received a packet from N node timer
    uint32_t keep_alive[NUM_NODES + 1] = {0};
    //timer for printing status
    uint32_t keep_alive_print_timer[NUM_NODES + 1] = {0};
};

template <class T> class RF24PacketTimer _commit_payload_template(RF24NetworkHeader& header, uint16_t payloadSize, T& value, RF24PacketTimer& timer)
{
    uint8_t dataBuffer[MAX_PAYLOAD_SIZE]; //MAX_PAYLOAD_SIZE is defined in RF24Network_config.h
    for (int i = 1; i < NUM_NODES; i++) {
      uint32_t elapsed_time = millis() - timer.keep_alive[i];
      if (elapsed_time >= 1000UL) {
        if (millis() - timer.keep_alive_print_timer[i] >= 1000UL) {
          uint32_t temp = elapsed_time / 1000;
          ts();
          Serial.print(F("\"ERROR\":\"NO_RX_RECEIVED_FROM_NODE_")); Serial.print(i); Serial.print(F("_IN_THE_LAST_")); Serial.print(temp); Serial.println(F("_SECONDS\""));
          timer.keep_alive_print_timer[i] = millis();
        }
      }
    }
    if (header.from_node > NUM_NODES) {
      ts();
      Serial.print(F("\"ERROR\":\"RF24_NODE_")); Serial.print(header.from_node); Serial.print(F("_UNKNOWN\"}}\r\n"));
      return timer;
    }
    uint32_t elapsed_millis = millis() - timer.time_between_packets[header.from_node];
    uint32_t elapsed_millis_since_print = millis() - timer.time_since_print[header.from_node];
    if (elapsed_millis > 300UL || elapsed_millis_since_print >= 10000UL) {
      ts();
      Serial.print(F("\"MSG\":\"NODE_")); Serial.print(header.from_node); Serial.print(F("_RX_")); Serial.print(payloadSize);
      Serial.print(F("_BYTES_")); Serial.print(elapsed_millis); Serial.print(F("MS_SINCE_LAST_RX_"));
      Serial.print(elapsed_millis_since_print); Serial.print(F("MS_SINCE_PRINT\"}}\r\n"));
      timer.time_since_print[header.from_node] = millis();
    }
    if (payloadSize == sizeof(value)) {
      network.read(header, &dataBuffer, payloadSize);
      timer.time_between_packets[header.from_node] = millis();
      timer.keep_alive[header.from_node] = millis();
      memmove(&value, &dataBuffer, sizeof(value));
      digitalWrite(RF24_RX_STATUS_LED_PIN, HIGH);
      led_toggle_timer = millis();
      return timer;
    }
    else {

      network.read(header, &dataBuffer, payloadSize); // Get the data
      return timer;
    }
}

//add new nodes here
RF24PacketTimer payload_switch_case(RF24NetworkHeader& header, uint16_t payloadSize, RF24PacketTimer& timer) {
  switch (header.from_node) {
    case 1: {
        timer = _commit_payload_template(header, payloadSize, rx1_payload, timer);
        return timer;
        break;
      }
    case 2: {
        timer = _commit_payload_template(header, payloadSize, rx2_payload, timer);
        return timer;
        break;
      }
    case 3: {
        timer = _commit_payload_template(header, payloadSize, rx3_payload, timer);
        return timer;
        break;
      }
    default: {
        ts();
        Serial.print(F("\"ERROR\":\"RF24_NODE_")); Serial.print(header.from_node); Serial.print(F("_UNKNOWN\"}}\r\n"));
        uint8_t dataBuffer[MAX_PAYLOAD_SIZE]; //MAX_PAYLOAD_SIZE is defined in RF24Network_config.h
        network.read(header, &dataBuffer, payloadSize); // Get the data
        return timer;
        break;
      }
  }
  return timer;
}

void network_handler() {

  //turn off status led if it's time
  if ((millis() - led_toggle_timer) >= led_toggle_on_time_msec) {
    digitalWrite(RF24_RX_STATUS_LED_PIN, LOW);
  }
  network.update();                                 // Check the network regularly
  while (network.available()) {                     // Is there anything ready for us?
    RF24NetworkHeader header;                       // If so, grab it and print it out
    static RF24PacketTimer timer;  // packet timer class for timing packets
    uint16_t payloadSize = network.peek(header);    // Use peek() to get the size of the payload
    //bool ok = false;
    timer = payload_switch_case(header, payloadSize, timer);
  }
}
 

dstroy0

Zeroes and Ones
Here's what I've finished so far on the teensy controller, most of the basic framework that I need is set up and I'll be transplanting functions from the last controller to the new one. I have basic user input functionality, I can more easily test things now.

Next on the to do list is using analog switches to gracefully bring up/down SPI and i2c buses to drop into low power mode/wake up, but I need to do the pcb layouts first. I'm just planning on using perfboard again.
 

Attachments

  • libraries.zip
    6.8 MB · Views: 0
  • t41rf24nc.zip
    10.3 KB · Views: 0

dstroy0

Zeroes and Ones
I'm reworking how I do my logging, this should save me some time when making new log messages, and will let me set up traps when I finish the log level functionality. I'll mirror anything 4 and above out the usb serial monitor as well as to the database (serial1). I want to be able to suppress logging out of the usb serial monitor if it's me using it because having 10000 log messages fly by as you are entering commands is doodoo.

Eventually, I will be able to just feed it string literals and it will add the extra syntax to make that into json.

I guess I could make _log a variadic function and make _combine irrelevant but I don't quite know how to do that yet

This is how I'm using it:
C++:
LOG obj;
String buf = obj._combine(F("\"ERROR\":\"RF24_NODE_"),header.from_node,F("_UNKNOWN\""));  
obj._log(Serial, buf, _NOTICE);

C++:
#ifndef log_H_
#define log_H_

#include <Arduino.h>
#include <String.h>
#include "macros.h"

/*
  TIME_STAMP_BUFFER_SIZE is the buffer for snprintf_P() in print_time_stamp
*/
#ifndef TIME_STAMP_BUFFER_SIZE
#define TIME_STAMP_BUFFER_SIZE 96
#endif

#ifndef CONTROLLER_SOFTWARE_VERSION
#error CONTROLLER_SOFTWARE_VERSION undefined, print_time_stamp() uses CONTROLLER_SOFTWARE_VERSION to generate the time stamp for log messages
#endif

#define TIMESTAMP_VERSION CONTROLLER_SOFTWARE_VERSION

#define _EMERGENCY 0
#define _ALERT 1
#define _CRITICAL 2
#define _ERROR 3
#define _WARNING 4
#define _NOTICE 5
#define _INFORMATIONAL 6
#define _DEBUG 7

class LOG {
  public:
    void _log(Stream& serial, String msg, uint8_t level);
    String _combine(String msg1, String msg2 = "", String msg3 = "", String msg4 = "", String msg5 = "",
                   String msg6 = "", String msg7 = "", String msg8 = "", String msg9 = "", String msg10 = "",
                   String msg11 = "", String msg12 = "", String msg13 = "", String msg14 = "", String msg15 = "",
                   String msg16 = "", String msg17 = "", String msg18 = "", String msg19 = "", String msg20 = ""
                  );
    void print_time_stamp(Stream& serial);
};

#endif

C++:
#ifndef log_CPP_
#define log_CPP_

#include "log.h"
#include <Arduino.h>
#include <String.h>
#include <TimeLib.h>

void LOG::print_time_stamp(Stream& serial) {
  char pream_buf[TIME_STAMP_BUFFER_SIZE] = {'\0'};
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("{\"C_1_v%d\":{\"TS\":{"), TIMESTAMP_VERSION);
  serial.print(pream_buf);
  uint32_t rt = millis() / 1000UL;
  uint32_t ms = millis() % 1000UL;
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("\"DT\":\"%lu\",\"RT\":\"%lu.%lu\"},"), now(), rt, ms);
  serial.print(pream_buf);
}

void LOG::_log(Stream& serial, String msg, uint8_t level)
{
  LOG::print_time_stamp(serial);
  serial.print(msg);
  serial.print(F("}}\r\n"));
}

String LOG::_combine(String msg1, String msg2, String msg3, String msg4, String msg5,
                     String msg6, String msg7, String msg8, String msg9, String msg10,
                     String msg11, String msg12, String msg13, String msg14, String msg15,
                     String msg16, String msg17, String msg18, String msg19, String msg20
                    )
{
  String buf = "";
  buf = msg1 + msg2 + msg3 + msg4 + msg5 + msg6 + msg7 + msg8 + msg9 + msg10
        + msg11 + msg12 + msg13 + msg14 + msg15 + msg16 + msg17 + msg18 + msg19 + msg20;
  return buf;
}
#endif
 

dstroy0

Zeroes and Ones
I figured out how to make this work with a variadic function, I'd like to make append() a parameter of logs() so that it's like logger.logs(message, level); instead of formatting and then logging.

C++:
LOG logger(USER_CONSOLE_SERIAL,DATABASE_CONNECTION_SERIAL);

setup()
{
    //I could move these streams into LOG's constructor, but I don't know if that's a good idea
    Serial.begin(SERIAL_0_BAUD);
    Serial1.begin(SERIAL_1_BAUD);
    
    String buf = logger.append("this_node = ",this_node);
    logger.logs(buf, _INFORMATIONAL);
    
    logger.set_stream_log_level(USER_CONSOLE, _NOTICE);
    logger.set_stream_log_level(DATABASE_CONSOLE, _DEBUG);
}

C++:
#ifndef log_H_
#define log_H_

#include <Arduino.h>
#include <String.h>
#include "macros.h"

#ifndef CONTROLLER_SOFTWARE_VERSION
#error CONTROLLER_SOFTWARE_VERSION undefined, print_time_stamp() uses CONTROLLER_SOFTWARE_VERSION to generate the time stamp for log messages
#endif

#ifndef USER_CONSOLE_SERIAL
#error USER_CONSOLE_SERIAL undefined, #define USER_CONSOLE_SERIAL Stream
#endif

#ifndef DATABASE_CONNECTION_SERIAL
#error DATABASE_CONNECTION_SERIAL undefined, #define DATABASE_CONNECTION_SERIAL Stream
#endif

/*
  TIME_STAMP_BUFFER_SIZE is the buffer for snprintf_P() in print_time_stamp
*/
#define TIME_STAMP_BUFFER_SIZE 96

#define TIMESTAMP_VERSION CONTROLLER_SOFTWARE_VERSION

#define USER_CONSOLE 0
#define DATABASE_CONSOLE 1

#define _EMERGENCY 0
#define _ALERT 1
#define _CRITICAL 2
#define _ERROR 3
#define _WARNING 4
#define _NOTICE 5
#define _INFORMATIONAL 6
#define _DEBUG 7

class LOG {
  public:
    LOG(Stream& _uc, Stream& _dbc) : _user_console(_uc), _database_connection(_dbc)
    {
      _user_console_display_level = _DEBUG;
      _database_console_display_level = _DEBUG;
    }
    
    void set_stream_log_level(uint8_t stream, uint8_t display_this_level = _DEBUG);
      
    void print_time_stamp(Stream& serial);
    void print_log_to_stream(Stream& serial, String msg, uint8_t level);
    
    String append(String msg);
    template<typename... arguments>
    String append(String msg, arguments... user_arguments)
    {
      return msg + append(user_arguments...);
    }   
    
    void logs(String msg, uint8_t level);
    
  private:
    Stream &_user_console;
    Stream &_database_connection;
    uint8_t _user_console_display_level = _DEBUG;
    uint8_t _database_console_display_level = _DEBUG;
};

#endif

C++:
#ifndef log_CPP_
#define log_CPP_

#include "log.h"
#include <Arduino.h>
#include <String.h>
#include <TimeLib.h>

String LOG::append(String msg)
{
  return msg;
}

void LOG::print_log_to_stream(Stream& serial, String msg, uint8_t level)
{
  LOG::print_time_stamp(serial);
  serial.print(msg);
  serial.print(F("}}\r\n"));
}

void LOG::set_stream_log_level(uint8_t stream, uint8_t display_this_level)
{
  if (stream == USER_CONSOLE)
  {
    _user_console_display_level = display_this_level;
  }
  if (stream == DATABASE_CONSOLE)
  {
    _database_console_display_level = display_this_level;
  }
}

void LOG::logs(String msg, uint8_t level)
{
  if (level <= _user_console_display_level)
  {
    LOG::print_log_to_stream(USER_CONSOLE_SERIAL, msg, level);
  }
  if (level <= _database_console_display_level)
  {
    LOG::print_log_to_stream(DATABASE_CONNECTION_SERIAL, msg, level);
  }
}

void LOG::print_time_stamp(Stream& serial) {
  char pream_buf[TIME_STAMP_BUFFER_SIZE] = {'\0'};
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("{\"C_1_v%d\":{\"TS\":{"), TIMESTAMP_VERSION);
  serial.print(pream_buf);
  uint32_t rt = millis() / 1000UL;
  uint32_t ms = millis() % 1000UL;
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("\"DT\":\"%lu\",\"RT\":\"%lu.%lu\"},"), now(), rt, ms);
  serial.print(pream_buf);
}
#endif
 

dstroy0

Zeroes and Ones
It's sorta working, takes log messages and turns them into json

C++:
void send_env_message(uint16_t selected_area) {
  float temp[3] = {0, 0, 0};
  temp[0] = area_config[selected_area].feed_solenoid_open_duration * 0.001;
  temp[1] = area_config[selected_area].feed_solenoid_open_interval * 0.001;
  temp[2] = area[selected_area].timers.time_until_feed * 0.001;

  logger.logs(_INFORMATIONAL,
              PSTR("ENV :{"), PSTR("A"), (selected_area + 1), PSTR(" :{ "),
              PSTR("LP :"), (area[selected_area].feed_line_pressure ? PSTR("NORMAL") : PSTR("LOW,")),
              PSTR("WL :"), (area[selected_area].tray_water_level_full ? PSTR("NORMAL") : PSTR("LOW,")),
              PSTR("TDS :"), (String(area[selected_area].return_line_tds, 2)), PSTR(","),
              PSTR("RT :"), (String(area[selected_area].return_line_temperature, 3)), PSTR(","),
              PSTR("RH :"), (String(area[selected_area].relative_humidity, 1)), PSTR(","),
              PSTR("RHE :"), (String(area[selected_area].relative_humidity_error, 1)), PSTR(","),
              PSTR("AT :"), (String(area[selected_area].air_temperature, 2)), PSTR(","),
              PSTR("ATE :"), (String(area[selected_area].air_temperature_error, 2)), PSTR(","),
              PSTR("CO2 :"), (String(area[selected_area].co2)), PSTR(","),
              PSTR("CO2E :"), (String(area[selected_area].co2_error)), PSTR(","),
              PSTR("FAN :"), (area[selected_area].fan_on ? PSTR("ON") : PSTR("OFF")), PSTR(","),
              PSTR("L :"), (area[selected_area].lights_on ? PSTR("ON") : PSTR("OFF")), PSTR(","),
              PSTR("LI :"), (area[selected_area].intensity), PSTR(","),
              PSTR("FS :"), (area[selected_area].spray_active ? PSTR("OPEN") : PSTR("CLOSED")), PSTR(","),
              PSTR("FD :"), (String(temp[0], 3)), PSTR(","),
              PSTR("FT :"), (area[selected_area].spray_trend ? PSTR("INC") : PSTR("DEC")), PSTR(","),
              PSTR("FI :"), (String(temp[1], 2)), PSTR(","),
              PSTR("TTF :"), (String(temp[2], 3)), PSTR(","),
              PSTR("HAM :"), (String(area[selected_area].relative_humidity, 1)), PSTR(","),
              PSTR("HAH :"), (String(area[selected_area].relative_humidity, 1)), PSTR(","),
              PSTR("HAD :"), (String(area[selected_area].relative_humidity, 1)), PSTR("}}")
             );
}

300 something lines down to 30 and the PSTR() macro stores everything in flash memory. I think the teensy does that automagically though because even without using the PSTR() or F() macros flash memory usage increases and sram use stays static with bare string literals in use.

1621866410154.png


oh man I wish I could use REGEX

1621866838001.png

lol
 

dstroy0

Zeroes and Ones
I added a "non standard" log level _LOG_DEBUG, to help me out in the future when I bork things up


1622211550265.png

log.h
C++:
#ifndef log_H_
#define log_H_

#include <Arduino.h>
#include <String.h>
#include "macros.h" //application specific macro definitions

//controller software version is user defined
#ifndef CONTROLLER_SOFTWARE_VERSION
#error CONTROLLER_SOFTWARE_VERSION undefined, print_time_stamp() uses CONTROLLER_SOFTWARE_VERSION to generate the time stamp for log messages
#endif

//user console serial is user defined
#ifndef USER_CONSOLE_SERIAL
#error USER_CONSOLE_SERIAL undefined, #define USER_CONSOLE_SERIAL Stream
#endif

//database connection serial is user defined
#ifndef DATABASE_CONNECTION_SERIAL
#error DATABASE_CONNECTION_SERIAL undefined, #define DATABASE_CONNECTION_SERIAL Stream
#endif

/*
  TIME_STAMP_BUFFER_SIZE is the buffer for snprintf_P() in print_time_stamp
*/
#define TIME_STAMP_BUFFER_SIZE 96

#define TIMESTAMP_VERSION CONTROLLER_SOFTWARE_VERSION

#define USER_CONSOLE 0
#define DATABASE_CONSOLE 1

#define _EMERGENCY 0
#define _ALERT 1
#define _CRITICAL 2
#define _ERROR 3
#define _WARNING 4
#define _NOTICE 5
#define _INFORMATIONAL 6
#define _DEBUG 7
#define _LOG_DEBUG 8

class LOG {
  public:
    /*
      default constructor LOG object(serial_port_the_user_uses, serial_port_the_database_uses, maximum_message_length_not_counting_timestamp);
    */
    LOG(Stream& _uc, Stream& _dbc, uint16_t _mlmc) :
      _user_console(_uc),
      _database_connection(_dbc),
      _max_log_message_characters(_mlmc)
    {
      _user_console_display_level = _DEBUG;  //log everything by default
      _database_console_display_level = _DEBUG;   //log everything by default
      _log_message = ""; //initialize message bucket
      _log_message.reserve(_max_log_message_characters);  //to avoid heap fragmentation, work in reserved space
    }
    /*
      usage:
      0 = user console
      1 = database console
      object.set_stream_log_level(0, _DEBUG);  will display every valid non-jumbo log message up to and including _DEBUG
      object.set_stream_log_level(0, _EMERGENCY)  will display every valid non-jumbo log message up to and including _EMERGENCY
    */
    void set_stream_log_level(uint8_t stream, uint8_t display_this_level = _DEBUG);
    /*
      usage:
      object.print_time_stamp(Stream); //LOG::print_time_stamp(user_selected_stream)
      object.print_time_stamp(Serial); //will print a time stamp to the indicated stream
    */
    void print_time_stamp(Stream& serial);
    /*
      usage:
      object.logs(message_level, comma separated String arguments...);
      formatting numbers:
      object.logs(message_level, String(number_to_format, decimal_places_to_keep), comma separated String arguments...);
    */
    template<typename... logs_arguments>
    void logs(uint8_t level, String msg, logs_arguments... user_arguments)
    {
      if (level <= _user_console_display_level || level <= _database_console_display_level)
      {
        uint32_t elapsed_time = millis();
        _log_message = "" + String(msg);
        _log_message = LOG::append(_log_message, user_arguments...);
        _log_message = LOG::format(_log_message);
        if (level <= _user_console_display_level)
        {
          LOG::print_log_to_stream(USER_CONSOLE_SERIAL, _log_message, level);
        }
        if (level <= _database_console_display_level)
        {
          LOG::print_log_to_stream(DATABASE_CONNECTION_SERIAL, _log_message, level);
        }
        if (_user_console_display_level == _LOG_DEBUG)
        {
          elapsed_time = millis() - elapsed_time;
          _log_message = PSTR("MSG : _LOG_DEBUG; USER_CONSOLE_SERIAL message took ") + String(elapsed_time) + PSTR("ms to print");
          _log_message = LOG::format(_log_message);
          LOG::print_log_to_stream(USER_CONSOLE_SERIAL, _log_message, level);
        }
      }
    }
  private:
    void print_log_to_stream(Stream& serial, String msg, uint8_t level);    //prints log messages to selected stream
    String format(String msg);    //formats log messages into json documents
    /*
       append accepts a variable number of String arguments
    */
    String append(String msg);
    template<typename... append_arguments>
    String append(String msg, append_arguments... user_arguments)
    {
      msg = msg + append(user_arguments...);
      if (msg.length() >= _max_log_message_characters)
      {
        msg = PSTR("ERROR : LOG append() _max_log_message_characters exceeded!  Check message format; or increase _max_log_message_characters.  The timestamp that gets prepended doesn't count as part of _max_log_message_characters.");
      }
      return msg;
    }
    Stream &_user_console;    //user console stream
    Stream &_database_connection;   //database connection stream
    uint8_t _user_console_display_level;   //display this level and below to user console
    uint8_t _database_console_display_level; //display this level and below to database connection
    String _log_message = "";   //log message bucket
    uint16_t _max_log_message_characters;   //log message character limit
};
#endif

log.cpp
C++:
#ifndef log_CPP_
#define log_CPP_

#include "log.h"
#include <Arduino.h>
#include <String.h>
#include <TimeLib.h>

String LOG::append(String msg)
{
  return msg;
}

String LOG::format(String msg)
{
  msg.replace(" : ", "\":\"");
  msg.replace(" :", "\":\"");
  msg.replace(":\"{ ", "\":{\"");
  msg.replace(" :{ ", "\":{\"");
  msg.replace(" :{", "\":{\"");
  msg.replace(" }", "}");
  msg.replace("}", "\"}");
  msg.replace(", ", ",");
  msg.replace(",", "\",\"");
  msg.replace("\"\"", "\"");
  msg = "\"" + msg + "\"}}\r\n";
  msg.replace("}\"}", "}}");
  msg.replace("}}\"}}", "}}}}");
  return msg;
}

void LOG::print_log_to_stream(Stream& serial, String msg, uint8_t level)
{
  LOG::print_time_stamp(serial);
  serial.print(msg);
}

void LOG::set_stream_log_level(uint8_t stream, uint8_t display_this_level)
{
  const String logging_levels[9] = {"_EMERGENCY", "_ALERT", "_CRITICAL", "_ERROR", "_WARNING", "_NOTICE", "_INFORMATIONAL", "_DEBUG", "_LOG_DEBUG"};
  if (stream == USER_CONSOLE && display_this_level < NELEMS(logging_levels))
  {
    LOG::logs(_NOTICE, PSTR("MSG : USER_CONSOLE LOG LEVEL = "), logging_levels[display_this_level]);
    _user_console_display_level = display_this_level;
  }
  if (stream == DATABASE_CONSOLE)
  {
    LOG::logs(_WARNING, PSTR("MSG : DATABASE_CONNECTION LOG LEVEL = "), logging_levels[display_this_level]);
    _database_console_display_level = display_this_level;
  }
}

void LOG::print_time_stamp(Stream& serial) {
  char pream_buf[TIME_STAMP_BUFFER_SIZE] = {'\0'};
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("{\"C_1_v%d\":{\"TS\":{"), TIMESTAMP_VERSION);
  serial.print(pream_buf);
  uint32_t rt = millis() / 1000UL;
  uint32_t ms = millis() % 1000UL;
  snprintf_P(pream_buf, sizeof(pream_buf), PSTR("\"DT\":\"%lu\",\"RT\":\"%lu.%03lu\"},"), now(), rt, ms);
  serial.print(pream_buf);
}
#endif
 

dstroy0

Zeroes and Ones
Getting ready for a new run, cleaned out the tents today. I drilled a bunch more holes into the bottom of the trash can pots.

I'm going to try grape ape and see how it does.

Working on the new controller software is taking quite a bit of time, I'm able to reuse a lot of stuff but some things need to be rewritten, and I'm spinning up things that I wanted to for a while, like the logging function which has saved quite a bit of time when writing new functions.
 

dstroy0

Zeroes and Ones
save/loading from fram works now, I ended up putting a struct inside the area_config classes that has all the configuration variables that get written to/read from fram, then making the copy constructor just copy that struct so I can use assignment and comparison operators over the whole struct.

The only variable outside of the structure is feed_solenoid_pin, cant figure out how to pass the object number to the inner controller_config_variables struct.

1622405686005.png

C++:
class controller_config
{
  private:
    uint16_t controller_config_object_number; //what object I am
    static uint16_t controller_config_object_count; //how many of these objects total exist
  public:
    controller_config() //constructor (what do)
    {
      controller_config_object_number = ++controller_config_object_count; //increment static member for every instance of this class
    }
    ~controller_config() //deconstructor (bye bye)
    {
      --controller_config_object_count; //decrement static member when deconstructing
    }
    /*
       feed_solenoid_pin will correspond to the macro inserted in area_feed_solenoid_pin[]
       else, it will be 99 if the object number is greater than the total number of controlled areas
    */
    uint8_t feed_solenoid_pin = (controller_config_object_number <= (NUM_CONTROLLED_AREAS - 1)) ? area_feed_solenoid_pin_lookup_table[controller_config_object_number] : 99;

    struct controller_config_variables {
      uint16_t _version = CONTROLLER_SOFTWARE_VERSION;

      bool cozir_command = DEFAULT_CONTROLLER_CONFIG_VARIABLES_COZIR_COMMAND;
      bool fade_tick_recalc = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FADE_TICK_RECALC;
      bool is_flowering = DEFAULT_CONTROLLER_CONFIG_VARIABLES_IS_FLOWERING;
      bool co2_enable = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_ENABLE;
      bool humidifier_enable = DEFAULT_CONTROLLER_CONFIG_VARIABLES_HUMIDIFIER_ENABLE;
      bool ssr_outlet_on_with_lights = DEFAULT_CONTROLLER_CONFIG_VARIABLES_SSR_OUTLET_ON_WITH_LIGHTS;
      uint8_t fan_min_on_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FAN_MIN_ON_DURATION;
      uint8_t fan_min_off_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FAN_MIN_OFF_DURATION;
      uint16_t fan_max_off_lights_off = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FAN_MAX_OFF_LIGHTS_OFF;
      uint8_t fan_min_on_lights_off = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FAN_MIN_ON_LIGHTS_OFF;
      uint8_t lights_on_time = DEFAULT_CONTROLLER_CONFIG_VARIABLES_LIGHTS_ON_TIME;
      uint8_t lights_off_time = DEFAULT_CONTROLLER_CONFIG_VARIABLES_LIGHTS_OFF_TIME;
      uint8_t intensity = DEFAULT_CONTROLLER_CONFIG_VARIABLES_INTENSITY;
      uint8_t intensity_min = DEFAULT_CONTROLLER_CONFIG_VARIABLES_INTENSITY_MIN;
      uint8_t intensity_max = DEFAULT_CONTROLLER_CONFIG_VARIABLES_INTENSITY_MAX;
      uint8_t fade_lights_in_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FADE_LIGHTS_IN_DURATION;
      uint8_t fade_lights_out_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FADE_LIGHTS_OUT_DURATION;
      uint8_t vegetative_light_period = DEFAULT_CONTROLLER_CONFIG_VARIABLES_VEGETATIVE_LIGHT_PERIOD;
      uint8_t flowering_light_period = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FLOWERING_LIGHT_PERIOD;
      uint16_t co2_on_time = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_ON_TIME;
      uint16_t co2_off_time = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_OFF_TIME;
      uint16_t co2_target = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_TARGET;
      uint16_t co2_vegetative_ppm_target = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_VEGETATIVE_PPM_TARGET;
      uint16_t co2_flowering_ppm_target = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_FLOWERING_PPM_TARGET;
      uint8_t co2_deadband = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_DEADBAND;
      float air_relative_humidity_target = DEFAULT_CONTROLLER_CONFIG_VARIABLES_AIR_RELATIVE_HUMIDITY_TARGET;
      float air_relative_humidity_deadband = DEFAULT_CONTROLLER_CONFIG_VARIABLES_AIR_RELATIVE_HUMIDITY_DEADBAND;
      float air_temperature_target = DEFAULT_CONTROLLER_CONFIG_VARIABLES_AIR_TEMPERATURE_TARGET;
      float air_temperature_deadband = DEFAULT_CONTROLLER_CONFIG_VARIABLES_AIR_TEMPERATURE_DEADBAND;
      uint32_t feed_solenoid_open_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FEED_SOLENOID_OPEN_DURATION;
      uint32_t feed_solenoid_open_interval = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FEED_SOLENOID_OPEN_INTERVAL;
      uint32_t feed_solenoid_open_duration_trim = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FEED_SOLENOID_OPEN_DURATION_TRIM;
      uint32_t feed_solenoid_trim_step_size = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FEED_SOLENOID_TRIM_STEP_SIZE;
      uint32_t feed_solenoid_trim_adjust_delay = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FEED_SOLENOID_TRIM_ADJUST_DELAY;
      uint32_t relative_humidity_window_buffer_sample_delay = DEFAULT_CONTROLLER_CONFIG_VARIABLES_RELATIVE_HUMIDITY_WINDOW_BUFFER_SAMPLE_DELAY;
      uint32_t co2_dose_duration = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_DOSE_DURATION;
      uint32_t co2_dose_interval = DEFAULT_CONTROLLER_CONFIG_VARIABLES_CO2_DOSE_INTERVAL;
      uint32_t fade_lights_up_tick_delay = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FADE_LIGHTS_UP_TICK_DELAY;
      uint32_t fade_lights_down_tick_delay = DEFAULT_CONTROLLER_CONFIG_VARIABLES_FADE_LIGHTS_DOWN_TICK_DELAY;
      uint32_t humidifier_power_enable_delay = DEFAULT_CONTROLLER_CONFIG_VARIABLES_HUMIDIFIER_POWER_ENABLE_DELAY;
    };
    controller_config_variables var;

    //copy constructor
    controller_config(const controller_config_variables &p1)
    {
      var._version = p1._version;
      var.cozir_command = p1.cozir_command;
      var.fade_tick_recalc = p1.fade_tick_recalc;
      var.is_flowering = p1.is_flowering;
      var.co2_enable = p1.co2_enable;
      var.humidifier_enable = p1.humidifier_enable;
      var.ssr_outlet_on_with_lights = p1.ssr_outlet_on_with_lights;
      //var.feed_solenoid_pin = p1.feed_solenoid_pin;
      var.fan_min_on_duration = p1.fan_min_on_duration;
      var.fan_min_off_duration = p1.fan_min_off_duration;
      var.fan_max_off_lights_off = p1.fan_max_off_lights_off;
      var.fan_min_on_lights_off = p1.fan_min_on_lights_off;
      var.lights_on_time = p1.lights_on_time;
      var.lights_off_time = p1.lights_off_time;
      var.intensity = p1.intensity;
      var.intensity_min = p1.intensity_min;
      var.intensity_max = p1.intensity_max;
      var.fade_lights_in_duration = p1.fade_lights_in_duration;
      var.fade_lights_out_duration = p1.fade_lights_out_duration;
      var.vegetative_light_period = p1.vegetative_light_period;
      var.flowering_light_period = p1.flowering_light_period;
      var.co2_on_time = p1.co2_on_time;
      var.co2_off_time = p1.co2_off_time;
      var.co2_target = p1.co2_target;
      var.co2_vegetative_ppm_target = p1.co2_vegetative_ppm_target;
      var.co2_flowering_ppm_target = p1.co2_flowering_ppm_target;
      var.co2_deadband = p1.co2_deadband;
      var.air_relative_humidity_target = p1.air_relative_humidity_target;
      var.air_relative_humidity_deadband = p1.air_relative_humidity_deadband;
      var.air_temperature_target = p1.air_temperature_target;
      var.air_temperature_deadband = p1.air_temperature_deadband;
      var.feed_solenoid_open_duration = p1.feed_solenoid_open_duration;
      var.feed_solenoid_open_interval = p1.feed_solenoid_open_interval;
      var.feed_solenoid_open_duration_trim = p1.feed_solenoid_open_duration_trim;
      var.feed_solenoid_trim_step_size = p1.feed_solenoid_trim_step_size;
      var.feed_solenoid_trim_adjust_delay = p1.feed_solenoid_trim_adjust_delay;
      var.relative_humidity_window_buffer_sample_delay = p1.relative_humidity_window_buffer_sample_delay;
      var.co2_dose_duration = p1.co2_dose_duration;
      var.co2_dose_interval = p1.co2_dose_interval;
      var.fade_lights_up_tick_delay = p1.fade_lights_up_tick_delay;
      var.fade_lights_down_tick_delay = p1.fade_lights_down_tick_delay;
      var.humidifier_power_enable_delay = p1.humidifier_power_enable_delay;
    }
};
uint16_t controller_config::controller_config_object_count;
controller_config area_config[NUM_CONTROLLED_AREAS];
 

dstroy0

Zeroes and Ones
env sensor package
ESP32 MCU set up for OTA updates so I don't have to plug it into a computer to flash new code
power in is from 5v usb wall wart
I pick off the 5v usb from the Vin leg of the ESP32's ams1117 regulator and pass it to an external 800mA ams1117 which powers all the sensors, this seemed like the easiest/safest way for me to power this project, but I'm not an electrical engineer
-cozir co2/rh/air temp
-0x49 ads1115 4 leaf temp 100kohm ntc thermistor
-0x50 ads1115 return EC (temp compensated)
-ds18b20 return nutrient temp - these come potted into stainless steel sleeves
-ds18b20 root zone temp
-0x50 ads1115 differential pressure - differential air pressure internal/external to the grow area, air pressure has an effect on transpiration
-tray float switch (logic HIGH when switch closed)

Probably some more stuff, I keep forgetting to write things down.

edit: lol I forgot to even mention the nrf24L01 radio hahahaha
 
Top Bottom