Monitoring The Air Compressor In My TARDIS

I wanted to monitor some basic parameters of my 5HP air compressor, which is located within a sound attenuating cabinet in the creepy hot tub / storage room attached to my shop. For the past year or so, I’ve been slowly working on a system when I have time, and finally got it running this Monday.

It is based on a Keyestudio W5500 Ethernet Development Board which is basically just an Arduino Uno clone w/built in W5500 Ethernet. Extremely handy.

I used the following sensors connecting them with their supporting circuitry to a prototype shield via screw terminals:

  • DS18B20 : chiller water temperature
  • DS18B20 : chiller air outlet temp
  • MAX6675 thermocouple : pump head temperature
  • DHT22 : cabinet ambient temp / humidity
  • Analog 200 PSI pressure transducer : receiver (tank) pressure, similar to this
  • Analog 200 PSI pressure transducer : regulator pressure
  • IR reflection sensor : pump speed
  • IR reflection sensor : motor speed
  • Power status input : 5V signal indicating compressor is switched on

A 3D printed enclosure mounts to the top of an existing printed enclosure (which contains an ATTINY85 based circuit to detect when the compressor pump runs and activate cooling fans with a timed delay after the pump stops, and pulse a solenoid to purge the receiver when the pump starts and the cooling cycle stops). All inputs through the rear of the enclosure with the sensors connected via CAT6 cable. Ethernet exits the side of the enclosure as well as a a USB cable (just in case) and power, which is provided via a 12V PoE splitter module.

I made some simple aluminium brackets to mount the IR sensors which pick up the reflection from a strip of aluminium tape on the pump and motor pulleys. Each time the tape passes the emitter/detector pair, a pulse is generated which interrupts the processor and runs a small function to increment a count. Each telemetry period the count is multiplied by the appropriate multiplier to extrapolate a minute of counts, which is a reasonable RPM measurement.

The pressure sensors T into the air lines before the regulator (receiver pressure) and after the regulator (regulator pressure). They are just 0.5 - 4.5V analog sensors. Each sensor connects to an analog pin, with a 0.1uF capacitor bypassing to ground. I’ve also ran the sensor signal wire in a twisted pair with the other wire of the pair connected to ground. The code takes 10 readings from the sensors, discards obviously bad readings which may be due to noise on the line, takes an average from the 10 readings and then uses the map() function to turn that into a 0 - 200 PSI reading.

Two DS18B20 one wire sensors provide temperature readings for the air chiller. The chiller is just a 50’ 1/2" copper tubing oil in a 5 gallon bucket of water, plumbed between the pump and the receiver. It makes an amazing difference in cooling the pump output to condense any water back to liquid, so it can easily be separated out to keep the air much dryer. One DS18B20 hangs in the bucket, and another is zip tied to the chiller output tube (right side). The OneWire library provides error correcting and averaging, so all I need to do is take the measurements.

Pump head temperature is much higher, up to about 400C or so, which requires a thermocouple to measure. The thermocouple is bolted to one of the head cooling fins and fed to a MAX6675 module to provide an SPI interface. As with the DS18B20, the library takes care of most of the dirty work so it is just a matter of taking the readings. The only thing I don’t like about the Adafruit library is that it bit bangs SPI communication instead of using the built in SPI bus of the Atmega 328. To do require disabling interrupts, which means losing pulses from the RPM sensors.

And of course the ubiquitous DHT22 sensor, which I love to hate, is screwed to the side of the case to provide ambient temperature readings.

I also have a 5V input, connected to a digital pin, which takes an input from the aforementioned ATTINY85 purge/fan circuit. That circuit is powered when the compressor is switched on, so thus my monitoring circuit knows when the compressor has power. Incidentally the ATTINY85 circuit detects whether the pump is running via a simple circuit with rectifier, dropping resistor and optocoupler connected to the (240V) output of the pressure switch.

Telemetry is sent to Home Assistant via MQTT with the code below:

Air Compressor Data Logging
Aaron Cake

Air compressor sensor monitoring.

Ethernet2: Arduino
MAX6675 Thermocouple:
Dallas DS18B20 One Wire Temp Sensor:
DHTxx Temp/Humidity Sensor:


MAX6675 CS: A3, A4, A5

DS18B20: 7
DS18B20 Addresses: 28FFBD2531180249 , 28FFD7E2301802F1

DHT22: 6

IR Switch 1 (motor tach): 2 (HIGH = off, LOW = on)
IR Switch 2 (pump tach): 3 (HIGH= off, LOW = ON

200 PSI pressure sensor (receiver pressure): A0									//both sensors are 0.5 - 4.5V
200 PSI pressure sensor (regulator pressure): A1

Compressor power detect pin: 5 (HIGH = on, LOW = off)

Version 1: Intital version with basic functionality


#include <MemoryFree.h>				//library with free memory function

 //libraries needed for W5500
#include <SPI.h> 
#include <Ethernet2.h>

//PubSubClient library for MQTT
#include <PubSubClient.h>

//#include <ArduinoJson.h>

//libraries for thermocouple
//#include <MAX6675_Thermocouple.h>

#include <max6675.h>

#define SCK_PIN A5    //10
#define CS_PIN  A4    //9
#define SO_PIN  A3     //8

// libraries for DS18B20
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 7													//pin for one wire bus

//library for DHTxx
#include <DHT.h>

//DHT22 options
#define DHTPIN 6														 // Digital pin connected to the DHT sensor
			//#define DHTTYPE DHT11   // DHT 11
#define DHTTYPE DHT22													 // DHT 22  (AM2302), AM2321
			//#define DHTTYPE DHT21   // DHT 21 (AM2301)

//MAX6675_Thermocouple* Thermocouple = NULL;

MAX6675 Thermocouple(SCK_PIN, CS_PIN, SO_PIN);

OneWire oneWire(ONE_WIRE_BUS);											// OneWire reference passing pin number

DallasTemperature DS18B20Sensors(&oneWire);									//DS18B20 passing OneWire reference

//DS18B20 devices 28FFBD2531180249 , 28FFD7E2301802F1
DeviceAddress WaterTempSensor = { 0x28, 0xFF, 0xBD, 0x25, 0x31, 0x18, 0x02, 0x49 };
DeviceAddress CoolerOutletTempSensor   = { 0x28, 0xFF, 0xD7, 0xE2, 0x30, 0x18, 0x02, 0xF1 };

DHT DHT22Sensor(DHTPIN, DHTTYPE);										//create a DHT22 sensor object

byte mac[] = {0xA0, 0xAA, 0xBA, 0xAC, 0x22, 0x02};						//Ethernet MAC
EthernetClient ethClient;												//Initialize ethernet client as ethClient

//MQTT sever address
#define MQTT_SERVER ""  

void MQTTCallback(char* topic, byte* payload, unsigned int length);		//function prototype for MQTT callback

char const* MQTTClientName = "cmprssr";								//MQTT client name. Keep short, don't waste RAM
const char TelemetryTopic[] = "/compressor/tele/";					//Suffix added to board topic for telemetry heatbeat
const char CompressorTele[] = "/compressor/comptele/";				//suffix to add to board topic for compressor telemetry status

PubSubClient MQTTClient(MQTT_SERVER, 1883, MQTTCallback, ethClient);	//pass Ethernet object to PubSubClient and create MQTT client

//StaticJsonBuffer<200> jsonBuffer;							//JSON buffer
//JsonObject& CompTeleJSON = jsonBuffer.createObject();
//JsonObject& HeartbeatJSON = jsonBuffer.createObject();


char TempBuffer[32];										//temp string buffer to hold temp stuff

char TelemetryBuffer[200];									//string to hold compressor telemetry JSON

volatile int IRQMotorTachCount = 0;								//counter to be incremented by motor tach interrupt function
volatile int IRQPumpTachCount = 0;								//counter to be incremented by motor tach interrupt function

int MotorRPM = 0;										//motor RPM
int PumpRPM = 0;										//pump RPM

int ReceiverPressurePSI = 0;							//receiver pressure PSI
int RegulatorPressurePSI = 0;							//regulagtor pressure

double HeadTempC = 0;									//compressor head/outlet temp
double WaterTempC = 0;									//water temperature
double WaterOutletTempC = 0;							//water outlet temperature
float AmbiantTempC = 0;									//ambiant temp and humidity
float AmbiantHumidityPercent = 0;						

byte CompressorPowerStatus = 0;							//compressor power...0 = LOW = off, 1 = HIGH = on

														//variables and constants for task timer
unsigned long PreviousMillis = 0;						//the last time the tach calc ran
const int TachCalcInterval = 1000;						//interval to calculate the tachs
unsigned long TachPreviousMillis = 0;					//the last time the tach calculation ran
int OffTelemetryInterval = 20000;						//telemetry period while compressor power is off...20 seconds
int OnTelemetryInterval = 2000;							//telemetry period while compressor power on...2 seconds
int TelemetryInterval = 2000;							//telemetry often MQTT sensor readings are sent
unsigned long TelemetryPreviousMillis = 0;				//last time telemetry was sent

float FirmwareVersion = 1.0;								//the version of this software

														//uptime counter and heartbeat variables
int UptimeSeconds = 0;										//uptime counter seconds, 0-60
int UptimeMinutes = 0;										//uptime counter minutes, 0-60
int UptimeHours = 0;										//uptime counter hours, 0 - 24
int UptimeDays = 0;											//uptime counter days, 0 - maxint...32,000 day uptime? Unlikely
unsigned long UptimePreviousMillis = 0;						//the last time the uptime counter updated
bool SendHeartbeat = false;									//flag to send the heartbeat. Set true in uptime loop to send a heartbeat

//#define SCK_PIN A5
#define MotorTachSensorPin 2						//pin for motor tach IR sensor
#define PumpTachSensorPin 3							//pin for pump tach IR sensor

#define ReceiverPressureSensorPin A0				//pin for analog receiver pressure sensor
#define RegulatorPressureSensorPin A1				//pin for analog regulator pressure sensor

#define CompressorPowerDetectPin 5					//pin to detect whether compressor powered on

// the setup function runs once when you press reset or power the board
void setup() {
	pinMode(MotorTachSensorPin, INPUT);
	attachInterrupt(digitalPinToInterrupt(MotorTachSensorPin), MotorTachCountISR, FALLING);			//set interrupt on pin 2 to fire MotorTachCount on falling (IR ON)
	Serial.println(F("Pin 2: INPUT, FALLING"));

	pinMode(PumpTachSensorPin, INPUT);
	attachInterrupt(digitalPinToInterrupt(PumpTachSensorPin), PumpTachCountISR, FALLING);			//set interrupt on pin 3 to fire PumpTachCount on falling (IR ON)
	Serial.println(F("Pin 3: INPUT, FALLING"));

	pinMode(ReceiverPressureSensorPin, INPUT);														//analog input pins for pressure sensors 0.5 - 4.5V
	pinMode(RegulatorPressureSensorPin, INPUT);
	Serial.println(F("Pins A0, A1: INPUT"));

	pinMode(CompressorPowerDetectPin, INPUT);														//digital pin to detect power
	Serial.println(F("Pin 5: INPUT"));

	delay(500);																// one second delay because DS18B20 needs time to boot up otherwise just reads 85 degrees

	// start the Ethernet and obtain an address via DHCP
	Serial.println(F("Start: Ethernet..."));
	if (Ethernet.begin(mac) == 0) {
		Serial.println(F("FAIL: Ethernet: No DHCP."));

	Serial.print(F("Success: Ethernet. DHCP IP: "));


	Serial.println(F("Start: MAX6675 Thermocouple"));
	//Thermocouple = new MAX6675_Thermocouple(SCK_PIN, CS_PIN, SO_PIN);
	Serial.println(F("Success: MAX6675 Thermocouple"));

	Serial.println(F("Start: DS18B20 Temp"));
	DS18B20Sensors.setResolution(WaterTempSensor, TEMPERATURE_PRECISION);
	DS18B20Sensors.setResolution(CoolerOutletTempSensor, TEMPERATURE_PRECISION);
	Serial.println(F("Success: DS18B20 Temp"));

	Serial.println(F("Start: DHT22 Temp/Humidity Sensor"));
	Serial.println(F("Success: DHT22 Temp/Humidity Sensor"));

	ConnectToMQTTBroker();														//connect MQTT


void loop() {

	unsigned long CurrentMillis = millis();					 //get the current time count

	CompressorPowerStatus = digitalRead(CompressorPowerDetectPin);			//compressor poewr status

	//reconnect if connection is lost
	if (!MQTTClient.connected()) { ConnectToMQTTBroker(); }

	//maintain MQTT connection

	if (CurrentMillis - TachPreviousMillis >= TachCalcInterval) {			//if TachCalcInterval has passed since last run, calculat the tach
		TachPreviousMillis = CurrentMillis;									//update the last time this ran
																			//calculate the tach
		detachInterrupt(digitalPinToInterrupt(MotorTachSensorPin));			//turn off interrupts during calc

		MotorRPM = 60 * IRQMotorTachCount;									//calculate the RPM. It is just the count (how many pules per second) * 60 seconds
		PumpRPM = 60 * IRQPumpTachCount;

		IRQMotorTachCount = 0;												//and reset the count
		IRQPumpTachCount = 0;

		//Serial.print(F("Motor RPM: "));
		//Serial.print(F("Pump RPM: "));

		UptimeCounter();													//since calculating tach once per second, also update the uptime counter

		attachInterrupt(digitalPinToInterrupt(MotorTachSensorPin), MotorTachCountISR, FALLING);			//reattach interrupt on pin 2 to fire MotorTachCount on falling (IR ON)
		attachInterrupt(digitalPinToInterrupt(PumpTachSensorPin), PumpTachCountISR, FALLING);			

	//telemetry send timer
	if (CurrentMillis - TelemetryPreviousMillis >= TelemetryInterval) {			//if TelemetryInterval has passed since last run, read sensor and send telemetry
		TelemetryPreviousMillis = CurrentMillis;								//update the last time this ran
		Serial.println(F("Telemetry: Begin"));

		//read the sensors
		//HeadTempC = Thermocouple->readCelsius();								//read head/outlet temp thermocouple		
		HeadTempC = Thermocouple.readCelsius();

		Serial.print(F("HeadTempC: "));

		DS18B20Sensors.requestTemperatures();									//tell the sensors to update...very important

		WaterTempC = DS18B20Sensors.getTempC(WaterTempSensor);					//read the two DS18B20s for water related temps
		Serial.print(F("WaterTempC: "));

		WaterOutletTempC = DS18B20Sensors.getTempC(CoolerOutletTempSensor);
		Serial.print(F("WaterOutletTempC: "));

		AmbiantHumidityPercent = DHT22Sensor.readHumidity();					//read temp and humidity from DHT22...takes about 250mS
		Serial.print(F("AmbiantHumidityPercent: "));

		AmbiantTempC = DHT22Sensor.readTemperature();

		Serial.print(F("AmbiantTempC: "));

		ReceiverPressurePSI = map(SmoothAnalogRead(ReceiverPressureSensorPin), 103, 922, 0, 200);			//read receiver pressure sensor analog value and
																				//map the value between 0 and 200 (PSI). Sensor is 0.5V - 4.5V.
																				//ADC 103 = 0.50342131V, 922= 4.506353861V, plotted in Excel with =ADC_value*(5 / 1023)
		//Serial.print(F("ReceiverPressureADC: "));

		Serial.print(F("ReceiverPressurePSI: "));

		RegulatorPressurePSI = map(SmoothAnalogRead(RegulatorPressureSensorPin), 103, 922, 0, 200);		//do the same for the regulator analog pressure sensor

		/*Serial.print(F("RegulatorPressureADC: "));

		Serial.print(F("RegulatorPressurePSI: "));

		//generate the JSON
		/*CompTeleJSON[F("HdTmpC")] = HeadTempC;
		CompTeleJSON[F("WatTmpC")] = WaterTempC;
		CompTeleJSON[F("WatOutTmpC")] = WaterOutletTempC;
		CompTeleJSON[F("AH")] = AmbiantHumidityPercent;
		CompTeleJSON[F("AT")] = AmbiantTempC;
		CompTeleJSON[F("RecPSI")] = ReceiverPressurePSI;
		CompTeleJSON[F("RegPSI")] = RegulatorPressurePSI;
		CompTeleJSON[F("CmpPwrStat")] = CompressorPowerStatus;
		CompTeleJSON[F("MotRPM")] = MotorRPM;
		CompTeleJSON[F("PumpRPM")] = PumpRPM;*/
		//construct the JSON in one big ugly evil sprintf...less memory and less buggy than ArduinoJSON!!
			(int)HeadTempC,(int)(HeadTempC*100)%100,				//sprintf on Arduino doesn't do floats/doubles so need to math it to two ints
			(int)WaterTempC,(int)(WaterTempC*100)%100,				//first int is left of decimal, 2nd int is right of decimal
			(int)WaterOutletTempC,(int)(WaterOutletTempC*100)%100,	//

		//CompTeleJSON.printTo(Serial);											//output the JSON to serial
		//Serial.println(F(" "));
		//CompTeleJSON.printTo(TelmetryPublish);									//output the JSON to TelemetryPublish variable

		Serial.println(F("Telemetry: Finish"));

		MQTTClient.publish(CompressorTele, TelemetryBuffer);					//publish the JSON to the compressor telemetry MQTT topic

	//if the compressor power is on, speed up the telemetry
	if (CompressorPowerStatus == 1) {
		TelemetryInterval = OnTelemetryInterval;
		TelemetryInterval = OffTelemetryInterval;


void MQTTCallback(char* topic, byte* payload, unsigned int length) {
	//function called when MQTT message received


void ConnectToMQTTBroker() {

	// Loop until connected to MQTT broker
	while (!MQTTClient.connected()) {
		Serial.println(F("MQTT: Attempting connection..."));

		//if connected, subscribe to the relay topics as BoardTopic/relay number
		if (MQTTClient.connect((char*)MQTTClientName)) {
			Serial.println(F("MQTT: Connected"));


void MotorTachCountISR() {

void PumpTachCountISR() {

//uptime counter function
void UptimeCounter() {
	UptimeSeconds++;									//increase uptime seconds by 1
	//Serial.print(F("Uptime: Seconds +1:"));
	if (UptimeSeconds >= 60) {							//if seconds are greater than 60 that's a minute
		UptimeMinutes++;								//increase minutes by 1
		UptimeSeconds = 0;								//reset seconds to zero
		SendHeartbeat = true;							//set the flag to send the heartbeat publish

	if (UptimeMinutes >= 60) {							//if minutes > 60 that's an hour
		UptimeHours++;									//increase hour count
		UptimeMinutes = 0;								//reset minutes to zero


	if (UptimeHours >= 24) {
		UptimeHours = 0;

	if (SendHeartbeat == true) {						//if the heartbeat flag is set (on minute changeover), send heartbeat
		SendHeartbeat = false;							//set the flag false to it doesn't run until next minute


//function to generate heartbeat ping on topic /sign/tele/

void Heartbeat() {

	char IPAddress[16];											//string to hold IP address

	//															//create an uptime in ISO 8601 format:
	//															/*P is the duration designator(for period) placed at the start of the duration representation.
	//															Y is the year designator that follows the value for the number of years.
	//															M is the month designator that follows the value for the number of months.
	//															W is the week designator that follows the value for the number of weeks.
	//															D is the day designator that follows the value for the number of days.
	//															T is the time designator that precedes the time components of the representation.
	//															H is the hour designator that follows the value for the number of hours.
	//															M is the minute designator that follows the value for the number of minutes.
	//															S is the second designator that follows the value for the number of seconds.

	//															For example, "P3Y6M4DT12H30M5S" represents a duration
	//															of "three years, six months, four days, twelve hours, thirty minutes, and five seconds".*/

	sprintf_P(TempBuffer, PSTR("P%dDT%dH%dM%dS"), UptimeDays, UptimeHours, UptimeMinutes, UptimeSeconds);;
	//HeartbeatJSON[F("uptime")] = TempBuffer;


	sprintf_P(IPAddress, PSTR("%d.%d.%d.%d"), Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]);
	//HeartbeatJSON[F("ip")] = TempBuffer;

	//firmware version
	//HeartbeatJSON[F("ver")] = FirmwareVersion;

	//HeartbeatJSON["freemem"] = freeMemory();

	sprintf_P(TelemetryBuffer, PSTR("{\"ip\":\"%s\",\"uptime\":\"%s\",\"ver\":\"%d.%02d\",\"freemem\":\"%d\"}"),
		IPAddress, TempBuffer, (int)FirmwareVersion, (int)(FirmwareVersion * 100) % 100, (int)freeMemory());

	Serial.print(F("Heartbeat: "));										//print it out to the serial for debugging

																			//build the MQTT topic to publish
	Serial.print(F("Heartbeat publish: "));								//write the heartbeat JSON to the serial port
	MQTTClient.publish(TelemetryTopic, TelemetryBuffer);


int SmoothAnalogRead(int AnalogReadPin) {
	//returns takes average of 20 analog reads on AnalogReadPin
	//de-noising the analogRead

	int ReadTotals;														//total of 20 analog reads
	int ReadAverage;													//calculated average

	ReadTotals = 0;

	for (int i = 0; i < 20; i++) {										//read 20 analogreads
		ReadTotals = ReadTotals + analogRead(AnalogReadPin);			//add up all the reads

	//Serial.print(F("Smooth Analog Read Total: "));

	ReadAverage = (ReadTotals / 20);									//calculate the average

	if (ReadAverage < 103) {											//noise on the line has caused a value less than minimum voltage output of sensor
		ReadAverage = 103;												//if invalid reading, just assign to minimum possible sensor reading
	}																	//stops reading from entering negative numbers after map() 

	//Serial.print(F("Smooth Analog Read Avg: "));

	return ReadAverage;


It takes all the sensor readings, then constructions a JSON string and sends via MQTT. When the compressor is detected as being powered on, telemetry is sent every 2 seconds. Otherwise, every 20 seconds.

And the data in Home Assistant looks something like this:

In general, for a first version everything seems to be fundamentally working.

Definitely have some noise on my pressure readings. I’m going to have to scope the lines to determine whether or not that is electrical noise or the sensor physically bouncing due to vibration. I’m thinking electrical noise based on the readings above the sensor maximum value. So I will likely have to modify the circuit with more aggressive filtering in the form of an R/C network and some series inductance.

I may have a similar issue with the IR sensor as there are spikes to 6000 RPM. Max speed of the pump is about 1600 RPM, and about 3400 RPM for the motor (from memory). The sensors have some sensitivity adjustments for the trigger threshold so I’ll set those at the minimum. And probably change the code to generate a running RPM average instead of a simple multiplication of the counts.

Oh, and why is it referred to as the Compressor TARDIS? Well, I admit to having a bit of fun when I built the cabinet.


Definitely have a ground loop issue between the PoE powered Arduino and the purge/fan control circuit. Anytime the purge/fan circuit is powered on, there is lots of noise on the analog pressure sensors even if the compressor isn’t running. Going to have to switch the Power Status input from a direct 5V signal from fan/purge circuit to an opto-isolated input, keeping the grounds of both circuits separate.