Add keypad, song status, volume, initial enclosure
This commit is contained in:
parent
110c8e9b7f
commit
bca0d18bfc
|
@ -5,6 +5,7 @@
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include "BluetoothA2DPSink.h"
|
#include "BluetoothA2DPSink.h"
|
||||||
#include <SparkFun_Qwiic_OLED.h> //http://librarymanager/All#SparkFun_Qwiic_OLED
|
#include <SparkFun_Qwiic_OLED.h> //http://librarymanager/All#SparkFun_Qwiic_OLED
|
||||||
|
#include "SparkFun_Qwiic_Keypad_Arduino_Library.h" //Click here to get the library: http://librarymanager/All#SparkFun_keypad
|
||||||
|
|
||||||
EspMQTTClient client(
|
EspMQTTClient client(
|
||||||
"YourSSID",
|
"YourSSID",
|
||||||
|
@ -29,14 +30,25 @@ WM8960 codec; // http://librarymanager/All#SparkFun_WM8960
|
||||||
BluetoothA2DPSink a2dp_sink; // https://github.com/pschatzmann/ESP32-A2DP
|
BluetoothA2DPSink a2dp_sink; // https://github.com/pschatzmann/ESP32-A2DP
|
||||||
Bsec iaqSensor; // Bsec Bosch sensors
|
Bsec iaqSensor; // Bsec Bosch sensors
|
||||||
QwiicMicroOLED myOLED; // OLED
|
QwiicMicroOLED myOLED; // OLED
|
||||||
|
KEYPAD keypad1; //Keypad
|
||||||
|
|
||||||
|
// via http://en.radzio.dxp.pl/bitmap_converter/
|
||||||
|
uint8_t volumeIcon[] = {
|
||||||
|
0x38, 0x38, 0x7C, 0x00, 0x10, 0x44, 0x38, 0x82, 0x7C,
|
||||||
|
};
|
||||||
|
|
||||||
String output;
|
String output;
|
||||||
String header = "Timestamp [ms], IAQ, IAQ accuracy, Static IAQ, CO2 equivalent, breath VOC equivalent, raw temp[°C], pressure [hPa], raw relative humidity [%], gas [Ohm], Stab Status, run in status, comp temp[°C], comp humidity [%], gas percentage";
|
String header = "Timestamp [ms], IAQ, IAQ accuracy, Static IAQ, CO2 equivalent, breath VOC equivalent, raw temp[°C], pressure [hPa], raw relative humidity [%], gas [Ohm], Stab Status, run in status, comp temp[°C], comp humidity [%], gas percentage";
|
||||||
unsigned long last_header = 0;
|
unsigned long last_header = 0;
|
||||||
unsigned long last_report = 0;
|
unsigned long last_sensor_run = 0;
|
||||||
int width, height;
|
int width, height;
|
||||||
uint8_t * playtime;
|
uint32_t playtime, play_position = 0; // milliseconds
|
||||||
String artist, title;
|
String artist, title;
|
||||||
|
int speakerVolume = 0;
|
||||||
|
int speakerAcGain = 0;
|
||||||
|
int speakerDcGain = 0;
|
||||||
|
bool useCelsius = false;
|
||||||
|
int playback_status = esp_avrc_playback_stat_t::ESP_AVRC_PLAYBACK_STOPPED;
|
||||||
|
|
||||||
// Helper functions declarations
|
// Helper functions declarations
|
||||||
void checkIaqSensorStatus(void);
|
void checkIaqSensorStatus(void);
|
||||||
|
@ -61,6 +73,8 @@ void setup()
|
||||||
|
|
||||||
a2dp_sink.set_avrc_metadata_attribute_mask(ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST | ESP_AVRC_MD_ATTR_PLAYING_TIME );
|
a2dp_sink.set_avrc_metadata_attribute_mask(ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST | ESP_AVRC_MD_ATTR_PLAYING_TIME );
|
||||||
a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback);
|
a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback);
|
||||||
|
a2dp_sink.set_avrc_rn_playstatus_callback(avrc_rn_playstatus_callback);
|
||||||
|
a2dp_sink.set_avrc_rn_play_pos_callback(avrc_rn_play_pos_callback);
|
||||||
a2dp_sink.start("WillTooth Audio"); // Note, you can give your device any name!
|
a2dp_sink.start("WillTooth Audio"); // Note, you can give your device any name!
|
||||||
|
|
||||||
// OLED setup
|
// OLED setup
|
||||||
|
@ -71,6 +85,11 @@ void setup()
|
||||||
width = myOLED.getWidth();
|
width = myOLED.getWidth();
|
||||||
height = myOLED.getHeight();
|
height = myOLED.getHeight();
|
||||||
|
|
||||||
|
if (keypad1.begin() == false)
|
||||||
|
{
|
||||||
|
Serial.println("Keypad setup failed.");
|
||||||
|
}
|
||||||
|
|
||||||
pinMode(LED_BUILTIN, OUTPUT);
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
digitalWrite(LED_BUILTIN, LOW);
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
iaqSensor.begin(BME68X_I2C_ADDR_LOW, Wire);
|
iaqSensor.begin(BME68X_I2C_ADDR_LOW, Wire);
|
||||||
|
@ -99,13 +118,25 @@ void setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
void avrc_metadata_callback(uint8_t data1, const uint8_t *data2) {
|
void avrc_metadata_callback(uint8_t data1, const uint8_t *data2) {
|
||||||
Serial.printf("AVRC metadata rsp: attribute id 0x%x, %s\n", data1, data2);
|
Serial.printf("AVRC metadata rsp: attribute id 0x%x \"%s\"\n", data1, data2);
|
||||||
if (data1 == 0x1)
|
if (data1 == 0x1)
|
||||||
title = String((char*)data2);
|
title = String((char*)data2);
|
||||||
else if (data1 == 0x2)
|
else if (data1 == 0x2)
|
||||||
artist = String((char*)data2);
|
artist = String((char*)data2);
|
||||||
// else if (data1 == 0x40)
|
else if (data1 == 0x40) {
|
||||||
// playtime = data2;
|
playtime = String((char*)data2).toInt(); // data2 is milliseconds as a text string
|
||||||
|
Serial.printf("Playtime \"%s\" stored as %d\n", data2, playtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void avrc_rn_play_pos_callback(uint32_t play_pos) {
|
||||||
|
Serial.printf("Got play_pos %d\n", play_pos);
|
||||||
|
play_position = play_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void avrc_rn_playstatus_callback(esp_avrc_playback_stat_t playback) {
|
||||||
|
Serial.printf("Got playstatus %d\n", playback);
|
||||||
|
playback_status = playback;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onConnectionEstablished() {
|
void onConnectionEstablished() {
|
||||||
|
@ -114,10 +145,58 @@ void onConnectionEstablished() {
|
||||||
|
|
||||||
void loop(void)
|
void loop(void)
|
||||||
{
|
{
|
||||||
|
keypad1.updateFIFO(); // necessary for keypad to pull button from stack to readable register
|
||||||
|
char button = keypad1.getButton();
|
||||||
|
if (button != 0 && button != -1) {
|
||||||
|
if (button == 0x32) { // '2'
|
||||||
|
if (speakerVolume < 9) speakerVolume += 1;
|
||||||
|
codec.setSpeakerVolumeDB(speakerVolume*2);
|
||||||
|
Serial.print("vol: ");
|
||||||
|
Serial.println(speakerVolume);
|
||||||
|
} else if (button == 0x38) { // '8'
|
||||||
|
if (speakerVolume > -10) speakerVolume -= 1;
|
||||||
|
codec.setSpeakerVolumeDB(speakerVolume*2);
|
||||||
|
Serial.print("vol: ");
|
||||||
|
Serial.println(speakerVolume);
|
||||||
|
} else if (button == 0x36) { // '6'
|
||||||
|
a2dp_sink.next();
|
||||||
|
Serial.print("next");
|
||||||
|
} else if (button == 0x34) { // '4'
|
||||||
|
a2dp_sink.previous();
|
||||||
|
Serial.print("prev");
|
||||||
|
} else if (button == 0x35) { // '5'
|
||||||
|
Serial.println("play/pause");
|
||||||
|
if (playback_status == esp_avrc_playback_stat_t::ESP_AVRC_PLAYBACK_PLAYING) a2dp_sink.pause();
|
||||||
|
else if (playback_status == esp_avrc_playback_stat_t::ESP_AVRC_PLAYBACK_PAUSED) a2dp_sink.play();
|
||||||
|
else if (playback_status == esp_avrc_playback_stat_t::ESP_AVRC_PLAYBACK_STOPPED) a2dp_sink.play();
|
||||||
|
else Serial.printf("pause err: %d\n", playback_status);
|
||||||
|
} else if (button == 0x23) { // '#'
|
||||||
|
useCelsius = !useCelsius;
|
||||||
|
Serial.print("celsius: ");
|
||||||
|
Serial.println(useCelsius);
|
||||||
|
} else {
|
||||||
|
Serial.print("Unknown: ");
|
||||||
|
Serial.println(button);
|
||||||
|
}
|
||||||
|
/*if( button == 0x31 ) Serial.println("(1)");
|
||||||
|
if( button == 0x32 ) Serial.println("(2)");
|
||||||
|
if( button == 0x33 ) Serial.println("(3)");
|
||||||
|
if( button == 0x34 ) Serial.println("(4)");
|
||||||
|
if( button == 0x35 ) Serial.println("(5)");
|
||||||
|
if( button == 0x36 ) Serial.println("(6)");
|
||||||
|
if( button == 0x37 ) Serial.println("(7)");
|
||||||
|
if( button == 0x38 ) Serial.println("(8)");
|
||||||
|
if( button == 0x39 ) Serial.println("(9)");
|
||||||
|
if( button == 0x2a ) Serial.println("(*)");
|
||||||
|
if( button == 0x30 ) Serial.println("(0)");
|
||||||
|
if( button == 0x23 ) Serial.println("(#)");*/
|
||||||
|
}
|
||||||
|
|
||||||
unsigned long time_trigger = millis();
|
unsigned long time_trigger = millis();
|
||||||
if (iaqSensor.run()) { // Do IAQ work and proceed unless there's an issue
|
if (last_sensor_run == 0 || time_trigger-last_sensor_run > 2000) {
|
||||||
if (last_report == 0 || time_trigger-last_report > 60000) {
|
last_sensor_run = time_trigger;
|
||||||
if (last_header == 0 || time_trigger-last_header > 600000) {
|
if (iaqSensor.run()) { // Do IAQ work and proceed unless there's an issue
|
||||||
|
if (last_header == 0 || time_trigger-last_header > 20000) {
|
||||||
Serial.println(header);
|
Serial.println(header);
|
||||||
last_header = time_trigger;
|
last_header = time_trigger;
|
||||||
}
|
}
|
||||||
|
@ -167,40 +246,79 @@ void loop(void)
|
||||||
String json;
|
String json;
|
||||||
serializeJson(obj,json);
|
serializeJson(obj,json);
|
||||||
client.publish("esp/status", json);
|
client.publish("esp/status", json);
|
||||||
|
// Serial.printf("Took %d ms\n", millis()-last_sensor_run);
|
||||||
digitalWrite(LED_BUILTIN, HIGH);
|
digitalWrite(LED_BUILTIN, HIGH);
|
||||||
last_report = time_trigger;
|
|
||||||
drawGraph(iaqSensor.temperature, iaqSensor.humidity, iaqSensor.co2Equivalent, iaqSensor.co2Accuracy);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
checkIaqSensorStatus();
|
checkIaqSensorStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
client.loop();
|
client.loop();
|
||||||
sleep(1);
|
drawGraph(iaqSensor.temperature, iaqSensor.humidity, iaqSensor.co2Equivalent, iaqSensor.co2Accuracy);
|
||||||
|
delay(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawGraph(int temp, int humid, int co2e, int co2acc)
|
void drawGraph(int temp, int humid, int co2e, int co2acc)
|
||||||
{
|
{
|
||||||
myOLED.erase();
|
myOLED.erase();
|
||||||
|
int lineHeight = myOLED.getStringHeight("H");
|
||||||
|
|
||||||
String out = "Temp: "+String(temp)+"'\n"+
|
String out;
|
||||||
"Hum: "+String(humid)+"%\n";
|
if (useCelsius)
|
||||||
|
out += String(temp)+"C ";
|
||||||
|
else
|
||||||
|
out += String((int)round(temp*1.8+32))+"F ";
|
||||||
|
|
||||||
if(co2acc>0)
|
out += String(humid)+"% ";
|
||||||
out += "CO2: "+String(co2e);
|
// out += String(speakerVolume+11); //+String(speakerAcGain)+" "+String(speakerDcGain)+"\n";
|
||||||
else
|
myOLED.setCursor(0, 0);
|
||||||
out += "CO2: --";
|
myOLED.print(out);
|
||||||
|
|
||||||
if(!title.isEmpty() && !artist.isEmpty())
|
int offset=0;
|
||||||
out += "\n"+title+" - "+artist;
|
if (speakerVolume < -5) offset=6;
|
||||||
|
else if (speakerVolume < 0) offset=4;
|
||||||
|
else if (speakerVolume < 5) offset=2;
|
||||||
|
// else leave at 0
|
||||||
|
|
||||||
myOLED.setCursor(0, 0);
|
myOLED.bitmap(myOLED.getWidth()-9, 0, 9-offset, 8, volumeIcon, 9, 8);
|
||||||
|
|
||||||
|
if(co2acc>0)
|
||||||
|
out = "CO2: "+String(co2e);
|
||||||
|
else
|
||||||
|
out = "CO2: --";
|
||||||
|
|
||||||
|
myOLED.setCursor(0, lineHeight*2);
|
||||||
|
myOLED.print(out);
|
||||||
|
|
||||||
|
if(!title.isEmpty() && !artist.isEmpty()) {
|
||||||
|
unsigned long time = millis();
|
||||||
|
int titlePos, artistPos = 0;
|
||||||
|
if (title.length() > 10) {
|
||||||
|
titlePos = (time/1000)%(title.length()-9);
|
||||||
|
}
|
||||||
|
if (artist.length() > 10) {
|
||||||
|
artistPos = (time/1000)%(artist.length()-9);
|
||||||
|
}
|
||||||
|
out = title.substring(titlePos,titlePos+10);
|
||||||
|
|
||||||
|
myOLED.setCursor(0, lineHeight*4-2);
|
||||||
myOLED.print(out);
|
myOLED.print(out);
|
||||||
|
|
||||||
//myOLED.line(xS, yS, xE, yE);
|
out = artist.substring(artistPos,artistPos+10);
|
||||||
myOLED.display();
|
|
||||||
delay(10);
|
myOLED.setCursor(0, lineHeight*5-2);
|
||||||
|
myOLED.print(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playtime>0) {
|
||||||
|
float playpct = (float)play_position/(float)playtime;
|
||||||
|
int height = myOLED.getHeight()-1;
|
||||||
|
// Serial.printf("(%d/%d = %.1fpct)\n", play_position, playtime, 100.0*playpct);
|
||||||
|
myOLED.line(0, height, round(myOLED.getWidth()*playpct), height);
|
||||||
|
}
|
||||||
|
|
||||||
|
myOLED.display();
|
||||||
|
delay(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function definitions
|
// Helper function definitions
|
||||||
|
|
88
SmartAirSpeaker.scad
Normal file
88
SmartAirSpeaker.scad
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
include <BOSL2/std.scad>
|
||||||
|
|
||||||
|
$fn=40;
|
||||||
|
|
||||||
|
leeway=0.5;
|
||||||
|
|
||||||
|
bodyFaceW=120;
|
||||||
|
bodyFaceH=100;
|
||||||
|
bodyBackW=20;
|
||||||
|
bodyBackH=15;
|
||||||
|
bodyDepth=50;
|
||||||
|
|
||||||
|
screenWidth=16.5+leeway; // visible
|
||||||
|
screenHeight=13+leeway; // visible
|
||||||
|
screenY=-35;
|
||||||
|
screenX=30;
|
||||||
|
screenCutoutWidth=19+leeway;
|
||||||
|
screenCutoutHeight=19.5+leeway;
|
||||||
|
screenMountWidth=21+leeway;
|
||||||
|
screenMountHeight=21+leeway;
|
||||||
|
|
||||||
|
keypadWidth=46+leeway; // visible
|
||||||
|
keypadHeight=57+leeway; // visible
|
||||||
|
keypadDepth=4;
|
||||||
|
keypadY=10;
|
||||||
|
keypadX=30;
|
||||||
|
keypadMountDia=1;
|
||||||
|
|
||||||
|
speakerWidth=45; // visible, 50 OD
|
||||||
|
speakerConeWidth=60;
|
||||||
|
speakerDepth=2.2;
|
||||||
|
speakerY=0;
|
||||||
|
speakerX=-27;
|
||||||
|
|
||||||
|
sensorWidth=4; // visible
|
||||||
|
sensorDepth=4;
|
||||||
|
sensorY=-15;
|
||||||
|
|
||||||
|
powerWidth=4; // visible
|
||||||
|
powerDepth=10;
|
||||||
|
powerY=0;
|
||||||
|
|
||||||
|
right_half()
|
||||||
|
diff("hole","mount") translate([0, 0, 0]) rotate([50,0,0]) {
|
||||||
|
// body
|
||||||
|
prismoid([bodyBackW, bodyBackH], [bodyFaceW, bodyFaceH], bodyDepth, rounding=4)
|
||||||
|
tag("hole") {
|
||||||
|
attach([TOP]) {
|
||||||
|
// screen
|
||||||
|
translate([screenX,-screenY,0.1]) cuboid([screenWidth,screenHeight,4], anchor=TOP);
|
||||||
|
// screen thickness cutout
|
||||||
|
translate([screenX,-screenY,-1]) cuboid([screenCutoutWidth,screenCutoutHeight,2], anchor=TOP)
|
||||||
|
tag("mount") {
|
||||||
|
position(FRONT+LEFT) translate([-2,2,-0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(BACK+LEFT) translate([-2,-4,-0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(FRONT+RIGHT) translate([2,2,-0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(BACK+RIGHT) translate([2,-4,-0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keypad
|
||||||
|
translate([keypadX,-keypadY,0.1]) cuboid([keypadWidth,keypadHeight,keypadDepth], rounding=4, edges=["Z"], anchor=TOP)
|
||||||
|
tag("mount") {
|
||||||
|
//TODO: actually measure these, consider screws
|
||||||
|
position(FRONT+LEFT) translate([-.2,-1,0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(BACK+LEFT) translate([-.2,1,0]) cylinder(h=2, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(FRONT+RIGHT) translate([.2,-1,0]) cylinder(h=1, d=keypadMountDia, anchor=TOP);
|
||||||
|
position(BACK+RIGHT) translate([.2,1,0]) cylinder(h=1, d=keypadMountDia, anchor=TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// speaker
|
||||||
|
translate([speakerX,-speakerY,0.1]) cylinder(h=speakerDepth, d1=speakerWidth, d2=speakerConeWidth, anchor=TOP)
|
||||||
|
tag("mount") {
|
||||||
|
zrot_copies([90, 180, 270]) left(speakerConeWidth/2)
|
||||||
|
cylinder(h=5, d1=2, d2=2, anchor=TOP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attach(BACK) {
|
||||||
|
// sensor
|
||||||
|
translate([0,-sensorY,0]) cylinder(h=sensorDepth, d=sensorWidth, center=true);
|
||||||
|
}
|
||||||
|
attach(BOTTOM) {
|
||||||
|
// power
|
||||||
|
translate([0,-powerY,0]) cylinder(h=powerDepth, d=powerWidth, center=true);
|
||||||
|
}
|
||||||
|
// hollow
|
||||||
|
prismoid([bodyBackW-5, bodyBackH-5], [bodyFaceW-6, bodyFaceH-6], bodyDepth-4, rounding=3, center=true);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user