Newer
Older
WH1080 / wh1080_rf.c
/*
* Maplin N96GY (Fine Offset WH1080/WH1081) RF receiver using a
* Raspberry Pi and an RFM01 or RFM12b transceiver module. I switched
* to an RFM01 module after frying the RFM12b; turns out it works *far*
* better anyway, so it was something of a blessing in disguise.
* 
* The code here is really just experimental, and is not intended to be used
* beyond a learning excercise. It conveys the basics of what's required
* to get the Raspberry Pi receiving sensor data, but that's about it!
*
* I can't be sure it still works with an RFM12b, but it shouldn't be far off
* the mark if not - a bit of debugging may be required, but I no longer
* have a working module to test.
*
* This program configures an RFM01 to receive RF transmissions from the
* weather station's sensors, and reads them directly from the receiver's
* demodulator via the DATA pin, in to a GPIO pin on the Raspberry Pi. The
* pulse widths are used to derive the data-packet that was transmitted.
*
* The process switches to SCHED_RR for realtime latency while it waits
* for a packet. It returns to SCHED_OTHER when a packet is received.
* This ensures that bit transitions aren't missed, and also allows very
* heavy loads to run on the Pi while maintaining reliable reads. Optionally,
* the command 'sysctl kernel.sched_wakeup_granularity_ns=100000' may
* further improve latency, though it seems to work with Raspbian defaults
* regardless.
*
* Includes Luc Small's version of CRC8 from the OneWire Arduino library
* adapted for Fine Offset's calculations that also happen to work for this
* weather station. The SPI code was derived from the driver example at 
* kernel.org.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation as version 2 of the License.
*
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <time.h>
#include <sched.h>
#include <libconfig.h>

#include "wh1080_rf.h"
#include "bcm2835.h"
#include "rfm01.h"
#include "wunderground.h"
#include "mqtt.h"

uint16_t bw_scale[6] = {BW_67, BW_134, BW_200, BW_270, BW_340, BW_400};

struct RSSI rssi_scale[24] = {
	L0R73,L0R79,L0R85,L0R91,L0R97,L0R103,
	L6R73,L6R79,L6R85,L6R91,L6R97,L6R103,
	L14R73,L14R79,L14R85,L14R91,L14R97,L14R103,
	L20R73,L20R79,L20R85,L20R91,L20R97,L20R103,
};

uint8_t _crc8( uint8_t *addr, uint8_t len);

static void pabort(const char *s)
{
	perror(s);
	abort();
}

static const char *device = "/dev/spidev0.0";
static uint8_t mode=0;
static uint8_t bits = 8;
static uint32_t speed = 1000000;
static uint16_t delay=0;


static uint16_t send_command16(int fd, uint16_t cmd)
{
	uint8_t tx[2];
	uint8_t *buf = (uint8_t *)&cmd;
	tx[0] = buf[1];
	tx[1] = buf[0];

	//printf("SPI %02x%02x\n", buf[1], buf[0]);

	uint8_t rx[2] = {0, 0};
	struct spi_ioc_transfer tr = {
		.tx_buf = (unsigned long)tx,
		.rx_buf = (unsigned long)rx,
		.len = 2,
		.delay_usecs = delay,
		.speed_hz = speed,
		.bits_per_word = bits,
	};

	if(ioctl(fd, SPI_IOC_MESSAGE(1), &tr) < 1)
		pabort("can't send spi message");
	
	return (((uint16_t)rx[0]) << 8) + rx[1];
}

int g_low_threshold = 1000;

uint16_t cmd_reset	= CMD_RESET;
uint16_t cmd_status = CMD_STATUS;

// Expected bit rate: 95 = 1959, 99 = 1700, 9c = 1500, a1 = 1268, aa = 1000, b8 - 756, d5 = 500
uint16_t cmd_drate = CMD_DRATE|0xaa;	// drate is c8xx rather than c6xx
//uint16_t cmd_freq	= CMD_FREQ|0x620; // 433.92 MHz
//uint16_t cmd_freq	= CMD_FREQ|0x7D0; // 435.00 MHz
uint16_t cmd_freq	= CMD_FREQ|0x640; // 434.00 MHz

#ifdef RFM01
	uint16_t cmd_afc	= CMD_AFC|AFC_ON|AFC_OUT_ON|AFC_MANUAL|AFC_FINE|AFC_RL_7;
	uint16_t cmd_dcycle = CMD_LOWDUTY|0x00;
	uint16_t cmd_fifo	= CMD_FIFO|0x00;

	uint16_t cmd_config	= CMD_CONFIG|BAND_433|LOAD_CAP_12C5|BW_67;
	uint16_t cmd_rcon = (CMD_RCON|RX_EN|VDI_DRSSI|LNA_0|RSSI_91);
	uint16_t cmd_dfilter = (CMD_DFILTER|CR_LOCK_FAST|FILTER_OOK);
#endif

#ifdef RFM12B
	uint16_t cmd_config	= 0x8017;
	uint16_t cmd_power	= 0x8281; // RFM01 doesn't support this
	uint16_t cmd_sync	= 0xce55;
	uint16_t cmd_afc	= 0xc407; // or C400 for no AFC. C6xx on RFM01
	uint16_t cmd_dcycle = 0xc800;
	uint16_t cmd_pll	= 0xcc1f;
	uint16_t cmd_fifo	= 0xca8a; // CExx rather than CAxx on RFM01

	uint16_t cmd_dfilter = 0xc260;
	uint16_t cmd_rcon = (CMD_RCON|P16|VDI_MEDIUM|LNA_MEDIUM|RSSI_97|BW_340);
#endif

void strobe_afc(int fd) {

	send_command16(fd, cmd_afc|AFC_STROBE); // Strobe high
	send_command16(fd, cmd_afc & (~AFC_ON)); // Strobe low, disable AFC processing
	uint16_t status = send_command16(fd, cmd_status);
	// get offs bits and extend two's complement to a byte
	int8_t offset = (status & STATUS_OFFS) | (status & STATUS_OFFSIGN ? 0xe0 : 0);
	
	float freq_offs = (float)offset;
	#ifdef RFM12B
		freq_offs *= 2.5;
	#endif
	send_command16(fd, cmd_afc); // Strobe low, re-enable AFC

	printf("Frequency deviation %0.1fKHz (%d)\n", freq_offs, (int)offset);

	send_command16(fd, cmd_rcon);
}

/*
 * Sample the DRSSI flag at 'interval' microsecond intervals over a period of 'duration' ms,
  *and return the average.
*/

float sample_rssi(int fd, int duration, int interval) {

	unsigned int start_time, now;
	unsigned int loop_count = 0, rssi_total = 0;

	start_time = TIMER_ARM_COUNT;

	do {
		uint16_t status = send_command16(fd, cmd_status);
		int rssi = (status & STATUS_RSSI) ? 1 : 0;
		loop_count++;
		rssi_total+=rssi;
		now = TIMER_ARM_COUNT;
		usleep(interval);	// microseconds
	} while(now - start_time < (duration * 1000));	// duration as microseconds

	float duty = ((float)rssi_total/loop_count) * 100;

	return duty;
}

// pressure function and value
extern int read_bmp280(float altitude);
extern float pressure_hpa;

#define CONFIG_FILE "/etc/wh1080.conf"

int main(int argc, char *argv[])
{
	//unsigned char bytes2[] = {0xa1,0x82,0x0a,0x59,0x03,0x06,0x00,0x4e,0x06,0xc8};
	//calculate_values(bytes2);
	//return -1;

	// Read configuration file
	config_t cfg;
	config_setting_t *mqtt_settings, *bmp280_settings, *wu_settings;
	const char *mqtt_host;
	int mqtt_port;
	const char *mqtt_topic;
	const char *mqtt_user;
	const char *mqtt_pass;
	double altitude;
	const char *wu_stationId, *wu_password;

	config_init(&cfg);
	if (!config_read_file(&cfg, CONFIG_FILE)) {
		fprintf(stderr, "%s:%d - %s\n", config_error_file(&cfg),
						config_error_line(&cfg), config_error_text(&cfg));
		config_destroy(&cfg);
		exit(-1);
	}

	// BMP280 options
	bmp280_settings = config_lookup(&cfg, "bmp280");
	if (!bmp280_settings) {
		printf("No settings for BMP280 pressure sensor, disabling.\n");
	} else {
		if (!config_setting_lookup_float(bmp280_settings, "altitude", &altitude)) {
			printf("No alititude provided, setting to 0 metres above sea level.\n");
			altitude = 0.0f;
		}
	}
	
	// MQTT options
	mqtt_settings = config_lookup(&cfg, "mqtt");
	if (!mqtt_settings) {
		printf("No settings for MQTT found, disabling.\n");
	} else {
		if (!(config_setting_lookup_string(mqtt_settings, "host", &mqtt_host) &&
					config_setting_lookup_int(mqtt_settings, "port", &mqtt_port) &&
					config_setting_lookup_string(mqtt_settings, "topic", &mqtt_topic))) {
			printf("MQTT requires host, port and topic.\n");
			config_destroy(&cfg);
			exit(-1);
		}
		if (!config_setting_lookup_string(mqtt_settings, "user", &mqtt_user)) {
			mqtt_user = NULL;
		}
		if (!config_setting_lookup_string(mqtt_settings, "pass", &mqtt_pass)) {
			mqtt_pass = NULL;
		}
		// Attempt to initialize MQTT as a client
		if (!MQTT_init(mqtt_host, mqtt_port, mqtt_user, mqtt_pass, mqtt_topic)) {
			printf("Unable to initialize MQTT.\n");
			config_destroy(&cfg);
			exit(-1);
		}
	}

	// WUnderground options
	wu_settings = config_lookup(&cfg, "wunderground");;
	if (!wu_settings) {
		printf("No settings for WUnderground found, disabling.\n");
	} else {
		if (!(config_setting_lookup_string(wu_settings, "station", &wu_stationId) &&
					config_setting_lookup_string(wu_settings, "password", &wu_password))) {
			printf("WUnderground requires station ID and password.\n");
			config_destroy(&cfg);
			exit(-1);
		}
		// initialize weather underground module
		WUnderground_Init(wu_stationId, wu_password);
	}
	
	uint8_t packet_sig = 0xfa;

	if(map_peripheral(&gpio) == -1 || map_peripheral(&timer_arm) == -1) {
		printf("Failed to map the GPIO or TIMER registers into the virtual memory space.\n");
		return -1;
	}

	// 0xF90200; // run at 1MHz
	TIMER_ARM_CONTROL = TIMER_ARM_C_DISABLE|TIMER_ARM_C_FREE_EN
							|TIMER_ARM_C_16BIT|TIMER_ARM_C_PS1
							|TIMER_ARM_C_FPS(0xf9);
	
	// Init GPIO21 (on pin 13) as input (DATA), GPIO22 (pin 15) as output (nRES)
	*(gpio.addr + 2) = (*(gpio.addr + 2) & 0xfffffe07)|(0x001 << 6);

	#ifdef RFM01
		printf("Initialising RFM01\n");
	#endif
	#ifdef RFM12B
		printf("Initialising RFM12b\n");
	#endif
	
	int fd;

	fd = open(device, O_RDWR);
	if (fd < 0)
		pabort("can't open device");

	// SPI mode
	if(ioctl(fd, SPI_IOC_WR_MODE, &mode) == -1)
		pabort("Can't set SPI mode");

	// Bits per word (driver only supports 8 -bits I think, but RFM12B handles this ok)
	if(ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) == -1)
		pabort("Can't set bits per word");

	// SPI clock speed (Hz)
	if(ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) == -1)
		pabort("Can't set SPI clock speed");

	printf("SPI: mode %d, %d-bit, %d KHz\n", mode, bits, speed/1000);

	// LED on
	*(gpio.addr + (0x1c >> 2)) = 1 << 22;

	// Reset the module? Maybe use software reset if needed.
	//send_command(fd, cmd_fifo); // in case reset sensitivity is low
	//send_command(fd, cmd_reset);
	usleep(100000);

	// LED off
	*(gpio.addr + (0x28 >> 2)) = 1 << 22;

	send_command16(fd, cmd_status);
	send_command16(fd, cmd_config);
	send_command16(fd, cmd_freq);
	send_command16(fd, cmd_drate);
	send_command16(fd, cmd_rcon);
	send_command16(fd, cmd_dfilter);
	send_command16(fd, cmd_fifo);
	send_command16(fd, cmd_afc);
	send_command16(fd, cmd_dcycle);
	#ifdef RFM12B
		send_command16(fd, cmd_power);
		send_command16(fd, cmd_sync);
		send_command16(fd, cmd_pll);
	#endif

	printf("Ctrl+C to exit\n");
	usleep(5000);	// Allow crystal oscillator to start

	
	int idx1, idx2;
	for(idx1=0; idx1 < 24; idx1++) {

		uint16_t cmd_rcon_mod = (cmd_rcon & ~(RSSI_X2|LNA_XX)) | (rssi_scale[idx1].rssi_setth |rssi_scale[idx1].g_lna);

		printf("%15s idx %-2d  ", rssi_scale[idx1].name, idx1);
		
		for(idx2=0; idx2 < 6; idx2++) {
		
			uint16_t cmd_config_mod = (cmd_config & ~BW_X2) | bw_scale[idx2];

			send_command16(fd, cmd_config_mod);
			send_command16(fd, cmd_rcon_mod);
			usleep(1000);
		
			rssi_scale[idx1].duty[idx2] = sample_rssi(fd, 25, 100);
			
			if(cmd_rcon_mod == cmd_rcon && cmd_config_mod == cmd_config)
				printf("%6.2f< ", rssi_scale[idx1].duty[idx2]);
			else
				printf("%6.2f  ", rssi_scale[idx1].duty[idx2]);
			fflush(stdout);
		}
		printf("\n");
	}
	send_command16(fd, cmd_config);
	send_command16(fd, cmd_rcon);
	usleep(1000);


	// Show the average RSSI to indicate noise at startup. If args dictate
	// then repeat forever. Note that an unshielded Ethernet cable will
	// radiate noise, so include a delay to allow console output to be flushed.
	do {
		float duty = sample_rssi(fd, 250, 100);
		printf("RSSI Duty %0.2f\r", duty);
		fflush(stdout);
		usleep(250000);
	} while(argc > 1);

	printf("\n");

	uint16_t status;

	uint8_t rssi = 0, oldrssi = 0;
	unsigned int shorts = 0;
	unsigned int rssitime, oldrssitime, now;
	int count = 0, timeout = 1;
	time_t last_valid = time(0);
	int crc_passed;

	unsigned int rssitime_buf[500];
	unsigned char bytes[10];

	oldrssitime = TIMER_ARM_COUNT;

	// Switch to realtime scheduler
	// scheduler_realtime();
	do {

		// Read the GPIO pin for clocked DATA value
		status = ((*(gpio.addr + 13)) >> 21) & 1;
		rssi = status;
		rssitime = TIMER_ARM_COUNT;
		// Check if the pin transitioned
		if(rssi != oldrssi) {
			// If falling edge (1 -> 0), then store bit pulse duration
			if(rssi == 0) {
				rssitime_buf[count] = rssitime - oldrssitime;
				if(++count == 500)
				count = 499;
			}
			oldrssi = rssi;
			oldrssitime = rssitime;
			timeout = 0;
		}
		// Check time since last transition. If timeout, then dump packet.
		int packet_offset = 0;
		now = TIMER_ARM_COUNT;
		if(!timeout && (now - oldrssitime) > 5000) { // && count > 0

			uint8_t sig_matched = 0;
		
			if(count > 60) { // then maybe something at least interesting
				// Look for device_id
				int idx;
				uint8_t bit, sig_in = 0;
				for(idx=0; idx < count; idx++) {
					bit = rssitime_buf[idx] < g_low_threshold ? 1 : 0;
					sig_in = (sig_in << 1) | bit;

					if((sig_matched = (sig_in == packet_sig))) {
						packet_offset = idx - 3;
						break;
					}
				}
				printf("\rData bits = %d   (offset %d) (%d short) %s\n",
					count, packet_offset, shorts, sig_matched ? "Packet signature found" : "No packet signature found");
				if(count == 88 && sig_matched) { // then probably a data packet

					// LED on
					*(gpio.addr + (0x1c >> 2)) = 1 << 22;

					strobe_afc(fd); // lock frequency to good signal

					int b;
					uint8_t byte;
					for(idx=0; idx < 10; idx++) {
						byte = 0;
						for(b=0; b < 8; b++) {
							// Short pulses 1, long pulses 0
							uint8_t bit = rssitime_buf[packet_offset + (idx * 8 + b)] < g_low_threshold ? 1 : 0;
							byte = (byte << 1) + bit;
						}
						bytes[idx] = byte;
						printf("%02x ", byte);
					}
					crc_passed = bytes[9] == _crc8(bytes, 9);
					printf("crc %s (gap %ds)\n", crc_passed ? "ok" : "fail", (int)(time(0) - last_valid));
					last_valid = time(0);
					fflush(stdout);
				}
			} else {
				if(shorts++ % 10 == 0) {
					printf(".");
					fflush(stdout);
				}
			}
			timeout = 1;

			// If we get enough bits, then dump stats to indicate pulse lengths coming from the device.
			if(count > 40) {
				// These are slightly confusing - lo used to mean low side of threshold, but printf below reports them as binary
				// 0 and 1. So the meanings are opposite - to be fixed.
				unsigned int idx, min_lo=999999, min_hi=999999, max_lo = 0, max_hi = 0;
				unsigned int val;
				for(idx = 0; idx < count; idx++) {
					// printf("RSSI 1 -> 0  %3d: %4dus ( %s )\n", idx, rssitime_buf[idx],
					//	rssitime_buf[idx] >= LOW_THRESHOLD ? "Hi" : "Lo");
					val = rssitime_buf[idx];
					
					// Short pulses are binary '1', long pulses are binary '0'
					if(val < g_low_threshold) {
						if(val < min_lo)
						min_lo = val;
						if(val > max_lo)
						max_lo = val;
					} else {
						if(val < min_hi)
						min_hi = val;
						if(val > max_hi)
						max_hi = val;
					}
				}

				printf("Pulse stats: Hi: %u - %u   Lo: %u - %u  (%d point)\n", min_lo, max_lo, min_hi, max_hi, count);

				// Recalculate the pulse threshold if we got a perfect read.
				if(count == 88 && crc_passed) {
					//g_low_threshold = ( ((max_lo + min_lo) / 2) + ((max_hi + min_hi) / 2)) / 2;
					g_low_threshold =(max_lo + min_hi) / 2;
					printf("Threshold now %d\n", g_low_threshold);
					
					// Note the time of the last reading...
					unsigned int wait_start = TIMER_ARM_COUNT, elapsed;

					// at this point, we can do other stuff that requires the RT scheduler
					
					if (bmp280_settings) {
						read_bmp280(altitude);	// read pressure, calculate for the given altitude
					}
					
					calculate_values(bytes, pressure_hpa, (wu_settings != NULL), (mqtt_settings != NULL));
					
					// Wait for remainder of 47 seconds in standard scheduler until we can expect the next read
					// scheduler_standard();

					do {
						elapsed = (TIMER_ARM_COUNT - wait_start) / 1000000;
						printf("Wait %us \r", 47 - elapsed);
						fflush(stdout);
						usleep(250000);
					} while(elapsed < 47);
					printf("Listening for transmission\n");
					// scheduler_realtime();
				}
			}
			count = 0;

			// LED off
			*(gpio.addr + (0x28 >> 2)) = 1 << 22;
		}
		usleep(5); // No point running with nanosecond loops when pulses are in the hundreds of microseconds...
		
	} while(1); // Ctrl+C to exit for now...


	// Currenty unreachable
	close(fd);
	config_destroy(&cfg);
	// Todo: clean up MQTT, WUnderground
	
	unmap_peripheral(&gpio);
	unmap_peripheral(&timer_arm);
	
	return 0;
}

char *direction_name[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"};

void calculate_values(unsigned char *buf, float pressure_hpa, bool send_wu, bool send_mqtt) {
	
	unsigned short device_id = ((unsigned short)buf[0] << 4) | (buf[1] >> 4);
	unsigned short temperature_raw = (((unsigned short)buf[1] & 0x0f) << 8) | buf[2];
	float temperature = ((float)temperature_raw - 400) / 10;
	int humidity = buf[3];
	
	unsigned short wind_avg_raw = (unsigned short)buf[4];
	float wind_avg_ms = roundf((float)wind_avg_raw * 34.0f) / 100;
	float wind_avg_mph = wind_avg_ms * 2.23693629f;

	unsigned short wind_gust_raw = (unsigned short)buf[5];
	float wind_gust_ms = roundf((float)wind_gust_raw * 34.0f) / 100;
	float wind_gust_mph = wind_gust_ms * 2.23693629f;
	
	unsigned short rain_raw = (((unsigned short)buf[6] & 0x0f) << 8) | buf[7];
	
	float rain = (float)rain_raw * 0.3f;
	
	int direction = buf[8] & 0x0f;
	
	char *direction_str = direction_name[direction];

	if (device_id == 0) {
	  printf("Invalid station Id, zero, ignoring.\n");
	  return;
	}
	printf("Station Id: %04X\n", device_id);
	printf("Temperature: %0.1fC, Humidity: %d%%\n", temperature, humidity);
	printf("Wind speed: %0.2f m/s, Gust Speed %0.2f m/s, %s\n", wind_avg_ms, wind_gust_ms, direction_str);
	printf("Wind speed: %0.1f mph, Gust Speed %0.1f mph, %s\n", wind_avg_mph, wind_gust_mph, direction_str);
	printf("Total rain: %0.1f mm\n", rain);
	printf("Pressure (sea level): %0.1f hpa\n", pressure_hpa);

	if (send_wu) {
		// submit observation to weather underground
	  WUnderground_Observation(temperature, humidity, wind_avg_mph,
						wind_gust_mph, direction_str, rain, pressure_hpa);
	}

	if (send_mqtt) {
		// publish to MQTT
		MQTT_observation(temperature, humidity, wind_avg_ms, wind_gust_ms, direction_str, rain, pressure_hpa);
	}
}

/*
* Function taken from Luc Small (http://lucsmall.com), itself
* derived from the OneWire Arduino library. Modifications to
* the polynomial according to Fine Offset's CRC8 calulations.
*/
uint8_t _crc8( uint8_t *addr, uint8_t len)
{
	uint8_t crc = 0;

	// Indicated changes are from reference CRC-8 function in OneWire library
	while (len--) {
		uint8_t inbyte = *addr++;
		uint8_t i;
		for (i = 8; i; i--) {
			uint8_t mix = (crc ^ inbyte) & 0x80; // changed from & 0x01
			crc <<= 1; // changed from right shift
			if (mix) crc ^= 0x31;// changed from 0x8C;
			inbyte <<= 1; // changed from right shift
		}
	}
	return crc;
}

void scheduler_realtime() {

	struct sched_param p;
	
	p.__sched_priority = sched_get_priority_max(SCHED_RR);
	
	if( sched_setscheduler( 0, SCHED_RR, &p ) == -1 ) {
		perror("Failed to switch to realtime scheduler.");
	}
}

void scheduler_standard() {

	struct sched_param p;
	
	p.__sched_priority = 0;
	
	if( sched_setscheduler( 0, SCHED_OTHER, &p ) == -1 ) {
		perror("Failed to switch to normal scheduler.");
	}
}