From a29a8c3fbf5a4b842080c49acdbce0320af4229f Mon Sep 17 00:00:00 2001 From: banoz Date: Fri, 7 Nov 2025 23:42:03 -0600 Subject: [PATCH 1/2] ESP-IDF-ability --- src/remote_scales.cpp | 8 ++++++++ src/remote_scales.h | 5 ++++- src/scales/acaia.h | 1 - src/scales/bookoo.h | 1 - src/scales/eclair.h | 1 - src/scales/timemore.cpp | 2 +- src/scales/timemore.h | 1 - src/scales/weighmybru.cpp | 4 ++-- src/scales/weighmybru.h | 1 - 9 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/remote_scales.cpp b/src/remote_scales.cpp index 2a0bd39..7ffca36 100644 --- a/src/remote_scales.cpp +++ b/src/remote_scales.cpp @@ -79,6 +79,14 @@ std::string RemoteScales::byteArrayToHexString(const uint8_t* byteArray, size_t return hexString; } +uint32_t RemoteScales::millis() { +#ifdef ARDUINO + return ::millis(); +#elif defined(ESP_PLATFORM) + return esp_timer_get_time() / 1000; +#endif + return 0; +} // --------------------------------------------------------------------------------------- // ------------------------ RemoteScales methods ------------------------------ diff --git a/src/remote_scales.h b/src/remote_scales.h index d59145b..767d5a7 100644 --- a/src/remote_scales.h +++ b/src/remote_scales.h @@ -1,6 +1,8 @@ #pragma once +#ifdef ARDUINO + #include +#endif #include -#include #include #include #include @@ -52,6 +54,7 @@ class RemoteScales { void log(std::string msgFormat, ...); std::string byteArrayToHexString(const uint8_t* byteArray, size_t length); + uint32_t millis(); private: using WeightCallback = void (*)(float); diff --git a/src/scales/acaia.h b/src/scales/acaia.h index 47231c5..ed168d5 100644 --- a/src/scales/acaia.h +++ b/src/scales/acaia.h @@ -1,7 +1,6 @@ #pragma once #include "remote_scales.h" #include "remote_scales_plugin_registry.h" -#include #include #include #include diff --git a/src/scales/bookoo.h b/src/scales/bookoo.h index e357968..fc70a8e 100644 --- a/src/scales/bookoo.h +++ b/src/scales/bookoo.h @@ -1,7 +1,6 @@ #pragma once #include "remote_scales.h" #include "remote_scales_plugin_registry.h" -#include #include #include #include diff --git a/src/scales/eclair.h b/src/scales/eclair.h index 6e83909..36235dc 100644 --- a/src/scales/eclair.h +++ b/src/scales/eclair.h @@ -1,7 +1,6 @@ #pragma once #include "remote_scales.h" #include "remote_scales_plugin_registry.h" -#include #include #include #include diff --git a/src/scales/timemore.cpp b/src/scales/timemore.cpp index c48a523..a5155cd 100644 --- a/src/scales/timemore.cpp +++ b/src/scales/timemore.cpp @@ -97,7 +97,7 @@ bool TimemoreScales::decodeAndHandleNotification() { // E.g. 78 08 00 00 = 2168 / 10 = 216.8g //float_t dripperWeight = dataBuffer[1] | (dataBuffer[2] << 8) | (dataBuffer[3] << 16) | (dataBuffer[4] << 24); - float_t scaleWeight = dataBuffer[5] | (dataBuffer[6] << 8) | (dataBuffer[7] << 16) | (dataBuffer[8] << 24); + uint32_t scaleWeight = dataBuffer[5] | (dataBuffer[6] << 8) | (dataBuffer[7] << 16) | (dataBuffer[8] << 24); RemoteScales::setWeight(scaleWeight / 10.0f); // Convert to floating point } diff --git a/src/scales/timemore.h b/src/scales/timemore.h index b75bd38..d611069 100644 --- a/src/scales/timemore.h +++ b/src/scales/timemore.h @@ -1,7 +1,6 @@ #pragma once #include "remote_scales.h" #include "remote_scales_plugin_registry.h" -#include #include #include #include diff --git a/src/scales/weighmybru.cpp b/src/scales/weighmybru.cpp index 266b942..acb851e 100644 --- a/src/scales/weighmybru.cpp +++ b/src/scales/weighmybru.cpp @@ -170,7 +170,7 @@ bool WeighMyBrewScales::performConnectionHandshake() { sendNotificationRequest(); RemoteScales::log("Sent notification request\n"); - lastHeartbeat = millis(); + lastHeartbeat = RemoteScales::millis(); return true; } @@ -196,7 +196,7 @@ void WeighMyBrewScales::sendHeartbeat() { return; } - uint32_t now = millis(); + uint32_t now = RemoteScales::millis(); if (now - lastHeartbeat < 2000) { return; } diff --git a/src/scales/weighmybru.h b/src/scales/weighmybru.h index ebcacc4..9339ee2 100644 --- a/src/scales/weighmybru.h +++ b/src/scales/weighmybru.h @@ -1,7 +1,6 @@ #pragma once #include "remote_scales.h" #include "remote_scales_plugin_registry.h" -#include #include #include #include From 4a88138079ed1320840f5e883566b3106b3d1d74 Mon Sep 17 00:00:00 2001 From: banoz Date: Sat, 8 Nov 2025 00:57:02 -0600 Subject: [PATCH 2/2] tencent generic implementation --- src/scales/tencent.cpp | 134 +++++++++++++++++++++++++++++++++++++++++ src/scales/tencent.h | 62 +++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/scales/tencent.cpp create mode 100644 src/scales/tencent.h diff --git a/src/scales/tencent.cpp b/src/scales/tencent.cpp new file mode 100644 index 0000000..a59a1a5 --- /dev/null +++ b/src/scales/tencent.cpp @@ -0,0 +1,134 @@ +#include "tencent.h" +#include + +// Initialize UUID constants +const NimBLEUUID TencentScales::DATA_SERVICE_UUID("FFE0"); +const NimBLEUUID TencentScales::DATA_CHARACTERISTIC_UUID("FFE1"); + +TencentScales::TencentScales(const DiscoveredDevice& device) : RemoteScales(device) {} + +bool TencentScales::connect() { + if (isConnected()) { + log("Already connected.\n"); + return true; + } + + if (!clientConnect()) { + clientCleanup(); + return false; + } + + if (!performConnectionHandshake()) { + clientCleanup(); + return false; + } + setWeight(0.f); + return true; +} + +void TencentScales::disconnect() { + clientCleanup(); +} + +bool TencentScales::isConnected() { + return clientIsConnected(); +} + +void TencentScales::update() { + if (markedForReconnection) { + log("Reconnecting...\n"); + clientCleanup(); + connect(); + markedForReconnection = false; + } + else { + verifyConnected(); + } +} + +bool TencentScales::tare() { + if (!verifyConnected()) return false; + log("Tare command sent.\n"); + uint8_t tareCommand[] = { CMD_TARE }; + dataCharacteristic->writeValue(tareCommand, sizeof(tareCommand), true); + return true; +} + +bool TencentScales::performConnectionHandshake() { + log("Performing handshake...\n"); + + service = clientGetService(DATA_SERVICE_UUID); + if (!service) { + log("Service not found.\n"); + return false; + } + + dataCharacteristic = service->getCharacteristic(DATA_CHARACTERISTIC_UUID); + if (!dataCharacteristic) { + log("Characteristic not found.\n"); + return false; + } + + if (dataCharacteristic->canNotify()) { + dataCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic* characteristic, uint8_t* data, size_t length, bool isNotify) { + notifyCallback(characteristic, data, length, isNotify); + }); + } + else { + log("Notifications not supported.\n"); + return false; + } + + return true; +} +bool TencentScales::verifyConnected() { + if (markedForReconnection) { + return false; + } + if (!isConnected()) { + markedForReconnection = true; + return false; + } + return true; +} + +void TencentScales::notifyCallback(NimBLERemoteCharacteristic* characteristic, uint8_t* data, size_t length, bool isNotify) { + log("Notification received.\n"); + if (length < 11) { + log("Malformed data.\n"); + return; + } + parseStatusUpdate(data, length); +} + +void TencentScales::parseStatusUpdate(const uint8_t* data, size_t length) { + + if (data[0] == 0xA7 && data[1] == 0x00 && data[2] == 0x24) { + if (data[3] == 0x0C && data[4] == 0x13 && length >= 18) { + float weight = static_cast(parseWeight(data)); + setWeight(weight); + log("Weight updated: %.1f g\n", weight); + } + } +} + +float TencentScales::parseWeight(const uint8_t* data) { + + bool isNegative = (data[7] >> 4) & 0x01; + + uint32_t value = data[8] << 16 | data[9] << 8 | data[10]; + + if (isNegative) { + return -(value / 10.0f); + } + + return value / 10.f; +} + +uint8_t TencentScales::calculateChecksum(const uint8_t* data, size_t length) { + uint8_t checksum = 0; + for (size_t i = 0; i < length - 1; ++i) { + checksum += data[i]; + } + return checksum; +} diff --git a/src/scales/tencent.h b/src/scales/tencent.h new file mode 100644 index 0000000..04f7c45 --- /dev/null +++ b/src/scales/tencent.h @@ -0,0 +1,62 @@ +#pragma once +#include "remote_scales.h" +#include "remote_scales_plugin_registry.h" +#include + +class TencentScales : public RemoteScales { +public: + TencentScales(const DiscoveredDevice& device); + + bool tare() override; + bool isConnected() override; + bool connect() override; + void disconnect() override; + void update() override; + +private: + NimBLERemoteService* service = nullptr; + NimBLERemoteCharacteristic* dataCharacteristic = nullptr; + uint32_t lastHeartbeat = 0; + bool markedForReconnection = false; + + void notifyCallback(NimBLERemoteCharacteristic* characteristic, uint8_t* data, size_t length, bool isNotify); + bool performConnectionHandshake(); + void toggleUnit(); + void togglePrecision(); + bool verifyConnected(void); + void parseStatusUpdate(const uint8_t* data, size_t length); + uint8_t calculateChecksum(const uint8_t* data, size_t length); + float parseWeight(const uint8_t* data); + + // Constants specific to Tencent Scales + static const NimBLEUUID DATA_SERVICE_UUID; + static const NimBLEUUID DATA_CHARACTERISTIC_UUID; + static constexpr uint8_t CMD_TARE = 0x54; + static constexpr uint8_t CMD_TOGGLE_UNIT = 0x55; + static constexpr uint8_t CMD_TOGGLE_PRECISION = 0x44; +}; + + +class TencentScalesPlugin { +public: + static void apply() { + RemoteScalesPlugin plugin; + plugin.id = "plugin-tencent"; + plugin.handles = &TencentScalesPlugin::handles; + plugin.initialise = &TencentScalesPlugin::initialise; + RemoteScalesPluginRegistry::getInstance()->registerPlugin(plugin); + } + +private: + static bool handles(const DiscoveredDevice& device) { + const std::string& deviceName = device.getName(); + return !deviceName.empty() + && ( + deviceName.find("DSD TECH") == 0 + ); + } + + static std::unique_ptr initialise(const DiscoveredDevice& device) { + return std::make_unique(device); + } +};