|
/* |
|
* Automatic, audio-triggered power switch |
|
* |
|
* This program controls the mains power to a set of speakers based on the |
|
* presence of an audio signal. To switch the mains power, it sends a 433.92MHz |
|
* radio signal to a MS-6148 ("Mains Outlet with Remote" from JayCar), emulating |
|
* the MS-6147-RC remote control. |
|
* |
|
* Created: 15/05/2022 |
|
* Author : Stephen Holdaway |
|
*/ |
|
|
|
#include <avr/interrupt.h> |
|
#include <avr/io.h> |
|
#include <avr/sleep.h> |
|
#include <avr/wdt.h> |
|
#include <stdbool.h> |
|
#include <stdlib.h> |
|
#include <util/delay.h> |
|
|
|
#define ADC_IN_PIN PB4 |
|
#define RADIO_OUT_PIN PB3 |
|
|
|
#define WDT_PERIOD_SECS 0.032 |
|
|
|
// Delays for RF modulation |
|
#define SHORTPULSE_US 316 |
|
#define LONGPULSE_US 818 |
|
|
|
enum RemoteCommand { |
|
kRF_One_On = 0xF, |
|
kRF_One_Off = 0xE, |
|
kRF_TWo_On = 0xD, |
|
kRF_Two_Off = 0xC, |
|
kRF_Three_On = 0xB, |
|
kRF_Three_Off = 0xA, |
|
kRF_Four_On = 0x7, |
|
kRF_Four_Off = 0x6, |
|
kRF_All_On = 0x4, |
|
kRF_All_Off = 0x8, |
|
}; |
|
|
|
// 20-bit address of the remote |
|
// |
|
// Your remote's address can be found by capturing a signal from any button |
|
// press with a either a software-defined radio or wiring directly to a test |
|
// point on the remote's PCB: |
|
// |
|
// > ...solder jumper wires to...the large solder pad next to C11 (if you follow |
|
// > this trace, it leads back to pin 2 on the IC). This is the raw signal out of |
|
// > the IC before it gets to the 433MHz transmitter. |
|
// |
|
// The remote's address is the first 20 bits (short on, long off = 0; long on, short off = 1). |
|
static const uint32_t kAddress = 0xFFFFF; // Replace with your remote's address |
|
|
|
/** |
|
* Reverse order of bits in byte |
|
*/ |
|
uint8_t reverse_bits(uint8_t val) |
|
{ |
|
return ((val & 0x80) >> 7) | |
|
((val & 0x40) >> 5) | |
|
((val & 0x20) >> 3) | |
|
((val & 0x10) >> 1) | |
|
((val & 0x08) << 1) | |
|
((val & 0x04) << 3) | |
|
((val & 0x02) << 5) | |
|
((val & 0x01) << 7); |
|
} |
|
|
|
/** |
|
* Calculate CRC on lower 24 bits of input for MS-6148 |
|
*/ |
|
uint8_t rf_crc(uint32_t data) |
|
{ |
|
uint8_t a = reverse_bits(data >> 16); |
|
uint8_t b = reverse_bits(data >> 8); |
|
uint8_t c = reverse_bits(data); |
|
|
|
return reverse_bits(a + b + c); |
|
} |
|
|
|
/** |
|
* Create a packet with the passed 20-bit address, command byte and a checksum |
|
*/ |
|
uint32_t packet(uint32_t addr, uint8_t cmd) |
|
{ |
|
uint32_t data = ((addr & 0xFFFFF) << 4) | (cmd & 0xF); |
|
return (data << 8) | rf_crc(data); |
|
} |
|
|
|
/** |
|
* Transmit sequence representing a 1 bit |
|
*/ |
|
void rf_send_high() |
|
{ |
|
PORTB |= _BV(RADIO_OUT_PIN); |
|
_delay_us(LONGPULSE_US); |
|
PORTB &= ~_BV(RADIO_OUT_PIN); |
|
_delay_us(SHORTPULSE_US); |
|
} |
|
|
|
/** |
|
* Transmit sequence representing a 0 bit |
|
*/ |
|
void rf_send_low() |
|
{ |
|
PORTB |= _BV(RADIO_OUT_PIN); |
|
_delay_us(SHORTPULSE_US); |
|
PORTB &= ~_BV(RADIO_OUT_PIN); |
|
_delay_us(LONGPULSE_US); |
|
} |
|
|
|
void sendrf(uint32_t packet) |
|
{ |
|
// Repeat packet multiple times to ensure delivery |
|
for(int i = 0; i < 8; i++) { |
|
|
|
// Send all bits (MSB-first) |
|
for(uint32_t bit = 0x80000000UL; bit > 0; bit >>= 1) { |
|
if(bit & packet){ |
|
rf_send_high(); |
|
} else { |
|
rf_send_low(); |
|
} |
|
} |
|
|
|
// 2 more low bits |
|
rf_send_low(); |
|
rf_send_low(); |
|
|
|
// Brief delay between repeats |
|
_delay_ms(10 * 2.06); |
|
} |
|
} |
|
|
|
inline void setup() |
|
{ |
|
uint8_t ddr = 0; |
|
uint8_t port = 0; |
|
|
|
// Radio modulation output, initially low |
|
ddr |= _BV(RADIO_OUT_PIN); |
|
port &= ~_BV(RADIO_OUT_PIN); |
|
|
|
// Audio signal input, no pull-up |
|
ddr &= ~_BV(ADC_IN_PIN); |
|
port &= ~_BV(ADC_IN_PIN); |
|
|
|
DDRB = ddr; |
|
PORTB = port; |
|
} |
|
|
|
inline void adc_init() |
|
{ |
|
ADMUX = 2; // Select ADC2 (PB4) |
|
DIDR0 = _BV(ADC2D); // Disable digital input on ADC2 |
|
|
|
// Enable ADC |
|
ADCSRA = _BV(ADEN); |
|
} |
|
|
|
inline void watchdog_init() |
|
{ |
|
// Set up watchdog timer for waking from sleep |
|
WDTCR |= _BV(WDCE); // Allow watchdog changes in the next 3 cycles |
|
WDTCR = _BV(WDP0) | _BV(WDTIE); // Set watchdog timeout to 32 ms, interrupt-only |
|
} |
|
|
|
int main(void) |
|
{ |
|
setup(); |
|
adc_init(); |
|
watchdog_init(); |
|
|
|
sei(); |
|
|
|
// State for managing the wireless power switch |
|
// Active ticks initially acts as a wait for readings to stablise (while higher than maxTickAccumulation) |
|
bool is_powered = false; |
|
uint16_t activeTicks = 0xFFFF - ((1 /*xecconds*/ / WDT_PERIOD_SECS) * 5); |
|
|
|
// Fixed-point accumulation filter for measuring audio energy over time |
|
int32_t sum_filter = 0; |
|
|
|
// Settings for power on and off |
|
// Each tick is a watchdog timer overflow period |
|
const uint16_t powerOnInertia = (2 /*xecconds*/ / WDT_PERIOD_SECS); |
|
const uint16_t maxTickAccumulation = ((20 /*minutes */ * 60) / WDT_PERIOD_SECS); |
|
|
|
uint16_t delta_filter = 0; |
|
uint16_t previous_sum = 0; |
|
|
|
while (1) { |
|
uint16_t sum = 0; |
|
|
|
// Take multiple samples over a short period of time |
|
const uint8_t samples = 4; |
|
for (uint8_t i = samples; i != 0; --i) { |
|
// Start an ADC conversion and wait for it to finish |
|
ADCSRA |= _BV(ADSC); |
|
while (ADCSRA & _BV(ADSC)); |
|
|
|
// Grab reading, centred to half VCC |
|
uint16_t reading = ADCL; |
|
reading |= (ADCH<<8); |
|
reading -= 512; |
|
|
|
sum += abs(reading); |
|
|
|
// Sample over a period of time |
|
_delay_us(250); |
|
} |
|
|
|
// Pass summed energy through a first-order infinite impulse response (IIR) filter |
|
// This accumulates energy over time, so we get an idea of how much audio signal is present |
|
{ |
|
int32_t local = sum; |
|
local <<= 16; |
|
|
|
sum_filter += (local - sum_filter) >> 5; |
|
sum = (sum_filter + (1<<15)) >> 16; |
|
} |
|
|
|
// Monitor changes over time in the measured energy to detect audio signals |
|
{ |
|
uint16_t delta = abs(sum - previous_sum); |
|
|
|
delta_filter += (delta << 5); |
|
|
|
if (delta_filter > 0) { |
|
delta_filter -= delta_filter >> 4; |
|
} |
|
|
|
previous_sum = sum; |
|
} |
|
|
|
const uint16_t activeness = delta_filter >> 4; |
|
|
|
if (activeTicks > maxTickAccumulation) { |
|
// Still in the stablising period after reset: don't use the signal yet |
|
activeTicks++; |
|
|
|
} else { |
|
// Handle turning on and off |
|
if (activeness >= 10) { |
|
// Turn on when there appears to be an audio signal |
|
if (!is_powered && activeTicks > powerOnInertia) { |
|
sendrf(packet(kAddress, kRF_One_On)); |
|
is_powered = true; |
|
|
|
// Add extra time to avoid turning on and off repeatedly |
|
const uint16_t histeresis = (60/WDT_PERIOD_SECS); |
|
activeTicks = histeresis; |
|
} |
|
|
|
if (activeTicks < maxTickAccumulation) { |
|
activeTicks++; |
|
} |
|
|
|
} else { |
|
if (activeTicks != 0) { |
|
activeTicks--; |
|
} |
|
|
|
if (activeTicks == 0 && is_powered) { |
|
sendrf(packet(kAddress, kRF_One_Off)); |
|
is_powered = false; |
|
} |
|
} |
|
} |
|
|
|
// Go to sleep (idle) until the watchdog timer wakes us up again |
|
sleep_enable(); |
|
sleep_cpu(); |
|
sleep_disable(); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
ISR(WDT_vect) |
|
{ |
|
// An actual interrupt is required to wake from sleep using the watchdog timer. |
|
// This empty implementation is here to override the default vector of a soft reset |
|
return; |
|
} |