├── ADS1232_ADC.cpp ├── ADS1232_ADC.h ├── ADS1232_ADC_CONFIG.h ├── Hardware └── README.md ├── LICENSE ├── README.md ├── Scale Case ├── README.md ├── half_decent_scale v9.step └── half_decent_scale_electrionics v8.step ├── ble.h ├── config.h ├── declare.h ├── display.h ├── espnow.h ├── gyro.h ├── hds.ino ├── menu.h ├── parameter.h ├── power.h ├── usbcomm.h ├── web_apps ├── Quality_Control_Assistant │ ├── README.md │ ├── qc.css │ ├── quality_control.html │ └── quality_control.js ├── README.md ├── Weigh_Save │ ├── README.md │ ├── main.js │ ├── modules │ │ ├── connections │ │ │ ├── base-connection.js │ │ │ ├── bluetooth-connection.js │ │ │ └── usb-connection.js │ │ ├── constants.js │ │ ├── debug-logger.js │ │ ├── dosing.js │ │ ├── export.js │ │ ├── led.js │ │ ├── presets.js │ │ ├── scale.js │ │ ├── state-machine.js │ │ ├── timer.js │ │ └── ui-controller.js │ ├── webweigh_output.css │ └── weigh_save.html └── dosing_assistant │ ├── README.md │ ├── dosing_assistant.html │ ├── dosing_assistant.md │ ├── main.js │ ├── modules │ ├── constants.js │ ├── dosing.js │ ├── export.js │ ├── preset.js │ ├── scale.js │ ├── state-machine.js │ └── ui-controller.js │ └── same_weight.css └── wifi_ota.h /ADS1232_ADC.h: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------------- 3 | ADS1232_ADC 4 | Arduino library for ADS1232 24-Bit Analog-to-Digital Converter for Weight Scales 5 | By Sofronio Chen July2024 6 | Based on HX711_ADC By Olav Kallhovd sept2017 7 | ------------------------------------------------------------------------------------- 8 | */ 9 | 10 | #ifndef ADS1232_ADC_h 11 | #define ADS1232_ADC_h 12 | 13 | #include 14 | #include "ADS1232_ADC_CONFIG.h" 15 | 16 | /* 17 | Note: ADS1232_ADC configuration values has been moved to file config.h 18 | */ 19 | 20 | #define DATA_SET SAMPLES + IGN_HIGH_SAMPLE + IGN_LOW_SAMPLE // total samples in memory 21 | 22 | #if (SAMPLES != 1) & (SAMPLES != 2) & (SAMPLES != 4) & (SAMPLES != 8) & (SAMPLES != 16) & (SAMPLES != 32) & (SAMPLES != 64) & (SAMPLES != 128) 23 | #error "number of SAMPLES not valid!" 24 | #endif 25 | 26 | #if (CHANNEL != 0) & (SAMPLES != 1) 27 | #error "CHANNEL number is not valid!" 28 | #endif 29 | 30 | #if (SAMPLES == 1) & ((IGN_HIGH_SAMPLE != 0) | (IGN_LOW_SAMPLE != 0)) 31 | #error "number of SAMPLES not valid!" 32 | #endif 33 | 34 | #if (SAMPLES == 1) 35 | #define DIVB 0 36 | #elif (SAMPLES == 2) 37 | #define DIVB 1 38 | #elif (SAMPLES == 4) 39 | #define DIVB 2 40 | #elif (SAMPLES == 8) 41 | #define DIVB 3 42 | #elif (SAMPLES == 16) 43 | #define DIVB 4 44 | #elif (SAMPLES == 32) 45 | #define DIVB 5 46 | #elif (SAMPLES == 64) 47 | #define DIVB 6 48 | #elif (SAMPLES == 128) 49 | #define DIVB 7 50 | #endif 51 | 52 | #define SIGNAL_TIMEOUT 100 53 | 54 | class ADS1232_ADC { 55 | 56 | public: 57 | ADS1232_ADC(uint8_t dout, uint8_t sck, uint8_t pwdn, uint8_t a0); //constructor 58 | ADS1232_ADC(uint8_t dout, uint8_t sck, uint8_t pwdn); //constructor 59 | void setGain(uint8_t gain = 128); //value must be 32, 64 or 128* 60 | void begin(); //set pinMode, ADS1232 gain and power up the ADS1232 61 | void begin(uint8_t gain); //set pinMode, ADS1232 selected gain and power up the ADS1232 62 | void start(unsigned long t); //start ADS1232 and do tare 63 | void start(unsigned long t, bool dotare); //start ADS1232, do tare if selected 64 | int startMultiple(unsigned long t); //start and do tare, multiple ADS1232 simultaniously 65 | int startMultiple(unsigned long t, bool dotare); //start and do tare if selected, multiple ADS1232 simultaniously 66 | void tare(); //zero the scale, wait for tare to finnish (blocking) 67 | void tareNoDelay(); //zero the scale, initiate the tare operation to run in the background (non-blocking) 68 | bool getTareStatus(); //returns 'true' if tareNoDelay() operation is complete 69 | void setCalFactor(float cal); //set new calibration factor, raw data is divided by this value to convert to readable data 70 | float getCalFactor(); //returns the current calibration factor 71 | float getData(); //returns data from the moving average dataset 72 | 73 | int getReadIndex(); //for testing and debugging 74 | float getConversionTime(); //for testing and debugging 75 | float getSPS(); //for testing and debugging 76 | bool getTareTimeoutFlag(); //for testing and debugging 77 | void disableTareTimeout(); //for testing and debugging 78 | long getSettlingTime(); //for testing and debugging 79 | void powerDown(); //power down the ADS1232 80 | void powerUp(); //power up the ADS1232 81 | long getTareOffset(); //get the tare offset (raw data value output without the scale "calFactor") 82 | void setTareOffset(long newoffset); //set new tare offset (raw data value input without the scale "calFactor") 83 | uint8_t update(); //if conversion is ready; read out 24 bit data and add to dataset 84 | bool dataWaitingAsync(); //checks if data is available to read (no conversion yet) 85 | bool updateAsync(); //read available data and add to dataset 86 | void setSamplesInUse(int samples); //overide number of samples in use 87 | int getSamplesInUse(); //returns current number of samples in use 88 | void resetSamplesIndex(); //resets index for dataset 89 | bool refreshDataSet(); //Fill the whole dataset up with new conversions, i.e. after a reset/restart (this function is blocking once started) 90 | bool getDataSetStatus(); //returns 'true' when the whole dataset has been filled up with conversions, i.e. after a reset/restart 91 | float getNewCalibration(float known_mass); //returns and sets a new calibration value (calFactor) based on a known mass input 92 | bool getSignalTimeoutFlag(); //returns 'true' if it takes longer time then 'SIGNAL_TIMEOUT' for the dout pin to go low after a new conversion is started 93 | void setReverseOutput(); //reverse the output value 94 | void setChannelInUse(int channel); //select channel from 0 or 1, channel 0 is default 95 | int getChannelInUse(); //returns current channel number 96 | 97 | protected: 98 | void conversion24bit(); //if conversion is ready: returns 24 bit data and starts the next conversion 99 | long smoothedData(); //returns the smoothed data value calculated from the dataset 100 | uint8_t sckPin; //ADS1232 pd_sck pin 101 | uint8_t doutPin; //ADS1232 dout pin 102 | uint8_t GAIN; //ADS1232 GAIN 103 | uint8_t pdwnPin; //ADS1232 PWDN for power down 104 | uint8_t a0Pin; //ADS1232 A0 for choose channel 105 | float calFactor = 1.0; //calibration factor as given in function setCalFactor(float cal) 106 | float calFactorRecip = 1.0; //reciprocal calibration factor (1/calFactor), the ADS1232 raw data is multiplied by this value 107 | volatile long dataSampleSet[DATA_SET + 1]; // dataset, make voltile if interrupt is used 108 | long tareOffset = 0; 109 | int readIndex = 0; 110 | unsigned long conversionStartTime = 0; 111 | unsigned long conversionTime = 0; 112 | uint8_t isFirst = 1; 113 | uint8_t tareTimes = 0; 114 | uint8_t divBit = DIVB; 115 | const uint8_t divBitCompiled = DIVB; 116 | bool doTare = 0; 117 | bool startStatus = 0; 118 | unsigned long startMultipleTimeStamp = 0; 119 | unsigned long startMultipleWaitTime = 0; 120 | uint8_t convRslt = 0; 121 | bool tareStatus = 0; 122 | unsigned int tareTimeOut = (SAMPLES + IGN_HIGH_SAMPLE + IGN_HIGH_SAMPLE) * 150; // tare timeout time in ms, no of samples * 150ms (10SPS + 50% margin) 123 | bool tareTimeoutFlag = 0; 124 | bool tareTimeoutDisable = 0; 125 | int samplesInUse = SAMPLES; 126 | int channelInUse = 0; 127 | long lastSmoothedData = 0; 128 | bool dataOutOfRange = 0; 129 | unsigned long lastDoutLowTime = 0; 130 | bool signalTimeoutFlag = 0; 131 | bool reverseVal = 0; 132 | bool dataWaiting = 0; 133 | }; 134 | 135 | #endif 136 | -------------------------------------------------------------------------------- /ADS1232_ADC_CONFIG.h: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------------- 3 | ADS1232_ADC 4 | Arduino library for ADS1232 24-Bit Analog-to-Digital Converter for Weight Scales 5 | By Sofronio Chen July2024 6 | Based on HX711_ADC By Olav Kallhovd sept2017 7 | ------------------------------------------------------------------------------------- 8 | */ 9 | 10 | /* 11 | ADS1232_ADC configuration 12 | 13 | Allowed values for "SAMPLES" is 1, 2, 4, 8, 16, 32, 64 or 128. 14 | Higher value = improved filtering/smoothing of returned value, but longer setteling time and increased memory usage 15 | Lower value = visa versa 16 | 17 | The settling time can be calculated as follows: 18 | Settling time = SAMPLES + IGN_HIGH_SAMPLE + IGN_LOW_SAMPLE / SPS 19 | 20 | Example on calculating settling time using the values SAMPLES = 16, IGN_HIGH_SAMPLE = 1, IGN_LOW_SAMPLE = 1, and HX711 sample rate set to 10SPS: 21 | (16+1+1)/10 = 1.8 seconds settling time. 22 | 23 | Note that you can also overide (reducing) the number of samples in use at any time with the function: setSamplesInUse(samples). 24 | 25 | */ 26 | 27 | #ifndef ADS1232_ADC_config_h 28 | #define ADS1232_ADC_config_h 29 | 30 | //number of samples in moving average dataset, value must be 1, 2, 4, 8, 16, 32, 64 or 128. 31 | #define SAMPLES 2 //default value: 16 32 | 33 | //adds extra sample(s) to the dataset and ignore peak high/low sample, value must be 0 or 1. 34 | #define IGN_HIGH_SAMPLE 1 //default value: 1 35 | #define IGN_LOW_SAMPLE 1 //default value: 1 36 | 37 | //microsecond delay after writing sck pin high or low. This delay could be required for faster mcu's. 38 | //So far the only mcu reported to need this delay is the ESP32 (issue #35), both the Arduino Due and ESP8266 seems to run fine without it. 39 | //Change the value to '1' to enable the delay. 40 | #define SCK_DELAY 0 //default value: 0 41 | 42 | //if you have some other time consuming (>60μs) interrupt routines that trigger while the sck pin is high, this could unintentionally set the ADS1232 into "power down" mode 43 | //if required you can change the value to '1' to disable interrupts when writing to the sck pin. 44 | #define SCK_DISABLE_INTERRUPTS 0 //default value: 0 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /Hardware/README.md: -------------------------------------------------------------------------------- 1 | # Bill of Materials (BOM) for Decent Open Scale. 2 | 3 | | Part Number | Part Name | Description | Quantity | Vendor/Source | Notes | 4 | |-------------|--------------|---------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| 5 | | 1 | ESP32 V1.0.0 | Main control unit | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8f7c3d&id=628078369041&spm=a1z09.2.0.0.10072e8dCAfwWX) | This particular one has battery support, which I assume it has a TP4054 on it. Two versions are available: MicroUSB and Type-C. | 6 | | 2 | Buzzer 9650 | Make sound | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8fd3d7&id=634342562820&spm=a1z09.2.0.0.10072e8dCAfwWX) | Use a powered one for ease of use, but a powerless one can change the tone. | 7 | | 3 | SSD1306 SH1116| OLED display | 1 | [Taobao](https://item.taobao.com/item.htm?_u=7nmg8f9838&id=677128362825&spm=a1z09.2.0.0.43f22e8dkkGbTz&skuId=5426714191529) | SH1116 is a bit cheaper, should add a line: #define SH1116. | 8 | | 4 | MPU6050 | 6-axle sensor | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8f529e&id=609979451344&spm=a1z09.2.0.0.10072e8dCAfwWX) | For side and bottom up power off, and avoid accidental power on. | 9 | | 5 | HX711 | ADC | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8f6256&id=611734839533&spm=a1z09.2.0.0.10072e8dCAfwWX) | The red one has a metal shield, I don't know if it's good, I just didn't see the difference. | 10 | | 6 | Loadcell | i2000 | 1 | [1688](https://detail.1688.com/offer/671859449208.html?spm=a26352.13672862.offerlist.60.318a40e0k6dDDa) | Bigger ones are more stable, but we have to choose the thinner ones. | 11 | | 7 | Female USB-c | Charging and firmware update | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8fa987&id=721229692231&spm=a1z09.2.0.0.10072e8dCAfwWX) | | 12 | | 8 | Male USB-c | Connect to MCU | 1 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8fe973&id=609433305908&spm=a1z09.2.0.0.10072e8dCAfwWX) | | 13 | | 9 | 503450 Battery| | 1 | [1688](https://detail.1688.com/offer/741144487402.html?spm=a360q.8274423.0.0.7b854c9ayJtmcd) | With protect board and 2 wires out. | 14 | | 10 | 100K resistor| 1/4w | 2 | [Tmall](https://detail.tmall.com/item.htm?_u=pnmg8fed34&id=13302997879&spm=a1z09.2.0.0.10072e8dCAfwWX&skuId=3756188445710) | Voltage divider for ESP32 3.3v max ADC reading the battery level. | 15 | | 11 | TTP223 | Touch button | 2 | [Taobao](https://item.taobao.com/item.htm?_u=pnmg8f71e5&id=611366998227&spm=a1z09.2.0.0.10072e8dCAfwWX) | Link point A to have logic low output. | 16 | # Wiring 17 | ![Wiring](https://github.com/decentespresso/openscale/assets/11464550/76d51924-c86f-4896-b179-38d1a397d9e2) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decent Open Scale (aka "openscale") 2 | Decent Open Scale 3 | Copyright 2024 Decent Espresso International Ltd 4 | 5 | Credits: 6 | Invention and authorship: Chen Zhichao (aka "Sofronio") 7 | 8 | # Introduction: 9 | The Decent Open Scale is a full open sourced(software/hardware/cad design) BLE scale. Currently you can use it with de1app and Decent Espresso machine. But with Decent Scale API, you can use it for anything.
10 | To make it work, you need at least an ESP32 for MCU, a Loadcell for weighing, an HX711 for Loadcell ADC, a SSD1306 for OLED display, a MPU6050 for gyro function.
11 | The current PCB for HDS uses ESP32S3N16, ADS1232(instead of HX711), SH1106/SH1116(instead of SSD1306), ADS1115(for battery voltage), TP4056(battery charging), CH340K(COM to USB), REF5025(analog VDD), BMA400(instead of MPU6050, but gyro will be dropped in the future). 12 | If you want to use it unplugged, you'll also need a 3.7v battery.
13 | If you only want to burn the firmware, please read How to upload HEX file.
14 | 15 | # Library needed: 16 | AceButton https://github.com/bxparks/AceButton
17 | Stopwatch_RT https://github.com/RobTillaart/Stopwatch_RT
18 | HX711_ADC https://github.com/olkal/HX711_ADC
19 | u8g2 https://github.com/olikraus/u8g2
20 | Adafruit_MPU6050 https://github.com/adafruit/Adafruit_MPU6050
21 | Adafruit_ADS1X15 https://github.com/adafruit/Adafruit_ADS1X15
22 | SparkFun_BMA400_Arduino_Library https://github.com/sparkfun/SparkFun_BMA400_Arduino_Library
23 | 24 | # Compile Guide: 25 | In Arduino IDE, selete tool menu, and change: 26 | - Board to "ESP32S3 Dev Module"
27 | - CPU Frequency to "80MHz (WiFi)"
28 | - Flash Size to "16MB (128Mb)"
29 | - Partition Scheme to "16MB Flash (3MB APP/9.9MB FATFS)"
30 | 31 | # How to upload HEX file 32 | Web USB Flash(please use Chrome/Edge, Safari or Firefox is not supported):
33 | https://adafruit.github.io/Adafruit_WebSerial_ESPTool/
34 | The offset values are:
35 | hds.bootloader.bin 0x0000
36 | hds.ino.partitions.bin 0x8000
37 | hds.ino.bin 0x10000
38 | This tool works great, but need to reset by pressing the button on the PCB.
39 | And as it erase the eprom, a calibration is also required.
40 | 41 | Or use OpenJumper™ Serial Assistant, link as below.(In Chinese)
42 | https://www.cnblogs.com/wind-under-the-wing/p/14686625.html
-------------------------------------------------------------------------------- /Scale Case/README.md: -------------------------------------------------------------------------------- 1 | # Print Step Files Guide 2 | 3 | This guide provides detailed instructions for printing components from STEP files. Ensure you follow the recommendations for materials and slicer software to achieve the best results for your prints. 4 | 5 | ## Prerequisites 6 | 7 | Before starting, ensure you have the following software installed on your computer: 8 | 9 | - [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) *(Link to download Orca Slicer)* 10 | - [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer) *(Link to download PrusaSlicer)* 11 | - [BambuStudio](https://github.com/bambulab/BambuStudio) *(Link to download Bambu Studio)* 12 | 13 | ## Instructions 14 | 15 | ### Opening STEP Files 16 | 17 | 1. **Choose a Slicer:** Start by opening either Orca Slicer, PrusaSlicer, or Bambu Studio on your computer. 18 | 2. **Import STEP File:** Locate the option to import or open a file in the slicer software. Select the STEP file you wish to print. 19 | 20 | ### Material Selection 21 | 22 | Different parts of the model require different materials for optimal results: 23 | 24 | - **TPU:** Print the battery strap, rubber feet, and top pad with TPU for flexibility and durability. 25 | - **ABS:** The top casing and bottom casing should be printed with ABS to ensure structural integrity and heat resistance. 26 | 27 | #### Important Notes on Materials: 28 | - **PETG:** PETG may be suitable for some components, offering a good balance between flexibility and strength. Test individual parts for suitability. 29 | - **PLA:** Avoid using PLA, as it may not provide the necessary durability or heat resistance for the components. 30 | 31 | ### Multi-color Material printing 32 | - If you have Bambulab AMS or Prusa MMU, then split the step file into components, you'll find the two buttons. 33 | - If you don't have those, you can just delete the two buttons, resulting still good looking bridging buttons. 34 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | // BLE UUIDs 5 | 6 | #define CUUID_DECENTSCALE_READ "0000FFF4-0000-1000-8000-00805F9B34FB" 7 | #define CUUID_DECENTSCALE_WRITE "000036F5-0000-1000-8000-00805F9B34FB" 8 | #define CUUID_DECENTSCALE_WRITEBACK "83CDC3D4-3BA2-13FC-CC5E-106C351A9352" 9 | #define SUUID_DECENTSCALE "0000FFF0-0000-1000-8000-00805F9B34FB" 10 | 11 | //#define ESPNOW 12 | 13 | // #define SW_SPI //HW_I2C HW_SPI SW_I2C SW_SPI //oled linkage 14 | // #define SH1106 15 | #define V7_5 16 | #define WIFIOTA 17 | //#define CAL 18 | 19 | //#define SSD1306 20 | //#define OLD_PIN //use old pins on hx711, buzzer. Notice: It'll cover FIVE_BUTTON pin 21 | 22 | #define TWO_BUTTON 23 | 24 | //POWER DISPLAY 25 | #define SHOWBATTERY 26 | //#define CHECKBATTERY 27 | 28 | //SCALE CONFIG 29 | #define LINE1 (char*)"FW: 2.7.1" 30 | #define LINE2 (char*)"Built-date(YYYYMMDD): 20250519" 31 | #define LINE3 (char*)"S/N: HDS001" //Serial number 32 | #define VERSION /*version*/ LINE1, /*compile date*/ LINE2, /*sn*/ LINE3 33 | //About info 34 | #define FIRMWARE_VER LINE1 35 | //#define WELCOME1 (char*)"Lian" 36 | #define WELCOME1 (char*)"Half Decent" 37 | #define WELCOME2 (char*)"w2" 38 | #define WELCOME3 (char*)"w3" 39 | #define WELCOME WELCOME1, FONT_EXTRACTION 40 | #define BUZZER_DURATION 5 41 | 42 | #define PositiveTolerance 25 // positive tolerance range in grams 43 | #define NegativeTolerance 5 // negative tolerance range in grams 44 | #define OledTolerance 0.09 45 | 46 | //ntc 47 | //#define THERMISTOR_PIN 39 48 | #define SERIESRESISTOR 10000 49 | #define NOMINAL_RESISTANCE 10000 50 | #define NOMINAL_TEMPERATURE 25 51 | #define BCOEFFICIENT 3950 52 | #define FILTER_CONSTANT 0.1 53 | 54 | //#define CAL //both button down during welcome text, start calibration 55 | #define BT 56 | 57 | //ADC BIT DEPTH 58 | #define ADC_BIT 12 59 | 60 | //BUTTON 61 | 62 | #define ACEBUTTON //ACEBUTTON ACEBUTTONT 63 | #define DEBOUNCE 200 64 | #define LONGCLICK 1500 65 | #define DOUBLECLICK 800 66 | #define CLICK 400 67 | #define BUTTON_KEY_DELAY 150 68 | 69 | //DISPLAY 70 | #define Margin_Top 0 //显示边框 71 | #define Margin_Bottom 0 72 | #define Margin_Left 0 73 | #define Margin_Right 0 74 | 75 | //ESP32S3 76 | 77 | #ifdef ESP32 78 | 79 | 80 | #ifdef V8_0 81 | #define PCB_VER (char*)"PCB: 8.0.0" 82 | #define HW_SPI 83 | #define SH1106 84 | #define ADS1232ADC 85 | #define ADS1115ADC 86 | #define ACC_BMA400 87 | #define ROTATION_180 88 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 89 | 90 | #define I2C_SCL 4 91 | #define I2C_SDA 5 92 | #define BATTERY_PIN 6 //wasn't used but to keep getVoltage(battery_pin) working. Any number is good for that. 93 | #define OLED_SDIN 7 94 | #define OLED_SCLK 15 95 | #define OLED_DC 16 96 | #define OLED_RST 17 97 | #define OLED_CS 18 98 | #define USB_DET 8 99 | #define PWR_CTRL 3 100 | //#define NTC 9 101 | #define BATTERY_CHARGING 10 102 | #define SCALE_DOUT 11 103 | #define SCALE_SCLK 12 104 | #define SCALE_PDWN 13 105 | #define ACC_PWR_CTRL 14 106 | #define ACC_INT 21 107 | #define SCALE2_DOUT 47 108 | #define SCALE2_SCLK 48 109 | #define SCALE2_PDWN 9 110 | #define BUTTON_CIRCLE 1 //33 111 | #define BUTTON_SQUARE 2 112 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 113 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_1 114 | #endif 115 | #define BUZZER 38 116 | 117 | #define SCALE_A0 -1 118 | #define HX711_SCL 12 119 | #define HX711_SDA 11 120 | #endif 121 | 122 | 123 | #ifdef V7_5 124 | #define PCB_VER (char*)"PCB: 7.5" 125 | #define HW_SPI 126 | #define SH1106 127 | #define ADS1232ADC 128 | #define ACC_MPU6050 129 | #define ROTATION_180 130 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 131 | 132 | #define I2C_SCL 4 133 | #define I2C_SDA 5 134 | #define BATTERY_PIN 6 135 | #define OLED_SDIN 7 136 | #define OLED_SCLK 15 137 | #define OLED_DC 16 138 | #define OLED_RST 17 139 | #define OLED_CS 18 140 | #define USB_DET 8 141 | #define PWR_CTRL 3 142 | #define NTC 9 143 | #define BATTERY_CHARGING 10 144 | #define SCALE_DOUT 11 145 | #define SCALE_SCLK 12 146 | #define SCALE_PDWN 13 147 | #define ACC_PWR_CTRL 14 148 | #define ACC_INT 21 149 | #define SCALE2_DOUT 47 150 | #define SCALE2_SCLK 48 151 | #define SCALE2_PDWN 39 152 | #define BUTTON_CIRCLE 1 //33 153 | #define BUTTON_SQUARE 2 154 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 155 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_1 156 | #endif 157 | #define BUZZER 38 158 | 159 | #define SCALE_A0 -1 160 | #define HX711_SCL 12 161 | #define HX711_SDA 11 162 | #endif 163 | 164 | 165 | #ifdef V7_4 166 | #define HW_SPI 167 | #define SH1106 168 | #define ADS1232ADC 169 | #define ROTATION_180 170 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 171 | 172 | #define I2C_SCL 4 173 | #define I2C_SDA 5 174 | #define BATTERY_CHARGING 6 175 | #define BATTERY_PIN 7 176 | #define OLED_DC 15 177 | #define OLED_RST 16 178 | #define USB_DET 17 179 | #define SCALE_PDWN 18 180 | #define SCALE_DOUT 8 181 | #define PWR_CTRL 3 182 | #define SCALE_SCLK 9 183 | #define OLED_CS 10 184 | #define OLED_SDIN 11 185 | #define OLED_SCLK 12 186 | #define ACC_PWR_CTRL 13 187 | #define NTC 14 188 | #define ACC_INT 21 189 | #define SCALE2_DOUT 47 190 | #define SCALE2_SCLK 48 191 | #define SCALE2_PDWN 35 192 | #define BUTTON_CIRCLE 1 //33 193 | #define BUTTON_SQUARE 2 194 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 195 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_1 196 | #endif 197 | #define BUZZER 38 198 | 199 | #define SCALE_A0 -1 200 | #define HX711_SCL 9 201 | #define HX711_SDA 8 202 | #endif 203 | 204 | 205 | #ifdef V7_3 206 | #define HW_SPI 207 | #define SH1106 208 | #define ADS1232ADC 209 | #define ROTATION_180 210 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 211 | 212 | #define I2C_SCL 4 213 | #define I2C_SDA 5 214 | #define BATTERY_CHARGING 6 215 | #define BATTERY_PIN 7 216 | #define OLED_DC 15 217 | #define OLED_RST 16 218 | #define SCALE_PDWN 18 219 | #define SCALE2_DOUT 8 220 | #define PWR_CTRL 3 221 | #define SCALE2_SCLK 9 222 | #define OLED_CS 10 223 | #define OLED_SDIN 11 224 | #define OLED_SCLK 12 225 | #define ACC_PWR_CTRL 13 226 | #define NTC 14 227 | #define MPU_INT 21 228 | #define SCALE_DOUT 47 229 | #define SCALE_SCLK 48 230 | #define BUTTON_CIRCLE 1 //33 231 | #define BUTTON_SQUARE 2 232 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 233 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_1 234 | #endif 235 | #define BUZZER 38 236 | 237 | #define SCALE_A0 -1 238 | #define BUZZER_LED 38 239 | #define HX711_SCL 9 240 | #define HX711_SDA 8 241 | #endif 242 | 243 | #ifdef V7_2 244 | #define PCB_VER (char*)"PCB: 7.2" 245 | #define HW_SPI 246 | #define SH1106 247 | #define ACC_MPU6050 248 | #define ADS1232ADC 249 | #define ROTATION_180 250 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 251 | #define PWR_CTRL 3 252 | #define OLED_CS 10 253 | #define OLED_DC 15 254 | #define OLED_RST 16 255 | #define OLED_SDIN 11 256 | #define OLED_SCLK 12 257 | #define SCALE_DOUT 8 258 | #define SCALE_SCLK 9 259 | #define SCALE_PDWN 13 260 | #define SCALE_A0 -1 261 | #define I2C_SCL 4 262 | #define I2C_SDA 5 263 | #define ROTATION_180 264 | 265 | #define BATTERY_PIN 7 266 | #define BATTERY_CHARGING 6 267 | #define BUTTON_CIRCLE 1 //33 268 | #define BUTTON_SQUARE 2 269 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 270 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_1 271 | #endif 272 | 273 | #define BUZZER 38 274 | #define BUZZER_LED 38 275 | #define HX711_SCL 9 276 | #define HX711_SDA 8 277 | #endif 278 | 279 | #ifdef V6 280 | #define HW_SPI 281 | #define SH1106 282 | #define ADS1232ADC 283 | #define ROTATION_180 284 | #define GYROFACEDOWN //GYRO //#define GYROFACEUP 285 | #define OLED_CS 10 286 | #define OLED_DC 15 287 | #define OLED_RST 16 288 | #define OLED_SDIN 13 289 | #define OLED_SCLK 12 290 | #define SCALE_DOUT 8 291 | #define SCALE_SCLK 9 292 | #define SCALE_PDWN 11 293 | #define SCALE_A0 -1 294 | #define I2C_SCL 4 295 | #define I2C_SDA 5 296 | #define ROTATION_180 297 | 298 | #define BATTERY_PIN 7 299 | #define BUTTON_CIRCLE 1 //33 300 | #define BUTTON_SQUARE 2 301 | #if defined(TWO_BUTTON) || defined(FOUR_BUTTON) 302 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_2 303 | #endif 304 | 305 | #define BUZZER 38 306 | #define BUZZER_LED 38 307 | #define HX711_SCL 9 308 | #define HX711_SDA 8 309 | #endif 310 | 311 | #ifdef V5 312 | #define SW_SPI 313 | #define SH1116 314 | #define ADS1232ADC 315 | #define ROTATION_180 316 | #define GYROFACEDOWN 317 | #define OLED_CS 8 318 | #define OLED_DC 15 319 | #define OLED_RST 16 320 | #define OLED_SDIN 6 321 | #define OLED_SCLK 7 322 | #define SCALE_DOUT 9 323 | #define SCALE_SCLK 10 324 | #define SCALE_PDWN 11 325 | #define SCALE_A0 -1 326 | #define I2C_SCL 4 327 | #define I2C_SDA 5 328 | #define ROTATION_180 329 | 330 | #define BATTERY_PIN 3 331 | #define USB_PIN 12 332 | #define BUTTON_SQUARE 1 //33 333 | #define BUTTON_CIRCLE 2 334 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_2 335 | 336 | 337 | #define BUZZER 13 338 | #define BUZZER_LED 13 339 | #define HX711_SCL 18 340 | #define HX711_SDA 5 341 | 342 | #endif 343 | 344 | #ifdef V4 345 | //v4 pin 346 | #define BATTERY_PIN 14 347 | //#define USB_PIN 35 348 | #define PIN_CHRG 48 349 | #define PIN_STDBY 47 350 | #define BUTTON_CIRCLE 2 //(v3.0 39, changed to 13 for rtc)33 351 | #define BUTTON_SQUARE 1 352 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_2 353 | 354 | 355 | #define BUZZER 38 356 | #define BUZZER_LED 38 //set to different for only led indicating 357 | #define I2C_SCL 4 358 | #define I2C_SDA 5 359 | #define OLED_CS 10 360 | #define OLED_DC 21 361 | #define OLED_RST 13 362 | #define OLED_SDIN 11 363 | #define OLED_SCLK 12 364 | #define SCALE_DOUT 6 365 | #define SCALE_SCLK 7 366 | #define SCALE_PDWN 15 367 | #define SCALE_TEMP 16 368 | #define SCALE_A0 8 369 | #define HX711_SCL 3 370 | #define HX711_SDA 9 371 | #endif 372 | 373 | #ifdef V3 374 | #define BATTERY_PIN 1 375 | //#define USB_PIN 35 376 | #define PIN_CHRG 42 377 | #define PIN_STDBY 41 378 | #define BUTTON_SET 2 //(v3.0 39, changed to 13 for rtc)33 379 | #define BUTTON_TARE 38 380 | #define GPIO_NUM_BUTTON_POWER GPIO_NUM_2 381 | 382 | 383 | #define BUZZER 15 384 | #define BUZZER_LED 15 //set to different for only led indicating 385 | #define I2C_SCL 9 386 | #define I2C_SDA 8 387 | #define OLED_CS 10 388 | #define OLED_DC 21 389 | #define OLED_RST 14 390 | // #define HX711_SCL 18 391 | // #define HX711_SDA 5 392 | #define SCALE_DOUT 45 393 | #define SCALE_SCLK 48 394 | #define SCALE_PDWN 47 395 | #define HX711_SCL SCALE_SCLK 396 | #define HX711_SDA SCALE_DOUT 397 | //#define OLED_RST 17 398 | 399 | #endif 400 | 401 | #endif 402 | #endif -------------------------------------------------------------------------------- /declare.h: -------------------------------------------------------------------------------- 1 | #ifndef DECLARE_H 2 | #define DECLARE_H 3 | #include "config.h" 4 | 5 | #ifdef HX711ADC 6 | #include 7 | HX711_ADC scale(HX711_SDA, HX711_SCL); //HX711模数转换初始化 8 | #endif 9 | #ifdef ADS1232ADC 10 | #include "ADS1232_ADC.h" 11 | ADS1232_ADC scale(SCALE_DOUT, SCALE_SCLK, SCALE_PDWN, SCALE_A0); 12 | #endif 13 | 14 | #include 15 | #include 16 | // #include 17 | // BluetoothSerial SerialBT; 18 | #include 19 | #include 20 | #include 21 | #include 22 | BLEServer *pServer = NULL; 23 | BLEService *pService = NULL; 24 | BLEAdvertising *pAdvertising = NULL; 25 | BLECharacteristic *pReadCharacteristic = NULL; 26 | BLECharacteristic *pWriteCharacteristic = NULL; 27 | bool deviceConnected = false; 28 | 29 | // The model byte is always 03 for Decent scales 30 | const byte modelByte = 0x03; 31 | 32 | 33 | 34 | CoffeeData coffeeData; 35 | StopWatch stopWatch; 36 | using namespace ace_button; 37 | ButtonConfig config1; 38 | AceButton buttonCircle(&config1); 39 | AceButton buttonSquare(&config1); 40 | 41 | 42 | namespace hds_buzzer { 43 | class Buzzer { 44 | public: 45 | Buzzer(int buzzerPin) { 46 | _buzzerPin = buzzerPin; 47 | pinMode(_buzzerPin, OUTPUT); 48 | } 49 | 50 | void beep(int times, int duration) { 51 | _buzzerTimes = times; 52 | _buzzerDuration = duration; 53 | _buzzerTimeStamp = millis(); 54 | } 55 | 56 | void off() { 57 | digitalWrite(BUZZER, LOW); 58 | } 59 | 60 | void check() { 61 | if (millis() - _buzzerTimeStamp < _buzzerDuration && b_beep) { 62 | digitalWrite(BUZZER, HIGH); 63 | isOn = true; 64 | } else { 65 | isOn = false; 66 | digitalWrite(BUZZER, LOW); 67 | } 68 | } 69 | protected: 70 | bool isOn; 71 | 72 | private: 73 | int _buzzerPin; 74 | int _buzzerLedPin; 75 | int _buzzerTimes; 76 | int _buzzerDuration; 77 | long _buzzerTimeStamp; 78 | }; 79 | } 80 | 81 | using namespace hds_buzzer; 82 | Buzzer buzzer(BUZZER); 83 | 84 | 85 | #endif -------------------------------------------------------------------------------- /display.h: -------------------------------------------------------------------------------- 1 | #ifndef DISPLAY_H 2 | #define DISPLAY_H 3 | #include "config.h" 4 | #include 5 | #ifdef U8X8_HAVE_HW_SPI 6 | #include 7 | #endif 8 | #ifdef U8X8_HAVE_HW_I2C 9 | #include 10 | #endif 11 | #if defined(ESP8266) || defined(ESP32) || defined(AVR) || defined(ARDUINO_ARCH_RP2040) 12 | #ifdef HW_I2C 13 | #ifdef SSD1306 14 | U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/OLED_RST); 15 | #endif 16 | #ifdef SSD1312 17 | U8G2_SSD1312_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/OLED_RST); 18 | #endif 19 | #if defined(SH1106) || defined(SH1116) 20 | U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/OLED_RST); 21 | #endif 22 | #endif 23 | #ifdef HW_SPI 24 | #ifdef SSD1306 25 | U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/OLED_CS, /* dc=*/OLED_DC, /* reset=*/OLED_RST); 26 | #endif 27 | #if defined(SH1106) || defined(SH1116) 28 | U8G2_SH1106_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/OLED_CS, /* dc=*/OLED_DC, /* reset=*/OLED_RST); 29 | #endif 30 | #endif 31 | #ifdef SW_SPI 32 | #if defined(SH1106) || defined(SH1116) 33 | U8G2_SH1106_128X64_NONAME_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/OLED_SCLK, /* sata=*/OLED_SDIN, /* cs=*/OLED_CS, /* dc=*/OLED_DC, /* reset=*/OLED_RST); 34 | #endif 35 | #endif 36 | #ifdef SW_SPI_OLD 37 | U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/13, /* data=*/11, /* cs=*/10, /* dc=*/9, /* reset=*/8); 38 | #endif 39 | #ifdef SW_SPI 40 | U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/OLED_SCLK, /* data=*/OLED_SDIN, /* cs=*/OLED_CS, /* dc=*/OLED_DC, /* reset=*/OLED_RST); 41 | #endif 42 | #ifdef SW_I2C 43 | U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g2(U8G2_R0, /* clock=*/OLED_SCL, /* data=*/OLED_SDA, /* reset=*/U8X8_PIN_NONE); 44 | #endif 45 | #endif 46 | //显示屏初始化 https://github.com/olikraus/u8g2/wiki/u8g2reference 47 | //设置字体 https://github.com/olikraus/u8g2/wiki/fntlistall 48 | //中文字体 https://github.com/larryli/u8g2_wqy 49 | //#include "u8g2_soso.h" 50 | #define FONT_L u8g2_font_logisoso24_tf 51 | #define FONT_M u8g2_font_fub14_tr //u8g2_font_wqy16_t_gb2312 u8g2_font_fub14_tr soso16 52 | #define FONT_S u8g2_font_helvR10_tr //u8g2_font_wqy14_t_gb2312 u8g2_font_helvR12_tr soso14 53 | #define FONT_TIMER u8g2_font_luBS18_tr 54 | #define FONT_GRAM u8g2_font_luBS18_tr 55 | #define FONT_WEIGHT u8g2_font_logisoso32_tf 56 | #define OLED_BUTTON_WINDOW_WIDTH 10 57 | #define OLED_BLE_WINDOW_HEIGHT 3 58 | //#define FONT_XS u8g2_font_wqy12_t_gb2312 59 | #define FONT_EXTRACTION u8g2_font_fub14_tr 60 | //#define FONT_S FONT_M 61 | #define FONT_BATTERY u8g2_font_battery19_tn 62 | //macros 63 | //文本对齐 AC居中 AR右对齐 AL左对齐 T为要显示的文本 64 | #define LCDWidth u8g2.getDisplayWidth() 65 | #define LCDHeight u8g2.getDisplayHeight() 66 | #define AC(T) ((LCDWidth - u8g2.getUTF8Width(T)) / 2 - Margin_Left - Margin_Right) 67 | #define AR(T) (LCDWidth - u8g2.getUTF8Width(T) - Margin_Right) 68 | #define AL(T) (u8g2.getUTF8Width(T) + Margin_Left) 69 | #define AM() ((LCDHeight + u8g2.getMaxCharHeight()) / 2 - Margin_Top - Margin_Bottom) 70 | #define AB() (LCDHeight - u8g2.getMaxCharHeight() - Margin_Bottom) 71 | #define AT() (u8g2.getMaxCharHeight() + Margin_Top) 72 | 73 | char minsec[10] = "00:00"; //时钟显示样式00:00 74 | // void refreshOLED(char* input); 75 | // void refreshOLED(char* input, const uint8_t* font); 76 | // void refreshOLED(char* input1, char* input2); 77 | // void refreshOLED(char* input1, char* input2, const uint8_t* font); 78 | // void refreshOLED(int input0, char* input1, char* input2); 79 | // void refreshOLED(char* input1, char* input2, char* input3); 80 | //icon tool https://lopaka.app/sandbox 81 | static const unsigned char image_circle[] = { 0xe0, 0x03, 0x38, 0x0e, 0x0c, 0x18, 0x06, 0x30, 0x02, 0x20, 0x03, 0x60, 0x01, 0x40, 0x01, 0x40, 0x01, 0x40, 0x03, 0x60, 0x02, 0x20, 0x06, 0x30, 0x0c, 0x18, 0x38, 0x0e, 0xe0, 0x03, 0x00, 0x00 }; 82 | static const unsigned char image_square[] = { 0xfe, 0x1f, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0xfe, 0x1f, 0x00, 0x00 }; 83 | static const unsigned char image_ble_enabled[] = { 0x03, 0x05, 0x09, 0x11, 0x09, 0x05, 0x03, 0x05, 0x09, 0x11, 0x09, 0x05, 0x03 }; 84 | static const unsigned char image_ble_disabled[] = { 0x18, 0x00, 0x28, 0x00, 0x49, 0x00, 0x8a, 0x00, 0x4c, 0x00, 0x28, 0x00, 0x18, 0x00, 0x28, 0x00, 0x48, 0x00, 0x88, 0x00, 0x48, 0x01, 0x28, 0x02, 0x18, 0x00 }; 85 | static const unsigned char image_battery_0[] = { 0x08, 0x36, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x3e }; 86 | static const unsigned char image_battery_1[] = { 0x08, 0x36, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x5d, 0x41, 0x3e }; 87 | static const unsigned char image_battery_2[] = { 0x08, 0x36, 0x41, 0x41, 0x41, 0x41, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x3e }; 88 | static const unsigned char image_battery_3[] = { 0x08, 0x36, 0x41, 0x41, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x3e }; 89 | static const unsigned char image_battery_4[] = { 0x08, 0x36, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x5d, 0x41, 0x3e }; 90 | static const unsigned char image_battery_charging[] = { 0x08, 0x36, 0x41, 0x51, 0x59, 0x7d, 0x7f, 0x5f, 0x4d, 0x45, 0x41, 0x3e }; 91 | char* sec2minsec(long n); 92 | char* sec2sec(long n); 93 | char* ltrim(char* s); 94 | char* rtrim(char* s); 95 | char* trim(char* s); 96 | 97 | 98 | void refreshOLED(char* input) { 99 | u8g2.firstPage(); 100 | u8g2.setFont(FONT_M); 101 | do { 102 | //1行 103 | //FONT_M = u8g2_font_fub14_tn; 104 | u8g2.drawUTF8(AC(input), AM() - 5, input); 105 | } while (u8g2.nextPage()); 106 | } 107 | 108 | void refreshOLED(char* input, const uint8_t* font) { 109 | u8g2.firstPage(); 110 | u8g2.setFont(font); 111 | do { 112 | //1行 113 | //FONT_M = u8g2_font_fub14_tn; 114 | u8g2.drawUTF8(AC(input), AM() - 5, input); 115 | } while (u8g2.nextPage()); 116 | } 117 | 118 | void refreshOLED(char* input1, char* input2) { 119 | u8g2.firstPage(); 120 | u8g2.setFont(FONT_M); 121 | do { 122 | //2行 123 | //FONT_M = u8g2_font_fub14_tn; 124 | u8g2.drawUTF8(AC(input1), u8g2.getMaxCharHeight(), input1); 125 | u8g2.drawUTF8(AC(input2), LCDHeight - 5, input2); 126 | } while (u8g2.nextPage()); 127 | } 128 | 129 | void refreshOLED(char* input1, char* input2, const uint8_t* font) { 130 | u8g2.firstPage(); 131 | u8g2.setFont(font); 132 | do { 133 | //2行 134 | //FONT_M = u8g2_font_fub14_tn; 135 | u8g2.drawUTF8(AC(input1), u8g2.getMaxCharHeight(), input1); 136 | u8g2.drawUTF8(AC(input2), LCDHeight - 5, input2); 137 | } while (u8g2.nextPage()); 138 | } 139 | 140 | void refreshOLED(int input0, char* input1, char* input2) { 141 | u8g2.firstPage(); 142 | u8g2.setFont(FONT_M); 143 | do { 144 | //3行 145 | //FONT_M = u8g2_font_fub14_tn; 146 | u8g2.drawUTF8(AC(input1), u8g2.getMaxCharHeight() + 5, input1); 147 | u8g2.drawUTF8(AC(input2), LCDHeight - 10, input2); 148 | u8g2.drawLine(94, 40, 90, 46); 149 | } while (u8g2.nextPage()); 150 | } 151 | 152 | void refreshOLED(char* input1, char* input2, char* input3) { 153 | u8g2.firstPage(); 154 | u8g2.setFont(FONT_S); 155 | do { 156 | //3行 157 | //FONT_M = u8g2_font_fub14_tn; 158 | u8g2.drawUTF8(AC(input1), u8g2.getMaxCharHeight(), input1); 159 | u8g2.drawUTF8(AC(input2), AM(), input2); 160 | u8g2.drawUTF8(AC(input3), LCDHeight, input3); 161 | } while (u8g2.nextPage()); 162 | } 163 | 164 | void refreshOLED(char* input1, char* input2, char* input3, const uint8_t* font) { 165 | u8g2.firstPage(); 166 | u8g2.setFont(font); 167 | do { 168 | //3行 169 | //FONT_M = u8g2_font_fub14_tn; 170 | u8g2.drawUTF8(AC(input1), u8g2.getMaxCharHeight(), input1); 171 | u8g2.drawUTF8(AC(input2), AM(), input2); 172 | u8g2.drawUTF8(AC(input3), LCDHeight, input3); 173 | } while (u8g2.nextPage()); 174 | } 175 | 176 | // macros from DateTime.h 177 | /* Useful Constants */ 178 | #define SECS_PER_MIN (60UL) 179 | #define SECS_PER_HOUR (3600UL) 180 | #define SECS_PER_DAY (SECS_PER_HOUR * 24L) 181 | /* Useful Macros for getting elapsed time */ 182 | #define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) 183 | #define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) 184 | #define numberOfHours(_time_) ((_time_ % SECS_PER_DAY) / SECS_PER_HOUR) 185 | #define elapsedDays(_time_) (_time_ / SECS_PER_DAY) 186 | 187 | char* sec2minsec(long n) { 188 | //手冲称使用xx:yy显示时间 189 | int minute = 0; 190 | int second = 0; 191 | if (n < 99 * 60 + 60) { 192 | if (n >= 60) { 193 | minute = n / 60; 194 | n = n % 60; 195 | } 196 | second = n; 197 | } else { 198 | minute = 99; 199 | second = 59; 200 | } 201 | snprintf(minsec, 6, "%02d:%02d", minute, second); 202 | return (char*)minsec; 203 | } 204 | 205 | char* sec2sec(long n) { 206 | //意式称使用xx秒显示时间 207 | long second = 0; 208 | // if (n < 99) { 209 | // second = n; 210 | // } else 211 | // second = 99; 212 | second = n; 213 | snprintf(minsec, 6, "%ds", second); 214 | // Serial.print("minsec "); 215 | // Serial.println(minsec); 216 | return (char*)minsec; 217 | } 218 | 219 | //自定义trim消除空格 220 | char* ltrim(char* s) { 221 | while (isspace(*s)) s++; 222 | return s; 223 | } 224 | 225 | char* rtrim(char* s) { 226 | char* back = s + strlen(s); 227 | while (isspace(*--back)) 228 | ; 229 | *(back + 1) = '\0'; 230 | return s; 231 | } 232 | 233 | char* trim(char* s) { 234 | return rtrim(ltrim(s)); 235 | } 236 | 237 | #endif -------------------------------------------------------------------------------- /espnow.h: -------------------------------------------------------------------------------- 1 | #ifndef ESPNOW_H 2 | #define ESPNOW_H 3 | #ifdef ESPNOW 4 | #include 5 | #include 6 | 7 | 8 | void updateEspnow() { 9 | if (millis() > t_esp_now_refresh + i_esp_now_interval) { 10 | t_esp_now_refresh = millis(); 11 | // Send data to all devices in promiscuous mode 12 | coffeeData.b_mode = b_mode; 13 | coffeeData.b_extraction = b_extraction; 14 | coffeeData.f_flow_rate = f_flow_rate; 15 | coffeeData.f_displayedValue = f_displayedValue; 16 | coffeeData.t_extraction_begin = t_extraction_begin; // Replace with your actual timestamp 17 | coffeeData.t_extraction_first_drop_num = t_extraction_first_drop_num; // Replace with your actual timestamp 18 | coffeeData.t_extraction_last_drop = t_extraction_last_drop; // Replace with your actual timestamp 19 | coffeeData.t_elapsed = stopWatch.elapsed(); 20 | coffeeData.b_running = stopWatch.isRunning(); 21 | coffeeData.f_weight_dose = f_weight_dose; 22 | coffeeData.dataFlag = 3721; // Use any value to identify the type of data 23 | coffeeData.b_power_off = b_power_off; 24 | 25 | // Send data to the receiver 26 | uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; 27 | esp_now_peer_info_t peerInfo = {}; 28 | memcpy(&peerInfo.peer_addr, broadcastAddress, 6); 29 | if (!esp_now_is_peer_exist(broadcastAddress)) { 30 | esp_now_add_peer(&peerInfo); 31 | } 32 | esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *)&coffeeData, sizeof(coffeeData)); 33 | #ifdef DEBUG 34 | // Print results to serial monitor 35 | if (result == ESP_OK) { 36 | Serial.println("Broadcast message success"); 37 | } else if (result == ESP_ERR_ESPNOW_NOT_INIT) { 38 | Serial.println("ESP-NOW not Init."); 39 | } else if (result == ESP_ERR_ESPNOW_ARG) { 40 | Serial.println("Invalid Argument"); 41 | } else if (result == ESP_ERR_ESPNOW_INTERNAL) { 42 | Serial.println("Internal Error"); 43 | } else if (result == ESP_ERR_ESPNOW_NO_MEM) { 44 | Serial.println("ESP_ERR_ESPNOW_NO_MEM"); 45 | } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) { 46 | Serial.println("Peer not found."); 47 | } else { 48 | Serial.println("Unknown error"); 49 | } 50 | #endif 51 | } 52 | } 53 | 54 | void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { 55 | // Handle the data sent callback if needed 56 | } 57 | 58 | void updateEspnow(int input) { 59 | for (int i = 0; i < input; i++) { 60 | coffeeData.b_mode = b_mode; 61 | coffeeData.b_extraction = b_extraction; 62 | coffeeData.f_flow_rate = f_flow_rate; 63 | coffeeData.f_displayedValue = f_displayedValue; 64 | coffeeData.t_extraction_begin = t_extraction_begin; // Replace with your actual timestamp 65 | coffeeData.t_extraction_first_drop_num = t_extraction_first_drop_num; // Replace with your actual timestamp 66 | coffeeData.t_extraction_last_drop = t_extraction_last_drop; // Replace with your actual timestamp 67 | coffeeData.t_elapsed = stopWatch.elapsed(); 68 | coffeeData.b_running = stopWatch.isRunning(); 69 | coffeeData.f_weight_dose = f_weight_dose; 70 | coffeeData.dataFlag = 3721; // Use any value to identify the type of data 71 | coffeeData.b_power_off = b_power_off; 72 | 73 | // Send data to the receiver 74 | uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; 75 | esp_now_peer_info_t peerInfo = {}; 76 | memcpy(&peerInfo.peer_addr, broadcastAddress, 6); 77 | if (!esp_now_is_peer_exist(broadcastAddress)) { 78 | esp_now_add_peer(&peerInfo); 79 | } 80 | esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *)&coffeeData, sizeof(coffeeData)); 81 | 82 | #ifdef DEBUG 83 | // Print results to serial monitor 84 | if (result == ESP_OK) { 85 | Serial.println("Broadcast message success"); 86 | } else if (result == ESP_ERR_ESPNOW_NOT_INIT) { 87 | Serial.println("ESP-NOW not Init."); 88 | } else if (result == ESP_ERR_ESPNOW_ARG) { 89 | Serial.println("Invalid Argument"); 90 | } else if (result == ESP_ERR_ESPNOW_INTERNAL) { 91 | Serial.println("Internal Error"); 92 | } else if (result == ESP_ERR_ESPNOW_NO_MEM) { 93 | Serial.println("ESP_ERR_ESPNOW_NO_MEM"); 94 | } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) { 95 | Serial.println("Peer not found."); 96 | } else { 97 | Serial.println("Unknown error"); 98 | } 99 | #endif 100 | delay(100); 101 | } 102 | } 103 | 104 | 105 | void formatMacAddress(const uint8_t *macAddr, char *buffer, int maxLength) 106 | // Formats MAC Address 107 | { 108 | snprintf(buffer, maxLength, "%02x:%02x:%02x:%02x:%02x:%02x", macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]); 109 | } 110 | 111 | void receiveCallback(const uint8_t *macAddr, const uint8_t *data, int dataLen) 112 | // Called when data is received 113 | { 114 | // Only allow a maximum of 250 characters in the message + a null terminating byte 115 | char buffer[ESP_NOW_MAX_DATA_LEN + 1]; 116 | int msgLen = min(ESP_NOW_MAX_DATA_LEN, dataLen); 117 | strncpy(buffer, (const char *)data, msgLen); 118 | 119 | // Make sure we are null terminated 120 | buffer[msgLen] = 0; 121 | 122 | // Format the MAC address 123 | char macStr[18]; 124 | formatMacAddress(macAddr, macStr, 18); 125 | 126 | // Send Debug log message to the serial port 127 | Serial.printf("Received message from: %s - %s\n", macStr, buffer); 128 | } 129 | 130 | 131 | void sentCallback(const uint8_t *macAddr, esp_now_send_status_t status) 132 | // Called when data is sent 133 | { 134 | char macStr[18]; 135 | formatMacAddress(macAddr, macStr, 18); 136 | #ifdef DEBUG 137 | Serial.print("Last Packet Sent to: "); 138 | Serial.println(macStr); 139 | Serial.print("Last Packet Send Status: "); 140 | Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); 141 | #endif 142 | } 143 | #endif 144 | #endif -------------------------------------------------------------------------------- /gyro.h: -------------------------------------------------------------------------------- 1 | #ifndef GYRO_H 2 | #define GYRO_H 3 | #include 4 | 5 | #ifdef ACC_MPU6050 6 | #include 7 | #include 8 | 9 | Adafruit_MPU6050 mpu; 10 | Adafruit_Sensor *mpu_temp; 11 | int i_gyro_print_interval = 100; 12 | long t_gyro_refresh = 0; 13 | 14 | void ACC_init() { 15 | if (!mpu.begin()) { 16 | Serial.println("Failed to find MPU6050 chip"); 17 | for (int i = 0; i < 4; i++) { 18 | digitalWrite(BUZZER, HIGH); 19 | delay(100); 20 | digitalWrite(BUZZER, LOW); 21 | delay(100); 22 | } 23 | // while (1) { 24 | // delay(1000); 25 | // Serial.println("Failed to find MPU6050 chip"); 26 | // } 27 | Serial.println("Failed to find MPU6050 chip"); 28 | b_gyroEnabled = false; 29 | return; 30 | } 31 | Serial.println("MPU6050 Found!"); 32 | 33 | mpu.setAccelerometerRange(MPU6050_RANGE_8_G); 34 | Serial.print("Accelerometer range set to: "); 35 | switch (mpu.getAccelerometerRange()) { 36 | case MPU6050_RANGE_2_G: 37 | Serial.println("+-2G"); 38 | break; 39 | case MPU6050_RANGE_4_G: 40 | Serial.println("+-4G"); 41 | break; 42 | case MPU6050_RANGE_8_G: 43 | Serial.println("+-8G"); 44 | break; 45 | case MPU6050_RANGE_16_G: 46 | Serial.println("+-16G"); 47 | break; 48 | } 49 | mpu.setGyroRange(MPU6050_RANGE_500_DEG); 50 | Serial.print("Gyro range set to: "); 51 | switch (mpu.getGyroRange()) { 52 | case MPU6050_RANGE_250_DEG: 53 | Serial.println("+- 250 deg/s"); 54 | break; 55 | case MPU6050_RANGE_500_DEG: 56 | Serial.println("+- 500 deg/s"); 57 | break; 58 | case MPU6050_RANGE_1000_DEG: 59 | Serial.println("+- 1000 deg/s"); 60 | break; 61 | case MPU6050_RANGE_2000_DEG: 62 | Serial.println("+- 2000 deg/s"); 63 | break; 64 | } 65 | 66 | mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); 67 | Serial.print("Filter bandwidth set to: "); 68 | switch (mpu.getFilterBandwidth()) { 69 | case MPU6050_BAND_260_HZ: 70 | Serial.println("260 Hz"); 71 | break; 72 | case MPU6050_BAND_184_HZ: 73 | Serial.println("184 Hz"); 74 | break; 75 | case MPU6050_BAND_94_HZ: 76 | Serial.println("94 Hz"); 77 | break; 78 | case MPU6050_BAND_44_HZ: 79 | Serial.println("44 Hz"); 80 | break; 81 | case MPU6050_BAND_21_HZ: 82 | Serial.println("21 Hz"); 83 | break; 84 | case MPU6050_BAND_10_HZ: 85 | Serial.println("10 Hz"); 86 | break; 87 | case MPU6050_BAND_5_HZ: 88 | Serial.println("5 Hz"); 89 | break; 90 | } 91 | 92 | Serial.println(""); 93 | delay(100); 94 | } 95 | 96 | double gyro_z() { 97 | if (b_gyroEnabled) { 98 | sensors_event_t a, g, temp; 99 | mpu.getEvent(&a, &g, &temp); 100 | // if (millis() > t_gyro_refresh + i_gyro_print_interval) { 101 | // //达到设定的gyro刷新频率后进行刷新 102 | // t_gyro_refresh = millis(); 103 | // Serial.print("\tGyro Z: \t"); 104 | // Serial.println(a.acceleration.z); 105 | // Serial.print("\t\tTemp: \t"); 106 | // Serial.println(temp.temperature); 107 | // Serial.print("\t\t\tESP32 Hall: \t"); 108 | // Serial.println(hallRead()); 109 | // } 110 | return (double)a.acceleration.z; 111 | } else 112 | return 0.0; 113 | } 114 | 115 | // float gyro_temp() { 116 | // sensors_event_t temp; 117 | // mpu.getEvent(&temp); 118 | // Serial.print("Gyro Temp: \t"); 119 | // Serial.println(temp.temperature); 120 | // return a.temperature; 121 | // } 122 | #endif 123 | 124 | 125 | #ifdef ACC_BMA400 126 | #include 127 | 128 | BMA400 acc; 129 | int i_gyro_print_interval = 100; 130 | long t_gyro_refresh = 0; 131 | // I2C address selection 132 | uint8_t i2cAddress = BMA400_I2C_ADDRESS_DEFAULT; // 0x14 133 | //uint8_t i2cAddress = BMA400_I2C_ADDRESS_SECONDARY; // 0x15 134 | 135 | void ACC_init() { 136 | while (acc.beginI2C(i2cAddress) != BMA400_OK) { 137 | // Not connected, inform user 138 | Serial.println("Failed to find BMA400 chip"); 139 | 140 | for (int i = 0; i < 4; i++) { 141 | digitalWrite(BUZZER, HIGH); 142 | delay(100); 143 | digitalWrite(BUZZER, LOW); 144 | delay(100); 145 | } 146 | // Wait a bit to see if connection is established 147 | delay(1000); 148 | // while (1) { 149 | // delay(1000); 150 | // Serial.println("Failed to find BMA400 chip"); 151 | // } 152 | Serial.println("Failed to find BMA400 chip"); 153 | b_gyroEnabled = false; 154 | return; 155 | } 156 | Serial.println("BMA400 Found!"); 157 | Serial.println(""); 158 | delay(100); 159 | } 160 | 161 | double gyro_z() { 162 | if (b_gyroEnabled) { 163 | acc.getSensorData(); 164 | float result = acc.data.accelZ * 10; 165 | return (double)result; 166 | } else 167 | return 0.0; 168 | } 169 | 170 | #endif 171 | 172 | #endif -------------------------------------------------------------------------------- /parameter.h: -------------------------------------------------------------------------------- 1 | #ifndef PARAMETER_H 2 | #define PARAMETER_H 3 | //declaration 4 | 5 | //ble 6 | bool b_ble_enabled = false; 7 | bool b_usbweight_enabled = false; 8 | unsigned long lastBleWeightNotifyTime = 0; // Stores the last time the weight notification was sent 9 | unsigned long lastUsbWeightNotifyTime = 0; // Stores the last time the weight notification was sent 10 | 11 | unsigned long lastWeightTextNotifyTime = 0; // Stores the last time the weight notification was sent 12 | unsigned long weightBleNotifyInterval = 100; // Interval at which to send weight notifications (milliseconds) 13 | unsigned long weightUsbNotifyInterval = 100; // Interval at which to send weight notifications (milliseconds) 14 | unsigned long weightTextNotifyInterval = 1000; 15 | int i_onWrite_counter = 0; 16 | long t_heartBeat = 0; 17 | bool b_requireHeartBeat = true; 18 | 19 | // 20 | int windowLength = 5; // default window length 21 | // define circular buffer 22 | float circularBuffer[5]; 23 | int bufferIndex = 0; 24 | 25 | //constant 常量 26 | //const int sample[] = { 1, 2, 4, 8, 16, 32, 64, 128 }; 27 | 28 | const int i_margin_top = 8; 29 | const int i_margin_bottom = 8; 30 | 31 | int b_beep = 1; //although it should be bool, change it from int to bool will affect eeprom address, because sizeof(b_beep) was used. 32 | bool b_about = false; 33 | bool b_debug = false; 34 | 35 | long t_batteryIcon = 0; 36 | bool b_showBatteryIcon = true; 37 | bool b_softSleep = false; 38 | bool b_gyroEnabled = true; 39 | 40 | //varables 变量 41 | uint64_t GPIO_reason = 0; 42 | bool b_usbLinked = false; 43 | int GPIO_power_on_with = -1; 44 | 45 | unsigned long t_power_on_button = 0; // Variable to store the timestamp when the button is pressed 46 | bool b_button_pressed = false; // Boolean flag to indicate whether the button is currently pressed 47 | 48 | 49 | float INPUTCOFFEEPOUROVER = 20.0; 50 | float INPUTCOFFEEESPRESSO = 20.0; 51 | float f_batteryCalibrationFactor = 0.66; 52 | String str_welcome = "welcome"; 53 | float f_calibration_value; //称重单元校准值 54 | float f_up_battery; //开机时电池电压 55 | long t_up_battery; //开机到现在时间 56 | 57 | bool b_chargingOLED = true; 58 | long t_shutdownFailBle = 0; //for popping up shut down fail due to ble is connected. 59 | bool b_shutdownFailBle = false; 60 | bool b_u8g2Sleep = true; 61 | long t_bootTare = 0; 62 | bool b_bootTare = false; 63 | int i_bootTareDelay = 1000; 64 | int i_tareDelay = 200; //tare delay for button 65 | long t_tareByButton = 0; //tare time stamp used by button to mimic delay 66 | bool b_tareByButton = false; 67 | long t_tareByBle = 0; 68 | bool b_tareByBle = false; 69 | long t_tareStatus = 0; //tare done time stamp 70 | long t_power_off; //关机倒计时 71 | long t_power_off_gyro = 0; //侧放关机倒计时 72 | long t_button_pressed; //进入萃取模式的时间点 73 | long t_temp; //上次更新温度和度数时间 74 | float f_temp_tare = 0; 75 | // int i_sample = 0; //采样数0-7 76 | // int i_sample_step = 0; //设置采样数的第几步 77 | int i_icon = 0; //充电指示电量数字0-6 78 | int i_setContainerWeight = 0; 79 | float f_filtered_temperature = 0; 80 | 81 | 82 | 83 | //电子秤参数和计时点 84 | bool b_weight_in_serial = false; 85 | bool b_negativeWeight = false; 86 | 87 | bool b_weight_quick_zero = false; //Tare后快速显示为0优化 88 | char c_weight[10]; //咖啡重量显示 89 | char c_brew_ratio[10]; //粉水比显示 90 | unsigned long t_extraction_begin = 0; //开始萃取打点 91 | unsigned long t_extraction_first_drop = 0; //下液第一滴打点 92 | unsigned long t_extraction_last_drop = 0; //下液结束打点 93 | unsigned long t_ready_to_brew = 0; //准备好冲煮的时刻(手冲) 94 | int i_extraction_minimium_timer = 7; //前7秒不判断停止计时 95 | 96 | unsigned long t_PowerDog = 0; //电源5s看门狗 97 | int tareCounter = 0; //不稳计数器 98 | const float f_weight_default_coffee = 0; //默认咖啡粉重量 99 | 100 | float aWeight = 0; //稳定状态比对值(g) 101 | float aWeightDiff = 0.15; //稳定停止波动值(g) 102 | float atWeight = 0; //自动归零比对值(g) 103 | float atWeightDiff = 0.3; //自动归零波动值(g) 104 | float asWeight = 0; //下液停止比对值(g) 105 | float asWeightDiff = 0.1; //下液停止波动值(g) 106 | float f_weight_adc = 0.0; //原始读出值(g) 107 | float f_weight_smooth; 108 | float f_displayedValue; 109 | float f_flow_rate; 110 | 111 | float f_weight_before_input; //按录入按钮之前的第几个读数(重量) 112 | 113 | unsigned long t_auto_tare = 0; //自动归零打点 114 | unsigned long t_auto_stop = 0; //下液停止打点 115 | unsigned long t_scale_stable = 0; //稳定状态打点 116 | unsigned long t_time_out = 0; //超时打点 117 | unsigned long t_last_weight_adc = 0; //最后一次重量输出打点 118 | unsigned long t_oled_refresh = 0; //最后一次oled刷新打点 119 | unsigned long t_esp_now_refresh = 0; //最后一次espnow刷新打点 120 | unsigned long t_flow_rate = 0; //上次流量计时 121 | int t_extraction_first_drop_num = 0; 122 | int b_power_off = 0; 123 | struct CoffeeData { 124 | int b_mode; 125 | int b_running; 126 | bool b_extraction; 127 | float f_flow_rate; 128 | float f_displayedValue; 129 | float f_weight_dose; 130 | long t_extraction_begin; 131 | long t_extraction_first_drop_num; 132 | long t_extraction_last_drop; 133 | long t_elapsed; 134 | long dataFlag; // Flag to identify the type of data 135 | int b_power_off; //if 1 then power off. 136 | }; 137 | 138 | const int autoTareInterval = 500; //自动归零检测间隔(毫秒) 139 | const int autoStopInterval = 500; //下液停止检测间隔(毫秒) 140 | const int scaleStableInterval = 500; //稳定状态监测间隔(毫秒) 141 | const int timeOutInterval = 30 * 1000; //超时检测间隔(毫秒) 142 | const int i_oled_print_interval = 0; //oled刷新间隔(毫秒) 143 | const int i_esp_now_interval = 100; //espnow刷新间隔(毫秒) 144 | const int i_serial_print_interval = 0; //称重输出间隔(毫秒) 145 | //flags 模式标识 146 | bool b_extraction = false; //萃取模式标识 147 | int b_mode = 0; //0 = pourover; 1 = espresso; 148 | 149 | bool b_menu = false; 150 | bool b_calibration = false; //Calibration flag 151 | bool b_ota = false; //wifi ota flag 152 | int i_calibration = 0; //0 for manual cal, 1 for smart cal 153 | //bool b_set_sample = false; //开机菜单 设置采样数 154 | bool b_show_info = false; //开机菜单 显示信息 155 | bool b_set_container = false; //开机菜单 设置称豆容器重量 156 | bool b_minus_container = false; //是否减去称豆容器 157 | bool b_minus_container_button = false; //是否减去称豆容器 158 | bool b_ready_to_brew = false; //准备冲煮并计时 159 | bool b_is_charging = false; //正在充电标识 160 | bool b_espnow = false; 161 | //bool b_debug = DEBUG; //debug信息显示 162 | #if DEBUG_BATTERY 163 | bool b_debug_battery = false; //debug电池信息 164 | #endif //DEBUG_BATTERY 165 | 166 | //电子秤校准参数 167 | int i_button_cal_status = 0; //校准的不同阶段 168 | int i_cal_weight = 0; //the weight to select 169 | float f_weight_dose = 0.0; //咖啡粉重量 170 | float f_weight_container = 0.0; //咖啡手柄重量 171 | 172 | int i_decimal_precision = 1; //小数精度 0.1g/0.01g 173 | char c_flow_rate[10]; //流速文字 174 | float f_flow_rate_last_weight = 0.0; //流速上次记重 175 | 176 | char* c_battery = (char*)"0"; //电池字符 0-5有显示 6是充电图标 177 | char* c_batteryTemp = (char*)"0"; //临时暂存电池状态 以便电量显示不跳跃 178 | unsigned long t_battery = 0; //电池充电循环判断打点 179 | int i_battery = 0; //电池充电循环变量 180 | int batteryRefreshTareInterval = 1 * 1000; //1秒刷新一次电量显示 181 | unsigned long t_batteryRefresh = 0; //电池充电循环判断打点 182 | #ifdef AVR 183 | float f_vref = 4.72; //5V pin true reading 184 | float f_true_battery_reading = 4.72; //需测量usb vcc电压(不一定是usb,可以是电池电压) 185 | float f_adc_battery_reading = 4.72; //mcu读取的电压 186 | float f_divider_factor = f_true_battery_reading / f_adc_battery_reading * 4.07 / 4.38; 187 | #endif 188 | #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_MBED_RP2040) 189 | float f_vref = 3.3; //3.3V pin true reading 190 | float f_true_battery_reading = 4.24; //需测量usb vcc电压(不一定是usb,可以是电池电压) 191 | float f_adc_battery_reading = 1.99; //mcu读取的电压 192 | float f_divider_factor = f_true_battery_reading / f_adc_battery_reading * 4.07 / 4.38; //for 3.3v board use 2x100k resistor 193 | #endif 194 | 195 | int i_display_rotation = 0; //旋转方向 0 1 2 3 : 0 90 180 270 196 | 197 | //EEPROM地址列表 198 | int i_addr_calibration_value = 0; //EEPROM从0开始记录 校准值以float类型表示 float calibrationValue; 199 | //int i_addr_sample = i_addr_calibration_value + sizeof(f_calibration_value); //int sample值 200 | int i_addr_container = i_addr_calibration_value + sizeof(f_calibration_value); //采样数以float类型表示 float f_weight_container 201 | int i_addr_mode = i_addr_container + sizeof(f_weight_container); //模式以int类型表示 int b_mode 202 | int INPUTCOFFEEPOUROVER_ADDRESS = i_addr_mode + sizeof(b_mode); 203 | int INPUTCOFFEEESPRESSO_ADDRESS = INPUTCOFFEEPOUROVER_ADDRESS + sizeof(INPUTCOFFEEPOUROVER); 204 | int i_addr_beep = INPUTCOFFEEESPRESSO_ADDRESS + sizeof(INPUTCOFFEEESPRESSO); 205 | int i_addr_welcome = i_addr_beep + sizeof(b_beep); //str_welcome 206 | int i_addr_batteryCalibrationFactor = i_addr_welcome + sizeof(str_welcome); //f_batteryCalibrationFactor 207 | int i_addr_heartbeat = i_addr_batteryCalibrationFactor + sizeof(f_batteryCalibrationFactor); //b_requireHeartBeat 208 | 209 | //int i_addr_debug = i_addr_batteryCalibrationFactor + sizeof(f_batteryCalibrationFactor); //str_welcome 210 | 211 | #endif -------------------------------------------------------------------------------- /usbcomm.h: -------------------------------------------------------------------------------- 1 | #ifndef USBCOMM_H 2 | #define USBCOMM_H 3 | 4 | //functions 5 | void sendUsbVoltage(); 6 | void sendUsbGyro(); 7 | 8 | class MyUsbCallbacks { 9 | public: 10 | uint8_t calculateChecksum(uint8_t *data, size_t len) { 11 | uint8_t xorSum = 0; 12 | // 遍历数据中的每个字节,排除最后一个字节(假设它是校验和) 13 | for (size_t i = 0; i < len - 1; i++) { 14 | xorSum ^= data[i]; 15 | } 16 | return xorSum; 17 | } 18 | 19 | // 校验数据的校验和 20 | bool validateChecksum(uint8_t *data, size_t len) { 21 | if (len < 2) { // 至少需要 1 字节数据和 1 字节校验和 22 | return false; 23 | } 24 | uint8_t expectedChecksum = data[len - 1]; 25 | uint8_t calculatedChecksum = calculateChecksum(data, len); 26 | return expectedChecksum == calculatedChecksum; 27 | } 28 | 29 | void onWrite(uint8_t *data, size_t len) { 30 | //this is what the esp32 received via usb 31 | Serial.print("Timer "); 32 | Serial.print(millis()); 33 | Serial.print(" onWrite counter:"); 34 | Serial.print(i_onWrite_counter++); 35 | Serial.print(" "); 36 | 37 | 38 | if (data != nullptr && len > 0) { 39 | // 打印接收到的 HEX 数据 40 | Serial.print("Received HEX: "); 41 | for (size_t i = 0; i < len; i++) { 42 | if (data[i] < 0x10) { // 如果字节小于 0x10 43 | Serial.print("0"); // 打印前导零 44 | } 45 | Serial.print(data[i], HEX); // 以 HEX 格式打印字节 46 | } 47 | Serial.print(" "); 48 | 49 | if (data[0] == 0x03) { 50 | //check if it's a decent scale message 51 | if (data[1] == 0x0F) { 52 | //taring 53 | if (validateChecksum(data, len)) { 54 | Serial.println("Valid checksum for tare operation. Taring"); 55 | } else { 56 | Serial.println("Invalid checksum for tare operation."); 57 | } 58 | b_tareByBle = true; 59 | t_tareByBle = millis(); 60 | if (data[5] == 0x00) { 61 | /* 62 | Tare the scale by sending "030F000000000C" (old version, disables heartbeat) 63 | Tare the scale by sending "030F000000010D" (new version, leaves heartbeat as set) 64 | */ 65 | b_requireHeartBeat = false; 66 | Serial.println("*** Heartbeat detection Off ***"); 67 | } 68 | if (data[5] == 0x01) { 69 | /* 70 | Tare the scale by sending "030F000000000C" (old version, disables heartbeat) 71 | Tare the scale by sending "030F000000010D" (new version, leaves heartbeat as set) 72 | */ 73 | Serial.print("*** Heartbeat detection remained "); 74 | if (b_requireHeartBeat) 75 | Serial.print("On"); 76 | else 77 | Serial.print("Off"); 78 | Serial.println(" ***"); 79 | } 80 | } else if (data[1] == 0x0A) { 81 | if (data[2] == 0x00) { 82 | Serial.println("LED off detected. Turn off OLED."); 83 | u8g2.setPowerSave(1); 84 | b_u8g2Sleep = true; 85 | } else if (data[2] == 0x01) { 86 | Serial.print("LED on detected. Turn on OLED."); 87 | u8g2.setPowerSave(0); 88 | b_u8g2Sleep = false; 89 | if (data[5] == 0x00) { 90 | b_requireHeartBeat = false; 91 | Serial.println(" *** Heartbeat detection Off ***"); 92 | } 93 | if (data[5] == 0x01) { 94 | Serial.print("*** Heartbeat detection remained "); 95 | if (b_requireHeartBeat) 96 | Serial.print("On"); 97 | else 98 | Serial.print("Off"); 99 | Serial.println(" ***"); 100 | } 101 | } else if (data[2] == 0x02) { 102 | Serial.println("Power off detected."); 103 | shut_down_now_nobeep(); 104 | } else if (data[2] == 0x03) { 105 | if (data[3] == 0x01) { 106 | Serial.println("Start Low Power Mode."); 107 | u8g2.setContrast(0); 108 | } else if (data[3] == 0x00) { 109 | Serial.println("Exit low power mode."); 110 | u8g2.setContrast(255); 111 | } 112 | } else if (data[2] == 0x04) { 113 | if (data[3] == 0x01) { 114 | Serial.println("Start Soft Sleep."); 115 | u8g2.setPowerSave(1); 116 | b_softSleep = true; 117 | digitalWrite(PWR_CTRL, LOW); 118 | digitalWrite(ACC_PWR_CTRL, LOW); 119 | } else if (data[3] == 0x00) { 120 | Serial.println("Exit Soft Sleep."); 121 | digitalWrite(PWR_CTRL, HIGH); 122 | digitalWrite(ACC_PWR_CTRL, HIGH); 123 | u8g2.setPowerSave(0); 124 | b_softSleep = false; 125 | } 126 | } 127 | } else if (data[1] == 0x0B) { 128 | if (data[2] == 0x03) { 129 | Serial.println("Timer start detected."); 130 | stopWatch.reset(); 131 | stopWatch.start(); 132 | } else if (data[2] == 0x00) { 133 | Serial.println("Timer stop detected."); 134 | stopWatch.stop(); 135 | } else if (data[2] == 0x02) { 136 | Serial.println("Timer zero detected."); 137 | stopWatch.reset(); 138 | } 139 | } else if (data[1] == 0x1A) { 140 | if (data[2] == 0x00) { 141 | Serial.println("Manual Calibration via BLE"); 142 | i_button_cal_status = 1; 143 | i_calibration = 0; 144 | b_calibration = true; 145 | } else if (data[2] == 0x01) { 146 | Serial.println("Smart Calibration via BLE"); 147 | i_button_cal_status = 1; 148 | i_calibration = 1; 149 | b_calibration = true; 150 | } 151 | } else if (data[1] == 0x1B) { 152 | Serial.println("Start WiFi OTA"); 153 | wifiUpdate(); 154 | } else if (data[1] == 0x1C) { //buzzer settings 155 | if (data[2] == 0x00) { 156 | Serial.println("Buzzer Off"); 157 | b_beep = false; // won't store into eeprom 158 | } else if (data[2] == 0x01) { 159 | Serial.println("Buzzer On"); 160 | b_beep = true; // won't store into eeprom 161 | } else if (data[2] == 0x02) { 162 | Serial.println("Buzzer Beep"); 163 | buzzer.beep(1, 50); 164 | } 165 | } else if (data[1] == 0x1D) { //Sample settings 166 | if (data[2] == 0x00) { 167 | scale.setSamplesInUse(1); 168 | Serial.print("Samples in use set to: "); 169 | Serial.println(scale.getSamplesInUse()); 170 | } else if (data[2] == 0x01) { 171 | scale.setSamplesInUse(2); 172 | Serial.print("Samples in use set to: "); 173 | Serial.println(scale.getSamplesInUse()); 174 | } else if (data[2] == 0x03) { 175 | scale.setSamplesInUse(4); 176 | Serial.print("Samples in use set to: "); 177 | Serial.println(scale.getSamplesInUse()); 178 | } 179 | } else if (data[1] == 0x1E) { 180 | if (data[2] == 0x00) { 181 | //menu control 182 | if (data[3] == 0x00) { 183 | //hide menu 184 | Serial.println("Hide menu"); 185 | b_menu = false; 186 | } else if (data[3] == 0x01) { 187 | //show menu 188 | Serial.println("Show menu"); 189 | b_menu = true; 190 | } 191 | } else if (data[2] == 0x01) { 192 | //about info 193 | if (data[3] == 0x00) { 194 | //hide about info 195 | Serial.println("Hide about info"); 196 | if (b_menu) 197 | b_showAbout = false; //hide about info(code in menu) if it's enabled via menu 198 | else 199 | b_about = false; //hide about info(code in loop) if it's enabled via ble command 200 | } else if (data[3] == 0x01) { 201 | //show about info 202 | Serial.println("Show about info"); 203 | b_debug = false; 204 | b_about = true; 205 | b_menu = false; 206 | } 207 | } else if (data[2] == 0x02) { 208 | //debug info 209 | if (data[3] == 0x00) { 210 | //hide debug info 211 | Serial.println("Hide debug info"); 212 | b_debug = false; 213 | } else if (data[3] == 0x01) { 214 | //show debug info 215 | Serial.println("Show debug info"); 216 | b_about = false; 217 | b_debug = true; 218 | b_menu = false; 219 | } 220 | } 221 | } else if (data[1] == 0x1F) { 222 | reset(); 223 | } else if (data[1] == 0x20) { 224 | if (data[2] == 0x00) { 225 | Serial.println("Weight by USB disabled"); 226 | b_usbweight_enabled = false; 227 | } else if (data[2] == 0x01) { 228 | Serial.println("Weight by USB enabled"); 229 | b_usbweight_enabled = true; 230 | if (len >= 4) { 231 | uint16_t interval = 100; 232 | uint8_t multiplier = data[3]; 233 | if (multiplier < 1) multiplier = 1; 234 | if (multiplier > 50) multiplier = 50; // 最大支持 5000ms 235 | 236 | interval = multiplier * 100; 237 | weightUsbNotifyInterval = interval; 238 | 239 | Serial.print("USB weight interval set to "); 240 | Serial.print(weightUsbNotifyInterval); 241 | Serial.println(" ms"); 242 | } 243 | } 244 | 245 | } else if (data[1] == 0x21) { 246 | sendUsbGyro(); 247 | } else if (data[1] == 0x22) { 248 | sendUsbVoltage(); 249 | } 250 | } 251 | } 252 | } 253 | }; 254 | 255 | void sendUsbVoltage() { 256 | Serial.print("Battery Voltage:"); 257 | float voltage = getVoltage(BATTERY_PIN); 258 | Serial.print(voltage); 259 | #ifndef ADS1115ADC 260 | int adcValue = analogRead(BATTERY_PIN); // Read the value from ADC 261 | float voltageAtPin = (adcValue / adcResolution) * referenceVoltage; // Calculate voltage at ADC pin 262 | Serial.print("\tADC Voltage:"); 263 | Serial.print(voltageAtPin); 264 | Serial.print("\tbatteryCalibrationFactor: "); 265 | Serial.print(f_batteryCalibrationFactor); 266 | #endif 267 | Serial.print("\tlowBatteryCounterTotal: "); 268 | Serial.print(i_lowBatteryCountTotal); 269 | 270 | byte data[7]; 271 | // float weight = scale.getData(); 272 | byte voltageByte1, voltageByte2; 273 | 274 | encodeWeight(voltage, voltageByte1, voltageByte2); 275 | 276 | data[0] = modelByte; 277 | data[1] = 0x22; // Type byte for gyro data 278 | data[2] = voltageByte1; 279 | data[3] = voltageByte2; 280 | // Fill the rest with dummy data or real data as needed 281 | data[4] = 0x00; 282 | data[5] = 0x00; 283 | data[6] = calculateXOR(data, 6); // Last byte is XOR validation 284 | 285 | // Use Serial.write to send data via USB (serial) 286 | Serial.write(data, 7); // 7 bytes of data 287 | } 288 | 289 | void sendUsbGyro() { 290 | byte data[7]; 291 | // float weight = scale.getData(); 292 | float gyro = gyro_z(); 293 | byte gyroByte1, gyroByte2; 294 | 295 | encodeWeight(gyro, gyroByte1, gyroByte2); 296 | 297 | data[0] = modelByte; 298 | data[1] = 0x21; // Type byte for gyro data 299 | data[2] = gyroByte1; 300 | data[3] = gyroByte2; 301 | // Fill the rest with dummy data or real data as needed 302 | data[4] = 0x00; 303 | data[5] = 0x00; 304 | data[6] = calculateXOR(data, 6); // Last byte is XOR validation 305 | 306 | // Use Serial.write to send data via USB (serial) 307 | Serial.write(data, 7); // 7 bytes of data 308 | } 309 | 310 | void sendUsbWeight() { 311 | unsigned long currentMillis = millis(); 312 | if (currentMillis - lastUsbWeightNotifyTime >= weightUsbNotifyInterval) { 313 | // Save the last time you sent the weight notification 314 | lastUsbWeightNotifyTime = currentMillis; 315 | 316 | byte data[7]; 317 | // float weight = scale.getData(); 318 | float weight = f_displayedValue; 319 | byte weightByte1, weightByte2; 320 | 321 | encodeWeight(weight, weightByte1, weightByte2); 322 | 323 | data[0] = modelByte; 324 | data[1] = 0xCE; // Type byte for weight stable 325 | data[2] = weightByte1; 326 | data[3] = weightByte2; 327 | // Fill the rest with dummy data or real data as needed 328 | data[4] = 0x00; 329 | data[5] = 0x00; 330 | data[6] = calculateXOR(data, 6); // Last byte is XOR validation 331 | 332 | // Use Serial.write to send data via USB (serial) 333 | Serial.write(data, 7); // 7 bytes of data 334 | } 335 | } 336 | 337 | void sendUsbTextWeight() { 338 | unsigned long currentMillis = millis(); 339 | if (currentMillis - lastWeightTextNotifyTime >= weightTextNotifyInterval) { 340 | // Save the last time you sent the weight notification 341 | lastWeightTextNotifyTime = currentMillis; 342 | Serial.print(lastWeightTextNotifyTime); 343 | Serial.print(" Weight: "); // 7 bytes of data 344 | Serial.println(f_displayedValue); 345 | } 346 | } 347 | 348 | void sendUsbButton(int buttonNumber, int buttonShortPress) { 349 | // buttonNumber 1 for button O, 2 for button[] 350 | // buttonShortPress 1 for short press, 2 for long press 351 | byte data[7]; 352 | 353 | data[0] = modelByte; 354 | data[1] = 0xAA; // Type byte for button press 355 | data[2] = buttonNumber; 356 | data[3] = buttonShortPress; 357 | // Fill the rest with dummy data or real data as needed 358 | data[4] = 0x00; 359 | data[5] = 0x00; 360 | data[6] = calculateXOR(data, 6); // Last byte is XOR validation 361 | 362 | // Use Serial.write to send data via USB (serial) 363 | Serial.write(data, 7); // 7 bytes of data 364 | } 365 | #endif -------------------------------------------------------------------------------- /web_apps/Quality_Control_Assistant/README.md: -------------------------------------------------------------------------------- 1 | # A Quality Control Weighing Application for Decent Scales 2 | 3 | This web app is a web-based tool designed to streamline **quality control weighing processes**. Built with pure JavaScript, HTML, and CSS, it leverages **Web Bluetooth Low Energy (Web BLE)** to connect directly to your **Half Decent Scale**, providing a guided workflow for repetitive measurements, real-time feedback, and easy data export. 4 | 5 | --- 6 | 7 | ## Live Demo 8 | 9 | Experience the app in action! It requires a **Chrome or Edge browser (version 70 or newer)** on a device with Bluetooth enabled: 10 | 11 | [https://decentespresso.com/support/scale/decentscale_qcweigh](https://decentespresso.com/support/scale/decentscale_qcweigh) 12 | 13 | --- 14 | 15 | ## Features 16 | 17 | * **Real-time Weight Display:** Get live weight readings from your Half Decent Scale in grams. 18 | * **Guided QC Workflow:** A robust **state machine** guides you through the measurement process, prompting for object placement and removal. 19 | * **Configurable Thresholds:** Define **goal weight**, **low threshold**, **high threshold**, and a **minimum weight** for object detection to customize pass/fail criteria. 20 | * **Audible Feedback:** Optional sound alerts indicate whether a measurement **passed or failed**, allowing for quick, non-visual feedback. 21 | * **Comprehensive Measurement Logging:** Every QC reading is saved with: 22 | * A unique reading ID 23 | * Timestamp 24 | * Measured weight 25 | * Pass/Fail result 26 | * The exact QC settings active at the time of measurement. 27 | * **Local Data Storage:** All measurement data persists locally in your browser's cache (even if you close the tab, it will be there when you open it again). 28 | * **Flexible Data Export:** Easily download your collected QC data in: 29 | * **CSV (Comma Separated Values):** Perfect for spreadsheet analysis. 30 | * **JSON (JavaScript Object Notation):** Ideal for programmatic use or integration with other systems. 31 | * **User Presets:** Save and load your specific QC settings (e.g., for different coffee beans or products) to quickly switch between workflows. Presets are stored locally for convenience. 32 | * **Fullscreen Mode:** Toggle full-screen view for a distraction-free experience. 33 | * **No Frameworks:** Built purely with native JavaScript, HTML, and CSS for a lightweight and transparent codebase. 34 | 35 | --- 36 | 37 | ## How to Use 38 | 39 | 1. **Open in Browser:** Navigate to the live demo link provided above using a Chrome or Edge browser (version 70 or newer) on a device with Bluetooth. 40 | 2. **Ensure Scale is Ready:** Turn on your Half Decent Scale and make sure it's within Bluetooth range. 41 | 3. **Connect to Scale:** Click the **"Connect to Scale"** button. A browser prompt will appear; select your "Decent Scale" from the list and click "Pair". 42 | 4. **Configure Settings:** 43 | * Input your target **Goal Weight**, allowable **Low Threshold**, and **High Threshold**. 44 | * Set the **Min Weight** (the minimum weight detected to consider an object placed). 45 | * Check **"Enable Sounds"** if you want audio cues for pass/fail. 46 | * You can also save your current settings as a **preset** by typing a name in "Object Name" and selecting "Save Current Settings as Preset" from the dropdown. To load a saved preset, simply select it from the dropdown. 47 | 5. **Start QC Mode:** Click the **"Start QC Mode"** button. The app will tare the scale and display status messages. 48 | 6. **Follow On-Screen Prompts:** 49 | * "Place object on scale": Place your first item on the scale. 50 | * "Stabilizing...": The app waits for the weight to become stable. 51 | * "Measurement done, place next object": Once a stable reading is recorded and evaluated (Pass/Fail), a sound will play (if enabled), and you'll be prompted to remove the current item and place the next. 52 | 7. **Export Data:** After completing your measurements, click **"Export to CSV"** or **"Export to JSON"** to download your collected data. 53 | 8. **Stop QC Mode:** Click **"Stop QC Mode"** to end the current QC session. 54 | 55 | --- 56 | 57 | ## Technical Details 58 | 59 | The `decentscale_qcweigh` application relies on a few key technical aspects: 60 | 61 | * **Web BLE Communication:** 62 | * The app connects to the Half Decent Scale using its **service UUID `0000fff0-0000-1000-8000-00805f9b34fb`**. 63 | * **Read Characteristic:** `0000fff4-0000-1000-8000-00805f9b34fb` is used to receive weight data notifications. 64 | * **Write Characteristic:** `000036f5-0000-1000-8000-00805f9b34fb` is used to send commands like `tare` to the scale. 65 | * A **command queue (`commandQueue`)** ensures that commands sent to the scale are executed sequentially, preventing conflicts. 66 | * **QC State Machine:** The core of the QC logic is managed by a state machine with three states: 67 | * **`WAITING_FOR_NEXT`**: The scale is waiting for an object to be placed. 68 | * **`MEASURING`**: An object has been detected, and the app is waiting for the weight to stabilize. 69 | * **`REMOVAL_PENDING`**: A measurement has been recorded, and the app is waiting for the object to be removed before prompting for the next. 70 | * **Weight Stability:** Stability is determined by monitoring the change in weight over time. A `stabilityThreshold` (default **0.2g**) defines the maximum allowed fluctuation for a reading to be considered stable. 71 | * **Local Storage for Presets:** User-defined QC settings are stored in `localStorage` under the key `'decentScalePresets'`, allowing them to persist across browser sessions. The `lastUsedPreset` is also stored. 72 | * **Sound Generation:** Basic pass/fail sounds are generated directly in the browser using the **Web Audio API** (`AudioContext`, `OscillatorNode`, `GainNode`). 73 | * **Fullscreen API:** Utilizes the HTML Fullscreen API (`requestFullscreen`, `exitFullscreen`) for a more immersive user interface. 74 | 75 | --- 76 | 77 | ## Development Setup 78 | 79 | To explore, modify, or contribute to this application: 80 | 81 | 1. **Download all files in this folder**. 82 | 2. **Navigate to the folder** in your terminal: 83 | 84 | ```bash 85 | cd half-decent-web-apps/decentscale_qcweigh 86 | ``` 87 | 88 | 3. **Serve the files locally over HTTP/HTTPS:** 89 | Web BLE requires your application to be served over `https://` or `http://localhost`. You can use a simple local web server: 90 | 91 | **Using Python's `http.server` (Python 3, recommended):** 92 | 93 | ```bash 94 | python -m http.server 8000 95 | ``` 96 | 97 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 98 | 99 | **Using Node.js `http-server` (if you have Node.js installed):** 100 | 101 | ```bash 102 | npm install -g http-server 103 | http-server -p 8000 104 | ``` 105 | 106 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 107 | 108 | --- 109 | 110 | ## Code Structure Highlights 111 | 112 | The core logic resides within the `DecentScale` class (`ble_jvs_QCv2.1.js`): 113 | 114 | * **`constructor()`**: Initializes properties, sets up event listeners after DOM is loaded, and loads saved presets. 115 | * **`initializeElements()` & `addEventListeners()`**: Handles linking JavaScript to HTML elements and attaching user interaction handlers. 116 | * **`_findAddress()`, `_connect()`, `disconnect()`**: Manages the Web BLE connection lifecycle. 117 | * **`notification_handler(event)`**: The primary event listener for incoming weight data, where the QC state machine logic and weight evaluation happen. 118 | * **`toggleConnectDisconnect()`**: A utility function to switch between connecting and disconnecting. 119 | * **`executeCommand(command)` & `_send(cmd)`**: Provides a queued approach to sending commands to the scale, improving reliability. 120 | * **`_tare()` / `tare()`**: Implements the tare functionality for the scale. 121 | * **`evaluateWeight(weight)`**: Simple logic to determine if a reading is "pass" or "fail" based on thresholds. 122 | * **`playSound(type)`**: Generates audible feedback for pass/fail results. 123 | * **`toggleQCmode()`, `startQCMode()`, `stopQCMode()`**: Controls the overall QC mode activation and deactivation. 124 | * **`updateQCStatus(state, message)`**: Updates the user interface with current QC status messages and dynamic styling. 125 | * **`saveMeasurement(weight, result)`**: Records each individual QC measurement, including its context. 126 | * **`displayWeightReadings()`**: Updates the displayed list of past measurements and manages export button states. 127 | * **`exportToCSV()` / `exportToJSON()` / `downloadFile()`**: Handles the logic for converting `weightData` into downloadable CSV and JSON files. 128 | * **`setupPresetHandlers()`, `saveCurrentPreset()`, `savePreset()`, `getPresets()`, `getPreset()`, `loadPresets()`, `loadPreset()`, `updatePresetList()`**: A comprehensive set of methods for managing user-defined QC presets via `localStorage`. 129 | * **`toggleFullScreen()`, `setupFullscreenHandler()`, `updateFullscreenState()`**: Manages the fullscreen browser functionality. 130 | 131 | --- 132 | 133 | ## Contributing 134 | 135 | We welcome community contributions! If you have suggestions, bug reports, or want to contribute code, please refer to the main [Programmers guide to the Half Decent Scale](https://decentespresso.com/docs/programmers_guide_to_the_half_decent_scale). 136 | 137 | 138 | --- 139 | 140 | ## License 141 | 142 | This project is licensed under the **GNU General Public License v3.0**. See the `LICENSE` file in the main repository for more details. 143 | 144 | --- 145 | -------------------------------------------------------------------------------- /web_apps/Quality_Control_Assistant/quality_control.html: -------------------------------------------------------------------------------- 1 | Decent Scale QC Assistant 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 | Quality Control Assistant 13 |
14 | Status: Not connected 15 | 21 | 22 |
23 | 27 | 61 |
62 | 66 |
67 |
68 |
69 | 70 | 71 |
72 |
Quality Control Settings
73 | 74 | 75 |
76 |
77 | 78 | 81 |
82 |
83 | 84 | 89 |
90 |
91 | 92 | 93 |
94 |
95 | 96 | 98 |
99 |
100 | 101 | 103 |
104 |
105 | 106 | 108 |
109 |
110 | 111 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 |
123 | 124 | 125 |
126 |
Weight: N/A
127 | 131 |
132 | 133 | 134 |
136 |
137 |
138 | 139 | 140 |
141 |
142 | History 143 | 146 | 149 |
150 |
151 |
    152 |
    153 |
    154 |
    155 |
    156 | 157 | 158 | 159 | 162 | 163 | 168 | 169 | -------------------------------------------------------------------------------- /web_apps/README.md: -------------------------------------------------------------------------------- 1 | ## Half Decent Web Apps 2 | 3 | Half Decent Web Apps Leveraging Low-Barrier Tech for Enhanced Scale Functionality. Half Decent Web Apps is a collection of three web applications built with plain JavaScript, HTML, and CSS, demonstrating various functionalities available through Web Bluetooth Low Energy (Web BLE) and Web USB.These apps aim to open up new ways to interact with your Half Decent Scale, providing powerful tools for quality control, precise weighing, and simplified dosing directly from your web browser. 4 | 5 | *** 6 | 7 | ### Experience the Apps: Live Demos 8 | 9 | Get hands-on with our web tools. (Currently available for desktop/laptop browsers and Android devices. iOS is not supported at this time due to Web BLE/USB limitations on that platform.) 10 | 11 | * **[Weigh and Save](https://decentespresso.com/support/scale/decentscale_webweigh)**: Your go-to app for general weighing tasks. Precisely measure items within a set timeframe and easily export the results for analysis or record-keeping. 12 | * **[Quality Control Assistant](https://decentespresso.com/support/scale/decentscale_qcweigh)**: Perfect for ensuring consistency. This app allows you to repeatedly weigh the same items, simplifying data tracking and quality checks. 13 | * **[Dosing Assistant](https://decentespresso.com/support/scale/samew_dosing_ast)**: Simplify your dosing process with an intuitive interface designed for achieving accurate and repeatable measurements. 14 | 15 | *** 16 | 17 | ### Getting Started: How to Connect & Use 18 | 19 | Connecting your Half Decent Scale to our web apps is straightforward. Follow these steps: 20 | 21 | 1. **Browser Check**: Ensure you're using an up-to-date version of Google Chrome (version 136 or newer) or Microsoft Edge (version 136 or newer). 22 | 2. **Open an App**: Navigate to one of the app links provided above. 23 | 3. **Enable Pairing Mode**: Put your Half Decent Scale into Bluetooth pairing mode (refer to your scale's manual if needed). 24 | 4. **Connect via App**: 25 | * Click the "Connect" button within the web application. 26 | * A pop-up window will appear, listing available Bluetooth devices. 27 | * Select "Decent Scale" (or your scale's specific name) from the list. 28 | 5. **Connection Confirmed**: Once connected, the "Connect" button will turn red and change its text to "Disconnect." You will also see live weight updates from your scale displayed on the screen. 29 | 6. **Begin**: Click the "Start" button and follow the on-screen instructions specific to the app you're using. 30 | 31 | **Using USB-C Connection (Optional for "Weigh and Save")**: 32 | The "Weigh and Save" app also supports a direct USB-C connection. If you choose this method: 33 | 34 | * You will need to install CH34X serial drivers on your device. 35 | * Follow the [USB-C driver installation instructions here](https://decentespresso.com/docs/how_to_use_usbc_to_upgrade_the_firmware_on_your_half_decent_scale). 36 | 37 | *** 38 | 39 | ### Purpose & Audience 40 | 41 | These web applications serve two main goals: 42 | 43 | 1. **For Scale Users**: To provide practical, easy-to-use tools that enhance the functionality of your Half Decent Scale for everyday tasks. 44 | 2. **For Developers**: To offer a real-world demonstration of Web Bluetooth and Web USB capabilities, encouraging exploration and innovation with these technologies. 45 | 46 | **These apps are ideal for:** 47 | 48 | * **Decent Espresso Machine and/or Half Decent Scale Owners**: Enhance your daily weighing and coffee preparation routines. 49 | * **Quality Control Professionals**: Streamline weighing processes, improve consistency, and simplify data collection. 50 | * **Developers & Tech Enthusiasts**: Explore a practical implementation of Web BLE and Web USB in an IoT context. 51 | 52 | *** 53 | 54 | ### For Developers: Technical Deep Dive 55 | 56 | * **All codes are available at**[ openscale repo. ](https://github.com/decentespresso/openscale/tree/main/web_apps) 57 | * **Foundation**: Built with standard HTML, CSS, and JavaScript. 58 | * **Styling**: [Tailwind CSS](https://tailwindcss.com/) is used for a utility-first approach to styling, ensuring a responsive and modern interface. (Note: While Tailwind is a CSS framework, the core logic remains in plain JavaScript, avoiding heavy JS frameworks.) 59 | * **Connectivity**: 60 | * **Web Bluetooth API (Web BLE)**: Enables wireless communication with the Half Decent Scale. [Learn more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API). 61 | * **Web USB API**: Provides an alternative wired connection method. [Learn more](https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API). 62 | * *Browser Compatibility*: Natively supported in Chrome (version 80+) and Edge (version 80+). **Not currently supported on iOS devices.** 63 | * **Data Storage**: Data such as readings and presets are stored locally in the browser's cache. Users can download their data as JSON or CSV files. 64 | 65 | *** 66 | 67 | ### Code Structure & Key Modules 68 | 69 | The codebase is designed to be understandable and adaptable. "Weigh and Save" and "Dosing Assistant" feature a modular structure, while "Quality Control Assistant" uses a monolithic structure for a potentially simpler overview of function interactions. 70 | 71 | Key JavaScript modules and their roles: 72 | 73 | * `scale.js`: Handles BLE protocol, communication with the Half Decent Scale (HDS), and core dosing mode functionality. 74 | * `constants.js`: Stores pre-programmed 10-byte messages for HDS communication and various threshold values. 75 | * `state-machine.js`: Implements the core logic for "Dosing Assistant" and "Quality Control Assistant" using a Finite State Machine model. 76 | * `export.js`: Manages the functionality for exporting weight readings and evaluation data as CSV or JSON files. 77 | * `presets.js`: Allows users in "Dosing Assistant" and "Quality Control Assistant" to save target weights as presets, cached locally by the browser. 78 | * `ui-controller.js`: Manages updates and changes to the HTML interface. 79 | * `modules/connection/` (in "Weigh and Save"): Contains the specific implementations for BLE and USB connection methods. 80 | 81 | For comprehensive details on the scale's communication protocols, refer to the [Programmer's Guide to the Half Decent Scale](https://decentespresso.com/docs/programmers_guide_to_the_half_decent_scale). 82 | 83 | *** 84 | 85 | ### Development Setup 86 | 87 | To get a local copy up and running, follow these simple steps: 88 | 89 | **Clone the repository:** 90 | 91 | `git clone https://github.com/decentespresso/openscale/tree/main/web_apps/` 92 | 93 | **Navigate into the project directory:** 94 | 95 | `bash cd dosing_assistant` 96 | 97 | **Serve the files with a local HTTP server:** 98 | 99 | Since these are web applications using Web BLE, they need to be served over https:// or [http://localhost](http://localhost/) 100 | 101 | You can use a simple Python HTTP server or any other local server you prefer. 102 | Using Python's http.server (Python 3): 103 | 104 | `bash python -m http.server 8000` 105 | 106 | Then, open your Chrome browser and navigate to 107 | [http://localhost:8000/decentscale_webweigh/](http://localhost:8000/decentscale_webweigh/) 108 | 109 | [http://localhost:8000/decentscale_qcweigh/](http://localhost:8000/decentscale_qcweigh/) 110 | 111 | Using Node.js http-server (if you have Node.js installed): 112 | 113 | `bash npm install -g http-server http-server -p 8000` 114 | 115 | Then, navigate to the respective app URLs as above. 116 | 117 | **This project is open source, and we encourage developers to explore, learn from, and contribute to the codebase.** 118 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/README.md: -------------------------------------------------------------------------------- 1 | **Simple Weigh & Save Web App for Decent Scales (USB-C & Bluetooth)** 2 | 3 | This compact web application provides a straightforward way to read weights from your **Half Decent Scale** and record them. Built with pure JavaScript, HTML, and CSS, it offers **flexible connectivity options** via both **Web Bluetooth Low Energy (Web BLE)** and **Web Serial API (USB-C)**. This allows you to easily log multiple measurements and export your data for future use. 4 | 5 | --- 6 | 7 | ## Live Demo 8 | 9 | Experience the app in action! It requires a **Chrome or Edge browser (version 70 or newer)** on a device with Bluetooth or a USB-C port (for serial connection): 10 | 11 | [https://decentespresso.com/support/scale/decentscale_weighandsave](https://decentespresso.com/support/scale/decentscale_weighandsave) 12 | 13 | --- 14 | 15 | ## Features 16 | 17 | * **Dual Connectivity:** Connect to your Half Decent Scale via either: 18 | * **Wireless Bluetooth (Web BLE):** For convenience and portability. 19 | * **Wired USB-C (Web Serial API):** For a potentially more stable and faster connection. 20 | * **Real-time Weight Display:** Get live weight readings from your Half Decent Scale in grams. 21 | * **Manual Measurement Saving:** Click a button to save the current stable weight reading. 22 | * **Measurement Logging:** Each saved reading is logged with: 23 | * A timestamp 24 | * The measured weight 25 | * **Local Data Storage:** Measuremenet is saved locally, make sure you download before you reload the page, otherwise it won't save. 26 | * **Flexible Data Export:** Easily download your collected data in: 27 | * **CSV (Comma Separated Values):** Great for spreadsheet analysis. 28 | * **JSON (JavaScript Object Notation):** Ideal for programmatic use or integration with other systems. 29 | * **No Frameworks:** Built purely with native JavaScript, HTML, and CSS for a lightweight and transparent codebase. 30 | 31 | --- 32 | 33 | ## How to Use 34 | 35 | 1. **Open in Browser:** Navigate to the live demo link provided above using a Chrome or Edge browser (version 70 or newer) on a device with Bluetooth or a USB-C port. 36 | 2. **Ensure Scale is Ready:** Turn on your Half Decent Scale. 37 | 3. **Choose Connection Method:** 38 | * **For Bluetooth:** Ensure your scale is within Bluetooth range. Click the **"Connect via Bluetooth"** button. A browser prompt will appear; select your "Decent Scale" from the list and click "Pair". 39 | * **For USB-C:** Connect your scale to your computer via a USB-C cable. Click the **"Connect via USB-C"** button. A browser prompt will appear; select your "Decent Scale" serial port from the list and click "Connect". 40 | 4. **Weigh and Save:** 41 | * Place an object on the scale. 42 | * Once the weight stabilizes, click the **"Save Reading"** button. The reading will be added to the history list below. 43 | 5. **Timer:** Once you click start it will record weight at each interval , and calculate rate per each interval. 44 | 6. **Export Data:** After collecting your measurements, click **"CSV"** or **"JSON"** to download your collected data. 45 | 7. **Zero Scale:** Use the **"Zero Scale"** button to tare the scale at any time. 46 | 47 | --- 48 | 49 | ## Technical Details 50 | 51 | The `weigh_save` application leverages advanced browser APIs for hardware interaction: 52 | 53 | * **Web BLE Communication:** 54 | * The app connects to the Half Decent Scale using its **service UUID `0000fff0-0000-1000-8000-00805f9b34fb`**. 55 | * **Read Characteristic:** `0000fff4-0000-1000-8000-00805f9b34fb` is used to receive weight data notifications. 56 | * **Write Characteristic:** `000036f5-0000-1000-8000-00805f9b34fb` is used to send commands like `tare` to the scale. 57 | * A **command queue (`commandQueue`)** ensures that commands sent to the scale are executed sequentially. 58 | * **Web Serial API Communication (USB-C):** 59 | * Utilizes `navigator.serial` to open a serial port connection to the Decent Scale when connected via USB-C. 60 | * Reads incoming data from the serial port to get weight readings. 61 | * Sends commands (like tare) over the serial port. 62 | * **Weight Stability:** Readings are typically saved when the weight is stable, determined by minimal fluctuation over a short period. 63 | * **Local Storage for Data:** All collected readings are stored in `localStorage` under the key `'decentScaleWeighSaveData'`, allowing them to persist across browser sessions. 64 | * **Data Export:** The app dynamically generates CSV and JSON files from the stored data for download. 65 | 66 | --- 67 | 68 | ## Development Setup 69 | 70 | To explore, modify, or contribute to this application: 71 | 72 | 1. **Download all files in this folder**. 73 | 2. **Navigate to the folder** in your terminal: 74 | 75 | ```bash 76 | cd openscale/web_apps/Weigh_Save 77 | ``` 78 | 79 | 3. **Serve the files locally over HTTP/HTTPS:** 80 | Web BLE and Web Serial API both require your application to be served over `https://` or `http://localhost`. You can use a simple local web server: 81 | 82 | **Using Python's `http.server` (Python 3, recommended):** 83 | 84 | ```bash 85 | python -m http.server 8000 86 | ``` 87 | 88 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 89 | 90 | **Using Node.js `http-server` (if you have Node.js installed):** 91 | 92 | ```bash 93 | npm install -g http-server 94 | http-server -p 8000 95 | ``` 96 | 97 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 98 | 99 | --- 100 | 101 | ## Code Structure Highlights 102 | 103 | The core logic is likely split across files to handle different connection methods, but the overall structure would include: 104 | 105 | * **`DecentScale` Class (or similar):** A central class managing the scale's state, data handling, and UI updates. 106 | * **Bluetooth-specific JS file (e.g., `ble_jvs_weigh_save.js`):** Handles Web BLE connection, characteristic reading/writing, and notification handling. 107 | * **Serial-specific JS file (e.g., `serial_api_weigh_save.js`):** Handles Web Serial API connection, reading data from the serial port, and writing commands. 108 | * **Shared logic:** Functions for saving/loading data to `localStorage`, displaying readings, and exporting data. 109 | 110 | --- 111 | 112 | ## Contributing 113 | 114 | We welcome community contributions! If you have suggestions, bug reports, or want to contribute code, please refer to the main [Programmers guide to the Half Decent Scale](https://decentespresso.com/docs/programmers_guide_to_the_half_decent_scale). 115 | --- 116 | 117 | ## License 118 | 119 | This project is licensed under the **GNU General Public License v3.0**. See the `LICENSE` file in the main repository for more details. 120 | 121 | --- 122 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/main.js: -------------------------------------------------------------------------------- 1 | import { DecentScale } from './modules/scale.js'; 2 | import { UIController } from './modules/ui-controller.js'; 3 | import { TimerManager } from './modules/timer.js'; 4 | import { Led } from './modules/led.js'; 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | // Initialize components in correct order 8 | const timerManager = new TimerManager(); 9 | const ui = new UIController(timerManager); 10 | timerManager.setUIController(ui); 11 | const scale = new DecentScale(ui, timerManager); 12 | const led = new Led(scale); 13 | 14 | // Connect button 15 | document.getElementById('connect')?.addEventListener('click', async () => { 16 | if (scale.activeConnection) { 17 | await scale.disconnect(); 18 | } else { 19 | const connectionType = document.getElementById('connectionType').value; 20 | await scale.connect(connectionType); 21 | } 22 | }); 23 | 24 | // Timer control 25 | document.getElementById('toggleTimer')?.addEventListener('click', () => { 26 | ui.toggleTimer(); 27 | }); 28 | 29 | // Other controls 30 | document.getElementById('tareButton')?.addEventListener('click', () => scale.tare()); 31 | document.getElementById('toggleLed')?.addEventListener('click', () => led.toggleLed()); 32 | document.getElementById('exportCSV')?.addEventListener('click', () => scale.exportToCSV()); 33 | document.getElementById('exportJSON')?.addEventListener('click', () => scale.exportToJSON()); 34 | }); 35 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/connections/base-connection.js: -------------------------------------------------------------------------------- 1 | import { DebugLogger as Debug } from '../debug-logger.js'; 2 | 3 | export class BaseConnection { 4 | constructor(uiController) { 5 | this.ui = uiController; 6 | this.isConnected = false; 7 | this.notificationHandler = null; 8 | this.heartbeatInterval = null; 9 | } 10 | 11 | setNotificationHandler(handler) { 12 | this.notificationHandler = handler; 13 | } 14 | //NOTE: heartbeat is older android version to stay connected with Decent scale. See programmer's guide for more details. 15 | // startHeartbeat() { 16 | // Debug.log('HEARTBEAT', 'Starting heartbeat'); 17 | // if (this.heartbeatInterval) { 18 | // clearInterval(this.heartbeatInterval); 19 | // } 20 | 21 | // // Send initial heartbeat immediately 22 | // this.sendHeartbeat(); 23 | 24 | // // Then set up interval 25 | // this.heartbeatInterval = setInterval(() => { 26 | // this.sendHeartbeat(); 27 | // }, 4000); // 4 seconds interval 28 | // } 29 | 30 | // async sendHeartbeat() { 31 | // try { 32 | // await this.sendCommand(new Uint8Array([0x03, 0x0a, 0x03, 0xff, 0xff, 0x00, 0x0a])); 33 | // Debug.log('HEARTBEAT', 'Heartbeat sent successfully'); 34 | // } catch (error) { 35 | // Debug.log('HEARTBEAT_ERROR', 'Failed to send heartbeat:', error); 36 | // // If heartbeat fails, attempt to stop it 37 | // this.stopHeartbeat(); 38 | // } 39 | // } 40 | 41 | // stopHeartbeat() { 42 | // Debug.log('HEARTBEAT', 'Stopping heartbeat'); 43 | // if (this.heartbeatInterval) { 44 | // clearInterval(this.heartbeatInterval); 45 | // this.heartbeatInterval = null; 46 | // } 47 | // } 48 | 49 | async connect() { throw new Error('Must implement connect()'); } 50 | async disconnect() { throw new Error('Must implement disconnect()'); } 51 | async sendCommand(data) { throw new Error('Must implement sendCommand()'); } 52 | } 53 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/connections/bluetooth-connection.js: -------------------------------------------------------------------------------- 1 | import { BaseConnection } from './base-connection.js'; 2 | import { SCALE_CONSTANTS } from '../constants.js'; 3 | import { DebugLogger as Debug } from '../debug-logger.js'; 4 | 5 | export class BluetoothConnection extends BaseConnection { 6 | constructor(uiController) { 7 | super(uiController); 8 | this.device = null; 9 | this.server = null; 10 | this.readCharacteristic = null; 11 | this.writeCharacteristic = null; 12 | } 13 | 14 | async connect() { 15 | try { 16 | this.ui.updateStatus('Scanning...'); 17 | this.device = await this._findAddress(); 18 | 19 | if (!this.device) { 20 | throw new Error('Decent Scale not found'); 21 | } 22 | 23 | this.ui.updateStatus('Connecting...'); 24 | await this._connect(); 25 | 26 | // Make sure we set status AFTER successful connection 27 | this.isConnected = true; 28 | // this.startHeartbeat(); 29 | // Debug.log('BLE', 'Started heartbeat'); 30 | this.ui.updateStatus('Connected to Decent Scale'); // Updated status message 31 | this.ui.updateConnectButton('Disconnect'); 32 | Debug.log('Connectbutton', 'ButtonUI update to Disconnect'); 33 | return true; 34 | } catch (error) { 35 | Debug.log('BLE_ERROR', 'Connection error:', error); 36 | 37 | // Handle user cancellation specifically 38 | if (error.name === 'NotFoundError') { 39 | this.ui.updateStatus('Connection cancelled'); 40 | } else { 41 | this.ui.updateStatus(`Connection failed: ${error.message}`); 42 | } 43 | 44 | this.isConnected = false; 45 | return false; 46 | } 47 | } 48 | 49 | async _findAddress() { 50 | const device = await navigator.bluetooth.requestDevice({ 51 | filters: [{ name: 'Decent Scale' }], 52 | optionalServices: [SCALE_CONSTANTS.CHAR_READ] 53 | }); 54 | return device; 55 | } 56 | 57 | async _connect() { 58 | try { 59 | this.server = await this.device.gatt.connect(); 60 | const service = await this.server.getPrimaryService(SCALE_CONSTANTS.CHAR_READ); 61 | 62 | this.readCharacteristic = await service.getCharacteristic(SCALE_CONSTANTS.READ_CHARACTERISTIC); 63 | this.writeCharacteristic = await service.getCharacteristic(SCALE_CONSTANTS.WRITE_CHARACTERISTIC); 64 | 65 | await this._enableNotification(); 66 | Debug.log('BLE', 'Connected and characteristics initialized'); 67 | return true; 68 | } catch (error) { 69 | Debug.log('BLE_ERROR', '_connect failed:', error); 70 | throw error; // Propagate error to main connect method 71 | } 72 | } 73 | 74 | async _enableNotification() { 75 | await this.readCharacteristic.startNotifications(); 76 | // Use arrow function to preserve 'this' context and handle events properly 77 | this.readCharacteristic.addEventListener('characteristicvaluechanged', 78 | (event) => { 79 | if (typeof this.notificationHandler === 'function') { 80 | this.notificationHandler(event); 81 | } else { 82 | Debug.log('BLE_ERROR', 'Notification handler not set or invalid'); 83 | } 84 | } 85 | ); 86 | Debug.log('BLE', 'Notifications enabled'); 87 | } 88 | async _disable_notification() { 89 | if (this.readCharacteristic) { 90 | await this.readCharacteristic.stopNotifications(); 91 | // this.readCharacteristic.removeEventListener('characteristicvaluechanged', this.notification_handler.bind(this)); 92 | } 93 | } 94 | async disconnect() { 95 | // this.stopHeartbeat(); 96 | await this._disconnect(); 97 | 98 | this.isConnected = false; 99 | this.ui.updateStatus('Bluetooth Disconnected'); 100 | this.ui.updateConnectButton('Connect to Scale'); // Reset button to Connect 101 | } 102 | async _disconnect() { 103 | if (this.server) { 104 | try { 105 | if (this.readCharacteristic) { 106 | try { 107 | await this._disable_notification(); 108 | } catch (err) { 109 | Debug.log('BLE_WARN', 'Failed to stop notifications (may already be disconnected):', err); 110 | } 111 | } 112 | await this.server.disconnect(); 113 | } catch (err) { 114 | Debug.log('BLE_WARN', 'Error during server disconnect:', err); 115 | } 116 | } 117 | this.device = null; 118 | this.server = null; 119 | this.readCharacteristic = null; 120 | this.writeCharacteristic = null; 121 | if (this.ui) this.ui.updateStatus('Not connected'); 122 | } 123 | async sendCommand(data) { 124 | if (!this.isConnected) throw new Error('Device not connected'); 125 | await this.writeCharacteristic.writeValue(new Uint8Array(data)); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/connections/usb-connection.js: -------------------------------------------------------------------------------- 1 | import { BaseConnection } from './base-connection.js'; 2 | import { DebugLogger as Debug } from '../debug-logger.js'; 3 | //This is the module to make use of WEB USB api to establish connection with half decent scale, and pass data to other modules similar to BLE connection. 4 | export class USBConnection extends BaseConnection { 5 | constructor(uiController) { 6 | super(uiController); 7 | this.device = null; 8 | this.interfaceNumber = 0; 9 | this.pollInterval = null; 10 | this.endpointIn = null; // Only use IN endpoint for both in/out 11 | this.VENDOR_ID = 0x1a86; // CH340 vendor ID 12 | this.PRODUCT_ID = 0x7523; // CH340 product ID 13 | this.BAUD_RATE = 115200; // ESP32-S3 baud rate 14 | this.dataBuffer = new Uint8Array(0); 15 | this._checkUSBSupport(); 16 | } 17 | 18 | _checkUSBSupport() { 19 | if (!navigator.usb) { 20 | console.error('WebUSB not available'); 21 | return false; 22 | } 23 | console.log('WebUSB is available'); 24 | return true; 25 | } 26 | 27 | async _initializeEndpoints() { 28 | const interfaceObj = this.device.configuration.interfaces[0]; 29 | const alternate = interfaceObj.alternate; 30 | Debug.log('USB', 'Available endpoints:', alternate.endpoints); 31 | 32 | // Find the first IN endpoint (just like your HTML) 33 | const inEp = alternate.endpoints.find(e => e.direction === 'in'); 34 | if (!inEp) throw new Error('No IN endpoint found'); 35 | this.endpointIn = inEp.endpointNumber; 36 | Debug.log('USB', `Using IN endpoint: 0x${this.endpointIn.toString(16)}`); 37 | } 38 | 39 | async _initializeDevice() { 40 | try { 41 | await this.device.open(); 42 | Debug.log('USB', 'Device opened'); 43 | 44 | if (this.device.configuration === null) { 45 | await this.device.selectConfiguration(1); 46 | } 47 | 48 | this.interfaceNumber = this.device.configuration.interfaces[0].interfaceNumber; 49 | Debug.log('USB', `Claiming interface ${this.interfaceNumber}`); 50 | 51 | await this.device.claimInterface(this.interfaceNumber); 52 | 53 | await this._initCH340(); 54 | Debug.log('USB', 'CH340 Initialized'); 55 | 56 | await this._initializeEndpoints(); 57 | 58 | // --- Send command to enable weight output (use IN endpoint, like HTML) --- 59 | const enableWeightCmd = new Uint8Array([0x03, 0x20, 0x01]); 60 | await this.device.transferOut(this.endpointIn, enableWeightCmd); 61 | Debug.log('USB', 'Sent enable weight command:', enableWeightCmd); 62 | 63 | this.startPolling(); 64 | Debug.log('USB', 'Device initialized successfully'); 65 | return true; 66 | } catch (error) { 67 | Debug.log('USB_ERROR', 'Device initialization failed:', error); 68 | if (this.device && this.interfaceNumber != null) { 69 | try { await this.device.releaseInterface(this.interfaceNumber); } catch(e) {} 70 | } 71 | if (this.device && this.device.opened) { 72 | try { await this.device.close(); } catch(e) {} 73 | } 74 | this.device = null; 75 | return false; 76 | } 77 | } 78 | 79 | async connect() { 80 | try { 81 | if (!this._checkUSBSupport()) { 82 | throw new Error('WebUSB not supported'); 83 | } 84 | 85 | Debug.log('USB', 'Requesting USB device...'); 86 | try { 87 | this.device = await navigator.usb.requestDevice({ 88 | filters: [] 89 | }); 90 | } catch (error) { 91 | if (error.name === 'NotFoundError') { 92 | Debug.log('USB', 'Device selection cancelled'); 93 | this.ui.updateStatus('Connection cancelled'); 94 | return false; 95 | } 96 | throw error; 97 | } 98 | 99 | const initialized = await this._initializeDevice(); 100 | if (!initialized) { 101 | throw new Error('Failed to initialize device'); 102 | } 103 | 104 | this.isConnected = true; 105 | this.ui.updateStatus('USB Connected'); 106 | this.ui.updateConnectButton('Disconnect'); 107 | //this.startHeartbeat(); 108 | return true; 109 | 110 | } catch (error) { 111 | Debug.log('USB_ERROR', 'Connection failed:', error); 112 | this.ui.updateStatus(`USB Connection failed: ${error.message}`); 113 | return false; 114 | } 115 | } 116 | 117 | async _initCH340() { 118 | // CH340 specific initialization sequence 119 | await this.device.controlTransferOut({ 120 | requestType: 'vendor', 121 | recipient: 'device', 122 | request: 0x9A, 123 | value: 0x2518, 124 | index: 0x0000 125 | }); 126 | 127 | // Set baud rate 128 | const encodedBaud = this._encodeCH340BaudRate(this.BAUD_RATE); 129 | await this.device.controlTransferOut({ 130 | requestType: 'vendor', 131 | recipient: 'device', 132 | request: 0x9A, 133 | value: 0x2518, 134 | index: encodedBaud 135 | }); 136 | 137 | // Set line control 138 | await this.device.controlTransferOut({ 139 | requestType: 'vendor', 140 | recipient: 'device', 141 | request: 0x9A, 142 | value: 0x2518, 143 | index: 0x00C3 144 | }); 145 | } 146 | 147 | _encodeCH340BaudRate(baudRate) { 148 | const CH340_BAUDBASE_FACTOR = 1532620800; 149 | const divisor = Math.floor(CH340_BAUDBASE_FACTOR / baudRate); 150 | return ((divisor & 0xFF00) | 0x00C0); 151 | } 152 | 153 | startPolling() { 154 | Debug.log('USB', 'Starting USB polling'); 155 | let errorCount = 0; 156 | this.dataBuffer = new Uint8Array(0); 157 | 158 | if (this.pollInterval) { 159 | clearInterval(this.pollInterval); 160 | this.pollInterval = null; 161 | } 162 | 163 | this.pollInterval = setInterval(async () => { 164 | if (!this.device || !this.endpointIn || !this.device.opened) { 165 | Debug.log('USB_WARN', 'Polling attempted but device is not ready or open.'); 166 | this.stopPolling(); 167 | return; 168 | } 169 | 170 | try { 171 | const result = await this.device.transferIn(this.endpointIn, 64); 172 | if (result.status === 'ok' && result.data && result.data.byteLength > 0) { 173 | const newData = new Uint8Array(result.data.buffer); 174 | this.dataBuffer = this._appendBuffer(this.dataBuffer, newData); 175 | this._processBuffer(); 176 | errorCount = 0; 177 | } else if (result.status !== 'ok') { 178 | Debug.log('USB_WARN', `USB transferIn status: ${result.status}`); 179 | if(result.status === 'stall') { 180 | Debug.log('USB', 'Attempting to clear IN endpoint stall...'); 181 | try { 182 | await this.device.clearHalt('in', this.endpointIn); 183 | Debug.log('USB', 'Cleared IN endpoint stall successfully.'); 184 | } catch (clearError) { 185 | Debug.log('USB_ERROR', 'Failed to clear IN endpoint stall:', clearError); 186 | errorCount++; 187 | } 188 | } else { 189 | errorCount++; 190 | } 191 | } 192 | } catch (error) { 193 | errorCount++; 194 | Debug.log('USB_ERROR', `Polling error (Count: ${errorCount}):`, error); 195 | if (error.message.includes("disconnected") || error.name === 'NetworkError' || error.message.includes("device unavailable") || error.message.includes("No device selected")) { 196 | Debug.log('USB_ERROR', 'Device disconnected detected during polling.'); 197 | this.stopPolling(); 198 | this.isConnected = false; 199 | this.device = null; 200 | if (this.ui) { 201 | this.ui.updateStatus('USB Disconnected (Polling Error)'); 202 | this.ui.updateConnectButton('Connect to Scale'); 203 | } 204 | return; 205 | } else if (errorCount > 5) { 206 | Debug.log('USB_ERROR', `Too many consecutive polling errors (${errorCount}), attempting disconnect...`); 207 | this.stopPolling(); 208 | await this.disconnect(); 209 | return; 210 | } 211 | } 212 | }, 100); 213 | } 214 | 215 | stopPolling() { 216 | if (this.pollInterval) { 217 | Debug.log('USB', 'Stopping USB polling'); 218 | clearInterval(this.pollInterval); 219 | this.pollInterval = null; 220 | } 221 | } 222 | 223 | _appendBuffer(buffer1, buffer2) { 224 | let tmp = new Uint8Array(buffer1.length + buffer2.length); 225 | tmp.set(buffer1, 0); 226 | tmp.set(buffer2, buffer1.length); 227 | return tmp; 228 | } 229 | 230 | // Process 7-byte packets starting with 0x03 0xCE or 0x03 0xCA 231 | _processBuffer() { 232 | if (this.dataBuffer.length > 0) { 233 | Debug.log('USB_DATA', 'Raw buffer:', Array.from(this.dataBuffer).map(b => '0x' + b.toString(16)).join(' ')); 234 | } 235 | while (this.dataBuffer.length >= 7) { 236 | if ( 237 | this.dataBuffer[0] === 0x03 && 238 | (this.dataBuffer[1] === 0xCE || this.dataBuffer[1] === 0xCA) 239 | ) { 240 | const packet = this.dataBuffer.slice(0, 7); 241 | Debug.log('USB_DATA', '7-byte packet:', packet); 242 | 243 | // Notify as DataView for compatibility 244 | if (this.notificationHandler) { 245 | const dataView = new DataView(packet.buffer, packet.byteOffset, packet.byteLength); 246 | this.notificationHandler({ 247 | target: { 248 | value: dataView 249 | } 250 | }); 251 | } 252 | 253 | // Remove processed packet 254 | this.dataBuffer = this.dataBuffer.slice(7); 255 | } else { 256 | // Skip one byte and try again (resync) 257 | Debug.log('USB_DATA', 'Skipping byte (not a valid packet start):', this.dataBuffer[0]); 258 | this.dataBuffer = this.dataBuffer.slice(1); 259 | } 260 | } 261 | } 262 | 263 | async disconnect() { 264 | //this.stopHeartbeat(); 265 | this.stopPolling(); 266 | if (this.device) { 267 | try { 268 | await this.device.releaseInterface(this.interfaceNumber); 269 | await this.device.close(); 270 | this.device = null; 271 | this.isConnected = false; 272 | this.ui.updateStatus('USB Disconnected'); 273 | this.ui.updateConnectButton('Connect to Scale'); 274 | Debug.log('USB', 'Device disconnected'); 275 | } catch (error) { 276 | Debug.log('USB_ERROR', 'Disconnect error:', error); 277 | console.error('USB Disconnect error:', error); 278 | this.ui.updateStatus(`USB Disconnect failed: ${error.message}`); 279 | } 280 | } 281 | } 282 | 283 | // Send command using IN endpoint (like your HTML) 284 | async sendCommand(data) { 285 | if (!this.device || !this.endpointIn) { 286 | throw new Error('Device not properly initialized'); 287 | } 288 | try { 289 | Debug.log('USB_CMD', `Sending command to endpoint 0x${this.endpointIn.toString(16)}:`, Debug.hexView(data)); 290 | const result = await this.device.transferOut(this.endpointIn, data); 291 | Debug.log('USB_CMD', 'Command sent:', result.status); 292 | return result; 293 | } catch (error) { 294 | Debug.log('USB_ERROR', 'Send command error:', error); 295 | console.error('USB send command error:', error); 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/constants.js: -------------------------------------------------------------------------------- 1 | export const SCALE_CONSTANTS = { 2 | CHAR_READ: '0000fff0-0000-1000-8000-00805f9b34fb', 3 | CHAR_WRITE: '000036f5-0000-1000-8000-00805f9b34fb', 4 | READ_CHARACTERISTIC: '0000fff4-0000-1000-8000-00805f9b34fb', 5 | WRITE_CHARACTERISTIC: '000036f5-0000-1000-8000-00805f9b34fb', //protocol values to talk to Decent Scale. 6 | 7 | FSM_STATES: { 8 | MEASURING: 'measuring', 9 | REMOVAL_PENDING: 'removal_pending', 10 | WAITING_FOR_NEXT: 'waiting_for_next', 11 | CONTAINER_REMOVED: 'container_removed' 12 | }, 13 | WEIGHT_THRESHOLDS: { 14 | MINIMUM: -0.4, // Threshold for container removal detection 15 | CONTAINER_TOLERANCE: 0.6, // Increased tolerance for container replacement 16 | ZERO_TOLERANCE: 0.2 // Tolerance for considering weight as "zero" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/debug-logger.js: -------------------------------------------------------------------------------- 1 | export class DebugLogger {//debug helper 2 | static DEBUG = true; 3 | 4 | static log(category, message, data = null) { 5 | if (!this.DEBUG) return; 6 | 7 | const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); 8 | const logMessage = `[${timestamp}] [${category}] ${message}`; 9 | 10 | if (data) { 11 | console.log(logMessage, data); 12 | } else { 13 | console.log(logMessage); 14 | } 15 | } 16 | 17 | static hexView(data) { 18 | if (!this.DEBUG || !data) return; 19 | 20 | if (data instanceof Uint8Array) { 21 | return Array.from(data).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '); 22 | } 23 | return 'Not a Uint8Array'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/dosing.js: -------------------------------------------------------------------------------- 1 | import { SCALE_CONSTANTS } from './modules/constants.js'; 2 | //this is a module which can be deployed if you would like to have dosing feature on your app. 3 | export class DosingManager { 4 | constructor(uiController) { 5 | this.ui = uiController; 6 | this.dosingMode = false; 7 | this.settings = { 8 | targetWeight: 0, 9 | lowThreshold: 0, 10 | highThreshold: 0 11 | }; 12 | this.currentProgress = 0; 13 | this.readingCount = 0; 14 | this.weightData = []; 15 | this.weightReadings = []; 16 | this.containerWeight = 0; 17 | this.waitingForContainerWeight = false; 18 | } 19 | 20 | startDosing() { 21 | this.dosingMode = true; 22 | this.settings = { 23 | targetWeight: parseFloat(this.ui.getTargetWeight()), 24 | lowThreshold: parseFloat(this.ui.getLowThreshold()), 25 | highThreshold: parseFloat(this.ui.getHighThreshold()) 26 | }; 27 | this.currentProgress = 0; 28 | this.waitingForContainerWeight = true; 29 | 30 | // Update UI 31 | this.ui.updateProgressBar(0); 32 | this.ui.updateProgressBarColor('default'); 33 | this.ui.updateGuidance('Place container on scale and click set container weight.'); 34 | this.ui.showSetContainerWeightButton(); 35 | 36 | console.log('Dosing started with settings:', this.settings); 37 | } 38 | 39 | stopDosing() { 40 | this.dosingMode = false; 41 | this.currentProgress = 0; 42 | this.waitingForContainerWeight = false; 43 | 44 | // Update UI 45 | this.ui.updateProgressBar(0); 46 | this.ui.updateProgressBarColor('default'); 47 | this.ui.updateGuidance('Dosing stopped'); 48 | this.ui.hideSetContainerWeightButton(); 49 | 50 | console.log('Dosing stopped'); 51 | } 52 | 53 | handleWeightUpdate(weight, netWeight) { 54 | if (!this.dosingMode) return; 55 | 56 | // If waiting for container weight, don't update progress 57 | if (this.waitingForContainerWeight) { 58 | this.ui.updateGuidance('Set container weight to continue'); 59 | return; 60 | } 61 | 62 | // Calculate progress based on net weight 63 | const target = this.settings.targetWeight; 64 | const progress = Math.min((netWeight / target) * 100, 100); 65 | this.currentProgress = progress; 66 | 67 | // Update UI 68 | this.ui.updateProgressBar(progress); 69 | this.updateProgressColor(netWeight); 70 | 71 | console.log('Weight update:', { 72 | netWeight, 73 | progress: progress.toFixed(1) + '%', 74 | target 75 | }); 76 | } 77 | 78 | updateProgressColor(netWeight) { 79 | if (netWeight >= this.settings.lowThreshold && 80 | netWeight <= this.settings.highThreshold) { 81 | this.ui.updateProgressBarColor('success'); 82 | this.ui.updateGuidance('Perfect! Remove container'); 83 | } else if (netWeight > this.settings.highThreshold) { 84 | this.ui.updateProgressBarColor('error'); 85 | this.ui.updateGuidance(`Remove ${(netWeight - this.settings.targetWeight).toFixed(1)}g`); 86 | } else { 87 | this.ui.updateProgressBarColor('warning'); 88 | this.ui.updateGuidance(`Add ${(this.settings.targetWeight - netWeight).toFixed(1)}g more`); 89 | } 90 | } 91 | 92 | setContainerWeight(weight) { 93 | this.containerWeight = weight; 94 | this.waitingForContainerWeight = false; 95 | this.ui.updateGuidance('Container weight set. Ready for dosing!'); 96 | this.ui.hideSetContainerWeightButton(); 97 | console.log('Container weight set to:', weight); 98 | } 99 | 100 | saveMeasurement(weight) { 101 | this.readingCount++; 102 | const reading = { 103 | readings: this.readingCount, 104 | timestamp: new Date().toLocaleString('en-GB'), 105 | weight: weight.toFixed(1), 106 | target: this.settings.targetWeight, 107 | lowThreshold: this.settings.lowThreshold, 108 | highThreshold: this.settings.highThreshold, 109 | status: this.evaluateWeight(weight) 110 | }; 111 | 112 | this.weightData.push(reading); 113 | this.weightReadings.push( 114 | `${reading.readings}. ${reading.timestamp}: ${reading.weight}g / ${reading.target}g - ${reading.status.toUpperCase()}` 115 | ); 116 | 117 | this.ui.displayWeightReadings(this.weightReadings); 118 | return reading; 119 | } 120 | 121 | evaluateWeight(weight) { 122 | return (weight >= this.settings.lowThreshold && 123 | weight <= this.settings.highThreshold) ? 'pass' : 'fail'; 124 | } 125 | 126 | isDosing() { 127 | return this.dosingMode; 128 | } 129 | 130 | isWaitingForContainer() { 131 | return this.waitingForContainerWeight; 132 | } 133 | 134 | getWeightData() { 135 | return this.weightData; 136 | } 137 | 138 | getWeightReadings() { 139 | return this.weightReadings; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/export.js: -------------------------------------------------------------------------------- 1 | export class DataExport { // export methods : JSON and CSV functions below. 2 | static exportToCSV(weightData) { 3 | const headers = ['Timestamp', 'Weight (g)', 'Elapsed Time (s)']; 4 | const csvContent = [ 5 | headers.join(','), 6 | ...weightData.map(reading => [ 7 | reading.timestamp, 8 | reading.weight, 9 | reading.elapsedTime 10 | ].join(',')) 11 | ].join('\n'); 12 | 13 | return { 14 | content: csvContent, 15 | filename: `scale-readings-${new Date().toISOString().split('T')[0]}.csv`, 16 | type: 'text/csv' 17 | }; 18 | } 19 | 20 | static exportToJSON(weightData) { 21 | const jsonOutput = { 22 | export_date: new Date().toISOString(), 23 | total_readings: weightData.length, 24 | readings: weightData 25 | }; 26 | 27 | return { 28 | content: JSON.stringify(jsonOutput, null, 2), 29 | filename: `scale-readings-${new Date().toISOString().split('T')[0]}.json`, 30 | type: 'application/json' 31 | }; 32 | } 33 | 34 | static downloadFile(content, filename, contentType) { 35 | const blob = new Blob([content], { type: contentType }); 36 | const url = URL.createObjectURL(blob); 37 | const a = document.createElement('a'); 38 | a.href = url; 39 | a.download = filename; 40 | document.body.appendChild(a); 41 | a.click(); 42 | document.body.removeChild(a); 43 | URL.revokeObjectURL(url); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/led.js: -------------------------------------------------------------------------------- 1 | export class Led { // methods to switch on and off LED panels on Decent scale. 2 | constructor(scale) { 3 | this.ledOn = false; 4 | this.scale = scale; 5 | this.toggleButton = document.getElementById('toggleLed'); 6 | this.updateButtonText(); 7 | } 8 | 9 | async _led_on() { 10 | console.log('Turning LED on...'); 11 | await this.scale._send([0x03, 0x0A, 0x01, 0x01, 0x00, 0x00, 0x09]); 12 | this.ledOn = true; 13 | this.updateButtonText(); 14 | console.log('LED should be on, state:', this.ledOn); 15 | } 16 | 17 | async _led_off() { 18 | console.log('Turning LED off...'); 19 | await this.scale._send([0x03, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x09]); 20 | this.ledOn = false; 21 | this.updateButtonText(); 22 | console.log('LED should be off, state:', this.ledOn); 23 | } 24 | 25 | updateButtonText() { 26 | if (this.toggleButton) { 27 | if (this.ledOn){this.toggleButton.textContent ='LED OFF';} 28 | else{this.toggleButton.textContent ='LED ON';} 29 | 30 | } 31 | } 32 | 33 | async toggleLed() { 34 | try { 35 | console.log('Toggle LED requested, current state:', this.ledOn); 36 | if (this.ledOn) { 37 | await this.scale.executeCommand(() => this._led_off()); 38 | } else { 39 | await this.scale.executeCommand(() => this._led_on()); 40 | } 41 | } catch (error) { 42 | console.error('Toggle LED error:', error); 43 | this.scale.statusDisplay.textContent = `Status: LED Toggle Error - ${error.message}`; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/presets.js: -------------------------------------------------------------------------------- 1 | export class PresetManager { 2 | constructor() { 3 | this.scale = null; 4 | } 5 | 6 | init(scale) { 7 | this.scale = scale; 8 | } 9 | 10 | setupPresetHandlers() { 11 | console.log('setupPresetHandlers() is being called'); 12 | const presetSelect = document.getElementById('presetSelect'); 13 | 14 | if (presetSelect) { 15 | presetSelect.addEventListener('change', (e) => { 16 | const selectedValue = e.target.value; 17 | console.log('Preset selected:', selectedValue); 18 | 19 | if (selectedValue === 'save_preset') { 20 | console.log('Save preset option selected'); 21 | this.saveCurrentPreset(); 22 | e.target.value = ''; // Reset dropdown after saving 23 | } else if (selectedValue) { 24 | this.loadPreset(selectedValue); 25 | } 26 | }); 27 | } else { 28 | console.error('Preset select element not found'); 29 | } 30 | } 31 | 32 | saveCurrentPreset() { 33 | const objectName = document.getElementById('objectName').value.trim(); 34 | console.log('Attempting to save preset for:', objectName); 35 | 36 | if (!objectName) { 37 | console.warn('Save preset failed: No object name provided'); 38 | alert('Please enter an object name'); 39 | return; 40 | } 41 | 42 | const targetWeightInput = document.getElementById('targetWeight'); 43 | const highThresholdInput = document.getElementById('highThreshold'); 44 | const lowThresholdInput = document.getElementById('lowThreshold'); 45 | 46 | if (!targetWeightInput || !highThresholdInput || !lowThresholdInput) { 47 | console.error('One or more input elements not found'); 48 | return; 49 | } 50 | 51 | const preset = { 52 | name: objectName, 53 | settings: { 54 | targetWeight: parseFloat(targetWeightInput.value), 55 | highThreshold: parseFloat(highThresholdInput.value), 56 | lowThreshold: parseFloat(lowThresholdInput.value), 57 | } 58 | }; 59 | 60 | console.log('Saving preset with settings:', preset); 61 | this.savePreset(preset); 62 | this.updatePresetList(); 63 | console.log('Preset saved successfully:', preset); 64 | alert(`Object "${objectName}" saved successfully`); 65 | } 66 | 67 | savePreset(preset) { 68 | const presets = this.getPresets(); 69 | presets[preset.name] = preset; 70 | localStorage.setItem('decentScalePresets', JSON.stringify(presets)); 71 | } 72 | 73 | getPresets() { 74 | const presetsJson = localStorage.getItem('decentScalePresets'); 75 | return presetsJson ? JSON.parse(presetsJson) : {}; 76 | } 77 | 78 | loadPreset(name) { 79 | console.log('loadPreset function called with name:', name); 80 | const preset = this.getPreset(name); 81 | 82 | if (!preset || !preset.settings) { 83 | console.warn(`Preset "${name}" not found or has invalid settings.`); 84 | return; 85 | } 86 | 87 | document.getElementById('objectName').value = name; 88 | document.getElementById('targetWeight').value = preset.settings.targetWeight; 89 | document.getElementById('lowThreshold').value = preset.settings.lowThreshold; 90 | document.getElementById('highThreshold').value = preset.settings.highThreshold; 91 | localStorage.setItem('lastUsedPreset', name); 92 | } 93 | 94 | getPreset(name) { 95 | const presets = this.getPresets(); 96 | return presets[name]; 97 | } 98 | 99 | loadPresets() { 100 | const presets = this.getPresets(); 101 | const presetSelect = document.getElementById('presetSelect'); 102 | 103 | if (!presetSelect) { 104 | console.error('Preset select element not found'); 105 | return; 106 | } 107 | 108 | // Clear existing options except first two (default and save) 109 | while (presetSelect.options.length > 2) { 110 | presetSelect.remove(2); 111 | } 112 | 113 | // Add presets to dropdown 114 | Object.keys(presets).forEach(name => { 115 | const option = document.createElement('option'); 116 | option.value = name; 117 | option.textContent = name; 118 | presetSelect.appendChild(option); 119 | }); 120 | 121 | // Load last used preset if available 122 | const lastUsed = localStorage.getItem('lastUsedPreset'); 123 | if (lastUsed && presets[lastUsed]) { 124 | this.loadPreset(lastUsed); 125 | presetSelect.value = lastUsed; 126 | } 127 | } 128 | 129 | updatePresetList() { 130 | this.loadPresets(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/state-machine.js: -------------------------------------------------------------------------------- 1 | import { SCALE_CONSTANTS } from './constants.js'; 2 | 3 | export class StateMachine { 4 | constructor(uiController) { 5 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 6 | this.removalTimeout = null; 7 | this.removalTimeoutDuration = 5000; // 5 seconds 8 | this.uiController = uiController; // Store UI controller reference 9 | } 10 | 11 | setCurrentState(state) { 12 | this.currentState = state; 13 | } 14 | 15 | handleWeightUpdate(netWeight, scale) { 16 | switch (this.currentState) { 17 | case SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT: 18 | this.handleWaitingState(netWeight, scale); 19 | break; 20 | 21 | case SCALE_CONSTANTS.FSM_STATES.MEASURING: 22 | this.handleMeasuringState(netWeight, scale); 23 | break; 24 | 25 | case SCALE_CONSTANTS.FSM_STATES.REMOVAL_PENDING: 26 | this.handleRemovalPendingState(netWeight, scale); 27 | break; 28 | 29 | case SCALE_CONSTANTS.FSM_STATES.CONTAINER_REMOVED: 30 | this.handleContainerRemovedState(netWeight, scale); 31 | break; 32 | } 33 | } 34 | 35 | handleWaitingState(netWeight, scale) { 36 | if (netWeight == 0) { 37 | console.log('No weight detected, transitioning to WAITING_FOR_NEXT'); 38 | this.uiController.updateGuidance('Place object on scale', 'info'); 39 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 40 | } else if (netWeight >= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 41 | console.log('State: WAITING_FOR_NEXT. New object detected, transitioning to MEASURING'); 42 | this.uiController.updateGuidance('Measuring in progress...', 'info'); 43 | this.currentState = SCALE_CONSTANTS.FSM_STATES.MEASURING; 44 | scale.weightIsStable = false; 45 | scale.lastWeight = netWeight; 46 | } else if (netWeight < SCALE_CONSTANTS.WEIGHT_THRESHOLDS.MINIMUM) { 47 | console.log('Container removed, pausing dosing mode'); 48 | scale.dosingPausedForContainerRemoval = true; 49 | scale.stopDosing(false); // Pass false to indicate it's not a manual stop 50 | this.currentState = SCALE_CONSTANTS.FSM_STATES.CONTAINER_REMOVED; 51 | this.uiController.updateGuidance('Container removed, dosing paused', 'warning'); 52 | console.log('Current Flags at WAITING_FOR_NEXT state end, moved to container removed state'); 53 | } 54 | } 55 | 56 | handleMeasuringState(netWeight, scale) { 57 | const isStable = this.checkWeightStability(scale.stableWeightReadings); 58 | 59 | if (isStable) { 60 | scale.weightIsStable = true; 61 | 62 | const target = scale.dosingSettings.targetWeight; 63 | const lowThreshold = scale.dosingSettings.lowThreshold; 64 | const highThreshold = scale.dosingSettings.highThreshold; 65 | 66 | const remaining = target - netWeight; 67 | 68 | // Update UI based on weight 69 | if (netWeight >= lowThreshold && netWeight <= highThreshold) { 70 | console.log('Weight in target range:', netWeight); 71 | 72 | // Only save if not already saved 73 | if (!scale.doseSaved) { 74 | scale.saveDosing(netWeight); 75 | scale.doseSaved = true; // Mark as saved 76 | this.uiController.updateGuidance('Target weight reached!', 'success'); 77 | this.uiController.updateProgressBarColor('success'); 78 | 79 | // Set timeout for container removal 80 | if (!this.removalTimeout) { 81 | this.currentState = SCALE_CONSTANTS.FSM_STATES.REMOVAL_PENDING; 82 | this.uiController.updateGuidance('Target reached! Remove container.', 'success'); 83 | 84 | this.removalTimeout = setTimeout(() => { 85 | console.log('Removal timeout expired, resetting to waiting state'); 86 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 87 | this.uiController.updateGuidance('Ready for next measurement', 'info'); 88 | this.removalTimeout = null; 89 | }, this.removalTimeoutDuration); 90 | } 91 | } 92 | } else { 93 | // Reset the saved flag if weight goes out of range 94 | scale.doseSaved = false; 95 | if (netWeight < 0) { 96 | console.log('Container removed, pausing dosing mode'); 97 | scale.dosingPausedForContainerRemoval = true; 98 | this.uiController.updateProgressBarColor('error'); 99 | this.currentState = SCALE_CONSTANTS.FSM_STATES.CONTAINER_REMOVED; 100 | this.uiController.updateGuidance('Container removed, dosing paused', 'warning'); 101 | } else if (netWeight < lowThreshold) { 102 | console.log('Weight below low threshold:', netWeight); 103 | this.uiController.updateGuidance(`Add ${remaining.toFixed(1)}g more`, 'info'); 104 | this.uiController.updateProgressBarColor('warning'); 105 | } else if (netWeight > highThreshold) { 106 | console.log('Weight above high threshold:', netWeight); 107 | this.uiController.updateGuidance(`Remove ${(-remaining).toFixed(1)}g`, 'error'); 108 | this.uiController.updateProgressBarColor('error'); 109 | } 110 | } 111 | } else { 112 | // Weight not stable 113 | scale.weightIsStable = false; 114 | console.log('Weight not stable yet:', netWeight); 115 | this.uiController.updateGuidance('Stabilizing...', 'info'); 116 | } 117 | 118 | scale.lastWeight = netWeight; 119 | } 120 | 121 | handleRemovalPendingState(netWeight, scale) { 122 | if (netWeight > SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 123 | // Object still on scale 124 | this.uiController.updateGuidance('Remove this object, and then place the next object', 'warning'); 125 | } else if (Math.abs(netWeight) <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 126 | // Weight is close to zero - object removed 127 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 128 | this.uiController.updateGuidance('Ready! Place next object on scale', 'success'); 129 | 130 | if (scale.dosingPausedForContainerRemoval) { 131 | scale.startDosingAutomatically(); 132 | } 133 | } 134 | } 135 | 136 | handleContainerRemovedState(netWeight, scale) { 137 | // Check if weight is back within container tolerance 138 | if (netWeight >= -SCALE_CONSTANTS.WEIGHT_THRESHOLDS.CONTAINER_TOLERANCE && 139 | netWeight <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.CONTAINER_TOLERANCE) { 140 | console.log('Container put back on, resuming dosing'); 141 | scale.dosingPausedForContainerRemoval = false; 142 | 143 | // Resume dosing automatically 144 | scale.startDosingAutomatically(); 145 | 146 | // Update UI and state 147 | this.uiController.updateGuidance('Container back on scale, ready for next dose', 'success'); 148 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 149 | } 150 | } 151 | 152 | checkWeightStability(stableWeightReadings, threshold = 0.2, minReadings = 4) { 153 | // Check if we have enough readings 154 | if (!stableWeightReadings || stableWeightReadings.length < minReadings) { 155 | console.log('Weight Stability: Not enough readings yet.'); 156 | return false; 157 | } 158 | 159 | // Get the last n readings 160 | const recentReadings = stableWeightReadings.slice(-minReadings); 161 | 162 | // Calculate max and min from recent readings 163 | const maxWeight = Math.max(...recentReadings); 164 | const minWeight = Math.min(...recentReadings); 165 | const weightDifference = maxWeight - minWeight; 166 | 167 | // Check if weight is stable 168 | const isStable = weightDifference <= threshold; 169 | 170 | console.log('Weight Stability Check:', { 171 | readings: recentReadings, 172 | maxWeight: maxWeight.toFixed(2), 173 | minWeight: minWeight.toFixed(2), 174 | difference: weightDifference.toFixed(2), 175 | threshold: threshold, 176 | isStable: isStable 177 | }); 178 | 179 | return isStable; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /web_apps/Weigh_Save/modules/timer.js: -------------------------------------------------------------------------------- 1 | export class TimerManager { 2 | constructor() { 3 | this.uiController = null; 4 | this.timerInterval = null; 5 | this.elapsedTime = 0; 6 | this.measurementInterval = null; 7 | this.lastWeightReadingTime = 0; 8 | } 9 | 10 | setUIController(uiController) { 11 | this.uiController = uiController; 12 | } 13 | 14 | startTimer( interval) { 15 | if (!this.uiController) { 16 | console.error('UIController not set'); 17 | return; 18 | } 19 | 20 | console.log('Starting timer with interval:', interval); 21 | this.elapsedTime = 0; 22 | this.measurementInterval = interval * 1000; 23 | this.lastWeightReadingTime = 0; 24 | 25 | clearInterval(this.timerInterval); 26 | this.timerInterval = setInterval(() => { 27 | this.elapsedTime++; 28 | this.uiController.updateTimer(this.elapsedTime); 29 | 30 | // if (this.elapsedTime >= duration) { 31 | // this.stopTimer(); 32 | // } 33 | }, 1000); 34 | } 35 | 36 | stopTimer() { 37 | console.log('Stopping timer'); 38 | clearInterval(this.timerInterval); 39 | this.timerInterval = null; 40 | this.uiController.showMeasurementAlert(); 41 | // Reset UI state when timer stops 42 | this.uiController.resetTimerState(); 43 | } 44 | 45 | shouldTakeMeasurement() { 46 | const currentTime = Date.now(); 47 | if (this.timerInterval && (currentTime - this.lastWeightReadingTime) >= this.measurementInterval) { 48 | console.log("Should take measurement - Time elapsed:", currentTime - this.lastWeightReadingTime); 49 | this.lastWeightReadingTime = currentTime; 50 | return true; 51 | } 52 | return false; 53 | } 54 | } -------------------------------------------------------------------------------- /web_apps/Weigh_Save/weigh_save.html: -------------------------------------------------------------------------------- 1 | Decent Scale Weigh and Save 2 | 3 | 4 | 5 | 6 | 7 |
    8 | 9 |
    10 | 11 |
    12 |
    13 | Weigh and Save 14 |
    15 | 22 | Status: Not connected 23 |
    24 | 30 | 64 |
    65 |
    66 |
    67 |
    68 | 69 | 70 |
    71 |
    72 | 73 |
    74 | Interval (s): 75 | 82 |
    83 |
    84 |
    85 | 86 | 87 |
    88 |
    89 | 90 |
    91 | 97 | 103 | 104 | 105 |
    106 |
    107 | 108 |
    109 |

    Timer: 0s

    110 |

    Weight: 0.0g

    111 |

    Rate Change: 0.0 g/s

    112 |
    113 | 114 | 120 |
    121 | 122 | 123 |
    124 |
    125 | History 126 | 127 | 128 |
    129 | 130 |
    131 |
      132 |
      133 |
      134 |
      135 |
      136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/README.md: -------------------------------------------------------------------------------- 1 | **Intelligent Dosing Assistant for Decent Scales (USB-C & Bluetooth)** 2 | 3 | The `dosing_assistant` web application provides an advanced, guided workflow for precise, multi-stage weighing operations. 4 | --- 5 | 6 | ## Live Demo 7 | 8 | Experience the app in action! It requires a **Chrome or Edge browser (version 70 or newer)** on a device with Bluetooth. 9 | 10 | [https://decentespresso.com/support/scale/decentscale_dosingassistant](https://decentespresso.com/support/scale/samew_dosing_ast) 11 | 12 | --- 13 | 14 | ## Features 15 | 16 | * ** Connectivity:** Connect to your Half Decent Scale via : 17 | * **Wireless Bluetooth (Web BLE):** For convenience and portability. 18 | * **Configurable Dosing Settings:** 19 | * Set **target weight** 20 | * Define **overdose/underdose thresholds** enter your min or max limit for acceptable range of each dosing. 21 | * Configure **stability thresholds** to determine when a reading is stable enough to proceed. 22 | * Adjust **pre-wet/wait times** between stages. 23 | * Optionally enable **audible feedback** for stage transitions and completion. 24 | * **Real-time Weight Display:** Get live weight readings from your Half Decent Scale in grams. 25 | * **Measurement Logging:** Each completed dosing sequence is logged with: 26 | * Timestamp 27 | * Final weight and overall result (Pass/Fail) 28 | * Detailed breakdown of each stage's target, actual weight, and individual result. 29 | * **Local Data Storage:** All logged dosing sessions persist locally in your browser's cache. 30 | * **Flexible Data Export:** Easily download your collected dosing data in: 31 | * **CSV (Comma Separated Values):** Great for spreadsheet analysis. 32 | * **JSON (JavaScript Object Notation):** Ideal for programmatic use or integration with other systems. 33 | * **User Presets:** Save and load your specific dosing settings for different recipes or workflows, making setup quick and consistent. 34 | * **No Frameworks:** Built purely with native JavaScript, HTML, and CSS for a lightweight and transparent codebase. 35 | 36 | --- 37 | 38 | ## How to Use 39 | 40 | 1. **Open in Browser:** Navigate to the live demo link provided above using a Chrome or Edge browser (version 70 or newer) on a device with Bluetooth or a USB-C port. 41 | 2. **Ensure Scale is Ready:** Turn on your Half Decent Scale. 42 | 3. **Choose Connection Method:** 43 | * **For Bluetooth:** Ensure your scale is within Bluetooth range. Click the **"Connect via Bluetooth"** button. A browser prompt will appear; select your "Decent Scale" from the list and click "Pair". 44 | 4. **Configure Dosing Settings:** 45 | * Define the **target weight** 46 | * Define **highThreshold/lowThreshold** 47 | * Enter **Preset Name** 48 | * Use the "Object Name" and preset dropdown to **save or load your dosing configurations**. 49 | * Check "Enable Sounds" for audio cues. 50 | 51 | 5. **Start Dosing:** Click the **"Start"** button. The app will tare the scale and guide you through the defined stages. 52 | 6. **Follow On-Screen Prompts:** 53 | * The app will prompt you for each stage after you click start. 54 | * First it will prompt you to **Put Container on the scale and click set container weight**, after click it will tare the scale. 55 | * Now you can start dosing. 56 | * It will detect when the target weight for a stage is reached and stabilized before moving to the next. 57 | * Sounds will indicate completion of a stage or errors. 58 | 7. **Export Data:** After completing a dosing sequence, the results are automatically logged. You can click **"CSV"** or **"JSON"** to download your complete dosing history. 59 | 8. **Stop Dosing:** Click **"Stop Dosing"** to end the current sequence or reset the process. 60 | 61 | --- 62 | 63 | ## Technical Details 64 | 65 | The `dosing_assistant` application is built upon sophisticated browser APIs and a robust state management system: 66 | 67 | * **Web BLE :** Provides connectivity options, allowing the user to choose the preferred method for interacting with the scale. The app dynamically switches between the `ble_controller.js` and `serial_controller.js` logic based on user selection. 68 | * **Dosing State Machine (`state-machine.js`):** This is the core of the app, managing the sequential flow of the dosing process. States likely include: 69 | * `WAITING_FOR_NEXT`: Waiting to start a dosing cycle. 70 | * `MEASURING`: For measuring the object on scale. 71 | * `REMOVAL_PENDING`: After Measuring is done, it will prompt user to removal object from scale and put on the next one. 72 | * `CONTAINER_REMOVED`: A stage target if the container is removed or the weight goes below 0. 73 | * **Weight Stability & Target Detection:** The app constantly monitors the weight stream, determining stability (`checkWeightStability`) and checking if each stage's target weight is met within its defined thresholds (`lowThreshold`, `highThreshold`). 74 | * **Command Queue:** A robust command queue ensures reliable communication with the scale, preventing commands from being dropped or interfering with each other. 75 | * **Local Storage for Presets & Data:** 76 | * Dosing presets are stored in `localStorage` under keys like `'decentScaleDosingPresets'`, ensuring user configurations persist. 77 | * Dosing history is also saved locally for later retrieval and export. 78 | 79 | --- 80 | 81 | ## Development Setup 82 | 83 | To explore, modify, or contribute to this application: 84 | 85 | 1. **Download all files in this folder**. 86 | 2. **Navigate to the folder** in your terminal: 87 | 88 | ```bash 89 | cd openscale/web_apps/dosing_assistant 90 | ``` 91 | 92 | 3. **Serve the files locally over HTTP/HTTPS:** 93 | Web BLE and Web Serial API both require your application to be served over `https://` or `http://localhost`. You can use a simple local web server: 94 | 95 | **Using Python's `http.server` (Python 3, recommended):** 96 | 97 | ```bash 98 | python -m http.server 8000 99 | ``` 100 | 101 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 102 | 103 | **Using Node.js `http-server` (if you have Node.js installed):** 104 | 105 | ```bash 106 | npm install -g http-server 107 | http-server -p 8000 108 | ``` 109 | 110 | Then, open your Chrome or Edge browser and go to: `http://localhost:8000/` 111 | 112 | --- 113 | 114 | ## Code Structure Highlights 115 | 116 | The core logic is modularized to handle different aspects of the dosing process: 117 | 118 | * **`main.js`**: Likely the entry point, coordinating the different components, managing UI, and handling overall application state. 119 | * **`scale.js`**: Encapsulates all Web Bluetooth API interactions, including device discovery, connection, characteristic operations, and more. 120 | * **`dosing.js`**: Contains the dosing mode, evaluate weight against target, container weight , enter and exit the dosing mode. 121 | * **`ui_controller.js`**: Responsible for updating the DOM, displaying status messages, managing input fields, and handling user interface events. 122 | * **`state-machine.js`**: Handles state machine transition and state definition. 123 | * **`presets.js`**: Manages user presets , save the user input date. 124 | 125 | --- 126 | 127 | ## Contributing 128 | 129 | We welcome community contributions! If you have suggestions, bug reports, or want to contribute code, 130 | please refer to the main [Programmers guide to the Half Decent Scale](https://decentespresso.com/docs/programmers_guide_to_the_half_decent_scale). 131 | --- 132 | 133 | ## License 134 | 135 | This project is licensed under the **GNU General Public License v3.0**. See the `LICENSE` file in the main repository for more details. 136 | 137 | --- 138 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/dosing_assistant.html: -------------------------------------------------------------------------------- 1 | Decent Scale Dosing Assistant 2 | 3 | 4 | 5 | 6 | 7 | 8 |
      9 | 10 |
      11 | 12 |
      13 |
      14 | Dosing Assistant 15 |
      16 | 22 | 23 | 57 |
      58 |
      59 |
      60 | 61 | 62 |
      63 |
      64 | 65 |
      66 |
      67 | Target (g): 68 | 75 |
      76 |
      77 | Min 78 | 85 |
      86 |
      87 | Max 88 | 95 |
      96 |
      97 | 98 | 99 |
      100 |
      101 | Name: 102 | 108 |
      109 |
      110 | Preset: 111 | 118 |
      119 |
      120 | 126 | 129 |
      130 |
      131 |
      132 |
      133 | 134 | 135 |
      136 |
      137 |
      138 |
      139 |
      Weight: 0.0g
      140 | 146 |
      147 |
      148 | 154 | 161 |
      162 |
      163 | 164 |
      165 |
      0%
      166 | 167 |
      168 | 169 | 170 |
      171 |
      176 |
      177 | 178 |

      Click Start Dosing After Connected.

      179 |
      180 |
      181 | 182 | 183 |
      184 |
      185 | History 186 | 187 | 188 |
      189 |
        190 |
        191 |
        192 |
        193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/dosing_assistant.md: -------------------------------------------------------------------------------- 1 | # Decent Scale Dosing Assistant Documentation 2 | 3 | ## Project Overview 4 | The Decent Scale Dosing Assistant is a web application that interfaces with a Bluetooth-enabled scale to assist with precise dosing measurements. It provides real-time feedback, tracks measurement history, and allows data export. It is suitable for dosing multiple 18grams coffee beans, baking preparation or any other situation where you need to dose repeatly. 5 | 6 | ## Module Structure 7 | 8 | ### Main Application (`main.js`) 9 | **Purpose:** Entry point that initializes and connects all components. 10 | 11 | **Key Functionality:** 12 | - Initializes UI, state machine, and preset manager 13 | - Sets up event listeners for buttons 14 | - Configures export functionality 15 | 16 | **Concerns:** 17 | - Ensures proper initialization order of components 18 | - Manages component dependencies 19 | 20 | ### Scale Interface (`modules/scale.js`) 21 | **Purpose:** Handles communication with the Decent Scale via Bluetooth. 22 | 23 | **Key Functionality:** 24 | - Bluetooth device connection/disconnection 25 | - Weight measurement and tare operations 26 | - Dosing mode management 27 | - Measurement recording and evaluation 28 | 29 | **Concerns:** 30 | - Bluetooth connection reliability 31 | - Command sequencing and timing 32 | - Weight data accuracy 33 | - State management during dosing operations 34 | 35 | ### UI Controller (`modules/ui-controller.js`) 36 | **Purpose:** Manages all user interface elements and interactions. 37 | 38 | **Key Functionality:** 39 | - Updates weight display and status indicators 40 | - Manages progress bar visualization 41 | - Displays measurement history 42 | - Controls button states 43 | 44 | **Concerns:** 45 | - Responsive UI updates 46 | - Clear user guidance 47 | - Consistent visual feedback 48 | - Proper display of measurement history (newest first) 49 | 50 | ### State Machine (`modules/state-machine.js`) 51 | **Purpose:** Manages application state transitions during dosing operations. 52 | 53 | **Key Functionality:** 54 | - Defines and transitions between states (measuring, removal pending, etc.) 55 | - Enforces valid state transitions 56 | - Triggers appropriate UI updates based on state 57 | 58 | **Concerns:** 59 | - State consistency 60 | - Proper transition validation 61 | - Clear state-based user guidance 62 | 63 | ### Data Export (`modules/export.js`) 64 | **Purpose:** Handles exporting measurement data to different formats. 65 | 66 | **Key Functionality:** 67 | - CSV export with appropriate headers and formatting 68 | - JSON export with structured data 69 | - File download handling 70 | 71 | **Concerns:** 72 | - Proper data formatting 73 | - Consistent file naming 74 | - Browser compatibility for downloads 75 | 76 | ### Preset Manager (`modules/presets.js`) 77 | **Purpose:** Manages saved dosing presets. 78 | 79 | **Key Functionality:** 80 | - Saving and loading user presets 81 | - Applying presets to current dosing settings 82 | 83 | **Concerns:** 84 | - Persistent storage of presets 85 | - Validation of preset data 86 | 87 | ### Constants (`modules/constants.js`) 88 | **Purpose:** Centralizes application constants. 89 | 90 | **Key Functionality:** 91 | - Defines Bluetooth characteristics 92 | - Specifies state machine states 93 | - Sets weight thresholds 94 | 95 | **Concerns:** 96 | - Consistent use throughout application 97 | - Proper threshold values for accurate measurements 98 | 99 | ### Dosing Module (`modules/dosing.js`) 100 | **Purpose:** Handles the core dosing logic. 101 | 102 | **Key Functionality:** 103 | - Measurement evaluation against thresholds 104 | - Recording and formatting of measurement data 105 | - Weight result will be announced via visual (color bar) and sound feedback to user. 106 | **Concerns:** 107 | - Accurate threshold comparison 108 | - Proper status determination 109 | - Consistent data recording 110 | 111 | ## Data Flow 112 | 113 | 1. User connects to scale via Bluetooth 114 | 2. Scale readings are processed and displayed in real-time 115 | 3. When in dosing mode, measurements are evaluated against thresholds 116 | 4. Completed measurements are saved to history with pass and fail results. 117 | 5. User can export history in CSV or JSON format 118 | 119 | ## Key Features 120 | 121 | - Real-time weight display 122 | - Visual progress indication 123 | - Customizable target weights and thresholds 124 | - Measurement history with status indicators 125 | - Data export in multiple formats 126 | - Preset management for common dosing targets 127 | 128 | ## Technical Implementation Notes 129 | 130 | - Uses Web Bluetooth API for scale communication 131 | - Implements a state machine pattern for dosing workflow 132 | - Employs modular architecture for maintainability 133 | - Uses Tailwind CSS for responsive styling 134 | - Recommend to use this on Chrome, Edge brower on laptop and Andorid devices. Currently it is not supported by iOS devices. 135 | - To test locally , make sure you are at the file directory and run this command in terminal : npx http-server to host and access the web app in your browser. 136 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/main.js: -------------------------------------------------------------------------------- 1 | import { DecentScale } from './modules/scale.js'; 2 | import { StateMachine } from './modules/state-machine.js'; 3 | import { UIController } from './modules/ui-controller.js'; 4 | import { DataExport } from './modules/export.js'; 5 | import { PresetManager } from './modules/presets.js'; 6 | import { SCALE_CONSTANTS } from './modules/constants.js'; 7 | document.addEventListener('DOMContentLoaded', () => { 8 | // Initialize components 9 | const ui = new UIController(); 10 | const stateMachine = new StateMachine(ui); 11 | const presetManager = new PresetManager(); 12 | 13 | // Pass components to DecentScale 14 | const scale = new DecentScale(ui, stateMachine, presetManager); 15 | ui.setScale(scale); // Add this line 16 | 17 | // Connect button event listener - using correct ID 18 | document.getElementById('connectButton').addEventListener('click', async () => { 19 | if (scale.device) { 20 | await scale.disconnect(); 21 | } else { 22 | await scale.connect(); 23 | await scale.tare(); 24 | } 25 | }); 26 | 27 | // Other button listeners - using correct IDs 28 | document.getElementById('tareButton')?.addEventListener('click', () => 29 | scale.tare() 30 | ); 31 | 32 | document.getElementById('setContainerWeightButton')?.addEventListener('click', () => 33 | scale.setContainerWeight() 34 | ); 35 | 36 | document.getElementById('dosingToggleButton')?.addEventListener('click', () => 37 | scale.toggleDosingMode() 38 | ); 39 | 40 | // Setup preset manager with reference to scale 41 | presetManager.init(scale); 42 | presetManager.loadPresets(); 43 | presetManager.setupPresetHandlers(); 44 | 45 | // Add export functionality 46 | const exportCSVButton = document.getElementById('exportCSV'); 47 | const exportJSONButton = document.getElementById('exportJSON'); 48 | 49 | if (exportCSVButton) { 50 | exportCSVButton.addEventListener('click', () => { 51 | scale.exportToCSV(); 52 | }); 53 | } 54 | 55 | if (exportJSONButton) { 56 | exportJSONButton.addEventListener('click', () => { 57 | scale.exportToJSON(); 58 | }); 59 | } 60 | 61 | // Initialize UI 62 | if (ui.setContainerWeightButton) { 63 | ui.setContainerWeightButton.classList.add('invisible'); 64 | ui.setContainerWeightButton.setAttribute('disabled', ''); 65 | } 66 | 67 | if (ui.guidanceElement) { 68 | ui.guidanceElement.textContent = 'Connect to scale to begin'; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/modules/constants.js: -------------------------------------------------------------------------------- 1 | export const SCALE_CONSTANTS = { 2 | CHAR_READ: '0000fff0-0000-1000-8000-00805f9b34fb', 3 | CHAR_WRITE: '000036f5-0000-1000-8000-00805f9b34fb', 4 | READ_CHARACTERISTIC: '0000fff4-0000-1000-8000-00805f9b34fb', 5 | WRITE_CHARACTERISTIC: '000036f5-0000-1000-8000-00805f9b34fb', 6 | 7 | FSM_STATES: { 8 | MEASURING: 'measuring', 9 | REMOVAL_PENDING: 'removal_pending', 10 | WAITING_FOR_NEXT: 'waiting_for_next', 11 | CONTAINER_REMOVED: 'container_removed' 12 | }, 13 | WEIGHT_THRESHOLDS: { 14 | MINIMUM: -0.4, // Threshold for container removal detection 15 | CONTAINER_TOLERANCE: 0.6, // Increased tolerance for container replacement 16 | ZERO_TOLERANCE: 0.2, // Tolerance for considering weight as "zero" 17 | NEW_CONTAINER_TOLERANCE: 1 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/modules/dosing.js: -------------------------------------------------------------------------------- 1 | import { SCALE_CONSTANTS } from './modules/constants.js'; 2 | 3 | export class DosingManager { 4 | constructor(uiController) { 5 | this.ui = uiController; 6 | this.dosingMode = false; 7 | this.settings = { 8 | targetWeight: 0, 9 | lowThreshold: 0, 10 | highThreshold: 0 11 | }; 12 | this.currentProgress = 0; 13 | this.readingCount = 0; 14 | this.weightData = []; 15 | this.weightReadings = []; 16 | this.containerWeight = 0; 17 | this.waitingForContainerWeight = false; 18 | } 19 | 20 | startDosing() { 21 | this.dosingMode = true; 22 | this.settings = { 23 | targetWeight: parseFloat(this.ui.getTargetWeight()), 24 | lowThreshold: parseFloat(this.ui.getLowThreshold()), 25 | highThreshold: parseFloat(this.ui.getHighThreshold()) 26 | }; 27 | this.currentProgress = 0; 28 | this.waitingForContainerWeight = true; 29 | 30 | // Update UI 31 | this.ui.updateProgressBar(0); 32 | this.ui.updateProgressBarColor('default'); 33 | this.ui.updateGuidance('Place container on scale and click set container weight.'); 34 | this.ui.showSetContainerWeightButton(); 35 | 36 | console.log('Dosing started with settings:', this.settings); 37 | } 38 | 39 | stopDosing() { 40 | this.dosingMode = false; 41 | this.currentProgress = 0; 42 | this.waitingForContainerWeight = false; 43 | 44 | // Update UI 45 | this.ui.updateProgressBar(0); 46 | this.ui.updateProgressBarColor('default'); 47 | this.ui.updateGuidance('Dosing stopped'); 48 | this.ui.hideSetContainerWeightButton(); 49 | 50 | console.log('Dosing stopped'); 51 | } 52 | 53 | handleWeightUpdate(weight, netWeight) { 54 | if (!this.dosingMode) return; 55 | 56 | // If waiting for container weight, don't update progress 57 | if (this.waitingForContainerWeight) { 58 | this.ui.updateGuidance('Set container weight to continue'); 59 | return; 60 | } 61 | 62 | // Calculate progress based on net weight 63 | const target = this.settings.targetWeight; 64 | const progress = Math.min((netWeight / target) * 100, 100); 65 | this.currentProgress = progress; 66 | 67 | // Update UI 68 | this.ui.updateProgressBar(progress); 69 | this.updateProgressColor(netWeight); 70 | 71 | console.log('Weight update:', { 72 | netWeight, 73 | progress: progress.toFixed(1) + '%', 74 | target 75 | }); 76 | } 77 | 78 | updateProgressColor(netWeight) { 79 | if (netWeight >= this.settings.lowThreshold && 80 | netWeight <= this.settings.highThreshold) { 81 | this.ui.updateProgressBarColor('success'); 82 | this.ui.updateGuidance('Perfect! Remove container'); 83 | } else if (netWeight > this.settings.highThreshold) { 84 | this.ui.updateProgressBarColor('error'); 85 | this.ui.updateGuidance(`Remove ${(netWeight - this.settings.targetWeight).toFixed(1)}g`); 86 | } else { 87 | this.ui.updateProgressBarColor('warning'); 88 | this.ui.updateGuidance(`Add ${(this.settings.targetWeight - netWeight).toFixed(1)}g more`); 89 | } 90 | } 91 | 92 | setContainerWeight(weight) { 93 | this.containerWeight = weight; 94 | this.waitingForContainerWeight = false; 95 | this.ui.updateGuidance('Container weight set. Ready for dosing!'); 96 | this.ui.hideSetContainerWeightButton(); 97 | console.log('Container weight set to:', weight); 98 | } 99 | 100 | saveMeasurement(weight) { 101 | this.readingCount++; 102 | const reading = { 103 | readings: this.readingCount, 104 | timestamp: new Date().toLocaleString('en-GB'), 105 | weight: weight.toFixed(1), 106 | target: this.settings.targetWeight, 107 | lowThreshold: this.settings.lowThreshold, 108 | highThreshold: this.settings.highThreshold, 109 | status: this.evaluateWeight(weight) 110 | }; 111 | 112 | this.weightData.push(reading); 113 | this.weightReadings.push( 114 | `${reading.readings}. ${reading.timestamp}: ${reading.weight}g / ${reading.target}g - ${reading.status.toUpperCase()}` 115 | ); 116 | 117 | this.ui.displayWeightReadings(this.weightReadings); 118 | return reading; 119 | } 120 | 121 | evaluateWeight(weight) { 122 | return (weight >= this.settings.lowThreshold && 123 | weight <= this.settings.highThreshold) ? 'pass' : 'fail'; 124 | } 125 | 126 | isDosing() { 127 | return this.dosingMode; 128 | } 129 | 130 | isWaitingForContainer() { 131 | return this.waitingForContainerWeight; 132 | } 133 | 134 | getWeightData() { 135 | return this.weightData; 136 | } 137 | 138 | getWeightReadings() { 139 | return this.weightReadings; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/modules/export.js: -------------------------------------------------------------------------------- 1 | export class DataExport { 2 | static getFormattedDate() { 3 | const now = new Date(); 4 | const year = now.getFullYear(); 5 | const month = String(now.getMonth() + 1).padStart(2, '0'); 6 | const day = String(now.getDate()).padStart(2, '0'); 7 | return `${year}${month}${day}`; 8 | } 9 | 10 | static exportToCSV(weightData) { 11 | if (weightData.length === 0) { 12 | return { 13 | content: '', 14 | filename: `dosing-assistant-${this.getFormattedDate()}.csv`, 15 | type: 'text/csv' 16 | }; 17 | } 18 | 19 | const headers = ['Reading', 'Timestamp', 'Weight (g)', 'Target (g)', 'Low Threshold (g)', 'High Threshold (g)', 'Status']; 20 | const rows = weightData.map(reading => [ 21 | reading.readings, 22 | reading.timestamp, 23 | reading.weight, 24 | reading.target, 25 | reading.lowThreshold, 26 | reading.highThreshold, 27 | reading.status 28 | ]); 29 | 30 | const csvContent = [ 31 | headers.join(','), 32 | ...rows.map(row => row.join(',')) 33 | ].join('\n'); 34 | 35 | return { 36 | content: csvContent, 37 | filename: `dosing-assistant-${this.getFormattedDate()}.csv`, 38 | type: 'text/csv' 39 | }; 40 | } 41 | 42 | static exportToJSON(weightData) { 43 | if (weightData.length === 0) { 44 | return { 45 | content: '', 46 | filename: `dosing-assistant-${this.getFormattedDate()}.json`, 47 | type: 'application/json' 48 | }; 49 | } 50 | 51 | const jsonOutput = { 52 | export_date: new Date().toISOString(), 53 | total_readings: weightData.length, 54 | readings: weightData.map(reading => ({ 55 | reading_number: reading.readings, 56 | timestamp: reading.timestamp, 57 | weight_g: reading.weight, 58 | target_weight_g: reading.target, 59 | thresholds: { 60 | low_g: reading.lowThreshold, 61 | high_g: reading.highThreshold 62 | }, 63 | status: reading.status 64 | })) 65 | }; 66 | 67 | return { 68 | content: JSON.stringify(jsonOutput, null, 2), 69 | filename: `dosing-assistant-${this.getFormattedDate()}.json`, 70 | type: 'application/json' 71 | }; 72 | } 73 | 74 | static downloadFile(content, filename, contentType) { 75 | const blob = new Blob([content], { type: contentType }); 76 | const url = window.URL.createObjectURL(blob); 77 | const a = document.createElement('a'); 78 | a.href = url; 79 | a.download = filename; 80 | a.click(); 81 | window.URL.revokeObjectURL(url); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/modules/preset.js: -------------------------------------------------------------------------------- 1 | export class PresetManager { 2 | constructor() { 3 | this.scale = null; 4 | } 5 | 6 | init(scale) { 7 | this.scale = scale; 8 | } 9 | 10 | setupPresetHandlers() { 11 | console.log('setupPresetHandlers() is being called'); 12 | const presetSelect = document.getElementById('presetSelect'); 13 | 14 | if (presetSelect) { 15 | presetSelect.addEventListener('change', (e) => { 16 | const selectedValue = e.target.value; 17 | console.log('Preset selected:', selectedValue); 18 | 19 | if (selectedValue === 'save_preset') { 20 | console.log('Save preset option selected'); 21 | this.saveCurrentPreset(); 22 | e.target.value = ''; // Reset dropdown after saving 23 | } else if (selectedValue) { 24 | this.loadPreset(selectedValue); 25 | } 26 | }); 27 | } else { 28 | console.error('Preset select element not found'); 29 | } 30 | } 31 | 32 | saveCurrentPreset() { 33 | const objectName = document.getElementById('objectName').value.trim(); 34 | console.log('Attempting to save preset for:', objectName); 35 | 36 | if (!objectName) { 37 | console.warn('Save preset failed: No object name provided'); 38 | alert('Please enter an object name'); 39 | return; 40 | } 41 | 42 | const targetWeightInput = document.getElementById('targetWeight'); 43 | const highThresholdInput = document.getElementById('highThreshold'); 44 | const lowThresholdInput = document.getElementById('lowThreshold'); 45 | 46 | if (!targetWeightInput || !highThresholdInput || !lowThresholdInput) { 47 | console.error('One or more input elements not found'); 48 | return; 49 | } 50 | 51 | const preset = { 52 | name: objectName, 53 | settings: { 54 | targetWeight: parseFloat(targetWeightInput.value), 55 | highThreshold: parseFloat(highThresholdInput.value), 56 | lowThreshold: parseFloat(lowThresholdInput.value), 57 | } 58 | }; 59 | 60 | console.log('Saving preset with settings:', preset); 61 | this.savePreset(preset); 62 | this.updatePresetList(); 63 | console.log('Preset saved successfully:', preset); 64 | alert(`Object "${objectName}" saved successfully`); 65 | } 66 | 67 | savePreset(preset) { 68 | const presets = this.getPresets(); 69 | presets[preset.name] = preset; 70 | localStorage.setItem('decentScalePresets', JSON.stringify(presets)); 71 | } 72 | 73 | getPresets() { 74 | const presetsJson = localStorage.getItem('decentScalePresets'); 75 | return presetsJson ? JSON.parse(presetsJson) : {}; 76 | } 77 | 78 | loadPreset(name) { 79 | console.log('loadPreset function called with name:', name); 80 | const preset = this.getPreset(name); 81 | 82 | if (!preset || !preset.settings) { 83 | console.warn(`Preset "${name}" not found or has invalid settings.`); 84 | return; 85 | } 86 | 87 | document.getElementById('objectName').value = name; 88 | document.getElementById('targetWeight').value = preset.settings.targetWeight; 89 | document.getElementById('lowThreshold').value = preset.settings.lowThreshold; 90 | document.getElementById('highThreshold').value = preset.settings.highThreshold; 91 | localStorage.setItem('lastUsedPreset', name); 92 | } 93 | 94 | getPreset(name) { 95 | const presets = this.getPresets(); 96 | return presets[name]; 97 | } 98 | 99 | loadPresets() { 100 | const presets = this.getPresets(); 101 | const presetSelect = document.getElementById('presetSelect'); 102 | 103 | if (!presetSelect) { 104 | console.error('Preset select element not found'); 105 | return; 106 | } 107 | 108 | // Clear existing options except first two (default and save) 109 | while (presetSelect.options.length > 2) { 110 | presetSelect.remove(2); 111 | } 112 | 113 | // Add presets to dropdown 114 | Object.keys(presets).forEach(name => { 115 | const option = document.createElement('option'); 116 | option.value = name; 117 | option.textContent = name; 118 | presetSelect.appendChild(option); 119 | }); 120 | 121 | // Load last used preset if available 122 | const lastUsed = localStorage.getItem('lastUsedPreset'); 123 | if (lastUsed && presets[lastUsed]) { 124 | this.loadPreset(lastUsed); 125 | presetSelect.value = lastUsed; 126 | } 127 | } 128 | 129 | updatePresetList() { 130 | this.loadPresets(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /web_apps/dosing_assistant/modules/state-machine.js: -------------------------------------------------------------------------------- 1 | import { SCALE_CONSTANTS } from './constants.js'; 2 | import { DecentScale } from './scale.js'; 3 | //the main core for dosing assistant logic is a Finite-state machine , below you will 4 states - Waiting for next , Measuring, removal pending, container removed. 4 | export class StateMachine { 5 | constructor(uiController) { 6 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 7 | this.removalTimeout = null; 8 | this.removalTimeoutDuration = 5000; // 5 seconds 9 | this.uiController = uiController; // Store UI controller reference 10 | this.weightStableTimeout = null; // Timeout for stable weight 11 | } 12 | 13 | setCurrentState(state) { 14 | this.currentState = state; 15 | } 16 | 17 | handleWeightUpdate(netWeight, scale) { 18 | switch (this.currentState) { 19 | case SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT: 20 | this.handleWaitingState(netWeight, scale); 21 | break; 22 | 23 | case SCALE_CONSTANTS.FSM_STATES.MEASURING: 24 | this.handleMeasuringState(netWeight, scale); 25 | break; 26 | 27 | case SCALE_CONSTANTS.FSM_STATES.REMOVAL_PENDING: 28 | this.handleRemovalPendingState(netWeight, scale); 29 | break; 30 | 31 | case SCALE_CONSTANTS.FSM_STATES.CONTAINER_REMOVED: 32 | this.handleContainerRemovedState(netWeight, scale); 33 | break; 34 | } 35 | } 36 | 37 | handleWaitingState(netWeight, scale) { 38 | if (Math.abs(netWeight) <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 39 | scale.stableWeightReadings = []; // Clear stability readings 40 | this.uiController.updateGuidance('Place object on scale', 'info'); 41 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 42 | } else if (netWeight > SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 43 | this.currentState = SCALE_CONSTANTS.FSM_STATES.MEASURING; 44 | this.uiController.updateGuidance('Measuring in progress...', 'info'); 45 | } 46 | } 47 | 48 | handleMeasuringState(netWeight, scale) { 49 | // First check for negative weight 50 | if (netWeight < -SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 51 | console.log('Container removed - negative weight detected:', netWeight); 52 | this.currentState = SCALE_CONSTANTS.FSM_STATES.CONTAINER_REMOVED; 53 | scale.dosingPausedForContainerRemoval = true; 54 | scale.stableWeightReadings = []; // Clear readings 55 | this.uiController.updateGuidance('Container removed, please replace it', 'warning'); 56 | return; 57 | } 58 | 59 | const isStable = this.checkWeightStability(scale.stableWeightReadings); 60 | 61 | if (isStable) { 62 | scale.weightIsStable = true; 63 | 64 | // Start stability timeout if not already started 65 | if (!this.weightStableTimeout) { 66 | console.log('Weight stable - starting timeout'); 67 | this.weightStableTimeout = setTimeout(() => { 68 | if (!scale.doseSaved && this.checkWeightStability(scale.stableWeightReadings)) { 69 | // Weight remained stable - use existing evaluation logic 70 | const target = scale.dosingSettings.targetWeight; 71 | const lowThreshold = scale.dosingSettings.lowThreshold; 72 | const highThreshold = scale.dosingSettings.highThreshold; 73 | const remaining = target - netWeight; 74 | 75 | scale.saveDosing(netWeight); 76 | scale.doseSaved = true; 77 | 78 | if (netWeight >= lowThreshold && netWeight <= highThreshold) { 79 | console.log('Weight stable - success'); 80 | this.uiController.updateGuidance('Target weight reached!', 'success'); 81 | this.uiController.updateProgressBarColor('success'); 82 | scale.playSound('pass'); // Play success sound 83 | } else { 84 | if (netWeight < lowThreshold) { 85 | this.uiController.updateGuidance(`Failed: Under target by ${remaining.toFixed(1)}g`, 'warning'); 86 | this.uiController.updateProgressBarColor('warning'); 87 | console.log('Under Weight - fail'); 88 | scale.playSound('fail'); // Play fail sound 89 | } else { 90 | console.log('Over Weight - fail'); 91 | this.uiController.updateGuidance(`Failed: Over target by ${(-remaining).toFixed(1)}g`, 'error'); 92 | this.uiController.updateProgressBarColor('error'); 93 | scale.playSound('fail'); // Play fail sound 94 | } 95 | } 96 | 97 | this.currentState = SCALE_CONSTANTS.FSM_STATES.REMOVAL_PENDING; 98 | } 99 | this.weightStableTimeout = null; 100 | }, 2500); 101 | } 102 | } else { 103 | // Clear timeout if weight becomes unstable 104 | if (this.weightStableTimeout) { 105 | clearTimeout(this.weightStableTimeout); 106 | this.weightStableTimeout = null; 107 | } 108 | scale.weightIsStable = false; 109 | scale.doseSaved = false; 110 | this.uiController.updateGuidance('Stabilizing...', 'info'); 111 | } 112 | } 113 | 114 | handleRemovalPendingState(netWeight, scale) { 115 | console.log('handleRemovalPendingState:', { 116 | netWeight, 117 | zeroTolerance: SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE 118 | }); 119 | 120 | // Add stricter weight check 121 | if (Math.abs(netWeight) <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 122 | // Reset for next measurement 123 | scale.doseSaved = false; 124 | scale.stableWeightReadings = []; // Clear stability readings 125 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 126 | 127 | // Force tare when weight is near zero 128 | scale.tare(); 129 | console.log("Tare called - weight near zero"); 130 | this.uiController.updateGuidance('Ready! Place next object on scale', 'success'); 131 | } else if (netWeight > SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE) { 132 | // Still has weight on scale 133 | this.uiController.updateGuidance('Measurement Done - Put the next object on', 'warning'); 134 | } 135 | } 136 | 137 | handleContainerRemovedState(netWeight, scale) { 138 | // Check if weight is back within container tolerance 139 | console.log("handleContainerRemovedState"); 140 | if (netWeight >= -SCALE_CONSTANTS.WEIGHT_THRESHOLDS.CONTAINER_TOLERANCE && 141 | netWeight <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.CONTAINER_TOLERANCE) { 142 | console.log('Container put back on, resuming dosing'); 143 | scale.dosingPausedForContainerRemoval = false; 144 | 145 | // Resume dosing automatically 146 | scale.startDosingAutomatically(); 147 | 148 | // Update UI and state 149 | this.uiController.updateGuidance('Container back on scale, ready for next dose', 'success'); 150 | this.currentState = SCALE_CONSTANTS.FSM_STATES.WAITING_FOR_NEXT; 151 | } 152 | else if (netWeight>SCALE_CONSTANTS.NEW_CONTAINER_TOLERANCE){ 153 | this.uiController.updateGuidance('New container detected, click zero to continue', 'warning'); 154 | } 155 | } 156 | 157 | checkWeightStability(stableWeightReadings, threshold = 0.4, minReadings = 4) { 158 | // Check if we have enough readings 159 | if (!stableWeightReadings || stableWeightReadings.length < minReadings) { 160 | console.log('Weight Stability: Not enough readings yet.'); 161 | return false; 162 | } 163 | 164 | // Get the last n readings 165 | const recentReadings = stableWeightReadings.slice(-minReadings); 166 | 167 | // NEW: Check if readings are near zero (noise) 168 | const isNearZero = recentReadings.every(weight => 169 | Math.abs(weight) <= SCALE_CONSTANTS.WEIGHT_THRESHOLDS.ZERO_TOLERANCE 170 | ); 171 | 172 | if (isNearZero) { 173 | console.log('Weight Stability: Readings near zero - considering noise'); 174 | return false; 175 | } 176 | 177 | // Calculate max and min from recent readings 178 | const maxWeight = Math.max(...recentReadings); 179 | const minWeight = Math.min(...recentReadings); 180 | const weightDifference = maxWeight - minWeight; 181 | 182 | const isStable = weightDifference <= threshold; 183 | 184 | console.log('Weight Stability Check:', { 185 | readings: recentReadings, 186 | maxWeight: maxWeight.toFixed(2), 187 | minWeight: minWeight.toFixed(2), 188 | difference: weightDifference.toFixed(2), 189 | threshold: threshold, 190 | isStable: isStable, 191 | isNearZero: isNearZero // Added to logging 192 | }); 193 | 194 | return isStable; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /wifi_ota.h: -------------------------------------------------------------------------------- 1 | #ifdef WIFIOTA 2 | #ifndef WIFI_OTA_H 3 | #define WIFI_OTA_H 4 | #include "display.h" 5 | /* please remember to edit the ESPAsyncWebServer.h 6 | add the following line 7 | 8 | #define ELEGANTOTA_USE_ASYNC_WEBSERVER 1 9 | 10 | or edit the 11 | 12 | #ifndef ELEGANTOTA_USE_ASYNC_WEBSERVER 13 | #define ELEGANTOTA_USE_ASYNC_WEBSERVER 0 14 | #endif 15 | 16 | into 17 | 18 | #ifndef ELEGANTOTA_USE_ASYNC_WEBSERVER 19 | #define ELEGANTOTA_USE_ASYNC_WEBSERVER 1 20 | #endif 21 | #define ELEGANTOTA_USE_ASYNC_WEBSERVER 1 22 | */ 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | const char *ssid = "DecentScale"; 29 | const char *password = "12345678"; 30 | AsyncWebServer server(80); 31 | unsigned long ota_progress_millis = 0; 32 | long t_otaEnd = 0; 33 | 34 | void onOTAStart() { 35 | // Log when OTA has started 36 | Serial.println("OTA update started!"); 37 | } 38 | 39 | void onOTAProgress(size_t current, size_t final) { 40 | // Log every 1 second 41 | if (millis() - ota_progress_millis > 50) { 42 | ota_progress_millis = millis(); 43 | Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final); 44 | Serial.printf("Progress: %u%%\n", (current * 100) / final); 45 | char buffer[50]; 46 | snprintf(buffer, sizeof(buffer), "Uploading: %u%%", (current * 100) / final); 47 | u8g2.firstPage(); 48 | u8g2.setFont(FONT_S); 49 | do { 50 | u8g2.drawUTF8(AC((char *)trim(buffer)), AM(), (char *)trim(buffer)); 51 | } while (u8g2.nextPage()); 52 | } 53 | } 54 | 55 | void onOTAEnd(bool success) { 56 | // Log when OTA has finished 57 | t_otaEnd = millis(); 58 | if (success) { 59 | Serial.println("OTA update finished successfully!"); 60 | u8g2.setFont(FONT_S); 61 | while (millis() - t_otaEnd < 1000) { 62 | u8g2.firstPage(); 63 | do { 64 | u8g2.drawUTF8(AC((char *)"OTA update finished"), AM(), (char *)"OTA update finished"); 65 | } while (u8g2.nextPage()); 66 | } 67 | } else { 68 | Serial.println("There was an error during OTA update!"); 69 | u8g2.setFont(FONT_S); 70 | while (millis() - t_otaEnd < 1000) { 71 | u8g2.firstPage(); 72 | do { 73 | u8g2.drawUTF8(AC((char *)"OTA update failed"), AM(), (char *)"OTA update failed"); 74 | } while (u8g2.nextPage()); 75 | } 76 | } 77 | } 78 | 79 | void wifiOta() { 80 | u8g2.firstPage(); 81 | u8g2.setFont(FONT_S); 82 | do { 83 | u8g2.drawUTF8(AC((char *)"Starting OTA"), AM(), (char *)"Starting OTA"); 84 | } while (u8g2.nextPage()); 85 | 86 | WiFi.mode(WIFI_AP); 87 | WiFi.softAPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0)); 88 | WiFi.softAP(ssid, password); 89 | Serial.println(""); 90 | 91 | Serial.print("WiFi Access Point: "); 92 | Serial.println(ssid); 93 | Serial.print("IP address: "); 94 | Serial.println(WiFi.softAPIP()); 95 | 96 | server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { 97 | // Redirect to "/update" 98 | request->redirect("/update"); 99 | }); 100 | 101 | ElegantOTA.begin(&server); // Start ElegantOTA 102 | // ElegantOTA callbacks 103 | ElegantOTA.setAutoReboot(true); 104 | ElegantOTA.onStart(onOTAStart); 105 | ElegantOTA.onProgress(onOTAProgress); 106 | ElegantOTA.onEnd(onOTAEnd); 107 | 108 | server.begin(); 109 | Serial.println("HTTP server started"); 110 | b_ota = true; 111 | u8g2.firstPage(); 112 | u8g2.setFont(FONT_S); 113 | do { 114 | char ver[50]; 115 | sprintf(ver, "%s %s", PCB_VER, FIRMWARE_VER); 116 | //u8g2.drawUTF8(AC((char *)"Please connect WiFi"), u8g2.getMaxCharHeight() + i_margin_top - 5, (char *)"Please connect WiFi"); 117 | 118 | u8g2.drawUTF8(AC((char *)"WiFi: DecentScale"), u8g2.getMaxCharHeight() + i_margin_top - 5, (char *)"WiFi: DecentScale"); 119 | u8g2.drawUTF8(AC((char *)"Pwd: 12345678"), AM(), (char *)"Pwd: 12345678"); 120 | u8g2.drawUTF8(AC(trim(ver)), LCDHeight - i_margin_bottom + 5, trim(ver)); 121 | } while (u8g2.nextPage()); 122 | } 123 | #endif //WIFI_OTA_H 124 | #endif --------------------------------------------------------------------------------