I'm planning to do a big refactor to all scales inside the scales/ folder. My plan is to switch the hardcoded and forked logic for each scale implementation to use new C++ mixins. In order to reliably do that I need to know, for each scale, all the details that are common between them and all the smaller details that are very unique. For instance:
- are they all bluetooth
- do they use one uuid descriptor for weight or more than one
- same for command uuid (one or more)
- are they all using notifications (the class subscribes to the weight UUID, receives the payload and decodes the data, notifies the weight callback.
- do they contain warnings/miss used logic, bad patterns, unnecessary heap allocations, potential leaks
- do they need a heartbeat
Take your time analyzing each scale, write your findings in a REFACTOR.md file
Present both detailed info for each scale and a table with all the details, use a blank/green/yellow/red marker to note the scale does not use feature A, uses it, uses it but has warnings and errors
After you compiled all this data and written the REFACTOR.md file, create a list of all the mixins I might need. Make sure you cluster them by category. For instance one mixin category will be hearbeat. I would expect to have an empty one for scales that don't need a heartbeat but also one that only needs to call one or two bt commands or one that just needs a lambda for custom processing. Extract the rest of the details for the other categories.
Also extract info on which scales have binary protocols, which ones use add checksum vs xor, custom protocols, etc.
Ask questions if needed.
Scales analysed: Acaia Β· Bookoo Β· Decent Β· Difluid Β· Eclair Β· Eureka Β· Felicita Β· myscale Β· Timemore Β· Varia Β· WeighMyBru
Marker legend
Symbol Meaning βͺ Not applicable / feature not used π’ Correct implementation π‘ Used but has issues / warnings π΄ Has bugs or errors
Handles: ACAIA, PYXIS, LUNAR, PEARL, PROCH, UMBRA (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service (new) | 49535343-fe7d-4ae5-8fa9-9fafd205e455 |
| Service (old) | 00001820-0000-1000-8000-00805f9b34fb |
| Service (Umbra) | 0000fe40-cc7a-482a-984a-7f2ed5b3e58f |
| Weight char | 49535343-1e4d-4bd9-ba61-23c647249616 / old=00002a80 / Umbra=0000fe42 |
| Command char | 49535343-8841-43f4-a8d4-ecbe34729bb3 / old=same as weight / Umbra=0000fe41 |
Protocol:
[EF DD] [MsgType] [PayloadLen] [Payload...] [CkSum1] [CkSum2]
Custom 2-byte alternating checksum: even bytes β cksum1, odd bytes β cksum2. Full framed binary with header scanning (cleanupJunkData). Accumulating dataBuffer.
Handshake: Writes CCCD {0x01,0x00} manually β sendId() (15 0x2D bytes) β sendNotificationRequest() (EVENT with notification flags).
Heartbeat: Every 2000 ms β sends SYSTEM, sendNotificationRequest(), HANDSHAKE (3 writes).
Weight decoding: 2-byte LE integer + 1-byte scaling factor (Γ·10β¦Γ·10000). Sign bit in payload[5] bit 1. Umbra variant reads weight at bytes 2β3 instead of 0β1.
Issues:
sendEvent()andsendMessage()allocate heap on every call viastd::make_unique<uint8_t[]>. On a microcontroller this is frequent for a heartbeat every 2 s.- Old service uses the same characteristic for both weight and command β subscribed twice in
subscribeToNotifications()which is harmless but redundant. - Subscribes
commandCharacteristictoo (for old model parity), even on new models where it cannot notify. GuardcanNotify()prevents actual duplicate, but the code path is confusing. - Large commented-out blocks (timer, key, ACK event handling) left in production code.
PEARLSdevice name substring check is fragile (no trailing char check βPEARLSOMETHINGwould match).
Handles: BOOKOO_SC (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | 0FFE |
| Weight char | FF11 |
| Command char | FF12 |
Protocol: Fixed 20-byte binary packet. Product ID byte + message type byte + data fields + XOR checksum (last byte). Payload<N> template struct computes XOR checksum at compile time using constexpr. Clean struct layout with WeightPayload union.
Handshake: Sends eventNotification command. No CCCD write (NimBLE handles it through subscribe()).
Heartbeat: Every 2000 ms β 3 writes: cmdHeartbeat1, eventNotification, cmdHeartbeat2.
Extras exposed: BatteryExtra + TimerExtra (only scale exposing both).
Issues:
- π΄ Return value bug in
decodeAndHandleNotification: returnsdataBuffer.size() < expectedWeightLengthwhich evaluates totrue(keep looping) when the buffer is empty. The correct condition should be>= expectedWeightLength. Effect: after processing one packet and clearing the buffer, the while-loop callsdecodeAndHandleNotificationonce more (finds size=0 < 20 = true), then calls it again (same thing, returns false). Two extra no-op calls per packet instead of stopping. If two packets arrive in one notify, the second packet is silently dropped becausedataBuffer.erase(begin, end)clears the entire buffer (not just the consumed 20 bytes). - π΄ Entire buffer erased, not just consumed bytes:
dataBuffer.erase(dataBuffer.begin(), dataBuffer.end())on the error path AND on the normal path wipes all buffered data. Should usedataBuffer.begin() + expectedWeightLength. - π΄
decodeAndHandleNotificationcallsBookooScales::tare()when it receives aBookooMessageType::Systemmessage β this looks like a copy-paste artifact; it's unclear why a received SYSTEM packet should trigger an outbound tare command. - π‘
update()always logs "Heartbeat sent." even whensendHeartbeat()did nothing (interval not elapsed).
Handles: Decent Scale, EspressiScale (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | FFF0 |
| Read char | FFF4 |
| Write char | 36F5 |
Protocol: 7 or 10 bytes: [03] [CA or CE] [WeightH] [WeightL] [β¦] [XOR]. XOR checksum computed over all bytes, but skipped if the checksum byte is 0 (unusual). Weight is signed 16-bit big-endian Γ· 10. No accumulating buffer.
Handshake: Minimal β get service + characteristics. Then turnOnOLED(). No notification-enable command.
Heartbeat: Every 5000 ms (longest of all) β single command: {0x03, 0x0A, 0x03, 0xFF, 0xFF, 0x00, 0x0A}.
Special: OLED control on connect/disconnect (unique to this scale). Uses a verifyConnected() helper.
Issues:
- π΄
#include <iostream>β never used, pulls in host-side STL code on Arduino. - π‘
using namespace std;in the .cpp file β pollutes the namespace. - π‘ Empty
if (service != nullptr) {}block inperformConnectionHandshake()β no-op body. - π‘ Tare command comment says "should also send 01 as the last data byte" but the byte IS already present at position [5]. Stale comment.
- π‘
turnOffOLED()is called insidedisconnect()which guards withif (isConnected())β butclientCleanup()is called right after regardless; if BLE stack dropped the connection beforedisconnect()was called, theturnOffOLED()write is skipped silently. - π‘
verifyConnected()pattern is correct but the distinct reconnection path (usesconnect()which itself callsclientConnect()which callsclientCleanup()) means cleanup is done inside connect, not in the same pattern as other scales.
Handles: Microbalance, Mb (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service (primary) | 00EE |
| Service (fallback) | 00DD |
| Characteristic | AA01 (used for BOTH write AND notify) |
Protocol: [DF DF] [Func] [Cmd] [DataLen] [Dataβ¦] [Sum]. Additive checksum (sum of all bytes except last, mod 256). Big-endian signed 32-bit weight Γ· 10. No accumulating buffer.
Handshake: Subscribe β setUnitToGram() β enableAutoNotifications(). Two service UUIDs tried in order. Sends command to force gram unit on connect.
Heartbeat: Every 2000 ms β single command (Get Device Status: {0xDF,0xDF,0x03,0x05,0x00,checksum}).
Issues:
- π‘
notifyCallbacklogs each byte with a separatelog()call in a loop β extremely noisy and inefficient (one formatted log call per byte vs onebyteArrayToHexStringcall). - π‘ Battery level is read from heartbeat acknowledgment (
battery_capacitylocal variable innotifyCallback) but never stored or exposed β dead read. - π‘
markedForReconnection = trueis set insidesendHeartbeat()(when!isConnected()), butsendHeartbeat()is only called fromupdate()when NOT alreadymarkedForReconnection. This inconsistency means the reconnection detection is driven by the heartbeat timer rather than BLE events. Different pattern from all other scales. - π‘ Single characteristic used for both write and notify β valid but unusual. Commands are written with
waitResponse = truewhich blocks. - π‘
heartbeatCommandcomment shows hardcoded0xC6which is wrong (actual XOR =0xDE), but it gets overwritten bycalculateChecksumso it's harmless.
Handles: ECLAIR- (name prefix, note trailing dash)
UUIDs:
| Role | UUID |
|---|---|
| Service | B905EAEA-2E63-0E04-7582-7913F10D8F81 |
| Data char (weight/flow, notifies) | AD736C5F-BBC9-1F96-D304-CB5D5F41E160 |
| Config char (commands + battery acks, also notifies) | 4F9A45BA-8E1B-4E07-E157-0814D393B968 |
Protocol: [Type] [Dataβ¦] [XOR]. XOR computed over data bytes only (excluding type byte). Weight is signed 32-bit little-endian int Γ· 1000 (milligram precision). Battery from config notifications. Two separate notification channels.
Handshake: Minimal β get service + characteristics. Calls subscribeToNotifications() for both chars separately (one callback, dispatches by UUID).
Heartbeat: Every 2000 ms β sends TIMER_STATUS (0x43) with {0x00}. Effectively sends [43 00 00] (XOR of {0x00} = 0x00). This looks like a placeholder β sending a timer-status query as a keepalive is unusual and undocumented.
Issues:
- π‘ Heartbeat is
sendMessage(EclairMessageType::TIMER_STATUS, β¦)β confusing semantics; a real heartbeat command should be a no-op ping, not a timer status request. - π‘
batteryfield is tracked but not exposed viagetBattery()override. - π‘
update()does not usemarkedForReconnectionflag; it checks!isConnected()directly and callsconnect(). Sinceconnect()callsclientConnect()which callsclientCleanup()internally, this is functionally equivalent but inconsistent with most other scales. - π‘
sendMessage()usesstd::make_unique<uint8_t[]>for heap allocation on every send.
Handles: CFS-9002, LSJ-001 (name prefix) OR manufacturer data containing a6bc or starting with 042 (when name is empty)
UUIDs:
| Role | UUID |
|---|---|
| Service | FFF0 |
| Weight char | FFF1 |
| Command char | FFF2 |
β οΈ Same service/characteristic UUIDs as Varia β these scales must be disambiguated by name/manufacturer data at the detection stage.
Protocol: Fixed 11-byte binary. No header magic bytes, no checksum. Weight: 2-byte little-endian integer at bytes [7:8] Γ· 10. Sign byte at byte [6].
Handshake: Minimal β get service + characteristics. No CCCD write, no init command.
Heartbeat: sendHeartbeat() method exists but body is empty (comment: "No heartbeat necessary").
Issues:
- π‘
sendMessage()allocates heap viastd::make_unique<uint8_t[]>thenmemcpyβ a passthrough that could callcommandCharacteristic->writeValue(payload, length, waitResponse)directly. - π‘
productNumber = dataBuffer[0]is read but never used indecodeAndHandleNotification. - π‘ Tare payload:
uint8_t payload[6] = { CMD_HEADER, CMD_BASE, CMD_TARE, CMD_TARE }β array is 6 bytes but only 4 initialisers; trailing two bytes are implicitly0. Whether this is intentional is undocumented. - π‘ Subscribes
commandCharacteristicto notifications even though in practice it likely only exists for writing. - π‘ Manufacturer data detection:
NimBLEUtils::buildHexData(nullptr, β¦)returns achar*allocated by NimBLE β this pointer is never freed, causing a heap leak on every scan result. - βͺ No checksum validation on received packets.
Handles: FELICITA (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | FFE0 |
| Data char (read + write) | FFE1 |
Protocol: 18+ byte ASCII text packet. Weight encoded as ASCII digit characters at bytes [3..8]. Sign at byte [2] (0x2D = -). Weight Γ· 100.
Handshake: Minimal β get service + characteristic + subscribe. No init commands.
Heartbeat: None (field lastHeartbeat exists but is never read or written).
Issues:
- π΄
toggleUnit()andtogglePrecision()are declared in the header but never defined in the .cpp. If any code calls them, this is a linker error. - π΄
calculateChecksum()is defined but never called β dead code. - π‘
parseWeight()validation:(data[3] | β¦ | data[8]) < '0'ORs all six bytes and checks if result <0x30. This is onlytrueif all bytes are zero β a meaningless guard. Malformed ASCII will slip through. - π‘
lastHeartbeatmember declared but never used. - π‘
verifyConnected()is called inupdate()but only marks for reconnection β it never triggers reconnection itself within the same call. Works, but on embedded hardware the nextupdate()call handles the reconnect while this call silently returnsfalse.
Handles: blackcoffee, my_scale, MY_SCALE (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | 0000FFB0-0000-1000-8000-00805F9B34FB |
| Data char (notify) | 0000FFB2-0000-1000-8000-00805F9B34FB |
| Write char | 0000FFB1-0000-1000-8000-00805F9B34FB |
Protocol: 15+ byte binary packet. Weight uses bit manipulation: sign from nibble (data[2] >> 4), magnitude from bytes [3..6] with masking. Weight Γ· 1000 (milligram precision).
Handshake: Get service + characteristics. Writes CCCD {0x01, 0x00} manually AND calls subscribe() β the manual CCCD write is redundant since NimBLE's subscribe() handles it.
Heartbeat: None (field lastHeartbeat exists but never written or read).
Issues:
- π΄
toggleUnit()andtogglePrecision()declared but never defined (identical issue to Felicita β likely copy-paste). - π΄
calculateChecksum()defined but never called β dead code. - π‘ Class name is lowercase
myscaleβ inconsistent with all other scale class names. - π‘ Manual CCCD write is redundant (NimBLE
subscribe()writes it automatically). - π‘
lastHeartbeatmember declared but never used. - π‘ The tare command is a 20-byte blob with magic bytes; no protocol spec referenced.
Handles: Timemore Scale (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | 181D (standard BLE Weight Scale service) |
| Weight char | 2A9D (standard BLE Weight Measurement characteristic) |
| Command char | 553f4e49-bf21-4468-9c6c-0e4fb5b17697 |
Protocol: Fixed 9-byte binary. Type byte at [0] (0x10 = weight). Two 32-bit little-endian integers: dripper weight [1..4] and scale weight [5..8]. Scale weight Γ· 10.
Handshake: Writes CCCD {0x01, 0x00} manually (notifications). Sends sendNotificationRequest() ({0x02, 0x00} to weight char).
Heartbeat: Every 2000 ms β single write {0x00} to weight char via sendMessage(WEIGHT, β¦).
Issues:
- π΄ Dual subscription conflict:
performConnectionHandshake()writes CCCD descriptor manually for notifications ({0x01, 0x00}), butsubscribeToNotifications()then callscanIndicate()/subscribe(false, callback, true)which subscribes to indications. These are different subscription types. The result is that BOTH notifications AND indications may be enabled simultaneously, or one may override the other depending on NimBLE internals. - π‘ Dripper weight field (
dataBuffer[1..4]) is parsed but immediately commented out β dead variable. - π‘
performConnectionHandshake()logs "Got Service\n" twice (lines 122 and 127). - π‘
sendMessage()dispatches to different characteristics based onmsgTypeβ an unusual pattern where the enum value determines which BLE characteristic is the write target.
Handles: AKU MINI SCALE, VARIA AKU, Varia AKU, AKU SCALE (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | FFF0 |
| Weight char (notify) | FFF1 |
| Command char (write) | FFF2 |
β οΈ Same service/characteristic UUIDs as Eureka.
Protocol: Variable-length compound messages. Outer frame: [FA] [SubType] [Len] [Dataβ¦] [XOR]. A single BLE notification can contain multiple concatenated sub-messages (weight, timer, battery, timer events). Pointer arithmetic used to walk through them. XOR checksum validated per sub-message.
Handshake: Minimal β no init commands.
Heartbeat: None β update() has an empty body ({}).
Issues:
- π‘
update()is defined as empty in the header file (void update() override {}). This is valid C++ but unusual β behaviour cannot be changed without editing the header. - π‘
batteryPercentandtimerSecondsare tracked innotifyCallbackbut not exposed viagetBattery()/getTimer()overrides (Bookoo, by contrast, exposes these properly). - π‘
sendMessage()usesstd::make_unique<uint8_t[]>for a variable-length message β could use a stack-allocated buffer for the typical short command sizes. - π‘ No
markedForReconnectionflag and no reconnection logic anywhere.
Handles: WeighMyBru (name prefix)
UUIDs:
| Role | UUID |
|---|---|
| Service | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E (Nordic UART) |
| Weight char (notify) | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E |
| Command char | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E |
Protocol: Fixed 20-byte binary. Product ID [0] + message type [1] + data + XOR checksum (last byte). Sign byte at [6] (45 = '-'). Weight: 3 bytes at [7..9] Γ· 0.01.
Handshake: Writes CCCD, sends sendNotificationRequest() (which calls sendEvent()).
Heartbeat: Every 2000 ms β 3 writes (mirrors Acaia pattern): {0x02,0x00}, sendNotificationRequest(), {0x00}.
Issues:
- π΄ Wrong CCCD endianness: writes
{0x00, 0x01}to the CCCD descriptor. The BLE standard stores notification-enable as little-endian0x0001={0x01, 0x00}.{0x00, 0x01}=0x0100= reserved/undefined. Compare with Acaia which correctly writes{0x01, 0x00}. This may inadvertently enable indications on some BLE stacks. - π΄
decodeAndHandleNotification()callsWeighMyBrewScales::tare()whenmessageType == SYSTEMβ same copy-paste artifact as Bookoo. - π‘
update()always logs "Heartbeat sent." unconditionally. - π‘
sendEvent()andsendMessage()both allocate heap viastd::make_unique. - π‘ Subscribes
commandCharacteristicto notifications (NUS TX char) β this is the correct NUS pattern actually (RX notifies), but the naming (commandCharacteristic) is confusing: in NUS, 6E400002 is RX (you write to it) and 6E400003 is TX (it notifies you). The naming in this code is inverted vs. NUS convention. - π‘
batteryfield declared but never read from protocol or exposed.
| Feature | Acaia | Bookoo | Decent | Difluid | Eclair | Eureka | Felicita | myscale | Timemore | Varia | WeighMyBru |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Heartbeat | π’ 2000 ms | π‘ 2000 ms | π’ 5000 ms | π‘ 2000 ms | π‘ 2000 ms | βͺ (method empty) | βͺ none | βͺ none | π’ 2000 ms | βͺ none | π‘ 2000 ms |
| Heartbeat writes (per cycle) | 3 | 3 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 3 |
| Separate weight + cmd chars | π‘ (old=same) | π’ | π’ | βͺ (one char) | π’ (both notify) | π’ | βͺ (one char) | π’ | π’ | π’ | π’ |
| Multiple service UUIDs | π‘ (3 variants) | βͺ | βͺ | π‘ (2 variants) | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ |
| BLE Notifications | π’ | π’ | π’ | π’ | π’ | π’ | π’ | π’ | π΄ (uses indications) | π’ | π’ |
| Manual CCCD write | π’ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | π‘ (redundant) | π΄ (conflicts) | βͺ | π΄ (wrong endian) |
| Accumulating data buffer | π’ | π΄ (erase bug) | βͺ | βͺ | βͺ | π‘ (erase all) | βͺ | βͺ | π’ | βͺ | π’ |
| Protocol framing | π’ (EF DD header) | π’ (fixed 20B) | π’ (fixed 7/10B) | π’ (DF DF header) | π’ (type byte) | π‘ (fixed 11B, no cksum) | π‘ (ASCII, no cksum) | π‘ (binary, no cksum) | π’ (fixed 9B) | π’ (variable, framed) | π‘ (fixed 20B) |
| Inbound checksum | π’ 2-byte alt-sum | π’ XOR | π‘ XOR (opt-out if 0) | π’ additive | π’ XOR | βͺ none | βͺ none (fn unused) | βͺ none (fn unused) | βͺ none | π’ XOR | π’ XOR |
| Outbound checksum | π’ 2-byte alt-sum | π’ XOR (compile-time) | βͺ none | π’ additive | π’ XOR | βͺ none | βͺ none | βͺ none | βͺ none | π’ XOR | π’ XOR |
| Init command(s) in handshake | π’ sendId + notifReq | π’ notifReq | βͺ | π’ setUnit + enableNotif | βͺ | βͺ | βͺ | βͺ | π’ notifReq | βͺ | π’ notifReq |
| Reconnect pattern | π’ flag | π’ flag | π‘ flag+verifyConnected | π‘ flag (in heartbeat) | π‘ direct | π’ flag | π‘ flag+verifyConnected | π‘ flag+verifyConnected | π’ flag | βͺ none | π’ flag |
| Battery exposed | βͺ (tracked, not exposed) | π’ getBattery() |
βͺ | βͺ (dead read) | βͺ (tracked, not exposed) | βͺ | βͺ | βͺ | βͺ | βͺ (tracked, not exposed) | βͺ |
| Timer exposed | βͺ | π’ getTimer() |
βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ (tracked, not exposed) | βͺ |
| OLED / display control | βͺ | βͺ | π’ on connect/disconnect | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ | βͺ |
| Detection by manufacturer data | βͺ | βͺ | βͺ | βͺ | βͺ | π‘ (memory leak) | βͺ | βͺ | βͺ | βͺ | βͺ |
| Heap alloc per message send | π‘ make_unique | βͺ (direct write) | βͺ | βͺ | π‘ make_unique | π‘ make_unique (unnecessary) | βͺ | βͺ | βͺ | π‘ make_unique | π‘ make_unique |
| Dead code | π‘ large commented blocks | βͺ | βͺ | βͺ | βͺ | βͺ | π΄ undefined decls | π΄ undefined decls | π‘ dripper weight | βͺ | βͺ |
| Scale | Format | Encoding | Inbound cksum | Outbound cksum | Weight precision |
|---|---|---|---|---|---|
| Acaia | Framed binary (EF DD header, variable len) |
Custom scaling byte (Γ·10β¦Γ·10000) | 2-byte alternating sum | 2-byte alternating sum | 0.01 g (or 0.0001 g) |
| Bookoo | Fixed 20-byte binary | 3-byte unsigned packed Γ 0.01 | XOR (last byte) | XOR (compile-time) | 0.01 g |
| Decent | Fixed 7 or 10 bytes | Signed 16-bit BE Γ· 10 | XOR (optional) | None | 0.1 g |
| Difluid | Framed binary (DF DF header) |
Signed 32-bit BE Γ· 10 | Additive sum | Additive sum | 0.1 g |
| Eclair | Type-prefixed [T][Dβ¦][XOR] |
Signed 32-bit LE Γ· 1000 | XOR | XOR | 0.001 g |
| Eureka | Fixed 11-byte binary | Unsigned 16-bit LE Γ· 10 + sign byte | None | None | 0.1 g |
| Felicita | ASCII text β₯18 bytes | ASCII digits Γ 0.01 | None (fn unused) | None | 0.01 g |
| myscale | Binary β₯15 bytes | Bit-field extract Γ· 1000 | None (fn unused) | None | 0.001 g |
| Timemore | Fixed 9-byte binary | Signed 32-bit LE Γ· 10 | None | None | 0.1 g |
| Varia | Variable, compound messages (FA outer) |
3-byte packed Γ 0.01 + sign bit | XOR per sub-message | XOR | 0.01 g |
| WeighMyBru | Fixed 20-byte binary | 3-byte unsigned Γ 0.01 + sign byte | XOR (last byte) | XOR | 0.01 g |
Each category is a set of related behaviours. Within each category, there will be one concrete mixin per distinct behaviour variant. Scales with no need for a feature get a NoX or EmptyX mixin.
The update() call drives heartbeat. All variants track a lastHeartbeat timestamp and guard on an interval.
| Mixin | Interval | Writes | Used by |
|---|---|---|---|
NoHeartbeatMixin |
β | 0 | Varia, Felicita, myscale |
EmptyHeartbeatMixin |
β | 0 | Eureka (method stub) |
SingleCommandHeartbeatMixin<Interval> |
Configurable | 1 | Decent (5000), Difluid (2000), Timemore (2000) |
MultiCommandHeartbeatMixin<Interval> |
Configurable | 3 | Acaia (2000), Bookoo (2000), WeighMyBru (2000) |
The multi-command variant needs to know which commands to send β this is scale-specific and best supplied as a small lambda or a virtual doHeartbeat() that the concrete class overrides, while the mixin owns the timer logic.
How the scale subscribes to BLE characteristic notifications.
| Mixin | Behaviour | Used by |
|---|---|---|
DirectSubscribeMixin |
characteristic->subscribe(true, callback) via canNotify() |
Bookoo, Decent, Eclair, Eureka, Felicita, myscale, Varia, WeighMyBru |
ManualCCCDThenSubscribeMixin |
Write CCCD {0x01,0x00} + subscribe(true, β¦) |
Acaia (correct) |
IndicationSubscribeMixin |
canIndicate() + subscribe(false, callback, true) |
Timemore (but currently broken β see Β§1.9) |
ManualCCCDThenSubscribeMixin is only needed when NimBLE's subscribe() is not writing the CCCD automatically for some peripheral β all new scales should use DirectSubscribeMixin.
Commands sent immediately after BLE connection, before entering steady-state operation.
| Mixin | Behaviour | Used by |
|---|---|---|
MinimalHandshakeMixin |
Just discover service + characteristics | Eureka, Varia |
SubscribeHandshakeMixin |
Discover β subscribe (subscribe is done in handshake, not separately) | Felicita, myscale, Difluid |
NotificationRequestHandshakeMixin |
Discover β subscribe β send notification-enable command | Bookoo, Timemore, WeighMyBru |
IdentifyHandshakeMixin |
Discover β send ID bytes β send notification-enable command | Acaia |
UnitInitHandshakeMixin |
Discover β subscribe β set unit β enable auto-notifications | Difluid |
OLEDHandshakeMixin |
Wraps another mixin β calls turnOnOLED() post-connect, turnOffOLED() pre-disconnect |
Decent |
These can be composed. For example, Difluid is UnitInitHandshakeMixin + SubscribeHandshakeMixin.
How the scale detects and recovers from disconnection.
| Mixin | Behaviour | Used by |
|---|---|---|
FlaggedReconnectMixin |
Sets markedForReconnection = true in callback/heartbeat; update() checks flag and calls clientCleanup() + connect() |
Acaia, Bookoo, Decent, Difluid, Eureka, Felicita, myscale, Timemore, WeighMyBru |
DirectReconnectMixin |
update() checks !isConnected() directly; calls connect() (which internally calls clientCleanup()) |
Eclair |
NoReconnectMixin |
No reconnection logic | Varia |
How incoming BLE notification payloads are handled.
| Mixin | Behaviour | Used by |
|---|---|---|
SingleShotFramingMixin |
No buffer; parse each BLE notification payload as a complete, self-contained message | Decent, Difluid, Eclair, Felicita, myscale |
FixedLengthBufferMixin<N> |
Accumulate into a vector<uint8_t>; consume exactly N bytes per parse call |
Bookoo (20), WeighMyBru (20), Eureka (11), Timemore (9) |
HeaderScanBufferMixin<H1, H2> |
Accumulate; scan for 2-byte magic header to find packet start | Acaia (EF DD), Difluid (DF DF) |
CompoundMessageMixin |
Single notification may contain multiple back-to-back sub-messages; walk with pointer arithmetic | Varia |
FixedLengthBufferMixin must erase only the consumed bytes from the front, not the entire buffer.
Validation of incoming packet integrity.
| Mixin | Algorithm | Used by |
|---|---|---|
NoInboundChecksumMixin |
No validation | Eureka, Felicita, myscale, Timemore |
XorChecksumMixin |
XOR all payload bytes, compare to trailing byte | Decent, Eclair, Varia (per sub-msg), WeighMyBru |
ConditionalXorChecksumMixin |
Skip validation if checksum byte == 0 | Decent (special variant) |
AdditiveChecksumMixin |
Sum all bytes (mod 256), compare to trailing byte | Difluid |
AlternatingByteChecksumMixin |
Compute two bytes by summing even/odd-indexed bytes separately | Acaia |
StructXorChecksumMixin |
XOR all struct bytes, compare to embedded field | Bookoo (WeightPayload::valid()) |
Appended to commands sent to the scale.
| Mixin | Algorithm | Used by |
|---|---|---|
NoOutboundChecksumMixin |
No checksum appended | Decent, Felicita, myscale, Timemore, Eureka |
XorOutboundChecksumMixin |
XOR all payload bytes, append as last byte | Varia, WeighMyBru, Eclair |
CompileTimeXorPayloadMixin |
Payload<N> / CmdPayload β checksum computed by constexpr constructor at compile time |
Bookoo |
AdditiveOutboundChecksumMixin |
Sum all bytes (mod 256), append as last byte | Difluid |
AlternatingByteOutboundChecksumMixin |
Two-byte alternating sum appended | Acaia |
How raw bytes from the notification are turned into a float weight.
| Mixin | Encoding | Used by |
|---|---|---|
AsciiDecimalWeightMixin |
ASCII digit characters in packet β integer β Γ·100 | Felicita |
Int16LittleEndianWeightMixin<Divisor> |
Signed 16-bit LE integer with sign bit + divisor | Decent (Γ·10), Eureka (Γ·10, separate sign byte) |
Int32LittleEndianWeightMixin<Divisor> |
Signed 32-bit LE integer | Eclair (Γ·1000), Timemore (Γ·10) |
Int32BigEndianWeightMixin<Divisor> |
Signed 32-bit BE integer | Difluid (Γ·10) |
PackedByteWeightMixin<Divisor> |
3-byte unsigned packed + sign byte/bit | Bookoo (Γ0.01), WeighMyBru (Γ0.01), Varia (Γ0.01) |
BitFieldWeightMixin |
Nibble + byte extraction with sign from nibble | myscale |
ScalingFactorWeightMixin |
2-byte LE integer + scaling-factor byte (Γ·10^n) + sign bit | Acaia |
How the scale finds its BLE service and characteristic handles.
| Mixin | Behaviour | Used by |
|---|---|---|
SingleServiceMixin |
Look up one fixed service UUID | Bookoo, Decent, Eclair, Eureka, Felicita, myscale, Timemore, Varia, WeighMyBru |
FallbackServiceMixin |
Try a primary UUID, then fall back to alternates | Acaia (3 UUIDs), Difluid (2 UUIDs) |
SingleReadWriteCharMixin |
One characteristic used for both notify + write | Felicita, Difluid |
SplitCharMixin |
Separate characteristics for read (notify) and write | most scales |
DualNotifyCharMixin |
Two characteristics both notifying; callback dispatches by UUID | Eclair |
How the plugin's handles() method identifies a device.
| Mixin | Behaviour | Used by |
|---|---|---|
NamePrefixDetectionMixin<Prefixβ¦> |
deviceName.find(prefix) == 0 for one or more prefixes |
all except Eureka |
ManufacturerDataDetectionMixin |
Parse manufacturer data hex string | Eureka (fallback when name is empty) |
The Eureka memory leak (heap-allocated hex string never freed) belongs in this category's fix.
Scales that expose battery or timer state to callers.
| Mixin | Behaviour | Used by (correctly) | Should be added to |
|---|---|---|---|
BatteryExtraMixin |
Holds BatteryExtra, overrides getBattery() |
Bookoo | Eclair, Varia (track but don't expose), Difluid |
TimerExtraMixin |
Holds TimerExtra, overrides getTimer() |
Bookoo | Varia |
OLEDControlMixin |
turnOnOLED()/turnOffOLED() commands |
Decent | β |
| Issue | Affected scales | Fix |
|---|---|---|
Heap allocation on every BLE write (make_unique) |
Acaia, Eclair, Eureka, Varia, WeighMyBru | Use stack-allocated fixed buffers; only heap-allocate for truly variable sizes > ~32 bytes |
dataBuffer.erase(begin, end) erases entire buffer |
Bookoo, Eureka | Erase only begin, begin + messageLength |
| Inverted loop-continue condition | Bookoo | return dataBuffer.size() >= expectedWeightLength |
Calls tare() on received SYSTEM message |
Bookoo, WeighMyBru | Remove β likely copy-paste artefact |
Wrong CCCD endianness {0x00, 0x01} |
WeighMyBru | Change to {0x01, 0x00} |
| Dual subscription (CCCD write + indicate subscribe) | Timemore | Decide on notifications or indications; remove the other |
#include <iostream> + using namespace std |
Decent | Remove both |
| Declared but undefined methods | Felicita, myscale | Either implement toggleUnit()/togglePrecision() or remove declarations |
calculateChecksum defined but never called |
Felicita, myscale | Remove or wire up |
| Memory leak: manufacturer data hex string not freed | Eureka | Free via free(pHex) after use |
lastHeartbeat member declared but never used |
Felicita, myscale | Remove field |
| Battery/timer tracked internally but not exposed | Acaia, Eclair, Varia, Difluid | Add getBattery() / getTimer() overrides with proper extra types |
| Unconditional "Heartbeat sent." log in update | Bookoo, WeighMyBru | Move log inside sendHeartbeat() after the interval check |
| Heartbeat command type semantically wrong | Eclair | Use a proper keepalive command; check protocol docs |
markedForReconnection set inside sendHeartbeat() |
Difluid | Use BLE disconnect callback pattern instead |