From 1207810310561e2d429ce8c31c5ed0e5d36a7856 Mon Sep 17 00:00:00 2001 From: Matthieu Mitrani Date: Wed, 15 Apr 2026 15:10:05 +0100 Subject: [PATCH 1/2] Adds BOOKOO scales commands --- AcaiaArduinoBLE.cpp | 36 ++++++++++++++++++++++++ AcaiaArduinoBLE.h | 2 ++ examples/shotStopper/AcaiaArduinoBLE.cpp | 36 ++++++++++++++++++++++++ examples/shotStopper/AcaiaArduinoBLE.h | 2 ++ 4 files changed, 76 insertions(+) diff --git a/AcaiaArduinoBLE.cpp b/AcaiaArduinoBLE.cpp index f95c931..a59b552 100644 --- a/AcaiaArduinoBLE.cpp +++ b/AcaiaArduinoBLE.cpp @@ -21,6 +21,15 @@ byte TARE_GENERIC[6] = { 0x03, 0x0a, 0x01, 0x00, 0x00, 0x08 }; byte START_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x04, 0x00, 0x00, 0x0a }; byte STOP_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x05, 0x00, 0x00, 0x0d }; byte RESET_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x06, 0x00, 0x00, 0x0c }; +byte TARE_START_TIMER_BOOKOO[6] = { 0x03, 0x0a, 0x07, 0x00, 0x00, 0x00 }; +const byte BEEP_LEVEL_BOOKOO[6][6] = { + {0x03, 0x0A, 0x02, 0x00, 0x00, 0x0B}, + {0x03, 0x0A, 0x02, 0x00, 0x01, 0x0A}, + {0x03, 0x0A, 0x02, 0x00, 0x02, 0x09}, + {0x03, 0x0A, 0x02, 0x00, 0x03, 0x08}, + {0x03, 0x0A, 0x02, 0x00, 0x04, 0x0F}, + {0x03, 0x0A, 0x02, 0x00, 0x05, 0x0E} +}; /* Generic commands from https://github.com/graphefruit/Beanconqueror/blob/master/src/classes/devices/felicita/constants.ts @@ -197,6 +206,33 @@ bool AcaiaArduinoBLE::resetTimer(){ } } +bool AcaiaArduinoBLE::tareStartTimer(){ + if(_write.writeValue(TARE_START_TIMER_BOOKOO, 6)){ + Serial.println("tare and start timer write successful"); + return true; + }else{ + _connected = false; + Serial.println("tare and start timer write failed"); + return false; + } +} + +bool AcaiaArduinoBLE::beep(int level){ + if (level < 0 || level > 5) { + Serial.println("beep level should be between 0 and 5"); + return false; + } + + if(_write.writeValue(BEEP_LEVEL_BOOKOO[level], 6)){ + Serial.println("beep level write successful"); + return true; + }else{ + _connected = false; + Serial.println("beep level write failed"); + return false; + } +} + bool AcaiaArduinoBLE::heartbeat(){ if(_write.writeValue(HEARTBEAT, 7)){ _lastHeartBeat = millis(); diff --git a/AcaiaArduinoBLE.h b/AcaiaArduinoBLE.h index 75bfc05..7ee368c 100644 --- a/AcaiaArduinoBLE.h +++ b/AcaiaArduinoBLE.h @@ -38,6 +38,8 @@ class AcaiaArduinoBLE{ bool startTimer(); bool stopTimer(); bool resetTimer(); + bool tareStartTimer(); + bool beep(int level); bool heartbeat(); float getWeight(); bool heartbeatRequired(); diff --git a/examples/shotStopper/AcaiaArduinoBLE.cpp b/examples/shotStopper/AcaiaArduinoBLE.cpp index f95c931..a59b552 100644 --- a/examples/shotStopper/AcaiaArduinoBLE.cpp +++ b/examples/shotStopper/AcaiaArduinoBLE.cpp @@ -21,6 +21,15 @@ byte TARE_GENERIC[6] = { 0x03, 0x0a, 0x01, 0x00, 0x00, 0x08 }; byte START_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x04, 0x00, 0x00, 0x0a }; byte STOP_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x05, 0x00, 0x00, 0x0d }; byte RESET_TIMER_GENERIC[6] = { 0x03, 0x0a, 0x06, 0x00, 0x00, 0x0c }; +byte TARE_START_TIMER_BOOKOO[6] = { 0x03, 0x0a, 0x07, 0x00, 0x00, 0x00 }; +const byte BEEP_LEVEL_BOOKOO[6][6] = { + {0x03, 0x0A, 0x02, 0x00, 0x00, 0x0B}, + {0x03, 0x0A, 0x02, 0x00, 0x01, 0x0A}, + {0x03, 0x0A, 0x02, 0x00, 0x02, 0x09}, + {0x03, 0x0A, 0x02, 0x00, 0x03, 0x08}, + {0x03, 0x0A, 0x02, 0x00, 0x04, 0x0F}, + {0x03, 0x0A, 0x02, 0x00, 0x05, 0x0E} +}; /* Generic commands from https://github.com/graphefruit/Beanconqueror/blob/master/src/classes/devices/felicita/constants.ts @@ -197,6 +206,33 @@ bool AcaiaArduinoBLE::resetTimer(){ } } +bool AcaiaArduinoBLE::tareStartTimer(){ + if(_write.writeValue(TARE_START_TIMER_BOOKOO, 6)){ + Serial.println("tare and start timer write successful"); + return true; + }else{ + _connected = false; + Serial.println("tare and start timer write failed"); + return false; + } +} + +bool AcaiaArduinoBLE::beep(int level){ + if (level < 0 || level > 5) { + Serial.println("beep level should be between 0 and 5"); + return false; + } + + if(_write.writeValue(BEEP_LEVEL_BOOKOO[level], 6)){ + Serial.println("beep level write successful"); + return true; + }else{ + _connected = false; + Serial.println("beep level write failed"); + return false; + } +} + bool AcaiaArduinoBLE::heartbeat(){ if(_write.writeValue(HEARTBEAT, 7)){ _lastHeartBeat = millis(); diff --git a/examples/shotStopper/AcaiaArduinoBLE.h b/examples/shotStopper/AcaiaArduinoBLE.h index 75bfc05..7ee368c 100644 --- a/examples/shotStopper/AcaiaArduinoBLE.h +++ b/examples/shotStopper/AcaiaArduinoBLE.h @@ -38,6 +38,8 @@ class AcaiaArduinoBLE{ bool startTimer(); bool stopTimer(); bool resetTimer(); + bool tareStartTimer(); + bool beep(int level); bool heartbeat(); float getWeight(); bool heartbeatRequired(); From 97ee315e612656926b9c6d300c0e11cc8d68b8b0 Mon Sep 17 00:00:00 2001 From: Matthieu Mitrani Date: Wed, 15 Apr 2026 15:10:49 +0100 Subject: [PATCH 2/2] Handles command specific to BOOKOO scales in shotStopper.ino --- examples/shotStopper/shotStopper.ino | 72 +++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/examples/shotStopper/shotStopper.ino b/examples/shotStopper/shotStopper.ino index 7403708..f483338 100644 --- a/examples/shotStopper/shotStopper.ino +++ b/examples/shotStopper/shotStopper.ino @@ -29,7 +29,7 @@ #define MAX_OFFSET 5 // In case an error in brewing occured #define BUTTON_READ_PERIOD_MS 5 -#define EEPROM_SIZE 75 +#define EEPROM_SIZE 78 #define SIGNATURE_ADDR 0 // Use the first byte to store a magic number/signature to know if the memory has been initialized #define ENABLED_ADDR 1 #define WEIGHT_ADDR 2 // Use the second byte of EEPROM to store the goal weight @@ -42,6 +42,9 @@ #define DRIP_DELAY_S_ADDR 9 #define WIFI_SSID_ADDR 10 #define WIFI_PASS_ADDR 42 +#define TARE_START_TIMER_ADDR 74 +#define BEEP_ADDR 75 +#define BEEP_LEVEL_ADDR 76 #define SIGNATURE_VALUE 0xAA #define DEBUG false @@ -99,6 +102,10 @@ bool enabled = true; // The shotStopper status, if disabled it wo String wifiSsid = ""; String wifiPass = ""; +bool tareStartTimer = false; // Scale is able to tare and reset + start timer in one command (Bookoo support only) +bool beep = false; // Scale is able to beep without relying on a tare command (Bookoo support only) +uint8_t beepLevel = 0; // Scale beep level between 0 (silent) and 5 (loudest) (Bookoo support only) + typedef enum {BUTTON, WEIGHT, TIME, UNDEF} ENDTYPE; // RGB Colors {Red,Green,Blue} @@ -158,7 +165,9 @@ BLEByteCharacteristic otaModeRequestedCharacteristic("0xFF21", BLEWrite | BLERe BLECharacteristic wifiSsidCharacteristic("0xFF22", BLEWrite | BLERead, 32); BLECharacteristic wifiPassCharacteristic("0xFF23", BLEWrite, 32); BLECharacteristic wifiIpCharacteristic("0xFF24", BLERead | BLENotify, 16); - +BLEByteCharacteristic tareStartTimerCharacteristic("0xFF25", BLEWrite | BLERead); +BLEByteCharacteristic beepCharacteristic("0xFF26", BLEWrite | BLERead); +BLEByteCharacteristic beepLevelCharacteristic("0xFF27", BLEWrite | BLERead); enum ScaleStatus { STATUS_DISCONNECTED = 0, @@ -238,6 +247,9 @@ void loadOrInitEEPROM() { EEPROM.write(DRIP_DELAY_S_ADDR, dripDelayS); writeStringToEEPROM(WIFI_SSID_ADDR, wifiSsid); writeStringToEEPROM(WIFI_PASS_ADDR, wifiPass); + EEPROM.write(TARE_START_TIMER_ADDR, tareStartTimer); + EEPROM.write(BEEP_ADDR, beep); + EEPROM.write(BEEP_LEVEL_ADDR, beepLevel); EEPROM.commit(); Serial.println("EEPROM initialized with defaults"); } else { @@ -257,6 +269,9 @@ void loadOrInitEEPROM() { dripDelayS = EEPROM.read(DRIP_DELAY_S_ADDR); wifiSsid = readStringFromEEPROM(WIFI_SSID_ADDR, 32); wifiPass = readStringFromEEPROM(WIFI_PASS_ADDR, 32); + tareStartTimer = EEPROM.read(TARE_START_TIMER_ADDR); + beep = EEPROM.read(BEEP_ADDR); + beepLevel = EEPROM.read(BEEP_LEVEL_ADDR); } } @@ -280,6 +295,9 @@ void initializeBLE() { shotStopperService.addCharacteristic(wifiSsidCharacteristic); shotStopperService.addCharacteristic(wifiPassCharacteristic); shotStopperService.addCharacteristic(wifiIpCharacteristic); + shotStopperService.addCharacteristic(tareStartTimerCharacteristic); + shotStopperService.addCharacteristic(beepCharacteristic); + shotStopperService.addCharacteristic(beepLevelCharacteristic); BLE.addService(shotStopperService); enabledCharacteristic.writeValue(enabled ? 1 : 0); weightCharacteristic.writeValue(goalWeight); @@ -295,6 +313,9 @@ void initializeBLE() { otaModeRequestedCharacteristic.writeValue(otaModeRequested ? 1 : 0); writeStringToCharacteristic(wifiSsidCharacteristic, wifiSsid); writeStringToCharacteristic(wifiIpCharacteristic, lastWifiIp); + tareStartTimerCharacteristic.writeValue(tareStartTimer ? 1 : 0); + beepCharacteristic.writeValue(beep ? 1 : 0); + beepLevelCharacteristic.writeValue(beepLevel); BLE.advertise(); Serial.println("Bluetooth® device active, waiting for connections..."); BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler); @@ -372,6 +393,21 @@ void pollAndReadBLE() { writeStringToEEPROM(WIFI_PASS_ADDR, wifiPass); updated = true; } + if (tareStartTimerCharacteristic.written()) { + tareStartTimer = tareStartTimerCharacteristic.value() != 0; + EEPROM.write(TARE_START_TIMER_ADDR, tareStartTimer ? 1 : 0); + updated = true; + } + if (beepCharacteristic.written()) { + beep = beepCharacteristic.value() != 0; + EEPROM.write(BEEP_ADDR, beep ? 1 : 0); + updated = true; + } + if (beepLevelCharacteristic.written()) { + beepLevel = beepLevelCharacteristic.value(); + EEPROM.write(BEEP_LEVEL_ADDR, beepLevel); + updated = true; + } if (updated) { EEPROM.commit(); } @@ -523,7 +559,15 @@ void loop() { Serial.println("Button Latched"); digitalWrite(OUT,HIGH); Serial.println("wrote high"); // Get the scale to beep to inform user. - if(autoTare){ + if (beep) { + // 3 rapid beeps + scale.beep(beepLevel); + delay(100); + scale.beep(beepLevel); + delay(100); + scale.beep(beepLevel); + } + else if(autoTare){ scale.tare(); } } @@ -591,6 +635,14 @@ void loop() { EEPROM.commit(); } Serial.println(); + + // Inform user final weight detection is done and scale can be operated safely + if (beep) { + // Two long beeps + scale.beep(beepLevel); + delay(250); + scale.beep(beepLevel); + } } } @@ -600,11 +652,17 @@ void setBrewingState(bool brewing){ shot.start_timestamp_s = seconds_f(); shot.shotTimer = 0; shot.datapoints = 0; - scale.resetTimer(); - scale.startTimer(); - if(autoTare){ - scale.tare(); + + if (tareStartTimer) { + scale.tareStartTimer(); + } else { + scale.resetTimer(); + scale.startTimer(); + if(autoTare){ + scale.tare(); + } } + Serial.println("Weight Timer End"); }else{ Serial.print("ShotEnded by ");