Getting Artisan to talk to Arduino - Page 4

Discuss roast levels and profiles for espresso, equipment for roasting coffee.
hannson
Posts: 69
Joined: 4 years ago

#31: Post by hannson »

robhh wrote:Good to hear you got it working :)

You can increase the batch size as long as the beans move and change place... Specially at the beginning of the process when density is at its highest.

Yes, you will have to calibrate the temp sensor to get good results. Otherwise your system will perform different from others and the results can not be compared. I would go for a thermocouple sensor because they are linear by nature, and therefore easier to calibrate across the whole range.

I added an alog file which can be used as background profile (20-04-19_0058.alog). You set it as background under ''Roast', 'Background'. I made it with the 'Tools'. 'Designer' tool in Artisan. It is straight forward to use and a nice tool to set up a profile. It might be a bit darker than what you prefer, just try a small batch.
thanks robhh for the knowledge sharing and insight!

Spambojambo
Posts: 3
Joined: 4 years ago

#32: Post by Spambojambo »

I'm coming late to this thread, but I got Arduino and Artisan to talk back and forth over wifi using the ModbusTCP protocol in Arduino. I figured I would share the parts that took me a while to figure out :D

To get it to work, I had to setup the Arduino unit as the ModbusTCP Server (slave), write to holding registers, and then let Artisan can poll the arduino/server to read and write to those registers.

The WiFiModbusServerLED example (under ArduinoModbus>TCP) shows how to start the wifi and modbus server, poll for requests (from artisan), and write to a modbus coil: https://github.com/arduino-libraries/Ar ... verLED.ino

I had to use Modbus registers to pass 16-bit temperatures instead of 1-bit coils -- the code looks like this:
  modbusTCPServer.configureHoldingRegisters(0x00, 4); //start at 0x00 and create 4 registers
  modbusTCPServer.holdingRegisterWrite(0x00,0); //BT at 0x00, set to 0 by default
  modbusTCPServer.holdingRegisterWrite(0x01,0); //Air-fan config at 0x01, set to 0 by default (sent from Artisan, proof of concept)
  modbusTCPServer.holdingRegisterWrite(0x02,0); //DrumSpeed at 0x02, set to 0 by default (measured by arduino nano gyroscope)
  modbusTCPServer.holdingRegisterWrite(0x03,0); //ArduinoTemp at 0x03, set to 0 by default (thermocouple boards temp)
When it's time to grab the thermocouple/bean temps and write them, it looks like this:
    int32_t arduinoTemperature = MAX31855.readAmbient()/100; // retrieve MAX31855 die ambient temperature
    int32_t probeTemperature   = MAX31855.readProbe()/100;   // retrieve thermocouple probe temp
    //check for thermocouple faults/errors
        uint8_t faultCode          = MAX31855.fault();       // retrieve any error codes
        if ( faultCode )                                     // Display error code if present
        {
          Serial.print("Fault code ");
          Serial.print(faultCode);
          Serial.println(" returned.");
          modbusTCPServer.holdingRegisterWrite(0x00,0);
          modbusTCPServer.poll();
        }
        else
        {
         modbusTCPServer.holdingRegisterWrite(0x00, (uint16_t)(probeTemperature)); //update modbus register with thermocouple temp
         modbusTCPServer.holdingRegisterWrite(0x03, (uint16_t)(arduinoTemperature)); //update modbus register with MAX31855 temp
        }
Whenver artisan calls this TCP modbus server, it gets returned whatever the most-recent value of the register.

Configuring Artisan under 'devices' looks like this (the IP address is whatever your network assigns to the arduino.. you can check your wifi router to get this, write it out to serial in arduino ide, or snap and add a display like I did). Each of the registers from the arduino code get added:



I added extra signals as LCDs in Artisan:


And, I added a Slider which has a Modbus Command mapped to it (it writes the slider value to the Arduino, then the normal artisan dashboard picks up the change a few seconds later):



The extra LCDs and Slider show up like this:


I'm using an arduino Nano 33 IOT (which has a little NINA-w102 wifi/bluetooth module), a MAX31855 thermocouple board, and a little OLED display squished into an altoids-like tin.
I roast on an RK-drum on my BBQ and wanted to measure bean temp, so I needed wireless something that could attach to the rotisserie spit handle with a thermocouple running to the drum (and spin round and round and round). I run artisan on a computer inside the house, and I use my android phone to remote-desktop in and view Artisan while I roast outside.

Advertisement
hannson
Posts: 69
Joined: 4 years ago

#33: Post by hannson »

Spambojambo, thanks! That's awesome sharing.

Can you kindly share your codes as well?

One question, why did you decide on ModbusTCP protocol? Is it because you wanted to use the TCP/IP / Wireless network to exchange the information?

Oh.. and do show us the photo of the altoids tin :)

Spambojambo
Posts: 3
Joined: 4 years ago

#34: Post by Spambojambo »

Sure - I posted the code here: https://github.com/spambojambo/ArduinoToArtisanWiFi
Fair warning: there is a bunch of uninteresting code in there to deal with the OLED (posting info and status to the OLED, avoiding screen burning, turning the display off when the drum starts spinning) and attempts to smooth out the drumspeed signal.

Why ModbusTCP? I started down the bluetooth route and discovered my ardiono nano sense ble uses Bluetooth 4.0 (Low Energy). It turns out that 4.0 is low energy because it's not intended to maintain a steady, serial stream of data -- it hosts data other devices poll to get. This wasn't ideal and looked like more code on the arduino and on the receiving PC than I wanted to figure out.

While waiting on a new arduino nano that uses the older bluetooth (Nano 33 IoT), I learned more about Modbus since many devices use that with artisan. I discovered ModbusTCP as an option, realized my new Nano has wifi, and decided to try that route. I realized I could run artisan on a virtual machine on a computer in the house and then connect to it through my Android phone -- much simpler than putting a laptop in harm's way outside next to the smokey bbq roaster.

This is what the phone screen looks like (screen recorded through a windows app -- I didn't have a phone to take a picture of this phone):


Behold the mighty altoids tin....stuck to a USB battery pack... stuck to a rotisserie handle:




When it's time to roast, I screw the handle onto the spi trod where the previously rubberbanded thermometer lived (and spun and spun):


The design has iterated a bit (e.g., that thermocouple wire did not like that 90 degree bend + 500 degrees), and it's not the strongest wifi signal, but it's a fun project.

hannson
Posts: 69
Joined: 4 years ago

#35: Post by hannson »

woohoo!!! Spambojambo thanks for sharing your project, codes and especially the built!!!

hannson
Posts: 69
Joined: 4 years ago

#36: Post by hannson »

Hi..

recently, I found that k-type thermocouple connected to max6675 via Arduino to my raspberry PI seems to have more noise (in the readings), with my raspberry PI powered by a wall plug. a USB isolator doesn't eliminate the noise.

does anyone have similar experience?

i found that running raspberry PI off battery pack eliminates the problem, but i am pretty sure I didn't have this problem previously when my raspberry PI was connected to a wall plug (power supply). And I am using the same wall plug as before.

Additional Notes:
when I used Phidgets connected to the Raspberry PI (with the same k-type thermocouple), I do not have any noise (in the readings) issue.


hmmmmm....

hannson
Posts: 69
Joined: 4 years ago

#37: Post by hannson »

Update:

1. I suspected my ssr supports pwm, based on its specs - fotek ssr-40DA

2. Thus, I have made a minor modification to use analogwrite for the heater instead using this ssr and it works relatively well, with pid control :)


Advertisement
Doodads
Posts: 8
Joined: 5 years ago

#38: Post by Doodads »

Hi guys, has anyone had success communicating with Artisan using the TC4 protocol, but not actually using a TC4?

I'm running into the same issue Smplif8 had - I can only communicate with Artisan if I "hard reset" the Arduino by pressing the physical reset button on the Arduino. After the hard reset Artisan initializes the Arduino connection and the temperatures are displayed perfectly. I can also send the commands (CHAN, READ, etc) via serial monitor and get the correct response from the Arduino.

I don't want to reset the Arduino every time I connect, so this has me stumped for the moment.

My setup:
Arduino Mega 2560
Artisan 2.4.2, connected with USB
4x MAX 6675 thermocouple boards

//Arduino --> Artisan Roaster Scope Communication Test
// TC4 Communication Code adapted from FilePhil https://github.com/FilePhil/TC4-Emulator/blob/master/TC4-Emulator.ino
// TC4 Communication code also adapted from GreenCardigan https://github.com/greencardigan/TC4-shield/blob/master/applications/Artisan/aArtisan/tags/REL-310/libraries/cmndproc/cmndproc.cpp


#include "max6675.h"
#include <SPI.h>

//TC4 Emulator.ino

bool unit_F = false; //true = °F - false = °C

//Set Chip Select Pins
const int tc1CS = 24;
const int tc2CS = 28;
const int tc3CS = 30;
const int tc4CS = 32;
const int thermoDO = 26;
const int thermoCLK = 22;

const long BAUD = 115200;

unsigned long currentMillis;
unsigned long startMillis;
const unsigned long measureInterval = 250; //Max6675 thermocouple boards require about 250ms to stabilize between measurements otherwise they return the previous measurement.

MAX6675 thermocouple1(thermoCLK, tc1CS, thermoDO);
MAX6675 thermocouple2(thermoCLK, tc2CS, thermoDO);
MAX6675 thermocouple3(thermoCLK, tc3CS, thermoDO);
MAX6675 thermocouple4(thermoCLK, tc4CS, thermoDO);

double temp1;
double temp2;
double temp3;
double temp4;
String msg;

// These variables are used for serial data input and parsing. Taken from example 5 of Robin2's thread "Serial Input Basics - updated" https://forum.arduino.cc/index.php?topic=396450.0
#define MAX_CMND_LEN 40 // max overall characters in a command line
#define MAX_RESULT_LEN 9 // max length of result string sent back to caller

char cmndstr[MAX_CMND_LEN + 1];
char result[MAX_RESULT_LEN + 1];
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing

// variables to hold the parsed data
char messageFromPC[numChars] = {0};

int integerFromPC = 0;
float floatFromPC = 0.0;


boolean newData = false;
// end serial variables

double readThermoCouple(MAX6675 &thermo)
{

  double tempT;

  if (unit_F) {
    tempT = thermo.readFarenheit();
  }
  else {
    tempT = thermo.readCelsius();
  }


  if (isnan(tempT)) {
    return 0;
  }
  else {
    return tempT;
  }
}

void parseData() {      // split the data into its parts
  // This will require lots of changes for Artisan. Artisan lines to parse: "CHAN;ijkl" "UNITS;u" "READ"

  char * strtokIndx; // this is used by strtok() as an index

  strtokIndx = strtok(cmndstr, ";");     // get the first part - the string, from cmndst
  strcpy(messageFromPC, strtokIndx); // copy it to messageFromPC
  //Serial.println(messageFromPC);

  if (strcmp(messageFromPC, "READ") == 0) {
    Command_READ();
  }
  else if (strcmp(messageFromPC, "CHAN") == 0) {
    Serial.println("#OK");
  }
  else if (strcmp(messageFromPC, "UNITS") == 0) {
    strtokIndx = strtok(NULL, ";");
    //Serial.println(strtokIndx);

    if (strcmp(strtokIndx, "F") == 0) {
      unit_F = true;
      Serial.println("#OK Fahrenheit");
    }
    else if (strcmp(strtokIndx, "C") == 0) {
      unit_F = false;
      Serial.println("#OK Celsius");
    }
  }
  strtokIndx = strtok(NULL, ","); // this continues where the previous call left off
  integerFromPC = atoi(strtokIndx);     // convert this part to an integer

  strtokIndx = strtok(NULL, ",");
  floatFromPC = atof(strtokIndx);     // convert this part to a float

}

void recvWithEndMarker() { //changed example 5 to eliminate start markers - Artisan uses newlines only, no start markers.
  //removed "recvInProgress" because it's not used in recvwithEndMarker - will this cause issues elsewhere?
  static byte ndx = 0;
  char endMarker = '\n';
  char rc;

  while (Serial.available() > 0) {
    rc = Serial.read();
    //check for newline, buffer overflow
    uint8_t len = strlen(cmndstr);
    if ((rc == '\n') || (len == MAX_CMND_LEN)) { //if the character read is either newline or the string has reached max length, send result back to be parsed.
      strncpy(result, cmndstr, MAX_RESULT_LEN); //copy cmndstr to "result"
      result[MAX_RESULT_LEN] = '\0'; //adds a null character for "safety"
      parseData(); //send the "result" off for parsing
      cmndstr[0] = '\0'; //empty the buffer
      return result; //not sure if I need to return this, hopefully loop will exit in ParseData and this code won't be used?
    }//end if

    else if ( rc != '\r') { //skip carriage returns. If not a CR, add character to array.
      cmndstr[len] = toupper(rc); //converts char to upper case, appends it to cmndstr
      cmndstr[len + 1] = '\0'; //adds NULL to next spot in array
    } //end else
  } //end while
  return NULL;

  //            receivedChars[ndx] = rc;
  //            ndx++;
  //            if (ndx >= numChars) {
  //                ndx = numChars - 1;
  //            }
  //        }
  //        else {
  //            receivedChars[ndx] = '\0'; // terminate the string
  //            ndx = 0;
  //            newData = true;
  //        }
  //    }
}

//============


//Send Data
void Command_READ() {
  Serial.print("0.00,");
  Serial.print(temp1);
  Serial.print(",");
  Serial.print(temp2);
  Serial.print(",");
  Serial.print(temp3);
  Serial.print(",");
  Serial.println(temp4);
}

//Parsing Serial Commands
void handleSerialCommand() {

  recvWithEndMarker();

}

void setup() {
  //  SPI.begin();
  Serial.begin(BAUD);
}


void loop() {
  currentMillis = millis();

  if (currentMillis - startMillis >= measureInterval) //Test whether enough time has passed to poll Max6675 thermocouples
  {
    temp1 = readThermoCouple(thermocouple1);
    temp2 = readThermoCouple(thermocouple2);
    temp3 = readThermoCouple(thermocouple3);
    temp4 = readThermoCouple(thermocouple4);
    startMillis = currentMillis; //add current time to counter to reset interval.
  }


  handleSerialCommand();
}

yokusan
Posts: 15
Joined: 6 years ago

#39: Post by yokusan »

This is a simple Arduino sketch allowing tracking of two temperatures simulating a TC4 response to Artisan READ commands. It is written for two MAX6675 breakout boards with attached thermocouples with an Arduino Nano.You should be able to extend it to four breakout boards.
// Kaffee-Netz Thread: https://www.kaffee-netz.de/threads/roester-eigenbau-wie-artisan-verwenden.119706
#include <max6675.h>

int pinGND = 2;
int pinVCC = 3;
int pinSCK = 4;
int pinCS = 5;
int pinSO = 6;

int pinGND2 = 8;
int pinVCC2 = 9;
int pinSCK2 = 10;
int pinCS2 = 11;
int pinSO2 = 12;

MAX6675 thermocouple(pinSCK, pinCS, pinSO);
MAX6675 thermocouple2(pinSCK2, pinCS2, pinSO2);

void setup() {
  Serial.begin(19200);

  pinMode(pinVCC, OUTPUT);
  pinMode(pinGND, OUTPUT);
  pinMode(pinVCC2, OUTPUT);
  pinMode(pinGND2, OUTPUT);

  digitalWrite(pinVCC, HIGH);
  digitalWrite(pinGND, LOW);
  digitalWrite(pinVCC2, HIGH);
  digitalWrite(pinGND2, LOW);

  delay(500);
}
void loop() {
  if(Serial.available()) {
    String command = Serial.readStringUntil('\n');
    
    if (command.startsWith("CHAN") || command.startsWith("UNIT") || command.startsWith("FILT")) {
        Serial.println("#");
        Serial.flush();
        return;    
    }
    else if(command == "READ") {
      double cet = thermocouple.readCelsius();
      double cbt = thermocouple2.readCelsius();
      Serial.println("0,"+ String(cet) +"," + String(cbt));
      Serial.flush();
    }
  }
  delay(100);
}
Here is how you connect it in artisan:

https://www.dropbox.com/s/j5t1gvsqfd645 ... s.jpg?dl=0

Have fun!

yokusan
Posts: 15
Joined: 6 years ago

#40: Post by yokusan »

And this is an Arduino sketch which allows tracking of one temperature (Bean temperature) and control of a heating lamp via a cheap AC Dimmer module (RobotDyn AC Dimmer Module). The dimmer value is send by Artisan using a serial command. With this you can use the Artisan Software PID to control your AC heating element/lamp while reading bean temperature. This could be extended to more thermocouples if needed. Maybe someone finds this sketch useful. It works well with my halogen lamp air fryer, I posted the mod in this subforum.
/* Dieser Sketch erlaubt das gleichzeitige Auslesen der Thermocouple-Temperatur sowie das Einstellen des Dimmerwertes des RobotDyn Moduls
   in der Klarstein Vitar durch die Röstersoftware Artisan. Der Sketch basiert auf einem Sketch von User coffeewhiskey und simuliert Artisan
   ein TC4 Board. Einstellungen in Artisan siehe Datei.
*/
// siehe auch Kaffee-Netz Thread: https://www.kaffee-netz.de/threads/roester-eigenbau-wie-artisan-verwenden.119706
#include <max6675.h>
#include <RBDdimmer.h>

//#define USE_SERIAL  SerialUSB //Serial for boards whith USB serial port
#define USE_SERIAL  Serial
#define outputPin  3 
#define zerocross  2 // for boards with CHANGEABLE input pins

//dimmerLamp dimmer(outputPin, zerocross); //initialase port for dimmer for ESP8266, ESP32, Arduino due boards
dimmerLamp dimmer(outputPin); //initialase port for dimmer for MEGA, Leonardo, UNO, Arduino M0, Arduino Zero

int outVal = 0;

int pinGND = 9;
int pinVCC = 8;
int pinSCK = 7;
int pinCS = 6;
int pinSO = 5;
int pinDIMMERVCC = 11;

MAX6675 thermocouple(pinSCK, pinCS, pinSO);

void setup() {
  USE_SERIAL.begin(9600);
  dimmer.begin(NORMAL_MODE, ON); //dimmer initialisation: name.begin(MODE, STATE) 
  
  pinMode(pinVCC, OUTPUT);
  pinMode(pinGND, OUTPUT);
  pinMode(pinDIMMERVCC, OUTPUT);

  digitalWrite(pinVCC, HIGH);
  digitalWrite(pinGND, LOW);
  digitalWrite(pinDIMMERVCC, HIGH);

  delay(500);
}

void printSpace(int val)
{
  if ((val / 100) == 0) USE_SERIAL.print(" ");
  if ((val / 10) == 0) USE_SERIAL.print(" ");
}

void loop() {
  
  if(USE_SERIAL.available()) {
    String command = USE_SERIAL.readStringUntil('\n');

    if (command.startsWith("CHAN") || command.startsWith("UNIT") || command.startsWith("FILT")) {
        USE_SERIAL.println("#");
        USE_SERIAL.flush();
        return;
    }
    else if (command == "READ") {
      double cbt = thermocouple.readCelsius();
      printSpace(dimmer.getPower());
      USE_SERIAL.print(dimmer.getPower()); //writes Dimmer Power-Value as TC4 Ambient Temperature
      USE_SERIAL.println(",0," + String(cbt)); //write Dimmer value plus Bean Temperature for Artisan
      USE_SERIAL.flush();
      return;
    } 
    else {
      int preVal = outVal;
      int buf = command.toInt(); //writes string to integer for value of dimmer
      USE_SERIAL.flush();
      if (buf >= 10) {{outVal = buf;}
      digitalWrite(pinDIMMERVCC, HIGH);
      delay(100);    
      dimmer.setPower(outVal); // setPower(0-100%);
      delay(100);
      }
      else {
      {outVal = buf;}
      dimmer.setPower(outVal);
      delay(100);
      digitalWrite(pinDIMMERVCC, LOW);
      delay(100);
          }
      }
  }
  delay(100);
}