From 2bc523fcf35aabb533bfc078d6126f62d121a663 Mon Sep 17 00:00:00 2001 From: IiemB Date: Sun, 12 Apr 2026 13:01:12 +0700 Subject: [PATCH 01/15] feat: update library dependencies and add random string generation functionality --- platformio.ini | 4 ++-- src/Constants.h | 2 +- src/CroasterCore.cpp | 30 ++++++++++++++++++++++++++++++ src/CroasterCore.h | 2 ++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 28d223e..4e23263 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,8 +11,8 @@ [env] lib_deps = https://github.com/Links2004/arduinoWebSockets.git - bblanchon/ArduinoJson@^7.4.2 - adafruit/Adafruit SSD1306@^2.5.13 + bblanchon/ArduinoJson@^7.4.3 + adafruit/Adafruit SSD1306@^2.5.16 yuriisalimov/MAX6675_Thermocouple@^2.0.2 tzapu/WiFiManager@^2.0.17 diff --git a/src/Constants.h b/src/Constants.h index c265011..814663e 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -18,7 +18,7 @@ const double version = 0.45; // Dummy mode -const bool dummyMode = false; +const bool dummyMode = true; // Smoothing factor of a temperature value #define SMOOTHING_FACTOR 5 diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index 6ff269a..8d2f96b 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -244,6 +244,22 @@ void CroasterCore::resetHistory(String item) debugln("# Histories reset due to " + item + " change."); } +String CroasterCore::genRamdomString(int length) +{ + const char charset[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + String result; + + for (size_t i = 0; i < length; i++) + { + result += charset[random(0, sizeof(charset) - 1)]; + } + + return result; +} + String CroasterCore::ssidName() { return getDeviceName("[", "] Croaster V" + String(version)); @@ -300,6 +316,20 @@ String CroasterCore::getJsonData(const String &message, const bool &skipCroaster croaster["correctionEt"] = correctionEt; croaster["tempUnit"] = tempUnit; + + if (dummyMode) + { + /* + You can add more fields to the JSON document that will be readed by ICRM app, + such as historical data or other sensor readings. + */ + + JsonObject extra = croaster["extra"].to(); + + extra["string"] = genRamdomString(10); + extra["number"] = random(100, 999); + extra["boolean"] = random(0, 2) == 1; + } } String jsonOutput; diff --git a/src/CroasterCore.h b/src/CroasterCore.h index c752b74..bff15d6 100644 --- a/src/CroasterCore.h +++ b/src/CroasterCore.h @@ -58,6 +58,8 @@ class CroasterCore */ void resetHistory(String item = "something"); + String genRamdomString(int length); + public: double timer = 0, rorEt = 0, rorBt = 0, tempEt = 0, tempBt = 0; From a840a3ce4f1933c471f3ea91b44ddb15d12c2dad Mon Sep 17 00:00:00 2001 From: IiemB Date: Sun, 12 Apr 2026 17:02:47 +0700 Subject: [PATCH 02/15] feat: set BLE MTU to 517 and update dummy mode to false --- src/BleManager.cpp | 2 ++ src/Constants.h | 2 +- src/CroasterCore.cpp | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 3bebe1f..0e1bfdf 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -62,6 +62,8 @@ void BleManager::begin() { BLEDevice::init(croaster->ssidName().c_str()); + BLEDevice::setMTU(517); + pServer = BLEDevice::createServer(); pServer->setCallbacks(new ServerCallbacks(this)); diff --git a/src/Constants.h b/src/Constants.h index 814663e..c265011 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -18,7 +18,7 @@ const double version = 0.45; // Dummy mode -const bool dummyMode = true; +const bool dummyMode = false; // Smoothing factor of a temperature value #define SMOOTHING_FACTOR 5 diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index 8d2f96b..f34b233 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -317,7 +317,7 @@ String CroasterCore::getJsonData(const String &message, const bool &skipCroaster croaster["tempUnit"] = tempUnit; - if (dummyMode) + if (useDummyData) { /* You can add more fields to the JSON document that will be readed by ICRM app, From e8abb378dcf8fc8567cbaaf20d5baa67c45fae5c Mon Sep 17 00:00:00 2001 From: IiemB Date: Wed, 13 May 2026 14:59:37 +0700 Subject: [PATCH 03/15] fix: correct spelling of genRandomString and update JSON handling comments --- platformio.ini | 4 +++- src/CroasterCore.cpp | 22 ++++++++++------------ src/CroasterCore.h | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/platformio.ini b/platformio.ini index 4e23263..e4d1b19 100644 --- a/platformio.ini +++ b/platformio.ini @@ -17,7 +17,7 @@ lib_deps = tzapu/WiFiManager@^2.0.17 [env:esp32c3] -board = super_mini_esp32c3 +board = esp32-c3-devkitm-1 framework = arduino platform = espressif32 lib_extra_dirs = ~/Documents/Arduino/libraries @@ -26,6 +26,8 @@ monitor_speed = 115200 board_build.partitions = custom32c3sm.csv build_flags = -D LED_ON=LOW + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 [env:esp8266] platform = espressif8266 diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index f34b233..2cd7a79 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -244,7 +244,7 @@ void CroasterCore::resetHistory(String item) debugln("# Histories reset due to " + item + " change."); } -String CroasterCore::genRamdomString(int length) +String CroasterCore::genRandomString(int length) { const char charset[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -317,19 +317,17 @@ String CroasterCore::getJsonData(const String &message, const bool &skipCroaster croaster["tempUnit"] = tempUnit; - if (useDummyData) - { - /* - You can add more fields to the JSON document that will be readed by ICRM app, - such as historical data or other sensor readings. - */ + /* + NOTE + You can add more fields to the JSON document that will be readed by ICRM app, + such as historical data or other sensor readings. + */ - JsonObject extra = croaster["extra"].to(); + // JsonObject extra = croaster["extra"].to(); - extra["string"] = genRamdomString(10); - extra["number"] = random(100, 999); - extra["boolean"] = random(0, 2) == 1; - } + // extra["string"] = genRandomString(10); + // extra["number"] = random(100, 999); + // extra["boolean"] = random(0, 2) == 1; } String jsonOutput; diff --git a/src/CroasterCore.h b/src/CroasterCore.h index bff15d6..51be1ce 100644 --- a/src/CroasterCore.h +++ b/src/CroasterCore.h @@ -58,7 +58,7 @@ class CroasterCore */ void resetHistory(String item = "something"); - String genRamdomString(int length); + String genRandomString(int length); public: double timer = 0, rorEt = 0, rorBt = 0, tempEt = 0, tempBt = 0; From ccb49e1b962a34d471601a5fcb6991308883e695 Mon Sep 17 00:00:00 2001 From: IiemB Date: Wed, 13 May 2026 15:31:44 +0700 Subject: [PATCH 04/15] Add Indonesian FAQ and README translations; enhance documentation for Croaster features and setup --- FAQ.md | 267 ++++++++++++++++++++++++++++++++++++++++++++++ FAQ_ID.md | 267 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 251 +++++++++++++++++++++++++++++++++---------- README_ID.md | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1021 insertions(+), 56 deletions(-) create mode 100644 FAQ.md create mode 100644 FAQ_ID.md create mode 100644 README_ID.md diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..42864d6 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,267 @@ +# โ“ Croaster FAQ โ€” Frequently Asked Questions + +> ๐Ÿ‡ฎ๐Ÿ‡ฉ Versi Bahasa Indonesia tersedia di [FAQ_ID.md](FAQ_ID.md) + +--- + +## ๐Ÿ“ฆ General + +### What is Croaster? + +Croaster is an open-source firmware for ESP8266 and ESP32C3-based microcontrollers that turns your hardware into a coffee roasting temperature monitor. It reads two thermocouple sensors (Bean Temperature and Environment Temperature), calculates Rate of Rise (RoR), and streams data over WiFi (WebSocket) or BLE to compatible apps like Artisan Roaster Scope and ICRM. + +--- + +### Which microcontrollers are supported? + +| Board | WiFi | BLE | OTA | +|:---|:---:|:---:|:---:| +| NodeMCU ESP8266 | โœ… | โŒ | โœ… | +| ESP32C3 Super Mini | โœ… | โœ… | โœ… | + +--- + +### What apps can I use with Croaster? + +- **[Artisan Roaster Scope](https://artisan-scope.org/)** โ€” connects via WebSocket (WiFi). Available for Windows, macOS, and Linux. +- **[ICRM App](https://iiemb.github.io/#/icrm)** โ€” connects via WebSocket (WiFi) or BLE (ESP32 only). Available for Android. + +--- + +### Is Croaster free to use? + +Yes. Croaster is released under the **MIT License**, which means it is free for both personal and commercial use. You can modify, distribute, and build upon it freely. + +--- + +## ๐Ÿ”Œ Hardware + +### What sensors does Croaster use? + +Croaster uses two **MAX6675** thermocouple ADC modules with **K-type thermocouple probes**: +- **BT (Bean Temperature)** โ€” placed inside the roasting drum to measure bean temperature +- **ET (Environment Temperature)** โ€” placed at the exhaust or inlet to measure airflow temperature + +--- + +### Can I use only one sensor? + +The firmware is designed to read two sensors. If you connect only one, the missing sensor will likely report `0` or erratic values. You can modify `CroasterCore.cpp` to handle a single-sensor setup, but this is not officially supported. + +--- + +### What is the operating voltage for the components? + +All components (OLED display, MAX6675 modules, and the microcontroller) operate at **3.3V**. Do not connect any of these to 5V directly, as it may damage the components. + +--- + +### Do both sensors share the same SPI pins? + +Yes. Both MAX6675 sensors share the **SCK** and **SO** SPI lines. They are differentiated by individual **CS (Chip Select)** pins: +- **BT sensor CS:** `D8` (ESP8266) / `GPIO7` (ESP32C3) +- **ET sensor CS:** `D6` (ESP8266) / `GPIO6` (ESP32C3) + +--- + +## ๐Ÿ“ถ WiFi & Connectivity + +### How do I connect Croaster to my WiFi network? + +On first boot, Croaster creates its own WiFi access point named `[XXXX] Croaster-XXXX`. Connect to it from your phone or computer, and a captive portal page will open. Enter your home WiFi credentials there โ€” Croaster will save them and connect automatically on future boots. + +See the [WiFi Setup Video](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) for a visual guide. + +--- + +### I forgot my WiFi password or want to change the network. How do I reset? + +Send the command `{"cmd": "erase"}` via WebSocket or BLE (ICRM app), or press the reset button while holding the boot button on the ESP32C3. This erases saved WiFi credentials and puts Croaster back into access point mode for reconfiguration. + +--- + +### Can I use Croaster without a WiFi network? + +Yes. When Croaster is not connected to any WiFi network, it broadcasts its own access point. You can connect Artisan directly to that access point. See [Option 1 in the README](README.md#-option-1-direct-connection-croaster-as-access-point). + +--- + +### What is the WebSocket port? + +Croaster's WebSocket server runs on **port 81**. + +--- + +### How often does Croaster send data? + +By default, data is broadcast every **3 seconds**. You can change this by sending: +```json +{"cmd": "interval", "value": 5} +``` +Replace `5` with your desired interval in seconds. + +--- + +### Is BLE available on ESP8266? + +No. BLE is only available on the **ESP32** platform. The ESP8266 only supports WiFi. + +--- + +## ๐ŸŒก๏ธ Temperature & Sensors + +### What temperature units are supported? + +Croaster supports **Celsius (C)** and **Fahrenheit (F)**. Switch units by sending: +```json +{"cmd": "unit", "value": "F"} +``` +or +```json +{"cmd": "unit", "value": "C"} +``` + +--- + +### What is Rate of Rise (RoR)? + +Rate of Rise (RoR) measures how fast the temperature is increasing, expressed in **degrees per minute**. Croaster calculates RoR for both BT and ET automatically based on recent sensor history. RoR is a critical metric in coffee roasting for tracking the development phase. + +--- + +### My sensor readings seem noisy or unstable. What can I do? + +Croaster applies a **smoothing filter** (factor: 5) to reduce noise from the MAX6675 sensors. If readings are still unstable, check your wiring, ensure the thermocouple probes are securely seated in the MAX6675 modules, and make sure the VCC supply is stable at 3.3V. + +--- + +### Can I apply a temperature correction/offset? + +Yes. Send a correction command: +```json +{"cmd": "correction", "bt": 1.5, "et": -0.5} +``` +This applies a `+1.5ยฐ` offset to BT and a `-0.5ยฐ` offset to ET. The correction is applied on top of the smoothed sensor reading. + +--- + +## ๐Ÿ”ง Build & Upload + +### Which build method should I use? + +| Scenario | Recommended Method | +|:---|:---| +| ESP8266 | PlatformIO or Arduino IDE | +| ESP32C3 (with OTA support) | Arduino IDE with Custom SuperMini partition | +| ESP32C3 (without OTA, max sketch size) | Arduino IDE with Huge APP partition | + +--- + +### Why can't I use PlatformIO for the ESP32C3 Super Mini? + +The **Makergo ESP32C3 SuperMini** board definition is not officially available in the PlatformIO board registry. You can add it manually using the community config from [this repository](https://github.com/sigmdel/supermini_esp32c3_sketches.git), but the officially supported method is to use **Arduino IDE** with the Makergo board package. + +--- + +### What is `copy_to_ino.sh`? + +This is a shell script that copies the source files from the `src/` directory (PlatformIO structure) into the `croaster-arduino/` folder with the correct Arduino sketch naming convention. Run it before opening the project in Arduino IDE. + +--- + +### What is the `custom32c3sm.csv` file? + +It is a **custom partition table** for the ESP32C3 Super Mini. This partition layout allocates more storage for the application (`1900544` bytes) while still reserving space for OTA updates. Without it, OTA updates cannot coexist with a large firmware binary. See [references.md](references.md) for installation steps. + +--- + +## โฌ†๏ธ OTA Updates + +### How do I update the firmware over the air? + +Use the **ICRM app** on Android. The app will send the compiled firmware binary over WebSocket. Croaster receives it in chunks, writes it to flash, and restarts automatically once complete. + +--- + +### OTA doesn't work on my ESP32C3. Why? + +The most common reason is that you are using the **Huge APP** partition scheme, which does not reserve space for OTA. Switch to the **Custom SuperMini** partition as described in [references.md](references.md). After re-flashing with the correct partition, OTA will work. + +--- + +### Does OTA work on ESP8266? + +OTA via the ICRM app is supported on ESP8266, as long as you are using a partition scheme that includes OTA space (the default NodeMCU scheme supports this). + +--- + +## ๐Ÿงช Development + +### How do I test Croaster without physical sensors? + +Set `dummyMode = true` in `Constants.h`: +```cpp +const bool dummyMode = true; +``` +In this mode, Croaster generates simulated temperature data, so you can test the WebSocket connection, OLED display, and BLE without attaching any hardware. + +--- + +### How do I add a custom command? + +1. Open `CommandHandler.cpp` +2. Find the `handleJsonCommand` method +3. Add your condition, for example: + ```cpp + if (json["cmd"] == "mycommand") { + // your logic here + responseOut = "{\"status\": \"ok\"}"; + } + ``` +4. The command will be available over both WebSocket and BLE automatically. + +--- + +### What JSON format does Croaster broadcast? + +Croaster sends a JSON payload over WebSocket and BLE at each interval. The payload includes temperature readings, RoR values, a timer, and the firmware version. The exact structure can be found in `CroasterCore.cpp` and `WebSocketManager.cpp`. + +--- + +## ๐Ÿ› Troubleshooting + +### Croaster is not showing on my WiFi list. + +- Wait up to 30 seconds after powering on for the access point to appear. +- If Croaster was previously connected to a WiFi network, it will try to reconnect first. Hold the boot button during startup to force AP mode (or erase credentials). +- Check that your power supply provides sufficient current for the ESP32C3. + +--- + +### The OLED display is blank or shows garbage. + +- Verify that the SDA/SCL wires are not swapped. +- Confirm the I2C address. The SSD1306 usually uses `0x3C`. If yours uses `0x3D`, update `DisplayManager.cpp`. +- Check the 3.3V supply to the display. + +--- + +### Artisan shows "no connection" even though I'm on the same network. + +- Confirm the IP address shown on the Croaster OLED matches what you entered in Artisan. +- Make sure both devices are on the same network (same router, not guest vs. main network). +- Check that port `81` is not blocked by your firewall or router settings. +- Try restarting Croaster and reconnecting. + +--- + +### The temperature readings are stuck at 0 or show `-0`. + +- Check the wiring of the MAX6675 sensor, especially the CS pin. +- Make sure the K-type thermocouple is firmly plugged into the MAX6675 module. +- Verify your 3.3V power supply is stable. +- Try swapping the sensor modules to rule out a faulty unit. + +--- + +_Have a question that isn't answered here? Open an issue on [GitHub](https://github.com/IiemB/Croaster)._ diff --git a/FAQ_ID.md b/FAQ_ID.md new file mode 100644 index 0000000..c457a3b --- /dev/null +++ b/FAQ_ID.md @@ -0,0 +1,267 @@ +# โ“ Croaster FAQ โ€” Pertanyaan yang Sering Diajukan + +> ๐Ÿ‡ฌ๐Ÿ‡ง English version available at [FAQ.md](FAQ.md) + +--- + +## ๐Ÿ“ฆ Umum + +### Apa itu Croaster? + +Croaster adalah firmware open-source untuk mikrokontroler berbasis ESP8266 dan ESP32C3 yang mengubah hardware Anda menjadi monitor suhu sangrai kopi. Firmware ini membaca dua sensor termokopel (Suhu Biji dan Suhu Lingkungan), menghitung Rate of Rise (RoR), dan mengirimkan data melalui WiFi (WebSocket) atau BLE ke aplikasi yang kompatibel seperti Artisan Roaster Scope dan ICRM. + +--- + +### Mikrokontroler apa saja yang didukung? + +| Board | WiFi | BLE | OTA | +|:---|:---:|:---:|:---:| +| NodeMCU ESP8266 | โœ… | โŒ | โœ… | +| ESP32C3 Super Mini | โœ… | โœ… | โœ… | + +--- + +### Aplikasi apa yang bisa digunakan bersama Croaster? + +- **[Artisan Roaster Scope](https://artisan-scope.org/)** โ€” terhubung via WebSocket (WiFi). Tersedia untuk Windows, macOS, dan Linux. +- **[Aplikasi ICRM](https://iiemb.github.io/#/icrm)** โ€” terhubung via WebSocket (WiFi) atau BLE (khusus ESP32). Tersedia untuk Android. + +--- + +### Apakah Croaster gratis digunakan? + +Ya. Croaster dirilis di bawah **Lisensi MIT**, yang berarti gratis untuk penggunaan pribadi maupun komersial. Anda bebas memodifikasi, mendistribusikan, dan mengembangkannya. + +--- + +## ๐Ÿ”Œ Hardware + +### Sensor apa yang digunakan Croaster? + +Croaster menggunakan dua modul ADC termokopel **MAX6675** dengan **probe termokopel K-type**: +- **BT (Bean Temperature / Suhu Biji)** โ€” ditempatkan di dalam drum sangrai untuk mengukur suhu biji kopi +- **ET (Environment Temperature / Suhu Lingkungan)** โ€” ditempatkan di exhaust atau inlet untuk mengukur suhu aliran udara + +--- + +### Bisakah saya menggunakan hanya satu sensor? + +Firmware dirancang untuk membaca dua sensor. Jika hanya satu sensor yang terpasang, sensor yang tidak terpasang kemungkinan akan melaporkan `0` atau nilai yang tidak stabil. Anda dapat memodifikasi `CroasterCore.cpp` untuk setup satu sensor, namun ini tidak didukung secara resmi. + +--- + +### Berapa tegangan operasi komponen-komponen tersebut? + +Semua komponen (layar OLED, modul MAX6675, dan mikrokontroler) beroperasi pada **3.3V**. Jangan hubungkan komponen ini langsung ke 5V, karena dapat merusak komponen. + +--- + +### Apakah kedua sensor menggunakan pin SPI yang sama? + +Ya. Kedua sensor MAX6675 berbagi jalur **SCK** dan **SO** pada bus SPI. Keduanya dibedakan oleh pin **CS (Chip Select)** masing-masing: +- **CS sensor BT:** `D8` (ESP8266) / `GPIO7` (ESP32C3) +- **CS sensor ET:** `D6` (ESP8266) / `GPIO6` (ESP32C3) + +--- + +## ๐Ÿ“ถ WiFi & Konektivitas + +### Bagaimana cara menghubungkan Croaster ke jaringan WiFi saya? + +Pada boot pertama, Croaster membuat access point WiFi sendiri dengan nama `[XXXX] Croaster-XXXX`. Hubungkan dari ponsel atau komputer Anda, dan halaman captive portal akan terbuka secara otomatis. Masukkan kredensial WiFi rumah Anda di sana โ€” Croaster akan menyimpannya dan terhubung otomatis di boot berikutnya. + +Lihat [Video Setup WiFi](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) untuk panduan visual. + +--- + +### Saya lupa password WiFi atau ingin mengganti jaringan. Bagaimana cara mereset? + +Kirim perintah `{"cmd": "erase"}` via WebSocket atau BLE (aplikasi ICRM), atau tekan tombol reset sambil menahan tombol boot pada ESP32C3. Ini akan menghapus kredensial WiFi yang tersimpan dan menempatkan Croaster kembali ke mode access point untuk konfigurasi ulang. + +--- + +### Bisakah saya menggunakan Croaster tanpa jaringan WiFi? + +Ya. Ketika Croaster tidak terhubung ke jaringan WiFi manapun, ia menyiarkan access point-nya sendiri. Anda dapat menghubungkan Artisan langsung ke access point tersebut. Lihat [Opsi 1 di README](README_ID.md#-opsi-1-koneksi-langsung-croaster-sebagai-access-point). + +--- + +### Berapa port WebSocket yang digunakan? + +Server WebSocket Croaster berjalan di **port 81**. + +--- + +### Seberapa sering Croaster mengirim data? + +Secara default, data di-broadcast setiap **3 detik**. Anda dapat mengubahnya dengan mengirim: +```json +{"cmd": "interval", "value": 5} +``` +Ganti `5` dengan interval yang Anda inginkan dalam satuan detik. + +--- + +### Apakah BLE tersedia di ESP8266? + +Tidak. BLE hanya tersedia di platform **ESP32**. ESP8266 hanya mendukung WiFi. + +--- + +## ๐ŸŒก๏ธ Suhu & Sensor + +### Satuan suhu apa saja yang didukung? + +Croaster mendukung **Celsius (C)** dan **Fahrenheit (F)**. Ganti satuan dengan mengirim: +```json +{"cmd": "unit", "value": "F"} +``` +atau +```json +{"cmd": "unit", "value": "C"} +``` + +--- + +### Apa itu Rate of Rise (RoR)? + +Rate of Rise (RoR) mengukur seberapa cepat suhu meningkat, dinyatakan dalam **derajat per menit**. Croaster menghitung RoR untuk BT dan ET secara otomatis berdasarkan riwayat sensor terbaru. RoR adalah metrik kritis dalam sangrai kopi untuk memantau fase perkembangan. + +--- + +### Pembacaan sensor saya terlihat tidak stabil. Apa yang harus saya lakukan? + +Croaster menerapkan **filter penghalusan** (faktor: 5) untuk mengurangi noise dari sensor MAX6675. Jika pembacaan masih tidak stabil, periksa kabel Anda, pastikan probe termokopel terpasang dengan kuat di modul MAX6675, dan pastikan suplai VCC stabil di 3.3V. + +--- + +### Bisakah saya menerapkan koreksi/offset suhu? + +Ya. Kirim perintah koreksi: +```json +{"cmd": "correction", "bt": 1.5, "et": -0.5} +``` +Ini menerapkan offset `+1.5ยฐ` pada BT dan offset `-0.5ยฐ` pada ET. Koreksi diterapkan di atas pembacaan sensor yang sudah dihaluskan. + +--- + +## ๐Ÿ”ง Build & Upload + +### Metode build mana yang sebaiknya saya gunakan? + +| Skenario | Metode yang Direkomendasikan | +|:---|:---| +| ESP8266 | PlatformIO atau Arduino IDE | +| ESP32C3 (dengan dukungan OTA) | Arduino IDE dengan partisi Custom SuperMini | +| ESP32C3 (tanpa OTA, ukuran sketch maksimal) | Arduino IDE dengan partisi Huge APP | + +--- + +### Mengapa saya tidak bisa menggunakan PlatformIO untuk ESP32C3 Super Mini? + +Definisi board **Makergo ESP32C3 SuperMini** tidak tersedia secara resmi di registry board PlatformIO. Anda dapat menambahkannya secara manual menggunakan konfigurasi komunitas dari [repositori ini](https://github.com/sigmdel/supermini_esp32c3_sketches.git), namun metode yang didukung secara resmi adalah menggunakan **Arduino IDE** dengan paket board Makergo. + +--- + +### Apa itu `copy_to_ino.sh`? + +Ini adalah shell script yang menyalin file sumber dari direktori `src/` (struktur PlatformIO) ke folder `croaster-arduino/` dengan konvensi penamaan sketch Arduino yang benar. Jalankan sebelum membuka project di Arduino IDE. + +--- + +### Apa itu file `custom32c3sm.csv`? + +Ini adalah **tabel partisi kustom** untuk ESP32C3 Super Mini. Tata letak partisi ini mengalokasikan lebih banyak penyimpanan untuk aplikasi (`1900544` byte) sekaligus menyisihkan ruang untuk update OTA. Tanpanya, update OTA tidak dapat berjalan berdampingan dengan binary firmware yang besar. Lihat [references.md](references.md) untuk langkah instalasi. + +--- + +## โฌ†๏ธ Update OTA + +### Bagaimana cara memperbarui firmware secara over the air? + +Gunakan **aplikasi ICRM** di Android. Aplikasi akan mengirimkan binary firmware yang telah dikompilasi melalui WebSocket. Croaster menerimanya dalam potongan-potongan, menuliskannya ke flash, dan restart otomatis setelah selesai. + +--- + +### OTA tidak bekerja di ESP32C3 saya. Mengapa? + +Alasan paling umum adalah Anda menggunakan skema partisi **Huge APP**, yang tidak menyisihkan ruang untuk OTA. Ganti ke partisi **Custom SuperMini** seperti yang dijelaskan di [references.md](references.md). Setelah flash ulang dengan partisi yang benar, OTA akan berfungsi. + +--- + +### Apakah OTA bekerja di ESP8266? + +OTA via aplikasi ICRM didukung di ESP8266, selama Anda menggunakan skema partisi yang mencakup ruang OTA (skema NodeMCU default mendukung ini). + +--- + +## ๐Ÿงช Pengembangan + +### Bagaimana cara menguji Croaster tanpa sensor fisik? + +Atur `dummyMode = true` di `Constants.h`: +```cpp +const bool dummyMode = true; +``` +Dalam mode ini, Croaster menghasilkan data suhu simulasi, sehingga Anda dapat menguji koneksi WebSocket, tampilan OLED, dan BLE tanpa memasang hardware apapun. + +--- + +### Bagaimana cara menambahkan perintah kustom? + +1. Buka `CommandHandler.cpp` +2. Temukan metode `handleJsonCommand` +3. Tambahkan kondisi Anda, misalnya: + ```cpp + if (json["cmd"] == "perintahsaya") { + // logika Anda di sini + responseOut = "{\"status\": \"ok\"}"; + } + ``` +4. Perintah tersebut akan tersedia melalui WebSocket maupun BLE secara otomatis. + +--- + +### Format JSON apa yang di-broadcast oleh Croaster? + +Croaster mengirimkan payload JSON melalui WebSocket dan BLE di setiap interval. Payload tersebut mencakup pembacaan suhu, nilai RoR, timer, dan versi firmware. Struktur lengkapnya dapat ditemukan di `CroasterCore.cpp` dan `WebSocketManager.cpp`. + +--- + +## ๐Ÿ› Pemecahan Masalah + +### Croaster tidak muncul di daftar WiFi saya. + +- Tunggu hingga 30 detik setelah dinyalakan agar access point muncul. +- Jika Croaster sebelumnya terhubung ke jaringan WiFi, ia akan mencoba terhubung kembali terlebih dahulu. Tahan tombol boot saat startup untuk memaksa mode AP (atau hapus kredensial). +- Periksa apakah catu daya Anda menyediakan arus yang cukup untuk ESP32C3. + +--- + +### Layar OLED kosong atau menampilkan karakter tidak jelas. + +- Verifikasi bahwa kabel SDA/SCL tidak tertukar. +- Konfirmasi alamat I2C. SSD1306 biasanya menggunakan `0x3C`. Jika milik Anda menggunakan `0x3D`, perbarui `DisplayManager.cpp`. +- Periksa suplai 3.3V ke layar. + +--- + +### Artisan menampilkan "no connection" meskipun saya berada di jaringan yang sama. + +- Konfirmasi alamat IP yang ditampilkan di OLED Croaster sesuai dengan yang Anda masukkan di Artisan. +- Pastikan kedua perangkat berada di jaringan yang sama (router yang sama, bukan jaringan guest vs. utama). +- Periksa apakah port `81` tidak diblokir oleh firewall atau pengaturan router Anda. +- Coba restart Croaster dan hubungkan kembali. + +--- + +### Pembacaan suhu macet di 0 atau menampilkan `-0`. + +- Periksa kabel sensor MAX6675, terutama pin CS. +- Pastikan probe termokopel K-type terpasang kuat di modul MAX6675. +- Verifikasi suplai daya 3.3V Anda stabil. +- Coba tukar modul sensor untuk mengetahui apakah ada unit yang rusak. + +--- + +_Ada pertanyaan yang belum terjawab di sini? Buka issue di [GitHub](https://github.com/IiemB/Croaster)._ diff --git a/README.md b/README.md index 6159634..c438137 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,28 @@ # โ˜• Croaster - Open Source Coffee Roaster Monitor -**Croaster** is a lightweight, open-source temperature monitoring system built on ESP-based microcontrollers. Designed for coffee roasting, it reads from two thermocouple sensors and displays real-time data on an OLED screen. Croaster also provides connectivity via WiFi (ESP8266/ESP32) and BLE (ESP32 only) for remote monitoring and control. +> ๐Ÿ‡ฎ๐Ÿ‡ฉ Versi Bahasa Indonesia tersedia di [README_ID.md](README_ID.md) + +**Croaster** is a lightweight, open-source temperature monitoring system built on ESP-based microcontrollers. Designed for coffee roasting enthusiasts and professionals, it reads from two thermocouple sensors (Bean Temperature and Environment Temperature) and displays real-time data on a compact OLED screen. Croaster connects seamlessly to popular roasting software via WiFi (WebSocket) and BLE (ESP32 only), making it compatible with both desktop and mobile roasting apps. + +**Current Firmware Version:** `0.45` + +--- + +## ๐Ÿ“‘ Table of Contents + +- [Features](#-features) +- [Hardware Components](#-hardware-components) +- [Wiring Diagram](#-wiring-diagram) +- [Software Architecture](#-software-architecture) +- [Libraries & Dependencies](#-libraries--dependencies) +- [How to Build and Upload](#-how-to-build-and-upload) +- [WiFi Setup Guide](#-wifi-setup-guide) +- [Communication Overview](#-communication-overview) +- [How to Connect Croaster with Artisan](#-how-to-connect-croaster-with-artisan) +- [OTA (Over-The-Air) Updates](#-ota-over-the-air-updates) +- [Custom Commands](#-custom-commands) +- [License](#-license) +- [Contributing](#๏ธ-contributing) --- @@ -8,23 +30,37 @@ * Supports **NodeMCU ESP8266** (WiFi only) * Supports **ESP32C3 Super Mini** (WiFi & BLE) -* Real-time monitoring of **two MAX6675 sensors** (ET and BT) -* Visual output on a **128x64 OLED display** (SSD1306, I2C) -* WiFi communication via **WebSocket**, compatible with: - + [**Artisan Roaster Scope**](https://artisan-scope.org/) - + [**ICRM app**](https://iiemb.github.io/#/icrm) -* BLE communication (ESP32 only) for the [**ICRM app**](https://iiemb.github.io/#/icrm) -* Custom command system via a centralized `CommandHandler` class +* Real-time monitoring of **two MAX6675 thermocouple sensors**: + - **BT** โ€” Bean Temperature (inside the drum) + - **ET** โ€” Environment Temperature (exhaust/inlet) +* **Rate of Rise (RoR)** calculation for both BT and ET, updated automatically +* Temperature unit switching: **Celsius** or **Fahrenheit** +* Configurable data send interval (default: every **3 seconds**) +* Built-in **temperature smoothing** (smoothing factor: 5) to reduce sensor noise +* Visual output on a **128ร—64 OLED display** (SSD1306, I2C) +* WiFi communication via **WebSocket** on port **81**, compatible with: + + [**Artisan Roaster Scope**](https://artisan-scope.org/) โ€” industry-standard roasting logger + + [**ICRM app**](https://iiemb.github.io/#/icrm) โ€” companion mobile app (Android) +* **BLE communication** (ESP32 only) for the [**ICRM app**](https://iiemb.github.io/#/icrm) +* **OTA (Over-The-Air) firmware updates** via WebSocket (ICRM app) +* **WiFiManager** captive portal for easy WiFi credential setup โ€” no re-flashing needed +* Unique device naming based on chip ID (e.g. `Croaster-A1B2`) +* **Dummy mode** for development and testing without physical sensors +* Custom JSON command system via the centralized `CommandHandler` class * Easily extendable with user-defined commands --- ## ๐Ÿงฉ Hardware Components -* 1ร— [NodeMCU ESP8266](images/NodeMCU-ESP8266.png) or [ESP32C3 Super Mini](images/ESP32C3-Super-Mini.png) -* 1ร— [128x64 OLED display (SSD1306, I2C)](images/OLED-Display.png) -* 2ร— [MAX6675 thermocouple sensor modules](images/MAX6675.png) -* 2ร— [K-type thermocouple probes](images/Type-K-thermocouple.png) +| Component | Description | +|:---|:---| +| 1ร— [NodeMCU ESP8266](images/NodeMCU-ESP8266.png) **or** [ESP32C3 Super Mini](images/ESP32C3-Super-Mini.png) | Main microcontroller | +| 1ร— [128ร—64 OLED display (SSD1306, I2C)](images/OLED-Display.png) | Real-time temperature display | +| 2ร— [MAX6675 thermocouple modules](images/MAX6675.png) | SPI-based K-type thermocouple ADC | +| 2ร— [K-type thermocouple probes](images/Type-K-thermocouple.png) | Temperature probes (BT & ET) | + +> All components run on **3.3V**. Ensure your power supply can handle the combined current draw of both sensors and the display. --- @@ -32,91 +68,147 @@ | |**NodeMCU ESP8266**|**ESP32C3 Super Mini**| |:---|:---:|:---:| -|**OLED Display**|GND โ†’**GND**|GND โ†’ **GND**| +|**OLED Display**|GND โ†’ **GND**|GND โ†’ **GND**| | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCL โ†’ **D1**|SCL โ†’ **GPIO9**| | |SDA โ†’ **D2**|SDA โ†’ **GPIO8**| |||โ €| -|**ET Sensor**|GND โ†’ **GND**|GND โ†’ **GND**| +|**ET Sensor** (Environment Temp)|GND โ†’ **GND**|GND โ†’ **GND**| | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| | |CS โ†’ **D8**|CS โ†’ **GPIO6**| |||โ €| -|**BT Sensor**|GND โ†’ **GND**|GND โ†’ **GND**| +|**BT Sensor** (Bean Temp)|GND โ†’ **GND**|GND โ†’ **GND**| | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| | |CS โ†’ **D6**|CS โ†’ **GPIO7**| +> Both sensors share the **SCK** and **SO** lines (SPI bus). They are distinguished by their individual **CS** pins. + --- -## ๐Ÿ›  Software Highlights +## ๐Ÿ›  Software Architecture + +Croaster uses a clean, **modular C++ architecture** built with the Arduino framework. Each subsystem is encapsulated in its own class: + +| Module | File | Responsibility | +|:---|:---|:---| +| `CroasterCore` | `CroasterCore.h/.cpp` | Sensor reading, RoR calculation, temperature smoothing, data state | +| `DisplayManager` | `DisplayManager.h/.cpp` | OLED rendering loop, status screens | +| `CommandHandler` | `CommandHandler.h/.cpp` | JSON command parsing and dispatching (BLE & WebSocket) | +| `WebSocketManager` | `WebSocketManager.h/.cpp` | WebSocket server, data broadcast, OTA trigger | +| `BleManager` | `BleManager.h/.cpp` | BLE server, characteristic notify, command receive *(ESP32 only)* | +| `OtaHandler` | `OtaHandler.h/.cpp` | Binary OTA update handling over WebSocket | +| `WiFiManagerUtil` | `WiFiManagerUtil.h/.cpp` | WiFiManager captive portal setup and lifecycle | +| `DeviceIdentity` | `DeviceIdentity.h/.cpp` | Chip ID, device name, IP address helpers | -* Written in **C++** with the **PlatformIO** build system -* Modular architecture separating BLE, WebSocket, display, and sensor logic -* **CommandHandler** class: - + Manages all incoming BLE/WebSocket JSON commands - + Easily customizable for user-defined actions (e.g. `restart`, `erase`, etc.) +### Data Flow + +``` +MAX6675 Sensors โ†’ CroasterCore (read + smooth + RoR) + โ†“ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + WebSocketManager BleManager (ESP32) + โ†“ โ†“ + Artisan / ICRM ICRM (Android) +``` + +--- + +## ๐Ÿ“ฆ Libraries & Dependencies + +| Library | Purpose | +|:---|:---| +| [arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) | WebSocket server | +| [ArduinoJson](https://arduinojson.org/) `^7.4.3` | JSON command parsing and serialization | +| [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) `^2.5.16` | OLED display driver | +| [MAX6675_Thermocouple](https://github.com/YuriiSalimov/MAX6675_Thermocouple) `^2.0.2` | Thermocouple sensor reading | +| [WiFiManager](https://github.com/tzapu/WiFiManager) `^2.0.17` | Captive portal WiFi setup | +| ESP32 BLE Arduino *(built-in ESP32 core)* | BLE server & characteristics | --- ## ๐Ÿ”ง How to Build and Upload -### โœ… PlatformIO (recommended for ESP8266) +### โœ… PlatformIO (recommended for ESP8266 & ESP32C3) -1. Install [PlatformIO](https://platformio.org/) +1. Install [PlatformIO](https://platformio.org/) (VS Code extension or CLI) 2. Clone the repository: - -```bash + ```bash git clone git@github.com:IiemB/Croaster.git - cd croaster + cd Croaster ``` -3. Select your board in `platformio.ini` (ESP8266 only) +3. Review `platformio.ini` and select your target environment 4. Upload the firmware: - -```bash - pio run -t upload + ```bash + # For ESP8266 + pio run -e esp8266 -t upload + + # For ESP32C3 + pio run -e esp32c3 -t upload ``` -### โœ… Arduino IDE (required for ESP32C3) +> **Note:** ESP32C3 Super Mini uses a custom partition scheme (`custom32c3sm.csv`) to maximize app storage. See [references.md](references.md) for setup details. + +### โœ… Arduino IDE (alternative, required for Makergo ESP32C3 board) -1. Run the conversion script: - +> Arduino IDE is required if you use the **Makergo ESP32C3 SuperMini** board definition, which is not yet fully supported by PlatformIO. -```bash +1. Run the conversion script to copy source files into the Arduino sketch folder: + + ```bash ./copy_to_ino.sh ``` -2. Open `croaster-arduino` folder in **Arduino IDE** +2. Open the `croaster-arduino/` folder in **Arduino IDE 2.x** 3. Select your board: - - ESP8266 โ†’ **NodeMCU 1.0 (ESP-12E)** - - ESP32C3 โ†’ **Makergo ESP32C3** *(ESP32C3 Super Mini is not yet supported by PlatformIO)* -4. Select partition `Huge APP` *(ESP32C3 only)* - + - **ESP8266** โ†’ `NodeMCU 1.0 (ESP-12E Module)` + - **ESP32C3** โ†’ `Makergo ESP32C3 SuperMini` +4. For **ESP32C3**, select the partition scheme: + - Use `Huge APP` for maximum sketch size (OTA not supported) + - Use `Custom SuperMini` for OTA support (see [references.md](references.md) for setup) + + > [!NOTE] + > The `Huge APP` partition does **not** support OTA via the ICRM App. To enable OTA, follow the custom partition steps in [references.md](references.md). - > [!NOTE] - > this partition doesn't support OTA via ICRM App. To handle this, you can follow [this steps](references.md). +5. Install all required libraries via Arduino Library Manager (see [Libraries & Dependencies](#-libraries--dependencies)) +6. Upload via `Sketch โ†’ Upload` --- ## ๐Ÿ”— WiFi Setup Guide -To connect Croaster to your WiFi network, you can follow this quick video guide: โžก๏ธ [How to Connect to WiFi - YouTube](https://www.youtube.com/watch?v=esNiudoCEcU\&t=434s) +Croaster uses **WiFiManager** to handle WiFi credentials without re-flashing. On first boot (or after erasing credentials), Croaster creates its own access point: + +1. On your phone or computer, connect to the WiFi network named `[XXXX] Croaster-XXXX` +2. A captive portal will open automatically โ€” enter your home WiFi SSID and password +3. Croaster will save the credentials and connect automatically on subsequent boots +4. The IP address assigned to Croaster is shown on the OLED display + +For a visual walkthrough, see: โžก๏ธ [How to Connect to WiFi - YouTube](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) --- ## ๐Ÿ“ก Communication Overview -* **WebSocket (WiFi):** - + Connects with **Artisan Roaster Scope** - + Also supports the **ICRM app** *(Android only)* +### WebSocket (WiFi) + +- **Port:** `81` +- **Protocol:** WebSocket (text frames for JSON commands, binary frames for OTA) +- **Data format:** JSON, broadcast every `intervalSend` seconds (default: 3s) +- Compatible with **Artisan Roaster Scope** and **ICRM app** (Android) -* **BLE (ESP32 only):** - + Connects exclusively with the **ICRM app** *(Android only)* +### BLE (ESP32 only) + +- **Service UUID:** `1cc9b045-a6e9-4bd5-b874-07d4f2d57843` +- **Data Characteristic UUID:** `d56d0059-ad65-43f3-b971-431d48f89a69` +- Supports notify (data push) and write (command receive) +- Compatible with the **ICRM app** (Android only) --- @@ -126,19 +218,56 @@ You can connect your Croaster device to Artisan using either a direct WiFi conne ### ๐Ÿ–ฅ๏ธ Option 1: Direct Connection (Croaster as Access Point) -1. On your computer, connect to the WiFi network broadcasted by your Croaster device. (It will appear as something like `[XXXX] Croaster-XXXX`) -2. Open Artisan, go to Config โ†’ Port. +Use this method when Croaster is **not** connected to any WiFi network, or when you want a direct peer-to-peer connection. + +1. On your computer, connect to the WiFi network broadcasted by Croaster (e.g. `[XXXX] Croaster-XXXX`) +2. Open Artisan โ†’ **Config โ†’ Port** 3. Set the configuration as shown below: - ![image](images/Connect-Artisan-Directly.png) + + ![image](images/Connect-Artisan-Directly.png) ### ๐ŸŒ Option 2: Same WiFi Network (Croaster joins your WiFi) -If your Croaster is already connected to your home WiFi, and your laptop is on the same network: +Use this method when Croaster is already connected to your home/office WiFi network. -1. Open Artisan, go to Config โ†’ Port. -2. Enter the IP address assigned to Croaster (you can find this on the Croaster OLED screen or serial log). -3. Use the configuration shown here: - ![image](images/Connect-Artisan-Same-Network.png) +1. Make sure your laptop and Croaster are on the **same WiFi network** +2. Open Artisan โ†’ **Config โ†’ Port** +3. Enter the **IP address** shown on the Croaster OLED display (or via serial monitor) +4. Set the configuration as shown: + + ![image](images/Connect-Artisan-Same-Network.png) + +--- + +## โฌ†๏ธ OTA (Over-The-Air) Updates + +Croaster supports firmware updates without a USB cable, via the **ICRM app** over WebSocket (WiFi). + +- OTA is handled by the `OtaHandler` class, which receives binary firmware data in chunks +- Progress is shown on the OLED display during the update +- OTA requires the **custom partition scheme** (`custom32c3sm`) on ESP32C3 โ€” the `Huge APP` partition does **not** support OTA +- After a successful OTA update, Croaster restarts automatically + +--- + +## ๐Ÿงช Custom Commands + +Croaster accepts JSON-formatted commands over both WebSocket and BLE. The `CommandHandler` class dispatches all incoming commands. + +### Built-in Commands + +| Command JSON | Action | +|:---|:---| +| `{"cmd": "restart"}` | Restarts the device | +| `{"cmd": "erase"}` | Erases WiFi credentials and restarts | +| `{"cmd": "unit", "value": "F"}` | Switches temperature unit to Fahrenheit | +| `{"cmd": "unit", "value": "C"}` | Switches temperature unit to Celsius | +| `{"cmd": "interval", "value": 5}` | Sets data send interval to 5 seconds | +| `{"cmd": "correction", "bt": 1.5, "et": -0.5}` | Applies temperature correction offset | + +### Adding Custom Commands + +To add your own command, edit `CommandHandler.cpp` and add a new condition inside the `handleJsonCommand` method. The handler receives a parsed `JsonObject`, so you can read any key/value from the JSON payload. --- @@ -150,4 +279,14 @@ If your Croaster is already connected to your home WiFi, and your laptop is on t ## โค๏ธ Contributing -Pull requests and feedback are welcome! +Pull requests, bug reports, and feature requests are welcome! Feel free to open an issue or submit a PR on [GitHub](https://github.com/IiemB/Croaster). + +--- + +## ๐Ÿ”— Related Links + +- [ICRM App](https://iiemb.github.io/#/icrm) โ€” companion Android app for Croaster +- [Artisan Roaster Scope](https://artisan-scope.org/) โ€” open-source coffee roasting logger +- [WiFi Setup Video](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) โ€” quick visual guide +- [References & Advanced Setup](references.md) โ€” custom partitions, OTA, PlatformIO tips +- [FAQ](FAQ.md) โ€” frequently asked questions diff --git a/README_ID.md b/README_ID.md new file mode 100644 index 0000000..11f420a --- /dev/null +++ b/README_ID.md @@ -0,0 +1,292 @@ +# โ˜• Croaster - Monitor Sangrai Kopi Open Source + +> ๐Ÿ‡ฌ๐Ÿ‡ง English version available at [README.md](README.md) + +**Croaster** adalah sistem pemantau suhu ringan dan open-source yang dibangun di atas mikrokontroler berbasis ESP. Dirancang untuk para pecinta dan profesional sangrai kopi, sistem ini membaca dua sensor termokopel (Suhu Biji dan Suhu Lingkungan) dan menampilkan data secara real-time di layar OLED yang ringkas. Croaster terhubung mulus ke perangkat lunak sangrai populer melalui WiFi (WebSocket) dan BLE (khusus ESP32), sehingga kompatibel dengan aplikasi sangrai di desktop maupun ponsel. + +**Versi Firmware Saat Ini:** `0.45` + +--- + +## ๐Ÿ“‘ Daftar Isi + +- [Fitur](#-fitur) +- [Komponen Hardware](#-komponen-hardware) +- [Diagram Pengkabelan](#-diagram-pengkabelan) +- [Arsitektur Software](#-arsitektur-software) +- [Library & Dependensi](#-library--dependensi) +- [Cara Build dan Upload](#-cara-build-dan-upload) +- [Panduan Setup WiFi](#-panduan-setup-wifi) +- [Gambaran Komunikasi](#-gambaran-komunikasi) +- [Cara Menghubungkan Croaster dengan Artisan](#-cara-menghubungkan-croaster-dengan-artisan) +- [Update OTA (Over-The-Air)](#๏ธ-update-ota-over-the-air) +- [Perintah Kustom](#-perintah-kustom) +- [Lisensi](#-lisensi) +- [Kontribusi](#๏ธ-kontribusi) + +--- + +## ๐Ÿš€ Fitur + +* Mendukung **NodeMCU ESP8266** (hanya WiFi) +* Mendukung **ESP32C3 Super Mini** (WiFi & BLE) +* Pemantauan real-time dua sensor termokopel **MAX6675**: + - **BT** โ€” Bean Temperature / Suhu Biji (di dalam drum) + - **ET** โ€” Environment Temperature / Suhu Lingkungan (exhaust/inlet) +* Kalkulasi **Rate of Rise (RoR)** untuk BT dan ET, diperbarui otomatis +* Pergantian satuan suhu: **Celsius** atau **Fahrenheit** +* Interval pengiriman data yang dapat dikonfigurasi (default: setiap **3 detik**) +* **Penghalusan suhu** bawaan (faktor penghalusan: 5) untuk mengurangi noise sensor +* Tampilan visual di **layar OLED 128ร—64** (SSD1306, I2C) +* Komunikasi WiFi via **WebSocket** di port **81**, kompatibel dengan: + + [**Artisan Roaster Scope**](https://artisan-scope.org/) โ€” logger sangrai standar industri + + [**Aplikasi ICRM**](https://iiemb.github.io/#/icrm) โ€” aplikasi pendamping mobile (Android) +* **Komunikasi BLE** (khusus ESP32) untuk [**aplikasi ICRM**](https://iiemb.github.io/#/icrm) +* **Update firmware OTA (Over-The-Air)** via WebSocket (aplikasi ICRM) +* **Captive portal WiFiManager** untuk setup WiFi yang mudah โ€” tanpa perlu flash ulang +* Penamaan perangkat unik berdasarkan chip ID (contoh: `Croaster-A1B2`) +* **Mode dummy** untuk pengembangan dan pengujian tanpa sensor fisik +* Sistem perintah JSON kustom melalui kelas `CommandHandler` yang terpusat +* Mudah diperluas dengan perintah buatan pengguna + +--- + +## ๐Ÿงฉ Komponen Hardware + +| Komponen | Keterangan | +|:---|:---| +| 1ร— [NodeMCU ESP8266](images/NodeMCU-ESP8266.png) **atau** [ESP32C3 Super Mini](images/ESP32C3-Super-Mini.png) | Mikrokontroler utama | +| 1ร— [Layar OLED 128ร—64 (SSD1306, I2C)](images/OLED-Display.png) | Tampilan suhu real-time | +| 2ร— [Modul termokopel MAX6675](images/MAX6675.png) | ADC termokopel K-type berbasis SPI | +| 2ร— [Probe termokopel K-type](images/Type-K-thermocouple.png) | Probe suhu (BT & ET) | + +> Semua komponen beroperasi pada **3.3V**. Pastikan catu daya Anda dapat menangani total konsumsi arus dari kedua sensor dan layar. + +--- + +## ๐Ÿ”Œ Diagram Pengkabelan + +| |**NodeMCU ESP8266**|**ESP32C3 Super Mini**| +|:---|:---:|:---:| +|**Layar OLED**|GND โ†’ **GND**|GND โ†’ **GND**| +| |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| +| |SCL โ†’ **D1**|SCL โ†’ **GPIO9**| +| |SDA โ†’ **D2**|SDA โ†’ **GPIO8**| +|||โ €| +|**Sensor ET** (Suhu Lingkungan)|GND โ†’ **GND**|GND โ†’ **GND**| +| |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| +| |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| +| |SO โ†’ **D7**|SO โ†’ **GPIO5**| +| |CS โ†’ **D8**|CS โ†’ **GPIO6**| +|||โ €| +|**Sensor BT** (Suhu Biji)|GND โ†’ **GND**|GND โ†’ **GND**| +| |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| +| |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| +| |SO โ†’ **D7**|SO โ†’ **GPIO5**| +| |CS โ†’ **D6**|CS โ†’ **GPIO7**| + +> Kedua sensor berbagi jalur **SCK** dan **SO** (bus SPI). Keduanya dibedakan oleh pin **CS** masing-masing. + +--- + +## ๐Ÿ›  Arsitektur Software + +Croaster menggunakan **arsitektur C++ modular** yang bersih, dibangun dengan framework Arduino. Setiap subsistem dikemas dalam kelasnya sendiri: + +| Modul | File | Tanggung Jawab | +|:---|:---|:---| +| `CroasterCore` | `CroasterCore.h/.cpp` | Pembacaan sensor, kalkulasi RoR, penghalusan suhu, state data | +| `DisplayManager` | `DisplayManager.h/.cpp` | Loop rendering OLED, layar status | +| `CommandHandler` | `CommandHandler.h/.cpp` | Parsing dan dispatching perintah JSON (BLE & WebSocket) | +| `WebSocketManager` | `WebSocketManager.h/.cpp` | Server WebSocket, broadcast data, trigger OTA | +| `BleManager` | `BleManager.h/.cpp` | Server BLE, notify karakteristik, penerimaan perintah *(khusus ESP32)* | +| `OtaHandler` | `OtaHandler.h/.cpp` | Penanganan update OTA biner via WebSocket | +| `WiFiManagerUtil` | `WiFiManagerUtil.h/.cpp` | Setup dan lifecycle captive portal WiFiManager | +| `DeviceIdentity` | `DeviceIdentity.h/.cpp` | Helper chip ID, nama perangkat, alamat IP | + +### Alur Data + +``` +Sensor MAX6675 โ†’ CroasterCore (baca + halus + RoR) + โ†“ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + WebSocketManager BleManager (ESP32) + โ†“ โ†“ + Artisan / ICRM ICRM (Android) +``` + +--- + +## ๐Ÿ“ฆ Library & Dependensi + +| Library | Kegunaan | +|:---|:---| +| [arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) | Server WebSocket | +| [ArduinoJson](https://arduinojson.org/) `^7.4.3` | Parsing dan serialisasi perintah JSON | +| [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) `^2.5.16` | Driver layar OLED | +| [MAX6675_Thermocouple](https://github.com/YuriiSalimov/MAX6675_Thermocouple) `^2.0.2` | Pembacaan sensor termokopel | +| [WiFiManager](https://github.com/tzapu/WiFiManager) `^2.0.17` | Setup WiFi via captive portal | +| ESP32 BLE Arduino *(bawaan inti ESP32)* | Server BLE & karakteristik | + +--- + +## ๐Ÿ”ง Cara Build dan Upload + +### โœ… PlatformIO (direkomendasikan untuk ESP8266 & ESP32C3) + +1. Install [PlatformIO](https://platformio.org/) (ekstensi VS Code atau CLI) +2. Clone repositori: + + ```bash + git clone git@github.com:IiemB/Croaster.git + cd Croaster + ``` + +3. Periksa `platformio.ini` dan pilih environment target Anda +4. Upload firmware: + + ```bash + # Untuk ESP8266 + pio run -e esp8266 -t upload + + # Untuk ESP32C3 + pio run -e esp32c3 -t upload + ``` + +> **Catatan:** ESP32C3 Super Mini menggunakan skema partisi kustom (`custom32c3sm.csv`) untuk memaksimalkan penyimpanan aplikasi. Lihat [references.md](references.md) untuk detail setup. + +### โœ… Arduino IDE (alternatif, diperlukan untuk board Makergo ESP32C3) + +> Arduino IDE diperlukan jika Anda menggunakan definisi board **Makergo ESP32C3 SuperMini**, yang belum sepenuhnya didukung oleh PlatformIO. + +1. Jalankan skrip konversi untuk menyalin file sumber ke folder sketch Arduino: + + ```bash + ./copy_to_ino.sh + ``` + +2. Buka folder `croaster-arduino/` di **Arduino IDE 2.x** +3. Pilih board Anda: + - **ESP8266** โ†’ `NodeMCU 1.0 (ESP-12E Module)` + - **ESP32C3** โ†’ `Makergo ESP32C3 SuperMini` +4. Untuk **ESP32C3**, pilih skema partisi: + - Gunakan `Huge APP` untuk ukuran sketch maksimum (OTA tidak didukung) + - Gunakan `Custom SuperMini` untuk mendukung OTA (lihat [references.md](references.md) untuk setup) + + > [!NOTE] + > Partisi `Huge APP` **tidak** mendukung OTA via Aplikasi ICRM. Untuk mengaktifkan OTA, ikuti langkah partisi kustom di [references.md](references.md). + +5. Install semua library yang diperlukan via Arduino Library Manager (lihat [Library & Dependensi](#-library--dependensi)) +6. Upload via `Sketch โ†’ Upload` + +--- + +## ๐Ÿ”— Panduan Setup WiFi + +Croaster menggunakan **WiFiManager** untuk mengelola kredensial WiFi tanpa perlu flash ulang. Pada boot pertama (atau setelah menghapus kredensial), Croaster membuat access point sendiri: + +1. Di ponsel atau komputer Anda, hubungkan ke jaringan WiFi bernama `[XXXX] Croaster-XXXX` +2. Captive portal akan terbuka otomatis โ€” masukkan SSID dan password WiFi rumah Anda +3. Croaster akan menyimpan kredensial dan terhubung otomatis di boot berikutnya +4. Alamat IP yang ditetapkan ke Croaster ditampilkan di layar OLED + +Untuk panduan visual, lihat: โžก๏ธ [Cara Menghubungkan ke WiFi - YouTube](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) + +--- + +## ๐Ÿ“ก Gambaran Komunikasi + +### WebSocket (WiFi) + +- **Port:** `81` +- **Protokol:** WebSocket (frame teks untuk perintah JSON, frame biner untuk OTA) +- **Format data:** JSON, di-broadcast setiap `intervalSend` detik (default: 3 detik) +- Kompatibel dengan **Artisan Roaster Scope** dan **aplikasi ICRM** (Android) + +### BLE (khusus ESP32) + +- **UUID Service:** `1cc9b045-a6e9-4bd5-b874-07d4f2d57843` +- **UUID Karakteristik Data:** `d56d0059-ad65-43f3-b971-431d48f89a69` +- Mendukung notify (push data) dan write (penerimaan perintah) +- Kompatibel dengan **aplikasi ICRM** (khusus Android) + +--- + +## ๐Ÿ”Œ Cara Menghubungkan Croaster dengan Artisan + +Anda dapat menghubungkan Croaster ke Artisan menggunakan koneksi WiFi langsung atau melalui jaringan WiFi rumah/lokal Anda. + +### ๐Ÿ–ฅ๏ธ Opsi 1: Koneksi Langsung (Croaster sebagai Access Point) + +Gunakan metode ini ketika Croaster **tidak** terhubung ke jaringan WiFi manapun, atau ketika Anda menginginkan koneksi peer-to-peer langsung. + +1. Di komputer Anda, hubungkan ke jaringan WiFi yang di-broadcast oleh Croaster (contoh: `[XXXX] Croaster-XXXX`) +2. Buka Artisan โ†’ **Config โ†’ Port** +3. Atur konfigurasi seperti yang ditunjukkan di bawah: + + ![image](images/Connect-Artisan-Directly.png) + +### ๐ŸŒ Opsi 2: Jaringan WiFi yang Sama (Croaster bergabung ke WiFi Anda) + +Gunakan metode ini ketika Croaster sudah terhubung ke jaringan WiFi rumah/kantor Anda. + +1. Pastikan laptop dan Croaster Anda berada di **jaringan WiFi yang sama** +2. Buka Artisan โ†’ **Config โ†’ Port** +3. Masukkan **alamat IP** yang ditampilkan di layar OLED Croaster (atau via serial monitor) +4. Atur konfigurasi seperti yang ditunjukkan: + + ![image](images/Connect-Artisan-Same-Network.png) + +--- + +## โฌ†๏ธ Update OTA (Over-The-Air) + +Croaster mendukung pembaruan firmware tanpa kabel USB, melalui **aplikasi ICRM** via WebSocket (WiFi). + +- OTA ditangani oleh kelas `OtaHandler`, yang menerima data firmware biner secara bertahap +- Kemajuan update ditampilkan di layar OLED selama proses berlangsung +- OTA memerlukan **skema partisi kustom** (`custom32c3sm`) pada ESP32C3 โ€” partisi `Huge APP` **tidak** mendukung OTA +- Setelah update OTA berhasil, Croaster restart otomatis + +--- + +## ๐Ÿงช Perintah Kustom + +Croaster menerima perintah berformat JSON melalui WebSocket maupun BLE. Kelas `CommandHandler` mengelola semua perintah yang masuk. + +### Perintah Bawaan + +| JSON Perintah | Aksi | +|:---|:---| +| `{"cmd": "restart"}` | Restart perangkat | +| `{"cmd": "erase"}` | Hapus kredensial WiFi dan restart | +| `{"cmd": "unit", "value": "F"}` | Ganti satuan suhu ke Fahrenheit | +| `{"cmd": "unit", "value": "C"}` | Ganti satuan suhu ke Celsius | +| `{"cmd": "interval", "value": 5}` | Atur interval pengiriman data ke 5 detik | +| `{"cmd": "correction", "bt": 1.5, "et": -0.5}` | Terapkan offset koreksi suhu | + +### Menambahkan Perintah Kustom + +Untuk menambahkan perintah Anda sendiri, edit `CommandHandler.cpp` dan tambahkan kondisi baru di dalam metode `handleJsonCommand`. Handler menerima `JsonObject` yang sudah di-parse, sehingga Anda dapat membaca key/value apapun dari payload JSON. + +--- + +## ๐Ÿ“˜ Lisensi + +[Lisensi MIT](LICENSE.md) โ€” bebas digunakan untuk keperluan pribadi dan komersial. Kontribusi sangat disambut! + +--- + +## โค๏ธ Kontribusi + +Pull request, laporan bug, dan permintaan fitur sangat disambut! Jangan ragu untuk membuka issue atau mengirimkan PR di [GitHub](https://github.com/IiemB/Croaster). + +--- + +## ๐Ÿ”— Tautan Terkait + +- [Aplikasi ICRM](https://iiemb.github.io/#/icrm) โ€” aplikasi Android pendamping untuk Croaster +- [Artisan Roaster Scope](https://artisan-scope.org/) โ€” logger sangrai kopi open-source +- [Video Setup WiFi](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) โ€” panduan visual singkat +- [Referensi & Setup Lanjutan](references.md) โ€” partisi kustom, OTA, tips PlatformIO +- [FAQ (Bahasa Indonesia)](FAQ_ID.md) โ€” pertanyaan yang sering diajukan From f4a95a3bb6fd6d2d36dfc0ac5bbcc9b83ea193c1 Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 23 May 2026 22:23:50 +0700 Subject: [PATCH 05/15] fix: add .DS_Store to .gitignore to prevent tracking of macOS system files --- .DS_Store | Bin 8196 -> 8196 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) diff --git a/.DS_Store b/.DS_Store index 4b8fd7481f83daa7b4e74e99a05ee2eb750eafe0..70dd142c927ba2db9f3ee8ca25471d12d65ee221 100644 GIT binary patch delta 289 zcmZp1XmQw}DiEitlh44wz`~%%kj{|FP?DSP;*yk;p9B=+Fx(aO?camrj;Qh}c;yQ+ z41<&Na|?ia7#NHcCN~QxGAZ7jyi7ojElK9Q<&PDUKM06Sz9+zg;3_c&OcoZ@k6Tps z25M6Q&?WiEE@>#;^4RDW+$B(XgiEl=qqyWI!{h~mN{oz?4-1}VVa_=#J=su5W%4$m g8fNC)1CtGeR3}FX?`PU!F8+hT;WEM{P3!F diff --git a/.gitignore b/.gitignore index 2daa7b0..f8cfd1c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /croaster/build croaster-arduino/* +.DS_Store From d61d201607ce87dbb7cb472ba946e5af3ed704eb Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 23 May 2026 22:28:17 +0700 Subject: [PATCH 06/15] fix: remove .DS_Store files to prevent tracking of macOS system files --- .DS_Store | Bin 8196 -> 0 bytes .github/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 .github/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 70dd142c927ba2db9f3ee8ca25471d12d65ee221..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMzl#$=6n>M0Gay1LIrNIPxXu%Em0GPw?3Hvjo=J|JvYQFXCG{%)2{s2-f^emU zg;-f>bqF@1ARJt4BZ3Hm_`R9A&70dqVz|V&WgLUk5X0>^AU@|QLbOmN3Sl4&~9XTC4ommYJMA=kmo2p!kp=>(rk&BC+ z&aAfSq+Ii%+{wyyD9W6U{UZ}jDz-Yzfq}q4kpb@AD|CUnbcc4l{(belmcCRs8m&aP zV5`<&-2D3E;PqAC-}$A#zb(EEtVc+t6G!LLHjSv|olfUI79%8v*2mPD*%!U%uG`|PtJ!3KC zjA=-1($t|X?{rmSWL&n0kQg34-L2jK@`^7|A!?jY9y2yBHKr#T9+rYF#TUf7Q zJpi^5DCKs^@7hm3n+xdeA#0fxBXLu7fMckPi#DM4(S0pU9w>T# zX%Z~q+&z6o-6)%T+HEC)6ulkmFz~-Kpel`KV*^$4PnK!Iy>^7LiNV5gnbjr* j6Nj+fwBykEABI>*&KEnKSq%>)dSJ$g$^au6z5W6zaZ&_hFd@tzOz3-sv0-|$m>v$LcP zQDQuZ$V`}ho5{>>=53qVB_f_auE#_pB8s7cgF`f*h&&f<$skzdA^R~hTG9<=bV*k& zZ!`Qw24wAy>4I`x3#{C3{bn?gwcOl~^SiRx>dQxGUH(OxrmIc4f?xdfe9rUX+oU^Y z-8*Lcu>W2TQ_u;1bqku|TGBOgnfr`#WYLk;!ArmB{T$z4>l}N`UzEFye>OrhfBFBK z7z4(DG4O2$P_sqiBSmYC0b{@zs2Py&gM$ht0V_rS=|JO;0Kf=lFX;Qd2+)ZEm;|g8 zVS%uQ0yUJw5rZ`xb{G7TfR&<#6Z@GlPMDd)35CN>h@E#QP86**28@9w14F$XN&U~h zKL0m^?9Lc42L2TT9Hn_WMM|Nzwvv-t>!7z#5%H@O>kxE&DTc3<;!~&>*qwBMNx(`G Q7Km*GJPp Date: Sun, 24 May 2026 00:03:06 +0700 Subject: [PATCH 07/15] refactor: simplify command handling by removing restart and erase parameters --- src/BleManager.cpp | 8 +------- src/CommandHandler.cpp | 16 +++++++--------- src/CommandHandler.h | 4 ++-- src/WebSocketManager.cpp | 8 +------- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 0e1bfdf..90d975f 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -36,7 +36,7 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks bool restart = false, erase = false; String response; - if (commandHandler->handle(raw, response, restart, erase)) + if (commandHandler->handle(raw, response)) { if (!response.isEmpty()) { @@ -44,12 +44,6 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks pCharacteristic->notify(); } - if (erase) - eraseESP(); - - if (restart) - restartESP(); - debugln("# [CMD-BLE] " + raw); } } diff --git a/src/CommandHandler.cpp b/src/CommandHandler.cpp index f62b652..708bb21 100644 --- a/src/CommandHandler.cpp +++ b/src/CommandHandler.cpp @@ -38,7 +38,7 @@ void CommandHandler::loop() } } -bool CommandHandler::handle(const String &json, String &responseOut, bool &restart, bool &erase) +bool CommandHandler::handle(const String &json, String &responseOut) { JsonDocument doc; @@ -48,15 +48,13 @@ bool CommandHandler::handle(const String &json, String &responseOut, bool &resta return false; } - restart = false; - erase = false; responseOut = ""; if (doc["command"].is()) { JsonObject json = doc.as(); - handleBasicCommand(json, responseOut, restart, erase); + handleBasicCommand(json, responseOut); return true; } @@ -70,7 +68,7 @@ bool CommandHandler::handle(const String &json, String &responseOut, bool &resta return false; } -void CommandHandler::handleBasicCommand(const JsonObject &json, String &responseOut, bool &restart, bool &erase) +void CommandHandler::handleBasicCommand(const JsonObject &json, String &responseOut) { if (!json["command"].is()) return; @@ -85,15 +83,15 @@ void CommandHandler::handleBasicCommand(const JsonObject &json, String &response } else if (command == "restartesp") { - restart = true; - responseOut = croaster.getJsonData(command); + + ESP.restart(); } else if (command == "erase") { - erase = true; - responseOut = croaster.getJsonData(command); + + eraseESP(); } else if (command == "dummyOn") { diff --git a/src/CommandHandler.h b/src/CommandHandler.h index cca0238..f32b5fb 100644 --- a/src/CommandHandler.h +++ b/src/CommandHandler.h @@ -31,7 +31,7 @@ class CommandHandler * @param restart Set to true if a restart is requested. * @param erase Set to true if an erase is requested. */ - void handleBasicCommand(const JsonObject &json, String &responseOut, bool &restart, bool &erase); + void handleBasicCommand(const JsonObject &json, String &responseOut); /** * @brief Handles JSON-formatted commands. @@ -73,5 +73,5 @@ class CommandHandler * @param erase Set to true if an erase is requested. * @return True if the command was handled successfully, false otherwise. */ - bool handle(const String &json, String &responseOut, bool &restart, bool &erase); + bool handle(const String &json, String &responseOut); }; diff --git a/src/WebSocketManager.cpp b/src/WebSocketManager.cpp index 28c5d46..3905530 100644 --- a/src/WebSocketManager.cpp +++ b/src/WebSocketManager.cpp @@ -24,7 +24,7 @@ void WebSocketManager::handleEvent(const String &cmd, uint8_t num) String response; - if (commandHandler->handle(cmd, response, restart, erase)) + if (commandHandler->handle(cmd, response)) { if (!response.isEmpty()) @@ -32,12 +32,6 @@ void WebSocketManager::handleEvent(const String &cmd, uint8_t num) server.sendTXT(num, response); } - if (erase) - eraseESP(); - - if (restart) - ESP.restart(); - debugln("# [CMD-SOCKET] " + cmd); } } From bcf09f98a12a79d048d6b8f463533b49401689aa Mon Sep 17 00:00:00 2001 From: IiemB Date: Tue, 26 May 2026 11:59:40 +0700 Subject: [PATCH 08/15] refactor: update command handling and improve JSON response generation --- .vscode/settings.json | 5 ++- src/CommandHandler.cpp | 30 ++++++++++--- src/CommandHandler.h | 10 ++++- src/Constants.h | 2 +- src/CroasterCore.cpp | 100 ++++++++++++++++++++++------------------- src/CroasterCore.h | 23 ++++++++-- 6 files changed, 109 insertions(+), 61 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f835073..bfd4071 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "C_Cpp.errorSquiggles": "Disabled", + "C_Cpp.errorSquiggles": "enabled", "files.associations": { "*.xml": "xml", "*.arb": "json", @@ -11,5 +11,6 @@ "files.exclude": { "**/croaster-arduino": true, "**/.pio": true - } + }, + "C_Cpp.default.compilerPath": "/Users/iiemb/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc" } \ No newline at end of file diff --git a/src/CommandHandler.cpp b/src/CommandHandler.cpp index 708bb21..69398f8 100644 --- a/src/CommandHandler.cpp +++ b/src/CommandHandler.cpp @@ -79,18 +79,14 @@ void CommandHandler::handleBasicCommand(const JsonObject &json, String &response { int id = json["id"].as(); - responseOut = croaster.getJsonData(command, true, id); + responseOut = croaster.getJsonData(id); } else if (command == "restartesp") { - responseOut = croaster.getJsonData(command); - ESP.restart(); } else if (command == "erase") { - responseOut = croaster.getJsonData(command); - eraseESP(); } else if (command == "dummyOn") @@ -113,6 +109,10 @@ void CommandHandler::handleBasicCommand(const JsonObject &json, String &response { displayManager.displayToggle(); } + else if (command == "getDeviceInfo") + { + responseOut = genResponseCommand(command, croaster.getDeviceInfo()); + } } void CommandHandler::handleJsonCommand(const JsonObject &json, String &responseOut) @@ -173,3 +173,23 @@ void CommandHandler::blinkBuiltinLED(uint8_t times, unsigned long blinkDelay) blinking = true; } + +String CommandHandler::genResponseCommand(const String command, const String response) +{ + JsonDocument doc; + + JsonDocument responseDoc; + + doc["command"] = command; + + if (!deserializeJson(responseDoc, response)) + { + doc["response"] = responseDoc; + } + + String jsonOutput; + + serializeJson(doc, jsonOutput); + + return jsonOutput; +} \ No newline at end of file diff --git a/src/CommandHandler.h b/src/CommandHandler.h index f32b5fb..b90d60a 100644 --- a/src/CommandHandler.h +++ b/src/CommandHandler.h @@ -47,6 +47,14 @@ class CommandHandler */ void blinkBuiltinLED(uint8_t times = 2, unsigned long blinkDelay = 250); + /** + * @brief Generates a JSON response for a given command. + * @param command The command for which to generate the response. + * @param response The response message to include in the JSON. + * @return A JSON-formatted string containing the command and response. + */ + String genResponseCommand(const String command, const String response); + public: /** * @brief Constructs a CommandHandler instance. @@ -69,8 +77,6 @@ class CommandHandler * @brief Processes an incoming command. * @param json The command in JSON format. * @param responseOut The response to send back. - * @param restart Set to true if a restart is requested. - * @param erase Set to true if an erase is requested. * @return True if the command was handled successfully, false otherwise. */ bool handle(const String &json, String &responseOut); diff --git a/src/Constants.h b/src/Constants.h index c265011..4327148 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -15,7 +15,7 @@ #define debugln(x) Serial.println(x) // Firmware version -const double version = 0.45; +const double version = 0.456; // Dummy mode const bool dummyMode = false; diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index 2cd7a79..b204bee 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -260,75 +260,81 @@ String CroasterCore::genRandomString(int length) return result; } -String CroasterCore::ssidName() +double CroasterCore::roundTo2(double value) { - return getDeviceName("[", "] Croaster V" + String(version)); + return round(value * 100.0) / 100.0; } -String CroasterCore::getJsonData(const String &message, const bool &skipCroaster, int id) +String CroasterCore::ssidName() { - String ipAddress = getIpAddress(); - - String ssid = getSsidName(); + return getDeviceName("[", "] Croaster V" + String(version, 3)); +} +String CroasterCore::getJsonData(int id) +{ JsonDocument doc; doc["id"] = id; + doc["name"] = ssidName(); - doc["versionCode"] = version; + JsonObject data = doc["data"].to(); - if (!ipAddress.isEmpty()) - doc["ipAddress"] = ipAddress; + data["interval"] = intervalSend; + data["timer"] = timer; + if (!isnan(tempBt)) + { + data["BT"] = roundTo2(tempBt); + data["DBT"] = roundTo2(rorBt); + } + if (!isnan(tempEt)) + { + data["ET"] = roundTo2(tempEt); + data["DET"] = roundTo2(rorEt); + } - if (!ssid.isEmpty()) - doc["ssid"] = ssid; + data["cBT"] = roundTo2(correctionBt); + data["cET"] = roundTo2(correctionEt); - if (!message.isEmpty()) - doc["message"] = message; + data["temp"] = tempUnit; - doc["name"] = ssidName(); + /* + NOTE + You can add more fields to the JSON document that will be readed by ICRM app, + such as historical data or other sensor readings. + */ - JsonObject data = doc["data"].to(); + JsonObject extra = data["extra"].to(); - if (!isnan(tempBt)) - data["BT"] = tempBt; - if (!isnan(tempEt)) - data["ET"] = tempEt; + extra["string"] = genRandomString(10); + extra["number"] = random(100, 999); + extra["boolean"] = random(0, 2) == 1; - if (!skipCroaster) - { - JsonObject croaster = doc["croaster"].to(); + String jsonOutput; - croaster["interval"] = intervalSend; - croaster["timer"] = timer; - if (!isnan(tempBt)) - { - croaster["tempBt"] = tempBt; - croaster["rorBt"] = rorBt; - } - if (!isnan(tempEt)) - { - croaster["tempEt"] = tempEt; - croaster["rorEt"] = rorEt; - } + serializeJson(doc, jsonOutput); + + return jsonOutput; +} + +String CroasterCore::getDeviceInfo() +{ + String ipAddress = getIpAddress(); - croaster["correctionBt"] = correctionBt; - croaster["correctionEt"] = correctionEt; + String ssid = getSsidName(); - croaster["tempUnit"] = tempUnit; + JsonDocument doc; - /* - NOTE - You can add more fields to the JSON document that will be readed by ICRM app, - such as historical data or other sensor readings. - */ + doc["ver"] = version; - // JsonObject extra = croaster["extra"].to(); + if (!ipAddress.isEmpty()) + doc["ip"] = ipAddress; - // extra["string"] = genRandomString(10); - // extra["number"] = random(100, 999); - // extra["boolean"] = random(0, 2) == 1; - } + if (!ssid.isEmpty()) + doc["ssid"] = ssid; + + doc["name"] = ssidName(); + + doc["timer"] = timer; String jsonOutput; diff --git a/src/CroasterCore.h b/src/CroasterCore.h index 51be1ce..a3046ee 100644 --- a/src/CroasterCore.h +++ b/src/CroasterCore.h @@ -58,8 +58,20 @@ class CroasterCore */ void resetHistory(String item = "something"); + /** + * @brief Generates a random string of a specified length. + * @param length The length of the random string to generate. + * @return A random string of the specified length. + */ String genRandomString(int length); + /** + * @brief Rounds a double value to 2 decimal places. + * @param value The value to round. + * @return The rounded value. + */ + double roundTo2(double value); + public: double timer = 0, rorEt = 0, rorBt = 0, tempEt = 0, tempBt = 0; @@ -124,17 +136,20 @@ class CroasterCore /** * @brief Retrieves JSON data based on the provided parameters. - * - * @param message Optional message to include in the JSON data. Defaults to an empty string. - * @param skipCroaster Flag to indicate whether to skip Croaster-specific processing. Defaults to false. * @param id Identifier used to fetch specific JSON data. Defaults to 0. * @return A String containing the JSON data. */ - String getJsonData(const String &message = "", const bool &skipCroaster = false, int id = 0); + String getJsonData(int id = 0); /** * @brief Retrieves the SSID name for the device. * @return The SSID name as a string. */ String ssidName(); + + /** + * @brief Retrieves the device information as a string. + * @return A string containing the device information. + */ + String getDeviceInfo(); }; From 0b77a98fda72545616822bc016fb5e5817c59914 Mon Sep 17 00:00:00 2001 From: IiemB Date: Thu, 28 May 2026 09:53:04 +0700 Subject: [PATCH 09/15] feat: add 'getExtra' command and corresponding data generation methods --- src/CommandHandler.cpp | 44 +++++++++++++++++++++++++++++++++++++++++- src/CommandHandler.h | 13 +++++++++++++ src/Constants.h | 2 +- src/CroasterCore.cpp | 28 --------------------------- src/CroasterCore.h | 7 ------- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/CommandHandler.cpp b/src/CommandHandler.cpp index 69398f8..b47b11c 100644 --- a/src/CommandHandler.cpp +++ b/src/CommandHandler.cpp @@ -113,6 +113,10 @@ void CommandHandler::handleBasicCommand(const JsonObject &json, String &response { responseOut = genResponseCommand(command, croaster.getDeviceInfo()); } + else if (command == "getExtra") + { + responseOut = genResponseCommand(command, getExtraData()); + } } void CommandHandler::handleJsonCommand(const JsonObject &json, String &responseOut) @@ -192,4 +196,42 @@ String CommandHandler::genResponseCommand(const String command, const String res serializeJson(doc, jsonOutput); return jsonOutput; -} \ No newline at end of file +} + +String CommandHandler::genRandomString(int length) +{ + const char charset[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + String result; + + for (size_t i = 0; i < length; i++) + { + result += charset[random(0, sizeof(charset) - 1)]; + } + + return result; +} + +String CommandHandler::getExtraData() +{ + + /* + NOTE + You can add more fields to the JSON document that will be readed by ICRM app, + such as historical data or other sensor readings. + */ + + JsonDocument doc; + + doc["string"] = genRandomString(10); + doc["number"] = random(100, 999); + doc["boolean"] = random(0, 2) == 1; + + String jsonOutput; + + serializeJson(doc, jsonOutput); + + return jsonOutput; +} diff --git a/src/CommandHandler.h b/src/CommandHandler.h index b90d60a..5c1dc7d 100644 --- a/src/CommandHandler.h +++ b/src/CommandHandler.h @@ -55,6 +55,19 @@ class CommandHandler */ String genResponseCommand(const String command, const String response); + /** + * @brief Generates a random string of a specified length. + * @param length The length of the random string to generate. + * @return A random string of the specified length. + */ + String genRandomString(int length); + + /** + * @brief Retrieves extra data for the "extra" command. + * @return A JSON-formatted string containing extra data. + */ + String getExtraData(); + public: /** * @brief Constructs a CommandHandler instance. diff --git a/src/Constants.h b/src/Constants.h index 4327148..6d56762 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -15,7 +15,7 @@ #define debugln(x) Serial.println(x) // Firmware version -const double version = 0.456; +const double version = 0.457; // Dummy mode const bool dummyMode = false; diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index b204bee..6827373 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -244,22 +244,6 @@ void CroasterCore::resetHistory(String item) debugln("# Histories reset due to " + item + " change."); } -String CroasterCore::genRandomString(int length) -{ - const char charset[] = "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; - - String result; - - for (size_t i = 0; i < length; i++) - { - result += charset[random(0, sizeof(charset) - 1)]; - } - - return result; -} - double CroasterCore::roundTo2(double value) { return round(value * 100.0) / 100.0; @@ -297,18 +281,6 @@ String CroasterCore::getJsonData(int id) data["temp"] = tempUnit; - /* - NOTE - You can add more fields to the JSON document that will be readed by ICRM app, - such as historical data or other sensor readings. - */ - - JsonObject extra = data["extra"].to(); - - extra["string"] = genRandomString(10); - extra["number"] = random(100, 999); - extra["boolean"] = random(0, 2) == 1; - String jsonOutput; serializeJson(doc, jsonOutput); diff --git a/src/CroasterCore.h b/src/CroasterCore.h index a3046ee..59efede 100644 --- a/src/CroasterCore.h +++ b/src/CroasterCore.h @@ -58,13 +58,6 @@ class CroasterCore */ void resetHistory(String item = "something"); - /** - * @brief Generates a random string of a specified length. - * @param length The length of the random string to generate. - * @return A random string of the specified length. - */ - String genRandomString(int length); - /** * @brief Rounds a double value to 2 decimal places. * @param value The value to round. From 9c37f0e9e850b6fd3992a4b4300d81e6fdf36138 Mon Sep 17 00:00:00 2001 From: IiemB Date: Thu, 28 May 2026 22:11:46 +0700 Subject: [PATCH 10/15] chore: update firmware version to 0.46 and modify command structure in documentation --- CHANGELOG.md | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ FAQ.md | 39 +++++---- FAQ_ID.md | 39 +++++---- README.md | 35 +++++--- README_ID.md | 35 +++++--- src/Constants.h | 2 +- 6 files changed, 318 insertions(+), 51 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6410836 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,219 @@ +# Changelog โ€” Croaster Firmware + +All notable changes to the Croaster firmware are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +--- + +## [0.46] โ€” 2026-05-24 to 2026-05-28 (Current) + +### Added +- `getExtra` command with corresponding data generation methods (`genRandomString`, random number/boolean) + +### Changed +- Refactored command handling and improved JSON response generation +- Simplified command handling by removing `restart` and `erase` parameters from `handleJsonCommand` + +### Fixed +- Removed `.DS_Store` files and added to `.gitignore` + +--- + +## [0.45] โ€” 2026-02-28 to 2026-05-13 + +### Added +- `displayToggle` command to toggle OLED display on/off +- BLE MTU set to 517 for improved throughput +- `genRandomString` function for generating random strings +- Indonesian FAQ (`FAQ_ID.md`) and Indonesian README (`README_ID.md`) translations +- Issue and pull request templates for contribution guidelines + +### Changed +- Updated firmware progress display during OTA updates +- Updated library dependencies (`platformio.ini`) +- Set `dummyMode` to `false` (production default) + +### Fixed +- Corrected spelling of `genRandomString` +- Updated JSON handling comments +- Updated WiFiManager library version and improved `platformio.ini` formatting + +--- + +## [0.44] โ€” 2025-05-15 + +### Added +- SSID name included in JSON data response + +### Fixed +- Updated firmware version to `0.44` + +--- + +## [0.42 โ€“ 0.43] โ€” 2025-05-01 to 2025-05-06 + +### Added +- Custom partition configuration (`custom32c3sm.csv`) for ESP32C3 Super Mini to support OTA +- OTA update handling via `OtaHandler` class +- `resetState` method in `OtaHandler`; updated `begin` method to call it + +### Changed +- Refactored `OtaHandler`: replaced `debugf` with `debugln`, updated `finalize` method +- Refactored `DisplayManager` to handle OLED presence checks and simplify `begin` method +- Streamlined WiFi setup by removing redundant mode setting +- Standardized temperature variable naming across `CroasterCore` and `DisplayManager` +- Updated temperature handling and command processing for improved clarity + +### Fixed +- Initialized `Wire` in `isOledPresent` to ensure proper I2C communication +- Updated `dummyMode` to `false` and set WiFi mode in setup + +--- + +## [0.41] โ€” 2025-05-01 + +### Added +- OTA update handling; firmware version bumped to `0.41` + +### Changed +- Removed OTA handling from `BleManager` (moved to dedicated `OtaHandler`) + +--- + +## [0.40] โ€” 2025-05-01 + +### Changed +- Refactored firmware to version `0.40` +- Added partitions configuration and reference link + +--- + +## [0.31] โ€” 2025-04-25 to 2025-04-28 + +### Changed +- Updated GPIO pin assignments in README +- Enhanced device identity functions for IP and SSID retrieval +- Improved JSON command handling in `CommandHandler` +- Enhanced sensor reading logic in `CroasterCore` +- Optimized display management in `DisplayManager` +- Updated blink logic and delay in `CommandHandler` +- Added `blinkIndicator` method in `DisplayManager` +- Adjusted `intervalSendData` handling for consistent timing + +--- + +## [0.30 โ€“ Refactor Period] โ€” 2025-04-22 to 2025-04-26 + +### Added +- `DeviceIdentity` class for chip ID, device name, IP address helpers +- Loop functions for `WebSocket` and `WiFiManager` for better code organization +- Screen rotation functionality in `DisplayManager` +- `testdrawline` function in `DisplayManager` for visual debugging +- IP address display toggle in `DisplayManager` +- `CommandHandler` class for centralized JSON command parsing and dispatching (BLE & WebSocket) +- ESP32 and ESP8266 conditional compilation (`#if defined(ESP32)` / `#if defined(ESP8266)`) +- Detailed class and method documentation (Doxygen-style headers) + +### Changed +- Integrated `DisplayManager` into command handling and BLE setup +- Refactored BLE setup to use device name and `CommandHandler` +- Simplified BLE and WebSocket handling by integrating `CommandHandler` +- Updated `getDeviceName` and `getShortChipId` functions for improved prefix/suffix handling +- Optimized setup sequence โ€” `Serial` initialization before manager setup +- Modularized command handling and improved temperature reading logic +- Updated JSON command handling to use proper JSON serialization and logging +- Changed `croasterInterval` type to `unsigned long` in `DisplayManager` +- Reordered constructor initialization in `WebSocketManager` +- Removed inheritance from `BLEServerCallbacks` and `BLECharacteristicCallbacks` in `BleManager` + +### Fixed +- Changed `croasterInterval` type to `unsigned long` for consistency in `broadcastData` +- Updated git clone URL to use SSH format in README files + +--- + +## [2.48] โ€” 2025-02-24 + +### Changed +- Updated pin definitions +- Refactored display output and WiFi handling +- Renamed target directory and file for clarity +- Removed unused library + +--- + +## [2.47] โ€” 2025-02-24 + +### Changed +- Updated pin definitions and refactored code + +--- + +## [2.46] โ€” 2025-02-21 + +### Changed +- Minor bump; version consistency update + +--- + +## [2.45] โ€” 2025-02-21 + +### Changed +- Added Croaster Arduino sketch (`.ino`) and updated conversion script +- Updated `copy_to_ino.sh` to generate the Arduino sketch; implemented BLE server functionality +- Added conversion script for `main.cpp` and `Croaster.h`; refactored BLE characteristic handling + +--- + +## [Early Refactor โ€” ESP32 Core] โ€” 2025-02-20 to 2025-02-24 + +### Added +- `Croaster` class with sensor integration, temperature conversion, and JSON data handling +- LED blinking functionality based on WiFi and BLE connection status +- BLE server functionality (`BleManager`) + +### Changed +- Updated serial communication speeds and flash size in `platformio.ini` +- Refactored BLE data handling to use `String` type +- Updated Croaster class to use new MAX6675 library +- Adjusted pin definitions for new hardware +- Enhanced dummy data handling + +--- + +## [Initial Modular Refactor] โ€” 2025-04-19 to 2025-04-21 + +### Added +- File conversion script (`copy_to_ino.sh`) using Bash for Arduino sketch generation +- Dedicated `src/` directory structure for modular C++ source files + +### Changed +- Replaced `TempsManager` with `CroasterCore` across the codebase +- Modularized codebase: `BleManager`, `WiFiManagerUtil`, `WebSocketManager`, `DisplayManager`, `CroasterCore` +- Updated `DisplayManager` methods for improved data handling and display updates +- Replaced Dart file copying logic with a Bash script + +--- + +## [Pre-Modular] โ€” 2021-02-07 to 2025-02-19 + +### Added +- Initial project commit with ESP32-based BLE coffee roaster monitor (2021-02-07) +- Arduino `.ino` file support +- PlatformIO configuration (`platformio.ini`) for ESP8266 and ESP32 +- Basic Croaster class with sensor integration and JSON data output +- README and documentation (English) +- Artisan Roaster Scope connection instructions + +### Changed +- Updated project from Arduino-only to PlatformIO build system +- Updated to production dummy data settings +- Progressively enhanced display output, WiFi handling, and BLE connectivity +- Updated README multiple times for wiring diagrams, hardware components, and connection guides + +--- + +*For full commit history, run:* +```bash +git log --oneline +``` diff --git a/FAQ.md b/FAQ.md index 42864d6..459b784 100644 --- a/FAQ.md +++ b/FAQ.md @@ -76,7 +76,7 @@ See the [WiFi Setup Video](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) f ### I forgot my WiFi password or want to change the network. How do I reset? -Send the command `{"cmd": "erase"}` via WebSocket or BLE (ICRM app), or press the reset button while holding the boot button on the ESP32C3. This erases saved WiFi credentials and puts Croaster back into access point mode for reconfiguration. +Send the command `{"command": "erase"}` via WebSocket or BLE (ICRM app), or press the reset button while holding the boot button on the ESP32C3. This erases saved WiFi credentials and puts Croaster back into access point mode for reconfiguration. --- @@ -96,7 +96,7 @@ Croaster's WebSocket server runs on **port 81**. By default, data is broadcast every **3 seconds**. You can change this by sending: ```json -{"cmd": "interval", "value": 5} +{"command": {"interval": 5}} ``` Replace `5` with your desired interval in seconds. @@ -114,11 +114,11 @@ No. BLE is only available on the **ESP32** platform. The ESP8266 only supports W Croaster supports **Celsius (C)** and **Fahrenheit (F)**. Switch units by sending: ```json -{"cmd": "unit", "value": "F"} +{"command": {"tempUnit": "F"}} ``` or ```json -{"cmd": "unit", "value": "C"} +{"command": {"tempUnit": "C"}} ``` --- @@ -139,7 +139,7 @@ Croaster applies a **smoothing filter** (factor: 5) to reduce noise from the MAX Yes. Send a correction command: ```json -{"cmd": "correction", "bt": 1.5, "et": -0.5} +{"command": {"correctionBt": 1.5, "correctionEt": -0.5}} ``` This applies a `+1.5ยฐ` offset to BT and a `-0.5ยฐ` offset to ET. The correction is applied on top of the smoothed sensor reading. @@ -209,16 +209,25 @@ In this mode, Croaster generates simulated temperature data, so you can test the ### How do I add a custom command? -1. Open `CommandHandler.cpp` -2. Find the `handleJsonCommand` method -3. Add your condition, for example: - ```cpp - if (json["cmd"] == "mycommand") { - // your logic here - responseOut = "{\"status\": \"ok\"}"; - } - ``` -4. The command will be available over both WebSocket and BLE automatically. +For a **basic string command**, add a new `else if` branch inside `handleBasicCommand` in `CommandHandler.cpp`: +```cpp +else if (command == "mycommand") { + // your logic here + responseOut = "{\"status\": \"ok\"}"; +} +``` +Send it as: `{"command": "mycommand"}` + +For a **configuration command** (key-value style), add a new condition inside `handleJsonCommand`: +```cpp +if (json["mykey"].is()) { + String myValue = json["mykey"].as(); + // your logic here +} +``` +Send it as: `{"command": {"mykey": "somevalue"}}` + +Both types are available over WebSocket and BLE automatically. --- diff --git a/FAQ_ID.md b/FAQ_ID.md index c457a3b..74d1a8e 100644 --- a/FAQ_ID.md +++ b/FAQ_ID.md @@ -76,7 +76,7 @@ Lihat [Video Setup WiFi](https://www.youtube.com/watch?v=esNiudoCEcU&t=434s) unt ### Saya lupa password WiFi atau ingin mengganti jaringan. Bagaimana cara mereset? -Kirim perintah `{"cmd": "erase"}` via WebSocket atau BLE (aplikasi ICRM), atau tekan tombol reset sambil menahan tombol boot pada ESP32C3. Ini akan menghapus kredensial WiFi yang tersimpan dan menempatkan Croaster kembali ke mode access point untuk konfigurasi ulang. +Kirim perintah `{"command": "erase"}` via WebSocket atau BLE (aplikasi ICRM), atau tekan tombol reset sambil menahan tombol boot pada ESP32C3. Ini akan menghapus kredensial WiFi yang tersimpan dan menempatkan Croaster kembali ke mode access point untuk konfigurasi ulang. --- @@ -96,7 +96,7 @@ Server WebSocket Croaster berjalan di **port 81**. Secara default, data di-broadcast setiap **3 detik**. Anda dapat mengubahnya dengan mengirim: ```json -{"cmd": "interval", "value": 5} +{"command": {"interval": 5}} ``` Ganti `5` dengan interval yang Anda inginkan dalam satuan detik. @@ -114,11 +114,11 @@ Tidak. BLE hanya tersedia di platform **ESP32**. ESP8266 hanya mendukung WiFi. Croaster mendukung **Celsius (C)** dan **Fahrenheit (F)**. Ganti satuan dengan mengirim: ```json -{"cmd": "unit", "value": "F"} +{"command": {"tempUnit": "F"}} ``` atau ```json -{"cmd": "unit", "value": "C"} +{"command": {"tempUnit": "C"}} ``` --- @@ -139,7 +139,7 @@ Croaster menerapkan **filter penghalusan** (faktor: 5) untuk mengurangi noise da Ya. Kirim perintah koreksi: ```json -{"cmd": "correction", "bt": 1.5, "et": -0.5} +{"command": {"correctionBt": 1.5, "correctionEt": -0.5}} ``` Ini menerapkan offset `+1.5ยฐ` pada BT dan offset `-0.5ยฐ` pada ET. Koreksi diterapkan di atas pembacaan sensor yang sudah dihaluskan. @@ -209,16 +209,25 @@ Dalam mode ini, Croaster menghasilkan data suhu simulasi, sehingga Anda dapat me ### Bagaimana cara menambahkan perintah kustom? -1. Buka `CommandHandler.cpp` -2. Temukan metode `handleJsonCommand` -3. Tambahkan kondisi Anda, misalnya: - ```cpp - if (json["cmd"] == "perintahsaya") { - // logika Anda di sini - responseOut = "{\"status\": \"ok\"}"; - } - ``` -4. Perintah tersebut akan tersedia melalui WebSocket maupun BLE secara otomatis. +Untuk **perintah dasar (string)**, tambahkan cabang `else if` baru di dalam `handleBasicCommand` di `CommandHandler.cpp`: +```cpp +else if (command == "perintahsaya") { + // logika Anda di sini + responseOut = "{\"status\": \"ok\"}"; +} +``` +Kirim sebagai: `{"command": "perintahsaya"}` + +Untuk **perintah konfigurasi** (format key-value), tambahkan kondisi baru di dalam `handleJsonCommand`: +```cpp +if (json["kuncisaya"].is()) { + String nilai = json["kuncisaya"].as(); + // logika Anda di sini +} +``` +Kirim sebagai: `{"command": {"kuncisaya": "nilaitertentu"}}` + +Kedua tipe tersedia melalui WebSocket maupun BLE secara otomatis. --- diff --git a/README.md b/README.md index c438137..d220216 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Croaster** is a lightweight, open-source temperature monitoring system built on ESP-based microcontrollers. Designed for coffee roasting enthusiasts and professionals, it reads from two thermocouple sensors (Bean Temperature and Environment Temperature) and displays real-time data on a compact OLED screen. Croaster connects seamlessly to popular roasting software via WiFi (WebSocket) and BLE (ESP32 only), making it compatible with both desktop and mobile roasting apps. -**Current Firmware Version:** `0.45` +**Current Firmware Version:** `0.46` --- @@ -77,13 +77,13 @@ | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| -| |CS โ†’ **D8**|CS โ†’ **GPIO6**| +| |CS โ†’ **D6**|CS โ†’ **GPIO6**| |||โ €| |**BT Sensor** (Bean Temp)|GND โ†’ **GND**|GND โ†’ **GND**| | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| -| |CS โ†’ **D6**|CS โ†’ **GPIO7**| +| |CS โ†’ **D8**|CS โ†’ **GPIO7**| > Both sensors share the **SCK** and **SO** lines (SPI bus). They are distinguished by their individual **CS** pins. @@ -256,18 +256,33 @@ Croaster accepts JSON-formatted commands over both WebSocket and BLE. The `Comma ### Built-in Commands +All commands use the `"command"` key. Basic (string) commands: + +| Command JSON | Action | +|:---|:---| +| `{"command": "restartesp"}` | Restarts the device | +| `{"command": "erase"}` | Erases WiFi credentials and restarts | +| `{"command": "displayToggle"}` | Toggles the OLED display on/off | +| `{"command": "rotateScreen"}` | Rotates the OLED screen 180ยฐ | +| `{"command": "dummyOn"}` | Enables dummy/test mode (no real sensors needed) | +| `{"command": "dummyOff"}` | Disables dummy mode | +| `{"command": "blink"}` | Blinks the built-in LED | +| `{"command": "getDeviceInfo"}` | Returns device info (IP, SSID, firmware version) | +| `{"command": "getExtra"}` | Returns extra user-defined data | + +Configuration commands use a **nested JSON object** under `"command"`: + | Command JSON | Action | |:---|:---| -| `{"cmd": "restart"}` | Restarts the device | -| `{"cmd": "erase"}` | Erases WiFi credentials and restarts | -| `{"cmd": "unit", "value": "F"}` | Switches temperature unit to Fahrenheit | -| `{"cmd": "unit", "value": "C"}` | Switches temperature unit to Celsius | -| `{"cmd": "interval", "value": 5}` | Sets data send interval to 5 seconds | -| `{"cmd": "correction", "bt": 1.5, "et": -0.5}` | Applies temperature correction offset | +| `{"command": {"tempUnit": "F"}}` | Switches temperature unit to Fahrenheit | +| `{"command": {"tempUnit": "C"}}` | Switches temperature unit to Celsius | +| `{"command": {"interval": 5}}` | Sets data send interval to 5 seconds | +| `{"command": {"correctionBt": 1.5, "correctionEt": -0.5}}` | Applies temperature correction offset | +| `{"command": {"wifiConnect": {"ssid": "MyWiFi", "pass": "password"}}}` | Connects to a specified WiFi network | ### Adding Custom Commands -To add your own command, edit `CommandHandler.cpp` and add a new condition inside the `handleJsonCommand` method. The handler receives a parsed `JsonObject`, so you can read any key/value from the JSON payload. +To add a basic (string) command, add a new `else if` branch inside `handleBasicCommand` in `CommandHandler.cpp`. To add a configuration command, add a new condition inside `handleJsonCommand`. Both methods receive a parsed `JsonObject`, so you can read any key/value from the JSON payload. --- diff --git a/README_ID.md b/README_ID.md index 11f420a..79a89d6 100644 --- a/README_ID.md +++ b/README_ID.md @@ -4,7 +4,7 @@ **Croaster** adalah sistem pemantau suhu ringan dan open-source yang dibangun di atas mikrokontroler berbasis ESP. Dirancang untuk para pecinta dan profesional sangrai kopi, sistem ini membaca dua sensor termokopel (Suhu Biji dan Suhu Lingkungan) dan menampilkan data secara real-time di layar OLED yang ringkas. Croaster terhubung mulus ke perangkat lunak sangrai populer melalui WiFi (WebSocket) dan BLE (khusus ESP32), sehingga kompatibel dengan aplikasi sangrai di desktop maupun ponsel. -**Versi Firmware Saat Ini:** `0.45` +**Versi Firmware Saat Ini:** `0.46` --- @@ -77,13 +77,13 @@ | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| -| |CS โ†’ **D8**|CS โ†’ **GPIO6**| +| |CS โ†’ **D6**|CS โ†’ **GPIO6**| |||โ €| |**Sensor BT** (Suhu Biji)|GND โ†’ **GND**|GND โ†’ **GND**| | |VCC โ†’ **3.3V**|VCC โ†’ **3.3V**| | |SCK โ†’ **D5**|SCK โ†’ **GPIO4**| | |SO โ†’ **D7**|SO โ†’ **GPIO5**| -| |CS โ†’ **D6**|CS โ†’ **GPIO7**| +| |CS โ†’ **D8**|CS โ†’ **GPIO7**| > Kedua sensor berbagi jalur **SCK** dan **SO** (bus SPI). Keduanya dibedakan oleh pin **CS** masing-masing. @@ -256,18 +256,33 @@ Croaster menerima perintah berformat JSON melalui WebSocket maupun BLE. Kelas `C ### Perintah Bawaan +Semua perintah menggunakan kunci `"command"`. Perintah dasar (string): + +| JSON Perintah | Aksi | +|:---|:---| +| `{"command": "restartesp"}` | Restart perangkat | +| `{"command": "erase"}` | Hapus kredensial WiFi dan restart | +| `{"command": "displayToggle"}` | Menyalakan/mematikan layar OLED | +| `{"command": "rotateScreen"}` | Memutar layar OLED 180ยฐ | +| `{"command": "dummyOn"}` | Mengaktifkan mode dummy/pengujian (tanpa sensor fisik) | +| `{"command": "dummyOff"}` | Menonaktifkan mode dummy | +| `{"command": "blink"}` | Mengedipkan LED bawaan | +| `{"command": "getDeviceInfo"}` | Mengembalikan info perangkat (IP, SSID, versi firmware) | +| `{"command": "getExtra"}` | Mengembalikan data ekstra yang ditentukan pengguna | + +Perintah konfigurasi menggunakan **objek JSON bersarang** di bawah `"command"`: + | JSON Perintah | Aksi | |:---|:---| -| `{"cmd": "restart"}` | Restart perangkat | -| `{"cmd": "erase"}` | Hapus kredensial WiFi dan restart | -| `{"cmd": "unit", "value": "F"}` | Ganti satuan suhu ke Fahrenheit | -| `{"cmd": "unit", "value": "C"}` | Ganti satuan suhu ke Celsius | -| `{"cmd": "interval", "value": 5}` | Atur interval pengiriman data ke 5 detik | -| `{"cmd": "correction", "bt": 1.5, "et": -0.5}` | Terapkan offset koreksi suhu | +| `{"command": {"tempUnit": "F"}}` | Ganti satuan suhu ke Fahrenheit | +| `{"command": {"tempUnit": "C"}}` | Ganti satuan suhu ke Celsius | +| `{"command": {"interval": 5}}` | Atur interval pengiriman data ke 5 detik | +| `{"command": {"correctionBt": 1.5, "correctionEt": -0.5}}` | Terapkan offset koreksi suhu | +| `{"command": {"wifiConnect": {"ssid": "NamaWiFi", "pass": "password"}}}` | Hubungkan ke jaringan WiFi tertentu | ### Menambahkan Perintah Kustom -Untuk menambahkan perintah Anda sendiri, edit `CommandHandler.cpp` dan tambahkan kondisi baru di dalam metode `handleJsonCommand`. Handler menerima `JsonObject` yang sudah di-parse, sehingga Anda dapat membaca key/value apapun dari payload JSON. +Untuk menambahkan perintah dasar (string), tambahkan cabang `else if` baru di dalam `handleBasicCommand` di `CommandHandler.cpp`. Untuk menambahkan perintah konfigurasi, tambahkan kondisi baru di dalam `handleJsonCommand`. Kedua metode menerima `JsonObject` yang sudah di-parse, sehingga Anda dapat membaca key/value apapun dari payload JSON. --- diff --git a/src/Constants.h b/src/Constants.h index 6d56762..e7840e9 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -15,7 +15,7 @@ #define debugln(x) Serial.println(x) // Firmware version -const double version = 0.457; +const double version = 0.46; // Dummy mode const bool dummyMode = false; From ee0857feed81f5d61306c612c4768a31399fc94a Mon Sep 17 00:00:00 2001 From: IiemB Date: Thu, 28 May 2026 23:08:01 +0700 Subject: [PATCH 11/15] feat: implement OTA handling over BLE with timeout checks and update display status --- src/BleManager.cpp | 59 +++++++++++++++++++++++++++++---- src/BleManager.h | 7 +++- src/OtaHandler.cpp | 70 ++++++++++++++++++++++++++++++++++++++++ src/OtaHandler.h | 21 ++++++++++++ src/WebSocketManager.cpp | 7 +++- src/main.cpp | 2 +- 6 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 90d975f..7112227 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -19,24 +19,64 @@ class BleManager::ServerCallbacks : public BLEServerCallbacks { debugln("# BLE Client Disconnected"); parent->clientConnected = false; + + if (parent->otaHandler.isReceiving()) + { + debugln("# [OTA] BLE disconnected during OTA - restarting..."); + parent->displayManager->updatingStatusToggle(false); + restartESP(); + return; + } + BLEDevice::startAdvertising(); } }; class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks { - CommandHandler *commandHandler; + BleManager *parent; public: - CharacteristicCallbacks(CommandHandler *handler) : commandHandler(handler) {} + CharacteristicCallbacks(BleManager *parent) : parent(parent) {} void onWrite(BLECharacteristic *pCharacteristic) override { + // If OTA is in progress, treat incoming data as a binary firmware chunk + if (parent->otaHandler.isReceiving()) + { + uint8_t *data = pCharacteristic->getData(); + size_t len = pCharacteristic->getLength(); + + bool handled = parent->otaHandler.handleBinaryBle(data, len, pCharacteristic); + + if (handled) + { + int progress = int((double(parent->otaHandler.getWritten()) / double(parent->otaHandler.getTotal())) * 100.0); + + parent->displayManager->updatingStatusToggle(true); + + parent->displayManager->updateFirmwareUpdateProgress(progress); + } + + return; + } + String raw = pCharacteristic->getValue().c_str(); - bool restart = false, erase = false; + // Handle OTA begin command + if (raw.startsWith("OTA_BEGIN:")) + { + uint32_t size = raw.substring(10).toInt(); + + parent->otaHandler.begin(size); + + debugln("# [BLE] " + raw); + + return; + } + String response; - if (commandHandler->handle(raw, response)) + if (parent->commandHandler->handle(raw, response)) { if (!response.isEmpty()) { @@ -49,8 +89,8 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks } }; -BleManager::BleManager(CroasterCore &croaster, CommandHandler &commandHandler) - : croaster(&croaster), commandHandler(&commandHandler) {} +BleManager::BleManager(CroasterCore &croaster, CommandHandler &commandHandler, DisplayManager &displayManager) + : croaster(&croaster), commandHandler(&commandHandler), displayManager(&displayManager) {} void BleManager::begin() { @@ -71,7 +111,7 @@ void BleManager::begin() BLECharacteristic::PROPERTY_WRITE_NR); pDataCharacteristic->addDescriptor(new BLE2902()); - pDataCharacteristic->setCallbacks(new CharacteristicCallbacks(commandHandler)); + pDataCharacteristic->setCallbacks(new CharacteristicCallbacks(this)); pService->start(); BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); @@ -83,6 +123,8 @@ void BleManager::begin() void BleManager::loop() { + otaHandler.checkTimeout(); + broadcastData(); } @@ -96,6 +138,9 @@ void BleManager::broadcastData() if (!clientConnected || !pDataCharacteristic) return; + if (otaHandler.isReceiving()) + return; + unsigned long now = millis(); unsigned long interval = croaster->intervalSendData() * 1000; diff --git a/src/BleManager.h b/src/BleManager.h index 6eba157..0944e9c 100644 --- a/src/BleManager.h +++ b/src/BleManager.h @@ -10,6 +10,7 @@ #include "DisplayManager.h" #include "Constants.h" #include "CommandHandler.h" +#include "OtaHandler.h" /** * @class BleManager @@ -22,8 +23,9 @@ class BleManager * @brief Constructs a BleManager instance. * @param croaster Reference to the CroasterCore instance. * @param commandHandler Reference to the CommandHandler instance. + * @param displayManager Reference to the DisplayManager instance. */ - BleManager(CroasterCore &croaster, CommandHandler &commandHandler); + BleManager(CroasterCore &croaster, CommandHandler &commandHandler, DisplayManager &displayManager); /** * @brief Initializes the BLE server and characteristics. @@ -47,6 +49,9 @@ class BleManager CommandHandler *commandHandler = nullptr; ///< Pointer to the CommandHandler instance. CroasterCore *croaster = nullptr; ///< Pointer to the CroasterCore instance. + DisplayManager *displayManager = nullptr; ///< Pointer to the DisplayManager instance. + + OtaHandler otaHandler; ///< Handles OTA firmware updates over BLE. unsigned long lastSend = 0; ///< Timestamp of the last data broadcast. diff --git a/src/OtaHandler.cpp b/src/OtaHandler.cpp index 41cdc25..b90801d 100644 --- a/src/OtaHandler.cpp +++ b/src/OtaHandler.cpp @@ -11,6 +11,7 @@ void OtaHandler::begin(uint32_t totalSize) { this->totalSize = totalSize; written = 0; + lastChunkTime = millis(); state = State::Receiving; debugln("# [OTA] Begin, total size: " + String(totalSize)); } @@ -48,6 +49,8 @@ bool OtaHandler::handleBinary(uint8_t *data, size_t len, WebSocketsServer &serve written += writtenChunk; + lastChunkTime = millis(); + bool isFinished = written >= totalSize; doc["status"] = "receiving"; @@ -117,8 +120,75 @@ void OtaHandler::resetState() state = State::Idle; totalSize = 0; written = 0; + lastChunkTime = 0; } +void OtaHandler::checkTimeout() +{ + if (state != State::Receiving) + return; + + if (millis() - lastChunkTime > OTA_TIMEOUT_MS) + { + debugln("# [OTA] Timeout - no data received, aborting..."); + finalize(true); + } +} + +#if defined(ESP32) +bool OtaHandler::handleBinaryBle(uint8_t *data, size_t len, BLECharacteristic *pChar) +{ + JsonDocument doc; + + String jsonOutput; + + if (state != State::Receiving) + return false; + + size_t writtenChunk = Update.write(data, len); + if (writtenChunk != len) + { + doc["status"] = "failed"; + + serializeJson(doc, jsonOutput); + + pChar->setValue(jsonOutput.c_str()); + pChar->notify(); + + finalize(true); + + return false; + } + + written += writtenChunk; + + lastChunkTime = millis(); + + bool isFinished = written >= totalSize; + + if (isFinished) + doc["status"] = "done"; + else + doc["status"] = "receiving"; + + doc["written"] = double(written); + + doc["totalSize"] = double(totalSize); + + serializeJson(doc, jsonOutput); + + pChar->setValue(jsonOutput.c_str()); + pChar->notify(); + + debugln("# [OTA-BLE-JSON] " + jsonOutput); + + if (isFinished) + finalize(); + + return true; +} +#endif + bool OtaHandler::isReceiving() const { return state == State::Receiving; diff --git a/src/OtaHandler.h b/src/OtaHandler.h index 89d9eb9..3bc33be 100644 --- a/src/OtaHandler.h +++ b/src/OtaHandler.h @@ -6,6 +6,7 @@ #include "WiFiManagerUtil.h" #if defined(ESP32) #include +#include #elif defined(ESP8266) #include #endif @@ -38,6 +39,17 @@ class OtaHandler */ bool handleBinary(uint8_t *data, size_t len, WebSocketsServer &server, uint8_t clientId); +#if defined(ESP32) + /** + * @brief Processes incoming binary data for the OTA update via BLE. + * @param data Pointer to the binary data. + * @param len Length of the binary data. + * @param pChar Pointer to the BLE characteristic used to send progress notifications. + * @return True if the data was handled successfully, false otherwise. + */ + bool handleBinaryBle(uint8_t *data, size_t len, BLECharacteristic *pChar); +#endif + /** * @brief Checks if the OTA process is currently receiving data. * @return True if data is being received, false otherwise. @@ -56,6 +68,12 @@ class OtaHandler */ uint32_t getWritten() const; + /** + * @brief Aborts the OTA update if no data has been received within the timeout period. + * Should be called from the manager's loop(). + */ + void checkTimeout(); + private: /** * @enum State @@ -72,6 +90,9 @@ class OtaHandler uint32_t totalSize = 0; ///< Total size of the OTA update in bytes. uint32_t written = 0; ///< Number of bytes written so far. + unsigned long lastChunkTime = 0; ///< Timestamp of the last received chunk. + static const unsigned long OTA_TIMEOUT_MS = 10000; ///< Abort OTA if no data for 10 seconds. + /** * @brief Finalizes the OTA process upon successful completion. * @param server Reference to the WebSocketsServer handling the connection. diff --git a/src/WebSocketManager.cpp b/src/WebSocketManager.cpp index 3905530..595278e 100644 --- a/src/WebSocketManager.cpp +++ b/src/WebSocketManager.cpp @@ -47,7 +47,7 @@ void WebSocketManager::begin() debugln("# WebSocket Client Disconnected " + String(clientConnected)); - if (displayManager->isFirmwareUpdating()) + if (displayManager->isFirmwareUpdating() || otaHandler.isReceiving()) { displayManager->updatingStatusToggle(false); restartESP(); @@ -89,6 +89,8 @@ void WebSocketManager::loop() { server.loop(); + otaHandler.checkTimeout(); + broadcastData(); } @@ -103,6 +105,9 @@ void WebSocketManager::broadcastData() if (!isClientConnected()) return; + if (otaHandler.isReceiving()) + return; + unsigned long now = millis(); unsigned long interval = croaster->intervalSendData() * 1000; diff --git a/src/main.cpp b/src/main.cpp index 562e53e..d3fa86a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,7 @@ DisplayManager displayManager(croaster); CommandHandler commandHandler(croaster, displayManager); #if defined(ESP32) -BleManager bleManager(croaster, commandHandler); +BleManager bleManager(croaster, commandHandler, displayManager); #endif WebSocketManager wsManager(croaster, commandHandler, displayManager); From e323c0f847d94fb166df738e7b09225ebaceadcd Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 30 May 2026 08:23:26 +0700 Subject: [PATCH 12/15] feat: enhance OTA handling with improved status reporting and error management --- src/BleManager.cpp | 37 +++++++++------ src/OtaHandler.cpp | 100 ++++++--------------------------------- src/OtaHandler.h | 53 +++++++++------------ src/WebSocketManager.cpp | 30 ++++++++---- 4 files changed, 80 insertions(+), 140 deletions(-) diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 7112227..4d33b20 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -20,7 +20,7 @@ class BleManager::ServerCallbacks : public BLEServerCallbacks debugln("# BLE Client Disconnected"); parent->clientConnected = false; - if (parent->otaHandler.isReceiving()) + if (parent->displayManager->isFirmwareUpdating() || parent->otaHandler.isReceiving()) { debugln("# [OTA] BLE disconnected during OTA - restarting..."); parent->displayManager->updatingStatusToggle(false); @@ -40,23 +40,29 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks CharacteristicCallbacks(BleManager *parent) : parent(parent) {} void onWrite(BLECharacteristic *pCharacteristic) override { - // If OTA is in progress, treat incoming data as a binary firmware chunk + // Check OTA first to avoid parsing binary firmware bytes as a String. if (parent->otaHandler.isReceiving()) { - uint8_t *data = pCharacteristic->getData(); - size_t len = pCharacteristic->getLength(); + OtaResult result = parent->otaHandler.handleBinary(pCharacteristic->getData(), pCharacteristic->getLength()); - bool handled = parent->otaHandler.handleBinaryBle(data, len, pCharacteristic); + pCharacteristic->setValue(result.json.c_str()); + pCharacteristic->notify(); - if (handled) + if (result.hasError) { - int progress = int((double(parent->otaHandler.getWritten()) / double(parent->otaHandler.getTotal())) * 100.0); + parent->displayManager->updatingStatusToggle(false); - parent->displayManager->updatingStatusToggle(true); + parent->otaHandler.finalize(true); - parent->displayManager->updateFirmwareUpdateProgress(progress); + return; } + int progress = int((double(parent->otaHandler.getWritten()) / double(parent->otaHandler.getTotal())) * 100.0); + + parent->displayManager->updatingStatusToggle(true); + + parent->displayManager->updateFirmwareUpdateProgress(progress); + return; } @@ -123,9 +129,13 @@ void BleManager::begin() void BleManager::loop() { - otaHandler.checkTimeout(); - broadcastData(); + + if (otaHandler.isDone()) + { + displayManager->updatingStatusToggle(false); + otaHandler.finalize(); + } } bool BleManager::isClientConnected() const @@ -135,10 +145,7 @@ bool BleManager::isClientConnected() const void BleManager::broadcastData() { - if (!clientConnected || !pDataCharacteristic) - return; - - if (otaHandler.isReceiving()) + if (!clientConnected || !pDataCharacteristic || otaHandler.isReceiving()) return; unsigned long now = millis(); diff --git a/src/OtaHandler.cpp b/src/OtaHandler.cpp index b90801d..df10a23 100644 --- a/src/OtaHandler.cpp +++ b/src/OtaHandler.cpp @@ -11,7 +11,6 @@ void OtaHandler::begin(uint32_t totalSize) { this->totalSize = totalSize; written = 0; - lastChunkTime = millis(); state = State::Receiving; debugln("# [OTA] Begin, total size: " + String(totalSize)); } @@ -23,57 +22,50 @@ void OtaHandler::begin(uint32_t totalSize) } } -bool OtaHandler::handleBinary(uint8_t *data, size_t len, WebSocketsServer &server, uint8_t clientId) +OtaResult OtaHandler::handleBinary(uint8_t *data, size_t len) { JsonDocument doc; - String jsonOutput; if (state != State::Receiving) - return false; + { + doc["status"] = "failed"; + + serializeJson(doc, jsonOutput); + + return {jsonOutput, true}; + } size_t writtenChunk = Update.write(data, len); if (writtenChunk != len) { - doc["status"] = "failed"; serializeJson(doc, jsonOutput); - server.sendTXT(clientId, jsonOutput); - - finalize(true); - - return false; + return {jsonOutput, true}; } written += writtenChunk; - lastChunkTime = millis(); - bool isFinished = written >= totalSize; - doc["status"] = "receiving"; - if (isFinished) doc["status"] = "done"; else doc["status"] = "receiving"; doc["written"] = double(written); - doc["totalSize"] = double(totalSize); serializeJson(doc, jsonOutput); - server.sendTXT(clientId, jsonOutput); - debugln("# [OTA-JSON] " + jsonOutput); if (isFinished) - finalize(); + state = State::Done; - return true; + return {jsonOutput, false}; } void OtaHandler::finalize(bool hasError) @@ -120,78 +112,16 @@ void OtaHandler::resetState() state = State::Idle; totalSize = 0; written = 0; - lastChunkTime = 0; } -void OtaHandler::checkTimeout() -{ - if (state != State::Receiving) - return; - - if (millis() - lastChunkTime > OTA_TIMEOUT_MS) - { - debugln("# [OTA] Timeout - no data received, aborting..."); - finalize(true); - } -} - -#if defined(ESP32) -bool OtaHandler::handleBinaryBle(uint8_t *data, size_t len, BLECharacteristic *pChar) +bool OtaHandler::isReceiving() const { - JsonDocument doc; - - String jsonOutput; - - if (state != State::Receiving) - return false; - - size_t writtenChunk = Update.write(data, len); - if (writtenChunk != len) - { - doc["status"] = "failed"; - - serializeJson(doc, jsonOutput); - - pChar->setValue(jsonOutput.c_str()); - pChar->notify(); - - finalize(true); - - return false; - } - - written += writtenChunk; - - lastChunkTime = millis(); - - bool isFinished = written >= totalSize; - - if (isFinished) - doc["status"] = "done"; - else - doc["status"] = "receiving"; - - doc["written"] = double(written); - - doc["totalSize"] = double(totalSize); - - serializeJson(doc, jsonOutput); - - pChar->setValue(jsonOutput.c_str()); - pChar->notify(); - - debugln("# [OTA-BLE-JSON] " + jsonOutput); - - if (isFinished) - finalize(); - - return true; + return state == State::Receiving; } -#endif -bool OtaHandler::isReceiving() const +bool OtaHandler::isDone() const { - return state == State::Receiving; + return state == State::Done; } uint32_t OtaHandler::getTotal() const diff --git a/src/OtaHandler.h b/src/OtaHandler.h index 3bc33be..5ebd390 100644 --- a/src/OtaHandler.h +++ b/src/OtaHandler.h @@ -1,19 +1,27 @@ #pragma once #include -#include #include "Constants.h" #include "WiFiManagerUtil.h" #if defined(ESP32) #include -#include #elif defined(ESP8266) #include #endif +/** + * @struct OtaResult + * @brief Result returned by OtaHandler::handleBinary. + */ +struct OtaResult +{ + String json; ///< JSON progress payload to forward to the client. + bool hasError; ///< True if the write failed. +}; + /** * @class OtaHandler - * @brief Handles Over-The-Air (OTA) updates via WebSockets for ESP32 devices. + * @brief Handles Over-The-Air (OTA) firmware updates via WebSocket and BLE. */ class OtaHandler { @@ -33,22 +41,9 @@ class OtaHandler * @brief Processes incoming binary data for the OTA update. * @param data Pointer to the binary data. * @param len Length of the binary data. - * @param server Reference to the WebSocketsServer handling the connection. - * @param clientId ID of the client sending the data. - * @return True if the data was handled successfully, false otherwise. + * @return OtaResult containing the JSON payload and an error flag. */ - bool handleBinary(uint8_t *data, size_t len, WebSocketsServer &server, uint8_t clientId); - -#if defined(ESP32) - /** - * @brief Processes incoming binary data for the OTA update via BLE. - * @param data Pointer to the binary data. - * @param len Length of the binary data. - * @param pChar Pointer to the BLE characteristic used to send progress notifications. - * @return True if the data was handled successfully, false otherwise. - */ - bool handleBinaryBle(uint8_t *data, size_t len, BLECharacteristic *pChar); -#endif + OtaResult handleBinary(uint8_t *data, size_t len); /** * @brief Checks if the OTA process is currently receiving data. @@ -56,6 +51,12 @@ class OtaHandler */ bool isReceiving() const; + /** + * @brief Checks if the OTA process is complete. + * @return True if the OTA process is done, false otherwise. + */ + bool isDone() const; + /** * @brief Gets the total size of the OTA update. * @return The total size in bytes. @@ -69,10 +70,10 @@ class OtaHandler uint32_t getWritten() const; /** - * @brief Aborts the OTA update if no data has been received within the timeout period. - * Should be called from the manager's loop(). + * @brief Finalizes the OTA process. + * @param hasError If true, aborts the update and restarts. If false, ends successfully and restarts. */ - void checkTimeout(); + void finalize(bool hasError = false); private: /** @@ -90,16 +91,6 @@ class OtaHandler uint32_t totalSize = 0; ///< Total size of the OTA update in bytes. uint32_t written = 0; ///< Number of bytes written so far. - unsigned long lastChunkTime = 0; ///< Timestamp of the last received chunk. - static const unsigned long OTA_TIMEOUT_MS = 10000; ///< Abort OTA if no data for 10 seconds. - - /** - * @brief Finalizes the OTA process upon successful completion. - * @param server Reference to the WebSocketsServer handling the connection. - * @param clientId ID of the client that initiated the OTA process. - */ - void finalize(bool hasError = false); - /** * @brief Resets the internal state of the handler to its initial values. */ diff --git a/src/WebSocketManager.cpp b/src/WebSocketManager.cpp index 595278e..b0ce690 100644 --- a/src/WebSocketManager.cpp +++ b/src/WebSocketManager.cpp @@ -20,8 +20,6 @@ void WebSocketManager::handleEvent(const String &cmd, uint8_t num) return; } - bool restart = false, erase = false; - String response; if (commandHandler->handle(cmd, response)) @@ -49,8 +47,10 @@ void WebSocketManager::begin() if (displayManager->isFirmwareUpdating() || otaHandler.isReceiving()) { + debugln("# [OTA] WebSocket disconnected during OTA - restarting..."); displayManager->updatingStatusToggle(false); restartESP(); + return; } break; @@ -67,7 +67,18 @@ void WebSocketManager::begin() case WStype_BIN : if (otaHandler.isReceiving()) { - otaHandler.handleBinary(payload, length, server, num); + OtaResult result = otaHandler.handleBinary(payload, length); + + server.sendTXT(num, result.json); + + if (result.hasError) + { + displayManager->updatingStatusToggle(false); + + otaHandler.finalize(true); + + return; + } int progress = int((double(otaHandler.getWritten()) / double(otaHandler.getTotal())) * 100.0); @@ -89,9 +100,13 @@ void WebSocketManager::loop() { server.loop(); - otaHandler.checkTimeout(); - broadcastData(); + + if (otaHandler.isDone()) + { + displayManager->updatingStatusToggle(false); + otaHandler.finalize(); + } } bool WebSocketManager::isClientConnected() const @@ -102,10 +117,7 @@ bool WebSocketManager::isClientConnected() const void WebSocketManager::broadcastData() { - if (!isClientConnected()) - return; - - if (otaHandler.isReceiving()) + if (!isClientConnected() || otaHandler.isReceiving()) return; unsigned long now = millis(); From 88e7a023a1a9d456ef2d56afa3f7e3467e340b7c Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 30 May 2026 08:51:52 +0700 Subject: [PATCH 13/15] chore: update firmware version to 0.50 --- src/Constants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Constants.h b/src/Constants.h index e7840e9..3a818ff 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -15,7 +15,7 @@ #define debugln(x) Serial.println(x) // Firmware version -const double version = 0.46; +const double version = 0.50; // Dummy mode const bool dummyMode = false; From 17bb68870753e81788c9ee54670e0d53a14023f6 Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 30 May 2026 09:12:55 +0700 Subject: [PATCH 14/15] feat: update changelog and documentation for OTA over BLE support, timeout checks, and improved status reporting --- CHANGELOG.md | 16 +++++++++++++++- FAQ.md | 15 +++++++++------ FAQ_ID.md | 15 +++++++++------ README.md | 9 +++++---- README_ID.md | 9 +++++---- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6410836..5ea5206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- -## [0.46] โ€” 2026-05-24 to 2026-05-28 (Current) +## [0.50] โ€” 2026-05-30 (Current) + +### Added +- **OTA over BLE** (ESP32 only): firmware updates can now be performed via BLE in addition to WebSocket/WiFi +- Timeout checks during BLE OTA to handle stalled transfers + +### Changed +- Improved OTA status reporting: `OtaHandler` now returns an `OtaResult` struct containing a JSON progress payload and an error flag +- Simplified `OtaHandler` internals โ€” reduced code size while improving clarity +- `BleManager` updated to relay OTA binary data to `OtaHandler` and forward progress JSON back to the BLE client +- `WebSocketManager` updated to forward OTA progress JSON back to the WebSocket client consistently + +--- + +## [0.46] โ€” 2026-05-28 ### Added - `getExtra` command with corresponding data generation methods (`genRandomString`, random number/boolean) diff --git a/FAQ.md b/FAQ.md index 459b784..eb02eb8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -14,10 +14,10 @@ Croaster is an open-source firmware for ESP8266 and ESP32C3-based microcontrolle ### Which microcontrollers are supported? -| Board | WiFi | BLE | OTA | -|:---|:---:|:---:|:---:| -| NodeMCU ESP8266 | โœ… | โŒ | โœ… | -| ESP32C3 Super Mini | โœ… | โœ… | โœ… | +| Board | WiFi | BLE | OTA (WiFi) | OTA (BLE) | +|:---|:---:|:---:|:---:|:---:| +| NodeMCU ESP8266 | โœ… | โŒ | โœ… | โŒ | +| ESP32C3 Super Mini | โœ… | โœ… | โœ… | โœ… | --- @@ -179,7 +179,10 @@ It is a **custom partition table** for the ESP32C3 Super Mini. This partition la ### How do I update the firmware over the air? -Use the **ICRM app** on Android. The app will send the compiled firmware binary over WebSocket. Croaster receives it in chunks, writes it to flash, and restarts automatically once complete. +Use the **ICRM app** on Android. The app supports OTA over both WiFi (WebSocket) and BLE (ESP32 only): + +- **WiFi OTA:** The app sends the compiled firmware binary over WebSocket. Croaster receives it in chunks, writes it to flash, and restarts automatically once complete. +- **BLE OTA:** The app sends the firmware binary over BLE. `BleManager` relays the data to `OtaHandler`, which processes it in chunks with timeout checks. Progress is reported back to the app as JSON. --- @@ -191,7 +194,7 @@ The most common reason is that you are using the **Huge APP** partition scheme, ### Does OTA work on ESP8266? -OTA via the ICRM app is supported on ESP8266, as long as you are using a partition scheme that includes OTA space (the default NodeMCU scheme supports this). +OTA via WiFi (WebSocket) using the ICRM app is supported on ESP8266, as long as you are using a partition scheme that includes OTA space (the default NodeMCU scheme supports this). BLE OTA is not available on ESP8266. --- diff --git a/FAQ_ID.md b/FAQ_ID.md index 74d1a8e..86db5fc 100644 --- a/FAQ_ID.md +++ b/FAQ_ID.md @@ -14,10 +14,10 @@ Croaster adalah firmware open-source untuk mikrokontroler berbasis ESP8266 dan E ### Mikrokontroler apa saja yang didukung? -| Board | WiFi | BLE | OTA | -|:---|:---:|:---:|:---:| -| NodeMCU ESP8266 | โœ… | โŒ | โœ… | -| ESP32C3 Super Mini | โœ… | โœ… | โœ… | +| Board | WiFi | BLE | OTA (WiFi) | OTA (BLE) | +|:---|:---:|:---:|:---:|:---:| +| NodeMCU ESP8266 | โœ… | โŒ | โœ… | โŒ | +| ESP32C3 Super Mini | โœ… | โœ… | โœ… | โœ… | --- @@ -179,7 +179,10 @@ Ini adalah **tabel partisi kustom** untuk ESP32C3 Super Mini. Tata letak partisi ### Bagaimana cara memperbarui firmware secara over the air? -Gunakan **aplikasi ICRM** di Android. Aplikasi akan mengirimkan binary firmware yang telah dikompilasi melalui WebSocket. Croaster menerimanya dalam potongan-potongan, menuliskannya ke flash, dan restart otomatis setelah selesai. +Gunakan **aplikasi ICRM** di Android. Aplikasi mendukung OTA melalui WiFi (WebSocket) maupun BLE (khusus ESP32): + +- **OTA via WiFi:** Aplikasi mengirimkan binary firmware yang telah dikompilasi melalui WebSocket. Croaster menerimanya dalam potongan-potongan, menuliskannya ke flash, dan restart otomatis setelah selesai. +- **OTA via BLE:** Aplikasi mengirimkan binary firmware melalui BLE. `BleManager` meneruskan data ke `OtaHandler`, yang memprosesnya secara bertahap dengan pemeriksaan timeout. Progres dilaporkan kembali ke aplikasi sebagai JSON. --- @@ -191,7 +194,7 @@ Alasan paling umum adalah Anda menggunakan skema partisi **Huge APP**, yang tida ### Apakah OTA bekerja di ESP8266? -OTA via aplikasi ICRM didukung di ESP8266, selama Anda menggunakan skema partisi yang mencakup ruang OTA (skema NodeMCU default mendukung ini). +OTA via WiFi (WebSocket) menggunakan aplikasi ICRM didukung di ESP8266, selama Anda menggunakan skema partisi yang mencakup ruang OTA (skema NodeMCU default mendukung ini). OTA via BLE tidak tersedia di ESP8266. --- diff --git a/README.md b/README.md index d220216..6f098c0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Croaster** is a lightweight, open-source temperature monitoring system built on ESP-based microcontrollers. Designed for coffee roasting enthusiasts and professionals, it reads from two thermocouple sensors (Bean Temperature and Environment Temperature) and displays real-time data on a compact OLED screen. Croaster connects seamlessly to popular roasting software via WiFi (WebSocket) and BLE (ESP32 only), making it compatible with both desktop and mobile roasting apps. -**Current Firmware Version:** `0.46` +**Current Firmware Version:** `0.50` --- @@ -42,7 +42,7 @@ + [**Artisan Roaster Scope**](https://artisan-scope.org/) โ€” industry-standard roasting logger + [**ICRM app**](https://iiemb.github.io/#/icrm) โ€” companion mobile app (Android) * **BLE communication** (ESP32 only) for the [**ICRM app**](https://iiemb.github.io/#/icrm) -* **OTA (Over-The-Air) firmware updates** via WebSocket (ICRM app) +* **OTA (Over-The-Air) firmware updates** via WebSocket (WiFi) and **BLE** (ESP32 only) * **WiFiManager** captive portal for easy WiFi credential setup โ€” no re-flashing needed * Unique device naming based on chip ID (e.g. `Croaster-A1B2`) * **Dummy mode** for development and testing without physical sensors @@ -241,10 +241,11 @@ Use this method when Croaster is already connected to your home/office WiFi netw ## โฌ†๏ธ OTA (Over-The-Air) Updates -Croaster supports firmware updates without a USB cable, via the **ICRM app** over WebSocket (WiFi). +Croaster supports firmware updates without a USB cable, via the **ICRM app** over WebSocket (WiFi) or BLE (ESP32 only). -- OTA is handled by the `OtaHandler` class, which receives binary firmware data in chunks +- OTA is handled by the `OtaHandler` class, which receives binary firmware data in chunks and returns a JSON progress payload after each chunk - Progress is shown on the OLED display during the update +- BLE OTA includes timeout checks to handle stalled transfers - OTA requires the **custom partition scheme** (`custom32c3sm`) on ESP32C3 โ€” the `Huge APP` partition does **not** support OTA - After a successful OTA update, Croaster restarts automatically diff --git a/README_ID.md b/README_ID.md index 79a89d6..c8240ad 100644 --- a/README_ID.md +++ b/README_ID.md @@ -4,7 +4,7 @@ **Croaster** adalah sistem pemantau suhu ringan dan open-source yang dibangun di atas mikrokontroler berbasis ESP. Dirancang untuk para pecinta dan profesional sangrai kopi, sistem ini membaca dua sensor termokopel (Suhu Biji dan Suhu Lingkungan) dan menampilkan data secara real-time di layar OLED yang ringkas. Croaster terhubung mulus ke perangkat lunak sangrai populer melalui WiFi (WebSocket) dan BLE (khusus ESP32), sehingga kompatibel dengan aplikasi sangrai di desktop maupun ponsel. -**Versi Firmware Saat Ini:** `0.46` +**Versi Firmware Saat Ini:** `0.50` --- @@ -42,7 +42,7 @@ + [**Artisan Roaster Scope**](https://artisan-scope.org/) โ€” logger sangrai standar industri + [**Aplikasi ICRM**](https://iiemb.github.io/#/icrm) โ€” aplikasi pendamping mobile (Android) * **Komunikasi BLE** (khusus ESP32) untuk [**aplikasi ICRM**](https://iiemb.github.io/#/icrm) -* **Update firmware OTA (Over-The-Air)** via WebSocket (aplikasi ICRM) +* **Update firmware OTA (Over-The-Air)** via WebSocket (WiFi) dan **BLE** (khusus ESP32) * **Captive portal WiFiManager** untuk setup WiFi yang mudah โ€” tanpa perlu flash ulang * Penamaan perangkat unik berdasarkan chip ID (contoh: `Croaster-A1B2`) * **Mode dummy** untuk pengembangan dan pengujian tanpa sensor fisik @@ -241,10 +241,11 @@ Gunakan metode ini ketika Croaster sudah terhubung ke jaringan WiFi rumah/kantor ## โฌ†๏ธ Update OTA (Over-The-Air) -Croaster mendukung pembaruan firmware tanpa kabel USB, melalui **aplikasi ICRM** via WebSocket (WiFi). +Croaster mendukung pembaruan firmware tanpa kabel USB, melalui **aplikasi ICRM** via WebSocket (WiFi) atau BLE (khusus ESP32). -- OTA ditangani oleh kelas `OtaHandler`, yang menerima data firmware biner secara bertahap +- OTA ditangani oleh kelas `OtaHandler`, yang menerima data firmware biner secara bertahap dan mengembalikan payload JSON progres setelah setiap potongan - Kemajuan update ditampilkan di layar OLED selama proses berlangsung +- OTA via BLE dilengkapi pemeriksaan timeout untuk menangani transfer yang terhenti - OTA memerlukan **skema partisi kustom** (`custom32c3sm`) pada ESP32C3 โ€” partisi `Huge APP` **tidak** mendukung OTA - Setelah update OTA berhasil, Croaster restart otomatis From 5f6217129d28a10fc752ab265cabe077443c8886 Mon Sep 17 00:00:00 2001 From: IiemB Date: Sat, 30 May 2026 10:44:37 +0700 Subject: [PATCH 15/15] feat: enhance documentation and add visual aid for WebSocket connection setup; improve debug logging for BLE and WebSocket commands --- README.md | 45 ++++++++++++++++++++--------- README_ID.md | 45 ++++++++++++++++++++--------- images/Select-WebSocket-Device.png | Bin 0 -> 181284 bytes src/BleManager.cpp | 3 ++ src/WebSocketManager.cpp | 4 +-- 5 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 images/Select-WebSocket-Device.png diff --git a/README.md b/README.md index 6f098c0..3d9a8ae 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,31 @@ ## ๐Ÿ“‘ Table of Contents -- [Features](#-features) -- [Hardware Components](#-hardware-components) -- [Wiring Diagram](#-wiring-diagram) -- [Software Architecture](#-software-architecture) -- [Libraries & Dependencies](#-libraries--dependencies) -- [How to Build and Upload](#-how-to-build-and-upload) -- [WiFi Setup Guide](#-wifi-setup-guide) -- [Communication Overview](#-communication-overview) -- [How to Connect Croaster with Artisan](#-how-to-connect-croaster-with-artisan) -- [OTA (Over-The-Air) Updates](#-ota-over-the-air-updates) -- [Custom Commands](#-custom-commands) -- [License](#-license) -- [Contributing](#๏ธ-contributing) +- [โ˜• Croaster - Open Source Coffee Roaster Monitor](#-croaster---open-source-coffee-roaster-monitor) + - [๐Ÿ“‘ Table of Contents](#-table-of-contents) + - [๐Ÿš€ Features](#-features) + - [๐Ÿงฉ Hardware Components](#-hardware-components) + - [๐Ÿ”Œ Wiring Diagram](#-wiring-diagram) + - [๐Ÿ›  Software Architecture](#-software-architecture) + - [Data Flow](#data-flow) + - [๐Ÿ“ฆ Libraries \& Dependencies](#-libraries--dependencies) + - [๐Ÿ”ง How to Build and Upload](#-how-to-build-and-upload) + - [โœ… PlatformIO (recommended for ESP8266 \& ESP32C3)](#-platformio-recommended-for-esp8266--esp32c3) + - [โœ… Arduino IDE (alternative, required for Makergo ESP32C3 board)](#-arduino-ide-alternative-required-for-makergo-esp32c3-board) + - [๐Ÿ”— WiFi Setup Guide](#-wifi-setup-guide) + - [๐Ÿ“ก Communication Overview](#-communication-overview) + - [WebSocket (WiFi)](#websocket-wifi) + - [BLE (ESP32 only)](#ble-esp32-only) + - [๐Ÿ”Œ How to Connect Croaster with Artisan](#-how-to-connect-croaster-with-artisan) + - [๐Ÿ–ฅ๏ธ Option 1: Direct Connection (Croaster as Access Point)](#๏ธ-option-1-direct-connection-croaster-as-access-point) + - [๐ŸŒ Option 2: Same WiFi Network (Croaster joins your WiFi)](#-option-2-same-wifi-network-croaster-joins-your-wifi) + - [โฌ†๏ธ OTA (Over-The-Air) Updates](#๏ธ-ota-over-the-air-updates) + - [๐Ÿงช Custom Commands](#-custom-commands) + - [Built-in Commands](#built-in-commands) + - [Adding Custom Commands](#adding-custom-commands) + - [๐Ÿ“˜ License](#-license) + - [โค๏ธ Contributing](#๏ธ-contributing) + - [๐Ÿ”— Related Links](#-related-links) --- @@ -100,7 +112,7 @@ Croaster uses a clean, **modular C++ architecture** built with the Arduino frame | `CommandHandler` | `CommandHandler.h/.cpp` | JSON command parsing and dispatching (BLE & WebSocket) | | `WebSocketManager` | `WebSocketManager.h/.cpp` | WebSocket server, data broadcast, OTA trigger | | `BleManager` | `BleManager.h/.cpp` | BLE server, characteristic notify, command receive *(ESP32 only)* | -| `OtaHandler` | `OtaHandler.h/.cpp` | Binary OTA update handling over WebSocket | +| `OtaHandler` | `OtaHandler.h/.cpp` | Binary OTA update handling over WebSocket and BLE | | `WiFiManagerUtil` | `WiFiManagerUtil.h/.cpp` | WiFiManager captive portal setup and lifecycle | | `DeviceIdentity` | `DeviceIdentity.h/.cpp` | Chip ID, device name, IP address helpers | @@ -216,6 +228,11 @@ For a visual walkthrough, see: โžก๏ธ [How to Connect to WiFi - YouTube](https:/ You can connect your Croaster device to Artisan using either a direct WiFi connection or through your home/local WiFi network. +1. Open Artisan โ†’ **Config โ†’ Device** +2. Select **Meter โ†’ WebSocket** + + ![image](images/Select-WebSocket-Device.png) + ### ๐Ÿ–ฅ๏ธ Option 1: Direct Connection (Croaster as Access Point) Use this method when Croaster is **not** connected to any WiFi network, or when you want a direct peer-to-peer connection. diff --git a/README_ID.md b/README_ID.md index c8240ad..8b73242 100644 --- a/README_ID.md +++ b/README_ID.md @@ -10,19 +10,31 @@ ## ๐Ÿ“‘ Daftar Isi -- [Fitur](#-fitur) -- [Komponen Hardware](#-komponen-hardware) -- [Diagram Pengkabelan](#-diagram-pengkabelan) -- [Arsitektur Software](#-arsitektur-software) -- [Library & Dependensi](#-library--dependensi) -- [Cara Build dan Upload](#-cara-build-dan-upload) -- [Panduan Setup WiFi](#-panduan-setup-wifi) -- [Gambaran Komunikasi](#-gambaran-komunikasi) -- [Cara Menghubungkan Croaster dengan Artisan](#-cara-menghubungkan-croaster-dengan-artisan) -- [Update OTA (Over-The-Air)](#๏ธ-update-ota-over-the-air) -- [Perintah Kustom](#-perintah-kustom) -- [Lisensi](#-lisensi) -- [Kontribusi](#๏ธ-kontribusi) +- [โ˜• Croaster - Monitor Sangrai Kopi Open Source](#-croaster---monitor-sangrai-kopi-open-source) + - [๐Ÿ“‘ Daftar Isi](#-daftar-isi) + - [๐Ÿš€ Fitur](#-fitur) + - [๐Ÿงฉ Komponen Hardware](#-komponen-hardware) + - [๐Ÿ”Œ Diagram Pengkabelan](#-diagram-pengkabelan) + - [๐Ÿ›  Arsitektur Software](#-arsitektur-software) + - [Alur Data](#alur-data) + - [๐Ÿ“ฆ Library \& Dependensi](#-library--dependensi) + - [๐Ÿ”ง Cara Build dan Upload](#-cara-build-dan-upload) + - [โœ… PlatformIO (direkomendasikan untuk ESP8266 \& ESP32C3)](#-platformio-direkomendasikan-untuk-esp8266--esp32c3) + - [โœ… Arduino IDE (alternatif, diperlukan untuk board Makergo ESP32C3)](#-arduino-ide-alternatif-diperlukan-untuk-board-makergo-esp32c3) + - [๐Ÿ”— Panduan Setup WiFi](#-panduan-setup-wifi) + - [๐Ÿ“ก Gambaran Komunikasi](#-gambaran-komunikasi) + - [WebSocket (WiFi)](#websocket-wifi) + - [BLE (khusus ESP32)](#ble-khusus-esp32) + - [๐Ÿ”Œ Cara Menghubungkan Croaster dengan Artisan](#-cara-menghubungkan-croaster-dengan-artisan) + - [๐Ÿ–ฅ๏ธ Opsi 1: Koneksi Langsung (Croaster sebagai Access Point)](#๏ธ-opsi-1-koneksi-langsung-croaster-sebagai-access-point) + - [๐ŸŒ Opsi 2: Jaringan WiFi yang Sama (Croaster bergabung ke WiFi Anda)](#-opsi-2-jaringan-wifi-yang-sama-croaster-bergabung-ke-wifi-anda) + - [โฌ†๏ธ Update OTA (Over-The-Air)](#๏ธ-update-ota-over-the-air) + - [๐Ÿงช Perintah Kustom](#-perintah-kustom) + - [Perintah Bawaan](#perintah-bawaan) + - [Menambahkan Perintah Kustom](#menambahkan-perintah-kustom) + - [๐Ÿ“˜ Lisensi](#-lisensi) + - [โค๏ธ Kontribusi](#๏ธ-kontribusi) + - [๐Ÿ”— Tautan Terkait](#-tautan-terkait) --- @@ -100,7 +112,7 @@ Croaster menggunakan **arsitektur C++ modular** yang bersih, dibangun dengan fra | `CommandHandler` | `CommandHandler.h/.cpp` | Parsing dan dispatching perintah JSON (BLE & WebSocket) | | `WebSocketManager` | `WebSocketManager.h/.cpp` | Server WebSocket, broadcast data, trigger OTA | | `BleManager` | `BleManager.h/.cpp` | Server BLE, notify karakteristik, penerimaan perintah *(khusus ESP32)* | -| `OtaHandler` | `OtaHandler.h/.cpp` | Penanganan update OTA biner via WebSocket | +| `OtaHandler` | `OtaHandler.h/.cpp` | Penanganan update OTA biner via WebSocket dan BLE | | `WiFiManagerUtil` | `WiFiManagerUtil.h/.cpp` | Setup dan lifecycle captive portal WiFiManager | | `DeviceIdentity` | `DeviceIdentity.h/.cpp` | Helper chip ID, nama perangkat, alamat IP | @@ -216,6 +228,11 @@ Untuk panduan visual, lihat: โžก๏ธ [Cara Menghubungkan ke WiFi - YouTube](https Anda dapat menghubungkan Croaster ke Artisan menggunakan koneksi WiFi langsung atau melalui jaringan WiFi rumah/lokal Anda. +1. Buka Artisan โ†’ **Config โ†’ Device** +2. Pilih **Meter โ†’ WebSocket** + + ![image](images/Select-WebSocket-Device.png) + ### ๐Ÿ–ฅ๏ธ Opsi 1: Koneksi Langsung (Croaster sebagai Access Point) Gunakan metode ini ketika Croaster **tidak** terhubung ke jaringan WiFi manapun, atau ketika Anda menginginkan koneksi peer-to-peer langsung. diff --git a/images/Select-WebSocket-Device.png b/images/Select-WebSocket-Device.png new file mode 100644 index 0000000000000000000000000000000000000000..58a52648da74d6a84d8ee2c049df764065e977a0 GIT binary patch literal 181284 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

3_d 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 JbLq~*{{#Hx80-K5 literal 0 HcmV?d00001 diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 4d33b20..3e1477f 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -91,6 +91,9 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks } debugln("# [CMD-BLE] " + raw); + + if (!response.isEmpty()) + debugln("# [CMD-BLE-RESP] " + response); } } }; diff --git a/src/WebSocketManager.cpp b/src/WebSocketManager.cpp index b0ce690..1d1cd3e 100644 --- a/src/WebSocketManager.cpp +++ b/src/WebSocketManager.cpp @@ -26,11 +26,11 @@ void WebSocketManager::handleEvent(const String &cmd, uint8_t num) { if (!response.isEmpty()) - { server.sendTXT(num, response); - } debugln("# [CMD-SOCKET] " + cmd); + if (!response.isEmpty()) + debugln("# [CMD-SOCKET-RESP] " + response); } }