Open source extra-sensitive high resolution TED receiver

Previously on the bloggy blog, I posted a few of my projects related to home data acquisition and to The Energy Detective (TED), a whole-house power measurement device. I made a set of homebrew wireless temperature sensors that display graphs on a digital picture frame, I reverse engineered the TED protocol, built a small self-contained open source TED receiver, and used that design to make a nifty clock.

So, I wanted to take the next step and set up real-time power usage graphing alongside the temperature graphs. The only problem: My sensor receivers are on a different power line phase than the TED. I’d need a TED receiver that can deal with all of the noise that switching power supplies put out, and receive weak TED signals that have been coupled across phases by the utility’s pole transformer. There were probably easier ways to solve this problem, but it was a good excuse to get more experience with analog electronics :)

The end result was this cute little doodad:

Features:

  • Isolated power supplies for the host computer, receiver circuitry, and the power line signal itself. Separate voltage regulators for analog and digital.
  • High-Q LC filter, centered at 125 kHz.
  • Dual outputs: RS-232 serial and USB (Prolific USB-serial chip)
  • Generates human-readable text at 9600 baud:
    HC=245 KW=001.347 V=121.228 CNT=033
  • Decodes packets with up to 1 watt / 1 millivolt precision!
  • Last but not least, an oh-so-cute power/status LED which glows dimly to indicate power, and flashes brightly when a packet is received.

With all of this precision, the resulting graphs look pretty decent. Here are power and voltage graphs, for the same time period. You can certainly see the voltage dips when my refrigerator or heater fan kick on, but you can also see some loads on the voltage graph that don’t show up on the power graph. These are probably the neighbor’s appliances.

The circuit design is pretty straightforward, and it should be pretty easy to build for anyone an AVR programmer and some experience with hobbyist electronics. Inside the box the mains voltage is split into two 9V AC transformers: one for power, and one for data. The power transformer’s output is rectified, and the unregulated voltage is diverted to two 7805 regulators, one for the microcontroller and one for the amplifier. The amplifier is very sensitive to noise, and it helps to have some isolation from all the high frequency harmonics that a microcontroller will toss into its power supply.

I built the prototype on a piece of stripboard in a grounded aluminum box. From left to right: MCP6292 dual op-amp and all associated passives, power supply, ATtiny85 microcontroller, and some transistors to drive the LED brightness control circuit and the RS-232 output.

The analog front-end starts with a high-pass filter which cuts out nearly all of the 60 Hz AC power, leaving a manageable signal level. This goes through some clamping diodes, to protect the op-amp input from transients, then through a first amplification stage with a gain of about 68. It’s important that this pre-amplifier is non-inverting, to avoid oscillation. This then goes through an LC bandpass filter, and another amplification stage with a gain of about 31.

With these op-amps, you could achieve higher gains (around 80), but this is all I needed. Your mileage may vary. If you want variable gain control, try replacing the 3.3k resistor at the second op-amp’s inverting input with a multi-turn 5k trimmer pot. More ambitious readers may even want to design an AGC circuit. After the second gain stage, a final low-pass filter is very important in order to remove transients that can cause false transitions on the microcontroller’s input. The microcontroller finally gets a filtered and amplified signal. It proceeds to do the demodulation and decoding in software.

I apologize for the crude hand-drawn schematic. There are more pictures on Flickr, and software/firmware in Subversion (navi-misc). Happy hacking!


/* -*- Mode: C; c-basic-offset: 4 -*-
 *
 * ted-raw.h --
 *
 *   Definitions for TED's raw packet format, as transmitted along
 *   the power lines at 2400 baud. This information is based on
 *   original black-box reverse engineering work.
 *
 * Copyright (c) 2008 Micah Dowty <micah@navi.cx>
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#ifndef _TED_RAW_H
#define _TED_RAW_H

#ifndef NULL
#define NULL ((void*)0)
#endif

/*
 * Packet format notes:
 *
 *  11 bytes
 * 
 *  0.     Header (0x55)
 *  1.     House code
 *  2.     Packet counter
 *  3-5.   Raw power reading
 *  6-8.   Raw voltage reading
 *  9.     Unknown
 *  10.    Checksum
 *
 * The raw voltage and power readings can be converted to
 * kilowatts and volts using these experimentally determined
 * formulae:
 *
 *   kW = (raw_power - 16777177) / -62048
 *   V  = (raw_voltage - 170350) / 55822
 *
 * Byte 9 seems to always hover around 250 in my
 * measurements. This could be a flag byte, some kind of
 * parity signal, or maybe even a measurement of the AC line
 * frequency.
 *
 * Byte 10 is a simple checksum. Add all the bytes in the packet
 * except byte 9, modulo 256, and this byte ensures that the result is
 * zero.  Equivalently, we can state that the sum of all bytes in the
 * packet equals byte 9.
 */

typedef struct TEDPacket {
  uint8_t header;
  uint8_t houseCode;
  uint8_t packetCount;
  uint8_t power[3];
  uint8_t voltage[3];
  uint8_t unknown;
  uint8_t checksum;
} TEDPacket;

#define TED_HEADER  0x55

static uint8_t
ted_checksum(uint8_t *buf, uint8_t len)
{
    uint8_t sum = 0;

    while (len) {
	sum += *buf;
	buf++;
	len--;
    }

    return sum;
}

static inline TEDPacket *
ted_packet_validate(uint8_t *buf, uint8_t len)
{
    TEDPacket *packet = (TEDPacket *) buf;

    if (len != sizeof *packet) {
	return NULL;
    }

    if (packet->header != TED_HEADER) {
	return NULL;
    }

    if (ted_checksum(buf, len) != packet->unknown) {
	return NULL;
    }

    return packet;
}

static inline uint32_t
ted_packet_raw_power(TEDPacket *packet)
{
    return (uint32_t)packet->power[0] |
	   (((uint32_t)packet->power[1]) << 8) |
	   (((uint32_t)packet->power[2]) << 16);
}

static inline uint32_t
ted_packet_raw_voltage(TEDPacket *packet)
{
    return (uint32_t)packet->voltage[0] |
	   (((uint32_t)packet->voltage[1]) << 8) |
	   (((uint32_t)packet->voltage[2]) << 16);
}

static inline uint32_t
ted_packet_watts(TEDPacket *packet)
{
    return -(ted_packet_raw_power(packet) - 16777177L) * 100L / 6205L;
}

static inline uint32_t
ted_packet_millivolts(TEDPacket *packet)
{
    return (ted_packet_raw_voltage(packet) - 170350L) * 100L / 5582L;
}

#endif /* _TED_RAW_H */
/* -*- Mode: C; c-basic-offset: 4 -*-
 *
 * Main program for an open source receiver unit for The Energy Detective.
 *
 * Copyright (c) 2008 Micah Dowty <micah@navi.cx>
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <stdio.h>

#include <tedrx.h>
#include <ted-raw.h>

FUSES = {
    .low = 0xE1,       /* 16 MHz PLL */
    .high = 0xDF,      /* Defaults */
    .extended = 0xFF,  /* Defaults */
};

int main(void)
{
    uint8_t buf[16];

    filter_init();
    serial_init();
    sei();

    while (1) {
        uint8_t size;
	uint32_t power, voltage;
	TEDPacket *packet;
	
        size = filter_rx(buf, sizeof buf);
	packet = ted_packet_validate(buf, size);

	if (!packet) {
	    /* Bad packet */
	    continue;
	}

	power = ted_packet_watts(packet);
	voltage = ted_packet_millivolts(packet);

	/*
	 * Print a representation of this packet which is designed
	 * to be both human- and machine-readable.
	 */

	printf("HC=%03u KW=%03u.%03u V=%03u.%03u CNT=%03u\r\n",
	       packet->houseCode,
	       (int)(power / 1000),
	       (int)(power % 1000),
	       (int)(voltage / 1000),
	       (int)(voltage % 1000),
	       packet->packetCount);
    }
}