Skip to content

Commit cdcb6a1

Browse files
Add AtomHeart Eclair scale support
- Add ECLAIR scale type with BLE UUIDs (WRITE_CHAR_ECLAIR, READ_CHAR_ECLAIR) - Add Eclair command arrays: TARE, START_TIMER, STOP_TIMER, RESET_TIMER - Add calculateXOR() and readInt32LittleEndian() helpers - Add Eclair detection in init() via device name prefix and characteristic discovery - Skip IDENTIFY/NOTIFICATION_REQUEST for Eclair (not needed) - Add Eclair weight packet parsing ('W' header, int32 LE, XOR validation) - Skip heartbeat for Eclair (not required) - Add Eclair to scale compatibility table and checklists - Bump version to 3.4.0
1 parent a15fac2 commit cdcb6a1

4 files changed

Lines changed: 165 additions & 104 deletions

File tree

AcaiaArduinoBLE.cpp

Lines changed: 124 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,35 @@ byte BEEP_LEVEL_1_BOOKOO[6] = { 0x03, 0x0a, 0x02, 0x00, 0x01, 0x0a };
2828
byte TARE_FELICITA[1] = { 0x54 }; // 'Tare'
2929
byte START_TIMER_FELICITA[1] = { 0x52 }; // 'Start_Timer'
3030
byte STOP_TIMER_FELICITA[1] = { 0x53 }; // 'Stop_Timer'
31-
byte RESET_TIMER_FELICITA[1] = { 0x43 }; // 'Reset_Timer'
32-
byte WEIGHT_TIMER_MODE_FELICITA[1] = { 0x32 }; // 'Weight+timer mode (known-good wake state)
31+
byte RESET_TIMER_FELICITA[1] = { 0x43 }; // 'Reset_Timer'
32+
byte WEIGHT_TIMER_MODE_FELICITA[1] = { 0x32 }; // 'Weight+timer mode (known-good wake state)
33+
byte TARE_ECLAIR[3] = { 0x54, 0x01, 0x01 };
34+
byte START_TIMER_ECLAIR[3] = { 0x53, 0x01, 0x01 };
35+
byte STOP_TIMER_ECLAIR[3] = { 0x45, 0x01, 0x01 };
36+
byte RESET_TIMER_ECLAIR[3] = { 0x52, 0x01, 0x01 };
3337

3438
/* Generic commands from
3539
https://github.com/graphefruit/Beanconqueror/blob/master/src/classes/devices/felicita/constants.ts
36-
*/
37-
38-
AcaiaArduinoBLE::AcaiaArduinoBLE(bool debug){
40+
*/
41+
42+
uint8_t calculateXOR(const byte data[], int length){
43+
uint8_t result = 0;
44+
for(int i = 0; i < length; i++){
45+
result ^= data[i];
46+
}
47+
return result;
48+
}
49+
50+
int32_t readInt32LittleEndian(const byte data[]){
51+
return (int32_t)(
52+
((uint32_t)data[0]) |
53+
((uint32_t)data[1] << 8) |
54+
((uint32_t)data[2] << 16) |
55+
((uint32_t)data[3] << 24)
56+
);
57+
}
58+
59+
AcaiaArduinoBLE::AcaiaArduinoBLE(bool debug){
3960
_debug = debug;
4061
_currentWeight = 0;
4162
_connected = false;
@@ -125,12 +146,17 @@ bool AcaiaArduinoBLE::init(String mac){
125146
_type = GENERIC;
126147
_write = peripheral.characteristic(WRITE_CHAR_GENERIC);
127148
_read = peripheral.characteristic(READ_CHAR_GENERIC);
128-
} else if(peripheral.characteristic(READ_CHAR_FELICITA).canSubscribe()){
129-
Serial.println("Felicita Arc Detected");
130-
_type = FELICITA;
131-
_write = peripheral.characteristic(WRITE_CHAR_FELICITA);
132-
_read = peripheral.characteristic(READ_CHAR_FELICITA);
133-
}
149+
} else if(peripheral.characteristic(READ_CHAR_FELICITA).canSubscribe()){
150+
Serial.println("Felicita Arc Detected");
151+
_type = FELICITA;
152+
_write = peripheral.characteristic(WRITE_CHAR_FELICITA);
153+
_read = peripheral.characteristic(READ_CHAR_FELICITA);
154+
} else if(peripheral.characteristic(READ_CHAR_ECLAIR).canSubscribe()){
155+
Serial.println("Eclair Scale Detected");
156+
_type = ECLAIR;
157+
_write = peripheral.characteristic(WRITE_CHAR_ECLAIR);
158+
_read = peripheral.characteristic(READ_CHAR_ECLAIR);
159+
}
134160
else{
135161
Serial.println("unable to determine scale type");
136162
return false;
@@ -159,16 +185,16 @@ bool AcaiaArduinoBLE::init(String mac){
159185
Serial.println("notification request write failed");
160186
return false;
161187
}
162-
}else if(_type == FELICITA){
163-
// Wake the Felicita into a deterministic, known-good mode
164-
// (weight + timer) instead of whatever IDENTIFY used to leave it in.
165-
if(_write.writeValue(WEIGHT_TIMER_MODE_FELICITA, sizeof(WEIGHT_TIMER_MODE_FELICITA))){
166-
Serial.println("felicita weight+timer mode set");
167-
}else{
168-
Serial.println("felicita mode set failed");
169-
return false;
170-
}
171-
}
188+
}else if(_type == FELICITA){
189+
// Wake the Felicita into a deterministic, known-good mode
190+
// (weight + timer) instead of whatever IDENTIFY used to leave it in.
191+
if(_write.writeValue(WEIGHT_TIMER_MODE_FELICITA, sizeof(WEIGHT_TIMER_MODE_FELICITA))){
192+
Serial.println("felicita weight+timer mode set");
193+
}else{
194+
Serial.println("felicita mode set failed");
195+
return false;
196+
}
197+
}
172198
_connected = true;
173199
_packetPeriod = 0;
174200
return true;
@@ -185,11 +211,12 @@ bool AcaiaArduinoBLE::init(String mac){
185211
// leaking adjacent memory to the scale as extra commands.
186212
bool AcaiaArduinoBLE::tare(){
187213
bool ok;
188-
switch(_type){
189-
case GENERIC: ok = _write.writeValue(TARE_GENERIC, sizeof(TARE_GENERIC)); break;
190-
case FELICITA: ok = _write.writeValue(TARE_FELICITA, sizeof(TARE_FELICITA)); break;
191-
default: ok = _write.writeValue(TARE_ACAIA, sizeof(TARE_ACAIA)); break;
192-
}
214+
switch(_type){
215+
case GENERIC: ok = _write.writeValue(TARE_GENERIC, sizeof(TARE_GENERIC)); break;
216+
case FELICITA: ok = _write.writeValue(TARE_FELICITA, sizeof(TARE_FELICITA)); break;
217+
case ECLAIR: ok = _write.writeValue(TARE_ECLAIR, sizeof(TARE_ECLAIR)); break;
218+
default: ok = _write.writeValue(TARE_ACAIA, sizeof(TARE_ACAIA)); break;
219+
}
193220
if(ok){
194221
Serial.println("tare write successful");
195222
return true;
@@ -202,11 +229,12 @@ bool AcaiaArduinoBLE::tare(){
202229

203230
bool AcaiaArduinoBLE::startTimer(){
204231
bool ok;
205-
switch(_type){
206-
case GENERIC: ok = _write.writeValue(START_TIMER_GENERIC, sizeof(START_TIMER_GENERIC)); break;
207-
case FELICITA: ok = _write.writeValue(START_TIMER_FELICITA, sizeof(START_TIMER_FELICITA)); break;
208-
default: ok = _write.writeValue(START_TIMER, sizeof(START_TIMER)); break;
209-
}
232+
switch(_type){
233+
case GENERIC: ok = _write.writeValue(START_TIMER_GENERIC, sizeof(START_TIMER_GENERIC)); break;
234+
case FELICITA: ok = _write.writeValue(START_TIMER_FELICITA, sizeof(START_TIMER_FELICITA)); break;
235+
case ECLAIR: ok = _write.writeValue(START_TIMER_ECLAIR, sizeof(START_TIMER_ECLAIR)); break;
236+
default: ok = _write.writeValue(START_TIMER, sizeof(START_TIMER)); break;
237+
}
210238
if(ok){
211239
Serial.println("start timer write successful");
212240
return true;
@@ -219,11 +247,12 @@ bool AcaiaArduinoBLE::startTimer(){
219247

220248
bool AcaiaArduinoBLE::stopTimer(){
221249
bool ok;
222-
switch(_type){
223-
case GENERIC: ok = _write.writeValue(STOP_TIMER_GENERIC, sizeof(STOP_TIMER_GENERIC)); break;
224-
case FELICITA: ok = _write.writeValue(STOP_TIMER_FELICITA, sizeof(STOP_TIMER_FELICITA)); break;
225-
default: ok = _write.writeValue(STOP_TIMER, sizeof(STOP_TIMER)); break;
226-
}
250+
switch(_type){
251+
case GENERIC: ok = _write.writeValue(STOP_TIMER_GENERIC, sizeof(STOP_TIMER_GENERIC)); break;
252+
case FELICITA: ok = _write.writeValue(STOP_TIMER_FELICITA, sizeof(STOP_TIMER_FELICITA)); break;
253+
case ECLAIR: ok = _write.writeValue(STOP_TIMER_ECLAIR, sizeof(STOP_TIMER_ECLAIR)); break;
254+
default: ok = _write.writeValue(STOP_TIMER, sizeof(STOP_TIMER)); break;
255+
}
227256
if(ok){
228257
Serial.println("stop timer write successful");
229258
return true;
@@ -236,11 +265,12 @@ bool AcaiaArduinoBLE::stopTimer(){
236265

237266
bool AcaiaArduinoBLE::resetTimer(){
238267
bool ok;
239-
switch(_type){
240-
case GENERIC: ok = _write.writeValue(RESET_TIMER_GENERIC, sizeof(RESET_TIMER_GENERIC)); break;
241-
case FELICITA: ok = _write.writeValue(RESET_TIMER_FELICITA, sizeof(RESET_TIMER_FELICITA)); break;
242-
default: ok = _write.writeValue(RESET_TIMER, sizeof(RESET_TIMER)); break;
243-
}
268+
switch(_type){
269+
case GENERIC: ok = _write.writeValue(RESET_TIMER_GENERIC, sizeof(RESET_TIMER_GENERIC)); break;
270+
case FELICITA: ok = _write.writeValue(RESET_TIMER_FELICITA, sizeof(RESET_TIMER_FELICITA)); break;
271+
case ECLAIR: ok = _write.writeValue(RESET_TIMER_ECLAIR, sizeof(RESET_TIMER_ECLAIR)); break;
272+
default: ok = _write.writeValue(RESET_TIMER, sizeof(RESET_TIMER)); break;
273+
}
244274
if(ok){
245275
Serial.println("reset timer write successful");
246276
return true;
@@ -251,32 +281,46 @@ bool AcaiaArduinoBLE::resetTimer(){
251281
}
252282
}
253283

254-
bool AcaiaArduinoBLE::tareStartTimer(){
255-
if(_write.writeValue(TARE_START_TIMER_BOOKOO, 6)){
256-
Serial.println("tare and start timer write successful");
257-
return true;
284+
bool AcaiaArduinoBLE::tareStartTimer(){
285+
bool ok;
286+
if(_type == ECLAIR){
287+
ok = _write.writeValue(START_TIMER_ECLAIR, sizeof(START_TIMER_ECLAIR));
288+
}else{
289+
ok = _write.writeValue(TARE_START_TIMER_BOOKOO, sizeof(TARE_START_TIMER_BOOKOO));
290+
}
291+
if(ok){
292+
Serial.println("tare and start timer write successful");
293+
return true;
258294
}else{
259295
_connected = false;
260296
Serial.println("tare and start timer write failed");
261297
return false;
262298
}
263299
}
264300

265-
bool AcaiaArduinoBLE::beep(){
266-
267-
// for acaia's, use the tare command to generate the beep
268-
if(_write.writeValue((_type == GENERIC ? BEEP_LEVEL_1_BOOKOO : TARE_ACAIA), 6)){
269-
Serial.println("beep write successful");
270-
return true;
301+
bool AcaiaArduinoBLE::beep(){
302+
bool ok;
303+
switch(_type){
304+
case GENERIC: ok = _write.writeValue(BEEP_LEVEL_1_BOOKOO, sizeof(BEEP_LEVEL_1_BOOKOO)); break;
305+
case ECLAIR: ok = _write.writeValue(TARE_ECLAIR, sizeof(TARE_ECLAIR)); break;
306+
default: ok = _write.writeValue(TARE_ACAIA, sizeof(TARE_ACAIA)); break;
307+
}
308+
if(ok){
309+
Serial.println("beep write successful");
310+
return true;
271311
}else{
272312
_connected = false;
273313
Serial.println("beep level write failed");
274314
return false;
275315
}
276316
}
277317

278-
bool AcaiaArduinoBLE::heartbeat(){
279-
if(_write.writeValue(HEARTBEAT, 7)){
318+
bool AcaiaArduinoBLE::heartbeat(){
319+
if(_type == ECLAIR){
320+
return true;
321+
}
322+
323+
if(_write.writeValue(HEARTBEAT, 7)){
280324
_lastHeartBeat = millis();
281325
return true;
282326
}else{
@@ -313,7 +357,7 @@ bool AcaiaArduinoBLE::newWeightAvailable(){
313357
int l = _read.valueLength();
314358

315359
// Get packet
316-
if(10 >= l || //10 byte packets for pre-2021 lunar
360+
if(10 >= l || //10 byte packets for pre-2021 lunar and Eclair
317361
(13 >= l && OLD != _type) || //13 byte packets for pyxis and older lunar 2021 fw
318362
(14 == l && OLD == _type) || //14 byte packets for lunar 2021 AL008
319363
(17 == l && NEW == _type) || //17 byte packets for newer lunar 2021 fw
@@ -363,19 +407,27 @@ bool AcaiaArduinoBLE::newWeightAvailable(){
363407
_currentWeight = _currentWeight / 100;
364408
newWeightPacket = true;
365409

366-
}else if( FELICITA == _type && l == 18){
367-
// Felicita Arc 18-byte ASCII packet (matches pyfelicita lib):
368-
// byte 2 = sign ('-'/0x2D = negative)
369-
// bytes 3..8 = six ASCII weight digits, value / 100 = grams
370-
_currentWeight = (input[2] == 0x2D ? -1 : 1)
371-
* ( (input[3] - 0x30) * 1000
372-
+ (input[4] - 0x30) * 100
373-
+ (input[5] - 0x30) * 10
374-
+ (input[6] - 0x30) * 1
375-
+ (input[7] - 0x30) * 0.1
376-
+ (input[8] - 0x30) * 0.01 );
377-
newWeightPacket = true;
378-
}
410+
}else if( FELICITA == _type && l == 18){
411+
// Felicita Arc 18-byte ASCII packet (matches pyfelicita lib):
412+
// byte 2 = sign ('-'/0x2D = negative)
413+
// bytes 3..8 = six ASCII weight digits, value / 100 = grams
414+
_currentWeight = (input[2] == 0x2D ? -1 : 1)
415+
* ( (input[3] - 0x30) * 1000
416+
+ (input[4] - 0x30) * 100
417+
+ (input[5] - 0x30) * 10
418+
+ (input[6] - 0x30) * 1
419+
+ (input[7] - 0x30) * 0.1
420+
+ (input[8] - 0x30) * 0.01 );
421+
newWeightPacket = true;
422+
423+
}else if( ECLAIR == _type && l == 10 && input[0] == 'W'){
424+
if(calculateXOR(input + 1, 8) == input[9]){
425+
_currentWeight = readInt32LittleEndian(input + 1) / 1000.0;
426+
newWeightPacket = true;
427+
}else if(_debug){
428+
Serial.println("Eclair packet XOR failed");
429+
}
430+
}
379431
if(newWeightPacket){
380432
if(_lastPacket){
381433
_packetPeriod = millis() - _lastPacket;
@@ -396,10 +448,11 @@ bool AcaiaArduinoBLE::isScaleName(String name){
396448
|| nameShort == "PYXIS"
397449
|| nameShort == "LUNAR"
398450
|| nameShort == "PEARL"
399-
|| nameShort == "PROCH"
400-
|| nameShort == "BOOKO"
401-
|| nameShort == "FELIC";
402-
}
451+
|| nameShort == "PROCH"
452+
|| nameShort == "BOOKO"
453+
|| nameShort == "FELIC"
454+
|| nameShort == "ECLAI";
455+
}
403456

404457
void AcaiaArduinoBLE::exploreService(BLEService service) {
405458
// print the UUID of the service

AcaiaArduinoBLE.h

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,30 @@
1313
#ifndef AcaiaArduinoBLE_h
1414
#define AcaiaArduinoBLE_h
1515

16-
#define LIBRARY_VERSION "3.3.0"
16+
#define LIBRARY_VERSION "3.4.0"
1717
#define WRITE_CHAR_OLD_VERSION "2a80"
1818
#define READ_CHAR_OLD_VERSION "2a80"
1919
#define WRITE_CHAR_NEW_VERSION "49535343-8841-43f4-a8d4-ecbe34729bb3"
2020
#define READ_CHAR_NEW_VERSION "49535343-1e4d-4bd9-ba61-23c647249616"
21-
#define WRITE_CHAR_GENERIC "ff12"
22-
#define READ_CHAR_GENERIC "ff11"
23-
#define WRITE_CHAR_FELICITA "ffe1" // Felicita Arc: single ffe1 char (service ffe0) for both
24-
#define READ_CHAR_FELICITA "ffe1" // notify (weight) and write (commands)
25-
#define HEARTBEAT_PERIOD_MS 2750
26-
#define MAX_PACKET_PERIOD_MS 5000
21+
#define WRITE_CHAR_GENERIC "ff12"
22+
#define READ_CHAR_GENERIC "ff11"
23+
#define WRITE_CHAR_FELICITA "ffe1" // Felicita Arc: single ffe1 char (service ffe0) for both
24+
#define READ_CHAR_FELICITA "ffe1" // notify (weight) and write (commands)
25+
#define WRITE_CHAR_ECLAIR "4F9A45BA-8E1B-4E07-E157-0814D393B968"
26+
#define READ_CHAR_ECLAIR "AD736C5F-BBC9-1F96-D304-CB5D5F41E160"
27+
#define HEARTBEAT_PERIOD_MS 2750
28+
#define MAX_PACKET_PERIOD_MS 5000
2729

2830
#include "Arduino.h"
2931
#include <ArduinoBLE.h>
3032

3133
enum scale_type{
32-
OLD, // Lunar (pre-2021)
33-
NEW, // Lunar (2021), Pyxis
34-
GENERIC, // BooKoo Themis, Decent, etc (ff11/ff12, binary protocol)
35-
FELICITA // Felicita Arc (ffe1, single-byte ASCII protocol)
36-
};
34+
OLD, // Lunar (pre-2021)
35+
NEW, // Lunar (2021), Pyxis
36+
GENERIC, // BooKoo Themis, Decent, etc (ff11/ff12, binary protocol)
37+
FELICITA, // Felicita Arc (ffe1, single-byte ASCII protocol)
38+
ECLAIR // AtomHeart Eclair (custom service/characteristics)
39+
};
3740

3841
class AcaiaArduinoBLE{
3942
public:

README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# AcaiaArduinoBLE
2-
Acaia / Bookoo / Felicita Scale Gateway using the ArduinoBLE library for esp32-based devices.
2+
Acaia / Bookoo / Felicita / AtomHeart Eclair Scale Gateway using the ArduinoBLE library for esp32-based devices.
33
This is an Arduino Library which can be found in the Arduino IDE Library Manager.
44

55
## Scale Compatibility
@@ -11,9 +11,10 @@ This is an Arduino Library which can be found in the Arduino IDE Library Manager
1111
| Acaia | Pearl S | USB-Micro | v1.0.056 | Ok | Yes | Yes | Yes
1212
| Acaia | Pearl S | USB-C | ---- | Ok | Yes | Yes | Yes
1313
| Acaia | Pyxis | ---- | v1.0.022 | Good | Not Recommended (too sensitive) | Yes | Yes
14-
| Bookoo | Themis Mini | ---- | v1.0.5 | Great | Yes | Yes | Yes
15-
| Bookoo | Themis Ultra | ---- | ---- | Great | Yes | Yes | Yes
16-
| Felicita | Arc | ---- | ---- | ---- | Yes | Yes | Yes
14+
| Bookoo | Themis Mini | ---- | v1.0.5 | Great | Yes | Yes | Yes
15+
| Bookoo | Themis Ultra | ---- | ---- | Great | Yes | Yes | Yes
16+
| Felicita | Arc | ---- | ---- | ---- | Yes | Yes | Yes
17+
| AtomHeart | Eclair | ---- | v2.1.0 | Testing | Yes | Yes | Yes
1718

1819

1920
## Requirements
@@ -81,9 +82,11 @@ You can find a demo on Youtube:
8182

8283
☑ Pearl S
8384

84-
☑ Felicita Arc
85-
86-
☑ Bookoo
85+
☑ Felicita Arc
86+
87+
☑ Bookoo
88+
89+
☑ AtomHeart Eclair
8790

8891

8992
## Bugs/Missing
@@ -97,7 +100,9 @@ In addition to some minor notes from [pyacaia](https://github.com/lucapinello/py
97100

98101
Felicita Arc support contributions from baettigp and A-TWJ
99102

100-
Bookoo contributions from philgood and same31
101-
102-
lunar 2019 contributions from jniebuhr
103+
Bookoo contributions from philgood and same31
104+
105+
AtomHeart Eclair contributions from AtomHeart-Lang
106+
107+
lunar 2019 contributions from jniebuhr
103108

0 commit comments

Comments
 (0)