The Atorch AT24 DT24 UD18 Power Meter Family

Atorch provides several power meters with serial interfaces. Usually you can read the values with bluetooth and an app on your smartphone but some devices also offer a serial output so you can connect a microcontroller like an Arduino or an ESP8266/ESP32 to retrieve the measured data.

The sensor sends data permanently via serial interface. This page not only shows how to read this kind of serial device, but also how to read the variable size messages in general.

Atorch AT24 DT24 UD18 Protocol Basics

The Atorch protocol is an unencrypted protocol. Each line begins with two start bytes 0xFF and 0x55  followed by a message type, device type and variable length of payload. The last byte is as checksum. There is no dedicated end marker and the message length can differ depending on the message type, which makes parsing of the message tricky.

FF 55 01 03 00 01 F3 00 00 00 00 06 38 00 00 03 11 00 07 00 0A 00 00 00 12 2E 33 3C 00 00 00 00 00 00 00 4E

Parsing of Data - Some words to the Strategy

Like in most of my serial interface examples I'm following the tutorial "Serial Input Basics" as it gives a nice start for a reliable communication.

The function update() reads incoming bytes. The message doesn't contain a dedicated end tag. Each message starts with 0xFF 0x55 in the first two bytes and this is the only possibility to recognize a new message. Therefore we check continuously the stream for the sequence 0xFF 0x55 and will "restart" each time this sequence was received. This strategy might break an incoming packet if there is some data containing these values (decimal: 65365). Each incoming byte will be stored and the index (ndx) will be incremented. Consecutive data will be added to the receive buffer (receiveChars).

If we have collected the necessary amount of bytes according to the protocol definition parseLine() will be called.

parseLine() calls validateChecksum to verify if the received checksum is correct.

If the checksum is correct the data will be parsed.

If the data is correct and a callback function was defined the callback will be called.

Afterwards the current buffer will be deleted and the index (ndx) resetted to 0.

The microcontroller is ready to receive a new message from the device.

Member Functions of the Atorch Class

I decided to implement the Atorch device as class with following public member functions:

.update() call this method in loop(). It will read available data on the Serial interface and update the distance value accordingly. Each time update() is called only one byte is readed from the serial interface. But as update() is called over and over again in loop it will read the whole message. One advantage is, that a malfunction serial device can't block the execution of update() by flooding data.

Additionally you can define a callback function in your sketch. This callback function will be called, when the transmission of new data was completed.

.setOnNewData(callbackOnNewData) will inform the class to call "callbackOnNewData()" whenever the data was updated completely. Obviously you must define the function callbackOnNewData() in your user sketch.

All values of the payload are available with public member variables. I was just to lazy to write getter functions for each single variable.

The Example Sketch

The most important part in the example sketch is the class Atorch. The other functions are just to get the print out.

/*
   Atorch
   https://github.com/NiceLabs/atorch-console/blob/master/docs/protocol-design.md

   Serial Receive Example 
   two byte start / header, 
   Message Type, Variable Payload, 
   Checksum
   no end character

   Valid telegram
   FF55010100090400000E0000040000000000006401F40085002F00000A093C0000000039
   FF55010200011A00003C0004D40000002000006400000000002600590D363C00000000F0
   FF55010200011A0000000004D40000002000006400000000002A00590E343C000000003F
   FF55010200011A00003C0004D40000002000006400000000002600590D363C00000000F0
   FF5501030001F3000000000638000003110007000A000000122E333C000000000000004E

   by noiasca
   2022-04-01 https://forum.arduino.cc/t/softwareserial-binar-daten-lesen/975779/5
*/

class Atorch {
  protected:
    static const byte numChars = 36;   // receiver buffer size
    byte receivedChars[numChars];      // an array to store the received data
    byte ndx = 0;                      // length of received message
    boolean newData = false;           // flag to indicate when new data is complete
    Stream &stream;                    // a reference to the serial interface
    void (*cbOnNewData)();             // gets called after we received a full message

    void delBuffer()
    {
      memcpy(receivedChars, "", sizeof(receivedChars));
      ndx = 0;
    }

    void debugDataHex()
    {
      for (size_t i = 0; i < ndx; i++)
      {
        if (receivedChars[i] < 0x10) Serial.print('0');
        Serial.print(receivedChars[i], HEX);
        Serial.print(' ');
      }
      Serial.println();
    }

    void parseData()                   // parse data and store to internal variables
    {
      deviceType = receivedChars[3];
      if (deviceType == 1 || deviceType == 2 || deviceType == 3)
      {
        voltage = ((uint32_t)receivedChars[4] << 16 ) + (receivedChars[5] << 8) + receivedChars[6];
        amp = ((uint32_t)receivedChars[7] << 16 ) + (receivedChars[8] << 8) + receivedChars[9];
        wh = ((uint32_t)receivedChars[0x0D] << 24 ) + ((uint32_t)receivedChars[0x0E] << 16 ) + (receivedChars[0x0F] << 8) + receivedChars[0x10];
      }
      if (deviceType == 1)
      {
        price = ((uint32_t)receivedChars[0x11] << 16 ) + (receivedChars[0x12] << 8) + receivedChars[0x13];
        frequency = (receivedChars[0x14] << 8) + receivedChars[0x15];
      }
      if (deviceType == 1 || deviceType == 2)
      {
        watt = ((uint32_t)receivedChars[0x0A] << 16 ) + (receivedChars[0x0B] << 8) + receivedChars[0x0c];
        powerfactor = (receivedChars[0x16] << 8) + receivedChars[0x17];
        temperature = (receivedChars[0x18] << 8) + receivedChars[0x19];
        hour = (receivedChars[0x1A] << 8) + receivedChars[0x1B];
        minute = receivedChars[0x1C];
        second = receivedChars[0x1D];
        backlight = receivedChars[0x1E];
      }
      if (deviceType == 3)
      {
        ah = ((uint32_t)receivedChars[0x0A] << 16 ) + (receivedChars[0x0B] << 8) + receivedChars[0x0c];
        usbdminus = (receivedChars[0x11] << 8) + receivedChars[0x12];
        usbdplus =  (receivedChars[0x13] << 8) + receivedChars[0x14];
        temperature = (receivedChars[0x15] << 8) + receivedChars[0x16];
        hour = (receivedChars[0x17] << 8) + receivedChars[0x18];
        minute = receivedChars[0x19];
        second = receivedChars[0x1A];
        backlight = receivedChars[0x1B];
      }
    }

    bool validateChecksum()            // validate if checksum of telegram is correct
    {
      //return true;                   // for debugging only: return with OK
      int result = 0;
      for (int i = 2; i < ndx - 1; i++)
      {
        result += receivedChars[i];
        result &= 0xFF;
      }
      result ^= 0x44;
      //Serial.print(F("calculated checksum ")); Serial.println(result, HEX);
      if (result == receivedChars[ndx - 1])
        return true;
      else
        return false;
    }

    void parseLine()                   // process a telegram
    {
      //Serial.print(F("I101: This is just in:")); Serial.println(receivedChars);         // output all - ASCII
      Serial.print(F("I102: This is just in:")); debugDataHex();                          // output all - HEX
      if (validateChecksum())
      {
        parseData();
        if (cbOnNewData) cbOnNewData();
      }
    }

  public:
    byte messageType = 0;
    // payload data
    byte deviceType = 0;
    uint32_t voltage = 0;
    uint32_t amp = 0;
    uint32_t ah = 0;          // deviceType 3
    uint32_t watt = 0;
    uint32_t wh = 0;
    uint32_t price = 0;
    uint16_t frequency = 0;   // deviceType 1
    uint16_t powerfactor = 0; // deviceType 1
    uint16_t usbdminus = 0;   // deviceType 3
    uint16_t usbdplus = 0;    // deviceType 3
    int16_t temperature = 0;
    uint16_t hour = 0;
    uint8_t minute = 0;
    uint8_t second = 0;
    uint8_t backlight = 0;

    Atorch (Stream &stream) : stream(stream)
    {
      delBuffer();
    }

    void setOnNewData(void (*cbOnNewData)())     // set a callback function. Gets called when new data was received
    {
      (*this).cbOnNewData = cbOnNewData;
    }

    void update()                                // run this member function in loop()
    {
      constexpr byte startMarker = 0xFF;         // start flag
      constexpr byte secondMarker = 0x55;
      if (stream.available() > 0) {
        char rc = stream.read();
        // restart on FF 55
        if (ndx > 0 && receivedChars[ndx - 1] == startMarker && rc == secondMarker)
        {
          delBuffer();
          receivedChars[0] = startMarker;
          receivedChars[1] = rc;
          ndx = 2;
        }
        else
        {
          receivedChars[ndx] = rc;
          if (ndx == 2) messageType = rc;
          ndx++;
        }

        // parse data on reached packet length
        if (messageType == 1 && ndx == (2 + 1 + 32 + 1)) parseLine();

        // sanity check
        if (ndx >= numChars) delBuffer();
      }
    }
};

// create the sensor object and hand over the Serial interface to use:
HardwareSerial &mySerial = Serial;
Atorch device(mySerial);                  // use Hardware Serial (for example for testing with the PC)

//#include <SoftwareSerial.h>             // on an Uno you can use SoftSerial
//SoftwareSerial mySerial(2, 3);          // RX, TX
//Atorch device(mySerial);                // on an UNO you will need SoftSerial to connect to the device

// On a Mega you can simply use
// a Reference to an existing HW Serial:
//HardwareSerial &mySerial = Serial1;
//Atorch device(mySerial);

void output()                           // simple output of data
{
  Serial.print(F("deviceType= ")); Serial.print(device.deviceType); Serial.println();
  if (device.deviceType == 1 || device.deviceType == 2)
  {
    Serial.print(F("voltage=    ")); Serial.print(device.voltage / 10.0); Serial.println('V');
    Serial.print(F("amp=        ")); Serial.print(device.amp / 1000.0); Serial.println('A');
    Serial.print(F("watt=       ")); Serial.print(device.watt / 10.0); Serial.println('W');
    Serial.print(F("wh=         ")); Serial.print(device.wh / 100.0); Serial.println(F("Wh"));
    Serial.print(F("price=      ")); Serial.print(device.price / 100.0); Serial.println(F("/KWh"));
  }
  if (device.deviceType == 1)
  {
    Serial.print(F("powerfactor=")); Serial.print(device.powerfactor / 1000.0); Serial.println();
    Serial.print(F("frequency=  ")); Serial.print(device.frequency / 10.0); Serial.println(F("Hz"));
  }
  if (device.deviceType == 3)
  {
    Serial.print(F("voltage=    ")); Serial.print(device.voltage / 100.0); Serial.println('V');
    Serial.print(F("amp=        ")); Serial.print(device.amp / 100.0); Serial.println('A');
    Serial.print(F("ah=         ")); Serial.print(device.ah / 1000.0); Serial.println(F("Ah"));
    Serial.print(F("wh=         ")); Serial.print(device.wh / 100.0); Serial.println(F("Wh"));
    Serial.print(F("usbdminus=  ")); Serial.print(device.usbdminus / 100.0); Serial.println();
    Serial.print(F("usbdplus=   ")); Serial.print(device.usbdplus / 100.0); Serial.println();
  }
  Serial.print(F("temperature=")); Serial.print(device.temperature); Serial.println('C');
  Serial.print(F("durance=    ")); Serial.print(device.hour); Serial.print(':'); Serial.print(device.minute); Serial.print(':'); Serial.println(device.second);
  Serial.print(F("backlight=  ")); Serial.print(device.backlight); Serial.println();
}

void timerOutput()                     // a timer to output data to serial
{
  static uint32_t previousMillis = 0;  // time management
  const uint16_t interval = 3000;      // interval to display data
  if (millis() - previousMillis >= interval)
  {
    previousMillis = millis();
    output();
  }
}

void setup() {
  Serial.begin(9600);
  mySerial.begin(9600);
  //device.setOnNewData(output);  // register a callback function which gets called when a new telegram was received
}

void loop() {
  device.update();                // you must call the .update() method in your loop()
  timerOutput();
}

How to Test

In the code you will see some (deactivated) debug messages. You can activate them if you encounter a problem with the device communication.

Variants of the Serial Interface

I like to have several options about how to communicate with a device. During development and first tests on an UNO I would use the Serial interface on pin 0 and 1 of the Arduino. Therefore I declare a reference to the Hardware Serial and just use mySerial in the sketch:

HardwareSerial &mySerial = Serial;
Atorch device(mySerial);                  // use Hardware Serial (for example for testing with the PC)

In a productive system where soft serial is used for other communication also, I would use soft serial:

#include <SoftwareSerial.h>             // on an Uno you can use SoftSerial
SoftwareSerial mySerial(2, 3);          // RX, TX
Atorch device(mySerial);                // on an UNO you will need SoftSerial to connect to the device

On an Arduino Mega or a microcontroller with more hardware serial interfaces you can simply use a reference to an existing Serial:

HardwareSerial &mySerial = Serial1;
Atorch device(mySerial);

Disclaimer

Don't expect a perfect example - but in my opinion it should work very reliable including the checksum calculation.

Links

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

First upload: 2022-04-01 | Version: 2024-03-22