diff --git a/examples/shotStopper/shotStopper.ino b/examples/shotStopper/shotStopper.ino index 6ca94b5..3a5fd8f 100644 --- a/examples/shotStopper/shotStopper.ino +++ b/examples/shotStopper/shotStopper.ino @@ -24,35 +24,26 @@ #include #include +#define FIRMWARE_VERSION 1 #define MAX_OFFSET 5 // In case an error in brewing occured -#define MIN_SHOT_DURATION_S 3 //Useful for flushing the group. - // This ensure that the system will ignore - // "shots" that last less than this duration -#define MAX_SHOT_DURATION_S 50 //Primarily useful for latching switches, since user - // looses control of the paddle once the system - // latches. #define BUTTON_READ_PERIOD_MS 5 -#define DRIP_DELAY_S 3 // Time after the shot ended to measure the final weight -#define EEPROM_SIZE 2 // This is 1-Byte -#define WEIGHT_ADDR 0 // Use the first byte of EEPROM to store the goal weight -#define OFFSET_ADDR 1 +#define EEPROM_SIZE 9 +#define SIGNATURE_ADDR 0 // Use the first byte to store a magic number/signature to know if the memory has been initialized +#define WEIGHT_ADDR 1 // Use the second byte of EEPROM to store the goal weight +#define OFFSET_ADDR 2 +#define MOMENTARY_ADDR 3 +#define REEDSWITCH_ADDR 4 +#define AUTOTARE_ADDR 5 +#define MIN_SHOT_DURATION_S_ADDR 6 +#define MAX_SHOT_DURATION_S_ADDR 7 +#define DRIP_DELAY_S_ADDR 8 +#define SIGNATURE_VALUE 0xAA #define DEBUG false #define N 10 // Number of datapoints used to calculate trend line -//User defined*** -#define MOMENTARY false //Define brew switch style. - // True for momentary switches such as GS3 AV, Silvia Pro - // false for latching switches such as Linea Mini/Micra -#define REEDSWITCH false // Set to true if the brew state is being determined - // by a reed switch attached to the brew solenoid -#define AUTOTARE true // Automatically tare when shot is started - // and 3 seconds after a latching switch brew - // (as defined by MOMENTARY) -//*************** - // Board Hardware #ifdef ARDUINO_ESP32S3_DEV #define LED_RED 46 @@ -71,6 +62,23 @@ #define BUTTON_STATE_ARRAY_LENGTH 31 +// configuration variables +bool reedSwitch = false; // Set to true if the brew state is being determined + // by a reed switch attached to the brew solenoid +bool momentary = false; // Define brew switch style. + // True for momentary switches such as GS3 AV, Silvia Pro + // false for latching switches such as Linea Mini/Micra +bool autoTare = true; // Automatically tare when shot is started + // and 3 seconds after a latching switch brew + // (as defined by MOMENTARY) +uint8_t minShotDurationS = 3; // Useful for flushing the group. + // This ensure that the system will ignore + // "shots" that last less than this duration +uint8_t maxShotDurationS = 50; // Primarily useful for latching switches, since user + // looses control of the paddle once the system + // latches. +uint8_t dripDelayS = 3; // Time after the shot ended to measure the final weight + typedef enum {BUTTON, WEIGHT, TIME, UNDEF} ENDTYPE; // RGB Colors {Red,Green,Blue} @@ -92,7 +100,7 @@ float error = 0; int buttonArr[BUTTON_STATE_ARRAY_LENGTH]; // last 4 readings of the button // button -int in = REEDSWITCH ? REED_IN : IN; +int in = reedSwitch ? REED_IN : IN; bool buttonPressed = false; //physical status of button bool buttonLatched = false; //electrical status of button unsigned long lastButtonRead_ms = 0; @@ -114,35 +122,152 @@ struct Shot { Shot shot = {0,0,0,0,{},{},0,false,ENDTYPE::UNDEF}; //BLE peripheral device -BLEService weightService("0x0FFE"); // create service +BLEService shotStopperService("0x0FFE"); // create service BLEByteCharacteristic weightCharacteristic("0xFF11", BLEWrite | BLERead); +BLEByteCharacteristic reedSwitchCharacteristic("0xFF12", BLEWrite | BLERead); +BLEByteCharacteristic momentaryCharacteristic("0xFF13", BLEWrite | BLERead); +BLEByteCharacteristic autoTareCharacteristic("0xFF14", BLEWrite | BLERead); +BLEByteCharacteristic minShotDurationSCharacteristic("0xFF15", BLEWrite | BLERead); +BLEByteCharacteristic maxShotDurationSCharacteristic("0xFF16", BLEWrite | BLERead); +BLEByteCharacteristic dripDelaySCharacteristic("0xFF17", BLEWrite | BLERead); +BLEByteCharacteristic firmwareVersionCharacteristic("0xFF18", BLERead); +BLEByteCharacteristic scaleStatusCharacteristic("0xFF19", BLERead | BLENotify); + +enum ScaleStatus { + STATUS_DISCONNECTED = 0, + STATUS_CONNECTED = 1, +}; -void setup() { - setCpuFrequencyMhz(80); - Serial.begin(9600); - EEPROM.begin(EEPROM_SIZE); +uint8_t lastScaleStatus = 255; // Invalid initial value to force first update - // Get stored setpoint and offset - goalWeight = EEPROM.read(WEIGHT_ADDR); - weightOffset = EEPROM.read(OFFSET_ADDR)/10.0; - Serial.print("Goal Weight retrieved: "); - Serial.println(goalWeight); - Serial.print("offset retrieved: "); - Serial.println(goalWeight); - - //If eeprom isn't initialized and has an - // unreasonable weight/offset, default to 36g/1.5g - if( (goalWeight < 10) || (goalWeight > 200) ){ - goalWeight = 36; - Serial.print("Goal Weight set to: "); - Serial.println(goalWeight); +void updateScaleStatus(uint8_t newScaleStatus) { + if (newScaleStatus != lastScaleStatus) { + scaleStatusCharacteristic.writeValue(newScaleStatus); + lastScaleStatus = newScaleStatus; } - if(weightOffset > MAX_OFFSET){ +} + +void loadOrInitEEPROM() { + EEPROM.begin(EEPROM_SIZE); + if (EEPROM.read(SIGNATURE_ADDR) != SIGNATURE_VALUE) { + goalWeight = 36; weightOffset = 1.5; - Serial.print("Offset set to: "); + EEPROM.write(SIGNATURE_ADDR, SIGNATURE_VALUE); + EEPROM.write(WEIGHT_ADDR, goalWeight); + EEPROM.write(OFFSET_ADDR, (uint8_t)(weightOffset * 10)); + EEPROM.write(MOMENTARY_ADDR, momentary ? 1 : 0); + EEPROM.write(REEDSWITCH_ADDR, reedSwitch ? 1 : 0); + EEPROM.write(AUTOTARE_ADDR, autoTare ? 1 : 0); + EEPROM.write(MIN_SHOT_DURATION_S_ADDR, minShotDurationS); + EEPROM.write(MAX_SHOT_DURATION_S_ADDR, maxShotDurationS); + EEPROM.write(DRIP_DELAY_S_ADDR, dripDelayS); + EEPROM.commit(); + Serial.println("EEPROM initialized with defaults"); + } else { + goalWeight = EEPROM.read(WEIGHT_ADDR); + weightOffset = EEPROM.read(OFFSET_ADDR) / 10.0; + Serial.print("Goal Weight retrieved: "); + Serial.println(goalWeight); + Serial.print("Offset retrieved: "); Serial.println(weightOffset); + momentary = EEPROM.read(MOMENTARY_ADDR) != 0; + reedSwitch = EEPROM.read(REEDSWITCH_ADDR) != 0; + in = reedSwitch ? REED_IN : IN; + autoTare = EEPROM.read(AUTOTARE_ADDR) != 0; + minShotDurationS = EEPROM.read(MIN_SHOT_DURATION_S_ADDR); + maxShotDurationS = EEPROM.read(MAX_SHOT_DURATION_S_ADDR); + dripDelayS = EEPROM.read(DRIP_DELAY_S_ADDR); } - +} + +void initializeBLE() { + BLE.begin(); + BLE.setLocalName("shotStopper"); + BLE.setAdvertisedService(shotStopperService); + shotStopperService.addCharacteristic(weightCharacteristic); + shotStopperService.addCharacteristic(momentaryCharacteristic); + shotStopperService.addCharacteristic(reedSwitchCharacteristic); + shotStopperService.addCharacteristic(autoTareCharacteristic); + shotStopperService.addCharacteristic(minShotDurationSCharacteristic); + shotStopperService.addCharacteristic(maxShotDurationSCharacteristic); + shotStopperService.addCharacteristic(dripDelaySCharacteristic); + shotStopperService.addCharacteristic(firmwareVersionCharacteristic); + shotStopperService.addCharacteristic(scaleStatusCharacteristic); + BLE.addService(shotStopperService); + weightCharacteristic.writeValue(goalWeight); + momentaryCharacteristic.writeValue(momentary ? 1 : 0); + reedSwitchCharacteristic.writeValue(reedSwitch ? 1 : 0); + autoTareCharacteristic.writeValue(autoTare ? 1 : 0); + minShotDurationSCharacteristic.writeValue(minShotDurationS); + maxShotDurationSCharacteristic.writeValue(maxShotDurationS); + dripDelaySCharacteristic.writeValue(dripDelayS); + firmwareVersionCharacteristic.writeValue(FIRMWARE_VERSION); + scaleStatusCharacteristic.writeValue(STATUS_DISCONNECTED); + BLE.advertise(); + Serial.println("Bluetooth® device active, waiting for connections..."); + BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler); +} + +void blePeripheralDisconnectHandler(BLEDevice central) { + BLE.advertise(); +} + +void pollAndReadBLE() { + bool updated = false; + BLE.poll(); + if (weightCharacteristic.written()) { + Serial.print("goal weight updated from "); + Serial.print(goalWeight); + Serial.print(" to "); + goalWeight = weightCharacteristic.value(); + Serial.println(goalWeight); + EEPROM.write(WEIGHT_ADDR, goalWeight); //1 byte, 0-255 + updated = true; + EEPROM.commit(); + } + if (momentaryCharacteristic.written()) { + momentary = momentaryCharacteristic.value() != 0; + EEPROM.write(MOMENTARY_ADDR, momentary ? 1 : 0); + updated = true; + } + if (reedSwitchCharacteristic.written()) { + reedSwitch = reedSwitchCharacteristic.value() != 0; + in = reedSwitch ? REED_IN : IN; + EEPROM.write(REEDSWITCH_ADDR, reedSwitch ? 1 : 0); + updated = true; + } + if (autoTareCharacteristic.written()) { + autoTare = autoTareCharacteristic.value() != 0; + EEPROM.write(AUTOTARE_ADDR, autoTare ? 1 : 0); + updated = true; + } + if (minShotDurationSCharacteristic.written()) { + minShotDurationS = minShotDurationSCharacteristic.value(); + EEPROM.write(MIN_SHOT_DURATION_S_ADDR, minShotDurationS); + updated = true; + } + if (maxShotDurationSCharacteristic.written()) { + maxShotDurationS = maxShotDurationSCharacteristic.value(); + EEPROM.write(MAX_SHOT_DURATION_S_ADDR, maxShotDurationS); + updated = true; + } + if (dripDelaySCharacteristic.written()) { + dripDelayS = dripDelaySCharacteristic.value(); + EEPROM.write(DRIP_DELAY_S_ADDR, dripDelayS); + updated = true; + } + if (updated) { + EEPROM.commit(); + } +} + +void setup() { + setCpuFrequencyMhz(80); + Serial.begin(9600); + + // If eeprom isn't initialized default to 36g/1.5g + loadOrInitEEPROM(); + // initialize the GPIO hardware pinMode(LED_BUILTIN, OUTPUT); pinMode(in, INPUT_PULLUP); @@ -153,21 +278,18 @@ void setup() { setColor(OFF); // initialize the BLE hardware - BLE.begin(); - BLE.setLocalName("shotStopper"); - BLE.setAdvertisedService(weightService); - weightService.addCharacteristic(weightCharacteristic); - BLE.addService(weightService); - weightCharacteristic.writeValue(goalWeight); - BLE.advertise(); - Serial.println("Bluetooth® device active, waiting for connections..."); + initializeBLE(); } + + void loop() { + // Check for setpoint updates + pollAndReadBLE(); // Connect to scale - while(!scale.isConnected()){ - + if (!scale.isConnected()) { + updateScaleStatus(STATUS_DISCONNECTED); setColor(RED); scale.init(); currentWeight = 0; @@ -176,21 +298,12 @@ void loop() { } if(scale.isConnected()){ setColor(YELLOW); + updateScaleStatus(STATUS_CONNECTED); + } else { + return; } } - // Check for setpoint updates - BLE.poll(); - if (weightCharacteristic.written()) { - Serial.print("goal weight updated from "); - Serial.print(goalWeight); - Serial.print(" to "); - goalWeight = weightCharacteristic.value(); - Serial.println(goalWeight); - EEPROM.write(WEIGHT_ADDR, goalWeight); //1 byte, 0-255 - EEPROM.commit(); - } - // Send a heartbeat message to the scale periodically to maintain connection if(scale.heartbeatRequired()){ scale.heartbeat(); @@ -247,7 +360,7 @@ void loop() { //Serial.print(buttonArr[i]); } //Serial.println(); - if(REEDSWITCH && !shot.brewing && seconds_f() < (shot.start_timestamp_s + shot.end_s + 0.5)){ + if(reedSwitch && !shot.brewing && seconds_f() < (shot.start_timestamp_s + shot.end_s + 0.5)){ newButtonState = 0; } } @@ -256,23 +369,23 @@ void loop() { if(newButtonState && buttonPressed == false ){ Serial.println("ButtonPressed"); buttonPressed = true; - if(!MOMENTARY){ + if(!momentary){ shot.brewing = true; setBrewingState(shot.brewing); } } // button held. Take over for the rest of the shot. - else if(!MOMENTARY + else if(!momentary && shot.brewing && !buttonLatched - && (shot.shotTimer > MIN_SHOT_DURATION_S) + && (shot.shotTimer > minShotDurationS) ){ buttonLatched = true; Serial.println("Button Latched"); digitalWrite(OUT,HIGH); Serial.println("wrote high"); // Get the scale to beep to inform user. - if(AUTOTARE){ + if(autoTare){ scale.tare(); } } @@ -292,7 +405,7 @@ void loop() { } //Max duration reached - else if(shot.brewing && shot.shotTimer > MAX_SHOT_DURATION_S ){ + else if(shot.brewing && shot.shotTimer > maxShotDurationS ){ shot.brewing = false; Serial.println("Max brew duration reached"); shot.end = ENDTYPE::TIME; @@ -307,7 +420,7 @@ void loop() { //End shot if(shot.brewing && shot.shotTimer >= shot.expected_end_s - && shot.shotTimer > MIN_SHOT_DURATION_S + && shot.shotTimer > minShotDurationS ){ Serial.println("weight achieved"); shot.brewing = false; @@ -319,7 +432,7 @@ void loop() { if(shot.start_timestamp_s && shot.end_s && currentWeight >= (goalWeight - weightOffset) - && seconds_f() > shot.start_timestamp_s + shot.end_s + DRIP_DELAY_S){ + && seconds_f() > shot.start_timestamp_s + shot.end_s + dripDelayS){ shot.start_timestamp_s = 0; shot.end_s = 0; @@ -332,12 +445,10 @@ void loop() { if( abs(currentWeight - goalWeight + weightOffset) > MAX_OFFSET ){ Serial.print("g. Error assumed. Offset unchanged. "); - } - else{ + }else{ Serial.print("g. Next time I'll create an offset of "); weightOffset += currentWeight - goalWeight; Serial.print(weightOffset); - EEPROM.write(OFFSET_ADDR, weightOffset*10); //1 byte, 0-255 EEPROM.commit(); } @@ -353,7 +464,7 @@ void setBrewingState(bool brewing){ shot.datapoints = 0; scale.resetTimer(); scale.startTimer(); - if(AUTOTARE){ + if(autoTare){ scale.tare(); } Serial.println("Weight Timer End"); @@ -376,13 +487,13 @@ void setBrewingState(bool brewing){ shot.end_s = seconds_f() - shot.start_timestamp_s; scale.stopTimer(); - if(MOMENTARY && + if(momentary && (ENDTYPE::WEIGHT == shot.end || ENDTYPE::TIME == shot.end)){ //Pulse button to stop brewing digitalWrite(OUT,HIGH);Serial.println("wrote high"); delay(300); digitalWrite(OUT,LOW);Serial.println("wrote low"); - }else if(!MOMENTARY){ + }else if(!momentary){ buttonLatched = false; buttonPressed = false; Serial.println("Button Unlatched and not pressed"); @@ -397,7 +508,7 @@ void calculateEndTime(Shot* s){ // Do not predict end time if there aren't enough espresso measurements yet if( (s->datapoints < N) || (s->weight[s->datapoints-1] < 10) ){ - s->expected_end_s = MAX_SHOT_DURATION_S; + s->expected_end_s = maxShotDurationS; } else{ //Get line of best fit (y=mx+b) from the last 10 measurements