Today I'm taking that great leap to the cloud. The Perpetuum Ebner goes online and publishes what's happening.
What is the Turntable Publishing?
I have 3 interesting sets of data available in my design.
- The raw sampled data, 128 8-bit samples covering the audio frequency range (signal B).
- That same information transposed to the frequency domain: 64 buckets, each representing a subset of that frequency spectrum (signal C).
- Those 64 buckets grouped into a Bass, Mid and High range (signal D). That's the info that drives the LEDs of the light organ.
I could publish any of the three. To make the decision, I thought about what the consumer is going to do with the data.
My plan is to make a remote light organ, and a WiFi spectrum display.
For the light organ, signal D would be OK. In that case I could even simplify the signal and stick with 2 bits to hold the on-off information for the 3 LEDs.
But if I want to keep the options open for a wireless spectrum display, I'll need signal C. That signal is also usable as source for the remote light organ.
The easiest decision would be to send signal B, the full samples. But engineering is about making decisions and trade-offs.
I'm not painting myself in a corner here. In the future I can always switch to signal B if needed. I could publish them in parallel until all subscribers are converted, and then switch of signal C.
Firmware with MQTT Publishing
The firmware builds upon the previous versions. The only addition is that I now publish the 64 buckets every 20th sample.
This is again a trade-off. My design can go faster, but I figure that eclipse.org doesn't like to be hammered by my data every few milliseconds.
So I settled for the lowest speed that would still give an interesting and lively visual result.
I subscribed to my data with eclipse PaHo client. The screen capture above shows the received data. It's normal that the characters don't display right, because the payload isn't text. It's 64 8-bit nuggets of info.
And here's the latest firmware:
// includes for servo
#include <Servo.h>
// includes for light organ
#include <fix_fft.h>
// MQTT includes start
#include <Bridge.h>
#include <SPI.h>
#include <YunClient.h>
#include <IPStack.h>
#include <Countdown.h>
#include <MQTTClient.h>
#include <string.h>
// MQTT includes end
// servo declaration
Servo servo;
#define SERVO_PIN 9
#define SERVO_DELAY 25
#define SERVO_BASE 90
#define SERVO_TOP (SERVO_BASE + 61)
int servoPos = SERVO_BASE;
bool servoUp = false;
// light organ declaration
#define MUESTRAS 128 // Numero de muestras para el cálculo de la FFT
#define LOGM 7 // Logaritmo en base 2 del número de muestras
#define BAJOS_MEDIOS 7 // Nº de banda para el corte entre Bajos y Medios
#define MEDIOS_AGUDOS 35 // Nº de banda para el corte entre Medios y Agudos
#define BPIN 13 // Pin de salida Bajos
#define MPIN 12 // Pin de salida Medios
#define APIN 11 // Pin de salida Agudos
#define TIMERPIN 8
#define MAX_PASADAS 10 // Nº de pasadas para el cálculo de los lÃmites
char data[MUESTRAS]; // Array con los valores muestreados (parte real)
char im[MUESTRAS]; // Array con los valores muestreados (parte imaginaria)
unsigned char salida[MUESTRAS/2]; // Valores obtenidos de la FFT (espectro de 64 bandas)
unsigned char bajos,medios,agudos; // Valores calculados para cada canal
byte pasada, // nº de pasada para el cáculo de los lÃmites
acumBajos,acumMedios,acumAgudos, // acumuladores de veces que se supera el lÃmite
limBajos,limMedios,limAgudos; // lÃmites calculados para cada canal
// start MQTT declarations
YunClient c; // replace by a YunClient if running on a Yun
IPStack ipstack(c);
MQTT::Client<IPStack, Countdown> client = MQTT::Client<IPStack, Countdown>(ipstack);
char payLoadBuf[MUESTRAS/2] = {}; // I'm going to give one byte per range, so that I can choose to implement a graph if I want.
const char* send_topic = "PerpetuumEbnerSpectrum";
const char* _id = "1cfee8dd0afc_yun";
// end MQTT declarations
// both:
// decay is a decreasing counter that tells how long the servo will stay up after the last led flashed.
// reset to DECAY each time a led lights up
// looses one live each time no leds are active
#define DECAY 150U
unsigned int uDecay = 0U;
// servo functionality
void servoInit() {
servoUp = false;
servo.attach(SERVO_PIN);
servoPos = SERVO_BASE;
servo.write(SERVO_BASE);
delay(1000);
}
void servoGoUp() {
if (servoPos < SERVO_BASE) {
servoInit();
}
if (servoPos < SERVO_TOP) {
servoPos++;
servo.write(servoPos);
delay(SERVO_DELAY);
}
}
void servoGoDown() {
if (servoPos < SERVO_BASE) {
servoInit();
}
if (servoPos > SERVO_BASE) {
servoPos--;
servo.write(servoPos);
delay(SERVO_DELAY);
}
}
void servoGo() {
if (servoUp) {
servoGoUp();
} else {
servoGoDown();
}
}
// light organ fuctions
/*
* Funcion que aplica una ventana de Hann a los datos muestreados para reducir el
* efecto de las discontinuidades en los extremos
*/
void aplicaVentana (char *vData) {
double muestrasMenosUno = (double(MUESTRAS) - 1.0);
// Como la ventana es simétrica , se calcula para la mitad y se aplica el factor por los dos extremos
for (uint8_t i = 0; i < MUESTRAS/2 ; i++) {
double indiceMenosUno = double(i);
double ratio = (indiceMenosUno / muestrasMenosUno);
double factorPeso = 0.5 * (1.0 - cos(6.28 * ratio));
vData[i] *= factorPeso;
vData[MUESTRAS - (i + 1)] *= factorPeso;
}
}
void lightOrganSetup() {
// Configuramos el prescaler a 32 -> 16Mhz/32 = 500 KHz
// como cada conversion son 13 ciclos 500/13 ~ 38.4KHz
// Es decir podemos medir en teoria hasta unos 19KHz,
// que para este proyecto sobra.
bitWrite(ADCSRA,ADPS2,1);
bitWrite(ADCSRA,ADPS1,0);
bitWrite(ADCSRA,ADPS0,1);
// Como la señal es muy baja,utilizamos la referencia interna
// de 1.1 V en vez de la de defecto de 5 V.
analogReference(INTERNAL);
// Salidas para los canales de Bajos,Medios y Agudos
pinMode(BPIN,OUTPUT);
pinMode(MPIN,OUTPUT);
pinMode(APIN,OUTPUT);
// debug
pinMode(TIMERPIN,OUTPUT);
Serial.begin(9600);
// Variables para el cálculo de los lÃmites
pasada = 0;
acumBajos = acumMedios = acumAgudos = 0;
limBajos = limMedios = limAgudos = 50;
}
void lightOrganGo() {
// Realizamos el muestreo
// Serial.println("Start sampling:");
digitalWrite(TIMERPIN, HIGH);
for( int i=0; i < MUESTRAS; i++) {
data[i] = analogRead(0)/4 -128; //Convertimos de 0..1024 a -128..127
im[i] = 0; // parte imaginaria = 0
}
digitalWrite(TIMERPIN, LOW);
/*
{
Serial.println(" samples\n");
for( int i=0; i < MUESTRAS; i++) {
Serial.print(data[i], DEC);
Serial.print(",");
}
Serial.println(" ---");
}
*/
// Aplicamos la ventana de Hann
aplicaVentana (data);
/*
Serial.println(" window\n");
for( int i=0; i < MUESTRAS; i++) {
Serial.print(data[i], DEC);
Serial.print(",");
}
Serial.println(" ---");
*/
// Calculamos la FFT
fix_fft(data,im,LOGM,0);
// Sólo nos interesan los valores absolutos, no las fases, asi que
// calculamos el módulos de los vectores re*re + im*im.
// Dado que los valores son pequeños utilizamos el cuadrado
for (int i=0; i < MUESTRAS/2; i++){
salida[i] = data[i] * data[i] + im[i] * im[i];
payLoadBuf[i] = salida[i];
}
/*
Serial.println(" result\n");
for( int i=0; i < MUESTRAS/2; i++) {
Serial.print(salida[i], DEC);
Serial.print(",");
}
Serial.println(" ---");
*/
// Ahora repartimos las bandas entre las 3 salidas
// En vez de sacar la media, utilizo sólo el valor máximo de
// una banda
bajos = 0;
for (int i=2; i < BAJOS_MEDIOS; i++){
bajos += salida[i];
}
bajos = bajos/2;
medios = 0;
for (int i=BAJOS_MEDIOS ; i < MEDIOS_AGUDOS; i++){
medios += salida[i];
}
medios = medios/2;
agudos = 0;
for (int i=MEDIOS_AGUDOS; i < MUESTRAS/2; i++){
agudos += /*2**/(salida[i]); // jc 20150604 undid because Yun ADC more sensitive or so // 20150601 doubled the highs sesitivity
}
agudos = agudos/2;
// Calculamos si el canal correspondiente
// supera le lÃmite para encenderlo
int siBajos = bajos > limBajos;
int siMedios = medios > limMedios;
int siAgudos = agudos > limAgudos;
digitalWrite(BPIN,siBajos ? HIGH : LOW);
digitalWrite(MPIN,siMedios? HIGH : LOW);
digitalWrite(APIN,siAgudos? HIGH : LOW);
// Utilizamos las veces que se supera para
// recalcular el lÃmite y evitar que con los
// cambios de volumen los canales se saturen
// o no funcionen.
acumBajos += siBajos;
acumMedios += siMedios;
acumAgudos += siAgudos;
if ( ++pasada > MAX_PASADAS ) {
pasada = 0;
limBajos = 20 + acumBajos*5;
limMedios = 20 + acumMedios*5;
limAgudos = 20 + acumAgudos*5;
acumBajos = 0;
acumMedios = 0;
acumAgudos = 0;
}
if (siBajos | siMedios /*| siAgudos*/) { // jc 20150601 renew lift up decay if a led is on,
// ignore highs because I've made them more sensitive above
uDecay = DECAY;
} else if (uDecay > 0) {
uDecay--;
}
}
// MQTT start
char printbuf[100];
void mqttInit() {
// Ethernet.begin(mac); // replace by Bridge.begin() if running on a Yun
Bridge.begin();
connect();
}
void connect() // connect to the MQTT broker
{
char hostname[] = "iot.eclipse.org";
int port = 1883;
sprintf(printbuf, "Connecting to %s:%d\n", hostname, port);
Serial.print(printbuf);
int rc = ipstack.connect(hostname, port);
if (rc != 1)
{
sprintf(printbuf, "rc from TCP connect is %d\n", rc);
Serial.print(printbuf);
}
Serial.println("MQTT connecting");
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
data.MQTTVersion = 3;
data.clientID.cstring = (char*)_id;
rc = client.connect(data);
if (rc != 0)
{
sprintf(printbuf, "rc from MQTT connect is %d\n", rc);
Serial.print(printbuf);
}
Serial.println("MQTT connected");
}
void sendMessage() {
if (!client.isConnected())
connect();
MQTT::Message message;
int rc;
// Send QoS 1 message
// // Serial.println(payLoadBuf);
message.qos = MQTT::QOS1;
message.retained = false;
message.dup = false;
message.payload = (void*)payLoadBuf;
message.payloadlen = strlen(payLoadBuf)+1;
rc = client.publish(send_topic, message);
}
// end MQTT functionality ==============================
// arduino general
void setup()
{
Serial.begin(9600);
delay(10000); // give me time to start the yun monitor
servoInit();
lightOrganSetup();
// MQTT related tasks
mqttInit();
}
void loop()
{
static unsigned int counter = 0;
/*
// lift debug
while (Serial.available() > 0) {
uDecay = (Serial.parseInt());
}
*/
servoUp = (uDecay > 0);
servoGo();
lightOrganGo();
if (uDecay > 0) {
counter++;
if (counter == 20) {
counter = 0;
sendMessage();
}
}
}
Feel free to subscribe and animate your own gizmo.



Top Comments