Reverse engineering the wire protocol for the AVHzy CT-3 Power meter

November 26, 2023 7 min read
Reverse engineering the wire protocol for the AVHzy CT-3  Power meter

The AVHzy CT-3 is a great little power meter and one that is known by many other names. It's sold both on Amazon and AliExpress and prices vary quite a bit. Whatever form you find it in though, it is considered to be a great little piece of kit. Indeed it has lots of great features including the ability to connect it to a computer and dump real time data from it.

There is a slight problem with that though - although software is provided to do just that (once you figure out that you need to go to a strange website runnin on a strange port), it only runs on Windows. There do make an SDK available for it as well, but again it's effectively Windows only as it's a precompiled .NET library.

I'd love to be able to use this nicely on Linux, but despite searching their site, I couldn't find any way to reach out to them to ask them if they are willing to share any documentation for the protocol. So the next best thing is of course to start looking at the actual traffic on the wire to see if I can make any sense of it.

This turned out to be harder than I expected as finding software that can tap a USB based COM port is tricker than you might expect. I did find two pieces of software to try out, but the one that actually paid off was Serial Port Monitor. The reason it gets fiddly is because even though the software connects to it as a COM port, it is really just a virtual COM port, and that means you end up capturing USB level data rather than raw serial port traffic.

Still, once you can start seeing something....

Grabbing the data

So just for fun, here are two messages that I caught off the wire:

a5 18 00 00 00 0a 01 0e
80 68 36 01 00 90 2f 01
00 00 90 01 00 4c 0f 00
00 00 10 00 00 a6 5a

and:

a5 08 00 00 00 0a 02 0e 80
00 00 00 00 86 5a

Even with just these two messages we can see an immediate pattern (one that may or may not hold across all messages). Both messages start with 0xa5 and both end with 0x5a. Those could be our start and end markers. Let's mark that out on the shortest message:

a5 - start marker
08 00 00 00 0a 02 0e 80
00 00 00 00 86
5a - end marker

Now, given we know different length messages are used (by the observation that we already have two messages that differ in length), it's possible (and actually quite likely) that the next value is going to be a length of some sort.

💡
Even if we do find a length, it's never clear at this point what the length actually refers to. Is it the whole message or is it a part of the message? Maybe there's a data component and a header and footer component (we know already that there are start and end markers after all).

Again, with our short message, let's look at the next four bytes 08 00 00 00. The reason we're going for four bytes is that the three 0x00 values aren't contributing to the message. If they were part of something else, like a message header, we would expect them to contain other values. Still, we're basically deducing our way backwards through the data, so we'll start here and see where it gets us.

08 00 00 00 looks like a 32 bit little-endian encoded integer, which means the value is 8 in decimal. The entire message is 15 bytes in length, so it doesn't represent the full message. If we remove the start and end markers, that gets us down to 13 bytes, but that's still too many. It's common for lengths to not include themselves in the value. The length is four bytes long (based on our guess), which takes us down to 9 bytes. If this is our length byte, we have one byte unaccounted for - 0x86.

To sanity check, let's look at the larger message which is 31 bytes in length. The value that we'd expect to find the length in is 18 00 00 00. This converts to 24 when converted to decimal. Let's remove the two bytes for the markers and the four bytes for the length. That leaves us with 24 bytes - one byte more than we're looking for, just as before when we were looking at the small message.

So what could explain this? Well, it looks like the header and footer aren't considered part of the message itself, and this could imply that the remaining byte is some sort of checksum or CRC. This would make sense as it would provide a means to ensure that the message didn't arrive garbled. The value was 0x86 on the small message and 0xa6 on the large, meaning it's not a static value, which could support the case for a checksum.

Turns out, it's an XOR checksum

After a bit of fiddling about, I was able to determine that the missing byte is indeed a checksum byte and that it is calculated by xor'ing every byte in the data component of the message. I wrote some Rust code to test my understanding and it seems to be working:

// (1 byte header, 4 bytes length, 1 byte checksum, 1 byte end)
const PACKET_OVERHEAD: usize = 7;
// Happens to be the same for minimum packet size
const MIN_PACKET_SIZE: usize = PACKET_OVERHEAD;
// The header of the packet is 5 bytes (1 byte header and 4 bytes length)
const PACKET_HEADER_LENGTH: usize = 5;

pub fn packet_verify(packet: &[u8]) -> bool {
    if packet.len() < MIN_PACKET_SIZE {
        return false;
    }

    // Calculate payload length
    let payload_length = u32::from_le_bytes([packet[1], packet[2], packet[3], packet[4]]) as usize;

    // Check if the total packet length matches the expected length
    if packet.len() != payload_length + PACKET_OVERHEAD {
        return false;
    }

    let mut checksum = 0u8;
    let payload = &packet[PACKET_HEADER_LENGTH..PACKET_HEADER_LENGTH + payload_length];

    // Calculate checksum
    for &byte in payload {
        checksum ^= byte;
    }

    // Evaluates to true if the XOR of the data section matches the second to last byte
    checksum == packet[PACKET_HEADER_LENGTH + payload_length]
}

So now we have gained a lot more useful information:

a5 - start marker
08 00 00 00 - length in 32 bit little endian format
0a 02 0e 80 00 00 00 00 - myserious data
86 - XOR checksum of the mysterious data
5a - end marker

What next?

Well, so far we can decode messages and validate them. Even if we didn't receive them one at a time, we could easily use the start and end markers to synchronise to the feed and then use the length and checksum byte to validate the messages are not corrupt.

This isn't a bad start really but it doesn't give us much that we can actually use. I suspect the wire format is going to be more complicated than simply reading out some numbers as the device allows two way command and control and appears to be asynchronous in nature. This means we need to look out for things like request and response, as well as status messages and the like to determine the state of the system.

If I had to guess, I'd say that information lies in the next four bytes, but I haven't had time yet to dig further. It would be cool to be able to reverse engineer enough to be able to write a little Linux client for the meter. If anyone reading this happens to have the protocol spec, I'd love to get my hands on it!

achzyreverse engineering
Peter Membrey
Written By Peter Membrey

Peter Membrey is a Chartered Fellow of the British Computer Society, a Chartered IT Professional and a Chartered Engineer. He has a doctorate in engineering and a masters degree in IT specialising in Information Security. He's co-authored over a dozen books and a number of research papers on a variety of topics. These days he is focusing his efforts on creating a more private Internet, raising awareness of STEM and helping people to reach their potential in the field.

Read next